package handler import ( "baron-sso-backend/internal/domain" "baron-sso-backend/internal/repository" "baron-sso-backend/internal/service" "context" "fmt" "log/slog" "maps" "strings" ) const tenantAccessCleanupClientPageSize = 500 func cleanupDeletedTenantReferences(ctx context.Context, hydra *service.HydraAdminService, consentRepo repository.ClientConsentRepository, ketoOutbox repository.KetoOutboxRepository, deletedTenantIDs []string) error { if hydra == nil { return nil } deletedTenantSet := make(map[string]struct{}, len(deletedTenantIDs)) for _, tenantID := range deletedTenantIDs { tenantID = strings.TrimSpace(tenantID) if tenantID == "" { continue } deletedTenantSet[tenantID] = struct{}{} } if len(deletedTenantSet) == 0 { return nil } for offset := 0; ; offset += tenantAccessCleanupClientPageSize { clients, err := hydra.ListClients(ctx, tenantAccessCleanupClientPageSize, offset) if err != nil { return fmt.Errorf("failed to list hydra clients for tenant cleanup: %w", err) } for _, client := range clients { beforeMetadata := maps.Clone(client.Metadata) updatedMetadata, changed, removedOwnerTenantID := pruneDeletedTenantReferences(beforeMetadata, deletedTenantSet) if !changed { continue } updatedClient := client updatedClient.Metadata = updatedMetadata if _, err := hydra.UpdateClient(ctx, client.ClientID, updatedClient); err != nil { return fmt.Errorf("failed to update hydra client %s during tenant cleanup: %w", client.ClientID, err) } if removedOwnerTenantID != "" { if err := enqueueDeletedTenantRelyingPartyParentCleanup(ctx, ketoOutbox, client.ClientID, removedOwnerTenantID); err != nil { return fmt.Errorf("failed to cleanup RP parent relation for client %s during tenant cleanup: %w", client.ClientID, err) } } if tenantAccessPolicyChanged(beforeMetadata, updatedMetadata) { if err := revokeClientConsentsForPolicyChange(ctx, hydra, consentRepo, client.ClientID); err != nil { return fmt.Errorf("failed to revoke consent sessions for client %s during tenant cleanup: %w", client.ClientID, err) } } } if len(clients) < tenantAccessCleanupClientPageSize { return nil } } } func pruneDeletedTenantReferences(metadata map[string]any, deletedTenantSet map[string]struct{}) (map[string]any, bool, string) { if len(deletedTenantSet) == 0 { return metadata, false, "" } ownerTenantID := normalizeMetadataString(metadata["tenant_id"]) _, ownerDeleted := deletedTenantSet[ownerTenantID] allowedTenants := normalizeMetadataStringSlice(metadata[clientAllowedTenantsKey]) filtered := make([]string, 0, len(allowedTenants)) for _, tenantID := range allowedTenants { if _, ok := deletedTenantSet[tenantID]; ok { continue } filtered = append(filtered, tenantID) } allowedChanged := len(filtered) != len(allowedTenants) if !ownerDeleted && !allowedChanged { return metadata, false, "" } updated := maps.Clone(metadata) if ownerDeleted { delete(updated, "tenant_id") } if len(filtered) == 0 { delete(updated, clientAllowedTenantsKey) updated[clientTenantAccessRestrictedKey] = false return updated, true, ownerTenantID } updated[clientAllowedTenantsKey] = uniqueSortedStrings(filtered) updated[clientTenantAccessRestrictedKey] = true return updated, true, ownerTenantID } func enqueueDeletedTenantRelyingPartyParentCleanup(ctx context.Context, ketoOutbox repository.KetoOutboxRepository, clientID, tenantID string) error { if ketoOutbox == nil { return nil } clientID = strings.TrimSpace(clientID) tenantID = strings.TrimSpace(tenantID) if clientID == "" || tenantID == "" { return nil } return ketoOutbox.Create(ctx, &domain.KetoOutbox{ Namespace: "RelyingParty", Object: clientID, Relation: "parents", Subject: "Tenant:" + tenantID, Action: domain.KetoOutboxActionDelete, }) } func revokeClientConsentsForPolicyChange(ctx context.Context, hydra *service.HydraAdminService, consentRepo repository.ClientConsentRepository, clientID string) error { if consentRepo == nil || hydra == nil { return nil } subjects, err := consentRepo.ListSubjectsByClient(ctx, clientID) if err != nil { return err } for _, subject := range subjects { subject = strings.TrimSpace(subject) if subject == "" { continue } if err := hydra.RevokeConsentSessions(ctx, subject, clientID); err != nil { return err } } return consentRepo.DeleteByClient(ctx, clientID) } func logTenantCleanupFailure(err error, deletedTenantIDs []string) { if err == nil { return } slog.Error("Failed to cleanup RP tenant restrictions after tenant deletion", "tenant_ids", deletedTenantIDs, "error", err) }