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, pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy