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",