From 8256786bbced109d463ece6b7e905b01619b548d Mon Sep 17 00:00:00 2001 From: Allisson Azevedo Date: Sat, 7 Mar 2026 21:02:03 -0300 Subject: [PATCH 01/19] chore(conductor): Add new track 'Add Audit Log Filtering by Client' --- conductor/tracks.md | 3 ++ .../index.md | 5 +++ .../metadata.json | 8 ++++ .../plan.md | 37 +++++++++++++++++++ .../spec.md | 36 ++++++++++++++++++ 5 files changed, 89 insertions(+) create mode 100644 conductor/tracks/audit_log_filtering_by_client_20260307/index.md create mode 100644 conductor/tracks/audit_log_filtering_by_client_20260307/metadata.json create mode 100644 conductor/tracks/audit_log_filtering_by_client_20260307/plan.md create mode 100644 conductor/tracks/audit_log_filtering_by_client_20260307/spec.md diff --git a/conductor/tracks.md b/conductor/tracks.md index 0b5c54e..63dfff7 100644 --- a/conductor/tracks.md +++ b/conductor/tracks.md @@ -3,3 +3,6 @@ This file tracks all major tracks for the project. Each track has its own detailed plan in its respective folder. --- + +- [ ] **Track: Add Audit Log Filtering by Client** +*Link: [./tracks/audit_log_filtering_by_client_20260307/](./tracks/audit_log_filtering_by_client_20260307/)* diff --git a/conductor/tracks/audit_log_filtering_by_client_20260307/index.md b/conductor/tracks/audit_log_filtering_by_client_20260307/index.md new file mode 100644 index 0000000..3ed9226 --- /dev/null +++ b/conductor/tracks/audit_log_filtering_by_client_20260307/index.md @@ -0,0 +1,5 @@ +# Track audit_log_filtering_by_client_20260307 Context + +- [Specification](./spec.md) +- [Implementation Plan](./plan.md) +- [Metadata](./metadata.json) diff --git a/conductor/tracks/audit_log_filtering_by_client_20260307/metadata.json b/conductor/tracks/audit_log_filtering_by_client_20260307/metadata.json new file mode 100644 index 0000000..464c419 --- /dev/null +++ b/conductor/tracks/audit_log_filtering_by_client_20260307/metadata.json @@ -0,0 +1,8 @@ +{ + "track_id": "audit_log_filtering_by_client_20260307", + "type": "feature", + "status": "new", + "created_at": "2026-03-07T12:00:00Z", + "updated_at": "2026-03-07T12:00:00Z", + "description": "Add Audit Log Filtering by Client" +} diff --git a/conductor/tracks/audit_log_filtering_by_client_20260307/plan.md b/conductor/tracks/audit_log_filtering_by_client_20260307/plan.md new file mode 100644 index 0000000..f39b1ca --- /dev/null +++ b/conductor/tracks/audit_log_filtering_by_client_20260307/plan.md @@ -0,0 +1,37 @@ +# Implementation Plan: Add Audit Log Filtering by Client + +## Phase 1: Repository Layer Update +- [ ] Task: Update `AuditLogRepository` interface in `internal/auth/usecase/interface.go` to include `clientID *uuid.UUID` in `ListCursor`. +- [ ] Task: Update `PostgreSQLAuditLogRepository` in `internal/auth/repository/postgresql/postgresql_audit_log_repository.go`. + - [ ] Update `ListCursor` to support `client_id` filtering. + - [ ] Update/Add tests in `internal/auth/repository/postgresql/postgresql_audit_log_repository_test.go`. +- [ ] Task: Update `MySQLAuditLogRepository` in `internal/auth/repository/mysql/mysql_audit_log_repository.go`. + - [ ] Update `ListCursor` to support `client_id` filtering. + - [ ] Update/Add tests in `internal/auth/repository/mysql/mysql_audit_log_repository_test.go`. +- [ ] Task: Conductor - User Manual Verification 'Phase 1' (Protocol in workflow.md) + +## Phase 2: Use Case Layer Update +- [ ] Task: Update `AuditLogUseCase` interface in `internal/auth/usecase/interface.go` to include `clientID *uuid.UUID` in `ListCursor`. +- [ ] Task: Update `auditLogUseCase` in `internal/auth/usecase/audit_log_usecase.go`. + - [ ] Update `ListCursor` to pass `clientID` to the repository. + - [ ] Update/Add tests in `internal/auth/usecase/audit_log_usecase_test.go`. +- [ ] Task: Update `auditLogUseCaseWithMetrics` decorator in `internal/auth/usecase/metrics_decorator.go`. + - [ ] Update `ListCursor` signature and implementation. + - [ ] Update tests in `internal/auth/usecase/metrics_decorator_test.go`. +- [ ] Task: Conductor - User Manual Verification 'Phase 2' (Protocol in workflow.md) + +## Phase 3: HTTP Handler Layer Update +- [ ] Task: Update `AuditLogHandler.ListHandler` in `internal/auth/http/audit_log_handler.go`. + - [ ] Parse `client_id` query parameter. + - [ ] Validate `client_id` is a valid UUID. + - [ ] Pass `clientID` to the use case. + - [ ] Update/Add tests in `internal/auth/http/audit_log_handler_test.go`. +- [ ] Task: Conductor - User Manual Verification 'Phase 3' (Protocol in workflow.md) + +## Phase 4: Documentation and Integration Testing +- [ ] Task: Update Documentation. + - [ ] Document `client_id` filter in `docs/observability/audit-logs.md`. + - [ ] Update `docs/openapi.yaml` with the new query parameter. +- [ ] Task: Update Integration Tests. + - [ ] Add audit log filtering test case in `test/integration/auth_flow_test.go`. +- [ ] Task: Conductor - User Manual Verification 'Phase 4' (Protocol in workflow.md) diff --git a/conductor/tracks/audit_log_filtering_by_client_20260307/spec.md b/conductor/tracks/audit_log_filtering_by_client_20260307/spec.md new file mode 100644 index 0000000..5122c5f --- /dev/null +++ b/conductor/tracks/audit_log_filtering_by_client_20260307/spec.md @@ -0,0 +1,36 @@ +# Specification: Add Audit Log Filtering by Client + +## Overview +Currently, audit logs can be retrieved and filtered by date range. This track adds the ability to filter audit logs by a specific Client ID (UUID) via the API. + +## Functional Requirements +- **API Filtering:** The `GET /v1/audit-logs` endpoint must support an optional `client_id` query parameter. +- **Repository Support:** The `AuditLogRepository` must implement filtering by `client_id` in its `ListCursor` method for both PostgreSQL and MySQL implementations. +- **UseCase Support:** The `AuditLogUseCase` must pass the `client_id` filter from the handler to the repository. +- **Validation:** The `client_id` provided in the query parameter must be a valid UUID. +- **Empty Results:** If no audit logs match the specified `client_id`, the API should return an empty list with a `200 OK` status. +- **Documentation:** + - Update `docs/observability/audit-logs.md` to document the new `client_id` filter. + - Update `docs/openapi.yaml` to include the `client_id` query parameter for the audit logs list endpoint. +- **Integration Tests:** + - Update `test/integration/auth_flow_test.go` to include a test case for filtering audit logs by Client ID. + +## Non-Functional Requirements +- **Performance:** Ensure that the database query for filtering by `client_id` is performant. +- **Consistency:** Maintain existing cursor-based pagination and date filtering logic. + +## Acceptance Criteria +- [ ] `GET /v1/audit-logs?client_id=` returns only logs belonging to that client. +- [ ] Providing an invalid UUID for `client_id` returns a `400 Bad Request` error. +- [ ] If `client_id` is omitted, the API continues to return logs for all clients (existing behavior). +- [ ] Filtering by `client_id` works correctly in combination with `created_at_from` and `created_at_to` filters. +- [ ] Filtering by `client_id` works correctly with cursor-based pagination (`after_id`). +- [ ] `docs/observability/audit-logs.md` correctly reflects the new filtering capability. +- [ ] `docs/openapi.yaml` includes the new `client_id` query parameter. +- [ ] Integration tests in `test/integration/auth_flow_test.go` pass and verify the new filtering behavior. +- [ ] PostgreSQL implementation is verified with integration tests. +- [ ] MySQL implementation is verified with integration tests. + +## Out of Scope +- Filtering by Client Name. +- Adding filtering to the CLI `audit-log list` command. From 97bee6d94357a26bf31a3bbd5745afb0f1b32d43 Mon Sep 17 00:00:00 2001 From: Allisson Azevedo Date: Sat, 7 Mar 2026 21:06:58 -0300 Subject: [PATCH 02/19] feat(auth): add clientID filter to AuditLogRepository and AuditLogUseCase --- internal/auth/usecase/interface.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/internal/auth/usecase/interface.go b/internal/auth/usecase/interface.go index e5ba432..22843c9 100644 --- a/internal/auth/usecase/interface.go +++ b/internal/auth/usecase/interface.go @@ -73,12 +73,14 @@ type AuditLogRepository interface { // ListCursor retrieves audit logs ordered by created_at descending (newest first) with cursor-based pagination // and optional time-based filtering. If afterID is provided, returns logs with ID greater than afterID (UUIDv7 ordering). // Accepts createdAtFrom and createdAtTo as optional filters (nil means no filter). Both boundaries are inclusive (>= and <=). + // Accepts clientID as an optional filter (nil means no filter). // All timestamps are expected in UTC. Returns empty slice if no audit logs found. Limit is pre-validated (1-1000). ListCursor( ctx context.Context, afterID *uuid.UUID, limit int, createdAtFrom, createdAtTo *time.Time, + clientID *uuid.UUID, ) ([]*authDomain.AuditLog, error) // DeleteOlderThan removes audit logs with created_at before the specified timestamp. @@ -209,12 +211,14 @@ type AuditLogUseCase interface { // ListCursor retrieves audit logs ordered by created_at descending (newest first) with cursor-based pagination // and optional time-based filtering. If afterID is provided, returns logs with ID greater than afterID (UUIDv7 ordering). // Accepts createdAtFrom and createdAtTo as optional filters (nil means no filter). Both boundaries are inclusive (>= and <=). + // Accepts clientID as an optional filter (nil means no filter). // All timestamps are expected in UTC. Returns empty slice if no audit logs found. Limit is pre-validated (1-1000). ListCursor( ctx context.Context, afterID *uuid.UUID, limit int, createdAtFrom, createdAtTo *time.Time, + clientID *uuid.UUID, ) ([]*authDomain.AuditLog, error) // DeleteOlderThan removes audit logs older than the specified number of days. From 8501e96fb6fadcd4d0d6904b23d9958be69147ef Mon Sep 17 00:00:00 2001 From: Allisson Azevedo Date: Sat, 7 Mar 2026 21:10:32 -0300 Subject: [PATCH 03/19] feat(auth): implement clientID filter in PostgreSQL and MySQL AuditLog repositories --- .../mysql/mysql_audit_log_repository.go | 11 +++ .../mysql/mysql_audit_log_repository_test.go | 75 +++++++++++++++++-- .../postgresql_audit_log_repository.go | 8 ++ .../postgresql_audit_log_repository_test.go | 75 +++++++++++++++++-- 4 files changed, 157 insertions(+), 12 deletions(-) diff --git a/internal/auth/repository/mysql/mysql_audit_log_repository.go b/internal/auth/repository/mysql/mysql_audit_log_repository.go index 2f75f65..6646eda 100644 --- a/internal/auth/repository/mysql/mysql_audit_log_repository.go +++ b/internal/auth/repository/mysql/mysql_audit_log_repository.go @@ -162,6 +162,7 @@ func (m *MySQLAuditLogRepository) Get(ctx context.Context, id uuid.UUID) (*authD // ListCursor retrieves audit logs ordered by created_at descending (newest first) with cursor-based pagination // and optional time-based filtering. If afterID is provided, returns logs with ID greater than afterID (UUIDv7 ordering). // Accepts createdAtFrom and createdAtTo as optional filters (nil means no filter). Both boundaries are inclusive (>= and <=). +// Accepts clientID as an optional filter (nil means no filter). // All timestamps are expected in UTC. Returns empty slice if no audit logs found. Handles NULL metadata gracefully by // returning nil map for those entries. UUIDs are stored as BINARY(16) and must be unmarshaled. Limit is pre-validated (1-1000). func (m *MySQLAuditLogRepository) ListCursor( @@ -169,6 +170,7 @@ func (m *MySQLAuditLogRepository) ListCursor( afterID *uuid.UUID, limit int, createdAtFrom, createdAtTo *time.Time, + clientID *uuid.UUID, ) ([]*authDomain.AuditLog, error) { querier := database.GetTx(ctx, m.db) @@ -196,6 +198,15 @@ func (m *MySQLAuditLogRepository) ListCursor( args = append(args, *createdAtTo) } + if clientID != nil { + clientIDBinary, err := clientID.MarshalBinary() + if err != nil { + return nil, apperrors.Wrap(err, "failed to marshal clientID to binary") + } + conditions = append(conditions, "client_id = ?") + args = append(args, clientIDBinary) + } + // Build query query := `SELECT id, request_id, client_id, capability, path, metadata, signature, kek_id, is_signed, created_at FROM audit_logs` diff --git a/internal/auth/repository/mysql/mysql_audit_log_repository_test.go b/internal/auth/repository/mysql/mysql_audit_log_repository_test.go index c4aa31b..d6e8bb6 100644 --- a/internal/auth/repository/mysql/mysql_audit_log_repository_test.go +++ b/internal/auth/repository/mysql/mysql_audit_log_repository_test.go @@ -381,7 +381,7 @@ func TestMySQLAuditLogRepository_ListCursor_FirstPage(t *testing.T) { } // First page with no cursor (limit 3) - logs, err := repo.ListCursor(ctx, nil, 3, nil, nil) + logs, err := repo.ListCursor(ctx, nil, 3, nil, nil, nil) require.NoError(t, err) assert.Len(t, logs, 3) @@ -419,13 +419,13 @@ func TestMySQLAuditLogRepository_ListCursor_SubsequentPages(t *testing.T) { } // First page (no cursor, limit 3) - page1, err := repo.ListCursor(ctx, nil, 3, nil, nil) + page1, err := repo.ListCursor(ctx, nil, 3, nil, nil, nil) require.NoError(t, err) require.Len(t, page1, 3) // Second page (use last ID from page1 as cursor) lastIDPage1 := page1[len(page1)-1].ID - page2, err := repo.ListCursor(ctx, &lastIDPage1, 3, nil, nil) + page2, err := repo.ListCursor(ctx, &lastIDPage1, 3, nil, nil, nil) require.NoError(t, err) require.Len(t, page2, 3) @@ -438,7 +438,7 @@ func TestMySQLAuditLogRepository_ListCursor_SubsequentPages(t *testing.T) { // Third page (use last ID from page2 as cursor) lastIDPage2 := page2[len(page2)-1].ID - page3, err := repo.ListCursor(ctx, &lastIDPage2, 3, nil, nil) + page3, err := repo.ListCursor(ctx, &lastIDPage2, 3, nil, nil, nil) require.NoError(t, err) require.Len(t, page3, 3) @@ -455,7 +455,7 @@ func TestMySQLAuditLogRepository_ListCursor_EmptyResult(t *testing.T) { ctx := context.Background() // List with no data - logs, err := repo.ListCursor(ctx, nil, 10, nil, nil) + logs, err := repo.ListCursor(ctx, nil, 10, nil, nil, nil) require.NoError(t, err) assert.NotNil(t, logs) assert.Len(t, logs, 0) @@ -490,7 +490,7 @@ func TestMySQLAuditLogRepository_ListCursor_WithFilters(t *testing.T) { // Filter: created_at >= (now - 2 hours) filterFrom := now.Add(-2 * time.Hour) - logs, err := repo.ListCursor(ctx, nil, 10, &filterFrom, nil) + logs, err := repo.ListCursor(ctx, nil, 10, &filterFrom, nil, nil) require.NoError(t, err) assert.LessOrEqual(t, len(logs), 3) // Should get logs from 0, -1, -2 hours @@ -499,3 +499,66 @@ func TestMySQLAuditLogRepository_ListCursor_WithFilters(t *testing.T) { assert.True(t, log.CreatedAt.After(filterFrom) || log.CreatedAt.Equal(filterFrom)) } } + +func TestMySQLAuditLogRepository_ListCursor_ByClientID(t *testing.T) { + db := testutil.SetupMySQLDB(t) + defer testutil.TeardownDB(t, db) + defer testutil.CleanupMySQLDB(t, db) + + repo := NewMySQLAuditLogRepository(db) + ctx := context.Background() + + // Create test clients + clientID1 := testutil.CreateTestClient(t, db, "mysql", "client-1") + clientID2 := testutil.CreateTestClient(t, db, "mysql", "client-2") + + // Create logs for client 1 + for i := 0; i < 3; i++ { + auditLog := &authDomain.AuditLog{ + ID: uuid.Must(uuid.NewV7()), + RequestID: uuid.Must(uuid.NewV7()), + ClientID: clientID1, + Capability: authDomain.ReadCapability, + Path: "/secrets/client1", + CreatedAt: time.Now().UTC(), + } + require.NoError(t, repo.Create(ctx, auditLog)) + time.Sleep(1 * time.Millisecond) + } + + // Create logs for client 2 + for i := 0; i < 2; i++ { + auditLog := &authDomain.AuditLog{ + ID: uuid.Must(uuid.NewV7()), + RequestID: uuid.Must(uuid.NewV7()), + ClientID: clientID2, + Capability: authDomain.ReadCapability, + Path: "/secrets/client2", + CreatedAt: time.Now().UTC(), + } + require.NoError(t, repo.Create(ctx, auditLog)) + time.Sleep(1 * time.Millisecond) + } + + // Filter by client 1 + logs, err := repo.ListCursor(ctx, nil, 10, nil, nil, &clientID1) + require.NoError(t, err) + assert.Len(t, logs, 3) + for _, log := range logs { + assert.Equal(t, clientID1, log.ClientID) + } + + // Filter by client 2 + logs, err = repo.ListCursor(ctx, nil, 10, nil, nil, &clientID2) + require.NoError(t, err) + assert.Len(t, logs, 2) + for _, log := range logs { + assert.Equal(t, clientID2, log.ClientID) + } + + // Filter by non-existent client ID + nonExistentClientID := uuid.New() + logs, err = repo.ListCursor(ctx, nil, 10, nil, nil, &nonExistentClientID) + require.NoError(t, err) + assert.Len(t, logs, 0) +} diff --git a/internal/auth/repository/postgresql/postgresql_audit_log_repository.go b/internal/auth/repository/postgresql/postgresql_audit_log_repository.go index bf0bed6..516bae1 100644 --- a/internal/auth/repository/postgresql/postgresql_audit_log_repository.go +++ b/internal/auth/repository/postgresql/postgresql_audit_log_repository.go @@ -110,6 +110,7 @@ func (p *PostgreSQLAuditLogRepository) Get(ctx context.Context, id uuid.UUID) (* // ListCursor retrieves audit logs ordered by created_at descending (newest first) with cursor-based pagination // and optional time-based filtering. If afterID is provided, returns logs with ID greater than afterID (UUIDv7 ordering). // Accepts createdAtFrom and createdAtTo as optional filters (nil means no filter). Both boundaries are inclusive (>= and <=). +// Accepts clientID as an optional filter (nil means no filter). // All timestamps are expected in UTC. Returns empty slice if no audit logs found. Handles NULL metadata gracefully by // returning nil map for those entries. Limit is pre-validated (1-1000). func (p *PostgreSQLAuditLogRepository) ListCursor( @@ -117,6 +118,7 @@ func (p *PostgreSQLAuditLogRepository) ListCursor( afterID *uuid.UUID, limit int, createdAtFrom, createdAtTo *time.Time, + clientID *uuid.UUID, ) ([]*authDomain.AuditLog, error) { querier := database.GetTx(ctx, p.db) @@ -144,6 +146,12 @@ func (p *PostgreSQLAuditLogRepository) ListCursor( paramIndex++ } + if clientID != nil { + conditions = append(conditions, fmt.Sprintf("client_id = $%d", paramIndex)) + args = append(args, *clientID) + paramIndex++ + } + // Build query query := `SELECT id, request_id, client_id, capability, path, metadata, signature, kek_id, is_signed, created_at FROM audit_logs` diff --git a/internal/auth/repository/postgresql/postgresql_audit_log_repository_test.go b/internal/auth/repository/postgresql/postgresql_audit_log_repository_test.go index e96b250..984aa66 100644 --- a/internal/auth/repository/postgresql/postgresql_audit_log_repository_test.go +++ b/internal/auth/repository/postgresql/postgresql_audit_log_repository_test.go @@ -353,7 +353,7 @@ func TestPostgreSQLAuditLogRepository_ListCursor_FirstPage(t *testing.T) { } // First page with no cursor (limit 3) - logs, err := repo.ListCursor(ctx, nil, 3, nil, nil) + logs, err := repo.ListCursor(ctx, nil, 3, nil, nil, nil) require.NoError(t, err) assert.Len(t, logs, 3) @@ -391,13 +391,13 @@ func TestPostgreSQLAuditLogRepository_ListCursor_SubsequentPages(t *testing.T) { } // First page (no cursor, limit 3) - page1, err := repo.ListCursor(ctx, nil, 3, nil, nil) + page1, err := repo.ListCursor(ctx, nil, 3, nil, nil, nil) require.NoError(t, err) require.Len(t, page1, 3) // Second page (use last ID from page1 as cursor) lastIDPage1 := page1[len(page1)-1].ID - page2, err := repo.ListCursor(ctx, &lastIDPage1, 3, nil, nil) + page2, err := repo.ListCursor(ctx, &lastIDPage1, 3, nil, nil, nil) require.NoError(t, err) require.Len(t, page2, 3) @@ -410,7 +410,7 @@ func TestPostgreSQLAuditLogRepository_ListCursor_SubsequentPages(t *testing.T) { // Third page (use last ID from page2 as cursor) lastIDPage2 := page2[len(page2)-1].ID - page3, err := repo.ListCursor(ctx, &lastIDPage2, 3, nil, nil) + page3, err := repo.ListCursor(ctx, &lastIDPage2, 3, nil, nil, nil) require.NoError(t, err) require.Len(t, page3, 3) @@ -427,7 +427,7 @@ func TestPostgreSQLAuditLogRepository_ListCursor_EmptyResult(t *testing.T) { ctx := context.Background() // List with no data - logs, err := repo.ListCursor(ctx, nil, 10, nil, nil) + logs, err := repo.ListCursor(ctx, nil, 10, nil, nil, nil) require.NoError(t, err) assert.NotNil(t, logs) assert.Len(t, logs, 0) @@ -462,7 +462,7 @@ func TestPostgreSQLAuditLogRepository_ListCursor_WithFilters(t *testing.T) { // Filter: created_at >= (now - 2 hours) filterFrom := now.Add(-2 * time.Hour) - logs, err := repo.ListCursor(ctx, nil, 10, &filterFrom, nil) + logs, err := repo.ListCursor(ctx, nil, 10, &filterFrom, nil, nil) require.NoError(t, err) assert.LessOrEqual(t, len(logs), 3) // Should get logs from 0, -1, -2 hours @@ -471,3 +471,66 @@ func TestPostgreSQLAuditLogRepository_ListCursor_WithFilters(t *testing.T) { assert.True(t, log.CreatedAt.After(filterFrom) || log.CreatedAt.Equal(filterFrom)) } } + +func TestPostgreSQLAuditLogRepository_ListCursor_ByClientID(t *testing.T) { + db := testutil.SetupPostgresDB(t) + defer testutil.TeardownDB(t, db) + defer testutil.CleanupPostgresDB(t, db) + + repo := NewPostgreSQLAuditLogRepository(db) + ctx := context.Background() + + // Create test clients + clientID1 := testutil.CreateTestClient(t, db, "postgres", "client-1") + clientID2 := testutil.CreateTestClient(t, db, "postgres", "client-2") + + // Create logs for client 1 + for i := 0; i < 3; i++ { + auditLog := &authDomain.AuditLog{ + ID: uuid.Must(uuid.NewV7()), + RequestID: uuid.Must(uuid.NewV7()), + ClientID: clientID1, + Capability: authDomain.ReadCapability, + Path: "/secrets/client1", + CreatedAt: time.Now().UTC(), + } + require.NoError(t, repo.Create(ctx, auditLog)) + time.Sleep(1 * time.Millisecond) + } + + // Create logs for client 2 + for i := 0; i < 2; i++ { + auditLog := &authDomain.AuditLog{ + ID: uuid.Must(uuid.NewV7()), + RequestID: uuid.Must(uuid.NewV7()), + ClientID: clientID2, + Capability: authDomain.ReadCapability, + Path: "/secrets/client2", + CreatedAt: time.Now().UTC(), + } + require.NoError(t, repo.Create(ctx, auditLog)) + time.Sleep(1 * time.Millisecond) + } + + // Filter by client 1 + logs, err := repo.ListCursor(ctx, nil, 10, nil, nil, &clientID1) + require.NoError(t, err) + assert.Len(t, logs, 3) + for _, log := range logs { + assert.Equal(t, clientID1, log.ClientID) + } + + // Filter by client 2 + logs, err = repo.ListCursor(ctx, nil, 10, nil, nil, &clientID2) + require.NoError(t, err) + assert.Len(t, logs, 2) + for _, log := range logs { + assert.Equal(t, clientID2, log.ClientID) + } + + // Filter by non-existent client ID + nonExistentClientID := uuid.New() + logs, err = repo.ListCursor(ctx, nil, 10, nil, nil, &nonExistentClientID) + require.NoError(t, err) + assert.Len(t, logs, 0) +} From c606ac9022979bd44e43c00810b7faf3d5fa0176 Mon Sep 17 00:00:00 2001 From: Allisson Azevedo Date: Sat, 7 Mar 2026 21:12:16 -0300 Subject: [PATCH 04/19] feat(database): add index for client_id on audit_logs table --- migrations/mysql/000007_add_audit_log_client_id_index.down.sql | 1 + migrations/mysql/000007_add_audit_log_client_id_index.up.sql | 1 + .../postgresql/000007_add_audit_log_client_id_index.down.sql | 1 + .../postgresql/000007_add_audit_log_client_id_index.up.sql | 1 + 4 files changed, 4 insertions(+) create mode 100644 migrations/mysql/000007_add_audit_log_client_id_index.down.sql create mode 100644 migrations/mysql/000007_add_audit_log_client_id_index.up.sql create mode 100644 migrations/postgresql/000007_add_audit_log_client_id_index.down.sql create mode 100644 migrations/postgresql/000007_add_audit_log_client_id_index.up.sql diff --git a/migrations/mysql/000007_add_audit_log_client_id_index.down.sql b/migrations/mysql/000007_add_audit_log_client_id_index.down.sql new file mode 100644 index 0000000..6557481 --- /dev/null +++ b/migrations/mysql/000007_add_audit_log_client_id_index.down.sql @@ -0,0 +1 @@ +DROP INDEX IF EXISTS idx_audit_logs_client_id_created_at ON audit_logs; diff --git a/migrations/mysql/000007_add_audit_log_client_id_index.up.sql b/migrations/mysql/000007_add_audit_log_client_id_index.up.sql new file mode 100644 index 0000000..e28a20c --- /dev/null +++ b/migrations/mysql/000007_add_audit_log_client_id_index.up.sql @@ -0,0 +1 @@ +CREATE INDEX idx_audit_logs_client_id_created_at ON audit_logs (client_id, created_at DESC); diff --git a/migrations/postgresql/000007_add_audit_log_client_id_index.down.sql b/migrations/postgresql/000007_add_audit_log_client_id_index.down.sql new file mode 100644 index 0000000..5200052 --- /dev/null +++ b/migrations/postgresql/000007_add_audit_log_client_id_index.down.sql @@ -0,0 +1 @@ +DROP INDEX IF EXISTS idx_audit_logs_client_id_created_at; diff --git a/migrations/postgresql/000007_add_audit_log_client_id_index.up.sql b/migrations/postgresql/000007_add_audit_log_client_id_index.up.sql new file mode 100644 index 0000000..e28a20c --- /dev/null +++ b/migrations/postgresql/000007_add_audit_log_client_id_index.up.sql @@ -0,0 +1 @@ +CREATE INDEX idx_audit_logs_client_id_created_at ON audit_logs (client_id, created_at DESC); From a640ed90e1114446951ec148358016b933244f41 Mon Sep 17 00:00:00 2001 From: Allisson Azevedo Date: Sat, 7 Mar 2026 21:13:30 -0300 Subject: [PATCH 05/19] conductor(checkpoint): Checkpoint end of Phase 1 --- conductor/tracks.md | 2 +- .../plan.md | 16 +++++++++------- 2 files changed, 10 insertions(+), 8 deletions(-) diff --git a/conductor/tracks.md b/conductor/tracks.md index 63dfff7..7d11110 100644 --- a/conductor/tracks.md +++ b/conductor/tracks.md @@ -4,5 +4,5 @@ This file tracks all major tracks for the project. Each track has its own detail --- -- [ ] **Track: Add Audit Log Filtering by Client** +- [~] **Track: Add Audit Log Filtering by Client** *Link: [./tracks/audit_log_filtering_by_client_20260307/](./tracks/audit_log_filtering_by_client_20260307/)* diff --git a/conductor/tracks/audit_log_filtering_by_client_20260307/plan.md b/conductor/tracks/audit_log_filtering_by_client_20260307/plan.md index f39b1ca..f6993e1 100644 --- a/conductor/tracks/audit_log_filtering_by_client_20260307/plan.md +++ b/conductor/tracks/audit_log_filtering_by_client_20260307/plan.md @@ -1,13 +1,15 @@ # Implementation Plan: Add Audit Log Filtering by Client ## Phase 1: Repository Layer Update -- [ ] Task: Update `AuditLogRepository` interface in `internal/auth/usecase/interface.go` to include `clientID *uuid.UUID` in `ListCursor`. -- [ ] Task: Update `PostgreSQLAuditLogRepository` in `internal/auth/repository/postgresql/postgresql_audit_log_repository.go`. - - [ ] Update `ListCursor` to support `client_id` filtering. - - [ ] Update/Add tests in `internal/auth/repository/postgresql/postgresql_audit_log_repository_test.go`. -- [ ] Task: Update `MySQLAuditLogRepository` in `internal/auth/repository/mysql/mysql_audit_log_repository.go`. - - [ ] Update `ListCursor` to support `client_id` filtering. - - [ ] Update/Add tests in `internal/auth/repository/mysql/mysql_audit_log_repository_test.go`. +- [x] Task: Update `AuditLogRepository` interface in `internal/auth/usecase/interface.go` to include `clientID *uuid.UUID` in `ListCursor`. 97bee6d +- [x] Task: Update `PostgreSQLAuditLogRepository` in `internal/auth/repository/postgresql/postgresql_audit_log_repository.go`. 8501e96 + - [x] Update `ListCursor` to support `client_id` filtering. + - [x] Update/Add tests in `internal/auth/repository/postgresql/postgresql_audit_log_repository_test.go`. +- [x] Task: Update `MySQLAuditLogRepository` in `internal/auth/repository/mysql/mysql_audit_log_repository.go`. 8501e96 + - [x] Update `ListCursor` to support `client_id` filtering. + - [x] Update/Add tests in `internal/auth/repository/mysql/mysql_audit_log_repository_test.go`. +- [x] Task: Add database index for `client_id` in `audit_logs` table. c606ac9 + - [x] Create migration `000007_add_audit_log_client_id_index`. - [ ] Task: Conductor - User Manual Verification 'Phase 1' (Protocol in workflow.md) ## Phase 2: Use Case Layer Update From f9e19f2e77b299580cb1bbbaa6dca3902a47b29a Mon Sep 17 00:00:00 2001 From: Allisson Azevedo Date: Sat, 7 Mar 2026 21:13:47 -0300 Subject: [PATCH 06/19] conductor(plan): Mark Phase 1 as complete --- .../tracks/audit_log_filtering_by_client_20260307/plan.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/conductor/tracks/audit_log_filtering_by_client_20260307/plan.md b/conductor/tracks/audit_log_filtering_by_client_20260307/plan.md index f6993e1..e484885 100644 --- a/conductor/tracks/audit_log_filtering_by_client_20260307/plan.md +++ b/conductor/tracks/audit_log_filtering_by_client_20260307/plan.md @@ -1,6 +1,6 @@ # Implementation Plan: Add Audit Log Filtering by Client -## Phase 1: Repository Layer Update +## Phase 1: Repository Layer Update [checkpoint: a640ed9] - [x] Task: Update `AuditLogRepository` interface in `internal/auth/usecase/interface.go` to include `clientID *uuid.UUID` in `ListCursor`. 97bee6d - [x] Task: Update `PostgreSQLAuditLogRepository` in `internal/auth/repository/postgresql/postgresql_audit_log_repository.go`. 8501e96 - [x] Update `ListCursor` to support `client_id` filtering. @@ -10,7 +10,7 @@ - [x] Update/Add tests in `internal/auth/repository/mysql/mysql_audit_log_repository_test.go`. - [x] Task: Add database index for `client_id` in `audit_logs` table. c606ac9 - [x] Create migration `000007_add_audit_log_client_id_index`. -- [ ] Task: Conductor - User Manual Verification 'Phase 1' (Protocol in workflow.md) +- [x] Task: Conductor - User Manual Verification 'Phase 1' (Protocol in workflow.md) a640ed9 ## Phase 2: Use Case Layer Update - [ ] Task: Update `AuditLogUseCase` interface in `internal/auth/usecase/interface.go` to include `clientID *uuid.UUID` in `ListCursor`. From 991c9dd85ea058473889da6555a87c84c5f4b548 Mon Sep 17 00:00:00 2001 From: Allisson Azevedo Date: Sat, 7 Mar 2026 21:16:43 -0300 Subject: [PATCH 07/19] feat(auth): implement clientID filter in AuditLogUseCase and metrics decorator --- internal/auth/usecase/audit_log_usecase.go | 6 +- .../auth/usecase/audit_log_usecase_test.go | 64 ++++++++++++++++++- internal/auth/usecase/metrics_decorator.go | 3 +- .../auth/usecase/metrics_decorator_test.go | 22 +++++++ internal/auth/usecase/mocks/mocks.go | 60 ++++++++++------- 5 files changed, 127 insertions(+), 28 deletions(-) diff --git a/internal/auth/usecase/audit_log_usecase.go b/internal/auth/usecase/audit_log_usecase.go index e3be645..8abb41d 100644 --- a/internal/auth/usecase/audit_log_usecase.go +++ b/internal/auth/usecase/audit_log_usecase.go @@ -82,14 +82,16 @@ func (a *auditLogUseCase) Create( // ListCursor retrieves audit logs ordered by created_at descending (newest first) with cursor-based pagination // and optional time-based filtering. If afterID is provided, returns logs with ID greater than afterID (UUIDv7 ordering). // Accepts createdAtFrom and createdAtTo as optional filters (nil means no filter). Both boundaries are inclusive (>= and <=). +// Accepts clientID as an optional filter (nil means no filter). // All timestamps are expected in UTC. Returns empty slice if no audit logs found. Limit is pre-validated (1-1000). func (a *auditLogUseCase) ListCursor( ctx context.Context, afterID *uuid.UUID, limit int, createdAtFrom, createdAtTo *time.Time, + clientID *uuid.UUID, ) ([]*authDomain.AuditLog, error) { - auditLogs, err := a.auditLogRepo.ListCursor(ctx, afterID, limit, createdAtFrom, createdAtTo) + auditLogs, err := a.auditLogRepo.ListCursor(ctx, afterID, limit, createdAtFrom, createdAtTo, clientID) if err != nil { return nil, apperrors.Wrap(err, "failed to list audit logs with cursor") } @@ -160,7 +162,7 @@ func (a *auditLogUseCase) VerifyBatch( for { // Retrieve logs in time range - logs, err := a.auditLogRepo.ListCursor(ctx, afterID, pageSize, &startTime, &endTime) + logs, err := a.auditLogRepo.ListCursor(ctx, afterID, pageSize, &startTime, &endTime, nil) if err != nil { return nil, apperrors.Wrap(err, "failed to list audit logs") } diff --git a/internal/auth/usecase/audit_log_usecase_test.go b/internal/auth/usecase/audit_log_usecase_test.go index c4f02c1..491b6ee 100644 --- a/internal/auth/usecase/audit_log_usecase_test.go +++ b/internal/auth/usecase/audit_log_usecase_test.go @@ -48,8 +48,9 @@ func (m *mockAuditLogRepository) ListCursor( afterID *uuid.UUID, limit int, createdAtFrom, createdAtTo *time.Time, + clientID *uuid.UUID, ) ([]*authDomain.AuditLog, error) { - args := m.Called(ctx, afterID, limit, createdAtFrom, createdAtTo) + args := m.Called(ctx, afterID, limit, createdAtFrom, createdAtTo, clientID) if args.Get(0) == nil { return nil, args.Error(1) } @@ -452,3 +453,64 @@ func TestAuditLogUseCase_DeleteOlderThan(t *testing.T) { mockRepo.AssertExpectations(t) }) } + +func TestAuditLogUseCase_ListCursor(t *testing.T) { + ctx := context.Background() + + t.Run("Success_ListWithAllFilters", func(t *testing.T) { + mockRepo := &mockAuditLogRepository{} + + afterID := uuid.Must(uuid.NewV7()) + limit := 50 + from := time.Now().Add(-1 * time.Hour) + to := time.Now() + clientID := uuid.Must(uuid.NewV7()) + + expectedLogs := []*authDomain.AuditLog{{ID: uuid.New()}} + + mockRepo.On("ListCursor", ctx, &afterID, limit, &from, &to, &clientID). + Return(expectedLogs, nil). + Once() + + useCase := NewAuditLogUseCase(mockRepo, nil, nil) + + logs, err := useCase.ListCursor(ctx, &afterID, limit, &from, &to, &clientID) + + assert.NoError(t, err) + assert.Equal(t, expectedLogs, logs) + mockRepo.AssertExpectations(t) + }) + + t.Run("Success_ListWithNilFilters", func(t *testing.T) { + mockRepo := &mockAuditLogRepository{} + + mockRepo.On("ListCursor", ctx, (*uuid.UUID)(nil), 10, (*time.Time)(nil), (*time.Time)(nil), (*uuid.UUID)(nil)). + Return([]*authDomain.AuditLog{}, nil). + Once() + + useCase := NewAuditLogUseCase(mockRepo, nil, nil) + + logs, err := useCase.ListCursor(ctx, nil, 10, nil, nil, nil) + + assert.NoError(t, err) + assert.Empty(t, logs) + mockRepo.AssertExpectations(t) + }) + + t.Run("Error_RepositoryFailure", func(t *testing.T) { + mockRepo := &mockAuditLogRepository{} + + mockRepo.On("ListCursor", ctx, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything). + Return(nil, errors.New("db error")). + Once() + + useCase := NewAuditLogUseCase(mockRepo, nil, nil) + + logs, err := useCase.ListCursor(ctx, nil, 10, nil, nil, nil) + + assert.Error(t, err) + assert.Nil(t, logs) + assert.Contains(t, err.Error(), "failed to list audit logs with cursor") + mockRepo.AssertExpectations(t) + }) +} diff --git a/internal/auth/usecase/metrics_decorator.go b/internal/auth/usecase/metrics_decorator.go index cc439be..d11d27c 100644 --- a/internal/auth/usecase/metrics_decorator.go +++ b/internal/auth/usecase/metrics_decorator.go @@ -293,9 +293,10 @@ func (a *auditLogUseCaseWithMetrics) ListCursor( afterID *uuid.UUID, limit int, createdAtFrom, createdAtTo *time.Time, + clientID *uuid.UUID, ) ([]*authDomain.AuditLog, error) { start := time.Now() - logs, err := a.next.ListCursor(ctx, afterID, limit, createdAtFrom, createdAtTo) + logs, err := a.next.ListCursor(ctx, afterID, limit, createdAtFrom, createdAtTo, clientID) status := "success" if err != nil { diff --git a/internal/auth/usecase/metrics_decorator_test.go b/internal/auth/usecase/metrics_decorator_test.go index 8de4239..cc66adf 100644 --- a/internal/auth/usecase/metrics_decorator_test.go +++ b/internal/auth/usecase/metrics_decorator_test.go @@ -124,4 +124,26 @@ func TestAuditLogUseCaseWithMetrics(t *testing.T) { mockNext.AssertExpectations(t) mockMetrics.AssertExpectations(t) }) + + t.Run("List success", func(t *testing.T) { + afterID := uuid.New() + clientID := uuid.New() + from := time.Now().Add(-1 * time.Hour) + to := time.Now() + expectedLogs := []*authDomain.AuditLog{{ID: uuid.New()}} + + mockNext.On("ListCursor", ctx, &afterID, 50, &from, &to, &clientID). + Return(expectedLogs, nil). + Once() + mockMetrics.On("RecordOperation", ctx, "auth", "audit_log_list", "success").Return().Once() + mockMetrics.On("RecordDuration", ctx, "auth", "audit_log_list", mock.AnythingOfType("time.Duration"), "success"). + Return(). + Once() + + res, err := uc.ListCursor(ctx, &afterID, 50, &from, &to, &clientID) + assert.NoError(t, err) + assert.Equal(t, expectedLogs, res) + mockNext.AssertExpectations(t) + mockMetrics.AssertExpectations(t) + }) } diff --git a/internal/auth/usecase/mocks/mocks.go b/internal/auth/usecase/mocks/mocks.go index 80c7ce9..cfea200 100644 --- a/internal/auth/usecase/mocks/mocks.go +++ b/internal/auth/usecase/mocks/mocks.go @@ -1048,8 +1048,8 @@ func (_c *MockAuditLogRepository_Get_Call) RunAndReturn(run func(ctx context.Con } // ListCursor provides a mock function for the type MockAuditLogRepository -func (_mock *MockAuditLogRepository) ListCursor(ctx context.Context, afterID *uuid.UUID, limit int, createdAtFrom *time.Time, createdAtTo *time.Time) ([]*domain.AuditLog, error) { - ret := _mock.Called(ctx, afterID, limit, createdAtFrom, createdAtTo) +func (_mock *MockAuditLogRepository) ListCursor(ctx context.Context, afterID *uuid.UUID, limit int, createdAtFrom *time.Time, createdAtTo *time.Time, clientID *uuid.UUID) ([]*domain.AuditLog, error) { + ret := _mock.Called(ctx, afterID, limit, createdAtFrom, createdAtTo, clientID) if len(ret) == 0 { panic("no return value specified for ListCursor") @@ -1057,18 +1057,18 @@ func (_mock *MockAuditLogRepository) ListCursor(ctx context.Context, afterID *uu var r0 []*domain.AuditLog var r1 error - if returnFunc, ok := ret.Get(0).(func(context.Context, *uuid.UUID, int, *time.Time, *time.Time) ([]*domain.AuditLog, error)); ok { - return returnFunc(ctx, afterID, limit, createdAtFrom, createdAtTo) + if returnFunc, ok := ret.Get(0).(func(context.Context, *uuid.UUID, int, *time.Time, *time.Time, *uuid.UUID) ([]*domain.AuditLog, error)); ok { + return returnFunc(ctx, afterID, limit, createdAtFrom, createdAtTo, clientID) } - if returnFunc, ok := ret.Get(0).(func(context.Context, *uuid.UUID, int, *time.Time, *time.Time) []*domain.AuditLog); ok { - r0 = returnFunc(ctx, afterID, limit, createdAtFrom, createdAtTo) + if returnFunc, ok := ret.Get(0).(func(context.Context, *uuid.UUID, int, *time.Time, *time.Time, *uuid.UUID) []*domain.AuditLog); ok { + r0 = returnFunc(ctx, afterID, limit, createdAtFrom, createdAtTo, clientID) } else { if ret.Get(0) != nil { r0 = ret.Get(0).([]*domain.AuditLog) } } - if returnFunc, ok := ret.Get(1).(func(context.Context, *uuid.UUID, int, *time.Time, *time.Time) error); ok { - r1 = returnFunc(ctx, afterID, limit, createdAtFrom, createdAtTo) + if returnFunc, ok := ret.Get(1).(func(context.Context, *uuid.UUID, int, *time.Time, *time.Time, *uuid.UUID) error); ok { + r1 = returnFunc(ctx, afterID, limit, createdAtFrom, createdAtTo, clientID) } else { r1 = ret.Error(1) } @@ -1086,11 +1086,12 @@ type MockAuditLogRepository_ListCursor_Call struct { // - limit int // - createdAtFrom *time.Time // - createdAtTo *time.Time -func (_e *MockAuditLogRepository_Expecter) ListCursor(ctx interface{}, afterID interface{}, limit interface{}, createdAtFrom interface{}, createdAtTo interface{}) *MockAuditLogRepository_ListCursor_Call { - return &MockAuditLogRepository_ListCursor_Call{Call: _e.mock.On("ListCursor", ctx, afterID, limit, createdAtFrom, createdAtTo)} +// - clientID *uuid.UUID +func (_e *MockAuditLogRepository_Expecter) ListCursor(ctx interface{}, afterID interface{}, limit interface{}, createdAtFrom interface{}, createdAtTo interface{}, clientID interface{}) *MockAuditLogRepository_ListCursor_Call { + return &MockAuditLogRepository_ListCursor_Call{Call: _e.mock.On("ListCursor", ctx, afterID, limit, createdAtFrom, createdAtTo, clientID)} } -func (_c *MockAuditLogRepository_ListCursor_Call) Run(run func(ctx context.Context, afterID *uuid.UUID, limit int, createdAtFrom *time.Time, createdAtTo *time.Time)) *MockAuditLogRepository_ListCursor_Call { +func (_c *MockAuditLogRepository_ListCursor_Call) Run(run func(ctx context.Context, afterID *uuid.UUID, limit int, createdAtFrom *time.Time, createdAtTo *time.Time, clientID *uuid.UUID)) *MockAuditLogRepository_ListCursor_Call { _c.Call.Run(func(args mock.Arguments) { var arg0 context.Context if args[0] != nil { @@ -1112,12 +1113,17 @@ func (_c *MockAuditLogRepository_ListCursor_Call) Run(run func(ctx context.Conte if args[4] != nil { arg4 = args[4].(*time.Time) } + var arg5 *uuid.UUID + if args[5] != nil { + arg5 = args[5].(*uuid.UUID) + } run( arg0, arg1, arg2, arg3, arg4, + arg5, ) }) return _c @@ -1128,7 +1134,7 @@ func (_c *MockAuditLogRepository_ListCursor_Call) Return(auditLogs []*domain.Aud return _c } -func (_c *MockAuditLogRepository_ListCursor_Call) RunAndReturn(run func(ctx context.Context, afterID *uuid.UUID, limit int, createdAtFrom *time.Time, createdAtTo *time.Time) ([]*domain.AuditLog, error)) *MockAuditLogRepository_ListCursor_Call { +func (_c *MockAuditLogRepository_ListCursor_Call) RunAndReturn(run func(ctx context.Context, afterID *uuid.UUID, limit int, createdAtFrom *time.Time, createdAtTo *time.Time, clientID *uuid.UUID) ([]*domain.AuditLog, error)) *MockAuditLogRepository_ListCursor_Call { _c.Call.Return(run) return _c } @@ -2139,8 +2145,8 @@ func (_c *MockAuditLogUseCase_DeleteOlderThan_Call) RunAndReturn(run func(ctx co } // ListCursor provides a mock function for the type MockAuditLogUseCase -func (_mock *MockAuditLogUseCase) ListCursor(ctx context.Context, afterID *uuid.UUID, limit int, createdAtFrom *time.Time, createdAtTo *time.Time) ([]*domain.AuditLog, error) { - ret := _mock.Called(ctx, afterID, limit, createdAtFrom, createdAtTo) +func (_mock *MockAuditLogUseCase) ListCursor(ctx context.Context, afterID *uuid.UUID, limit int, createdAtFrom *time.Time, createdAtTo *time.Time, clientID *uuid.UUID) ([]*domain.AuditLog, error) { + ret := _mock.Called(ctx, afterID, limit, createdAtFrom, createdAtTo, clientID) if len(ret) == 0 { panic("no return value specified for ListCursor") @@ -2148,18 +2154,18 @@ func (_mock *MockAuditLogUseCase) ListCursor(ctx context.Context, afterID *uuid. var r0 []*domain.AuditLog var r1 error - if returnFunc, ok := ret.Get(0).(func(context.Context, *uuid.UUID, int, *time.Time, *time.Time) ([]*domain.AuditLog, error)); ok { - return returnFunc(ctx, afterID, limit, createdAtFrom, createdAtTo) + if returnFunc, ok := ret.Get(0).(func(context.Context, *uuid.UUID, int, *time.Time, *time.Time, *uuid.UUID) ([]*domain.AuditLog, error)); ok { + return returnFunc(ctx, afterID, limit, createdAtFrom, createdAtTo, clientID) } - if returnFunc, ok := ret.Get(0).(func(context.Context, *uuid.UUID, int, *time.Time, *time.Time) []*domain.AuditLog); ok { - r0 = returnFunc(ctx, afterID, limit, createdAtFrom, createdAtTo) + if returnFunc, ok := ret.Get(0).(func(context.Context, *uuid.UUID, int, *time.Time, *time.Time, *uuid.UUID) []*domain.AuditLog); ok { + r0 = returnFunc(ctx, afterID, limit, createdAtFrom, createdAtTo, clientID) } else { if ret.Get(0) != nil { r0 = ret.Get(0).([]*domain.AuditLog) } } - if returnFunc, ok := ret.Get(1).(func(context.Context, *uuid.UUID, int, *time.Time, *time.Time) error); ok { - r1 = returnFunc(ctx, afterID, limit, createdAtFrom, createdAtTo) + if returnFunc, ok := ret.Get(1).(func(context.Context, *uuid.UUID, int, *time.Time, *time.Time, *uuid.UUID) error); ok { + r1 = returnFunc(ctx, afterID, limit, createdAtFrom, createdAtTo, clientID) } else { r1 = ret.Error(1) } @@ -2177,11 +2183,12 @@ type MockAuditLogUseCase_ListCursor_Call struct { // - limit int // - createdAtFrom *time.Time // - createdAtTo *time.Time -func (_e *MockAuditLogUseCase_Expecter) ListCursor(ctx interface{}, afterID interface{}, limit interface{}, createdAtFrom interface{}, createdAtTo interface{}) *MockAuditLogUseCase_ListCursor_Call { - return &MockAuditLogUseCase_ListCursor_Call{Call: _e.mock.On("ListCursor", ctx, afterID, limit, createdAtFrom, createdAtTo)} +// - clientID *uuid.UUID +func (_e *MockAuditLogUseCase_Expecter) ListCursor(ctx interface{}, afterID interface{}, limit interface{}, createdAtFrom interface{}, createdAtTo interface{}, clientID interface{}) *MockAuditLogUseCase_ListCursor_Call { + return &MockAuditLogUseCase_ListCursor_Call{Call: _e.mock.On("ListCursor", ctx, afterID, limit, createdAtFrom, createdAtTo, clientID)} } -func (_c *MockAuditLogUseCase_ListCursor_Call) Run(run func(ctx context.Context, afterID *uuid.UUID, limit int, createdAtFrom *time.Time, createdAtTo *time.Time)) *MockAuditLogUseCase_ListCursor_Call { +func (_c *MockAuditLogUseCase_ListCursor_Call) Run(run func(ctx context.Context, afterID *uuid.UUID, limit int, createdAtFrom *time.Time, createdAtTo *time.Time, clientID *uuid.UUID)) *MockAuditLogUseCase_ListCursor_Call { _c.Call.Run(func(args mock.Arguments) { var arg0 context.Context if args[0] != nil { @@ -2203,12 +2210,17 @@ func (_c *MockAuditLogUseCase_ListCursor_Call) Run(run func(ctx context.Context, if args[4] != nil { arg4 = args[4].(*time.Time) } + var arg5 *uuid.UUID + if args[5] != nil { + arg5 = args[5].(*uuid.UUID) + } run( arg0, arg1, arg2, arg3, arg4, + arg5, ) }) return _c @@ -2219,7 +2231,7 @@ func (_c *MockAuditLogUseCase_ListCursor_Call) Return(auditLogs []*domain.AuditL return _c } -func (_c *MockAuditLogUseCase_ListCursor_Call) RunAndReturn(run func(ctx context.Context, afterID *uuid.UUID, limit int, createdAtFrom *time.Time, createdAtTo *time.Time) ([]*domain.AuditLog, error)) *MockAuditLogUseCase_ListCursor_Call { +func (_c *MockAuditLogUseCase_ListCursor_Call) RunAndReturn(run func(ctx context.Context, afterID *uuid.UUID, limit int, createdAtFrom *time.Time, createdAtTo *time.Time, clientID *uuid.UUID) ([]*domain.AuditLog, error)) *MockAuditLogUseCase_ListCursor_Call { _c.Call.Return(run) return _c } From b9f7b380b39a05bca93a729bc3bf53f35c8696d8 Mon Sep 17 00:00:00 2001 From: Allisson Azevedo Date: Sat, 7 Mar 2026 21:18:32 -0300 Subject: [PATCH 08/19] conductor(checkpoint): Checkpoint end of Phase 2 --- .../audit_log_filtering_by_client_20260307/plan.md | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/conductor/tracks/audit_log_filtering_by_client_20260307/plan.md b/conductor/tracks/audit_log_filtering_by_client_20260307/plan.md index e484885..513d626 100644 --- a/conductor/tracks/audit_log_filtering_by_client_20260307/plan.md +++ b/conductor/tracks/audit_log_filtering_by_client_20260307/plan.md @@ -13,13 +13,13 @@ - [x] Task: Conductor - User Manual Verification 'Phase 1' (Protocol in workflow.md) a640ed9 ## Phase 2: Use Case Layer Update -- [ ] Task: Update `AuditLogUseCase` interface in `internal/auth/usecase/interface.go` to include `clientID *uuid.UUID` in `ListCursor`. -- [ ] Task: Update `auditLogUseCase` in `internal/auth/usecase/audit_log_usecase.go`. - - [ ] Update `ListCursor` to pass `clientID` to the repository. - - [ ] Update/Add tests in `internal/auth/usecase/audit_log_usecase_test.go`. -- [ ] Task: Update `auditLogUseCaseWithMetrics` decorator in `internal/auth/usecase/metrics_decorator.go`. - - [ ] Update `ListCursor` signature and implementation. - - [ ] Update tests in `internal/auth/usecase/metrics_decorator_test.go`. +- [x] Task: Update `AuditLogUseCase` interface in `internal/auth/usecase/interface.go` to include `clientID *uuid.UUID` in `ListCursor`. 97bee6d +- [x] Task: Update `auditLogUseCase` in `internal/auth/usecase/audit_log_usecase.go`. 991c9dd + - [x] Update `ListCursor` to pass `clientID` to the repository. + - [x] Update/Add tests in `internal/auth/usecase/audit_log_usecase_test.go`. +- [x] Task: Update `auditLogUseCaseWithMetrics` decorator in `internal/auth/usecase/metrics_decorator.go`. 991c9dd + - [x] Update `ListCursor` signature and implementation. + - [x] Update tests in `internal/auth/usecase/metrics_decorator_test.go`. - [ ] Task: Conductor - User Manual Verification 'Phase 2' (Protocol in workflow.md) ## Phase 3: HTTP Handler Layer Update From 910b045db3be3e0de0620fc4b6e08954c4af55b5 Mon Sep 17 00:00:00 2001 From: Allisson Azevedo Date: Sat, 7 Mar 2026 21:18:52 -0300 Subject: [PATCH 09/19] conductor(plan): Mark Phase 2 as complete --- .../tracks/audit_log_filtering_by_client_20260307/plan.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/conductor/tracks/audit_log_filtering_by_client_20260307/plan.md b/conductor/tracks/audit_log_filtering_by_client_20260307/plan.md index 513d626..6bbfd89 100644 --- a/conductor/tracks/audit_log_filtering_by_client_20260307/plan.md +++ b/conductor/tracks/audit_log_filtering_by_client_20260307/plan.md @@ -12,7 +12,7 @@ - [x] Create migration `000007_add_audit_log_client_id_index`. - [x] Task: Conductor - User Manual Verification 'Phase 1' (Protocol in workflow.md) a640ed9 -## Phase 2: Use Case Layer Update +## Phase 2: Use Case Layer Update [checkpoint: b9f7b38] - [x] Task: Update `AuditLogUseCase` interface in `internal/auth/usecase/interface.go` to include `clientID *uuid.UUID` in `ListCursor`. 97bee6d - [x] Task: Update `auditLogUseCase` in `internal/auth/usecase/audit_log_usecase.go`. 991c9dd - [x] Update `ListCursor` to pass `clientID` to the repository. @@ -20,7 +20,7 @@ - [x] Task: Update `auditLogUseCaseWithMetrics` decorator in `internal/auth/usecase/metrics_decorator.go`. 991c9dd - [x] Update `ListCursor` signature and implementation. - [x] Update tests in `internal/auth/usecase/metrics_decorator_test.go`. -- [ ] Task: Conductor - User Manual Verification 'Phase 2' (Protocol in workflow.md) +- [x] Task: Conductor - User Manual Verification 'Phase 2' (Protocol in workflow.md) b9f7b38 ## Phase 3: HTTP Handler Layer Update - [ ] Task: Update `AuditLogHandler.ListHandler` in `internal/auth/http/audit_log_handler.go`. From 4ef8ee2ddb7174e3695d6a84ae509bb2ae4bc3c0 Mon Sep 17 00:00:00 2001 From: Allisson Azevedo Date: Sat, 7 Mar 2026 21:21:45 -0300 Subject: [PATCH 10/19] feat(auth): implement client_id filter in AuditLogHandler --- internal/auth/http/audit_log_handler.go | 18 +++- internal/auth/http/audit_log_handler_test.go | 86 +++++++++++++++++--- 2 files changed, 90 insertions(+), 14 deletions(-) diff --git a/internal/auth/http/audit_log_handler.go b/internal/auth/http/audit_log_handler.go index c016902..32b8b23 100644 --- a/internal/auth/http/audit_log_handler.go +++ b/internal/auth/http/audit_log_handler.go @@ -8,6 +8,7 @@ import ( "time" "github.com/gin-gonic/gin" + "github.com/google/uuid" "github.com/allisson/secrets/internal/auth/http/dto" authUseCase "github.com/allisson/secrets/internal/auth/usecase" @@ -32,11 +33,12 @@ func NewAuditLogHandler( } // ListHandler retrieves audit logs with cursor pagination and optional time-based filtering. -// GET /v1/audit-logs?after_id=&limit=50&created_at_from=2026-02-01T00:00:00Z&created_at_to=2026-02-14T23:59:59Z +// GET /v1/audit-logs?after_id=&limit=50&created_at_from=2026-02-01T00:00:00Z&created_at_to=2026-02-14T23:59:59Z&client_id= // Requires ReadCapability on path /v1/audit-logs. Returns 200 OK with paginated audit log list // ordered by created_at descending (newest first). Accepts optional created_at_from and // created_at_to query parameters in RFC3339 format. Timestamps are converted to UTC. Both // boundaries are inclusive (>= and <=). Uses cursor-based pagination with after_id parameter. +// Accepts optional client_id query parameter (UUID format). func (h *AuditLogHandler) ListHandler(c *gin.Context) { // Parse cursor and limit query parameters afterID, limit, err := httputil.ParseUUIDCursorPagination(c, "after_id") @@ -45,6 +47,19 @@ func (h *AuditLogHandler) ListHandler(c *gin.Context) { return } + // Parse optional client_id query parameter + var clientID *uuid.UUID + if clientIDStr := c.Query("client_id"); clientIDStr != "" { + parsed, err := uuid.Parse(clientIDStr) + if err != nil { + httputil.HandleBadRequestGin(c, + fmt.Errorf("invalid client_id format: must be a valid UUID (e.g., 550e8400-e29b-41d4-a716-446655440000)"), + h.logger) + return + } + clientID = &parsed + } + // Parse optional created_at_from query parameter var createdAtFrom *time.Time if fromStr := c.Query("created_at_from"); fromStr != "" { @@ -88,6 +103,7 @@ func (h *AuditLogHandler) ListHandler(c *gin.Context) { limit+1, createdAtFrom, createdAtTo, + clientID, ) if err != nil { httputil.HandleErrorGin(c, err, h.logger) diff --git a/internal/auth/http/audit_log_handler_test.go b/internal/auth/http/audit_log_handler_test.go index 9b77c3b..73cf39b 100644 --- a/internal/auth/http/audit_log_handler_test.go +++ b/internal/auth/http/audit_log_handler_test.go @@ -68,7 +68,7 @@ func TestAuditLogHandler_ListHandler(t *testing.T) { } mockUseCase.EXPECT(). - ListCursor(mock.Anything, (*uuid.UUID)(nil), 51, (*time.Time)(nil), (*time.Time)(nil)). + ListCursor(mock.Anything, (*uuid.UUID)(nil), 51, (*time.Time)(nil), (*time.Time)(nil), (*uuid.UUID)(nil)). Return(expectedAuditLogs, nil). Once() @@ -97,7 +97,7 @@ func TestAuditLogHandler_ListHandler(t *testing.T) { expectedAuditLogs := []*authDomain.AuditLog{} mockUseCase.EXPECT(). - ListCursor(mock.Anything, (*uuid.UUID)(nil), 101, (*time.Time)(nil), (*time.Time)(nil)). + ListCursor(mock.Anything, (*uuid.UUID)(nil), 101, (*time.Time)(nil), (*time.Time)(nil), (*uuid.UUID)(nil)). Return(expectedAuditLogs, nil). Once() @@ -119,7 +119,7 @@ func TestAuditLogHandler_ListHandler(t *testing.T) { expectedAuditLogs := []*authDomain.AuditLog{} mockUseCase.EXPECT(). - ListCursor(mock.Anything, (*uuid.UUID)(nil), 51, (*time.Time)(nil), (*time.Time)(nil)). + ListCursor(mock.Anything, (*uuid.UUID)(nil), 51, (*time.Time)(nil), (*time.Time)(nil), (*uuid.UUID)(nil)). Return(expectedAuditLogs, nil). Once() @@ -140,7 +140,7 @@ func TestAuditLogHandler_ListHandler(t *testing.T) { // Mock expectation - handler will proceed with defaults since offset is not a valid parameter mockUseCase.EXPECT(). - ListCursor(mock.Anything, (*uuid.UUID)(nil), 51, (*time.Time)(nil), (*time.Time)(nil)). + ListCursor(mock.Anything, (*uuid.UUID)(nil), 51, (*time.Time)(nil), (*time.Time)(nil), (*uuid.UUID)(nil)). Return([]*authDomain.AuditLog{}, nil). Once() @@ -156,7 +156,7 @@ func TestAuditLogHandler_ListHandler(t *testing.T) { // Mock expectation - handler will proceed with defaults since offset is not a valid parameter mockUseCase.EXPECT(). - ListCursor(mock.Anything, (*uuid.UUID)(nil), 51, (*time.Time)(nil), (*time.Time)(nil)). + ListCursor(mock.Anything, (*uuid.UUID)(nil), 51, (*time.Time)(nil), (*time.Time)(nil), (*uuid.UUID)(nil)). Return([]*authDomain.AuditLog{}, nil). Once() @@ -187,7 +187,7 @@ func TestAuditLogHandler_ListHandler(t *testing.T) { // Mock usecase to expect clamped limit of 1000 mockUseCase.EXPECT(). - ListCursor(mock.Anything, (*uuid.UUID)(nil), 1001, (*time.Time)(nil), (*time.Time)(nil)). + ListCursor(mock.Anything, (*uuid.UUID)(nil), 1001, (*time.Time)(nil), (*time.Time)(nil), (*uuid.UUID)(nil)). Return([]*authDomain.AuditLog{}, nil). Once() @@ -217,7 +217,7 @@ func TestAuditLogHandler_ListHandler(t *testing.T) { handler, mockUseCase := setupTestAuditLogHandler(t) mockUseCase.EXPECT(). - ListCursor(mock.Anything, (*uuid.UUID)(nil), 51, (*time.Time)(nil), (*time.Time)(nil)). + ListCursor(mock.Anything, (*uuid.UUID)(nil), 51, (*time.Time)(nil), (*time.Time)(nil), (*uuid.UUID)(nil)). Return(nil, errors.New("database error")). Once() @@ -256,7 +256,7 @@ func TestAuditLogHandler_ListHandler(t *testing.T) { } mockUseCase.EXPECT(). - ListCursor(mock.Anything, (*uuid.UUID)(nil), 51, &createdAtFrom, (*time.Time)(nil)). + ListCursor(mock.Anything, (*uuid.UUID)(nil), 51, &createdAtFrom, (*time.Time)(nil), (*uuid.UUID)(nil)). Return(expectedAuditLogs, nil). Once() @@ -300,7 +300,7 @@ func TestAuditLogHandler_ListHandler(t *testing.T) { } mockUseCase.EXPECT(). - ListCursor(mock.Anything, (*uuid.UUID)(nil), 51, (*time.Time)(nil), &createdAtTo). + ListCursor(mock.Anything, (*uuid.UUID)(nil), 51, (*time.Time)(nil), &createdAtTo, (*uuid.UUID)(nil)). Return(expectedAuditLogs, nil). Once() @@ -345,7 +345,7 @@ func TestAuditLogHandler_ListHandler(t *testing.T) { } mockUseCase.EXPECT(). - ListCursor(mock.Anything, (*uuid.UUID)(nil), 51, &createdAtFrom, &createdAtTo). + ListCursor(mock.Anything, (*uuid.UUID)(nil), 51, &createdAtFrom, &createdAtTo, (*uuid.UUID)(nil)). Return(expectedAuditLogs, nil). Once() @@ -381,7 +381,7 @@ func TestAuditLogHandler_ListHandler(t *testing.T) { expectedAuditLogs := []*authDomain.AuditLog{} mockUseCase.EXPECT(). - ListCursor(mock.Anything, (*uuid.UUID)(nil), 51, &createdAtFromUTC, (*time.Time)(nil)). + ListCursor(mock.Anything, (*uuid.UUID)(nil), 51, &createdAtFromUTC, (*time.Time)(nil), (*uuid.UUID)(nil)). Return(expectedAuditLogs, nil). Once() @@ -469,7 +469,7 @@ func TestAuditLogHandler_ListHandler(t *testing.T) { expectedAuditLogs := []*authDomain.AuditLog{} mockUseCase.EXPECT(). - ListCursor(mock.Anything, (*uuid.UUID)(nil), 51, &now, &now). + ListCursor(mock.Anything, (*uuid.UUID)(nil), 51, &now, &now, (*uuid.UUID)(nil)). Return(expectedAuditLogs, nil). Once() @@ -499,7 +499,7 @@ func TestAuditLogHandler_ListHandler(t *testing.T) { expectedAuditLogs := []*authDomain.AuditLog{} mockUseCase.EXPECT(). - ListCursor(mock.Anything, (*uuid.UUID)(nil), 26, &createdAtFrom, (*time.Time)(nil)). + ListCursor(mock.Anything, (*uuid.UUID)(nil), 26, &createdAtFrom, (*time.Time)(nil), (*uuid.UUID)(nil)). Return(expectedAuditLogs, nil). Once() @@ -518,4 +518,64 @@ func TestAuditLogHandler_ListHandler(t *testing.T) { assert.NoError(t, err) assert.Len(t, response.Data, 0) }) + + t.Run("Success_WithClientIDFilter", func(t *testing.T) { + handler, mockUseCase := setupTestAuditLogHandler(t) + + clientID := uuid.New() + id := uuid.Must(uuid.NewV7()) + + expectedAuditLogs := []*authDomain.AuditLog{ + { + ID: id, + RequestID: uuid.Must(uuid.NewV7()), + ClientID: clientID, + Capability: authDomain.ReadCapability, + Path: "/v1/secrets/test", + CreatedAt: time.Now().UTC(), + }, + } + + mockUseCase.EXPECT(). + ListCursor(mock.Anything, (*uuid.UUID)(nil), 51, (*time.Time)(nil), (*time.Time)(nil), &clientID). + Return(expectedAuditLogs, nil). + Once() + + c, w := createTestContext( + http.MethodGet, + "/v1/audit-logs?client_id="+clientID.String(), + nil, + ) + + handler.ListHandler(c) + + assert.Equal(t, http.StatusOK, w.Code) + + var response dto.ListAuditLogsResponse + err := json.Unmarshal(w.Body.Bytes(), &response) + assert.NoError(t, err) + assert.Len(t, response.Data, 1) + assert.Equal(t, id.String(), response.Data[0].ID) + assert.Equal(t, clientID.String(), response.Data[0].ClientID) + }) + + t.Run("Error_InvalidClientIDFormat", func(t *testing.T) { + handler, _ := setupTestAuditLogHandler(t) + + c, w := createTestContext( + http.MethodGet, + "/v1/audit-logs?client_id=invalid-uuid", + nil, + ) + + handler.ListHandler(c) + + assert.Equal(t, http.StatusBadRequest, w.Code) + + var response map[string]interface{} + err := json.Unmarshal(w.Body.Bytes(), &response) + assert.NoError(t, err) + assert.Equal(t, "bad_request", response["error"]) + assert.Contains(t, response["message"], "invalid client_id format") + }) } From c2c5e8a2512e4466195d7397a8d4165ce934fdea Mon Sep 17 00:00:00 2001 From: Allisson Azevedo Date: Sat, 7 Mar 2026 21:31:39 -0300 Subject: [PATCH 11/19] conductor(checkpoint): Checkpoint end of Phase 3 --- .../audit_log_filtering_by_client_20260307/plan.md | 10 +++++----- internal/auth/http/audit_log_handler.go | 10 +++++++--- test/integration/audit_log_signature_test.go | 10 +++++----- 3 files changed, 17 insertions(+), 13 deletions(-) diff --git a/conductor/tracks/audit_log_filtering_by_client_20260307/plan.md b/conductor/tracks/audit_log_filtering_by_client_20260307/plan.md index 6bbfd89..5e72bb7 100644 --- a/conductor/tracks/audit_log_filtering_by_client_20260307/plan.md +++ b/conductor/tracks/audit_log_filtering_by_client_20260307/plan.md @@ -23,11 +23,11 @@ - [x] Task: Conductor - User Manual Verification 'Phase 2' (Protocol in workflow.md) b9f7b38 ## Phase 3: HTTP Handler Layer Update -- [ ] Task: Update `AuditLogHandler.ListHandler` in `internal/auth/http/audit_log_handler.go`. - - [ ] Parse `client_id` query parameter. - - [ ] Validate `client_id` is a valid UUID. - - [ ] Pass `clientID` to the use case. - - [ ] Update/Add tests in `internal/auth/http/audit_log_handler_test.go`. +- [x] Task: Update `AuditLogHandler.ListHandler` in `internal/auth/http/audit_log_handler.go`. 4ef8ee2 + - [x] Parse `client_id` query parameter. + - [x] Validate `client_id` is a valid UUID. + - [x] Pass `clientID` to the use case. + - [x] Update/Add tests in `internal/auth/http/audit_log_handler_test.go`. - [ ] Task: Conductor - User Manual Verification 'Phase 3' (Protocol in workflow.md) ## Phase 4: Documentation and Integration Testing diff --git a/internal/auth/http/audit_log_handler.go b/internal/auth/http/audit_log_handler.go index 32b8b23..02717bb 100644 --- a/internal/auth/http/audit_log_handler.go +++ b/internal/auth/http/audit_log_handler.go @@ -52,9 +52,13 @@ func (h *AuditLogHandler) ListHandler(c *gin.Context) { if clientIDStr := c.Query("client_id"); clientIDStr != "" { parsed, err := uuid.Parse(clientIDStr) if err != nil { - httputil.HandleBadRequestGin(c, - fmt.Errorf("invalid client_id format: must be a valid UUID (e.g., 550e8400-e29b-41d4-a716-446655440000)"), - h.logger) + httputil.HandleBadRequestGin( + c, + fmt.Errorf( + "invalid client_id format: must be a valid UUID (e.g., 550e8400-e29b-41d4-a716-446655440000)", + ), + h.logger, + ) return } clientID = &parsed diff --git a/test/integration/audit_log_signature_test.go b/test/integration/audit_log_signature_test.go index e2e2eb9..8aa62e3 100644 --- a/test/integration/audit_log_signature_test.go +++ b/test/integration/audit_log_signature_test.go @@ -81,7 +81,7 @@ func TestAuditLogSignature_EndToEnd(t *testing.T) { require.NoError(t, err, "failed to create audit log") // Retrieve the created log - logs, err := auditLogUseCase.ListCursor(ctx, nil, 1, nil, nil) + logs, err := auditLogUseCase.ListCursor(ctx, nil, 1, nil, nil, nil) require.NoError(t, err, "failed to list audit logs") require.Len(t, logs, 1, "expected exactly one audit log") @@ -114,7 +114,7 @@ func TestAuditLogSignature_EndToEnd(t *testing.T) { require.NoError(t, err, "failed to create audit log") // Retrieve the log - logs, err := auditLogUseCase.ListCursor(ctx, nil, 1, nil, nil) + logs, err := auditLogUseCase.ListCursor(ctx, nil, 1, nil, nil, nil) require.NoError(t, err, "failed to list audit logs") require.Len(t, logs, 1, "expected exactly one audit log") @@ -168,7 +168,7 @@ func TestAuditLogSignature_EndToEnd(t *testing.T) { require.NoError(t, err) // List audit logs for this client and path - logs, err := auditLogUseCase.ListCursor(ctx, nil, 10, nil, nil) + logs, err := auditLogUseCase.ListCursor(ctx, nil, 10, nil, nil, nil) require.NoError(t, err) var rotationLog *authDomain.AuditLog @@ -249,7 +249,7 @@ func TestAuditLogSignature_EndToEnd(t *testing.T) { // Get the created logs endTime := time.Now().UTC().Add(1 * time.Second) - logs, err := auditLogUseCase.ListCursor(ctx, nil, 3, &startTime, &endTime) + logs, err := auditLogUseCase.ListCursor(ctx, nil, 3, &startTime, &endTime, nil) require.NoError(t, err, "failed to list audit logs") require.Len(t, logs, 3, "expected 3 audit logs") @@ -305,7 +305,7 @@ func TestAuditLogSignature_EndToEnd(t *testing.T) { require.NoError(t, err, "failed to create legacy audit log") // Retrieve the log - logs, err := legacyUseCase.ListCursor(ctx, nil, 1, nil, nil) + logs, err := legacyUseCase.ListCursor(ctx, nil, 1, nil, nil, nil) require.NoError(t, err, "failed to list audit logs") require.Len(t, logs, 1, "expected exactly one audit log") From fbb289ea2b29d87968b30aea59abfb06684b38a1 Mon Sep 17 00:00:00 2001 From: Allisson Azevedo Date: Sat, 7 Mar 2026 21:32:00 -0300 Subject: [PATCH 12/19] conductor(plan): Mark Phase 3 as complete --- .../tracks/audit_log_filtering_by_client_20260307/plan.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/conductor/tracks/audit_log_filtering_by_client_20260307/plan.md b/conductor/tracks/audit_log_filtering_by_client_20260307/plan.md index 5e72bb7..246f444 100644 --- a/conductor/tracks/audit_log_filtering_by_client_20260307/plan.md +++ b/conductor/tracks/audit_log_filtering_by_client_20260307/plan.md @@ -22,13 +22,13 @@ - [x] Update tests in `internal/auth/usecase/metrics_decorator_test.go`. - [x] Task: Conductor - User Manual Verification 'Phase 2' (Protocol in workflow.md) b9f7b38 -## Phase 3: HTTP Handler Layer Update +## Phase 3: HTTP Handler Layer Update [checkpoint: c2c5e8a] - [x] Task: Update `AuditLogHandler.ListHandler` in `internal/auth/http/audit_log_handler.go`. 4ef8ee2 - [x] Parse `client_id` query parameter. - [x] Validate `client_id` is a valid UUID. - [x] Pass `clientID` to the use case. - [x] Update/Add tests in `internal/auth/http/audit_log_handler_test.go`. -- [ ] Task: Conductor - User Manual Verification 'Phase 3' (Protocol in workflow.md) +- [x] Task: Conductor - User Manual Verification 'Phase 3' (Protocol in workflow.md) c2c5e8a ## Phase 4: Documentation and Integration Testing - [ ] Task: Update Documentation. From bf60d39b37c75eed52c1e4f37d8f5bebdef0c7dc Mon Sep 17 00:00:00 2001 From: Allisson Azevedo Date: Sat, 7 Mar 2026 21:34:02 -0300 Subject: [PATCH 13/19] docs(audit): document client_id filter and add integration tests --- docs/observability/audit-logs.md | 1 + docs/openapi.yaml | 6 +++ test/integration/auth_flow_test.go | 76 +++++++++++++++++++++++++++++- 3 files changed, 82 insertions(+), 1 deletion(-) diff --git a/docs/observability/audit-logs.md b/docs/observability/audit-logs.md index e89551e..b55448b 100644 --- a/docs/observability/audit-logs.md +++ b/docs/observability/audit-logs.md @@ -30,6 +30,7 @@ graph TD - `limit` (default 50, max 100) - `created_at_from` (RFC3339) - `created_at_to` (RFC3339) + - `client_id` (UUID filter) ```bash curl "http://localhost:8080/v1/audit-logs?created_at_from=2026-02-27T00:00:00Z&limit=20" diff --git a/docs/openapi.yaml b/docs/openapi.yaml index 48f8454..0fb7d79 100644 --- a/docs/openapi.yaml +++ b/docs/openapi.yaml @@ -1031,6 +1031,12 @@ paths: schema: type: string format: date-time + - name: client_id + in: query + description: Filter by specific client ID (UUID format) + schema: + type: string + format: uuid responses: "200": description: Audit logs list diff --git a/test/integration/auth_flow_test.go b/test/integration/auth_flow_test.go index ce217e2..e57d235 100644 --- a/test/integration/auth_flow_test.go +++ b/test/integration/auth_flow_test.go @@ -374,7 +374,81 @@ func TestIntegration_Auth_CompleteFlow(t *testing.T) { assert.NotEmpty(t, newTokenResponse.Token) }) - t.Logf("All 11 auth endpoint tests passed for %s", tc.dbDriver) + // [12/12] Test GET /v1/audit-logs - Filter by client ID + t.Run("12_AuditLogFilteringByClient", func(t *testing.T) { + // Create another client to have logs for a different client + clientRequest := authDTO.CreateClientRequest{ + Name: "Another Client", + IsActive: true, + Policies: []authDomain.PolicyDocument{{Path: "*", Capabilities: []authDomain.Capability{authDomain.ReadCapability}}}, + } + resp, body := ctx.makeRequest(t, http.MethodPost, "/v1/clients", clientRequest, true) + require.Equal(t, http.StatusCreated, resp.StatusCode) + + var clientResponse authDTO.CreateClientResponse + err := json.Unmarshal(body, &clientResponse) + require.NoError(t, err) + anotherClientID := clientResponse.ID + anotherClientSecret := clientResponse.Secret + + // Issue token for another client + issueRequest := authDTO.IssueTokenRequest{ + ClientID: anotherClientID, + ClientSecret: anotherClientSecret, + } + resp, body = ctx.makeRequest(t, http.MethodPost, "/v1/token", issueRequest, false) + require.Equal(t, http.StatusCreated, resp.StatusCode) + var tokenResponse authDTO.IssueTokenResponse + json.Unmarshal(body, &tokenResponse) + anotherToken := tokenResponse.Token + + // Perform an action with another client (GET self) + req, _ := http.NewRequest(http.MethodGet, ctx.server.URL+"/v1/clients/"+anotherClientID, nil) + req.Header.Set("Authorization", "Bearer "+anotherToken) + resp, err = http.DefaultClient.Do(req) + require.NoError(t, err) + assert.Equal(t, http.StatusOK, resp.StatusCode) + + // List audit logs filtered by the another client (using root token) + resp, body = ctx.makeRequest( + t, + http.MethodGet, + "/v1/audit-logs?client_id="+anotherClientID, + nil, + true, + ) + assert.Equal(t, http.StatusOK, resp.StatusCode) + + var response authDTO.ListAuditLogsResponse + err = json.Unmarshal(body, &response) + require.NoError(t, err) + assert.NotEmpty(t, response.Data) + + // All returned logs should belong to anotherClientID + for _, log := range response.Data { + assert.Equal(t, anotherClientID, log.ClientID) + } + + // List audit logs filtered by root client + resp, body = ctx.makeRequest( + t, + http.MethodGet, + "/v1/audit-logs?client_id="+ctx.rootClient.ID.String(), + nil, + true, + ) + assert.Equal(t, http.StatusOK, resp.StatusCode) + + err = json.Unmarshal(body, &response) + require.NoError(t, err) + assert.NotEmpty(t, response.Data) + + for _, log := range response.Data { + assert.Equal(t, ctx.rootClient.ID.String(), log.ClientID) + } + }) + + t.Logf("All 12 auth endpoint tests passed for %s", tc.dbDriver) }) } } From def8bbe664e1b7c394422e63777652ee808d9c8d Mon Sep 17 00:00:00 2001 From: Allisson Azevedo Date: Sat, 7 Mar 2026 21:45:43 -0300 Subject: [PATCH 14/19] conductor(checkpoint): Checkpoint end of Phase 4 --- .../audit_log_filtering_by_client_20260307/plan.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/conductor/tracks/audit_log_filtering_by_client_20260307/plan.md b/conductor/tracks/audit_log_filtering_by_client_20260307/plan.md index 246f444..5b1c482 100644 --- a/conductor/tracks/audit_log_filtering_by_client_20260307/plan.md +++ b/conductor/tracks/audit_log_filtering_by_client_20260307/plan.md @@ -31,9 +31,9 @@ - [x] Task: Conductor - User Manual Verification 'Phase 3' (Protocol in workflow.md) c2c5e8a ## Phase 4: Documentation and Integration Testing -- [ ] Task: Update Documentation. - - [ ] Document `client_id` filter in `docs/observability/audit-logs.md`. - - [ ] Update `docs/openapi.yaml` with the new query parameter. -- [ ] Task: Update Integration Tests. - - [ ] Add audit log filtering test case in `test/integration/auth_flow_test.go`. +- [x] Task: Update Documentation. bf60d39 + - [x] Document `client_id` filter in `docs/observability/audit-logs.md`. + - [x] Update `docs/openapi.yaml` with the new query parameter. +- [x] Task: Update Integration Tests. bf60d39 + - [x] Add audit log filtering test case in `test/integration/auth_flow_test.go`. - [ ] Task: Conductor - User Manual Verification 'Phase 4' (Protocol in workflow.md) From 133e75580a92ce34ff5a643013c918838ed51596 Mon Sep 17 00:00:00 2001 From: Allisson Azevedo Date: Sat, 7 Mar 2026 21:46:09 -0300 Subject: [PATCH 15/19] conductor(plan): Mark Phase 4 as complete --- .../tracks/audit_log_filtering_by_client_20260307/plan.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/conductor/tracks/audit_log_filtering_by_client_20260307/plan.md b/conductor/tracks/audit_log_filtering_by_client_20260307/plan.md index 5b1c482..7dec903 100644 --- a/conductor/tracks/audit_log_filtering_by_client_20260307/plan.md +++ b/conductor/tracks/audit_log_filtering_by_client_20260307/plan.md @@ -30,10 +30,10 @@ - [x] Update/Add tests in `internal/auth/http/audit_log_handler_test.go`. - [x] Task: Conductor - User Manual Verification 'Phase 3' (Protocol in workflow.md) c2c5e8a -## Phase 4: Documentation and Integration Testing +## Phase 4: Documentation and Integration Testing [checkpoint: def8bbe] - [x] Task: Update Documentation. bf60d39 - [x] Document `client_id` filter in `docs/observability/audit-logs.md`. - [x] Update `docs/openapi.yaml` with the new query parameter. - [x] Task: Update Integration Tests. bf60d39 - [x] Add audit log filtering test case in `test/integration/auth_flow_test.go`. -- [ ] Task: Conductor - User Manual Verification 'Phase 4' (Protocol in workflow.md) +- [x] Task: Conductor - User Manual Verification 'Phase 4' (Protocol in workflow.md) def8bbe From d51a8303effb8f4cb239df01e06ea09d3a5294ec Mon Sep 17 00:00:00 2001 From: Allisson Azevedo Date: Sat, 7 Mar 2026 21:46:22 -0300 Subject: [PATCH 16/19] chore(conductor): Mark track 'Add Audit Log Filtering by Client' as complete --- conductor/tracks.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/conductor/tracks.md b/conductor/tracks.md index 7d11110..6161b0f 100644 --- a/conductor/tracks.md +++ b/conductor/tracks.md @@ -4,5 +4,5 @@ This file tracks all major tracks for the project. Each track has its own detail --- -- [~] **Track: Add Audit Log Filtering by Client** +- [x] **Track: Add Audit Log Filtering by Client** *Link: [./tracks/audit_log_filtering_by_client_20260307/](./tracks/audit_log_filtering_by_client_20260307/)* From a6415fc789e0458ece10567f0c0533fbf78e9e2f Mon Sep 17 00:00:00 2001 From: Allisson Azevedo Date: Sat, 7 Mar 2026 21:46:51 -0300 Subject: [PATCH 17/19] docs(conductor): Synchronize docs for track 'Add Audit Log Filtering by Client' --- conductor/product.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/conductor/product.md b/conductor/product.md index 1d4836b..1acc6b4 100644 --- a/conductor/product.md +++ b/conductor/product.md @@ -17,7 +17,7 @@ To provide a secure, developer-friendly, and lightweight secrets management plat - **Tokenization Engine:** Format-preserving tokens for sensitive data types like credit card numbers. - **Auth Token Revocation:** Immediate invalidation of authentication tokens (single or client-wide) with full state management. - **Client Secret Rotation:** Self-service and administrative rotation of client secrets with automatic auth token revocation. -- **Audit Logs:** HMAC-signed audit trails capturing every access attempt and policy evaluation. +- **Audit Logs:** HMAC-signed audit trails capturing every access attempt and policy evaluation, with support for advanced filtering by client and date range. - **KMS Integration:** Native support for AWS KMS, Google Cloud KMS, Azure Key Vault, and HashiCorp Vault. ## Strategic Priorities From b8ae7a51e70b886f7c07ca7f9f6ff21fa4628eb2 Mon Sep 17 00:00:00 2001 From: Allisson Azevedo Date: Sat, 7 Mar 2026 21:47:52 -0300 Subject: [PATCH 18/19] chore(conductor): Archive track 'Add Audit Log Filtering by Client' --- .../index.md | 5 +++ .../metadata.json | 8 ++++ .../plan.md | 39 +++++++++++++++++++ .../spec.md | 36 +++++++++++++++++ conductor/tracks.md | 5 --- 5 files changed, 88 insertions(+), 5 deletions(-) create mode 100644 conductor/archive/audit_log_filtering_by_client_20260307/index.md create mode 100644 conductor/archive/audit_log_filtering_by_client_20260307/metadata.json create mode 100644 conductor/archive/audit_log_filtering_by_client_20260307/plan.md create mode 100644 conductor/archive/audit_log_filtering_by_client_20260307/spec.md diff --git a/conductor/archive/audit_log_filtering_by_client_20260307/index.md b/conductor/archive/audit_log_filtering_by_client_20260307/index.md new file mode 100644 index 0000000..3ed9226 --- /dev/null +++ b/conductor/archive/audit_log_filtering_by_client_20260307/index.md @@ -0,0 +1,5 @@ +# Track audit_log_filtering_by_client_20260307 Context + +- [Specification](./spec.md) +- [Implementation Plan](./plan.md) +- [Metadata](./metadata.json) diff --git a/conductor/archive/audit_log_filtering_by_client_20260307/metadata.json b/conductor/archive/audit_log_filtering_by_client_20260307/metadata.json new file mode 100644 index 0000000..464c419 --- /dev/null +++ b/conductor/archive/audit_log_filtering_by_client_20260307/metadata.json @@ -0,0 +1,8 @@ +{ + "track_id": "audit_log_filtering_by_client_20260307", + "type": "feature", + "status": "new", + "created_at": "2026-03-07T12:00:00Z", + "updated_at": "2026-03-07T12:00:00Z", + "description": "Add Audit Log Filtering by Client" +} diff --git a/conductor/archive/audit_log_filtering_by_client_20260307/plan.md b/conductor/archive/audit_log_filtering_by_client_20260307/plan.md new file mode 100644 index 0000000..7dec903 --- /dev/null +++ b/conductor/archive/audit_log_filtering_by_client_20260307/plan.md @@ -0,0 +1,39 @@ +# Implementation Plan: Add Audit Log Filtering by Client + +## Phase 1: Repository Layer Update [checkpoint: a640ed9] +- [x] Task: Update `AuditLogRepository` interface in `internal/auth/usecase/interface.go` to include `clientID *uuid.UUID` in `ListCursor`. 97bee6d +- [x] Task: Update `PostgreSQLAuditLogRepository` in `internal/auth/repository/postgresql/postgresql_audit_log_repository.go`. 8501e96 + - [x] Update `ListCursor` to support `client_id` filtering. + - [x] Update/Add tests in `internal/auth/repository/postgresql/postgresql_audit_log_repository_test.go`. +- [x] Task: Update `MySQLAuditLogRepository` in `internal/auth/repository/mysql/mysql_audit_log_repository.go`. 8501e96 + - [x] Update `ListCursor` to support `client_id` filtering. + - [x] Update/Add tests in `internal/auth/repository/mysql/mysql_audit_log_repository_test.go`. +- [x] Task: Add database index for `client_id` in `audit_logs` table. c606ac9 + - [x] Create migration `000007_add_audit_log_client_id_index`. +- [x] Task: Conductor - User Manual Verification 'Phase 1' (Protocol in workflow.md) a640ed9 + +## Phase 2: Use Case Layer Update [checkpoint: b9f7b38] +- [x] Task: Update `AuditLogUseCase` interface in `internal/auth/usecase/interface.go` to include `clientID *uuid.UUID` in `ListCursor`. 97bee6d +- [x] Task: Update `auditLogUseCase` in `internal/auth/usecase/audit_log_usecase.go`. 991c9dd + - [x] Update `ListCursor` to pass `clientID` to the repository. + - [x] Update/Add tests in `internal/auth/usecase/audit_log_usecase_test.go`. +- [x] Task: Update `auditLogUseCaseWithMetrics` decorator in `internal/auth/usecase/metrics_decorator.go`. 991c9dd + - [x] Update `ListCursor` signature and implementation. + - [x] Update tests in `internal/auth/usecase/metrics_decorator_test.go`. +- [x] Task: Conductor - User Manual Verification 'Phase 2' (Protocol in workflow.md) b9f7b38 + +## Phase 3: HTTP Handler Layer Update [checkpoint: c2c5e8a] +- [x] Task: Update `AuditLogHandler.ListHandler` in `internal/auth/http/audit_log_handler.go`. 4ef8ee2 + - [x] Parse `client_id` query parameter. + - [x] Validate `client_id` is a valid UUID. + - [x] Pass `clientID` to the use case. + - [x] Update/Add tests in `internal/auth/http/audit_log_handler_test.go`. +- [x] Task: Conductor - User Manual Verification 'Phase 3' (Protocol in workflow.md) c2c5e8a + +## Phase 4: Documentation and Integration Testing [checkpoint: def8bbe] +- [x] Task: Update Documentation. bf60d39 + - [x] Document `client_id` filter in `docs/observability/audit-logs.md`. + - [x] Update `docs/openapi.yaml` with the new query parameter. +- [x] Task: Update Integration Tests. bf60d39 + - [x] Add audit log filtering test case in `test/integration/auth_flow_test.go`. +- [x] Task: Conductor - User Manual Verification 'Phase 4' (Protocol in workflow.md) def8bbe diff --git a/conductor/archive/audit_log_filtering_by_client_20260307/spec.md b/conductor/archive/audit_log_filtering_by_client_20260307/spec.md new file mode 100644 index 0000000..5122c5f --- /dev/null +++ b/conductor/archive/audit_log_filtering_by_client_20260307/spec.md @@ -0,0 +1,36 @@ +# Specification: Add Audit Log Filtering by Client + +## Overview +Currently, audit logs can be retrieved and filtered by date range. This track adds the ability to filter audit logs by a specific Client ID (UUID) via the API. + +## Functional Requirements +- **API Filtering:** The `GET /v1/audit-logs` endpoint must support an optional `client_id` query parameter. +- **Repository Support:** The `AuditLogRepository` must implement filtering by `client_id` in its `ListCursor` method for both PostgreSQL and MySQL implementations. +- **UseCase Support:** The `AuditLogUseCase` must pass the `client_id` filter from the handler to the repository. +- **Validation:** The `client_id` provided in the query parameter must be a valid UUID. +- **Empty Results:** If no audit logs match the specified `client_id`, the API should return an empty list with a `200 OK` status. +- **Documentation:** + - Update `docs/observability/audit-logs.md` to document the new `client_id` filter. + - Update `docs/openapi.yaml` to include the `client_id` query parameter for the audit logs list endpoint. +- **Integration Tests:** + - Update `test/integration/auth_flow_test.go` to include a test case for filtering audit logs by Client ID. + +## Non-Functional Requirements +- **Performance:** Ensure that the database query for filtering by `client_id` is performant. +- **Consistency:** Maintain existing cursor-based pagination and date filtering logic. + +## Acceptance Criteria +- [ ] `GET /v1/audit-logs?client_id=` returns only logs belonging to that client. +- [ ] Providing an invalid UUID for `client_id` returns a `400 Bad Request` error. +- [ ] If `client_id` is omitted, the API continues to return logs for all clients (existing behavior). +- [ ] Filtering by `client_id` works correctly in combination with `created_at_from` and `created_at_to` filters. +- [ ] Filtering by `client_id` works correctly with cursor-based pagination (`after_id`). +- [ ] `docs/observability/audit-logs.md` correctly reflects the new filtering capability. +- [ ] `docs/openapi.yaml` includes the new `client_id` query parameter. +- [ ] Integration tests in `test/integration/auth_flow_test.go` pass and verify the new filtering behavior. +- [ ] PostgreSQL implementation is verified with integration tests. +- [ ] MySQL implementation is verified with integration tests. + +## Out of Scope +- Filtering by Client Name. +- Adding filtering to the CLI `audit-log list` command. diff --git a/conductor/tracks.md b/conductor/tracks.md index 6161b0f..22d3d64 100644 --- a/conductor/tracks.md +++ b/conductor/tracks.md @@ -1,8 +1,3 @@ # Project Tracks This file tracks all major tracks for the project. Each track has its own detailed plan in its respective folder. - ---- - -- [x] **Track: Add Audit Log Filtering by Client** -*Link: [./tracks/audit_log_filtering_by_client_20260307/](./tracks/audit_log_filtering_by_client_20260307/)* From 7bbb2f9142fe5d65621fc6c7112cff36c1e3265c Mon Sep 17 00:00:00 2001 From: Allisson Azevedo Date: Mon, 9 Mar 2026 07:12:44 -0300 Subject: [PATCH 19/19] feat(auth): implement audit log filtering by client_id Added the ability to filter audit logs by a specific client ID via the API, including database optimizations and full-stack support. Key changes: - API: Added optional client_id query parameter to the audit logs list endpoint. - Logic: Updated Repository and Use Case layers to support clientID filtering. - Database: Created migrations to add an index on client_id in the audit_logs table for PostgreSQL and MySQL. - Observability: Updated the metrics decorator to include the new filter parameter. - Testing: Added unit tests for all layers and a new integration test case in auth_flow_test.go. - Documentation: Updated OpenAPI specifications and audit log reference guides. --- .../index.md | 5 --- .../metadata.json | 8 ---- .../plan.md | 39 ------------------- .../spec.md | 36 ----------------- 4 files changed, 88 deletions(-) delete mode 100644 conductor/tracks/audit_log_filtering_by_client_20260307/index.md delete mode 100644 conductor/tracks/audit_log_filtering_by_client_20260307/metadata.json delete mode 100644 conductor/tracks/audit_log_filtering_by_client_20260307/plan.md delete mode 100644 conductor/tracks/audit_log_filtering_by_client_20260307/spec.md diff --git a/conductor/tracks/audit_log_filtering_by_client_20260307/index.md b/conductor/tracks/audit_log_filtering_by_client_20260307/index.md deleted file mode 100644 index 3ed9226..0000000 --- a/conductor/tracks/audit_log_filtering_by_client_20260307/index.md +++ /dev/null @@ -1,5 +0,0 @@ -# Track audit_log_filtering_by_client_20260307 Context - -- [Specification](./spec.md) -- [Implementation Plan](./plan.md) -- [Metadata](./metadata.json) diff --git a/conductor/tracks/audit_log_filtering_by_client_20260307/metadata.json b/conductor/tracks/audit_log_filtering_by_client_20260307/metadata.json deleted file mode 100644 index 464c419..0000000 --- a/conductor/tracks/audit_log_filtering_by_client_20260307/metadata.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "track_id": "audit_log_filtering_by_client_20260307", - "type": "feature", - "status": "new", - "created_at": "2026-03-07T12:00:00Z", - "updated_at": "2026-03-07T12:00:00Z", - "description": "Add Audit Log Filtering by Client" -} diff --git a/conductor/tracks/audit_log_filtering_by_client_20260307/plan.md b/conductor/tracks/audit_log_filtering_by_client_20260307/plan.md deleted file mode 100644 index 7dec903..0000000 --- a/conductor/tracks/audit_log_filtering_by_client_20260307/plan.md +++ /dev/null @@ -1,39 +0,0 @@ -# Implementation Plan: Add Audit Log Filtering by Client - -## Phase 1: Repository Layer Update [checkpoint: a640ed9] -- [x] Task: Update `AuditLogRepository` interface in `internal/auth/usecase/interface.go` to include `clientID *uuid.UUID` in `ListCursor`. 97bee6d -- [x] Task: Update `PostgreSQLAuditLogRepository` in `internal/auth/repository/postgresql/postgresql_audit_log_repository.go`. 8501e96 - - [x] Update `ListCursor` to support `client_id` filtering. - - [x] Update/Add tests in `internal/auth/repository/postgresql/postgresql_audit_log_repository_test.go`. -- [x] Task: Update `MySQLAuditLogRepository` in `internal/auth/repository/mysql/mysql_audit_log_repository.go`. 8501e96 - - [x] Update `ListCursor` to support `client_id` filtering. - - [x] Update/Add tests in `internal/auth/repository/mysql/mysql_audit_log_repository_test.go`. -- [x] Task: Add database index for `client_id` in `audit_logs` table. c606ac9 - - [x] Create migration `000007_add_audit_log_client_id_index`. -- [x] Task: Conductor - User Manual Verification 'Phase 1' (Protocol in workflow.md) a640ed9 - -## Phase 2: Use Case Layer Update [checkpoint: b9f7b38] -- [x] Task: Update `AuditLogUseCase` interface in `internal/auth/usecase/interface.go` to include `clientID *uuid.UUID` in `ListCursor`. 97bee6d -- [x] Task: Update `auditLogUseCase` in `internal/auth/usecase/audit_log_usecase.go`. 991c9dd - - [x] Update `ListCursor` to pass `clientID` to the repository. - - [x] Update/Add tests in `internal/auth/usecase/audit_log_usecase_test.go`. -- [x] Task: Update `auditLogUseCaseWithMetrics` decorator in `internal/auth/usecase/metrics_decorator.go`. 991c9dd - - [x] Update `ListCursor` signature and implementation. - - [x] Update tests in `internal/auth/usecase/metrics_decorator_test.go`. -- [x] Task: Conductor - User Manual Verification 'Phase 2' (Protocol in workflow.md) b9f7b38 - -## Phase 3: HTTP Handler Layer Update [checkpoint: c2c5e8a] -- [x] Task: Update `AuditLogHandler.ListHandler` in `internal/auth/http/audit_log_handler.go`. 4ef8ee2 - - [x] Parse `client_id` query parameter. - - [x] Validate `client_id` is a valid UUID. - - [x] Pass `clientID` to the use case. - - [x] Update/Add tests in `internal/auth/http/audit_log_handler_test.go`. -- [x] Task: Conductor - User Manual Verification 'Phase 3' (Protocol in workflow.md) c2c5e8a - -## Phase 4: Documentation and Integration Testing [checkpoint: def8bbe] -- [x] Task: Update Documentation. bf60d39 - - [x] Document `client_id` filter in `docs/observability/audit-logs.md`. - - [x] Update `docs/openapi.yaml` with the new query parameter. -- [x] Task: Update Integration Tests. bf60d39 - - [x] Add audit log filtering test case in `test/integration/auth_flow_test.go`. -- [x] Task: Conductor - User Manual Verification 'Phase 4' (Protocol in workflow.md) def8bbe diff --git a/conductor/tracks/audit_log_filtering_by_client_20260307/spec.md b/conductor/tracks/audit_log_filtering_by_client_20260307/spec.md deleted file mode 100644 index 5122c5f..0000000 --- a/conductor/tracks/audit_log_filtering_by_client_20260307/spec.md +++ /dev/null @@ -1,36 +0,0 @@ -# Specification: Add Audit Log Filtering by Client - -## Overview -Currently, audit logs can be retrieved and filtered by date range. This track adds the ability to filter audit logs by a specific Client ID (UUID) via the API. - -## Functional Requirements -- **API Filtering:** The `GET /v1/audit-logs` endpoint must support an optional `client_id` query parameter. -- **Repository Support:** The `AuditLogRepository` must implement filtering by `client_id` in its `ListCursor` method for both PostgreSQL and MySQL implementations. -- **UseCase Support:** The `AuditLogUseCase` must pass the `client_id` filter from the handler to the repository. -- **Validation:** The `client_id` provided in the query parameter must be a valid UUID. -- **Empty Results:** If no audit logs match the specified `client_id`, the API should return an empty list with a `200 OK` status. -- **Documentation:** - - Update `docs/observability/audit-logs.md` to document the new `client_id` filter. - - Update `docs/openapi.yaml` to include the `client_id` query parameter for the audit logs list endpoint. -- **Integration Tests:** - - Update `test/integration/auth_flow_test.go` to include a test case for filtering audit logs by Client ID. - -## Non-Functional Requirements -- **Performance:** Ensure that the database query for filtering by `client_id` is performant. -- **Consistency:** Maintain existing cursor-based pagination and date filtering logic. - -## Acceptance Criteria -- [ ] `GET /v1/audit-logs?client_id=` returns only logs belonging to that client. -- [ ] Providing an invalid UUID for `client_id` returns a `400 Bad Request` error. -- [ ] If `client_id` is omitted, the API continues to return logs for all clients (existing behavior). -- [ ] Filtering by `client_id` works correctly in combination with `created_at_from` and `created_at_to` filters. -- [ ] Filtering by `client_id` works correctly with cursor-based pagination (`after_id`). -- [ ] `docs/observability/audit-logs.md` correctly reflects the new filtering capability. -- [ ] `docs/openapi.yaml` includes the new `client_id` query parameter. -- [ ] Integration tests in `test/integration/auth_flow_test.go` pass and verify the new filtering behavior. -- [ ] PostgreSQL implementation is verified with integration tests. -- [ ] MySQL implementation is verified with integration tests. - -## Out of Scope -- Filtering by Client Name. -- Adding filtering to the CLI `audit-log list` command.