diff --git a/coderd/audit.go b/coderd/audit.go index ba845c8b048b0..1a8f0e79b8b8e 100644 --- a/coderd/audit.go +++ b/coderd/audit.go @@ -219,24 +219,18 @@ 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 + // Strings for workspace_builds follow the below format: + // "{user} started workspace build for {target}" + // where target is a workspace instead of the workspace build, + // passed in on the FE via AuditLog.AdditionalFields rather than derived in request.go:35 if alog.ResourceType == database.ResourceTypeWorkspaceBuild { - workspaceBytes := []byte(alog.AdditionalFields) - var workspaceResourceInfo WorkspaceResourceInfo - _ = json.Unmarshal(workspaceBytes, &workspaceResourceInfo) - str += " for workspace " + workspaceResourceInfo.WorkspaceName + str += " for" } // We don't display the name for git ssh keys. It's fairly long and doesn't diff --git a/coderd/audit/request.go b/coderd/audit/request.go index efba7ebb4304b..ef15611fa8de2 100644 --- a/coderd/audit/request.go +++ b/coderd/audit/request.go @@ -46,7 +46,7 @@ func ResourceTarget[T Auditable](tgt T) string { return typed.Name case database.WorkspaceBuild: // this isn't used - return string(typed.BuildNumber) + return "" case database.GitSSHKey: return typed.PublicKey case database.Group: diff --git a/coderd/workspacebuilds_test.go b/coderd/workspacebuilds_test.go index 46b18a1d7180f..3d152932bf866 100644 --- a/coderd/workspacebuilds_test.go +++ b/coderd/workspacebuilds_test.go @@ -536,13 +536,20 @@ func TestWorkspaceBuildStatus(t *testing.T) { ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong) defer cancel() auditor := audit.NewMock() + numLogs := len(auditor.AuditLogs) client, closeDaemon, api := coderdtest.NewWithAPI(t, &coderdtest.Options{IncludeProvisionerDaemon: true, Auditor: auditor}) user := coderdtest.CreateFirstUser(t, client) + numLogs++ // add an audit log for user version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil) + numLogs++ // add an audit log for template version + coderdtest.AwaitTemplateVersionJob(t, client, version.ID) closeDaemon.Close() template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID) + numLogs++ // add an audit log for template creation + workspace := coderdtest.CreateWorkspace(t, client, user.OrganizationID, template.ID) + numLogs++ // add an audit log for workspace creation // initial returned state is "pending" require.EqualValues(t, codersdk.WorkspaceStatusPending, workspace.LatestBuild.Status) @@ -561,11 +568,22 @@ func TestWorkspaceBuildStatus(t *testing.T) { require.NoError(t, err) require.EqualValues(t, codersdk.WorkspaceStatusStopped, workspace.LatestBuild.Status) + // assert an audit log has been created for workspace stopping + numLogs++ // add an audit log for workspace_build stop + require.Len(t, auditor.AuditLogs, numLogs) + require.Equal(t, database.AuditActionStop, auditor.AuditLogs[numLogs-1].Action) + _ = closeDaemon.Close() // after successful cancel is "canceled" build = coderdtest.CreateWorkspaceBuild(t, client, workspace, database.WorkspaceTransitionStart) err = client.CancelWorkspaceBuild(ctx, build.ID) require.NoError(t, err) + + numLogs++ // add an audit log for workspace build start + // assert an audit log has been created workspace starting + require.Len(t, auditor.AuditLogs, numLogs) + require.Equal(t, database.AuditActionStart, auditor.AuditLogs[numLogs-1].Action) + workspace, err = client.Workspace(ctx, workspace.ID) require.NoError(t, err) require.EqualValues(t, codersdk.WorkspaceStatusCanceled, workspace.LatestBuild.Status) @@ -577,8 +595,9 @@ func TestWorkspaceBuildStatus(t *testing.T) { workspace, err = client.DeletedWorkspace(ctx, workspace.ID) require.NoError(t, err) require.EqualValues(t, codersdk.WorkspaceStatusDeleted, workspace.LatestBuild.Status) + numLogs++ // add an audit log for workspace build deletion // assert an audit log has been created for deletion - require.Len(t, auditor.AuditLogs, 7) - assert.Equal(t, database.AuditActionDelete, auditor.AuditLogs[6].Action) + require.Len(t, auditor.AuditLogs, numLogs) + require.Equal(t, database.AuditActionDelete, auditor.AuditLogs[numLogs-1].Action) } diff --git a/docs/admin/audit-logs.md b/docs/admin/audit-logs.md index d7179c7ee590c..7ecd5142cb660 100644 --- a/docs/admin/audit-logs.md +++ b/docs/admin/audit-logs.md @@ -11,6 +11,7 @@ We track **create, update and delete** events for the following resources: - Template - TemplateVersion - Workspace +- Workspace start/stop - User - Group diff --git a/scripts/apitypings/main.go b/scripts/apitypings/main.go index 20ff1ba1f8250..5d27695fc6d43 100644 --- a/scripts/apitypings/main.go +++ b/scripts/apitypings/main.go @@ -688,6 +688,8 @@ func (g *Generator) typescriptType(ty types.Type) (TypescriptType, error) { return TypescriptType{ValueType: "string", Optional: true}, nil case "github.com/google/uuid.UUID": return TypescriptType{ValueType: "string"}, nil + case "encoding/json.RawMessage": + return TypescriptType{ValueType: "Record"}, nil } // Then see if the type is defined elsewhere. If it is, we can just diff --git a/site/src/api/typesGenerated.ts b/site/src/api/typesGenerated.ts index c006c13928eef..7a3bf62d1c6af 100644 --- a/site/src/api/typesGenerated.ts +++ b/site/src/api/typesGenerated.ts @@ -65,8 +65,7 @@ export interface AuditLog { readonly action: AuditAction readonly diff: AuditDiff readonly status_code: number - // This is likely an enum in an external package ("encoding/json.RawMessage") - readonly additional_fields: string + readonly additional_fields: Record readonly description: string readonly user?: User } diff --git a/site/src/components/AuditLogRow/AuditLogRow.stories.tsx b/site/src/components/AuditLogRow/AuditLogRow.stories.tsx index b552b868ac240..9b1239ebe7b82 100644 --- a/site/src/components/AuditLogRow/AuditLogRow.stories.tsx +++ b/site/src/components/AuditLogRow/AuditLogRow.stories.tsx @@ -5,7 +5,11 @@ import TableContainer from "@material-ui/core/TableContainer" import TableHead from "@material-ui/core/TableHead" import TableRow from "@material-ui/core/TableRow" import { ComponentMeta, Story } from "@storybook/react" -import { MockAuditLog, MockAuditLog2 } from "testHelpers/entities" +import { + MockAuditLog, + MockAuditLog2, + MockAuditLogWithWorkspaceBuild, +} from "testHelpers/entities" import { AuditLogRow, AuditLogRowProps } from "./AuditLogRow" export default { @@ -38,3 +42,8 @@ WithDiff.args = { auditLog: MockAuditLog2, defaultIsDiffOpen: true, } + +export const WithWorkspaceBuild = Template.bind({}) +WithWorkspaceBuild.args = { + auditLog: MockAuditLogWithWorkspaceBuild, +} diff --git a/site/src/components/AuditLogRow/AuditLogRow.test.tsx b/site/src/components/AuditLogRow/AuditLogRow.test.tsx new file mode 100644 index 0000000000000..3ee13f46e4bc0 --- /dev/null +++ b/site/src/components/AuditLogRow/AuditLogRow.test.tsx @@ -0,0 +1,41 @@ +import { readableActionMessage } from "./AuditLogRow" +import { + MockAuditLog, + MockAuditLogWithWorkspaceBuild, +} from "testHelpers/entities" + +describe("readableActionMessage()", () => { + it("renders the correct string for a workspaceBuild audit log", async () => { + // When + const friendlyString = readableActionMessage(MockAuditLogWithWorkspaceBuild) + + // Then + expect(friendlyString).toBe( + "TestUser stopped workspace build for test2", + ) + }) + it("renders the correct string for a workspaceBuild audit log with a duplicate word", async () => { + // When + const AuditLogWithRepeat = { + ...MockAuditLogWithWorkspaceBuild, + additional_fields: { + workspaceName: "workspace", + }, + } + const friendlyString = readableActionMessage(AuditLogWithRepeat) + + // Then + expect(friendlyString).toBe( + "TestUser stopped workspace build for workspace", + ) + }) + it("renders the correct string for a workspace audit log", async () => { + // When + const friendlyString = readableActionMessage(MockAuditLog) + + // Then + expect(friendlyString).toBe( + "TestUser updated workspace bruno-dev", + ) + }) +}) diff --git a/site/src/components/AuditLogRow/AuditLogRow.tsx b/site/src/components/AuditLogRow/AuditLogRow.tsx index 3f4bcea2e0490..c00d1564d5c95 100644 --- a/site/src/components/AuditLogRow/AuditLogRow.tsx +++ b/site/src/components/AuditLogRow/AuditLogRow.tsx @@ -16,10 +16,17 @@ import userAgentParser from "ua-parser-js" import { combineClasses } from "util/combineClasses" import { AuditLogDiff } from "./AuditLogDiff" -const readableActionMessage = (auditLog: AuditLog) => { +export const readableActionMessage = (auditLog: AuditLog): string => { + 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") { + target = auditLog.additional_fields.workspaceName.trim() + } + return auditLog.description .replace("{user}", `${auditLog.user?.username.trim()}`) - .replace("{target}", `${auditLog.resource_target.trim()}`) + .replace("{target}", `${target}`) } const httpStatusColor = (httpStatus: number): PaletteIndex => { diff --git a/site/src/testHelpers/entities.ts b/site/src/testHelpers/entities.ts index d02f8bfd5b60f..d4670f66baead 100644 --- a/site/src/testHelpers/entities.ts +++ b/site/src/testHelpers/entities.ts @@ -916,7 +916,7 @@ export const MockAuditLog: TypesGen.AuditLog = { }, }, status_code: 200, - additional_fields: "", + additional_fields: {}, description: "{user} updated workspace {target}", user: MockUser, } @@ -949,6 +949,18 @@ export const MockAuditLog2: TypesGen.AuditLog = { }, } +export const MockAuditLogWithWorkspaceBuild: TypesGen.AuditLog = { + ...MockAuditLog, + id: "f90995bf-4a2b-4089-b597-e66e025e523e", + request_id: "61555889-2875-475c-8494-f7693dd5d75b", + action: "stop", + resource_type: "workspace_build", + description: "{user} stopped workspace build for {target}", + additional_fields: { + workspaceName: "test2", + }, +} + export const MockWorkspaceQuota: TypesGen.WorkspaceQuota = { user_workspace_count: 0, user_workspace_limit: 100,