diff --git a/coderd/audit.go b/coderd/audit.go index c645e11e7b4e9..b30dd37c28683 100644 --- a/coderd/audit.go +++ b/coderd/audit.go @@ -161,8 +161,9 @@ func (api *API) convertAuditLogs(ctx context.Context, dblogs []database.GetAudit } type AdditionalFields struct { - WorkspaceName string - BuildNumber string + WorkspaceName string + BuildNumber string + WorkspaceOwner string } func (api *API) convertAuditLog(ctx context.Context, dblog database.GetAuditLogsOffsetRow) codersdk.AuditLog { @@ -198,8 +199,9 @@ func (api *API) convertAuditLog(ctx context.Context, dblog database.GetAuditLogs if err != nil { api.Logger.Error(ctx, "unmarshal additional fields", slog.Error(err)) resourceInfo := map[string]string{ - "workspaceName": "unknown", - "buildNumber": "unknown", + "workspaceName": "unknown", + "buildNumber": "unknown", + "workspaceOwner": "unknown", } dblog.AdditionalFields, err = json.Marshal(resourceInfo) api.Logger.Error(ctx, "marshal additional fields", slog.Error(err)) @@ -331,8 +333,12 @@ func auditLogResourceLink(alog database.GetAuditLogsOffsetRow, additionalFields return fmt.Sprintf("/users?filter=%s", alog.ResourceTarget) case database.ResourceTypeWorkspace: + workspaceOwner := alog.UserUsername.String + if len(additionalFields.WorkspaceOwner) != 0 && additionalFields.WorkspaceOwner != "unknown" { + workspaceOwner = additionalFields.WorkspaceOwner + } return fmt.Sprintf("/@%s/%s", - alog.UserUsername.String, alog.ResourceTarget) + workspaceOwner, alog.ResourceTarget) case database.ResourceTypeWorkspaceBuild: if len(additionalFields.WorkspaceName) == 0 || len(additionalFields.BuildNumber) == 0 { return "" diff --git a/coderd/workspaces.go b/coderd/workspaces.go index f69a98b8f0741..146d3bf2374a3 100644 --- a/coderd/workspaces.go +++ b/coderd/workspaces.go @@ -237,18 +237,27 @@ func (api *API) workspaceByOwnerAndName(rw http.ResponseWriter, r *http.Request) // Create a new workspace for the currently authenticated user. func (api *API) postWorkspacesByOrganization(rw http.ResponseWriter, r *http.Request) { var ( - ctx = r.Context() - organization = httpmw.OrganizationParam(r) - apiKey = httpmw.APIKey(r) - auditor = api.Auditor.Load() - user = httpmw.UserParam(r) - aReq, commitAudit = audit.InitRequest[database.Workspace](rw, &audit.RequestParams{ - Audit: *auditor, - Log: api.Logger, - Request: r, - Action: database.AuditActionCreate, - }) + ctx = r.Context() + organization = httpmw.OrganizationParam(r) + apiKey = httpmw.APIKey(r) + auditor = api.Auditor.Load() + user = httpmw.UserParam(r) + workspaceResourceInfo = map[string]string{ + "workspaceOwner": user.Username, + } ) + wriBytes, err := json.Marshal(workspaceResourceInfo) + if err != nil { + api.Logger.Warn(ctx, "marshal workspace owner name") + } + + aReq, commitAudit := audit.InitRequest[database.Workspace](rw, &audit.RequestParams{ + Audit: *auditor, + Log: api.Logger, + Request: r, + Action: database.AuditActionCreate, + AdditionalFields: wriBytes, + }) defer commitAudit() if !api.Authorize(r, rbac.ActionCreate, diff --git a/site/src/components/AuditLogRow/AuditLogDescription.test.tsx b/site/src/components/AuditLogRow/AuditLogDescription.test.tsx index cf8801678cca6..4f3f8162b8c69 100644 --- a/site/src/components/AuditLogRow/AuditLogDescription.test.tsx +++ b/site/src/components/AuditLogRow/AuditLogDescription.test.tsx @@ -1,6 +1,7 @@ import { MockAuditLog, MockAuditLogWithWorkspaceBuild, + MockWorkspaceCreateAuditLogForDifferentOwner, } from "testHelpers/entities" import { AuditLogDescription } from "./AuditLogDescription" import { render } from "../../testHelpers/renderHelpers" @@ -46,4 +47,16 @@ describe("AuditLogDescription", () => { getByTextContent("TestUser stopped build for workspace workspace"), ).toBeDefined() }) + it("renders the correct string for a workspace created for a different owner", async () => { + render( + , + ) + expect( + getByTextContent( + `TestUser created workspace bruno-dev on behalf of ${MockWorkspaceCreateAuditLogForDifferentOwner.additional_fields.workspaceOwner}`, + ), + ).toBeDefined() + }) }) diff --git a/site/src/components/AuditLogRow/AuditLogDescription.tsx b/site/src/components/AuditLogRow/AuditLogDescription.tsx index c559d3114c024..38382f96ff9a2 100644 --- a/site/src/components/AuditLogRow/AuditLogDescription.tsx +++ b/site/src/components/AuditLogRow/AuditLogDescription.tsx @@ -45,9 +45,20 @@ export const AuditLogDescription: FC<{ auditLog: AuditLog }> = ({ )} {auditLog.is_deleted && ( - <> {t("auditLog:table.logRow.deletedLabel")} + <>{t("auditLog:table.logRow.deletedLabel")} )} + {/* logs for workspaces created on behalf of other users indicate ownership in the description */} + {auditLog.additional_fields.workspaceOwner && + auditLog.additional_fields.workspaceOwner !== "unknown" && ( + + <> + {t("auditLog:table.logRow.onBehalfOf", { + owner: auditLog.additional_fields.workspaceOwner, + })} + + + )} ) } diff --git a/site/src/i18n/en/auditLog.json b/site/src/i18n/en/auditLog.json index b68d33ab5fcb1..b9b8068d20aaa 100644 --- a/site/src/i18n/en/auditLog.json +++ b/site/src/i18n/en/auditLog.json @@ -8,11 +8,12 @@ "emptyPage": "No audit logs available on this page", "noLogs": "No audit logs available", "logRow": { - "deletedLabel": "(deleted)", + "deletedLabel": " (deleted)", "ip": "IP: ", "os": "OS: ", "browser": "Browser: ", - "notAvailable": "Not available" + "notAvailable": "Not available", + "onBehalfOf": " on behalf of {{owner}}" } } } diff --git a/site/src/testHelpers/entities.ts b/site/src/testHelpers/entities.ts index 725383b3f3c98..c7425ebeca8c6 100644 --- a/site/src/testHelpers/entities.ts +++ b/site/src/testHelpers/entities.ts @@ -1020,6 +1020,13 @@ export const MockAuditLog2: TypesGen.AuditLog = { }, } +export const MockWorkspaceCreateAuditLogForDifferentOwner = { + ...MockAuditLog, + additional_fields: { + workspaceOwner: "Member", + }, +} + export const MockAuditLogWithWorkspaceBuild: TypesGen.AuditLog = { ...MockAuditLog, id: "f90995bf-4a2b-4089-b597-e66e025e523e",