Skip to content

Commit df3a6ce

Browse files
committed
auditing failed builds
1 parent 773fc73 commit df3a6ce

File tree

10 files changed

+164
-9
lines changed

10 files changed

+164
-9
lines changed

coderd/audit.go

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package coderd
22

33
import (
4+
"database/sql"
45
"encoding/json"
56
"fmt"
67
"net"
@@ -155,7 +156,7 @@ func (api *API) generateFakeAuditLog(rw http.ResponseWriter, r *http.Request) {
155156
Time: params.Time,
156157
UserID: user.ID,
157158
Ip: ipNet,
158-
UserAgent: r.UserAgent(),
159+
UserAgent: sql.NullString{String: r.UserAgent(), Valid: true},
159160
ResourceType: database.ResourceType(params.ResourceType),
160161
ResourceID: params.ResourceID,
161162
ResourceTarget: user.Username,
@@ -189,6 +190,11 @@ func convertAuditLog(dblog database.GetAuditLogsOffsetRow) codersdk.AuditLog {
189190
_ = json.Unmarshal(dblog.Diff, &diff)
190191

191192
var user *codersdk.User
193+
var agent string
194+
195+
if dblog.UserAgent.Valid {
196+
agent = dblog.UserAgent.String
197+
}
192198
if dblog.UserUsername.Valid {
193199
user = &codersdk.User{
194200
ID: dblog.UserID,
@@ -212,7 +218,7 @@ func convertAuditLog(dblog database.GetAuditLogsOffsetRow) codersdk.AuditLog {
212218
Time: dblog.Time,
213219
OrganizationID: dblog.OrganizationID,
214220
IP: ip,
215-
UserAgent: dblog.UserAgent,
221+
UserAgent: agent,
216222
ResourceType: codersdk.ResourceType(dblog.ResourceType),
217223
ResourceID: dblog.ResourceID,
218224
ResourceTarget: dblog.ResourceTarget,

coderd/audit/request.go

Lines changed: 50 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package audit
22

33
import (
44
"context"
5+
"database/sql"
56
"encoding/json"
67
"fmt"
78
"net"
@@ -32,6 +33,20 @@ type Request[T Auditable] struct {
3233
New T
3334
}
3435

36+
type BuildAuditParams[T Auditable] struct {
37+
Audit Auditor
38+
Log slog.Logger
39+
40+
UserID uuid.UUID
41+
JobID uuid.UUID
42+
Status int
43+
Action database.AuditAction
44+
AdditionalFields json.RawMessage
45+
46+
New T
47+
Old T
48+
}
49+
3550
func ResourceTarget[T Auditable](tgt T) string {
3651
switch typed := any(tgt).(type) {
3752
case database.Organization:
@@ -147,7 +162,7 @@ func InitRequest[T Auditable](w http.ResponseWriter, p *RequestParams) (*Request
147162
Time: database.Now(),
148163
UserID: httpmw.APIKey(p.Request).UserID,
149164
Ip: ip,
150-
UserAgent: p.Request.UserAgent(),
165+
UserAgent: sql.NullString{String: p.Request.UserAgent(), Valid: true},
151166
ResourceType: either(req.Old, req.New, ResourceType[T]),
152167
ResourceID: either(req.Old, req.New, ResourceID[T]),
153168
ResourceTarget: either(req.Old, req.New, ResourceTarget[T]),
@@ -164,6 +179,40 @@ func InitRequest[T Auditable](w http.ResponseWriter, p *RequestParams) (*Request
164179
}
165180
}
166181

182+
// BuildAudit creates an audit log for a workspace build.
183+
// The audit log is committed upon invocation.
184+
func BuildAudit[T Auditable](ctx context.Context, p *BuildAuditParams[T]) {
185+
// As the audit request has not been initiated directly by a user, we omit
186+
// certain user details.
187+
ip := parseIP("")
188+
// We do not show diffs for build audit logs
189+
var diffRaw = []byte("{}")
190+
191+
if p.AdditionalFields == nil {
192+
p.AdditionalFields = json.RawMessage("{}")
193+
}
194+
195+
err := p.Audit.Export(ctx, database.AuditLog{
196+
ID: uuid.New(),
197+
Time: database.Now(),
198+
UserID: p.UserID,
199+
Ip: ip,
200+
UserAgent: sql.NullString{},
201+
ResourceType: either(p.Old, p.New, ResourceType[T]),
202+
ResourceID: either(p.Old, p.New, ResourceID[T]),
203+
ResourceTarget: either(p.Old, p.New, ResourceTarget[T]),
204+
Action: p.Action,
205+
Diff: diffRaw,
206+
StatusCode: int32(p.Status),
207+
RequestID: p.JobID,
208+
AdditionalFields: p.AdditionalFields,
209+
})
210+
if err != nil {
211+
p.Log.Error(ctx, "export audit log", slog.Error(err))
212+
return
213+
}
214+
}
215+
167216
func either[T Auditable, R any](old, new T, fn func(T) R) R {
168217
if ResourceID(new) != uuid.Nil {
169218
return fn(new)

coderd/database/dump.sql

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
ALTER TABLE audit_logs ALTER COLUMN ip SET NOT NULL;
2+
ALTER TABLE audit_logs ALTER COLUMN user_agent SET NOT NULL;
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
ALTER TABLE audit_logs ALTER COLUMN ip DROP NOT NULL;
2+
ALTER TABLE audit_logs ALTER COLUMN user_agent DROP NOT NULL;

coderd/database/models.go

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

coderd/database/queries.sql.go

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

coderd/provisionerdaemons.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,7 @@ func (api *API) ListenProvisionerDaemon(ctx context.Context, acquireJobDebounce
8787
Logger: api.Logger.Named(fmt.Sprintf("provisionerd-%s", daemon.Name)),
8888
AcquireJobDebounce: acquireJobDebounce,
8989
QuotaCommitter: &api.QuotaCommitter,
90+
Auditor: &api.Auditor,
9091
})
9192
if err != nil {
9293
return nil, err

coderd/provisionerdserver/provisionerdserver.go

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import (
1919

2020
"cdr.dev/slog"
2121

22+
"github.com/coder/coder/coderd/audit"
2223
"github.com/coder/coder/coderd/database"
2324
"github.com/coder/coder/coderd/parameter"
2425
"github.com/coder/coder/coderd/telemetry"
@@ -43,6 +44,7 @@ type Server struct {
4344
Pubsub database.Pubsub
4445
Telemetry telemetry.Reporter
4546
QuotaCommitter *atomic.Pointer[proto.QuotaCommitter]
47+
Auditor *atomic.Pointer[audit.Auditor]
4648

4749
AcquireJobDebounce time.Duration
4850
}
@@ -492,6 +494,51 @@ func (server *Server) FailJob(ctx context.Context, failJob *proto.FailedJob) (*p
492494
ProvisionerJobs: []telemetry.ProvisionerJob{telemetry.ConvertProvisionerJob(job)},
493495
})
494496

497+
// if job.Type == database.ProvisionerJobTypeWorkspaceBuild {
498+
// auditor := server.Auditor.Load()
499+
// build, err := server.Database.GetWorkspaceBuildByJobID(ctx, job.ID)
500+
// var auditAction database.AuditAction
501+
// if build.Transition == database.WorkspaceTransitionStart {
502+
// auditAction = database.AuditActionStart
503+
// } else if build.Transition == database.WorkspaceTransitionStop {
504+
// auditAction = database.AuditActionStop
505+
// } else if build.Transition == database.WorkspaceTransitionDelete {
506+
// auditAction = database.AuditActionDelete
507+
// } else {
508+
// auditAction = database.AuditActionWrite
509+
// }
510+
// if err != nil {
511+
// server.Logger.Error(ctx, "failed to create audit log")
512+
// } else {
513+
// workspace, err := server.Database.GetWorkspaceByID(ctx, build.WorkspaceID)
514+
// if err != nil {
515+
// server.Logger.Error(ctx, "failed to create audit log")
516+
// } else {
517+
// // We pass the workspace name to the Auditor so that it
518+
// // can form a friendly string for the user.
519+
// workspaceResourceInfo := map[string]string{
520+
// "workspaceName": workspace.Name,
521+
// }
522+
//
523+
// wriBytes, err := json.Marshal(workspaceResourceInfo)
524+
// if err != nil {
525+
// server.Logger.Error(ctx, "could not marshal workspace name", slog.Error(err))
526+
// }
527+
// audit.BuildAudit(ctx, &audit.BuildAuditParams[database.WorkspaceBuild]{
528+
// Audit: *auditor,
529+
// Log: server.Logger,
530+
// UserID: job.InitiatorID,
531+
// JobID: job.ID,
532+
// Action: auditAction,
533+
// New: build,
534+
// Status: 500,
535+
// AdditionalFields: wriBytes,
536+
// })
537+
// }
538+
//
539+
// }
540+
// }
541+
495542
switch jobType := failJob.Type.(type) {
496543
case *proto.FailedJob_WorkspaceBuild_:
497544
if jobType.WorkspaceBuild.State == nil {
@@ -518,6 +565,51 @@ func (server *Server) FailJob(ctx context.Context, failJob *proto.FailedJob) (*p
518565
case *proto.FailedJob_TemplateImport_:
519566
}
520567

568+
if job.Type == database.ProvisionerJobTypeWorkspaceBuild {
569+
auditor := server.Auditor.Load()
570+
build, getBuildErr := server.Database.GetWorkspaceBuildByJobID(ctx, job.ID)
571+
if getBuildErr != nil {
572+
server.Logger.Error(ctx, "failed to create audit log - get build err", slog.Error(err))
573+
} else {
574+
var auditAction database.AuditAction
575+
if build.Transition == database.WorkspaceTransitionStart {
576+
auditAction = database.AuditActionStart
577+
} else if build.Transition == database.WorkspaceTransitionStop {
578+
auditAction = database.AuditActionStop
579+
} else if build.Transition == database.WorkspaceTransitionDelete {
580+
auditAction = database.AuditActionDelete
581+
} else {
582+
auditAction = database.AuditActionWrite
583+
}
584+
workspace, getWorkspaceErr := server.Database.GetWorkspaceByID(ctx, build.WorkspaceID)
585+
if getWorkspaceErr != nil {
586+
server.Logger.Error(ctx, "failed to create audit log - get workspace err", slog.Error(err))
587+
} else {
588+
// We pass the workspace name to the Auditor so that it
589+
// can form a friendly string for the user.
590+
workspaceResourceInfo := map[string]string{
591+
"workspaceName": workspace.Name,
592+
}
593+
594+
wriBytes, err := json.Marshal(workspaceResourceInfo)
595+
if err != nil {
596+
server.Logger.Error(ctx, "could not marshal workspace name", slog.Error(err))
597+
}
598+
599+
audit.BuildAudit(ctx, &audit.BuildAuditParams[database.WorkspaceBuild]{
600+
Audit: *auditor,
601+
Log: server.Logger,
602+
UserID: job.InitiatorID,
603+
JobID: job.ID,
604+
Action: auditAction,
605+
New: build,
606+
Status: 500,
607+
AdditionalFields: wriBytes,
608+
})
609+
}
610+
}
611+
}
612+
521613
data, err := json.Marshal(ProvisionerJobLogsNotifyMessage{EndOfLogs: true})
522614
if err != nil {
523615
return nil, xerrors.Errorf("marshal job log: %w", err)

site/src/components/AuditLogRow/AuditLogRow.tsx

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,10 @@ export const readableActionMessage = (auditLog: AuditLog): string => {
2020
let target = auditLog.resource_target.trim()
2121

2222
// audit logs with a resource_type of workspace build use workspace name as a target
23-
if (auditLog.resource_type === "workspace_build") {
23+
if (
24+
auditLog.resource_type === "workspace_build" &&
25+
auditLog.additional_fields.workspaceName
26+
) {
2427
target = auditLog.additional_fields.workspaceName.trim()
2528
}
2629

0 commit comments

Comments
 (0)