mirror of
https://github.com/coder/coder.git
synced 2025-07-03 16:13:58 +00:00
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:
@ -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,
|
||||
})
|
||||
}
|
||||
|
Reference in New Issue
Block a user