diff --git a/coderd/apidoc/docs.go b/coderd/apidoc/docs.go index 01b931f489010..a14ebd4ad5b70 100644 --- a/coderd/apidoc/docs.go +++ b/coderd/apidoc/docs.go @@ -5583,6 +5583,18 @@ const docTemplate = `{ } ] }, + "build_reason": { + "enum": [ + "autostart", + "autostop", + "initiator" + ], + "allOf": [ + { + "$ref": "#/definitions/codersdk.BuildReason" + } + ] + }, "resource_id": { "type": "string", "format": "uuid" diff --git a/coderd/apidoc/swagger.json b/coderd/apidoc/swagger.json index aa8bc00cbe81c..53b5b41efd2bd 100644 --- a/coderd/apidoc/swagger.json +++ b/coderd/apidoc/swagger.json @@ -4943,6 +4943,14 @@ } ] }, + "build_reason": { + "enum": ["autostart", "autostop", "initiator"], + "allOf": [ + { + "$ref": "#/definitions/codersdk.BuildReason" + } + ] + }, "resource_id": { "type": "string", "format": "uuid" diff --git a/coderd/audit.go b/coderd/audit.go index ba29ace95bdfb..ca7cfcde2e1f5 100644 --- a/coderd/audit.go +++ b/coderd/audit.go @@ -67,6 +67,7 @@ func (api *API) auditLogs(rw http.ResponseWriter, r *http.Request) { Email: filter.Email, DateFrom: filter.DateFrom, DateTo: filter.DateTo, + BuildReason: filter.BuildReason, }) if err != nil { httpapi.InternalServerError(rw, err) @@ -443,6 +444,7 @@ func auditSearchQuery(query string) (database.GetAuditLogsOffsetParams, []coders Email: parser.String(searchParams, "", "email"), DateFrom: parsedDateFrom, DateTo: parsedDateTo, + BuildReason: buildReasonFromString(parser.String(searchParams, "", "build_reason")), } return filter, parser.Errors @@ -488,3 +490,16 @@ func actionFromString(actionString string) string { } return "" } + +func buildReasonFromString(buildReasonString string) string { + switch codersdk.BuildReason(buildReasonString) { + case codersdk.BuildReasonInitiator: + return buildReasonString + case codersdk.BuildReasonAutostart: + return buildReasonString + case codersdk.BuildReasonAutostop: + return buildReasonString + default: + } + return "" +} diff --git a/coderd/audit_test.go b/coderd/audit_test.go index 8fcc479c23de0..d0060a1f3b3f2 100644 --- a/coderd/audit_test.go +++ b/coderd/audit_test.go @@ -179,6 +179,11 @@ func TestAuditLogsFilter(t *testing.T) { SearchQuery: "resource_type:workspace_build action:stop", ExpectedResult: 1, }, + { + Name: "FilterOnWorkspaceBuildStartByInitiator", + SearchQuery: "resource_type:workspace_build action:start build_reason:start", + ExpectedResult: 1, + }, } for _, testCase := range testCases { diff --git a/coderd/database/databasefake/databasefake.go b/coderd/database/databasefake/databasefake.go index 9c9c304df3a49..402149a56a981 100644 --- a/coderd/database/databasefake/databasefake.go +++ b/coderd/database/databasefake/databasefake.go @@ -3680,6 +3680,12 @@ func (q *fakeQuerier) GetAuditLogsOffset(ctx context.Context, arg database.GetAu continue } } + if arg.BuildReason != "" { + workspaceBuild, err := q.GetWorkspaceBuildByID(context.Background(), alog.ResourceID) + if err == nil && !strings.EqualFold(arg.BuildReason, string(workspaceBuild.Reason)) { + continue + } + } user, err := q.GetUserByID(ctx, alog.UserID) userValid := err == nil diff --git a/coderd/database/queries.sql.go b/coderd/database/queries.sql.go index 1b352587a032a..938bdf878177a 100644 --- a/coderd/database/queries.sql.go +++ b/coderd/database/queries.sql.go @@ -367,18 +367,41 @@ func (q *sqlQuerier) UpdateAPIKeyByID(ctx context.Context, arg UpdateAPIKeyByIDP const getAuditLogsOffset = `-- name: GetAuditLogsOffset :many SELECT - audit_logs.id, audit_logs.time, audit_logs.user_id, audit_logs.organization_id, audit_logs.ip, audit_logs.user_agent, audit_logs.resource_type, audit_logs.resource_id, audit_logs.resource_target, audit_logs.action, audit_logs.diff, audit_logs.status_code, audit_logs.additional_fields, audit_logs.request_id, audit_logs.resource_icon, + audit_logs.id, audit_logs.time, audit_logs.user_id, audit_logs.organization_id, audit_logs.ip, audit_logs.user_agent, audit_logs.resource_type, audit_logs.resource_id, audit_logs.resource_target, audit_logs.action, audit_logs.diff, audit_logs.status_code, audit_logs.additional_fields, audit_logs.request_id, audit_logs.resource_icon, users.username AS user_username, users.email AS user_email, users.created_at AS user_created_at, users.status AS user_status, users.rbac_roles AS user_roles, users.avatar_url AS user_avatar_url, - COUNT(audit_logs.*) OVER() AS count + COUNT(audit_logs.*) OVER () AS count FROM - audit_logs -LEFT JOIN - users ON audit_logs.user_id = users.id + audit_logs + LEFT JOIN users ON audit_logs.user_id = users.id + LEFT JOIN + -- First join on workspaces to get the initial workspace create + -- to workspace build 1 id. This is because the first create is + -- is a different audit log than subsequent starts. + workspaces ON + audit_logs.resource_type = 'workspace' AND + audit_logs.resource_id = workspaces.id + LEFT JOIN + workspace_builds ON + -- Get the reason from the build if the resource type + -- is a workspace_build + ( + audit_logs.resource_type = 'workspace_build' + AND audit_logs.resource_id = workspace_builds.id + ) + OR + -- Get the reason from the build #1 if this is the first + -- workspace create. + ( + audit_logs.resource_type = 'workspace' AND + audit_logs.action = 'create' AND + workspaces.id = workspace_builds.workspace_id AND + workspace_builds.build_number = 1 + ) WHERE -- Filter resource_type CASE @@ -428,6 +451,12 @@ WHERE "time" <= $10 ELSE true END + -- Filter by build_reason + AND CASE + WHEN $11::text != '' THEN + workspace_builds.reason::text = $11 + ELSE true + END ORDER BY "time" DESC LIMIT @@ -447,6 +476,7 @@ type GetAuditLogsOffsetParams struct { Email string `db:"email" json:"email"` DateFrom time.Time `db:"date_from" json:"date_from"` DateTo time.Time `db:"date_to" json:"date_to"` + BuildReason string `db:"build_reason" json:"build_reason"` } type GetAuditLogsOffsetRow struct { @@ -488,6 +518,7 @@ func (q *sqlQuerier) GetAuditLogsOffset(ctx context.Context, arg GetAuditLogsOff arg.Email, arg.DateFrom, arg.DateTo, + arg.BuildReason, ) if err != nil { return nil, err diff --git a/coderd/database/queries/auditlogs.sql b/coderd/database/queries/auditlogs.sql index 920b93461d77d..bffebae7c3961 100644 --- a/coderd/database/queries/auditlogs.sql +++ b/coderd/database/queries/auditlogs.sql @@ -2,18 +2,41 @@ -- ID. -- name: GetAuditLogsOffset :many SELECT - audit_logs.*, + audit_logs.*, users.username AS user_username, users.email AS user_email, users.created_at AS user_created_at, users.status AS user_status, users.rbac_roles AS user_roles, users.avatar_url AS user_avatar_url, - COUNT(audit_logs.*) OVER() AS count + COUNT(audit_logs.*) OVER () AS count FROM - audit_logs -LEFT JOIN - users ON audit_logs.user_id = users.id + audit_logs + LEFT JOIN users ON audit_logs.user_id = users.id + LEFT JOIN + -- First join on workspaces to get the initial workspace create + -- to workspace build 1 id. This is because the first create is + -- is a different audit log than subsequent starts. + workspaces ON + audit_logs.resource_type = 'workspace' AND + audit_logs.resource_id = workspaces.id + LEFT JOIN + workspace_builds ON + -- Get the reason from the build if the resource type + -- is a workspace_build + ( + audit_logs.resource_type = 'workspace_build' + AND audit_logs.resource_id = workspace_builds.id + ) + OR + -- Get the reason from the build #1 if this is the first + -- workspace create. + ( + audit_logs.resource_type = 'workspace' AND + audit_logs.action = 'create' AND + workspaces.id = workspace_builds.workspace_id AND + workspace_builds.build_number = 1 + ) WHERE -- Filter resource_type CASE @@ -63,6 +86,12 @@ WHERE "time" <= @date_to ELSE true END + -- Filter by build_reason + AND CASE + WHEN @build_reason::text != '' THEN + workspace_builds.reason::text = @build_reason + ELSE true + END ORDER BY "time" DESC LIMIT diff --git a/codersdk/audit.go b/codersdk/audit.go index 4ae98466c3798..4d6ff25bf07f8 100644 --- a/codersdk/audit.go +++ b/codersdk/audit.go @@ -125,6 +125,7 @@ type CreateTestAuditLogRequest struct { ResourceType ResourceType `json:"resource_type,omitempty" enums:"organization,template,template_version,user,workspace,workspace_build,git_ssh_key,api_key,group"` ResourceID uuid.UUID `json:"resource_id,omitempty" format:"uuid"` Time time.Time `json:"time,omitempty" format:"date-time"` + BuildReason BuildReason `json:"build_reason,omitempty" enums:"autostart,autostop,initiator"` } // AuditLogs retrieves audit logs from the given page. diff --git a/docs/admin/audit-logs.md b/docs/admin/audit-logs.md index 4a05bbb7f6bbe..71398362edce0 100644 --- a/docs/admin/audit-logs.md +++ b/docs/admin/audit-logs.md @@ -31,7 +31,8 @@ The supported filters are: - `username` - The username of the user who triggered the action. - `email` - The email of the user who triggered the action. - `date_from` - The inclusive start date with format `YYYY-MM-DD`. -- `date_to ` - the inclusive end date with format `YYYY-MM-DD`. +- `date_to` - The inclusive end date with format `YYYY-MM-DD`. +- `build_reason` - To be used with `resource_type:workspace_build`, the [initiator](https://pkg.go.dev/github.com/coder/coder/codersdk#BuildReason) behind the build start or stop. ## Enabling this feature diff --git a/docs/api/audit.md b/docs/api/audit.md index 6157c2b1dc922..3f62c5b261f25 100644 --- a/docs/api/audit.md +++ b/docs/api/audit.md @@ -106,6 +106,7 @@ curl -X POST http://coder-server:8080/api/v2/audit/testgenerate \ ```json { "action": "create", + "build_reason": "autostart", "resource_id": "4d5215ed-38bb-48ed-879a-fdb9ca58522f", "resource_type": "organization", "time": "2019-08-24T14:15:22Z" diff --git a/docs/api/schemas.md b/docs/api/schemas.md index 4da28ea65c779..48694c49322e1 100644 --- a/docs/api/schemas.md +++ b/docs/api/schemas.md @@ -783,6 +783,7 @@ CreateParameterRequest is a structure used to create a new parameter value for a ```json { "action": "create", + "build_reason": "autostart", "resource_id": "4d5215ed-38bb-48ed-879a-fdb9ca58522f", "resource_type": "organization", "time": "2019-08-24T14:15:22Z" @@ -794,6 +795,7 @@ CreateParameterRequest is a structure used to create a new parameter value for a | Name | Type | Required | Restrictions | Description | | --------------- | ---------------------------------------------- | -------- | ------------ | ----------- | | `action` | [codersdk.AuditAction](#codersdkauditaction) | false | | | +| `build_reason` | [codersdk.BuildReason](#codersdkbuildreason) | false | | | | `resource_id` | string | false | | | | `resource_type` | [codersdk.ResourceType](#codersdkresourcetype) | false | | | | `time` | string | false | | | @@ -807,6 +809,9 @@ CreateParameterRequest is a structure used to create a new parameter value for a | `action` | `delete` | | `action` | `start` | | `action` | `stop` | +| `build_reason` | `autostart` | +| `build_reason` | `autostop` | +| `build_reason` | `initiator` | | `resource_type` | `organization` | | `resource_type` | `template` | | `resource_type` | `template_version` | diff --git a/site/src/api/typesGenerated.ts b/site/src/api/typesGenerated.ts index a6c18ebdff770..c8f5b66483091 100644 --- a/site/src/api/typesGenerated.ts +++ b/site/src/api/typesGenerated.ts @@ -209,6 +209,7 @@ export interface CreateTestAuditLogRequest { readonly resource_type?: ResourceType readonly resource_id?: string readonly time?: string + readonly build_reason?: BuildReason } // From codersdk/apikey.go diff --git a/site/src/pages/AuditPage/AuditPageView.tsx b/site/src/pages/AuditPage/AuditPageView.tsx index dd057985a7f2b..1347a362c035b 100644 --- a/site/src/pages/AuditPage/AuditPageView.tsx +++ b/site/src/pages/AuditPage/AuditPageView.tsx @@ -40,6 +40,10 @@ const presetFilters = [ query: "resource_type:workspace_build action:start", name: "Started builds", }, + { + query: "resource_type:workspace_build action:start build_reason:initiator", + name: "Builds started by a user", + }, ] export interface AuditPageViewProps {