diff --git a/backend/internal/handler/dev_handler.go b/backend/internal/handler/dev_handler.go index ff67a0a7..fbbc67e3 100644 --- a/backend/internal/handler/dev_handler.go +++ b/backend/internal/handler/dev_handler.go @@ -437,7 +437,7 @@ func (h *DevHandler) auditClientIDsByPermit(c *fiber.Ctx, profile *domain.UserPr clientFilter = strings.TrimSpace(clientFilter) if clientFilter != "" { summary, err := h.loadClientSummary(c.Context(), clientFilter) - if err == nil && h.canOperateClientByPermit(c, profile, summary, "view_audit_logs") { + if err == nil && h.canOperateClientByPermit(c, profile, summary, "audit_viewer") { ids[summary.ID] = struct{}{} } return ids @@ -453,7 +453,7 @@ func (h *DevHandler) auditClientIDsByPermit(c *fiber.Ctx, profile *domain.UserPr continue } summary := h.mapClientSummary(client) - if h.canOperateClientByPermit(c, profile, summary, "view_audit_logs") { + if h.canOperateClientByPermit(c, profile, summary, "audit_viewer") { ids[summary.ID] = struct{}{} } } @@ -2243,6 +2243,9 @@ func (h *DevHandler) ListAuditLogs(c *fiber.Ctx) error { if tenantFilter == "" { tenantFilter = h.resolveDevTenantScope(c) } + if role != domain.RoleSuperAdmin && len(allowedClientIDs) > 0 { + tenantFilter = "" + } if role != domain.RoleSuperAdmin && tenantFilter == "" && len(allowedClientIDs) == 0 { tenantFilter = tenantIDFromProfile(profile) } @@ -2641,6 +2644,34 @@ func normalizeAuditAction(eventType string, details map[string]any) string { } } +func devAuditClientIDFromEventType(eventType string) string { + parts := strings.Split(strings.TrimSpace(eventType), " ") + if len(parts) != 2 { + return "" + } + path := strings.Trim(parts[1], "/") + segments := strings.Split(path, "/") + for idx := 0; idx+1 < len(segments); idx++ { + if segments[idx] == "clients" { + return strings.TrimSpace(segments[idx+1]) + } + } + return "" +} + +func resolveDevAuditClientID(logItem domain.AuditLog, details map[string]any) string { + targetID, _ := details["target_id"].(string) + clientID, _ := details["client_id"].(string) + resolvedID := strings.TrimSpace(targetID) + if resolvedID == "" { + resolvedID = strings.TrimSpace(clientID) + } + if resolvedID == "" { + resolvedID = devAuditClientIDFromEventType(logItem.EventType) + } + return resolvedID +} + func resolveStatusFromMetadata(metadata map[string]interface{}) string { if metadata != nil { if value, ok := metadata["status"].(string); ok && strings.ToLower(strings.TrimSpace(value)) == "inactive" { @@ -2676,19 +2707,12 @@ func (h *DevHandler) matchesDevAuditFilter( } } if clientFilter != "" { - targetID, _ := details["target_id"].(string) - clientID, _ := details["client_id"].(string) - if strings.TrimSpace(targetID) != clientFilter && strings.TrimSpace(clientID) != clientFilter { + if resolveDevAuditClientID(logItem, details) != clientFilter { return false } } if len(allowedClientIDs) > 0 { - targetID, _ := details["target_id"].(string) - clientID, _ := details["client_id"].(string) - resolvedID := strings.TrimSpace(targetID) - if resolvedID == "" { - resolvedID = strings.TrimSpace(clientID) - } + resolvedID := resolveDevAuditClientID(logItem, details) if resolvedID == "" { return false } diff --git a/backend/internal/handler/dev_handler_test.go b/backend/internal/handler/dev_handler_test.go index 2bee527d..213d26c7 100644 --- a/backend/internal/handler/dev_handler_test.go +++ b/backend/internal/handler/dev_handler_test.go @@ -1604,6 +1604,13 @@ func TestListAuditLogs_UserAllowedByRPAuditPermission(t *testing.T) { Timestamp: time.Now().UTC(), Details: `{"target_id":"client-allowed","tenant_id":"tenant-a","action":"ROTATE_SECRET"}`, }, + { + EventID: "evt-allowed-path", + EventType: "GET /api/v1/dev/clients/client-allowed/relations", + Status: "success", + Timestamp: time.Now().UTC().Add(-30 * time.Second), + Details: `{"request_id":"req-1"}`, + }, { EventID: "evt-denied", EventType: "POST /api/v1/dev/clients/client-denied/secret/rotate", @@ -1624,8 +1631,8 @@ func TestListAuditLogs_UserAllowedByRPAuditPermission(t *testing.T) { }) mockKeto := new(devMockKetoService) - mockKeto.On("CheckPermission", mock.Anything, "User:user-1", "RelyingParty", "client-allowed", "view_audit_logs").Return(true, nil) - mockKeto.On("CheckPermission", mock.Anything, "User:user-1", "RelyingParty", "client-denied", "view_audit_logs").Return(false, nil) + mockKeto.On("CheckPermission", mock.Anything, "User:user-1", "RelyingParty", "client-allowed", "audit_viewer").Return(true, nil) + mockKeto.On("CheckPermission", mock.Anything, "User:user-1", "RelyingParty", "client-denied", "audit_viewer").Return(false, nil) h := &DevHandler{ Hydra: &service.HydraAdminService{ @@ -1654,8 +1661,9 @@ func TestListAuditLogs_UserAllowedByRPAuditPermission(t *testing.T) { var result devAuditListResponse _ = json.NewDecoder(resp.Body).Decode(&result) - if assert.Len(t, result.Items, 1) { + if assert.Len(t, result.Items, 2) { assert.Equal(t, "evt-allowed", result.Items[0].EventID) + assert.Equal(t, "evt-allowed-path", result.Items[1].EventID) } mockKeto.AssertExpectations(t) } diff --git a/backend/internal/middleware/audit_middleware.go b/backend/internal/middleware/audit_middleware.go index f7baafad..6d555cfb 100644 --- a/backend/internal/middleware/audit_middleware.go +++ b/backend/internal/middleware/audit_middleware.go @@ -179,6 +179,7 @@ func AuditMiddleware(config AuditConfig) fiber.Handler { EventID: reqID, Timestamp: start, UserID: userID, + TenantID: tenantID, SessionID: sessionID, EventType: fmt.Sprintf("%s %s", c.Method(), c.Path()), Status: statusText, diff --git a/backend/internal/middleware/audit_middleware_test.go b/backend/internal/middleware/audit_middleware_test.go index 59031fe3..0244b64a 100644 --- a/backend/internal/middleware/audit_middleware_test.go +++ b/backend/internal/middleware/audit_middleware_test.go @@ -126,6 +126,7 @@ func TestAuditMiddleware(t *testing.T) { })) app.Post("/test", func(c *fiber.Ctx) error { + c.Locals("tenant_id", "tenant-a") c.Locals("audit_details_extra", map[string]any{ "client_id": "rp-1", "client_name": "Demo App", @@ -145,6 +146,9 @@ func TestAuditMiddleware(t *testing.T) { if details["client_name"] != "Demo App" { return false } + if log.TenantID != "tenant-a" || details["tenant_id"] != "tenant-a" { + return false + } skip, ok := details["auth_timeline_skip"].(bool) return ok && skip })).Return(nil) diff --git a/backend/internal/repository/clickhouse_repo.go b/backend/internal/repository/clickhouse_repo.go index c9b0d0e4..54ca02a7 100644 --- a/backend/internal/repository/clickhouse_repo.go +++ b/backend/internal/repository/clickhouse_repo.go @@ -118,8 +118,8 @@ func (r *ClickHouseRepository) FindPage(ctx context.Context, limit int, cursor * args := make([]any, 0, 5) if tenantID != "" { - query += " AND tenant_id = ?" - args = append(args, tenantID) + query += " AND (tenant_id = ? OR (tenant_id = '' AND JSONExtractString(details, 'tenant_id') = ?))" + args = append(args, tenantID, tenantID) } if cursor != nil {