diff --git a/coderd/audit.go b/coderd/audit.go index 62c26a24cb..8a87b1297d 100644 --- a/coderd/audit.go +++ b/coderd/audit.go @@ -46,7 +46,10 @@ func (api *API) auditLogs(rw http.ResponseWriter, r *http.Request) { Offset: int32(page.Offset), Limit: int32(page.Limit), ResourceType: filter.ResourceType, + ResourceID: filter.ResourceID, Action: filter.Action, + Username: filter.Username, + Email: filter.Email, }) if err != nil { httpapi.InternalServerError(rw, err) @@ -77,7 +80,10 @@ func (api *API) auditLogCount(rw http.ResponseWriter, r *http.Request) { count, err := api.Database.GetAuditLogCount(ctx, database.GetAuditLogCountParams{ ResourceType: filter.ResourceType, + ResourceID: filter.ResourceID, Action: filter.Action, + Username: filter.Username, + Email: filter.Email, }) if err != nil { httpapi.InternalServerError(rw, err) @@ -134,6 +140,9 @@ func (api *API) generateFakeAuditLog(rw http.ResponseWriter, r *http.Request) { if params.ResourceType == "" { params.ResourceType = codersdk.ResourceTypeUser } + if params.ResourceID == uuid.Nil { + params.ResourceID = uuid.New() + } _, err = api.Database.InsertAuditLog(ctx, database.InsertAuditLogParams{ ID: uuid.New(), @@ -142,7 +151,7 @@ func (api *API) generateFakeAuditLog(rw http.ResponseWriter, r *http.Request) { Ip: ipNet, UserAgent: r.UserAgent(), ResourceType: database.ResourceType(params.ResourceType), - ResourceID: user.ID, + ResourceID: params.ResourceID, ResourceTarget: user.Username, Action: database.AuditAction(params.Action), Diff: diff, @@ -251,7 +260,10 @@ func auditSearchQuery(query string) (database.GetAuditLogsOffsetParams, []coders parser := httpapi.NewQueryParamParser() filter := database.GetAuditLogsOffsetParams{ ResourceType: parser.String(searchParams, "", "resource_type"), + ResourceID: parser.UUID(searchParams, uuid.Nil, "resource_id"), Action: parser.String(searchParams, "", "action"), + Username: parser.String(searchParams, "", "username"), + Email: parser.String(searchParams, "", "email"), } return filter, parser.Errors diff --git a/coderd/audit_test.go b/coderd/audit_test.go index e8978654ec..9368746a88 100644 --- a/coderd/audit_test.go +++ b/coderd/audit_test.go @@ -4,6 +4,7 @@ import ( "context" "testing" + "github.com/google/uuid" "github.com/stretchr/testify/require" "github.com/coder/coder/coderd/coderdtest" @@ -47,6 +48,7 @@ func TestAuditLogsFilter(t *testing.T) { ctx := context.Background() client := coderdtest.New(t, nil) _ = coderdtest.CreateFirstUser(t, client) + userResourceID := uuid.New() // Create two logs with "Create" err := client.CreateTestAuditLog(ctx, codersdk.CreateTestAuditLogRequest{ @@ -57,6 +59,7 @@ func TestAuditLogsFilter(t *testing.T) { err = client.CreateTestAuditLog(ctx, codersdk.CreateTestAuditLogRequest{ Action: codersdk.AuditActionCreate, ResourceType: codersdk.ResourceTypeUser, + ResourceID: userResourceID, }) require.NoError(t, err) @@ -64,6 +67,7 @@ func TestAuditLogsFilter(t *testing.T) { err = client.CreateTestAuditLog(ctx, codersdk.CreateTestAuditLogRequest{ Action: codersdk.AuditActionDelete, ResourceType: codersdk.ResourceTypeUser, + ResourceID: userResourceID, }) require.NoError(t, err) @@ -93,9 +97,25 @@ func TestAuditLogsFilter(t *testing.T) { SearchQuery: "resource_type:template", ExpectedResult: 1, }, + { + Name: "FilterByEmail", + SearchQuery: "email:" + coderdtest.FirstUserParams.Email, + ExpectedResult: 3, + }, + { + Name: "FilterByUsername", + SearchQuery: "username:" + coderdtest.FirstUserParams.Username, + ExpectedResult: 3, + }, + { + Name: "FilterByResourceID", + SearchQuery: "resource_id:" + userResourceID.String(), + ExpectedResult: 2, + }, } for _, testCase := range testCases { + // Test filtering t.Run(testCase.Name, func(t *testing.T) { t.Parallel() auditLogs, err := client.AuditLogs(ctx, codersdk.AuditLogsRequest{ @@ -107,75 +127,15 @@ func TestAuditLogsFilter(t *testing.T) { require.NoError(t, err, "fetch audit logs") require.Len(t, auditLogs.AuditLogs, testCase.ExpectedResult, "expected audit logs returned") }) - } - }) -} -func TestAuditLogCountFilter(t *testing.T) { - t.Parallel() - - t.Run("Filter", func(t *testing.T) { - t.Parallel() - - ctx := context.Background() - client := coderdtest.New(t, nil) - _ = coderdtest.CreateFirstUser(t, client) - - // Create two logs with "Create" - err := client.CreateTestAuditLog(ctx, codersdk.CreateTestAuditLogRequest{ - Action: codersdk.AuditActionCreate, - ResourceType: codersdk.ResourceTypeTemplate, - }) - require.NoError(t, err) - err = client.CreateTestAuditLog(ctx, codersdk.CreateTestAuditLogRequest{ - Action: codersdk.AuditActionCreate, - ResourceType: codersdk.ResourceTypeUser, - }) - require.NoError(t, err) - - // Create one log with "Delete" - err = client.CreateTestAuditLog(ctx, codersdk.CreateTestAuditLogRequest{ - Action: codersdk.AuditActionDelete, - ResourceType: codersdk.ResourceTypeUser, - }) - require.NoError(t, err) - - // Test cases - testCases := []struct { - Name string - SearchQuery string - ExpectedResult int64 - }{ - { - Name: "FilterByCreateAction", - SearchQuery: "action:create", - ExpectedResult: 2, - }, - { - Name: "FilterByDeleteAction", - SearchQuery: "action:delete", - ExpectedResult: 1, - }, - { - Name: "FilterByUserResourceType", - SearchQuery: "resource_type:user", - ExpectedResult: 2, - }, - { - Name: "FilterByTemplateResourceType", - SearchQuery: "resource_type:template", - ExpectedResult: 1, - }, - } - - for _, testCase := range testCases { - t.Run(testCase.Name, func(t *testing.T) { + // Test count filtering + t.Run("GetCount"+testCase.Name, func(t *testing.T) { t.Parallel() response, err := client.AuditLogCount(ctx, codersdk.AuditLogCountRequest{ SearchQuery: testCase.SearchQuery, }) require.NoError(t, err, "fetch audit logs count") - require.Equal(t, response.Count, testCase.ExpectedResult, "expected audit logs count returned") + require.Equal(t, int(response.Count), testCase.ExpectedResult, "expected audit logs count returned") }) } }) diff --git a/coderd/database/databasefake/databasefake.go b/coderd/database/databasefake/databasefake.go index bdcb61505e..a63643c29e 100644 --- a/coderd/database/databasefake/databasefake.go +++ b/coderd/database/databasefake/databasefake.go @@ -2388,14 +2388,27 @@ func (q *fakeQuerier) GetAuditLogsOffset(ctx context.Context, arg database.GetAu arg.Offset-- continue } - if arg.Action != "" && !strings.Contains(string(alog.Action), arg.Action) { continue } - if arg.ResourceType != "" && !strings.Contains(string(alog.ResourceType), arg.ResourceType) { continue } + if arg.ResourceID != uuid.Nil && alog.ResourceID != arg.ResourceID { + continue + } + if arg.Username != "" { + user, err := q.GetUserByID(context.Background(), alog.UserID) + if err == nil && !strings.EqualFold(arg.Username, user.Username) { + continue + } + } + if arg.Email != "" { + user, err := q.GetUserByID(context.Background(), alog.UserID) + if err == nil && !strings.EqualFold(arg.Email, user.Email) { + continue + } + } user, err := q.GetUserByID(ctx, alog.UserID) userValid := err == nil @@ -2440,10 +2453,24 @@ func (q *fakeQuerier) GetAuditLogCount(_ context.Context, arg database.GetAuditL if arg.Action != "" && !strings.Contains(string(alog.Action), arg.Action) { continue } - if arg.ResourceType != "" && !strings.Contains(string(alog.ResourceType), arg.ResourceType) { continue } + if arg.ResourceID != uuid.Nil && alog.ResourceID != arg.ResourceID { + continue + } + if arg.Username != "" { + user, err := q.GetUserByID(context.Background(), alog.UserID) + if err == nil && !strings.EqualFold(arg.Username, user.Username) { + continue + } + } + if arg.Email != "" { + user, err := q.GetUserByID(context.Background(), alog.UserID) + if err == nil && !strings.EqualFold(arg.Email, user.Email) { + continue + } + } logs = append(logs, alog) } diff --git a/coderd/database/queries.sql.go b/coderd/database/queries.sql.go index acbd6df2b4..0da956761b 100644 --- a/coderd/database/queries.sql.go +++ b/coderd/database/queries.sql.go @@ -314,9 +314,9 @@ func (q *sqlQuerier) UpdateAPIKeyByID(ctx context.Context, arg UpdateAPIKeyByIDP const getAuditLogCount = `-- name: GetAuditLogCount :one SELECT - COUNT(*) as count + COUNT(*) as count FROM - audit_logs + audit_logs WHERE -- Filter resource_type CASE @@ -324,21 +324,56 @@ WHERE resource_type = $1 :: resource_type ELSE true END + -- Filter resource_id + AND CASE + WHEN $2 :: uuid != '00000000-00000000-00000000-00000000' THEN + resource_id = $2 + ELSE true + END + -- Filter by resource_target + AND CASE + WHEN $3 :: text != '' THEN + resource_target = $3 + ELSE true + END -- Filter action AND CASE - WHEN $2 :: text != '' THEN - action = $2 :: audit_action + WHEN $4 :: text != '' THEN + action = $4 :: audit_action + ELSE true + END + -- Filter by username + AND CASE + WHEN $5 :: text != '' THEN + user_id = (SELECT id from users WHERE users.username = $5 ) + ELSE true + END + -- Filter by user_email + AND CASE + WHEN $6 :: text != '' THEN + user_id = (SELECT id from users WHERE users.email = $6 ) ELSE true END ` type GetAuditLogCountParams struct { - ResourceType string `db:"resource_type" json:"resource_type"` - Action string `db:"action" json:"action"` + ResourceType string `db:"resource_type" json:"resource_type"` + ResourceID uuid.UUID `db:"resource_id" json:"resource_id"` + ResourceTarget string `db:"resource_target" json:"resource_target"` + Action string `db:"action" json:"action"` + Username string `db:"username" json:"username"` + Email string `db:"email" json:"email"` } func (q *sqlQuerier) GetAuditLogCount(ctx context.Context, arg GetAuditLogCountParams) (int64, error) { - row := q.db.QueryRowContext(ctx, getAuditLogCount, arg.ResourceType, arg.Action) + row := q.db.QueryRowContext(ctx, getAuditLogCount, + arg.ResourceType, + arg.ResourceID, + arg.ResourceTarget, + arg.Action, + arg.Username, + arg.Email, + ) var count int64 err := row.Scan(&count) return count, err @@ -364,10 +399,34 @@ WHERE resource_type = $3 :: resource_type ELSE true END + -- Filter resource_id + AND CASE + WHEN $4 :: uuid != '00000000-00000000-00000000-00000000' THEN + resource_id = $4 + ELSE true + END + -- Filter by resource_target + AND CASE + WHEN $5 :: text != '' THEN + resource_target = $5 + ELSE true + END -- Filter action AND CASE - WHEN $4 :: text != '' THEN - action = $4 :: audit_action + WHEN $6 :: text != '' THEN + action = $6 :: audit_action + ELSE true + END + -- Filter by username + AND CASE + WHEN $7 :: text != '' THEN + users.username = $7 + ELSE true + END + -- Filter by user_email + AND CASE + WHEN $8 :: text != '' THEN + users.email = $8 ELSE true END ORDER BY @@ -379,10 +438,14 @@ OFFSET ` type GetAuditLogsOffsetParams struct { - Limit int32 `db:"limit" json:"limit"` - Offset int32 `db:"offset" json:"offset"` - ResourceType string `db:"resource_type" json:"resource_type"` - Action string `db:"action" json:"action"` + Limit int32 `db:"limit" json:"limit"` + Offset int32 `db:"offset" json:"offset"` + ResourceType string `db:"resource_type" json:"resource_type"` + ResourceID uuid.UUID `db:"resource_id" json:"resource_id"` + ResourceTarget string `db:"resource_target" json:"resource_target"` + Action string `db:"action" json:"action"` + Username string `db:"username" json:"username"` + Email string `db:"email" json:"email"` } type GetAuditLogsOffsetRow struct { @@ -416,7 +479,11 @@ func (q *sqlQuerier) GetAuditLogsOffset(ctx context.Context, arg GetAuditLogsOff arg.Limit, arg.Offset, arg.ResourceType, + arg.ResourceID, + arg.ResourceTarget, arg.Action, + arg.Username, + arg.Email, ) if err != nil { return nil, err diff --git a/coderd/database/queries/auditlogs.sql b/coderd/database/queries/auditlogs.sql index 5d2e99af5c..5cb8e93db4 100644 --- a/coderd/database/queries/auditlogs.sql +++ b/coderd/database/queries/auditlogs.sql @@ -20,12 +20,36 @@ WHERE resource_type = @resource_type :: resource_type ELSE true END + -- Filter resource_id + AND CASE + WHEN @resource_id :: uuid != '00000000-00000000-00000000-00000000' THEN + resource_id = @resource_id + ELSE true + END + -- Filter by resource_target + AND CASE + WHEN @resource_target :: text != '' THEN + resource_target = @resource_target + ELSE true + END -- Filter action AND CASE WHEN @action :: text != '' THEN action = @action :: audit_action ELSE true END + -- Filter by username + AND CASE + WHEN @username :: text != '' THEN + users.username = @username + ELSE true + END + -- Filter by user_email + AND CASE + WHEN @email :: text != '' THEN + users.email = @email + ELSE true + END ORDER BY "time" DESC LIMIT @@ -35,9 +59,9 @@ OFFSET -- name: GetAuditLogCount :one SELECT - COUNT(*) as count + COUNT(*) as count FROM - audit_logs + audit_logs WHERE -- Filter resource_type CASE @@ -45,11 +69,35 @@ WHERE resource_type = @resource_type :: resource_type ELSE true END + -- Filter resource_id + AND CASE + WHEN @resource_id :: uuid != '00000000-00000000-00000000-00000000' THEN + resource_id = @resource_id + ELSE true + END + -- Filter by resource_target + AND CASE + WHEN @resource_target :: text != '' THEN + resource_target = @resource_target + ELSE true + END -- Filter action AND CASE WHEN @action :: text != '' THEN action = @action :: audit_action ELSE true + END + -- Filter by username + AND CASE + WHEN @username :: text != '' THEN + user_id = (SELECT id from users WHERE users.username = @username ) + ELSE true + END + -- Filter by user_email + AND CASE + WHEN @email :: text != '' THEN + user_id = (SELECT id from users WHERE users.email = @email ) + ELSE true END; -- name: InsertAuditLog :one diff --git a/codersdk/audit.go b/codersdk/audit.go index 9f6895ed93..068d8d63c9 100644 --- a/codersdk/audit.go +++ b/codersdk/audit.go @@ -114,6 +114,7 @@ type AuditLogCountResponse struct { type CreateTestAuditLogRequest struct { Action AuditAction `json:"action,omitempty"` ResourceType ResourceType `json:"resource_type,omitempty"` + ResourceID uuid.UUID `json:"resource_id,omitempty"` } // AuditLogs retrieves audit logs from the given page. diff --git a/site/src/api/typesGenerated.ts b/site/src/api/typesGenerated.ts index b668297544..192b33166c 100644 --- a/site/src/api/typesGenerated.ts +++ b/site/src/api/typesGenerated.ts @@ -180,6 +180,7 @@ export interface CreateTemplateVersionRequest { export interface CreateTestAuditLogRequest { readonly action?: AuditAction readonly resource_type?: ResourceType + readonly resource_id?: string } // From codersdk/users.go