diff --git a/coderd/audit.go b/coderd/audit.go index 2228af28c43e4..c645e11e7b4e9 100644 --- a/coderd/audit.go +++ b/coderd/audit.go @@ -160,6 +160,11 @@ func (api *API) convertAuditLogs(ctx context.Context, dblogs []database.GetAudit return alogs } +type AdditionalFields struct { + WorkspaceName string + BuildNumber string +} + func (api *API) convertAuditLog(ctx context.Context, dblog database.GetAuditLogsOffsetRow) codersdk.AuditLog { ip, _ := netip.AddrFromSlice(dblog.Ip.IPNet.IP) @@ -185,12 +190,29 @@ func (api *API) convertAuditLog(ctx context.Context, dblog database.GetAuditLogs } } - isDeleted := api.auditLogIsResourceDeleted(ctx, dblog) - var resourceLink string + var ( + additionalFieldsBytes = []byte(dblog.AdditionalFields) + additionalFields AdditionalFields + err = json.Unmarshal(additionalFieldsBytes, &additionalFields) + ) + if err != nil { + api.Logger.Error(ctx, "unmarshal additional fields", slog.Error(err)) + resourceInfo := map[string]string{ + "workspaceName": "unknown", + "buildNumber": "unknown", + } + dblog.AdditionalFields, err = json.Marshal(resourceInfo) + api.Logger.Error(ctx, "marshal additional fields", slog.Error(err)) + } + + var ( + isDeleted = api.auditLogIsResourceDeleted(ctx, dblog) + resourceLink string + ) if isDeleted { resourceLink = "" } else { - resourceLink = api.auditLogResourceLink(ctx, dblog) + resourceLink = auditLogResourceLink(dblog, additionalFields) } return codersdk.AuditLog{ @@ -209,23 +231,28 @@ func (api *API) convertAuditLog(ctx context.Context, dblog database.GetAuditLogs StatusCode: dblog.StatusCode, AdditionalFields: dblog.AdditionalFields, User: user, - Description: auditLogDescription(dblog), + Description: auditLogDescription(dblog, additionalFields), ResourceLink: resourceLink, IsDeleted: isDeleted, } } -func auditLogDescription(alog database.GetAuditLogsOffsetRow) string { +func auditLogDescription(alog database.GetAuditLogsOffsetRow, additionalFields AdditionalFields) string { str := fmt.Sprintf("{user} %s", codersdk.AuditAction(alog.Action).FriendlyString(), ) // Strings for starting/stopping workspace builds follow the below format: - // "{user} started build for workspace {target}" + // "{user} started build #{build_number} for workspace {target}" // where target is a workspace instead of a workspace build // passed in on the FE via AuditLog.AdditionalFields rather than derived in request.go:35 if alog.ResourceType == database.ResourceTypeWorkspaceBuild && alog.Action != database.AuditActionDelete { - str += " build for" + if len(additionalFields.BuildNumber) == 0 { + str += " build for" + } else { + str += fmt.Sprintf(" build #%s for", + additionalFields.BuildNumber) + } } // We don't display the name (target) for git ssh keys. It's fairly long and doesn't @@ -295,12 +322,7 @@ func (api *API) auditLogIsResourceDeleted(ctx context.Context, alog database.Get } } -type AdditionalFields struct { - WorkspaceName string - BuildNumber string -} - -func (api *API) auditLogResourceLink(ctx context.Context, alog database.GetAuditLogsOffsetRow) string { +func auditLogResourceLink(alog database.GetAuditLogsOffsetRow, additionalFields AdditionalFields) string { switch alog.ResourceType { case database.ResourceTypeTemplate: return fmt.Sprintf("/templates/%s", @@ -312,11 +334,8 @@ func (api *API) auditLogResourceLink(ctx context.Context, alog database.GetAudit return fmt.Sprintf("/@%s/%s", alog.UserUsername.String, alog.ResourceTarget) case database.ResourceTypeWorkspaceBuild: - additionalFieldsBytes := []byte(alog.AdditionalFields) - var additionalFields AdditionalFields - err := json.Unmarshal(additionalFieldsBytes, &additionalFields) - if err != nil { - api.Logger.Error(ctx, "unmarshal workspace name", slog.Error(err)) + if len(additionalFields.WorkspaceName) == 0 || len(additionalFields.BuildNumber) == 0 { + return "" } return fmt.Sprintf("/@%s/%s/builds/%s", alog.UserUsername.String, additionalFields.WorkspaceName, additionalFields.BuildNumber) diff --git a/coderd/provisionerdserver/provisionerdserver.go b/coderd/provisionerdserver/provisionerdserver.go index 0ee94d2f96ab7..c58e965f6d1a2 100644 --- a/coderd/provisionerdserver/provisionerdserver.go +++ b/coderd/provisionerdserver/provisionerdserver.go @@ -531,23 +531,23 @@ func (server *Server) FailJob(ctx context.Context, failJob *proto.FailedJob) (*p auditor := server.Auditor.Load() build, getBuildErr := server.Database.GetWorkspaceBuildByJobID(ctx, job.ID) if getBuildErr != nil { - server.Logger.Error(ctx, "failed to create audit log - get build err", slog.Error(err)) + server.Logger.Error(ctx, "audit log - get build", slog.Error(err)) } else { auditAction := auditActionFromTransition(build.Transition) workspace, getWorkspaceErr := server.Database.GetWorkspaceByID(ctx, build.WorkspaceID) if getWorkspaceErr != nil { - server.Logger.Error(ctx, "failed to create audit log - get workspace err", slog.Error(err)) + server.Logger.Error(ctx, "audit log - get workspace", slog.Error(err)) } else { // We pass the below information to the Auditor so that it // can form a friendly string for the user to view in the UI. - workspaceResourceInfo := map[string]string{ + buildResourceInfo := map[string]string{ "workspaceName": workspace.Name, "buildNumber": strconv.FormatInt(int64(build.BuildNumber), 10), } - wriBytes, err := json.Marshal(workspaceResourceInfo) + wriBytes, err := json.Marshal(buildResourceInfo) if err != nil { - server.Logger.Error(ctx, "could not marshal workspace name", slog.Error(err)) + server.Logger.Error(ctx, "marshal workspace resource info for failed job", slog.Error(err)) } audit.BuildAudit(ctx, &audit.BuildAuditParams[database.WorkspaceBuild]{ @@ -756,14 +756,14 @@ func (server *Server) CompleteJob(ctx context.Context, completed *proto.Complete // We pass the below information to the Auditor so that it // can form a friendly string for the user to view in the UI. - workspaceResourceInfo := map[string]string{ + buildResourceInfo := map[string]string{ "workspaceName": workspace.Name, "buildNumber": strconv.FormatInt(int64(workspaceBuild.BuildNumber), 10), } - wriBytes, err := json.Marshal(workspaceResourceInfo) + wriBytes, err := json.Marshal(buildResourceInfo) if err != nil { - server.Logger.Error(ctx, "marshal resource info", slog.Error(err)) + server.Logger.Error(ctx, "marshal resource info for successful job", slog.Error(err)) } audit.BuildAudit(ctx, &audit.BuildAuditParams[database.WorkspaceBuild]{ diff --git a/docs/admin/audit-logs.md b/docs/admin/audit-logs.md index bad6cd56cada2..32dfea8848c89 100644 --- a/docs/admin/audit-logs.md +++ b/docs/admin/audit-logs.md @@ -11,7 +11,7 @@ We track **create, update and delete** events for the following resources: - Template - TemplateVersion - Workspace -- Workspace start/stop +- WorkspaceBuild - User - Group diff --git a/site/src/components/AuditLogRow/AuditLogDescription.tsx b/site/src/components/AuditLogRow/AuditLogDescription.tsx index 240f40acea552..c559d3114c024 100644 --- a/site/src/components/AuditLogRow/AuditLogDescription.tsx +++ b/site/src/components/AuditLogRow/AuditLogDescription.tsx @@ -14,10 +14,7 @@ export const AuditLogDescription: FC<{ auditLog: AuditLog }> = ({ let target = auditLog.resource_target.trim() // audit logs with a resource_type of workspace build use workspace name as a target - if ( - auditLog.resource_type === "workspace_build" && - auditLog.additional_fields.workspaceName - ) { + if (auditLog.resource_type === "workspace_build") { target = auditLog.additional_fields.workspaceName.trim() }