1
0
forked from baron/baron-sso

Implement tenant import and RP auto login policies

This commit is contained in:
2026-04-30 15:45:34 +09:00
parent 24807eab0f
commit f7e4d43b16
76 changed files with 5307 additions and 441 deletions

View File

@@ -425,6 +425,15 @@ func (h *UserHandler) CreateUser(c *fiber.Ctx) error {
attributes["tenant_id"] = tenantID
}
if h.UserRepo != nil {
if err := h.ensureHanmacCreateEmailAllowed(c.Context(), email, req.CompanyCode, tenantID); err != nil {
if strings.Contains(err.Error(), "한맥가족") {
return errorJSON(c, fiber.StatusConflict, err.Error())
}
return errorJSON(c, fiber.StatusBadRequest, err.Error())
}
}
// Merge custom metadata into attributes
for k, v := range req.Metadata {
// Don't overwrite core fields
@@ -534,10 +543,14 @@ type bulkUserItem struct {
}
type bulkUserResult struct {
Email string `json:"email"`
Success bool `json:"success"`
Message string `json:"message,omitempty"`
UserID string `json:"userId,omitempty"`
Email string `json:"email"`
OriginalEmail string `json:"originalEmail,omitempty"`
SuggestedEmail string `json:"suggestedEmail,omitempty"`
Status string `json:"status,omitempty"`
Warnings []string `json:"warnings,omitempty"`
Success bool `json:"success"`
Message string `json:"message,omitempty"`
UserID string `json:"userId,omitempty"`
}
func (h *UserHandler) BulkCreateUsers(c *fiber.Ctx) error {
@@ -565,6 +578,9 @@ func (h *UserHandler) BulkCreateUsers(c *fiber.Ctx) error {
requester, _ := c.Locals("user_profile").(*domain.UserProfileResponse)
results := make([]bulkUserResult, 0, len(req.Users))
var hanmacScope *hanmacEmailScope
var hanmacLocalParts map[string]bool
hanmacScopeLoaded := false
// Pre-fetch tenant data to avoid redundant DB calls
type tenantCacheItem struct {
@@ -638,6 +654,53 @@ func (h *UserHandler) BulkCreateUsers(c *fiber.Ctx) error {
}
}
if h.UserRepo != nil && !hanmacScopeLoaded {
hanmacScopeLoaded = true
var err error
hanmacScope, err = h.resolveHanmacEmailScope(c.Context())
if err != nil {
results = append(results, bulkUserResult{Email: email, Success: false, Message: "failed to resolve Hanmac family tenant scope"})
continue
}
if hanmacScope != nil {
hanmacLocalParts, err = h.loadHanmacLocalParts(c.Context(), hanmacScope)
if err != nil {
results = append(results, bulkUserResult{Email: email, Success: false, Message: "failed to validate Hanmac family email policy"})
continue
}
}
}
userEmail := email
var emailEvaluation hanmacEmailEvaluation
if h.UserRepo != nil && hanmacScope != nil && hanmacScope.ContainsTenant(tItem.ID, tenantSlug) {
emailEvaluation = h.evaluateHanmacImportEmail(c.Context(), item, hanmacScope, hanmacLocalParts)
if emailEvaluation.Blocking {
results = append(results, bulkUserResult{
Email: emailEvaluation.Email,
OriginalEmail: emailEvaluation.OriginalEmail,
Status: emailEvaluation.Status,
Warnings: emailEvaluation.Warnings,
Success: false,
Message: emailEvaluation.Message,
})
continue
}
userEmail = emailEvaluation.Email
if emailEvaluation.LocalPart != "" {
hanmacLocalParts[emailEvaluation.LocalPart] = true
}
} else {
if _, _, err := domain.SplitEmailDomain(email); err != nil {
results = append(results, bulkUserResult{Email: email, Success: false, Status: "blockingError", Message: "invalid email format"})
continue
}
if localPart, err := domain.ExtractNormalizedEmailLocalPart(email); err != nil || localPart == "" {
results = append(results, bulkUserResult{Email: email, Success: false, Status: "blockingError", Message: "invalid email format"})
continue
}
}
password, _ := utils.GeneratePasswordWithPolicy(policy)
role := item.Role
if role == "" {
@@ -665,7 +728,6 @@ func (h *UserHandler) BulkCreateUsers(c *fiber.Ctx) error {
}
}
userEmail := email
userPhone := normalizePhoneNumber(item.Phone)
// Validate all collected LoginIDs
@@ -673,7 +735,7 @@ func (h *UserHandler) BulkCreateUsers(c *fiber.Ctx) error {
valid := true
for _, lid := range collectedIDs {
if err := domain.ValidateLoginID(lid, userEmail, userPhone); err != nil {
results = append(results, bulkUserResult{Email: email, Success: false, Message: "Invalid LoginID (" + lid + "): " + err.Error()})
results = append(results, bulkUserResult{Email: userEmail, OriginalEmail: emailEvaluation.OriginalEmail, SuggestedEmail: emailEvaluation.SuggestedEmail, Status: emailEvaluation.Status, Warnings: emailEvaluation.Warnings, Success: false, Message: "Invalid LoginID (" + lid + "): " + err.Error()})
valid = false
break
}
@@ -692,14 +754,14 @@ func (h *UserHandler) BulkCreateUsers(c *fiber.Ctx) error {
if err != nil {
// 만약 이미 존재하는 사용자라면 로컬 DB 및 Keto 관계만 업데이트(Sync)를 시도
if strings.Contains(err.Error(), "409") || strings.Contains(err.Error(), "already exists") || strings.Contains(err.Error(), "exists already") {
identityID, err = h.KratosAdmin.FindIdentityIDByIdentifier(c.Context(), email)
identityID, err = h.KratosAdmin.FindIdentityIDByIdentifier(c.Context(), userEmail)
if err != nil || identityID == "" {
results = append(results, bulkUserResult{Email: email, Success: false, Message: "이미 다른 사용자가 해당 식별자(이메일/사번 등)를 사용 중입니다."})
results = append(results, bulkUserResult{Email: userEmail, OriginalEmail: emailEvaluation.OriginalEmail, SuggestedEmail: emailEvaluation.SuggestedEmail, Status: "blockingError", Warnings: emailEvaluation.Warnings, Success: false, Message: "이미 다른 사용자가 해당 식별자(이메일/사번 등)를 사용 중입니다."})
continue
}
slog.Info("BulkCreate: User already exists, syncing local DB and Keto", "email", email, "identityID", identityID)
} else {
results = append(results, bulkUserResult{Email: email, Success: false, Message: err.Error()})
results = append(results, bulkUserResult{Email: userEmail, OriginalEmail: emailEvaluation.OriginalEmail, SuggestedEmail: emailEvaluation.SuggestedEmail, Status: emailEvaluation.Status, Warnings: emailEvaluation.Warnings, Success: false, Message: err.Error()})
continue
}
}
@@ -709,7 +771,7 @@ func (h *UserHandler) BulkCreateUsers(c *fiber.Ctx) error {
if h.UserRepo != nil {
localUser := &domain.User{
ID: identityID,
Email: email,
Email: userEmail,
Name: name,
Phone: normalizePhoneNumber(item.Phone),
Role: role,
@@ -776,7 +838,15 @@ func (h *UserHandler) BulkCreateUsers(c *fiber.Ctx) error {
}
}
results = append(results, bulkUserResult{Email: email, Success: true, UserID: identityID})
results = append(results, bulkUserResult{
Email: userEmail,
OriginalEmail: emailEvaluation.OriginalEmail,
SuggestedEmail: emailEvaluation.SuggestedEmail,
Status: emailEvaluation.Status,
Warnings: emailEvaluation.Warnings,
Success: true,
UserID: identityID,
})
}
return c.Status(fiber.StatusOK).JSON(fiber.Map{
@@ -870,12 +940,19 @@ func (h *UserHandler) ExportUsersCSV(c *fiber.Ctx) error {
defer writer.Flush()
// Header row
header := []string{"ID", "Email", "Name", "Phone", "Status", "Tenant", "Position", "JobTitle", "CreatedAt"}
includeIDs := includeCSVIds(c)
header := []string{"Email", "Name", "Phone", "Status", "tenant_slug", "Position", "JobTitle", "CreatedAt"}
if includeIDs {
header = []string{"user_id", "Email", "Name", "Phone", "Status", "tenant_id", "tenant_slug", "Position", "JobTitle", "CreatedAt"}
}
// Collect all possible metadata keys for dynamic columns
metaKeysMap := make(map[string]bool)
for _, u := range filtered {
for k := range u.Metadata {
if !includeIDs && csvMetadataKeyIsID(k) {
continue
}
metaKeysMap[k] = true
}
}
@@ -891,8 +968,11 @@ func (h *UserHandler) ExportUsersCSV(c *fiber.Ctx) error {
// Data rows
for _, u := range filtered {
tenantID := ""
if u.TenantID != nil {
tenantID = *u.TenantID
}
row := []string{
u.ID,
u.Email,
u.Name,
u.Phone,
@@ -902,6 +982,20 @@ func (h *UserHandler) ExportUsersCSV(c *fiber.Ctx) error {
u.JobTitle,
u.CreatedAt.Format(time.RFC3339),
}
if includeIDs {
row = []string{
u.ID,
u.Email,
u.Name,
u.Phone,
u.Status,
tenantID,
u.CompanyCode,
u.Position,
u.JobTitle,
u.CreatedAt.Format(time.RFC3339),
}
}
// Append metadata values in order
for _, k := range metaKeys {
val := ""
@@ -918,6 +1012,11 @@ func (h *UserHandler) ExportUsersCSV(c *fiber.Ctx) error {
return nil
}
func csvMetadataKeyIsID(key string) bool {
normalized := strings.ToLower(strings.TrimSpace(key))
return normalized == "id" || normalized == "user_id" || normalized == "tenant_id" || normalized == "tenantid"
}
func (h *UserHandler) BulkUpdateUsers(c *fiber.Ctx) error {
var req struct {
UserIDs []string `json:"userIds"`