1
0
forked from baron/baron-sso

chore: consolidate local integration changes

This commit is contained in:
2026-06-09 21:03:05 +09:00
parent aa2848c3b6
commit 1341f07ef9
158 changed files with 10995 additions and 1490 deletions

View File

@@ -176,17 +176,18 @@ type clientRelationUpsertRequest struct {
}
type consentSummary struct {
Subject string `json:"subject"`
UserName string `json:"userName,omitempty"`
ClientID string `json:"clientId"`
ClientName string `json:"clientName,omitempty"`
GrantedScopes []string `json:"grantedScopes"`
AuthenticatedAt string `json:"authenticatedAt,omitempty"`
CreatedAt time.Time `json:"createdAt"`
DeletedAt *time.Time `json:"deletedAt,omitempty"`
Status string `json:"status"`
TenantID string `json:"tenantId,omitempty"`
TenantName string `json:"tenantName,omitempty"`
Subject string `json:"subject"`
UserName string `json:"userName,omitempty"`
ClientID string `json:"clientId"`
ClientName string `json:"clientName,omitempty"`
GrantedScopes []string `json:"grantedScopes"`
AuthenticatedAt string `json:"authenticatedAt,omitempty"`
CreatedAt time.Time `json:"createdAt"`
DeletedAt *time.Time `json:"deletedAt,omitempty"`
Status string `json:"status"`
TenantID string `json:"tenantId,omitempty"`
TenantName string `json:"tenantName,omitempty"`
RPMetadata domain.JSONMap `json:"rpMetadata,omitempty"`
}
type consentListResponse struct {
@@ -217,10 +218,12 @@ type clientUpsertRequest struct {
}
type normalizedIDTokenClaim struct {
Namespace string `json:"namespace"`
Key string `json:"key"`
Value string `json:"value"`
ValueType string `json:"valueType"`
Namespace string `json:"namespace"`
Key string `json:"key"`
Value string `json:"value"`
ValueType string `json:"valueType"`
ReadPermission string `json:"readPermission"`
WritePermission string `json:"writePermission"`
}
var protectedSystemClientIDs = map[string]struct{}{
@@ -1535,19 +1538,202 @@ func (h *DevHandler) UpsertRPUserMetadata(c *fiber.Ctx) error {
if req.Metadata == nil {
req.Metadata = map[string]any{}
}
normalizedMetadata, err := normalizeRPUserMetadataForClient(req.Metadata, summary.Metadata)
if err != nil {
return errorJSON(c, fiber.StatusBadRequest, err.Error())
}
row := &domain.RPUserMetadata{
ClientID: clientID,
UserID: userID,
Metadata: domain.JSONMap(req.Metadata),
Metadata: normalizedMetadata,
}
if err := h.RPUserMetadataRepo.Upsert(c.Context(), row); err != nil {
return errorJSON(c, fiber.StatusInternalServerError, err.Error())
}
if err := h.syncRPUserMetadataToKratos(c.Context(), userID, clientID, normalizedMetadata); err != nil {
return errorJSON(c, fiber.StatusInternalServerError, err.Error())
}
return c.JSON(row)
}
func (h *DevHandler) syncRPUserMetadataToKratos(ctx context.Context, userID string, clientID string, metadata domain.JSONMap) error {
if h == nil || h.KratosAdmin == nil {
return nil
}
identity, err := h.KratosAdmin.GetIdentity(ctx, userID)
if err != nil {
return fmt.Errorf("failed to load kratos identity for rp user metadata: %w", err)
}
if identity == nil {
return errors.New("kratos identity not found for rp user metadata")
}
traits := identity.Traits
if traits == nil {
traits = map[string]any{}
}
rawRPClaims, _ := traits["rp_custom_claims"].(map[string]any)
if rawRPClaims == nil {
rawRPClaims = map[string]any{}
}
rawRPClaims[clientID] = metadata
traits["rp_custom_claims"] = rawRPClaims
_, err = h.KratosAdmin.UpdateIdentity(ctx, identity.ID, traits, identity.State)
if err != nil {
return fmt.Errorf("failed to update kratos rp user metadata: %w", err)
}
return nil
}
type rpUserMetadataClaimSchema struct {
Key string
ValueType string
ReadPermission string
WritePermission string
}
func normalizeCustomClaimPermission(value any) string {
permission := strings.TrimSpace(readInterfaceString(value, ""))
switch permission {
case "user_and_admin":
return "user_and_admin"
default:
return "admin_only"
}
}
func normalizeCustomClaimPermissions(value any, fallbackRead string, fallbackWrite string) map[string]any {
var record map[string]any
switch typed := value.(type) {
case map[string]any:
record = typed
case domain.JSONMap:
record = map[string]any(typed)
}
return map[string]any{
"readPermission": normalizeCustomClaimPermission(readMapValueOrFallback(record, "readPermission", fallbackRead)),
"writePermission": normalizeCustomClaimPermission(readMapValueOrFallback(record, "writePermission", fallbackWrite)),
}
}
func readMapValueOrFallback(values map[string]any, key string, fallback string) any {
if values == nil {
return fallback
}
if value, ok := values[key]; ok {
return value
}
return fallback
}
func normalizeRPUserMetadataForClient(metadata map[string]any, clientMetadata map[string]any) (domain.JSONMap, error) {
schemas, err := rpUserMetadataClaimSchemas(clientMetadata)
if err != nil {
return nil, err
}
normalized := domain.JSONMap{}
for rawKey, rawValue := range metadata {
key := strings.TrimSpace(rawKey)
if key == "" || isEmptyRPUserMetadataValue(rawValue) {
continue
}
if strings.HasSuffix(key, "_permissions") {
claimKey := strings.TrimSuffix(key, "_permissions")
schema, ok := schemas[claimKey]
if !ok {
return nil, fmt.Errorf("rp user metadata claim is not configured: %s", claimKey)
}
normalized[key] = normalizeCustomClaimPermissions(rawValue, schema.ReadPermission, schema.WritePermission)
continue
}
schema, ok := schemas[key]
if !ok {
return nil, fmt.Errorf("rp user metadata claim is not configured: %s", key)
}
textValue, err := stringifyRPUserMetadataValue(rawValue)
if err != nil {
return nil, fmt.Errorf("rp user metadata %s is invalid: %w", key, err)
}
parsed, err := parseConfiguredClaimValue(textValue, schema.ValueType)
if err != nil {
return nil, fmt.Errorf("rp user metadata %s is invalid: %w", key, err)
}
normalized[key] = parsed
permissionKey := key + "_permissions"
if _, exists := normalized[permissionKey]; !exists {
normalized[permissionKey] = map[string]any{
"readPermission": schema.ReadPermission,
"writePermission": schema.WritePermission,
}
}
}
return normalized, nil
}
func rpUserMetadataClaimSchemas(clientMetadata map[string]any) (map[string]rpUserMetadataClaimSchema, error) {
rawClaims, ok := clientMetadata[domain.MetadataIDTokenClaims]
if !ok || rawClaims == nil {
return map[string]rpUserMetadataClaimSchema{}, nil
}
claims, err := normalizeIDTokenClaimsForDevConsole(rawClaims)
if err != nil {
return nil, err
}
schemas := make(map[string]rpUserMetadataClaimSchema, len(claims))
for _, claim := range claims {
if claim.Namespace != "rp_claims" {
continue
}
schemas[claim.Key] = rpUserMetadataClaimSchema{
Key: claim.Key,
ValueType: claim.ValueType,
ReadPermission: claim.ReadPermission,
WritePermission: claim.WritePermission,
}
}
return schemas, nil
}
func isEmptyRPUserMetadataValue(value any) bool {
if value == nil {
return true
}
if text, ok := value.(string); ok {
return strings.TrimSpace(text) == ""
}
return false
}
func stringifyRPUserMetadataValue(value any) (string, error) {
switch typed := value.(type) {
case string:
return strings.TrimSpace(typed), nil
case bool:
return strconv.FormatBool(typed), nil
case float64:
return strconv.FormatFloat(typed, 'f', -1, 64), nil
case float32:
return strconv.FormatFloat(float64(typed), 'f', -1, 32), nil
case int:
return strconv.Itoa(typed), nil
case int64:
return strconv.FormatInt(typed, 10), nil
case int32:
return strconv.FormatInt(int64(typed), 10), nil
case json.Number:
return typed.String(), nil
default:
data, err := json.Marshal(value)
if err != nil {
return "", err
}
return string(data), nil
}
}
func (h *DevHandler) syncHeadlessJWKSCache(ctx context.Context, client domain.HydraClient, reason string) {
if h.HeadlessJWKS == nil {
h.HeadlessJWKS = service.NewHeadlessJWKSCacheService(h.Redis, nil)
@@ -2262,6 +2448,13 @@ func (h *DevHandler) ListConsents(c *fiber.Ctx) error {
}
}
var rpMetadata domain.JSONMap
if h.RPUserMetadataRepo != nil {
if row, err := h.RPUserMetadataRepo.Get(c.Context(), consent.ClientID, consent.Subject); err == nil && row != nil && len(row.Metadata) > 0 {
rpMetadata = row.Metadata
}
}
items = append(items, consentSummary{
Subject: consent.Subject,
UserName: userName,
@@ -2273,6 +2466,7 @@ func (h *DevHandler) ListConsents(c *fiber.Ctx) error {
Status: status,
TenantID: consent.TenantID,
TenantName: consent.TenantName,
RPMetadata: rpMetadata,
})
}
@@ -3107,7 +3301,7 @@ func normalizeIDTokenClaimsMetadata(metadata map[string]any) (map[string]any, er
return metadata, nil
}
normalized, err := normalizeIDTokenClaims(rawClaims)
normalized, err := normalizeIDTokenClaimsForDevConsole(rawClaims)
if err != nil {
return nil, err
}
@@ -3116,6 +3310,14 @@ func normalizeIDTokenClaimsMetadata(metadata map[string]any) (map[string]any, er
}
func normalizeIDTokenClaims(rawClaims any) ([]normalizedIDTokenClaim, error) {
return normalizeIDTokenClaimsWithOptions(rawClaims, true)
}
func normalizeIDTokenClaimsForDevConsole(rawClaims any) ([]normalizedIDTokenClaim, error) {
return normalizeIDTokenClaimsWithOptions(rawClaims, false)
}
func normalizeIDTokenClaimsWithOptions(rawClaims any, allowTopLevel bool) ([]normalizedIDTokenClaim, error) {
rawList, ok := rawClaims.([]any)
if !ok {
if typedList, ok := rawClaims.([]map[string]any); ok {
@@ -3154,6 +3356,9 @@ func normalizeIDTokenClaims(rawClaims any) ([]normalizedIDTokenClaim, error) {
if namespace != "top_level" && namespace != "rp_claims" {
return nil, fmt.Errorf("metadata.id_token_claims namespace must be top_level or rp_claims: %s", namespace)
}
if !allowTopLevel && namespace == "top_level" {
return nil, errors.New("metadata.id_token_claims top_level namespace is managed from admin user custom claims")
}
key := strings.TrimSpace(readInterfaceString(record["key"], ""))
if key == "" {
@@ -3168,7 +3373,7 @@ func normalizeIDTokenClaims(rawClaims any) ([]normalizedIDTokenClaim, error) {
valueType = "text"
}
switch valueType {
case "text", "number", "boolean", "array", "object":
case "text", "number", "boolean", "array", "object", "date", "datetime":
default:
return nil, fmt.Errorf("metadata.id_token_claims valueType is invalid: %s", valueType)
}
@@ -3185,10 +3390,12 @@ func normalizeIDTokenClaims(rawClaims any) ([]normalizedIDTokenClaim, error) {
seen[signature] = struct{}{}
normalized = append(normalized, normalizedIDTokenClaim{
Namespace: namespace,
Key: key,
Value: value,
ValueType: valueType,
Namespace: namespace,
Key: key,
Value: value,
ValueType: valueType,
ReadPermission: normalizeCustomClaimPermission(record["readPermission"]),
WritePermission: normalizeCustomClaimPermission(record["writePermission"]),
})
}
@@ -3258,6 +3465,25 @@ func parseConfiguredClaimValue(rawValue string, valueType string) (any, error) {
return nil, errors.New("object value must be valid JSON object")
}
return parsed, nil
case "date":
if trimmed == "" {
return nil, errors.New("date value is required")
}
if _, err := time.Parse("2006-01-02", trimmed); err != nil {
return nil, errors.New("date value must use YYYY-MM-DD")
}
return trimmed, nil
case "datetime":
if trimmed == "" {
return nil, errors.New("datetime value is required")
}
if _, err := time.Parse(time.RFC3339, trimmed); err == nil {
return trimmed, nil
}
if _, err := time.Parse("2006-01-02T15:04", trimmed); err == nil {
return trimmed, nil
}
return nil, errors.New("datetime value must use RFC3339 or YYYY-MM-DDTHH:mm")
default:
return nil, fmt.Errorf("unsupported claim value type: %s", valueType)
}