1
0
forked from baron/baron-sso

custom claim 권한체크 확인

This commit is contained in:
2026-06-11 08:29:25 +09:00
parent 839ca9d407
commit 4d77060b5d
79 changed files with 4268 additions and 670 deletions

View File

@@ -16,6 +16,7 @@ import (
"net/http"
"net/mail"
"os"
"reflect"
"regexp"
"sort"
"strconv"
@@ -170,6 +171,145 @@ func mergeUserAddTenantAppointment(traits map[string]any, metadata map[string]an
return metadata
}
func removeUserTenantAppointment(traits map[string]any, metadata map[string]any, tenant *domain.Tenant) map[string]any {
if tenant == nil {
return metadata
}
if metadata == nil {
metadata = map[string]any{}
}
targetID := strings.ToLower(strings.TrimSpace(tenant.ID))
targetSlug := strings.ToLower(strings.TrimSpace(tenant.Slug))
matchesTarget := func(raw any) bool {
appointment, ok := raw.(map[string]any)
if !ok {
return false
}
tenantID := strings.ToLower(normalizeMetadataString(appointment["tenantId"]))
tenantSlug := strings.ToLower(normalizeMetadataString(appointment["tenantSlug"]))
if tenantSlug == "" {
tenantSlug = strings.ToLower(normalizeMetadataString(appointment["slug"]))
}
return (targetID != "" && tenantID == targetID) ||
(targetSlug != "" && tenantSlug == targetSlug)
}
appointments := userAppointmentSliceFromRaw(traits["additionalAppointments"])
if len(appointments) == 0 {
if legacyMetadata, ok := traits["metadata"].(map[string]any); ok {
appointments = userAppointmentSliceFromRaw(legacyMetadata["additionalAppointments"])
}
}
if incoming := userAppointmentSliceFromRaw(metadata["additionalAppointments"]); len(incoming) > 0 {
appointments = incoming
}
filtered := make([]any, 0, len(appointments))
removedPrimary := false
for _, appointment := range appointments {
if matchesTarget(appointment) {
if value, ok := metadataBoolFromMap(appointment.(map[string]any), "isPrimary", "primary", "representative", "isRepresentative"); ok && value {
removedPrimary = true
}
continue
}
filtered = append(filtered, appointment)
}
if len(filtered) > 0 {
traits["additionalAppointments"] = filtered
metadata["additionalAppointments"] = filtered
} else {
delete(traits, "additionalAppointments")
delete(metadata, "additionalAppointments")
}
delete(traits, tenant.ID)
delete(metadata, tenant.ID)
if primaryTenantID := strings.ToLower(normalizeMetadataString(traits["primaryTenantId"])); primaryTenantID == targetID && targetID != "" {
removedPrimary = true
}
if primaryTenantSlug := strings.ToLower(normalizeMetadataString(traits["primaryTenantSlug"])); primaryTenantSlug == targetSlug && targetSlug != "" {
removedPrimary = true
}
if removedPrimary {
delete(traits, "primaryTenantId")
delete(traits, "primaryTenantSlug")
delete(traits, "primaryTenantName")
delete(traits, "primaryTenantIsOwner")
delete(metadata, "primaryTenantId")
delete(metadata, "primaryTenantSlug")
delete(metadata, "primaryTenantName")
delete(metadata, "primaryTenantIsOwner")
}
return metadata
}
func userMetadataRecordFromAny(value any) map[string]any {
switch typed := value.(type) {
case map[string]any:
return typed
case domain.JSONMap:
return map[string]any(typed)
default:
return nil
}
}
func enforceGlobalCustomClaimWritePermissions(traits map[string]any, metadata map[string]any, isAdmin bool) error {
if isAdmin || metadata == nil {
return nil
}
incomingClaims := userMetadataRecordFromAny(metadata["global_custom_claims"])
if incomingClaims == nil {
return nil
}
existingClaims := userMetadataRecordFromAny(traits["global_custom_claims"])
existingPermissions := userMetadataRecordFromAny(traits["global_custom_claim_permissions"])
existingTypes := userMetadataRecordFromAny(traits["global_custom_claim_types"])
claimKeys := map[string]bool{}
for key := range incomingClaims {
claimKeys[strings.TrimSpace(key)] = true
}
for key := range existingClaims {
claimKeys[strings.TrimSpace(key)] = true
}
for key := range claimKeys {
if key == "" {
continue
}
incomingValue, incomingExists := incomingClaims[key]
existingValue, existingExists := existingClaims[key]
if incomingExists && existingExists && reflect.DeepEqual(incomingValue, existingValue) {
continue
}
if !incomingExists && !existingExists {
continue
}
permission := "admin_only"
if rawPermission := userMetadataRecordFromAny(existingPermissions[key]); rawPermission != nil {
permission = normalizeCustomClaimPermission(rawPermission["writePermission"])
}
if permission != "user_and_admin" {
return fmt.Errorf("global custom claim %s is admin only", key)
}
}
if len(existingPermissions) > 0 {
metadata["global_custom_claim_permissions"] = existingPermissions
}
if len(existingTypes) > 0 {
metadata["global_custom_claim_types"] = existingTypes
}
return nil
}
func sanitizeUserRepresentativeTenants(ctx context.Context, tenantService service.TenantService, metadata map[string]any, appointments []map[string]any) (bool, error) {
if tenantService == nil || metadata == nil {
return false, nil
@@ -1864,6 +2004,7 @@ func (h *UserHandler) BulkUpdateUsers(c *fiber.Ctx) error {
Role *string `json:"role"`
TenantSlug *string `json:"tenantSlug"`
CompanyCode *string `json:"companyCode"`
IsAddTenant bool `json:"isAddTenant"`
Department *string `json:"department"`
Grade *string `json:"grade"`
Position *string `json:"position"`
@@ -1950,6 +2091,8 @@ func (h *UserHandler) BulkUpdateUsers(c *fiber.Ctx) error {
// Prepare updates
traits := identity.Traits
oldRoleForSync := roleFromTraits(traits)
oldTenantIDForSync := extractTraitString(traits, "tenant_id")
if req.Role != nil {
traits["role"] = *req.Role
}
@@ -1957,8 +2100,30 @@ func (h *UserHandler) BulkUpdateUsers(c *fiber.Ctx) error {
delete(traits, "companyCode")
delete(traits, "companyCodes")
// Resolve and update tenant_id in traits if changed
if tItem, exists := tenantCache[*req.CompanyCode]; exists {
if req.IsAddTenant {
if h.TenantService == nil {
results = append(results, map[string]any{"id": id, "success": false, "message": "tenant service not available"})
continue
}
tenant, err := h.TenantService.GetTenantBySlug(c.Context(), *req.CompanyCode)
if err != nil || tenant == nil {
results = append(results, map[string]any{"id": id, "success": false, "message": "invalid tenant assignment"})
continue
}
metadata := mergeUserAddTenantAppointment(traits, nil, tenant)
if appointments, ok := metadata["additionalAppointments"]; ok {
traits["additionalAppointments"] = appointments
}
if h.KetoOutboxRepo != nil {
_ = h.KetoOutboxRepo.Create(c.Context(), &domain.KetoOutbox{
Namespace: "Tenant",
Object: tenant.ID,
Relation: "members",
Subject: "User:" + id,
Action: domain.KetoOutboxActionCreate,
})
}
} else if tItem, exists := tenantCache[*req.CompanyCode]; exists {
traits["tenant_id"] = tItem.ID
} else if h.TenantService != nil {
tenant, err := h.TenantService.GetTenantBySlug(c.Context(), *req.CompanyCode)
@@ -1990,7 +2155,7 @@ func (h *UserHandler) BulkUpdateUsers(c *fiber.Ctx) error {
}
}
_, err = h.KratosAdmin.UpdateIdentity(c.Context(), id, traits, state)
updated, err := h.KratosAdmin.UpdateIdentity(c.Context(), id, traits, state)
if err != nil {
results = append(results, map[string]any{"id": id, "success": false, "message": err.Error()})
continue
@@ -1998,9 +2163,7 @@ func (h *UserHandler) BulkUpdateUsers(c *fiber.Ctx) error {
// Sync to local DB
if h.UserRepo != nil {
localUser := h.mapToLocalUser(*identity)
oldRole := roleFromTraits(identity.Traits)
oldTenantID := extractTraitString(identity.Traits, "tenant_id")
localUser := h.mapToLocalUser(*updated)
if req.Role != nil {
localUser.Role = *req.Role
@@ -2035,7 +2198,7 @@ func (h *UserHandler) BulkUpdateUsers(c *fiber.Ctx) error {
// [Keto Sync]
if h.KetoOutboxRepo != nil {
h.syncKetoRole(c.Context(), localUser.ID,
localUser.Role, oldRole, oldTenantID, localUser.TenantID)
localUser.Role, oldRoleForSync, oldTenantIDForSync, localUser.TenantID)
}
}
@@ -2241,6 +2404,9 @@ func (h *UserHandler) UpdateUser(c *fiber.Ctx) error {
// [Validation] Based on Tenant Schema (Multi-tenant aware)
isAdmin := requester != nil && requester.Role == domain.RoleSuperAdmin
if err := enforceGlobalCustomClaimWritePermissions(identity.Traits, req.Metadata, isAdmin); err != nil {
return errorJSON(c, fiber.StatusForbidden, "forbidden: "+err.Error())
}
// If metadata is namespaced (key is tenant ID), validate each namespace
// If it's flat, validate using schemaCompCode
@@ -2329,26 +2495,22 @@ func (h *UserHandler) UpdateUser(c *fiber.Ctx) error {
code := strings.TrimSpace(*req.CompanyCode)
if req.IsRemoveTenant {
if h.TenantService != nil && h.KetoOutboxRepo != nil && code != "" {
go func(removedSlug string) {
bgCtx := context.Background()
if t, err := h.TenantService.GetTenantBySlug(bgCtx, removedSlug); err == nil && t != nil {
_ = h.KetoOutboxRepo.Create(bgCtx, &domain.KetoOutbox{
Namespace: "Tenant",
Object: t.ID,
Relation: "members",
Subject: "User:" + userID,
Action: domain.KetoOutboxActionDelete,
})
}
}(code)
}
if h.TenantService != nil && code != "" {
if tenant, err := h.TenantService.GetTenantBySlug(c.Context(), code); err == nil && tenant != nil {
currentTenantID := extractTraitString(traits, "tenant_id")
if currentTenantID == tenant.ID {
traits["tenant_id"] = ""
}
req.Metadata = removeUserTenantAppointment(traits, req.Metadata, tenant)
if h.KetoOutboxRepo != nil {
_ = h.KetoOutboxRepo.Create(c.Context(), &domain.KetoOutbox{
Namespace: "Tenant",
Object: tenant.ID,
Relation: "members",
Subject: "User:" + userID,
Action: domain.KetoOutboxActionDelete,
})
}
}
}
} else if !req.IsAddTenant {