forked from baron/baron-sso
custom claim 권한체크 확인
This commit is contained in:
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user