forked from baron/baron-sso
사용자 테넌트 소속 데이터 정리
This commit is contained in:
@@ -256,6 +256,26 @@ func tenantSlugPointerFromRequest(tenantSlug *string, legacyCompanyCode *string)
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func identityTenantAccessKeys(traits map[string]interface{}) []string {
|
||||
keys := make([]string, 0, 2)
|
||||
if tenantID := strings.ToLower(strings.TrimSpace(extractTraitString(traits, "tenant_id"))); tenantID != "" {
|
||||
keys = append(keys, tenantID)
|
||||
}
|
||||
if legacySlug := strings.ToLower(strings.TrimSpace(extractTraitString(traits, "companyCode"))); legacySlug != "" {
|
||||
keys = append(keys, legacySlug)
|
||||
}
|
||||
return keys
|
||||
}
|
||||
|
||||
func anyTenantKeyAllowed(keys []string, allowed map[string]bool) bool {
|
||||
for _, key := range keys {
|
||||
if allowed[key] {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
type userSummary struct {
|
||||
ID string `json:"id"`
|
||||
Email string `json:"email"`
|
||||
@@ -590,7 +610,11 @@ func (h *UserHandler) CreateUser(c *fiber.Ctx) error {
|
||||
if err := c.BodyParser(&req); err != nil {
|
||||
return errorJSON(c, fiber.StatusBadRequest, "invalid request body")
|
||||
}
|
||||
req.CompanyCode = tenantSlugFromRequest(req.TenantSlug, req.CompanyCode)
|
||||
tenantSlug, err := tenantSlugFromRequest(req.TenantSlug, req.CompanyCode)
|
||||
if err != nil {
|
||||
return errorJSON(c, fiber.StatusBadRequest, err.Error())
|
||||
}
|
||||
req.CompanyCode = tenantSlug
|
||||
req.Metadata = sanitizeUserMetadata(mergeUserAppointmentMetadata(req.Metadata, req.AdditionalAppointments, req.PrimaryTenantID, req.PrimaryTenantName, req.PrimaryTenantIsOwner))
|
||||
|
||||
email := strings.TrimSpace(req.Email)
|
||||
@@ -643,7 +667,6 @@ func (h *UserHandler) CreateUser(c *fiber.Ctx) error {
|
||||
"position": req.Position,
|
||||
"jobTitle": req.JobTitle,
|
||||
"affiliationType": "internal",
|
||||
"companyCode": req.CompanyCode,
|
||||
}
|
||||
|
||||
// [Override with explicit LoginID if provided]
|
||||
@@ -687,7 +710,6 @@ func (h *UserHandler) CreateUser(c *fiber.Ctx) error {
|
||||
loginIDRecords := syncCustomLoginIDs(c.Context(), h.TenantService, attributes, req.Metadata, "")
|
||||
|
||||
attributes["role"] = role
|
||||
attributes["companyCode"] = req.CompanyCode
|
||||
if tenantID != "" {
|
||||
attributes["tenant_id"] = tenantID
|
||||
}
|
||||
@@ -969,13 +991,17 @@ func (h *UserHandler) BulkCreateUsers(c *fiber.Ctx) error {
|
||||
email := strings.TrimSpace(item.Email)
|
||||
name := strings.TrimSpace(item.Name)
|
||||
tenantID := strings.TrimSpace(item.TenantID)
|
||||
tenantSlug := tenantSlugFromRequest(item.TenantSlug, item.CompanyCode)
|
||||
tenantSlug, tenantSlugErr := tenantSlugFromRequest(item.TenantSlug, item.CompanyCode)
|
||||
dept := strings.TrimSpace(item.Department)
|
||||
|
||||
if email == "" || name == "" {
|
||||
results = append(results, bulkUserResult{Email: email, Success: false, Message: "email and name are required"})
|
||||
continue
|
||||
}
|
||||
if tenantSlugErr != nil {
|
||||
results = append(results, bulkUserResult{Email: email, Success: false, Message: tenantSlugErr.Error()})
|
||||
continue
|
||||
}
|
||||
|
||||
var tItem tenantCacheItem
|
||||
var err error
|
||||
@@ -1156,7 +1182,6 @@ func (h *UserHandler) BulkCreateUsers(c *fiber.Ctx) error {
|
||||
"position": strings.TrimSpace(item.Position),
|
||||
"jobTitle": strings.TrimSpace(item.JobTitle),
|
||||
"affiliationType": "internal",
|
||||
"companyCode": tenantSlug,
|
||||
"tenant_id": tItem.ID,
|
||||
"role": role,
|
||||
}
|
||||
@@ -1307,7 +1332,10 @@ func (h *UserHandler) BulkCreateUsers(c *fiber.Ctx) error {
|
||||
|
||||
func (h *UserHandler) ExportUsersCSV(c *fiber.Ctx) error {
|
||||
search := strings.TrimSpace(c.Query("search"))
|
||||
tenantSlug := tenantSlugFromRequest(c.Query("tenantSlug"), c.Query("companyCode"))
|
||||
tenantSlug, err := tenantSlugFromRequest(c.Query("tenantSlug"), c.Query("companyCode"))
|
||||
if err != nil {
|
||||
return errorJSON(c, fiber.StatusBadRequest, err.Error())
|
||||
}
|
||||
|
||||
var requesterRole string
|
||||
var manageableSlugs []string
|
||||
@@ -1481,7 +1509,11 @@ func (h *UserHandler) BulkUpdateUsers(c *fiber.Ctx) error {
|
||||
if err := c.BodyParser(&req); err != nil {
|
||||
return errorJSON(c, fiber.StatusBadRequest, "invalid request body")
|
||||
}
|
||||
req.CompanyCode = tenantSlugPointerFromRequest(req.TenantSlug, req.CompanyCode)
|
||||
tenantSlug, err := tenantSlugPointerFromRequest(req.TenantSlug, req.CompanyCode)
|
||||
if err != nil {
|
||||
return errorJSON(c, fiber.StatusBadRequest, err.Error())
|
||||
}
|
||||
req.CompanyCode = tenantSlug
|
||||
|
||||
if len(req.UserIDs) == 0 {
|
||||
return errorJSON(c, fiber.StatusBadRequest, "no user IDs provided")
|
||||
@@ -1539,9 +1571,8 @@ func (h *UserHandler) BulkUpdateUsers(c *fiber.Ctx) error {
|
||||
}
|
||||
|
||||
// Authorization check
|
||||
userComp := strings.ToLower(extractTraitString(identity.Traits, "companyCode"))
|
||||
if requester.Role == domain.RoleTenantAdmin {
|
||||
if !manageableSlugs[userComp] {
|
||||
if !anyTenantKeyAllowed(identityTenantAccessKeys(identity.Traits), manageableSlugs) {
|
||||
results = append(results, map[string]any{"id": id, "success": false, "message": "forbidden: user belongs to another tenant"})
|
||||
continue
|
||||
}
|
||||
@@ -1560,7 +1591,8 @@ func (h *UserHandler) BulkUpdateUsers(c *fiber.Ctx) error {
|
||||
traits["role"] = *req.Role
|
||||
}
|
||||
if req.CompanyCode != nil {
|
||||
traits["companyCode"] = *req.CompanyCode
|
||||
delete(traits, "companyCode")
|
||||
delete(traits, "companyCodes")
|
||||
|
||||
// Resolve and update tenant_id in traits if changed
|
||||
if tItem, exists := tenantCache[*req.CompanyCode]; exists {
|
||||
@@ -1702,8 +1734,7 @@ func (h *UserHandler) BulkDeleteUsers(c *fiber.Ctx) error {
|
||||
|
||||
// Authorization check
|
||||
if requester.Role == domain.RoleTenantAdmin {
|
||||
userComp := strings.ToLower(extractTraitString(identity.Traits, "companyCode"))
|
||||
if !manageableSlugs[userComp] {
|
||||
if !anyTenantKeyAllowed(identityTenantAccessKeys(identity.Traits), manageableSlugs) {
|
||||
results = append(results, map[string]any{"id": id, "success": false, "message": "forbidden"})
|
||||
continue
|
||||
}
|
||||
@@ -1767,8 +1798,18 @@ func (h *UserHandler) UpdateUser(c *fiber.Ctx) error {
|
||||
// [New] Check access scope
|
||||
requester, _ := c.Locals("user_profile").(*domain.UserProfileResponse)
|
||||
if requester != nil && domain.NormalizeRole(requester.Role) == domain.RoleTenantAdmin {
|
||||
compCode := extractTraitString(identity.Traits, "companyCode")
|
||||
if requester.CompanyCode == "" || compCode != requester.CompanyCode {
|
||||
allowed := map[string]bool{}
|
||||
if requester.TenantID != nil {
|
||||
allowed[strings.ToLower(*requester.TenantID)] = true
|
||||
}
|
||||
if requester.CompanyCode != "" {
|
||||
allowed[strings.ToLower(requester.CompanyCode)] = true
|
||||
}
|
||||
for _, tenant := range requester.ManageableTenants {
|
||||
allowed[strings.ToLower(tenant.ID)] = true
|
||||
allowed[strings.ToLower(tenant.Slug)] = true
|
||||
}
|
||||
if !anyTenantKeyAllowed(identityTenantAccessKeys(identity.Traits), allowed) {
|
||||
return errorJSON(c, fiber.StatusForbidden, "forbidden: cannot update user in another tenant")
|
||||
}
|
||||
}
|
||||
@@ -1797,7 +1838,11 @@ func (h *UserHandler) UpdateUser(c *fiber.Ctx) error {
|
||||
if err := c.BodyParser(&req); err != nil {
|
||||
return errorJSON(c, fiber.StatusBadRequest, "invalid request body")
|
||||
}
|
||||
req.CompanyCode = tenantSlugPointerFromRequest(req.TenantSlug, req.CompanyCode)
|
||||
tenantSlug, err := tenantSlugPointerFromRequest(req.TenantSlug, req.CompanyCode)
|
||||
if err != nil {
|
||||
return errorJSON(c, fiber.StatusBadRequest, err.Error())
|
||||
}
|
||||
req.CompanyCode = tenantSlug
|
||||
req.Metadata = sanitizeUserMetadata(mergeUserAppointmentMetadata(req.Metadata, req.AdditionalAppointments, req.PrimaryTenantID, req.PrimaryTenantName, req.PrimaryTenantIsOwner))
|
||||
if req.Role != nil {
|
||||
if requester == nil || domain.NormalizeRole(requester.Role) != domain.RoleSuperAdmin {
|
||||
@@ -1866,24 +1911,6 @@ func (h *UserHandler) UpdateUser(c *fiber.Ctx) error {
|
||||
delete(traits, "hanmacFamily")
|
||||
delete(traits, "userType")
|
||||
|
||||
// [Preserve & Merge] Multi-Tenant Info
|
||||
var existingCodes []string
|
||||
if codes, ok := traits["companyCodes"].([]interface{}); ok {
|
||||
for _, v := range codes {
|
||||
if str, ok := v.(string); ok && str != "" {
|
||||
existingCodes = append(existingCodes, str)
|
||||
}
|
||||
}
|
||||
}
|
||||
// Keto에서 "실제" 소속 정보를 먼저 확인 (엑셀 임포트 사용자 대응)
|
||||
if len(existingCodes) <= 1 && h.TenantService != nil {
|
||||
if joined, err := h.TenantService.ListJoinedTenants(c.Context(), userID); err == nil {
|
||||
for _, t := range joined {
|
||||
existingCodes = append(existingCodes, t.Slug)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if req.Name != nil {
|
||||
traits["name"] = strings.TrimSpace(*req.Name)
|
||||
}
|
||||
@@ -1898,29 +1925,7 @@ func (h *UserHandler) UpdateUser(c *fiber.Ctx) error {
|
||||
if req.CompanyCode != nil {
|
||||
code := strings.TrimSpace(*req.CompanyCode)
|
||||
|
||||
if req.IsAddTenant {
|
||||
// Add to existingCodes if not present
|
||||
found := false
|
||||
for _, existing := range existingCodes {
|
||||
if existing == code {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !found && code != "" {
|
||||
existingCodes = append(existingCodes, code)
|
||||
}
|
||||
} else if req.IsRemoveTenant {
|
||||
// Remove from existingCodes
|
||||
var newCodes []string
|
||||
for _, existing := range existingCodes {
|
||||
if existing != code {
|
||||
newCodes = append(newCodes, existing)
|
||||
}
|
||||
}
|
||||
existingCodes = newCodes
|
||||
|
||||
// [Keto Sync] Remove membership for the target tenant
|
||||
if req.IsRemoveTenant {
|
||||
if h.TenantService != nil && h.KetoOutboxRepo != nil && code != "" {
|
||||
go func(removedSlug string) {
|
||||
bgCtx := context.Background()
|
||||
@@ -1935,87 +1940,26 @@ func (h *UserHandler) UpdateUser(c *fiber.Ctx) error {
|
||||
}
|
||||
}(code)
|
||||
}
|
||||
|
||||
// If removing the primary company code, pick another one as primary if available
|
||||
currentPrimary := extractTraitString(traits, "companyCode")
|
||||
if currentPrimary == code {
|
||||
if len(existingCodes) > 0 {
|
||||
traits["companyCode"] = existingCodes[0]
|
||||
if h.TenantService != nil {
|
||||
if t, err := h.TenantService.GetTenantBySlug(c.Context(), existingCodes[0]); err == nil && t != nil {
|
||||
traits["tenant_id"] = t.ID
|
||||
}
|
||||
}
|
||||
} else {
|
||||
traits["companyCode"] = ""
|
||||
traits["tenant_id"] = ""
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Normal update (Move): replace primary company code and remove the old one from existingCodes
|
||||
currentPrimary := extractTraitString(traits, "companyCode")
|
||||
if currentPrimary != "" && currentPrimary != code {
|
||||
// Remove old primary from existingCodes
|
||||
var newCodes []string
|
||||
for _, existing := range existingCodes {
|
||||
if existing != currentPrimary {
|
||||
newCodes = append(newCodes, existing)
|
||||
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"] = ""
|
||||
}
|
||||
}
|
||||
existingCodes = newCodes
|
||||
|
||||
// [Keto Sync] Remove membership for the old tenant
|
||||
if h.TenantService != nil && h.KetoOutboxRepo != nil {
|
||||
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,
|
||||
})
|
||||
}
|
||||
}(currentPrimary)
|
||||
}
|
||||
}
|
||||
|
||||
traits["companyCode"] = code
|
||||
// Resolve TenantID for Kratos Trait
|
||||
} else if !req.IsAddTenant {
|
||||
if h.TenantService != nil && code != "" {
|
||||
if tenant, err := h.TenantService.GetTenantBySlug(c.Context(), code); err == nil && tenant != nil {
|
||||
traits["tenant_id"] = tenant.ID
|
||||
} else {
|
||||
traits["tenant_id"] = ""
|
||||
}
|
||||
}
|
||||
|
||||
found := false
|
||||
for _, existing := range existingCodes {
|
||||
if existing == code {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !found && code != "" {
|
||||
existingCodes = append(existingCodes, code)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Deduplicate and save back companyCodes
|
||||
var codesToSave []string
|
||||
seenCodes := map[string]bool{}
|
||||
for _, c := range existingCodes {
|
||||
if !seenCodes[c] && c != "" {
|
||||
seenCodes[c] = true
|
||||
codesToSave = append(codesToSave, c)
|
||||
}
|
||||
}
|
||||
if len(codesToSave) > 0 {
|
||||
traits["companyCodes"] = codesToSave
|
||||
} else {
|
||||
delete(traits, "companyCodes")
|
||||
}
|
||||
delete(traits, "companyCode")
|
||||
delete(traits, "companyCodes")
|
||||
|
||||
if req.Department != nil {
|
||||
traits["department"] = strings.TrimSpace(*req.Department)
|
||||
@@ -2233,8 +2177,18 @@ func (h *UserHandler) DeleteUser(c *fiber.Ctx) error {
|
||||
}
|
||||
if requester != nil && domain.NormalizeRole(requester.Role) == domain.RoleTenantAdmin {
|
||||
if identity != nil {
|
||||
compCode := extractTraitString(identity.Traits, "companyCode")
|
||||
if requester.CompanyCode == "" || compCode != requester.CompanyCode {
|
||||
allowed := map[string]bool{}
|
||||
if requester.TenantID != nil {
|
||||
allowed[strings.ToLower(*requester.TenantID)] = true
|
||||
}
|
||||
if requester.CompanyCode != "" {
|
||||
allowed[strings.ToLower(requester.CompanyCode)] = true
|
||||
}
|
||||
for _, tenant := range requester.ManageableTenants {
|
||||
allowed[strings.ToLower(tenant.ID)] = true
|
||||
allowed[strings.ToLower(tenant.Slug)] = true
|
||||
}
|
||||
if !anyTenantKeyAllowed(identityTenantAccessKeys(identity.Traits), allowed) {
|
||||
return errorJSON(c, fiber.StatusForbidden, "forbidden: cannot delete user in another tenant")
|
||||
}
|
||||
}
|
||||
@@ -2277,8 +2231,16 @@ func (h *UserHandler) mapIdentitySummary(ctx context.Context, identity service.K
|
||||
traits := identity.Traits
|
||||
role := roleFromTraits(traits)
|
||||
|
||||
compCode := extractTraitString(traits, "companyCode")
|
||||
slog.Debug("Mapping identity", "email", extractTraitString(traits, "email"), "compCode", compCode)
|
||||
tenantID := extractTraitString(traits, "tenant_id")
|
||||
tenantSlug := ""
|
||||
var tenantSummary *domain.Tenant
|
||||
if tenantID != "" && h.TenantService != nil {
|
||||
if tenant, err := h.TenantService.GetTenant(ctx, tenantID); err == nil && tenant != nil {
|
||||
tenantSlug = tenant.Slug
|
||||
tenantSummary = tenant
|
||||
}
|
||||
}
|
||||
slog.Debug("Mapping identity", "email", extractTraitString(traits, "email"), "tenantID", tenantID, "tenantSlug", tenantSlug)
|
||||
|
||||
var customLoginIDs []string
|
||||
if raw, ok := traits["custom_login_ids"]; ok {
|
||||
@@ -2302,8 +2264,8 @@ func (h *UserHandler) mapIdentitySummary(ctx context.Context, identity service.K
|
||||
Phone: extractTraitString(traits, "phone_number"),
|
||||
Role: role,
|
||||
Status: normalizeStatus(identity.State),
|
||||
TenantSlug: compCode,
|
||||
CompanyCode: compCode,
|
||||
TenantSlug: tenantSlug,
|
||||
CompanyCode: tenantSlug,
|
||||
Department: extractTraitString(traits, "department"),
|
||||
Grade: gradeFromTraits(traits),
|
||||
Position: extractTraitString(traits, "position"),
|
||||
@@ -2326,7 +2288,7 @@ func (h *UserHandler) mapIdentitySummary(ctx context.Context, identity service.K
|
||||
// Otherwise, we put them in a "legacy" or "flat" bucket if needed, but for now let's keep them in summary.Metadata
|
||||
coreTraits := map[string]bool{
|
||||
"email": true, "name": true, "phone_number": true,
|
||||
"grade": true, "companyCode": true, "department": true,
|
||||
"grade": true, "companyCode": true, "company_code": true, "companyCodes": true, "department": true,
|
||||
"position": true, "jobTitle": true,
|
||||
"affiliationType": true, "role": true, "tenant_id": true,
|
||||
"custom_login_ids": true, "id": true,
|
||||
@@ -2341,11 +2303,7 @@ func (h *UserHandler) mapIdentitySummary(ctx context.Context, identity service.K
|
||||
summary.Metadata[k] = v
|
||||
}
|
||||
|
||||
if compCode != "" && h.TenantService != nil {
|
||||
if tenant, err := h.TenantService.GetTenantBySlug(ctx, compCode); err == nil && tenant != nil {
|
||||
summary.Tenant = tenant
|
||||
}
|
||||
}
|
||||
summary.Tenant = tenantSummary
|
||||
|
||||
return summary
|
||||
}
|
||||
@@ -2357,10 +2315,6 @@ func (h *UserHandler) normalizePhoneNumber(phone string) string {
|
||||
func (h *UserHandler) mapToLocalUser(identity service.KratosIdentity) *domain.User {
|
||||
traits := identity.Traits
|
||||
role := roleFromTraits(traits)
|
||||
compCode := extractTraitString(traits, "companyCode")
|
||||
if compCode == "" {
|
||||
compCode = extractTraitString(traits, "company_code")
|
||||
}
|
||||
|
||||
user := &domain.User{
|
||||
ID: identity.ID,
|
||||
@@ -2369,7 +2323,6 @@ func (h *UserHandler) mapToLocalUser(identity service.KratosIdentity) *domain.Us
|
||||
Phone: extractTraitString(traits, "phone_number"),
|
||||
Role: role,
|
||||
Status: normalizeStatus(identity.State),
|
||||
CompanyCode: compCode,
|
||||
Department: extractTraitString(traits, "department"),
|
||||
Grade: gradeFromTraits(traits),
|
||||
Position: extractTraitString(traits, "position"),
|
||||
@@ -2379,37 +2332,17 @@ func (h *UserHandler) mapToLocalUser(identity service.KratosIdentity) *domain.Us
|
||||
UpdatedAt: identity.UpdatedAt,
|
||||
}
|
||||
|
||||
// [New] Sync multi-tenant codes
|
||||
if codes, ok := traits["companyCodes"].([]interface{}); ok {
|
||||
for _, v := range codes {
|
||||
if str, ok := v.(string); ok && str != "" {
|
||||
user.CompanyCodes = append(user.CompanyCodes, str)
|
||||
}
|
||||
}
|
||||
} else if codes, ok := traits["companyCodes"].([]string); ok {
|
||||
user.CompanyCodes = codes
|
||||
}
|
||||
|
||||
// 1. Try to get tenant_id directly from Kratos traits first (Fastest & most reliable)
|
||||
tID := extractTraitString(traits, "tenant_id")
|
||||
if tID != "" {
|
||||
user.TenantID = &tID
|
||||
}
|
||||
|
||||
// 2. Fallback to slug lookup only if tenant_id trait is missing
|
||||
if (user.TenantID == nil || *user.TenantID == "") && compCode != "" && h.TenantService != nil {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
defer cancel()
|
||||
if tenant, err := h.TenantService.GetTenantBySlug(ctx, compCode); err == nil && tenant != nil {
|
||||
user.TenantID = &tenant.ID
|
||||
}
|
||||
}
|
||||
|
||||
// Metadata handling (exclude core fields)
|
||||
user.Metadata = make(domain.JSONMap)
|
||||
coreTraits := map[string]bool{
|
||||
"email": true, "name": true, "phone_number": true,
|
||||
"grade": true, "companyCode": true, "department": true,
|
||||
"grade": true, "companyCode": true, "companyCodes": true, "department": true,
|
||||
"position": true, "jobTitle": true,
|
||||
"affiliationType": true, "role": true, "tenant_id": true, "company_code": true,
|
||||
"custom_login_ids": true, "id": true,
|
||||
|
||||
Reference in New Issue
Block a user