forked from baron/baron-sso
254 lines
6.1 KiB
Go
254 lines
6.1 KiB
Go
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)
|
|
}
|