From 148219fe2b546418d5254630bb9f3f2b7badf7d0 Mon Sep 17 00:00:00 2001 From: Jon Ayers Date: Wed, 4 Oct 2023 19:13:57 +0000 Subject: [PATCH 1/3] chore: add auditing to workspace dormancy - Adds an audit log for workspaces automatically transitioned to the dormant state. - Imposes a mininum of 1 minute on cleanup-related fields. This is to prevent accidental API misuse from resulting in catastrophe. --- cli/server.go | 2 +- coderd/autobuild/lifecycle_executor.go | 39 ++++++- coderd/coderdtest/coderdtest.go | 7 ++ coderd/templates.go | 20 ++-- coderd/workspaces.go | 17 ++- coderd/workspaces_test.go | 7 +- codersdk/audit.go | 3 + enterprise/coderd/templates_test.go | 152 ++++++++++++++++++------- enterprise/coderd/workspaces_test.go | 27 ++++- 9 files changed, 212 insertions(+), 62 deletions(-) diff --git a/cli/server.go b/cli/server.go index ea70451a941ce..f9ef1aaa65c8c 100644 --- a/cli/server.go +++ b/cli/server.go @@ -938,7 +938,7 @@ func (r *RootCmd) Server(newAPI func(context.Context, *coderd.Options) (*coderd. autobuildTicker := time.NewTicker(vals.AutobuildPollInterval.Value()) defer autobuildTicker.Stop() autobuildExecutor := autobuild.NewExecutor( - ctx, options.Database, options.Pubsub, coderAPI.TemplateScheduleStore, logger, autobuildTicker.C) + ctx, options.Database, options.Pubsub, coderAPI.TemplateScheduleStore, &coderAPI.Auditor, logger, autobuildTicker.C) autobuildExecutor.Run() hangDetectorTicker := time.NewTicker(vals.JobHangDetectorInterval.Value()) diff --git a/coderd/autobuild/lifecycle_executor.go b/coderd/autobuild/lifecycle_executor.go index 65e3e90afee49..ae5ef8b1ea444 100644 --- a/coderd/autobuild/lifecycle_executor.go +++ b/coderd/autobuild/lifecycle_executor.go @@ -3,6 +3,9 @@ package autobuild import ( "context" "database/sql" + "encoding/json" + "net/http" + "strconv" "sync" "sync/atomic" "time" @@ -12,6 +15,7 @@ import ( "golang.org/x/xerrors" "cdr.dev/slog" + "github.com/coder/coder/v2/coderd/audit" "github.com/coder/coder/v2/coderd/database" "github.com/coder/coder/v2/coderd/database/dbauthz" "github.com/coder/coder/v2/coderd/database/dbtime" @@ -29,6 +33,7 @@ type Executor struct { db database.Store ps pubsub.Pubsub templateScheduleStore *atomic.Pointer[schedule.TemplateScheduleStore] + auditor *atomic.Pointer[audit.Auditor] log slog.Logger tick <-chan time.Time statsCh chan<- Stats @@ -42,7 +47,7 @@ type Stats struct { } // New returns a new wsactions executor. -func NewExecutor(ctx context.Context, db database.Store, ps pubsub.Pubsub, tss *atomic.Pointer[schedule.TemplateScheduleStore], log slog.Logger, tick <-chan time.Time) *Executor { +func NewExecutor(ctx context.Context, db database.Store, ps pubsub.Pubsub, tss *atomic.Pointer[schedule.TemplateScheduleStore], auditor *atomic.Pointer[audit.Auditor], log slog.Logger, tick <-chan time.Time) *Executor { le := &Executor{ //nolint:gocritic // Autostart has a limited set of permissions. ctx: dbauthz.AsAutostart(ctx), @@ -51,6 +56,7 @@ func NewExecutor(ctx context.Context, db database.Store, ps pubsub.Pubsub, tss * templateScheduleStore: tss, tick: tick, log: log.Named("autobuild"), + auditor: auditor, } return le } @@ -166,13 +172,14 @@ func (e *Executor) runOnce(t time.Time) Stats { return nil } + var build *database.WorkspaceBuild if nextTransition != "" { builder := wsbuilder.New(ws, nextTransition). SetLastWorkspaceBuildInTx(&latestBuild). SetLastWorkspaceBuildJobInTx(&latestJob). Reason(reason) - _, job, err = builder.Build(e.ctx, tx, nil) + build, job, err = builder.Build(e.ctx, tx, nil) if err != nil { log.Error(e.ctx, "unable to transition workspace", slog.F("transition", nextTransition), @@ -185,6 +192,7 @@ func (e *Executor) runOnce(t time.Time) Stats { // Transition the workspace to dormant if it has breached the template's // threshold for inactivity. if reason == database.BuildReasonAutolock { + wsOld := ws ws, err = tx.UpdateWorkspaceDormantDeletingAt(e.ctx, database.UpdateWorkspaceDormantDeletingAtParams{ ID: ws.ID, DormantAt: sql.NullTime{ @@ -192,6 +200,33 @@ func (e *Executor) runOnce(t time.Time) Stats { Valid: true, }, }) + + fields := audit.AdditionalFields{ + WorkspaceName: ws.Name, + BuildReason: reason, + } + if build != nil { + fields.BuildNumber = strconv.FormatInt(int64(latestBuild.BuildNumber), 10) + } + + raw, err := json.Marshal(fields) + if err != nil { + e.log.Error(e.ctx, "marshal resource info for successful job", slog.Error(err)) + } + + audit.WorkspaceBuildAudit(e.ctx, &audit.BuildAuditParams[database.Workspace]{ + Audit: *e.auditor.Load(), + Log: e.log, + UserID: job.InitiatorID, + OrganizationID: ws.OrganizationID, + JobID: job.ID, + Action: database.AuditActionWrite, + Old: wsOld, + New: ws, + Status: http.StatusOK, + AdditionalFields: raw, + }) + if err != nil { log.Error(e.ctx, "unable to transition workspace to dormant", slog.F("transition", nextTransition), diff --git a/coderd/coderdtest/coderdtest.go b/coderd/coderdtest/coderdtest.go index e7067aa80d8d8..a091bc11148d0 100644 --- a/coderd/coderdtest/coderdtest.go +++ b/coderd/coderdtest/coderdtest.go @@ -262,12 +262,19 @@ func NewOptions(t testing.TB, options *Options) (func(http.Handler), context.Can } templateScheduleStore.Store(&options.TemplateScheduleStore) + var auditor atomic.Pointer[audit.Auditor] + if options.Auditor == nil { + options.Auditor = audit.NewNop() + } + auditor.Store(&options.Auditor) + ctx, cancelFunc := context.WithCancel(context.Background()) lifecycleExecutor := autobuild.NewExecutor( ctx, options.Database, options.Pubsub, &templateScheduleStore, + &auditor, slogtest.Make(t, nil).Named("autobuild.executor").Leveled(slog.LevelDebug), options.AutobuildTicker, ).WithStatsChannel(options.AutobuildStats) diff --git a/coderd/templates.go b/coderd/templates.go index 9adc77bfe37e5..221fb28b52e65 100644 --- a/coderd/templates.go +++ b/coderd/templates.go @@ -537,17 +537,19 @@ func (api *API) patchTemplateMeta(rw http.ResponseWriter, r *http.Request) { if req.AutostopRequirement.Weeks > schedule.MaxTemplateAutostopRequirementWeeks { validErrs = append(validErrs, codersdk.ValidationError{Field: "autostop_requirement.weeks", Detail: fmt.Sprintf("Must be less than %d.", schedule.MaxTemplateAutostopRequirementWeeks)}) } - if req.FailureTTLMillis < 0 { - validErrs = append(validErrs, codersdk.ValidationError{Field: "failure_ttl_ms", Detail: "Must be a positive integer."}) - } - if req.TimeTilDormantMillis < 0 { - validErrs = append(validErrs, codersdk.ValidationError{Field: "inactivity_ttl_ms", Detail: "Must be a positive integer."}) + + // The minimum valid value for a dormant TTL is 1 minute. This is + // to ensure an uninformed user does not send an unintentionally + // small number resulting in potentially catastrophic consequences. + const minTTL = 1000 * 60 + if req.FailureTTLMillis < 0 || (req.FailureTTLMillis > 0 && req.FailureTTLMillis < minTTL) { + validErrs = append(validErrs, codersdk.ValidationError{Field: "failure_ttl_ms", Detail: "Value must be at least one minute."}) } - if req.TimeTilDormantMillis < 0 { - validErrs = append(validErrs, codersdk.ValidationError{Field: "inactivity_ttl_ms", Detail: "Must be a positive integer."}) + if req.TimeTilDormantMillis < 0 || (req.TimeTilDormantMillis > 0 && req.TimeTilDormantMillis < minTTL) { + validErrs = append(validErrs, codersdk.ValidationError{Field: "time_til_dormant_ms", Detail: "Value must be at least one minute."}) } - if req.TimeTilDormantAutoDeleteMillis < 0 { - validErrs = append(validErrs, codersdk.ValidationError{Field: "locked_ttl_ms", Detail: "Must be a positive integer."}) + if req.TimeTilDormantAutoDeleteMillis < 0 || (req.TimeTilDormantAutoDeleteMillis > 0 && req.TimeTilDormantAutoDeleteMillis < minTTL) { + validErrs = append(validErrs, codersdk.ValidationError{Field: "time_til_dormant_autodelete_ms", Detail: "Value must be at least one minute."}) } if len(validErrs) > 0 { diff --git a/coderd/workspaces.go b/coderd/workspaces.go index 6aa08b991705d..a9b5f337c68fc 100644 --- a/coderd/workspaces.go +++ b/coderd/workspaces.go @@ -816,8 +816,20 @@ func (api *API) putWorkspaceTTL(rw http.ResponseWriter, r *http.Request) { // @Success 200 {object} codersdk.Workspace // @Router /workspaces/{workspace}/dormant [put] func (api *API) putWorkspaceDormant(rw http.ResponseWriter, r *http.Request) { - ctx := r.Context() - workspace := httpmw.WorkspaceParam(r) + var ( + ctx = r.Context() + workspace = httpmw.WorkspaceParam(r) + oldWorkspace = workspace + auditor = api.Auditor.Load() + aReq, commitAudit = audit.InitRequest[database.Workspace](rw, &audit.RequestParams{ + Audit: *auditor, + Log: api.Logger, + Request: r, + Action: database.AuditActionWrite, + }) + ) + aReq.Old = oldWorkspace + defer commitAudit() var req codersdk.UpdateWorkspaceDormancy if !httpapi.Read(ctx, rw, r, &req) { @@ -865,6 +877,7 @@ func (api *API) putWorkspaceDormant(rw http.ResponseWriter, r *http.Request) { return } + aReq.New = workspace httpapi.Write(ctx, rw, http.StatusOK, convertWorkspace( workspace, data.builds[0], diff --git a/coderd/workspaces_test.go b/coderd/workspaces_test.go index c299e32cedc01..f6b7c1546c04c 100644 --- a/coderd/workspaces_test.go +++ b/coderd/workspaces_test.go @@ -2824,7 +2824,10 @@ func TestWorkspaceDormant(t *testing.T) { t.Run("OK", func(t *testing.T) { t.Parallel() var ( - client = coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true}) + auditRecorder = audit.NewMock() + client = coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true, + Auditor: auditRecorder, + }) user = coderdtest.CreateFirstUser(t, client) version = coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil) _ = coderdtest.AwaitTemplateVersionJobCompleted(t, client, version.ID) @@ -2841,10 +2844,12 @@ func TestWorkspaceDormant(t *testing.T) { defer cancel() lastUsedAt := workspace.LastUsedAt + auditRecorder.ResetLogs() err := client.UpdateWorkspaceDormancy(ctx, workspace.ID, codersdk.UpdateWorkspaceDormancy{ Dormant: true, }) require.NoError(t, err) + require.Len(t, auditRecorder.AuditLogs(), 1) workspace = coderdtest.MustWorkspace(t, client, workspace.ID) require.NoError(t, err, "fetch provisioned workspace") diff --git a/codersdk/audit.go b/codersdk/audit.go index 5ceae81a21c42..3e10c3c06f894 100644 --- a/codersdk/audit.go +++ b/codersdk/audit.go @@ -72,6 +72,7 @@ const ( AuditActionLogin AuditAction = "login" AuditActionLogout AuditAction = "logout" AuditActionRegister AuditAction = "register" + AuditActionDormant AuditAction = "dormant" ) func (a AuditAction) Friendly() string { @@ -92,6 +93,8 @@ func (a AuditAction) Friendly() string { return "logged out" case AuditActionRegister: return "registered" + case AuditActionDormant: + return "has gone dormant" default: return "unknown" } diff --git a/enterprise/coderd/templates_test.go b/enterprise/coderd/templates_test.go index fe0d7cac2ec76..b499450d5315e 100644 --- a/enterprise/coderd/templates_test.go +++ b/enterprise/coderd/templates_test.go @@ -185,55 +185,123 @@ func TestTemplates(t *testing.T) { }) t.Run("CleanupTTLs", func(t *testing.T) { - t.Parallel() + t.Run("OK", func(t *testing.T) { + t.Parallel() - ctx := testutil.Context(t, testutil.WaitMedium) - client, user := coderdenttest.New(t, &coderdenttest.Options{ - Options: &coderdtest.Options{ - IncludeProvisionerDaemon: true, - }, - LicenseOptions: &coderdenttest.LicenseOptions{ - Features: license.Features{ - codersdk.FeatureAdvancedTemplateScheduling: 1, + ctx := testutil.Context(t, testutil.WaitMedium) + client, user := coderdenttest.New(t, &coderdenttest.Options{ + Options: &coderdtest.Options{ + IncludeProvisionerDaemon: true, }, - }, + LicenseOptions: &coderdenttest.LicenseOptions{ + Features: license.Features{ + codersdk.FeatureAdvancedTemplateScheduling: 1, + }, + }, + }) + + version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil) + coderdtest.AwaitTemplateVersionJobCompleted(t, client, version.ID) + template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID) + require.EqualValues(t, 0, template.TimeTilDormantMillis) + require.EqualValues(t, 0, template.FailureTTLMillis) + require.EqualValues(t, 0, template.TimeTilDormantAutoDeleteMillis) + + var ( + failureTTL time.Duration = 1 * time.Minute + inactivityTTL time.Duration = 2 * time.Minute + dormantTTL time.Duration = 3 * time.Minute + ) + + updated, err := client.UpdateTemplateMeta(ctx, template.ID, codersdk.UpdateTemplateMeta{ + Name: template.Name, + DisplayName: template.DisplayName, + Description: template.Description, + Icon: template.Icon, + AllowUserCancelWorkspaceJobs: template.AllowUserCancelWorkspaceJobs, + TimeTilDormantMillis: inactivityTTL.Milliseconds(), + FailureTTLMillis: failureTTL.Milliseconds(), + TimeTilDormantAutoDeleteMillis: dormantTTL.Milliseconds(), + }) + require.NoError(t, err) + require.Equal(t, failureTTL.Milliseconds(), updated.FailureTTLMillis) + require.Equal(t, inactivityTTL.Milliseconds(), updated.TimeTilDormantMillis) + require.Equal(t, dormantTTL.Milliseconds(), updated.TimeTilDormantAutoDeleteMillis) + + // Validate fetching the template returns the same values as updating + // the template. + template, err = client.Template(ctx, template.ID) + require.NoError(t, err) + require.Equal(t, failureTTL.Milliseconds(), updated.FailureTTLMillis) + require.Equal(t, inactivityTTL.Milliseconds(), updated.TimeTilDormantMillis) + require.Equal(t, dormantTTL.Milliseconds(), updated.TimeTilDormantAutoDeleteMillis) }) - version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil) - coderdtest.AwaitTemplateVersionJobCompleted(t, client, version.ID) - template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID) - require.EqualValues(t, 0, template.TimeTilDormantMillis) - require.EqualValues(t, 0, template.FailureTTLMillis) - require.EqualValues(t, 0, template.TimeTilDormantAutoDeleteMillis) + t.Run("BadRequest", func(t *testing.T) { + t.Parallel() - var ( - failureTTL int64 = 1 - inactivityTTL int64 = 2 - dormantTTL int64 = 3 - ) + ctx := testutil.Context(t, testutil.WaitMedium) + client, user := coderdenttest.New(t, &coderdenttest.Options{ + Options: &coderdtest.Options{ + IncludeProvisionerDaemon: true, + }, + LicenseOptions: &coderdenttest.LicenseOptions{ + Features: license.Features{ + codersdk.FeatureAdvancedTemplateScheduling: 1, + }, + }, + }) - updated, err := client.UpdateTemplateMeta(ctx, template.ID, codersdk.UpdateTemplateMeta{ - Name: template.Name, - DisplayName: template.DisplayName, - Description: template.Description, - Icon: template.Icon, - AllowUserCancelWorkspaceJobs: template.AllowUserCancelWorkspaceJobs, - TimeTilDormantMillis: inactivityTTL, - FailureTTLMillis: failureTTL, - TimeTilDormantAutoDeleteMillis: dormantTTL, - }) - require.NoError(t, err) - require.Equal(t, failureTTL, updated.FailureTTLMillis) - require.Equal(t, inactivityTTL, updated.TimeTilDormantMillis) - require.Equal(t, dormantTTL, updated.TimeTilDormantAutoDeleteMillis) + version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil) + coderdtest.AwaitTemplateVersionJobCompleted(t, client, version.ID) + template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID) - // Validate fetching the template returns the same values as updating - // the template. - template, err = client.Template(ctx, template.ID) - require.NoError(t, err) - require.Equal(t, failureTTL, updated.FailureTTLMillis) - require.Equal(t, inactivityTTL, updated.TimeTilDormantMillis) - require.Equal(t, dormantTTL, updated.TimeTilDormantAutoDeleteMillis) + type testcase struct { + Name string + TimeTilDormantMS int64 + FailureTTLMS int64 + DormantAutoDeleteMS int64 + } + + cases := []testcase{ + { + Name: "NegativeValue", + TimeTilDormantMS: -1, + FailureTTLMS: -2, + DormantAutoDeleteMS: -3, + }, + { + Name: "ValueTooSmall", + TimeTilDormantMS: 1, + FailureTTLMS: 999, + DormantAutoDeleteMS: 500, + }, + } + + for _, c := range cases { + c := c + + t.Run(c.Name, func(t *testing.T) { + t.Parallel() + + _, err := client.UpdateTemplateMeta(ctx, template.ID, codersdk.UpdateTemplateMeta{ + Name: template.Name, + DisplayName: template.DisplayName, + Description: template.Description, + Icon: template.Icon, + AllowUserCancelWorkspaceJobs: template.AllowUserCancelWorkspaceJobs, + TimeTilDormantMillis: c.TimeTilDormantMS, + FailureTTLMillis: c.FailureTTLMS, + TimeTilDormantAutoDeleteMillis: c.DormantAutoDeleteMS, + }) + require.Error(t, err) + cerr, ok := codersdk.AsError(err) + require.True(t, ok) + require.Len(t, cerr.Validations, 3) + require.Equal(t, "Value must be at least one minute.", cerr.Validations[0].Detail) + }) + } + }) }) t.Run("UpdateTimeTilDormantAutoDelete", func(t *testing.T) { diff --git a/enterprise/coderd/workspaces_test.go b/enterprise/coderd/workspaces_test.go index 43f2353b86280..e010d5edb0821 100644 --- a/enterprise/coderd/workspaces_test.go +++ b/enterprise/coderd/workspaces_test.go @@ -2,6 +2,7 @@ package coderd_test import ( "context" + "encoding/json" "fmt" "net/http" "sync/atomic" @@ -12,6 +13,7 @@ import ( "cdr.dev/slog/sloggers/slogtest" + "github.com/coder/coder/v2/coderd/audit" "github.com/coder/coder/v2/coderd/autobuild" "github.com/coder/coder/v2/coderd/coderdtest" "github.com/coder/coder/v2/coderd/database" @@ -237,10 +239,11 @@ func TestWorkspaceAutobuild(t *testing.T) { t.Parallel() var ( - ctx = testutil.Context(t, testutil.WaitMedium) - ticker = make(chan time.Time) - statCh = make(chan autobuild.Stats) - inactiveTTL = time.Minute + ctx = testutil.Context(t, testutil.WaitMedium) + ticker = make(chan time.Time) + statCh = make(chan autobuild.Stats) + inactiveTTL = time.Minute + auditRecorder = audit.NewMock() ) client, user := coderdenttest.New(t, &coderdenttest.Options{ @@ -249,6 +252,7 @@ func TestWorkspaceAutobuild(t *testing.T) { IncludeProvisionerDaemon: true, AutobuildStats: statCh, TemplateScheduleStore: schedule.NewEnterpriseTemplateScheduleStore(agplUserQuietHoursScheduleStore()), + Auditor: auditRecorder, }, LicenseOptions: &coderdenttest.LicenseOptions{ Features: license.Features{codersdk.FeatureAdvancedTemplateScheduling: 1}, @@ -268,6 +272,9 @@ func TestWorkspaceAutobuild(t *testing.T) { ws := coderdtest.CreateWorkspace(t, client, user.OrganizationID, template.ID) build := coderdtest.AwaitWorkspaceBuildJobCompleted(t, client, ws.LatestBuild.ID) require.Equal(t, codersdk.WorkspaceStatusRunning, build.Status) + + // Reset the audit log so we can verify a log is generated. + auditRecorder.ResetLogs() // Simulate being inactive. ticker <- ws.LastUsedAt.Add(inactiveTTL * 2) stats := <-statCh @@ -276,13 +283,23 @@ func TestWorkspaceAutobuild(t *testing.T) { // failure TTL. require.Len(t, stats.Transitions, 1) require.Equal(t, stats.Transitions[ws.ID], database.WorkspaceTransitionStop) + require.Len(t, auditRecorder.AuditLogs(), 1) + + auditLog := auditRecorder.AuditLogs()[0] + require.Equal(t, auditLog.Action, database.AuditActionWrite) + + var fields audit.AdditionalFields + err := json.Unmarshal(auditLog.AdditionalFields, &fields) + require.NoError(t, err) + require.Equal(t, ws.Name, fields.WorkspaceName) + require.Equal(t, database.BuildReasonAutolock, fields.BuildReason) // The workspace should be dormant. ws = coderdtest.MustWorkspace(t, client, ws.ID) require.NotNil(t, ws.DormantAt) lastUsedAt := ws.LastUsedAt - err := client.UpdateWorkspaceDormancy(ctx, ws.ID, codersdk.UpdateWorkspaceDormancy{Dormant: false}) + err = client.UpdateWorkspaceDormancy(ctx, ws.ID, codersdk.UpdateWorkspaceDormancy{Dormant: false}) require.NoError(t, err) // Assert that we updated our last_used_at so that we don't immediately From 6e70d6b06f6234d81d490a9e1b59fec7a2582a30 Mon Sep 17 00:00:00 2001 From: Jon Ayers Date: Thu, 5 Oct 2023 17:30:52 +0000 Subject: [PATCH 2/3] rm dead code --- coderd/autobuild/lifecycle_executor.go | 74 +++++++++++++++++--------- codersdk/audit.go | 3 -- enterprise/coderd/templates_test.go | 6 +-- 3 files changed, 53 insertions(+), 30 deletions(-) diff --git a/coderd/autobuild/lifecycle_executor.go b/coderd/autobuild/lifecycle_executor.go index ae5ef8b1ea444..e2ab0d7dc2cf0 100644 --- a/coderd/autobuild/lifecycle_executor.go +++ b/coderd/autobuild/lifecycle_executor.go @@ -201,30 +201,13 @@ func (e *Executor) runOnce(t time.Time) Stats { }, }) - fields := audit.AdditionalFields{ - WorkspaceName: ws.Name, - BuildReason: reason, - } - if build != nil { - fields.BuildNumber = strconv.FormatInt(int64(latestBuild.BuildNumber), 10) - } - - raw, err := json.Marshal(fields) - if err != nil { - e.log.Error(e.ctx, "marshal resource info for successful job", slog.Error(err)) - } - - audit.WorkspaceBuildAudit(e.ctx, &audit.BuildAuditParams[database.Workspace]{ - Audit: *e.auditor.Load(), - Log: e.log, - UserID: job.InitiatorID, - OrganizationID: ws.OrganizationID, - JobID: job.ID, - Action: database.AuditActionWrite, - Old: wsOld, - New: ws, - Status: http.StatusOK, - AdditionalFields: raw, + auditBuild(e.ctx, e.log, *e.auditor.Load(), auditParams{ + Build: build, + Job: latestJob, + Reason: reason, + Old: wsOld, + New: ws, + Success: err == nil, }) if err != nil { @@ -419,3 +402,46 @@ func isEligibleForFailedStop(build database.WorkspaceBuild, job database.Provisi job.CompletedAt.Valid && currentTick.Sub(job.CompletedAt.Time) > templateSchedule.FailureTTL } + +type auditParams struct { + Build *database.WorkspaceBuild + Job database.ProvisionerJob + Reason database.BuildReason + Old database.Workspace + New database.Workspace + Success bool +} + +func auditBuild(ctx context.Context, log slog.Logger, auditor audit.Auditor, params auditParams) { + fields := audit.AdditionalFields{ + WorkspaceName: params.New.Name, + BuildReason: params.Reason, + } + + if params.Build != nil { + fields.BuildNumber = strconv.FormatInt(int64(params.Build.BuildNumber), 10) + } + + raw, err := json.Marshal(fields) + if err != nil { + log.Error(ctx, "marshal resource info for successful job", slog.Error(err)) + } + + status := http.StatusInternalServerError + if params.Success { + status = http.StatusOK + } + + audit.WorkspaceBuildAudit(ctx, &audit.BuildAuditParams[database.Workspace]{ + Audit: auditor, + Log: log, + UserID: params.Job.InitiatorID, + OrganizationID: params.New.OrganizationID, + JobID: params.Job.ID, + Action: database.AuditActionWrite, + Old: params.Old, + New: params.New, + Status: status, + AdditionalFields: raw, + }) +} diff --git a/codersdk/audit.go b/codersdk/audit.go index 3e10c3c06f894..5ceae81a21c42 100644 --- a/codersdk/audit.go +++ b/codersdk/audit.go @@ -72,7 +72,6 @@ const ( AuditActionLogin AuditAction = "login" AuditActionLogout AuditAction = "logout" AuditActionRegister AuditAction = "register" - AuditActionDormant AuditAction = "dormant" ) func (a AuditAction) Friendly() string { @@ -93,8 +92,6 @@ func (a AuditAction) Friendly() string { return "logged out" case AuditActionRegister: return "registered" - case AuditActionDormant: - return "has gone dormant" default: return "unknown" } diff --git a/enterprise/coderd/templates_test.go b/enterprise/coderd/templates_test.go index b499450d5315e..fcf5343d55358 100644 --- a/enterprise/coderd/templates_test.go +++ b/enterprise/coderd/templates_test.go @@ -208,9 +208,9 @@ func TestTemplates(t *testing.T) { require.EqualValues(t, 0, template.TimeTilDormantAutoDeleteMillis) var ( - failureTTL time.Duration = 1 * time.Minute - inactivityTTL time.Duration = 2 * time.Minute - dormantTTL time.Duration = 3 * time.Minute + failureTTL = 1 * time.Minute + inactivityTTL = 2 * time.Minute + dormantTTL = 3 * time.Minute ) updated, err := client.UpdateTemplateMeta(ctx, template.ID, codersdk.UpdateTemplateMeta{ From ba26c94f57e89cb089dd9ec1d3969ca7a7740240 Mon Sep 17 00:00:00 2001 From: Jon Ayers Date: Thu, 5 Oct 2023 17:42:20 +0000 Subject: [PATCH 3/3] fmt --- coderd/workspaces_test.go | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/coderd/workspaces_test.go b/coderd/workspaces_test.go index f6b7c1546c04c..5cc4d5b9f0eec 100644 --- a/coderd/workspaces_test.go +++ b/coderd/workspaces_test.go @@ -2825,8 +2825,9 @@ func TestWorkspaceDormant(t *testing.T) { t.Parallel() var ( auditRecorder = audit.NewMock() - client = coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true, - Auditor: auditRecorder, + client = coderdtest.New(t, &coderdtest.Options{ + IncludeProvisionerDaemon: true, + Auditor: auditRecorder, }) user = coderdtest.CreateFirstUser(t, client) version = coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil)