diff --git a/coderd/audit.go b/coderd/audit.go index 5002bb6960c58..ba845c8b048b0 100644 --- a/coderd/audit.go +++ b/coderd/audit.go @@ -219,12 +219,26 @@ func convertAuditLog(dblog database.GetAuditLogsOffsetRow) codersdk.AuditLog { } } +type WorkspaceResourceInfo struct { + WorkspaceName string +} + func auditLogDescription(alog database.GetAuditLogsOffsetRow) string { str := fmt.Sprintf("{user} %s %s", codersdk.AuditAction(alog.Action).FriendlyString(), codersdk.ResourceType(alog.ResourceType).FriendlyString(), ) + // Strings for build updates follow the below format: + // "{user} started workspace build for workspace {target}" + // where target is a workspace instead of the workspace build + if alog.ResourceType == database.ResourceTypeWorkspaceBuild { + workspaceBytes := []byte(alog.AdditionalFields) + var workspaceResourceInfo WorkspaceResourceInfo + _ = json.Unmarshal(workspaceBytes, &workspaceResourceInfo) + str += " for workspace " + workspaceResourceInfo.WorkspaceName + } + // We don't display the name for git ssh keys. It's fairly long and doesn't // make too much sense to display. if alog.ResourceType != database.ResourceTypeGitSshKey { @@ -288,6 +302,8 @@ func resourceTypeFromString(resourceTypeString string) string { return resourceTypeString case codersdk.ResourceTypeWorkspace: return resourceTypeString + case codersdk.ResourceTypeWorkspaceBuild: + return resourceTypeString case codersdk.ResourceTypeGitSSHKey: return resourceTypeString case codersdk.ResourceTypeAPIKey: diff --git a/coderd/audit/diff.go b/coderd/audit/diff.go index 42a67e92a8559..4f902a44778d4 100644 --- a/coderd/audit/diff.go +++ b/coderd/audit/diff.go @@ -15,6 +15,7 @@ type Auditable interface { database.TemplateVersion | database.User | database.Workspace | + database.WorkspaceBuild | database.GitSSHKey | database.Group } diff --git a/coderd/audit/request.go b/coderd/audit/request.go index f330b321cd1ec..efba7ebb4304b 100644 --- a/coderd/audit/request.go +++ b/coderd/audit/request.go @@ -20,8 +20,9 @@ type RequestParams struct { Audit Auditor Log slog.Logger - Request *http.Request - Action database.AuditAction + Request *http.Request + Action database.AuditAction + AdditionalFields json.RawMessage } type Request[T Auditable] struct { @@ -43,6 +44,9 @@ func ResourceTarget[T Auditable](tgt T) string { return typed.Username case database.Workspace: return typed.Name + case database.WorkspaceBuild: + // this isn't used + return string(typed.BuildNumber) case database.GitSSHKey: return typed.PublicKey case database.Group: @@ -64,6 +68,8 @@ func ResourceID[T Auditable](tgt T) uuid.UUID { return typed.ID case database.Workspace: return typed.ID + case database.WorkspaceBuild: + return typed.ID case database.GitSSHKey: return typed.UserID case database.Group: @@ -85,6 +91,8 @@ func ResourceType[T Auditable](tgt T) database.ResourceType { return database.ResourceTypeUser case database.Workspace: return database.ResourceTypeWorkspace + case database.WorkspaceBuild: + return database.ResourceTypeWorkspaceBuild case database.GitSSHKey: return database.ResourceTypeGitSshKey case database.Group: @@ -129,6 +137,10 @@ func InitRequest[T Auditable](w http.ResponseWriter, p *RequestParams) (*Request } } + if p.AdditionalFields == nil { + p.AdditionalFields = json.RawMessage("{}") + } + ip := parseIP(p.Request.RemoteAddr) err := p.Audit.Export(ctx, database.AuditLog{ ID: uuid.New(), @@ -143,7 +155,7 @@ func InitRequest[T Auditable](w http.ResponseWriter, p *RequestParams) (*Request Diff: diffRaw, StatusCode: int32(sw.Status), RequestID: httpmw.RequestID(p.Request), - AdditionalFields: json.RawMessage("{}"), + AdditionalFields: p.AdditionalFields, }) if err != nil { p.Log.Error(logCtx, "export audit log", slog.Error(err)) diff --git a/coderd/database/dump.sql b/coderd/database/dump.sql index f3710e91f037b..11892c418db18 100644 --- a/coderd/database/dump.sql +++ b/coderd/database/dump.sql @@ -14,7 +14,9 @@ CREATE TYPE app_sharing_level AS ENUM ( CREATE TYPE audit_action AS ENUM ( 'create', 'write', - 'delete' + 'delete', + 'start', + 'stop' ); CREATE TYPE build_reason AS ENUM ( @@ -88,7 +90,8 @@ CREATE TYPE resource_type AS ENUM ( 'workspace', 'git_ssh_key', 'api_key', - 'group' + 'group', + 'workspace_build' ); CREATE TYPE user_status AS ENUM ( diff --git a/coderd/database/migrations/000064_add_audit_enums.down.sql b/coderd/database/migrations/000064_add_audit_enums.down.sql new file mode 100644 index 0000000000000..d1d1637f4fa90 --- /dev/null +++ b/coderd/database/migrations/000064_add_audit_enums.down.sql @@ -0,0 +1,2 @@ +-- It's not possible to drop enum values from enum types, so the UP has "IF NOT +-- EXISTS". diff --git a/coderd/database/migrations/000064_add_audit_enums.up.sql b/coderd/database/migrations/000064_add_audit_enums.up.sql new file mode 100644 index 0000000000000..dc623e05e77e2 --- /dev/null +++ b/coderd/database/migrations/000064_add_audit_enums.up.sql @@ -0,0 +1,4 @@ +ALTER TYPE audit_action ADD VALUE IF NOT EXISTS 'start'; +ALTER TYPE audit_action ADD VALUE IF NOT EXISTS 'stop'; + +ALTER TYPE resource_type ADD VALUE IF NOT EXISTS 'workspace_build'; diff --git a/coderd/database/models.go b/coderd/database/models.go index dd0bce6064514..59347c14f2c1b 100644 --- a/coderd/database/models.go +++ b/coderd/database/models.go @@ -60,6 +60,8 @@ const ( AuditActionCreate AuditAction = "create" AuditActionWrite AuditAction = "write" AuditActionDelete AuditAction = "delete" + AuditActionStart AuditAction = "start" + AuditActionStop AuditAction = "stop" ) func (e *AuditAction) Scan(src interface{}) error { @@ -302,6 +304,7 @@ const ( ResourceTypeGitSshKey ResourceType = "git_ssh_key" ResourceTypeApiKey ResourceType = "api_key" ResourceTypeGroup ResourceType = "group" + ResourceTypeWorkspaceBuild ResourceType = "workspace_build" ) func (e *ResourceType) Scan(src interface{}) error { diff --git a/coderd/workspacebuilds.go b/coderd/workspacebuilds.go index dc89f576b5484..0e1f5713e58c1 100644 --- a/coderd/workspacebuilds.go +++ b/coderd/workspacebuilds.go @@ -278,28 +278,59 @@ func (api *API) postWorkspaceBuilds(rw http.ResponseWriter, r *http.Request) { return } - // we only want to create audit logs for delete builds right now + auditor := api.Auditor.Load() + + // if user deletes a workspace, audit the workspace if action == rbac.ActionDelete { - var ( - auditor = api.Auditor.Load() - aReq, commitAudit = audit.InitRequest[database.Workspace](rw, &audit.RequestParams{ - Audit: *auditor, - Log: api.Logger, - Request: r, - Action: database.AuditActionDelete, - }) - ) + aReq, commitAudit := audit.InitRequest[database.Workspace](rw, &audit.RequestParams{ + Audit: *auditor, + Log: api.Logger, + Request: r, + Action: database.AuditActionDelete, + }) defer commitAudit() aReq.Old = workspace } + latestBuild, latestBuildErr := api.Database.GetLatestWorkspaceBuildByWorkspaceID(ctx, workspace.ID) + + // if a user starts/stops a workspace, audit the workspace build + if action == rbac.ActionUpdate { + var auditAction database.AuditAction + if createBuild.Transition == codersdk.WorkspaceTransitionStart { + auditAction = database.AuditActionStart + } else if createBuild.Transition == codersdk.WorkspaceTransitionStop { + auditAction = database.AuditActionStop + } else { + auditAction = database.AuditActionWrite + } + + // 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, _ := json.Marshal(workspaceResourceInfo) + + aReq, commitAudit := audit.InitRequest[database.WorkspaceBuild](rw, &audit.RequestParams{ + Audit: *auditor, + Log: api.Logger, + Request: r, + Action: auditAction, + AdditionalFields: wriBytes, + }) + + defer commitAudit() + aReq.Old = latestBuild + } + if createBuild.TemplateVersionID == uuid.Nil { - latestBuild, err := api.Database.GetLatestWorkspaceBuildByWorkspaceID(ctx, workspace.ID) - if err != nil { + if latestBuildErr != nil { httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{ Message: "Internal error fetching the latest workspace build.", - Detail: err.Error(), + Detail: latestBuildErr.Error(), }) return } diff --git a/coderd/workspacebuilds_test.go b/coderd/workspacebuilds_test.go index 983063a1907bc..46b18a1d7180f 100644 --- a/coderd/workspacebuilds_test.go +++ b/coderd/workspacebuilds_test.go @@ -579,6 +579,6 @@ func TestWorkspaceBuildStatus(t *testing.T) { require.EqualValues(t, codersdk.WorkspaceStatusDeleted, workspace.LatestBuild.Status) // assert an audit log has been created for deletion - require.Len(t, auditor.AuditLogs, 5) - assert.Equal(t, database.AuditActionDelete, auditor.AuditLogs[4].Action) + require.Len(t, auditor.AuditLogs, 7) + assert.Equal(t, database.AuditActionDelete, auditor.AuditLogs[6].Action) } diff --git a/codersdk/audit.go b/codersdk/audit.go index 1452af0c0f6b9..0fc10f27f05a4 100644 --- a/codersdk/audit.go +++ b/codersdk/audit.go @@ -19,6 +19,7 @@ const ( ResourceTypeTemplateVersion ResourceType = "template_version" ResourceTypeUser ResourceType = "user" ResourceTypeWorkspace ResourceType = "workspace" + ResourceTypeWorkspaceBuild ResourceType = "workspace_build" ResourceTypeGitSSHKey ResourceType = "git_ssh_key" ResourceTypeAPIKey ResourceType = "api_key" ResourceTypeGroup ResourceType = "group" @@ -36,6 +37,8 @@ func (r ResourceType) FriendlyString() string { return "user" case ResourceTypeWorkspace: return "workspace" + case ResourceTypeWorkspaceBuild: + return "workspace build" case ResourceTypeGitSSHKey: return "git ssh key" case ResourceTypeAPIKey: @@ -53,6 +56,8 @@ const ( AuditActionCreate AuditAction = "create" AuditActionWrite AuditAction = "write" AuditActionDelete AuditAction = "delete" + AuditActionStart AuditAction = "start" + AuditActionStop AuditAction = "stop" ) func (a AuditAction) FriendlyString() string { @@ -63,6 +68,10 @@ func (a AuditAction) FriendlyString() string { return "updated" case AuditActionDelete: return "deleted" + case AuditActionStart: + return "started" + case AuditActionStop: + return "stopped" default: return "unknown" } diff --git a/enterprise/audit/table.go b/enterprise/audit/table.go index 193b16a31687f..fd50610b6005e 100644 --- a/enterprise/audit/table.go +++ b/enterprise/audit/table.go @@ -103,6 +103,21 @@ var AuditableResources = auditMap(map[any]map[string]Action{ "ttl": ActionTrack, "last_used_at": ActionIgnore, }, + // We don't show any diff for the WorkspaceBuild resource + &database.WorkspaceBuild{}: { + "id": ActionIgnore, + "created_at": ActionIgnore, + "updated_at": ActionIgnore, + "workspace_id": ActionIgnore, + "template_version_id": ActionIgnore, + "build_number": ActionIgnore, + "transition": ActionIgnore, + "initiator_id": ActionIgnore, + "provisioner_state": ActionIgnore, + "job_id": ActionIgnore, + "deadline": ActionIgnore, + "reason": ActionIgnore, + }, &database.Group{}: { "id": ActionTrack, "name": ActionTrack, diff --git a/site/src/api/typesGenerated.ts b/site/src/api/typesGenerated.ts index 8cf296785becc..052ba5178e8ec 100644 --- a/site/src/api/typesGenerated.ts +++ b/site/src/api/typesGenerated.ts @@ -842,7 +842,7 @@ export interface WorkspacesRequest extends Pagination { export type APIKeyScope = "all" | "application_connect" // From codersdk/audit.go -export type AuditAction = "create" | "delete" | "write" +export type AuditAction = "create" | "delete" | "start" | "stop" | "write" // From codersdk/workspacebuilds.go export type BuildReason = "autostart" | "autostop" | "initiator" @@ -902,6 +902,7 @@ export type ResourceType = | "template_version" | "user" | "workspace" + | "workspace_build" // From codersdk/sse.go export type ServerSentEventType = "data" | "error" | "ping" diff --git a/site/src/components/AuditLogRow/AuditLogRow.tsx b/site/src/components/AuditLogRow/AuditLogRow.tsx index 6d72362911983..478c01e05aa19 100644 --- a/site/src/components/AuditLogRow/AuditLogRow.tsx +++ b/site/src/components/AuditLogRow/AuditLogRow.tsx @@ -130,13 +130,11 @@ export const AuditLogRow: React.FC = ({ -
- {isDiffOpen ? : } -
+ {shouldDisplayDiff ? ( +
{isDiffOpen ? : }
+ ) : ( +
+ )} {shouldDisplayDiff && ( @@ -190,8 +188,8 @@ const useStyles = makeStyles((theme) => ({ color: theme.palette.text.secondary, whiteSpace: "nowrap", }, - - disabledDropdownIcon: { - opacity: 0.5, + // offset the absence of the arrow icon on diff-less logs + columnWithoutDiff: { + marginLeft: "24px", }, }))