chore: add auditing to workspace dormancy (#10070)

- 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.
This commit is contained in:
Jon Ayers
2023-10-05 13:41:07 -05:00
committed by GitHub
parent 888b97fd86
commit 91265678ad
8 changed files with 236 additions and 62 deletions

View File

@ -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,16 @@ func (e *Executor) runOnce(t time.Time) Stats {
Valid: true,
},
})
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 {
log.Error(e.ctx, "unable to transition workspace to dormant",
slog.F("transition", nextTransition),
@ -384,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,
})
}