feat: add audit log filter for autostarted and autostopped workspace builds (#5830)

* added query

* fixed query

* added example to dropdown

* added documentation

* added test

* fixed formatting

* fixed format
This commit is contained in:
Kira Pilot 2023-01-24 15:34:29 -05:00 committed by GitHub
parent 36384aa3c1
commit 322a4d93e1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 130 additions and 11 deletions

12
coderd/apidoc/docs.go generated
View File

@ -5583,6 +5583,18 @@ const docTemplate = `{
}
]
},
"build_reason": {
"enum": [
"autostart",
"autostop",
"initiator"
],
"allOf": [
{
"$ref": "#/definitions/codersdk.BuildReason"
}
]
},
"resource_id": {
"type": "string",
"format": "uuid"

View File

@ -4943,6 +4943,14 @@
}
]
},
"build_reason": {
"enum": ["autostart", "autostop", "initiator"],
"allOf": [
{
"$ref": "#/definitions/codersdk.BuildReason"
}
]
},
"resource_id": {
"type": "string",
"format": "uuid"

View File

@ -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 ""
}

View File

@ -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 {

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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.

View File

@ -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

View File

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

View File

@ -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` |

View File

@ -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

View File

@ -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 {