forked from baron/baron-sso
feat(user): support fixed UUID registration and enhance bulk import results
- Added support for fixed UUIDs during bulk registration (Search-first + ExternalID mapping) - Implemented idempotency and visibility restoration for soft-deleted users - Enhanced bulk upload UI to show 'New/Updated/Unchanged' status and modified fields - Added logic to reclaim identifiers (login_id) from colliding records - Added frontend E2E and backend unit tests for UUID integrity and conflict handling - Fixed i18n, formatting, and mock tests to satisfy code-check - Applied 'go fix' for 'omitzero' tags and general Go standards
This commit is contained in:
@@ -14,6 +14,7 @@ import (
|
||||
"fmt"
|
||||
"io"
|
||||
"log/slog"
|
||||
"maps"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
@@ -104,21 +105,21 @@ type devRPUsageDailyResponse struct {
|
||||
}
|
||||
|
||||
type clientSummary struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Type string `json:"type"`
|
||||
Status string `json:"status"`
|
||||
CreatedAt *time.Time `json:"createdAt,omitempty"`
|
||||
RedirectURIs []string `json:"redirectUris"`
|
||||
Scopes []string `json:"scopes"`
|
||||
ClientSecret string `json:"clientSecret,omitempty"`
|
||||
TokenEndpointAuthMethod string `json:"tokenEndpointAuthMethod,omitempty"`
|
||||
SkipConsent bool `json:"skipConsent"`
|
||||
JwksUri string `json:"jwksUri,omitempty"`
|
||||
Jwks interface{} `json:"jwks,omitempty"`
|
||||
BackchannelLogoutURI string `json:"backchannelLogoutUri,omitempty"`
|
||||
BackchannelLogoutSessionRequired bool `json:"backchannelLogoutSessionRequired"`
|
||||
Metadata map[string]interface{} `json:"metadata,omitempty"`
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Type string `json:"type"`
|
||||
Status string `json:"status"`
|
||||
CreatedAt *time.Time `json:"createdAt,omitempty"`
|
||||
RedirectURIs []string `json:"redirectUris"`
|
||||
Scopes []string `json:"scopes"`
|
||||
ClientSecret string `json:"clientSecret,omitempty"`
|
||||
TokenEndpointAuthMethod string `json:"tokenEndpointAuthMethod,omitempty"`
|
||||
SkipConsent bool `json:"skipConsent"`
|
||||
JwksUri string `json:"jwksUri,omitempty"`
|
||||
Jwks any `json:"jwks,omitempty"`
|
||||
BackchannelLogoutURI string `json:"backchannelLogoutUri,omitempty"`
|
||||
BackchannelLogoutSessionRequired bool `json:"backchannelLogoutSessionRequired"`
|
||||
Metadata map[string]any `json:"metadata,omitempty"`
|
||||
}
|
||||
|
||||
type clientListResponse struct {
|
||||
@@ -198,21 +199,21 @@ type consentListResponse struct {
|
||||
}
|
||||
|
||||
type clientUpsertRequest struct {
|
||||
ID *string `json:"id"`
|
||||
Name *string `json:"name"`
|
||||
Type *string `json:"type"`
|
||||
Status *string `json:"status"`
|
||||
RedirectURIs *[]string `json:"redirectUris"`
|
||||
Scopes *[]string `json:"scopes"`
|
||||
GrantTypes *[]string `json:"grantTypes"`
|
||||
ResponseTypes *[]string `json:"responseTypes"`
|
||||
TokenEndpointAuthMethod *string `json:"tokenEndpointAuthMethod"`
|
||||
SkipConsent *bool `json:"skipConsent"`
|
||||
JwksUri *string `json:"jwksUri"`
|
||||
Jwks interface{} `json:"jwks"`
|
||||
BackchannelLogoutURI *string `json:"backchannelLogoutUri"`
|
||||
BackchannelLogoutSessionRequired *bool `json:"backchannelLogoutSessionRequired"`
|
||||
Metadata *map[string]interface{} `json:"metadata"`
|
||||
ID *string `json:"id"`
|
||||
Name *string `json:"name"`
|
||||
Type *string `json:"type"`
|
||||
Status *string `json:"status"`
|
||||
RedirectURIs *[]string `json:"redirectUris"`
|
||||
Scopes *[]string `json:"scopes"`
|
||||
GrantTypes *[]string `json:"grantTypes"`
|
||||
ResponseTypes *[]string `json:"responseTypes"`
|
||||
TokenEndpointAuthMethod *string `json:"tokenEndpointAuthMethod"`
|
||||
SkipConsent *bool `json:"skipConsent"`
|
||||
JwksUri *string `json:"jwksUri"`
|
||||
Jwks any `json:"jwks"`
|
||||
BackchannelLogoutURI *string `json:"backchannelLogoutUri"`
|
||||
BackchannelLogoutSessionRequired *bool `json:"backchannelLogoutSessionRequired"`
|
||||
Metadata *map[string]any `json:"metadata"`
|
||||
}
|
||||
|
||||
type normalizedIDTokenClaim struct {
|
||||
@@ -303,7 +304,7 @@ func tenantIDFromProfile(profile *domain.UserProfileResponse) string {
|
||||
func addClientIDToSet(set map[string]struct{}, raw any) {
|
||||
switch value := raw.(type) {
|
||||
case string:
|
||||
for _, chunk := range strings.Split(value, ",") {
|
||||
for chunk := range strings.SplitSeq(value, ",") {
|
||||
id := strings.TrimSpace(chunk)
|
||||
if id != "" {
|
||||
set[id] = struct{}{}
|
||||
@@ -672,7 +673,7 @@ func isProtectedSystemClientID(clientID string) bool {
|
||||
return ok
|
||||
}
|
||||
|
||||
func tenantAccessPolicyChanged(before, after map[string]interface{}) bool {
|
||||
func tenantAccessPolicyChanged(before, after map[string]any) bool {
|
||||
if clientTenantAccessRestricted(before) != clientTenantAccessRestricted(after) {
|
||||
return true
|
||||
}
|
||||
@@ -1162,7 +1163,7 @@ func extractAuthClaimsFromBearer(authHeader string) (string, string) {
|
||||
}
|
||||
}
|
||||
|
||||
var claims map[string]interface{}
|
||||
var claims map[string]any
|
||||
if err := json.Unmarshal(payload, &claims); err != nil {
|
||||
return "", ""
|
||||
}
|
||||
@@ -1295,10 +1296,7 @@ func (h *DevHandler) ListClients(c *fiber.Ctx) error {
|
||||
if offset > len(allItems) {
|
||||
offset = len(allItems)
|
||||
}
|
||||
end := offset + limit
|
||||
if end > len(allItems) {
|
||||
end = len(allItems)
|
||||
}
|
||||
end := min(offset+limit, len(allItems))
|
||||
items = allItems[offset:end]
|
||||
if len(allItems) > end && len(items) > 0 {
|
||||
lastTimestamp, lastID := clientSummaryCursorKey(items[len(items)-1])
|
||||
@@ -1788,7 +1786,7 @@ func (h *DevHandler) CreateClient(c *fiber.Ctx) error {
|
||||
|
||||
metadata := mergeMetadata(nil, req.Metadata)
|
||||
if metadata == nil {
|
||||
metadata = map[string]interface{}{}
|
||||
metadata = map[string]any{}
|
||||
}
|
||||
|
||||
// [Tenant Isolation] Record owner information
|
||||
@@ -1858,11 +1856,11 @@ func (h *DevHandler) CreateClient(c *fiber.Ctx) error {
|
||||
ResponseTypes: responseTypes,
|
||||
Scope: strings.Join(scopes, " "),
|
||||
TokenEndpointAuthMethod: tokenAuthMethod,
|
||||
SkipConsent: boolPtr(valueOrBool(req.SkipConsent, true)),
|
||||
SkipConsent: new(valueOrBool(req.SkipConsent, true)),
|
||||
JWKSUri: jwksURI,
|
||||
JWKS: jwks,
|
||||
BackChannelLogoutURI: backchannelLogoutURI,
|
||||
BackChannelLogoutSessionRequired: boolPtr(backchannelLogoutSessionRequired),
|
||||
BackChannelLogoutSessionRequired: new(backchannelLogoutSessionRequired),
|
||||
Metadata: metadata,
|
||||
}
|
||||
|
||||
@@ -2005,7 +2003,7 @@ func (h *DevHandler) UpdateClient(c *fiber.Ctx) error {
|
||||
metadata := mergeMetadata(current.Metadata, req.Metadata)
|
||||
if status != "" {
|
||||
if metadata == nil {
|
||||
metadata = map[string]interface{}{}
|
||||
metadata = map[string]any{}
|
||||
}
|
||||
metadata["status"] = status
|
||||
}
|
||||
@@ -2061,11 +2059,11 @@ func (h *DevHandler) UpdateClient(c *fiber.Ctx) error {
|
||||
ResponseTypes: derefSlice(req.ResponseTypes, current.ResponseTypes),
|
||||
Scope: buildScope(valueOrSlice(req.Scopes, strings.Fields(current.Scope))),
|
||||
TokenEndpointAuthMethod: resolvedTokenAuthMethod,
|
||||
SkipConsent: boolPtr(resolvedSkipConsent),
|
||||
SkipConsent: new(resolvedSkipConsent),
|
||||
JWKSUri: resolvedJWKSURI,
|
||||
JWKS: resolvedJWKS,
|
||||
BackChannelLogoutURI: strings.TrimSpace(resolvedBackchannelLogoutURI),
|
||||
BackChannelLogoutSessionRequired: boolPtr(resolvedBackchannelLogoutSessionRequired),
|
||||
BackChannelLogoutSessionRequired: new(resolvedBackchannelLogoutSessionRequired),
|
||||
Metadata: metadata,
|
||||
}
|
||||
if err := validateReservedSystemClientName(updated.ClientID, updated.ClientName); err != nil {
|
||||
@@ -2359,10 +2357,7 @@ func (h *DevHandler) ListConsents(c *fiber.Ctx) error {
|
||||
if offset > len(items) {
|
||||
offset = len(items)
|
||||
}
|
||||
end := offset + limit
|
||||
if end > len(items) {
|
||||
end = len(items)
|
||||
}
|
||||
end := min(offset+limit, len(items))
|
||||
pageItems := items[offset:end]
|
||||
if len(items) > end && len(pageItems) > 0 {
|
||||
lastTimestamp, lastID := consentSummaryCursorKey(pageItems[len(pageItems)-1])
|
||||
@@ -2948,7 +2943,7 @@ func (h *DevHandler) mapClientSummary(client domain.HydraClient) clientSummary {
|
||||
}
|
||||
}
|
||||
|
||||
func readMetadataStringValue(metadata map[string]interface{}, key string) string {
|
||||
func readMetadataStringValue(metadata map[string]any, key string) string {
|
||||
if metadata == nil {
|
||||
return ""
|
||||
}
|
||||
@@ -2956,7 +2951,7 @@ func readMetadataStringValue(metadata map[string]interface{}, key string) string
|
||||
return strings.TrimSpace(raw)
|
||||
}
|
||||
|
||||
func readMetadataBoolValue(metadata map[string]interface{}, key string) bool {
|
||||
func readMetadataBoolValue(metadata map[string]any, key string) bool {
|
||||
if metadata == nil {
|
||||
return false
|
||||
}
|
||||
@@ -2964,7 +2959,7 @@ func readMetadataBoolValue(metadata map[string]interface{}, key string) bool {
|
||||
return value
|
||||
}
|
||||
|
||||
func readStringSliceMetadata(metadata map[string]interface{}, key string) []string {
|
||||
func readStringSliceMetadata(metadata map[string]any, key string) []string {
|
||||
if metadata == nil {
|
||||
return nil
|
||||
}
|
||||
@@ -2981,7 +2976,7 @@ func readStringSliceMetadata(metadata map[string]interface{}, key string) []stri
|
||||
}
|
||||
}
|
||||
return result
|
||||
case []interface{}:
|
||||
case []any:
|
||||
result := make([]string, 0, len(typed))
|
||||
for _, item := range typed {
|
||||
if str, ok := item.(string); ok {
|
||||
@@ -2996,7 +2991,7 @@ func readStringSliceMetadata(metadata map[string]interface{}, key string) []stri
|
||||
}
|
||||
}
|
||||
|
||||
func readMetadataValueOrNil(metadata map[string]interface{}, key string) interface{} {
|
||||
func readMetadataValueOrNil(metadata map[string]any, key string) any {
|
||||
if metadata == nil {
|
||||
return nil
|
||||
}
|
||||
@@ -3007,9 +3002,9 @@ func readMetadataValueOrNil(metadata map[string]interface{}, key string) interfa
|
||||
return value
|
||||
}
|
||||
|
||||
func normalizeBackchannelLogoutMetadata(metadata map[string]interface{}, logoutURI string, sessionRequired bool) (map[string]interface{}, error) {
|
||||
func normalizeBackchannelLogoutMetadata(metadata map[string]any, logoutURI string, sessionRequired bool) (map[string]any, error) {
|
||||
if metadata == nil {
|
||||
metadata = map[string]interface{}{}
|
||||
metadata = map[string]any{}
|
||||
}
|
||||
|
||||
trimmedURI := strings.TrimSpace(logoutURI)
|
||||
@@ -3078,7 +3073,7 @@ func isAllowedLocalBackchannelLogoutHost(rawHost string) bool {
|
||||
return !strings.Contains(host, ".")
|
||||
}
|
||||
|
||||
func normalizeClientAutoLoginMetadata(metadata map[string]interface{}) (map[string]interface{}, error) {
|
||||
func normalizeClientAutoLoginMetadata(metadata map[string]any) (map[string]any, error) {
|
||||
if metadata == nil {
|
||||
return metadata, nil
|
||||
}
|
||||
@@ -3105,11 +3100,11 @@ func normalizeClientAutoLoginMetadata(metadata map[string]interface{}) (map[stri
|
||||
func normalizeHeadlessClientConfig(
|
||||
tokenAuthMethod string,
|
||||
jwksURI string,
|
||||
jwks interface{},
|
||||
metadata map[string]interface{},
|
||||
) (string, string, interface{}, map[string]interface{}) {
|
||||
jwks any,
|
||||
metadata map[string]any,
|
||||
) (string, string, any, map[string]any) {
|
||||
if metadata == nil {
|
||||
metadata = map[string]interface{}{}
|
||||
metadata = map[string]any{}
|
||||
}
|
||||
delete(metadata, domain.MetadataRequestObjectSigningAlg)
|
||||
|
||||
@@ -3145,7 +3140,7 @@ func normalizeHeadlessClientConfig(
|
||||
return tokenAuthMethod, jwksURI, jwks, metadata
|
||||
}
|
||||
|
||||
func validateHeadlessClientInput(jwksURI string, jwks interface{}, metadata map[string]interface{}) error {
|
||||
func validateHeadlessClientInput(jwksURI string, jwks any, metadata map[string]any) error {
|
||||
if !readMetadataBoolValue(metadata, domain.MetadataHeadlessLoginEnabled) {
|
||||
return nil
|
||||
}
|
||||
@@ -3164,14 +3159,14 @@ func validateHeadlessClientInput(jwksURI string, jwks interface{}, metadata map[
|
||||
return nil
|
||||
}
|
||||
|
||||
func normalizeClientTypeForHeadless(clientType string, metadata map[string]interface{}) string {
|
||||
func normalizeClientTypeForHeadless(clientType string, metadata map[string]any) string {
|
||||
if readMetadataBoolValue(metadata, domain.MetadataHeadlessLoginEnabled) {
|
||||
return "private"
|
||||
}
|
||||
return clientType
|
||||
}
|
||||
|
||||
func normalizeIDTokenClaimsMetadata(metadata map[string]interface{}) (map[string]interface{}, error) {
|
||||
func normalizeIDTokenClaimsMetadata(metadata map[string]any) (map[string]any, error) {
|
||||
if metadata == nil {
|
||||
return nil, nil
|
||||
}
|
||||
@@ -3189,16 +3184,16 @@ func normalizeIDTokenClaimsMetadata(metadata map[string]interface{}) (map[string
|
||||
return metadata, nil
|
||||
}
|
||||
|
||||
func normalizeIDTokenClaims(rawClaims interface{}) ([]normalizedIDTokenClaim, error) {
|
||||
rawList, ok := rawClaims.([]interface{})
|
||||
func normalizeIDTokenClaims(rawClaims any) ([]normalizedIDTokenClaim, error) {
|
||||
rawList, ok := rawClaims.([]any)
|
||||
if !ok {
|
||||
if typedList, ok := rawClaims.([]map[string]interface{}); ok {
|
||||
rawList = make([]interface{}, 0, len(typedList))
|
||||
if typedList, ok := rawClaims.([]map[string]any); ok {
|
||||
rawList = make([]any, 0, len(typedList))
|
||||
for _, item := range typedList {
|
||||
rawList = append(rawList, item)
|
||||
}
|
||||
} else if typedList, ok := rawClaims.([]map[string]any); ok {
|
||||
rawList = make([]interface{}, 0, len(typedList))
|
||||
rawList = make([]any, 0, len(typedList))
|
||||
for _, item := range typedList {
|
||||
rawList = append(rawList, item)
|
||||
}
|
||||
@@ -3211,13 +3206,11 @@ func normalizeIDTokenClaims(rawClaims interface{}) ([]normalizedIDTokenClaim, er
|
||||
seen := make(map[string]struct{}, len(rawList))
|
||||
|
||||
for _, item := range rawList {
|
||||
record, ok := item.(map[string]interface{})
|
||||
record, ok := item.(map[string]any)
|
||||
if !ok {
|
||||
if typedRecord, ok := item.(map[string]any); ok {
|
||||
record = make(map[string]interface{}, len(typedRecord))
|
||||
for key, value := range typedRecord {
|
||||
record[key] = value
|
||||
}
|
||||
record = make(map[string]any, len(typedRecord))
|
||||
maps.Copy(record, typedRecord)
|
||||
} else {
|
||||
return nil, errors.New("metadata.id_token_claims items must be objects")
|
||||
}
|
||||
@@ -3271,7 +3264,7 @@ func normalizeIDTokenClaims(rawClaims interface{}) ([]normalizedIDTokenClaim, er
|
||||
return normalized, nil
|
||||
}
|
||||
|
||||
func readInterfaceString(value interface{}, fallback string) string {
|
||||
func readInterfaceString(value any, fallback string) string {
|
||||
if value == nil {
|
||||
return fallback
|
||||
}
|
||||
@@ -3373,8 +3366,9 @@ func valueOr(ptr *string, fallback string) string {
|
||||
return *ptr
|
||||
}
|
||||
|
||||
//go:fix inline
|
||||
func boolPtr(value bool) *bool {
|
||||
return &value
|
||||
return new(value)
|
||||
}
|
||||
|
||||
func valueOrBool(ptr *bool, fallback bool) bool {
|
||||
@@ -3398,17 +3392,13 @@ func derefSlice(ptr *[]string, fallback []string) []string {
|
||||
return *ptr
|
||||
}
|
||||
|
||||
func mergeMetadata(current map[string]interface{}, incoming *map[string]interface{}) map[string]interface{} {
|
||||
func mergeMetadata(current map[string]any, incoming *map[string]any) map[string]any {
|
||||
if incoming == nil {
|
||||
return current
|
||||
}
|
||||
merged := map[string]interface{}{}
|
||||
for k, v := range current {
|
||||
merged[k] = v
|
||||
}
|
||||
for k, v := range *incoming {
|
||||
merged[k] = v
|
||||
}
|
||||
merged := map[string]any{}
|
||||
maps.Copy(merged, current)
|
||||
maps.Copy(merged, *incoming)
|
||||
return merged
|
||||
}
|
||||
|
||||
@@ -3433,9 +3423,7 @@ func (h *DevHandler) setAuditDetailsExtra(c *fiber.Ctx, extra map[string]any) {
|
||||
}
|
||||
if existing := c.Locals("audit_details_extra"); existing != nil {
|
||||
if m, ok := existing.(map[string]any); ok {
|
||||
for k, v := range extra {
|
||||
m[k] = v
|
||||
}
|
||||
maps.Copy(m, extra)
|
||||
c.Locals("audit_details_extra", m)
|
||||
return
|
||||
}
|
||||
@@ -3494,7 +3482,7 @@ func resolveDevAuditClientID(logItem domain.AuditLog, details map[string]any) st
|
||||
return resolvedID
|
||||
}
|
||||
|
||||
func resolveStatusFromMetadata(metadata map[string]interface{}) string {
|
||||
func resolveStatusFromMetadata(metadata map[string]any) string {
|
||||
if metadata != nil {
|
||||
if value, ok := metadata["status"].(string); ok && strings.ToLower(strings.TrimSpace(value)) == "inactive" {
|
||||
return "inactive"
|
||||
|
||||
Reference in New Issue
Block a user