1
0
forked from baron/baron-sso

동기화 기초구조 마련

This commit is contained in:
2026-05-12 12:25:31 +09:00
parent 3063450ee0
commit 5e649c279f
33 changed files with 3364 additions and 408 deletions

View File

@@ -118,6 +118,47 @@ func primaryTenantIDFromRequest(primaryTenantID string, metadata map[string]any,
return ""
}
func bulkUserEmailDomainCandidates(emailDomain string, email string) []string {
values := make([]string, 0, 2)
seen := map[string]bool{}
add := func(value string) {
normalized := strings.ToLower(strings.TrimSpace(value))
if normalized == "" || seen[normalized] {
return
}
seen[normalized] = true
values = append(values, normalized)
}
for _, value := range strings.FieldsFunc(emailDomain, func(r rune) bool {
return r == ',' || r == ';' || r == '\n' || r == '\r'
}) {
add(value)
}
if _, domainPart, err := domain.SplitEmailDomain(email); err == nil {
add(domainPart)
}
return values
}
func bulkUserAssignmentContainsTenant(appointments []any, primaryTenantID string, tenantID string) bool {
if strings.TrimSpace(tenantID) == "" {
return true
}
if primaryTenantID != "" && primaryTenantID == tenantID {
return true
}
for _, item := range appointments {
appointment, ok := item.(map[string]any)
if !ok {
continue
}
if normalizeMetadataString(appointment["tenantId"]) == tenantID {
return true
}
}
return false
}
func metadataBoolFromMap(metadata map[string]any, keys ...string) (bool, bool) {
for _, key := range keys {
value, ok := metadata[key]
@@ -664,17 +705,20 @@ func (h *UserHandler) CreateUser(c *fiber.Ctx) error {
}
type bulkUserItem struct {
Email string `json:"email"`
LoginID string `json:"loginId"`
Name string `json:"name"`
Phone string `json:"phone"`
Role string `json:"role"`
TenantSlug string `json:"tenantSlug"`
Department string `json:"department"`
Grade string `json:"grade"`
Position string `json:"position"`
JobTitle string `json:"jobTitle"`
Metadata map[string]any `json:"metadata"`
Email string `json:"email"`
LoginID string `json:"loginId"`
Name string `json:"name"`
Phone string `json:"phone"`
Role string `json:"role"`
TenantID string `json:"tenantId"`
TenantSlug string `json:"tenantSlug"`
EmailDomain string `json:"emailDomain"`
Department string `json:"department"`
Grade string `json:"grade"`
Position string `json:"position"`
JobTitle string `json:"jobTitle"`
AdditionalAppointments []map[string]any `json:"additionalAppointments"`
Metadata map[string]any `json:"metadata"`
}
type bulkUserResult struct {
@@ -720,15 +764,103 @@ func (h *UserHandler) BulkCreateUsers(c *fiber.Ctx) error {
// Pre-fetch tenant data to avoid redundant DB calls
type tenantCacheItem struct {
ID string
Slug string
Name string
Schema []interface{}
Groups []domain.UserGroup
LoginIDField string
}
tenantCache := make(map[string]tenantCacheItem)
tenantCacheByID := make(map[string]tenantCacheItem)
tenantCacheByDomain := make(map[string]tenantCacheItem)
buildTenantCacheItem := func(tenant *domain.Tenant) tenantCacheItem {
tItem := tenantCacheItem{
ID: tenant.ID,
Slug: tenant.Slug,
Name: tenant.Name,
}
if s, ok := tenant.Config["userSchema"].([]interface{}); ok {
tItem.Schema = s
}
if lf, ok := tenant.Config["loginIdField"].(string); ok {
tItem.LoginIDField = lf
}
if h.UserGroupRepo != nil {
if groups, err := h.UserGroupRepo.ListByTenantID(c.Context(), tenant.ID); err == nil {
tItem.Groups = groups
}
}
return tItem
}
cacheTenantItem := func(tItem tenantCacheItem) tenantCacheItem {
if tItem.Slug != "" {
tenantCache[strings.ToLower(strings.TrimSpace(tItem.Slug))] = tItem
}
if tItem.ID != "" {
tenantCacheByID[tItem.ID] = tItem
}
return tItem
}
resolveTenantBySlug := func(slug string) (tenantCacheItem, error) {
normalizedSlug := strings.ToLower(strings.TrimSpace(slug))
if normalizedSlug == "" {
return tenantCacheItem{}, errors.New("tenantSlug is required")
}
if tItem, exists := tenantCache[normalizedSlug]; exists {
return tItem, nil
}
if h.TenantService == nil {
return tenantCacheItem{}, errors.New("tenant service unavailable")
}
tenant, err := h.TenantService.GetTenantBySlug(c.Context(), normalizedSlug)
if err != nil || tenant == nil {
return tenantCacheItem{}, errors.New("invalid tenantSlug: tenant not found")
}
return cacheTenantItem(buildTenantCacheItem(tenant)), nil
}
resolveTenantByID := func(tenantID string) (tenantCacheItem, error) {
normalizedID := strings.TrimSpace(tenantID)
if normalizedID == "" {
return tenantCacheItem{}, errors.New("tenantId is required")
}
if tItem, exists := tenantCacheByID[normalizedID]; exists {
return tItem, nil
}
if h.TenantService == nil {
return tenantCacheItem{}, errors.New("tenant service unavailable")
}
tenant, err := h.TenantService.GetTenant(c.Context(), normalizedID)
if err != nil || tenant == nil {
return tenantCacheItem{}, errors.New("invalid tenantId: tenant not found")
}
return cacheTenantItem(buildTenantCacheItem(tenant)), nil
}
resolveTenantByDomain := func(domainName string) (tenantCacheItem, bool) {
normalizedDomain := strings.ToLower(strings.TrimSpace(domainName))
if normalizedDomain == "" || h.TenantService == nil {
return tenantCacheItem{}, false
}
if tItem, exists := tenantCacheByDomain[normalizedDomain]; exists {
return tItem, true
}
tenant, err := h.TenantService.GetTenantByDomain(c.Context(), normalizedDomain)
if err != nil || tenant == nil {
return tenantCacheItem{}, false
}
tItem := cacheTenantItem(buildTenantCacheItem(tenant))
tenantCacheByDomain[normalizedDomain] = tItem
return tItem, true
}
for _, item := range req.Users {
email := strings.TrimSpace(item.Email)
name := strings.TrimSpace(item.Name)
tenantID := strings.TrimSpace(item.TenantID)
tenantSlug := strings.TrimSpace(item.TenantSlug)
dept := strings.TrimSpace(item.Department)
@@ -737,9 +869,38 @@ func (h *UserHandler) BulkCreateUsers(c *fiber.Ctx) error {
continue
}
if tenantSlug == "" {
results = append(results, bulkUserResult{Email: email, Success: false, Message: "tenantSlug is required"})
continue
var tItem tenantCacheItem
var err error
if tenantID != "" {
tItem, err = resolveTenantByID(tenantID)
if err != nil {
results = append(results, bulkUserResult{Email: email, Success: false, Message: err.Error()})
continue
}
if tenantSlug != "" && !strings.EqualFold(tenantSlug, tItem.Slug) {
results = append(results, bulkUserResult{Email: email, Success: false, Message: "tenantId and tenantSlug do not match"})
continue
}
tenantSlug = tItem.Slug
} else if tenantSlug != "" {
tItem, err = resolveTenantBySlug(tenantSlug)
if err != nil {
results = append(results, bulkUserResult{Email: email, Success: false, Message: err.Error()})
continue
}
tenantSlug = tItem.Slug
} else {
for _, domainName := range bulkUserEmailDomainCandidates(item.EmailDomain, email) {
if domainTenant, ok := resolveTenantByDomain(domainName); ok {
tItem = domainTenant
tenantSlug = domainTenant.Slug
break
}
}
if tenantSlug == "" {
results = append(results, bulkUserResult{Email: email, Success: false, Message: "tenant assignment is required"})
continue
}
}
// Role-based access check
@@ -750,33 +911,47 @@ func (h *UserHandler) BulkCreateUsers(c *fiber.Ctx) error {
}
}
// Verify Tenant Existence and Resolve ID (with Cache)
var tItem tenantCacheItem
var exists bool
if tItem, exists = tenantCache[tenantSlug]; !exists {
if h.TenantService != nil {
tenant, err := h.TenantService.GetTenantBySlug(c.Context(), tenantSlug)
if err != nil || tenant == nil {
results = append(results, bulkUserResult{Email: email, Success: false, Message: "invalid tenantSlug: tenant not found"})
resolvedAppointments := make([]any, 0, len(item.AdditionalAppointments)+2)
if len(item.AdditionalAppointments) > 0 {
appointmentFailed := false
for _, rawAppointment := range item.AdditionalAppointments {
appointmentTenantSlug := strings.TrimSpace(normalizeMetadataString(rawAppointment["tenantSlug"]))
if appointmentTenantSlug == "" {
continue
}
tItem.ID = tenant.ID
if s, ok := tenant.Config["userSchema"].([]interface{}); ok {
tItem.Schema = s
if requester != nil && requester.Role == domain.RoleTenantAdmin && appointmentTenantSlug != requester.CompanyCode {
results = append(results, bulkUserResult{Email: email, Success: false, Message: "forbidden: cannot add users to another tenant"})
appointmentFailed = true
break
}
if lf, ok := tenant.Config["loginIdField"].(string); ok {
tItem.LoginIDField = lf
}
// [Fix] Cache user groups for this tenant to match department
if h.UserGroupRepo != nil {
if groups, err := h.UserGroupRepo.ListByTenantID(c.Context(), tenant.ID); err == nil {
tItem.Groups = groups
appointmentTenant, exists := tenantCache[strings.ToLower(appointmentTenantSlug)]
if !exists {
appointmentTenant, err = resolveTenantBySlug(appointmentTenantSlug)
if err != nil {
results = append(results, bulkUserResult{Email: email, Success: false, Message: strings.Replace(err.Error(), "tenantSlug", "additional tenantSlug", 1)})
appointmentFailed = true
break
}
}
tenantCache[tenantSlug] = tItem
} else {
results = append(results, bulkUserResult{Email: email, Success: false, Message: "tenant service unavailable"})
appointment := make(map[string]any, len(rawAppointment)+3)
for key, value := range rawAppointment {
if key == "tenantSlug" || key == "tenantId" || key == "tenantName" {
continue
}
appointment[key] = value
}
appointment["tenantId"] = appointmentTenant.ID
appointment["tenantSlug"] = appointmentTenant.Slug
if name := strings.TrimSpace(normalizeMetadataString(rawAppointment["tenantName"])); name != "" {
appointment["tenantName"] = name
} else {
appointment["tenantName"] = appointmentTenant.Name
}
resolvedAppointments = append(resolvedAppointments, appointment)
}
if appointmentFailed {
continue
}
}
@@ -836,6 +1011,26 @@ func (h *UserHandler) BulkCreateUsers(c *fiber.Ctx) error {
}
}
for _, domainName := range bulkUserEmailDomainCandidates(item.EmailDomain, userEmail) {
domainTenant, ok := resolveTenantByDomain(domainName)
if !ok || bulkUserAssignmentContainsTenant(resolvedAppointments, tItem.ID, domainTenant.ID) {
continue
}
resolvedAppointments = append(resolvedAppointments, map[string]any{
"tenantId": domainTenant.ID,
"tenantSlug": domainTenant.Slug,
"tenantName": domainTenant.Name,
"assignmentSource": "email_domain",
"sourceDomain": strings.ToLower(strings.TrimSpace(domainName)),
})
}
if len(resolvedAppointments) > 0 {
if item.Metadata == nil {
item.Metadata = map[string]any{}
}
item.Metadata["additionalAppointments"] = resolvedAppointments
}
password, _ := utils.GeneratePasswordWithPolicy(policy)
role := item.Role
if role == "" {