package handler import ( "baron-sso-backend/internal/domain" "encoding/json" "errors" "sort" "strings" "github.com/gofiber/fiber/v2" ) const ( clientTenantAccessRestrictedKey = "tenant_access_restricted" clientAllowedTenantsKey = "allowed_tenants" ) func normalizeClientTenantAccessMetadata(metadata map[string]interface{}) (map[string]interface{}, error) { if metadata == nil { metadata = map[string]interface{}{} } restricted := readMetadataBoolValue(metadata, clientTenantAccessRestrictedKey) allowedTenants := normalizeMetadataStringSlice(metadata[clientAllowedTenantsKey]) ownerTenantID := normalizeMetadataString(metadata["tenant_id"]) if len(allowedTenants) > 0 { restricted = true } if !restricted { delete(metadata, clientAllowedTenantsKey) metadata[clientTenantAccessRestrictedKey] = false return metadata, nil } if ownerTenantID != "" { allowedTenants = append(allowedTenants, ownerTenantID) } allowedTenants = uniqueSortedStrings(allowedTenants) if len(allowedTenants) == 0 { return nil, errors.New("allowed_tenants is required when tenant_access_restricted is enabled") } metadata[clientTenantAccessRestrictedKey] = true metadata[clientAllowedTenantsKey] = allowedTenants return metadata, nil } func clientTenantAccessRestricted(metadata map[string]interface{}) bool { if metadata == nil { return false } if readMetadataBoolValue(metadata, clientTenantAccessRestrictedKey) { return true } return len(normalizeMetadataStringSlice(metadata[clientAllowedTenantsKey])) > 0 } func clientAllowedTenants(metadata map[string]interface{}) []string { if metadata == nil { return nil } if !clientTenantAccessRestricted(metadata) { return nil } return uniqueSortedStrings(normalizeMetadataStringSlice(metadata[clientAllowedTenantsKey])) } func normalizeMetadataStringSlice(raw any) []string { switch value := raw.(type) { case []string: return uniqueSortedStrings(value) case []any: items := make([]string, 0, len(value)) for _, item := range value { if s, ok := item.(string); ok { items = append(items, s) } } return uniqueSortedStrings(items) default: return nil } } func normalizeMetadataString(raw any) string { s, ok := raw.(string) if !ok { return "" } return strings.TrimSpace(s) } func uniqueSortedStrings(values []string) []string { if len(values) == 0 { return nil } seen := make(map[string]struct{}, len(values)) out := make([]string, 0, len(values)) for _, value := range values { trimmed := strings.TrimSpace(value) if trimmed == "" { continue } if _, ok := seen[trimmed]; ok { continue } seen[trimmed] = struct{}{} out = append(out, trimmed) } sort.Strings(out) return out } func clientTenantAccessAllowed(profile *domain.UserProfileResponse, client domain.HydraClient) bool { if !clientTenantAccessRestricted(client.Metadata) { return true } allowed := clientAllowedTenants(client.Metadata) if len(allowed) == 0 { return false } keys := manageableTenantKeysFromProfile(profile) if len(keys) == 0 { return false } for _, tenantID := range allowed { if _, ok := keys[strings.ToLower(strings.TrimSpace(tenantID))]; ok { return true } } return false } func tenantNotAllowedError(c *fiber.Ctx) error { return errorJSONCode(c, fiber.StatusForbidden, "tenant_not_allowed", "허용되지 않은 테넌트입니다.") } func isClientTenantAccessAllowed(profile *domain.UserProfileResponse, client domain.HydraClient) bool { if profile == nil { return false } return clientTenantAccessAllowed(profile, client) } func enforceClientTenantAccess(c *fiber.Ctx, client domain.HydraClient, profile *domain.UserProfileResponse, resolveErr error) error { if !clientTenantAccessRestricted(client.Metadata) { return nil } if resolveErr != nil || profile == nil { return tenantNotAllowedError(c) } if !clientTenantAccessAllowed(profile, client) { return tenantNotAllowedError(c) } return nil } type clientStructuredScope struct { Name string `json:"name"` Mandatory bool `json:"mandatory"` Locked bool `json:"locked"` } func mergeRequestedScopesWithClientRequirements(client domain.HydraClient, requested []string) []string { combined := make([]string, 0, len(requested)+2) combined = append(combined, requested...) combined = append(combined, requiredClientScopes(client)...) return normalizeScopesInConsentOrder(combined) } func normalizeScopesInConsentOrder(scopes []string) []string { combined := make([]string, 0, len(scopes)) combined = append(combined, scopes...) seen := make(map[string]struct{}, len(combined)) out := make([]string, 0, len(combined)) appendIfPresent := func(scope string) { scope = strings.TrimSpace(scope) if scope == "" { return } if _, ok := seen[scope]; ok { return } for _, candidate := range combined { if strings.TrimSpace(candidate) != scope { continue } seen[scope] = struct{}{} out = append(out, scope) return } } appendIfPresent("openid") appendIfPresent("tenant") for _, scope := range combined { scope = strings.TrimSpace(scope) if scope == "" { continue } if _, ok := seen[scope]; ok { continue } seen[scope] = struct{}{} out = append(out, scope) } return out } func requiredClientScopes(client domain.HydraClient) []string { required := make([]string, 0, 4) if clientTenantAccessRestricted(client.Metadata) { required = append(required, "tenant") } if client.Metadata == nil { return normalizeScopesInConsentOrder(required) } rawStructuredScopes, ok := client.Metadata["structured_scopes"] if !ok || rawStructuredScopes == nil { return normalizeScopesInConsentOrder(required) } rawBytes, err := json.Marshal(rawStructuredScopes) if err != nil { return normalizeScopesInConsentOrder(required) } var scopes []clientStructuredScope if err := json.Unmarshal(rawBytes, &scopes); err != nil { return normalizeScopesInConsentOrder(required) } for _, scope := range scopes { name := strings.TrimSpace(scope.Name) if name == "" { continue } if scope.Mandatory || scope.Locked { required = append(required, name) } } return normalizeScopesInConsentOrder(required) }