forked from baron/baron-sso
chore: consolidate local integration changes
This commit is contained in:
@@ -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)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user