forked from baron/baron-sso
Implement tenant import and RP auto login policies
This commit is contained in:
244
backend/internal/handler/hanmac_email_policy.go
Normal file
244
backend/internal/handler/hanmac_email_policy.go
Normal file
@@ -0,0 +1,244 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"baron-sso-backend/internal/domain"
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
)
|
||||
|
||||
const hanmacFamilyTenantSlug = "hanmac-family"
|
||||
|
||||
type hanmacEmailScope struct {
|
||||
TenantIDs map[string]bool
|
||||
Slugs map[string]bool
|
||||
IDList []string
|
||||
SlugList []string
|
||||
}
|
||||
|
||||
type hanmacEmailEvaluation struct {
|
||||
Email string
|
||||
OriginalEmail string
|
||||
SuggestedEmail string
|
||||
Status string
|
||||
Warnings []string
|
||||
Message string
|
||||
Blocking bool
|
||||
LocalPart string
|
||||
}
|
||||
|
||||
func (h *UserHandler) evaluateHanmacImportEmail(ctx context.Context, item bulkUserItem, scope *hanmacEmailScope, usedLocalParts map[string]bool) hanmacEmailEvaluation {
|
||||
originalEmail := strings.TrimSpace(item.Email)
|
||||
name := strings.TrimSpace(item.Name)
|
||||
evaluation := hanmacEmailEvaluation{
|
||||
Email: originalEmail,
|
||||
OriginalEmail: originalEmail,
|
||||
Status: "valid",
|
||||
}
|
||||
|
||||
localPart, domainPart, err := domain.SplitEmailDomain(originalEmail)
|
||||
if err != nil {
|
||||
evaluation.Status = "blockingError"
|
||||
evaluation.Message = "invalid email format"
|
||||
evaluation.Blocking = true
|
||||
return evaluation
|
||||
}
|
||||
|
||||
base, needsReview, _ := domain.BuildKoreanNameEmailBase(name)
|
||||
if needsReview {
|
||||
evaluation.Warnings = append(evaluation.Warnings, "needsReview")
|
||||
evaluation.Status = "needsReview"
|
||||
}
|
||||
|
||||
if localPart == "" {
|
||||
if base == "" {
|
||||
evaluation.Status = "blockingError"
|
||||
evaluation.Message = "이름으로 이메일 ID를 제안할 수 없습니다."
|
||||
evaluation.Blocking = true
|
||||
return evaluation
|
||||
}
|
||||
nextLocalPart := nextAvailableHanmacLocalPart(base, usedLocalParts)
|
||||
evaluation.Email = nextLocalPart + "@" + domainPart
|
||||
evaluation.SuggestedEmail = evaluation.Email
|
||||
evaluation.LocalPart = nextLocalPart
|
||||
evaluation.Status = "suggested"
|
||||
evaluation.Warnings = appendUniqueString(evaluation.Warnings, "suggested")
|
||||
return evaluation
|
||||
}
|
||||
|
||||
evaluation.LocalPart = localPart
|
||||
if usedLocalParts[localPart] {
|
||||
evaluation.Status = "blockingError"
|
||||
evaluation.Message = "한맥가족 내에서 이미 사용 중인 이메일 ID입니다."
|
||||
evaluation.Blocking = true
|
||||
return evaluation
|
||||
}
|
||||
|
||||
if base != "" && !domain.MatchesSuggestedNameRule(localPart, base) {
|
||||
evaluation.Status = "ruleMismatch"
|
||||
evaluation.Warnings = appendUniqueString(evaluation.Warnings, "ruleMismatch")
|
||||
}
|
||||
|
||||
if evaluation.Status == "needsReview" && len(evaluation.Warnings) == 0 {
|
||||
evaluation.Warnings = append(evaluation.Warnings, "needsReview")
|
||||
}
|
||||
_ = scope
|
||||
return evaluation
|
||||
}
|
||||
|
||||
func (h *UserHandler) ensureHanmacCreateEmailAllowed(ctx context.Context, email string, tenantSlug string, tenantID string) error {
|
||||
scope, err := h.resolveHanmacEmailScope(ctx)
|
||||
if err != nil || scope == nil || !scope.ContainsTenant(tenantID, tenantSlug) {
|
||||
return nil
|
||||
}
|
||||
|
||||
localPart, err := domain.ExtractNormalizedEmailLocalPart(email)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
usedLocalParts, err := h.loadHanmacLocalParts(ctx, scope)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if usedLocalParts[localPart] {
|
||||
return fmt.Errorf("한맥가족 내에서 이미 사용 중인 이메일 ID입니다.")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h *UserHandler) resolveHanmacEmailScope(ctx context.Context) (*hanmacEmailScope, error) {
|
||||
if h.TenantService == nil {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
tenants, _, err := h.TenantService.ListTenants(ctx, 10000, 0, "")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var rootID string
|
||||
for _, tenant := range tenants {
|
||||
if strings.EqualFold(strings.TrimSpace(tenant.Slug), hanmacFamilyTenantSlug) {
|
||||
rootID = tenant.ID
|
||||
break
|
||||
}
|
||||
}
|
||||
if rootID == "" {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
tenantByID := make(map[string]domain.Tenant, len(tenants))
|
||||
for _, tenant := range tenants {
|
||||
tenantByID[tenant.ID] = tenant
|
||||
}
|
||||
|
||||
scope := &hanmacEmailScope{
|
||||
TenantIDs: make(map[string]bool),
|
||||
Slugs: make(map[string]bool),
|
||||
}
|
||||
for _, tenant := range tenants {
|
||||
if isTenantDescendantOf(tenant, rootID, tenantByID) {
|
||||
scope.TenantIDs[tenant.ID] = true
|
||||
scope.Slugs[strings.ToLower(strings.TrimSpace(tenant.Slug))] = true
|
||||
scope.IDList = append(scope.IDList, tenant.ID)
|
||||
scope.SlugList = append(scope.SlugList, tenant.Slug)
|
||||
}
|
||||
}
|
||||
return scope, nil
|
||||
}
|
||||
|
||||
func (h *UserHandler) loadHanmacLocalParts(ctx context.Context, scope *hanmacEmailScope) (map[string]bool, error) {
|
||||
used := make(map[string]bool)
|
||||
if h.UserRepo == nil || scope == nil {
|
||||
return used, nil
|
||||
}
|
||||
|
||||
if len(scope.IDList) > 0 {
|
||||
users, err := h.UserRepo.FindByTenantIDs(ctx, scope.IDList)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
addUserEmailLocalParts(used, users)
|
||||
}
|
||||
|
||||
if len(scope.SlugList) > 0 {
|
||||
users, err := h.UserRepo.FindByCompanyCodes(ctx, scope.SlugList)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
addUserEmailLocalParts(used, users)
|
||||
}
|
||||
|
||||
return used, nil
|
||||
}
|
||||
|
||||
func (s *hanmacEmailScope) ContainsTenant(tenantID string, slug string) bool {
|
||||
if s == nil {
|
||||
return false
|
||||
}
|
||||
if tenantID != "" && s.TenantIDs[tenantID] {
|
||||
return true
|
||||
}
|
||||
return s.Slugs[strings.ToLower(strings.TrimSpace(slug))]
|
||||
}
|
||||
|
||||
func isTenantDescendantOf(tenant domain.Tenant, rootID string, tenantByID map[string]domain.Tenant) bool {
|
||||
if tenant.ID == rootID {
|
||||
return true
|
||||
}
|
||||
visited := make(map[string]bool)
|
||||
parentID := ""
|
||||
if tenant.ParentID != nil {
|
||||
parentID = *tenant.ParentID
|
||||
}
|
||||
for parentID != "" {
|
||||
if parentID == rootID {
|
||||
return true
|
||||
}
|
||||
if visited[parentID] {
|
||||
return false
|
||||
}
|
||||
visited[parentID] = true
|
||||
parent, ok := tenantByID[parentID]
|
||||
if !ok || parent.ParentID == nil {
|
||||
return false
|
||||
}
|
||||
parentID = *parent.ParentID
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func addUserEmailLocalParts(target map[string]bool, users []domain.User) {
|
||||
for _, user := range users {
|
||||
localPart, err := domain.ExtractNormalizedEmailLocalPart(user.Email)
|
||||
if err == nil && localPart != "" {
|
||||
target[localPart] = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func nextAvailableHanmacLocalPart(base string, usedLocalParts map[string]bool) string {
|
||||
base = strings.ToLower(strings.TrimSpace(base))
|
||||
if base == "" {
|
||||
return ""
|
||||
}
|
||||
if !usedLocalParts[base] {
|
||||
return base
|
||||
}
|
||||
for index := 1; ; index++ {
|
||||
candidate := fmt.Sprintf("%s%d", base, index)
|
||||
if !usedLocalParts[candidate] {
|
||||
return candidate
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func appendUniqueString(values []string, value string) []string {
|
||||
for _, existing := range values {
|
||||
if existing == value {
|
||||
return values
|
||||
}
|
||||
}
|
||||
return append(values, value)
|
||||
}
|
||||
Reference in New Issue
Block a user