Audit build outcomes/kira pilot (#5143)

* auditing failed builds

* logging workspace build successes

* remove duplicate workspace build entry

* fixed workspacebuilds_test

* PR feedback

* lint and migrations

* fix nil auditors

* workspace_build test

* fixed workspaces_teest

Co-authored-by: Colin Adler <colin1adler@gmail.com>
This commit is contained in:
Kira Pilot
2022-11-22 13:22:56 -05:00
committed by GitHub
parent 1f20cab110
commit 6786ca2854
18 changed files with 184 additions and 85 deletions

View File

@ -6,6 +6,7 @@ import (
"encoding/json"
"errors"
"fmt"
"net/http"
"net/url"
"reflect"
"sync"
@ -21,6 +22,7 @@ import (
"cdr.dev/slog"
"github.com/coder/coder/coderd/audit"
"github.com/coder/coder/coderd/database"
"github.com/coder/coder/coderd/parameter"
"github.com/coder/coder/coderd/telemetry"
@ -46,6 +48,7 @@ type Server struct {
Pubsub database.Pubsub
Telemetry telemetry.Reporter
QuotaCommitter *atomic.Pointer[proto.QuotaCommitter]
Auditor *atomic.Pointer[audit.Auditor]
AcquireJobDebounce time.Duration
}
@ -522,6 +525,43 @@ func (server *Server) FailJob(ctx context.Context, failJob *proto.FailedJob) (*p
case *proto.FailedJob_TemplateImport_:
}
// if failed job is a workspace build, audit the outcome
if job.Type == database.ProvisionerJobTypeWorkspaceBuild {
auditor := server.Auditor.Load()
build, getBuildErr := server.Database.GetWorkspaceBuildByJobID(ctx, job.ID)
if getBuildErr != nil {
server.Logger.Error(ctx, "failed to create audit log - get build err", slog.Error(err))
} else {
auditAction := auditActionFromTransition(build.Transition)
workspace, getWorkspaceErr := server.Database.GetWorkspaceByID(ctx, build.WorkspaceID)
if getWorkspaceErr != nil {
server.Logger.Error(ctx, "failed to create audit log - get workspace err", slog.Error(err))
} else {
// We pass the workspace name to the Auditor so that it
// can form a friendly string for the user.
workspaceResourceInfo := map[string]string{
"workspaceName": workspace.Name,
}
wriBytes, err := json.Marshal(workspaceResourceInfo)
if err != nil {
server.Logger.Error(ctx, "could not marshal workspace name", slog.Error(err))
}
audit.BuildAudit(ctx, &audit.BuildAuditParams[database.WorkspaceBuild]{
Audit: *auditor,
Log: server.Logger,
UserID: job.InitiatorID,
JobID: job.ID,
Action: auditAction,
New: build,
Status: http.StatusInternalServerError,
AdditionalFields: wriBytes,
})
}
}
}
data, err := json.Marshal(ProvisionerJobLogsNotifyMessage{EndOfLogs: true})
if err != nil {
return nil, xerrors.Errorf("marshal job log: %w", err)
@ -600,11 +640,14 @@ func (server *Server) CompleteJob(ctx context.Context, completed *proto.Complete
return nil, xerrors.Errorf("get workspace build: %w", err)
}
var workspace database.Workspace
var getWorkspaceError error
err = server.Database.InTx(func(db database.Store) error {
now := database.Now()
var workspaceDeadline time.Time
workspace, err := db.GetWorkspaceByID(ctx, workspaceBuild.WorkspaceID)
if err == nil {
workspace, getWorkspaceError = db.GetWorkspaceByID(ctx, workspaceBuild.WorkspaceID)
if getWorkspaceError == nil {
if workspace.Ttl.Valid {
workspaceDeadline = now.Add(time.Duration(workspace.Ttl.Int64))
}
@ -704,6 +747,34 @@ func (server *Server) CompleteJob(ctx context.Context, completed *proto.Complete
return nil, xerrors.Errorf("complete job: %w", err)
}
// audit the outcome of the workspace build
if getWorkspaceError == nil {
auditor := server.Auditor.Load()
auditAction := auditActionFromTransition(workspaceBuild.Transition)
// We pass the workspace name to the Auditor so that it
// can form a friendly string for the user.
workspaceResourceInfo := map[string]string{
"workspaceName": workspace.Name,
}
wriBytes, err := json.Marshal(workspaceResourceInfo)
if err != nil {
server.Logger.Error(ctx, "marshal resource info", slog.Error(err))
}
audit.BuildAudit(ctx, &audit.BuildAuditParams[database.WorkspaceBuild]{
Audit: *auditor,
Log: server.Logger,
UserID: job.InitiatorID,
JobID: job.ID,
Action: auditAction,
New: workspaceBuild,
Status: http.StatusOK,
AdditionalFields: wriBytes,
})
}
err = server.Pubsub.Publish(codersdk.WorkspaceNotifyChannel(workspaceBuild.WorkspaceID), []byte{})
if err != nil {
return nil, xerrors.Errorf("update workspace: %w", err)
@ -1015,6 +1086,19 @@ func convertWorkspaceTransition(transition database.WorkspaceTransition) (sdkpro
}
}
func auditActionFromTransition(transition database.WorkspaceTransition) database.AuditAction {
switch transition {
case database.WorkspaceTransitionStart:
return database.AuditActionStart
case database.WorkspaceTransitionStop:
return database.AuditActionStop
case database.WorkspaceTransitionDelete:
return database.AuditActionDelete
default:
return database.AuditActionWrite
}
}
// WorkspaceProvisionJob is the payload for the "workspace_provision" job type.
type WorkspaceProvisionJob struct {
WorkspaceBuildID uuid.UUID `json:"workspace_build_id"`

View File

@ -5,6 +5,7 @@ import (
"database/sql"
"encoding/json"
"net/url"
"sync/atomic"
"testing"
"time"
@ -12,6 +13,7 @@ import (
"github.com/stretchr/testify/require"
"cdr.dev/slog/sloggers/slogtest"
"github.com/coder/coder/coderd/audit"
"github.com/coder/coder/coderd/database"
"github.com/coder/coder/coderd/database/databasefake"
"github.com/coder/coder/coderd/provisionerdserver"
@ -21,6 +23,13 @@ import (
sdkproto "github.com/coder/coder/provisionersdk/proto"
)
func mockAuditor() *atomic.Pointer[audit.Auditor] {
ptr := &atomic.Pointer[audit.Auditor]{}
mock := audit.Auditor(audit.NewMock())
ptr.Store(&mock)
return ptr
}
func TestAcquireJob(t *testing.T) {
t.Parallel()
t.Run("Debounce", func(t *testing.T) {
@ -36,6 +45,7 @@ func TestAcquireJob(t *testing.T) {
Pubsub: pubsub,
Telemetry: telemetry.NewNoop(),
AcquireJobDebounce: time.Hour,
Auditor: mockAuditor(),
}
job, err := srv.AcquireJob(context.Background(), nil)
require.NoError(t, err)
@ -799,5 +809,6 @@ func setup(t *testing.T) *provisionerdserver.Server {
Database: db,
Pubsub: pubsub,
Telemetry: telemetry.NewNoop(),
Auditor: mockAuditor(),
}
}