diff --git a/coderd/audit.go b/coderd/audit.go index 3b7d18f7b6496..62c26a24cb2a5 100644 --- a/coderd/audit.go +++ b/coderd/audit.go @@ -65,7 +65,20 @@ func (api *API) auditLogCount(rw http.ResponseWriter, r *http.Request) { return } - count, err := api.Database.GetAuditLogCount(ctx) + queryStr := r.URL.Query().Get("q") + filter, errs := auditSearchQuery(queryStr) + if len(errs) > 0 { + httpapi.Write(rw, http.StatusBadRequest, codersdk.Response{ + Message: "Invalid audit search query.", + Validations: errs, + }) + return + } + + count, err := api.Database.GetAuditLogCount(ctx, database.GetAuditLogCountParams{ + ResourceType: filter.ResourceType, + Action: filter.Action, + }) if err != nil { httpapi.InternalServerError(rw, err) return diff --git a/coderd/audit_test.go b/coderd/audit_test.go index 47b7daf9a425f..e8978654ec93e 100644 --- a/coderd/audit_test.go +++ b/coderd/audit_test.go @@ -23,7 +23,7 @@ func TestAuditLogs(t *testing.T) { err := client.CreateTestAuditLog(ctx, codersdk.CreateTestAuditLogRequest{}) require.NoError(t, err) - count, err := client.AuditLogCount(ctx) + count, err := client.AuditLogCount(ctx, codersdk.AuditLogCountRequest{}) require.NoError(t, err) alogs, err := client.AuditLogs(ctx, codersdk.AuditLogsRequest{ @@ -41,7 +41,7 @@ func TestAuditLogs(t *testing.T) { func TestAuditLogsFilter(t *testing.T) { t.Parallel() - t.Run("FilterByResourceType", func(t *testing.T) { + t.Run("Filter", func(t *testing.T) { t.Parallel() ctx := context.Background() @@ -110,3 +110,73 @@ func TestAuditLogsFilter(t *testing.T) { } }) } + +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) { + 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") + }) + } + }) +} diff --git a/coderd/database/databasefake/databasefake.go b/coderd/database/databasefake/databasefake.go index 57058c072c748..533c41f6f5ac6 100644 --- a/coderd/database/databasefake/databasefake.go +++ b/coderd/database/databasefake/databasefake.go @@ -2402,11 +2402,25 @@ func (q *fakeQuerier) GetAuditLogsOffset(ctx context.Context, arg database.GetAu return logs, nil } -func (q *fakeQuerier) GetAuditLogCount(_ context.Context) (int64, error) { +func (q *fakeQuerier) GetAuditLogCount(_ context.Context, arg database.GetAuditLogCountParams) (int64, error) { q.mutex.RLock() defer q.mutex.RUnlock() - return int64(len(q.auditLogs)), nil + logs := make([]database.AuditLog, 0) + + for _, alog := range q.auditLogs { + if arg.Action != "" && !strings.Contains(string(alog.Action), arg.Action) { + continue + } + + if arg.ResourceType != "" && !strings.Contains(string(alog.ResourceType), arg.ResourceType) { + continue + } + + logs = append(logs, alog) + } + + return int64(len(logs)), nil } func (q *fakeQuerier) InsertAuditLog(_ context.Context, arg database.InsertAuditLogParams) (database.AuditLog, error) { diff --git a/coderd/database/querier.go b/coderd/database/querier.go index 0129fca8f7a53..62d63b929f198 100644 --- a/coderd/database/querier.go +++ b/coderd/database/querier.go @@ -27,7 +27,7 @@ type querier interface { GetAPIKeyByID(ctx context.Context, id string) (APIKey, error) GetAPIKeysLastUsedAfter(ctx context.Context, lastUsed time.Time) ([]APIKey, error) GetActiveUserCount(ctx context.Context) (int64, error) - GetAuditLogCount(ctx context.Context) (int64, error) + GetAuditLogCount(ctx context.Context, arg GetAuditLogCountParams) (int64, error) // GetAuditLogsBefore retrieves `row_limit` number of audit logs before the provided // ID. GetAuditLogsOffset(ctx context.Context, arg GetAuditLogsOffsetParams) ([]GetAuditLogsOffsetRow, error) diff --git a/coderd/database/queries.sql.go b/coderd/database/queries.sql.go index 0233d611a487e..58d0832d48dbe 100644 --- a/coderd/database/queries.sql.go +++ b/coderd/database/queries.sql.go @@ -292,10 +292,28 @@ SELECT COUNT(*) as count FROM audit_logs +WHERE + -- Filter resource_type + CASE + WHEN $1 :: text != '' THEN + resource_type = $1 :: resource_type + ELSE true + END + -- Filter action + AND CASE + WHEN $2 :: text != '' THEN + action = $2 :: audit_action + ELSE true + END ` -func (q *sqlQuerier) GetAuditLogCount(ctx context.Context) (int64, error) { - row := q.db.QueryRowContext(ctx, getAuditLogCount) +type GetAuditLogCountParams struct { + ResourceType string `db:"resource_type" json:"resource_type"` + Action string `db:"action" json:"action"` +} + +func (q *sqlQuerier) GetAuditLogCount(ctx context.Context, arg GetAuditLogCountParams) (int64, error) { + row := q.db.QueryRowContext(ctx, getAuditLogCount, arg.ResourceType, arg.Action) var count int64 err := row.Scan(&count) return count, err diff --git a/coderd/database/queries/auditlogs.sql b/coderd/database/queries/auditlogs.sql index 3e29c74de4947..5d2e99af5cd7f 100644 --- a/coderd/database/queries/auditlogs.sql +++ b/coderd/database/queries/auditlogs.sql @@ -37,7 +37,20 @@ OFFSET SELECT COUNT(*) as count FROM - audit_logs; + audit_logs +WHERE + -- Filter resource_type + CASE + WHEN @resource_type :: text != '' THEN + resource_type = @resource_type :: resource_type + ELSE true + END + -- Filter action + AND CASE + WHEN @action :: text != '' THEN + action = @action :: audit_action + ELSE true + END; -- name: InsertAuditLog :one INSERT INTO diff --git a/codersdk/audit.go b/codersdk/audit.go index 810cf72c2f608..9f6895ed93b17 100644 --- a/codersdk/audit.go +++ b/codersdk/audit.go @@ -103,6 +103,10 @@ type AuditLogResponse struct { AuditLogs []AuditLog `json:"audit_logs"` } +type AuditLogCountRequest struct { + SearchQuery string `json:"q,omitempty"` +} + type AuditLogCountResponse struct { Count int64 `json:"count"` } @@ -142,8 +146,16 @@ func (c *Client) AuditLogs(ctx context.Context, req AuditLogsRequest) (AuditLogR } // AuditLogCount returns the count of all audit logs in the product. -func (c *Client) AuditLogCount(ctx context.Context) (AuditLogCountResponse, error) { - res, err := c.Request(ctx, http.MethodGet, "/api/v2/audit/count", nil) +func (c *Client) AuditLogCount(ctx context.Context, req AuditLogCountRequest) (AuditLogCountResponse, error) { + res, err := c.Request(ctx, http.MethodGet, "/api/v2/audit/count", nil, func(r *http.Request) { + q := r.URL.Query() + var params []string + if req.SearchQuery != "" { + params = append(params, req.SearchQuery) + } + q.Set("q", strings.Join(params, " ")) + r.URL.RawQuery = q.Encode() + }) if err != nil { return AuditLogCountResponse{}, err } diff --git a/site/src/api/api.ts b/site/src/api/api.ts index 7d4520c11a2a2..ea16315c3896e 100644 --- a/site/src/api/api.ts +++ b/site/src/api/api.ts @@ -446,8 +446,14 @@ export const getAuditLogs = async ( return response.data } -export const getAuditLogsCount = async (): Promise => { - const response = await axios.get(`/api/v2/audit/count`) +export const getAuditLogsCount = async ( + options: TypesGen.AuditLogCountRequest = {}, +): Promise => { + const searchParams = new URLSearchParams() + if (options.q) { + searchParams.set("q", options.q) + } + const response = await axios.get(`/api/v2/audit/count?${searchParams.toString()}`) return response.data } diff --git a/site/src/api/typesGenerated.ts b/site/src/api/typesGenerated.ts index 109b8b17124f6..b668297544877 100644 --- a/site/src/api/typesGenerated.ts +++ b/site/src/api/typesGenerated.ts @@ -76,6 +76,11 @@ export interface AuditLog { readonly user?: User } +// From codersdk/audit.go +export interface AuditLogCountRequest { + readonly q?: string +} + // From codersdk/audit.go export interface AuditLogCountResponse { readonly count: number