forked from baron/baron-sso
feat: update worksmobile sync and restore planning
This commit is contained in:
@@ -51,15 +51,21 @@ type WorksmobilePasswordConfig struct {
|
||||
Password string `json:"password"`
|
||||
}
|
||||
|
||||
type WorksmobilePasswordResetPayload struct {
|
||||
Email string `json:"email"`
|
||||
PasswordConfig WorksmobilePasswordConfig `json:"passwordConfig"`
|
||||
}
|
||||
|
||||
type WorksmobileUserOrganization struct {
|
||||
DomainID int64 `json:"domainId,omitempty"`
|
||||
Primary bool `json:"primary,omitempty"`
|
||||
Email string `json:"email,omitempty"`
|
||||
Primary bool `json:"primary"`
|
||||
OrgUnits []WorksmobileUserOrgUnit `json:"orgUnits"`
|
||||
}
|
||||
|
||||
type WorksmobileUserOrgUnit struct {
|
||||
OrgUnitID string `json:"orgUnitId"`
|
||||
Primary bool `json:"primary,omitempty"`
|
||||
Primary bool `json:"primary"`
|
||||
PositionID string `json:"positionId,omitempty"`
|
||||
IsManager *bool `json:"isManager,omitempty"`
|
||||
}
|
||||
@@ -156,12 +162,11 @@ func BuildWorksmobileUserPayloadForDomainTenants(user domain.User, tenant domain
|
||||
tenantByID = map[string]domain.Tenant{}
|
||||
}
|
||||
tenantByID[tenant.ID] = tenant
|
||||
domainTenant := worksmobileDomainClassificationTenant(tenant, tenantByID)
|
||||
domainID, err := ResolveWorksmobileDomainIDFromTenant(domainTenant, rootConfig)
|
||||
domainID, err := ResolveWorksmobileAccountDomainIDFromEmail(user.Email, tenant, rootConfig)
|
||||
if err != nil {
|
||||
return WorksmobileUserPayload{}, err
|
||||
}
|
||||
employeeNumber := metadataString(user.Metadata, "employee_id", "employeeNumber", "employee_number")
|
||||
employeeNumber := metadataEmployeeNumber(user.Metadata)
|
||||
organizations, task, err := buildWorksmobileUserOrganizations(user, tenant, tenantByID, rootConfig)
|
||||
if err != nil {
|
||||
return WorksmobileUserPayload{}, err
|
||||
@@ -202,28 +207,19 @@ func buildWorksmobileUserOrganizations(user domain.User, tenant domain.Tenant, t
|
||||
if len(appointments) == 0 {
|
||||
appointments = []worksmobileAppointment{{TenantID: tenant.ID, IsPrimary: true}}
|
||||
}
|
||||
primaryTenantID := metadataString(user.Metadata, "primaryTenantId", "primary_tenant_id")
|
||||
if primaryTenantID == "" && user.TenantID != nil {
|
||||
primaryTenantID = *user.TenantID
|
||||
}
|
||||
hasPrimary := false
|
||||
for i := range appointments {
|
||||
if appointments[i].TenantID == primaryTenantID || appointments[i].IsPrimary {
|
||||
appointments[i].IsPrimary = true
|
||||
hasPrimary = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !hasPrimary {
|
||||
for i := range appointments {
|
||||
if appointments[i].TenantID == tenant.ID {
|
||||
appointments[i].IsPrimary = true
|
||||
break
|
||||
}
|
||||
}
|
||||
accountDomainTenant := worksmobileAccountDomainTenantFromEmail(user.Email, tenant, tenantByID)
|
||||
accountDomainEnvKey := worksmobileTenantDomainIDEnvKey(accountDomainTenant)
|
||||
if !worksmobileAppointmentsContainDomain(appointments, tenantByID, accountDomainEnvKey) && accountDomainTenant.ID != "" {
|
||||
appointments = append([]worksmobileAppointment{{
|
||||
TenantID: accountDomainTenant.ID,
|
||||
IsPrimary: true,
|
||||
JobTitle: strings.TrimSpace(user.JobTitle),
|
||||
PositionID: metadataString(user.Metadata, "worksmobilePositionId", "positionId", "position_id"),
|
||||
}}, appointments...)
|
||||
}
|
||||
|
||||
organizations := make([]WorksmobileUserOrganization, 0, len(appointments))
|
||||
organizations := make([]WorksmobileUserOrganization, 0)
|
||||
organizationIndexByDomainID := map[int64]int{}
|
||||
seen := map[string]bool{}
|
||||
task := ""
|
||||
for _, appointment := range appointments {
|
||||
@@ -242,21 +238,34 @@ func buildWorksmobileUserOrganizations(user domain.User, tenant domain.Tenant, t
|
||||
if err != nil {
|
||||
return nil, "", err
|
||||
}
|
||||
isAccountDomain := worksmobileTenantDomainIDEnvKey(domainTenant) == accountDomainEnvKey
|
||||
isPrimaryOrganization := isAccountDomain && !worksmobileOrganizationsHavePrimary(organizations)
|
||||
organizationIndex, organizationExists := organizationIndexByDomainID[domainID]
|
||||
orgUnit := WorksmobileUserOrgUnit{
|
||||
OrgUnitID: "externalKey:" + appointmentTenant.ID,
|
||||
Primary: appointment.IsPrimary,
|
||||
Primary: !organizationExists,
|
||||
PositionID: appointment.PositionID,
|
||||
}
|
||||
if appointment.HasManager {
|
||||
isManager := appointment.IsManager
|
||||
orgUnit.IsManager = &isManager
|
||||
}
|
||||
organizations = append(organizations, WorksmobileUserOrganization{
|
||||
DomainID: domainID,
|
||||
Primary: appointment.IsPrimary,
|
||||
OrgUnits: []WorksmobileUserOrgUnit{orgUnit},
|
||||
})
|
||||
if appointment.IsPrimary && strings.TrimSpace(appointment.JobTitle) != "" {
|
||||
if organizationExists {
|
||||
if isPrimaryOrganization {
|
||||
organizations[organizationIndex].Primary = true
|
||||
organizations[organizationIndex].Email = worksmobileOrganizationEmail(user, domainTenant)
|
||||
}
|
||||
organizations[organizationIndex].OrgUnits = append(organizations[organizationIndex].OrgUnits, orgUnit)
|
||||
} else {
|
||||
organizationIndexByDomainID[domainID] = len(organizations)
|
||||
organizations = append(organizations, WorksmobileUserOrganization{
|
||||
DomainID: domainID,
|
||||
Email: worksmobileOrganizationEmail(user, domainTenant),
|
||||
Primary: isPrimaryOrganization,
|
||||
OrgUnits: []WorksmobileUserOrgUnit{orgUnit},
|
||||
})
|
||||
}
|
||||
if isPrimaryOrganization && strings.TrimSpace(appointment.JobTitle) != "" {
|
||||
task = strings.TrimSpace(appointment.JobTitle)
|
||||
}
|
||||
seen[appointment.TenantID] = true
|
||||
@@ -264,10 +273,39 @@ func buildWorksmobileUserOrganizations(user domain.User, tenant domain.Tenant, t
|
||||
if len(organizations) == 0 {
|
||||
return nil, "", errors.New("no valid worksmobile organization")
|
||||
}
|
||||
if !worksmobileOrganizationsHavePrimary(organizations) {
|
||||
organizations[0].Primary = true
|
||||
if len(organizations[0].OrgUnits) > 0 {
|
||||
organizations[0].OrgUnits[0].Primary = true
|
||||
}
|
||||
}
|
||||
sortWorksmobileOrganizations(organizations)
|
||||
return organizations, task, nil
|
||||
}
|
||||
|
||||
func worksmobileAppointmentsContainDomain(appointments []worksmobileAppointment, tenantByID map[string]domain.Tenant, envKey string) bool {
|
||||
for _, appointment := range appointments {
|
||||
tenant, ok := tenantByID[appointment.TenantID]
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
domainTenant := worksmobileDomainClassificationTenant(tenant, tenantByID)
|
||||
if worksmobileTenantDomainIDEnvKey(domainTenant) == envKey {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func worksmobileOrganizationsHavePrimary(organizations []WorksmobileUserOrganization) bool {
|
||||
for _, organization := range organizations {
|
||||
if organization.Primary {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func worksmobileAppointmentsFromMetadata(metadata domain.JSONMap) []worksmobileAppointment {
|
||||
rawAppointments, ok := metadata["additionalAppointments"].([]any)
|
||||
if !ok {
|
||||
@@ -326,7 +364,7 @@ func BuildWorksmobileAliasEmails(user domain.User, tenant domain.Tenant) []strin
|
||||
} {
|
||||
candidates = append(candidates, metadataStringList(user.Metadata, key)...)
|
||||
}
|
||||
employeeNumber := metadataString(user.Metadata, "employee_id", "employeeNumber", "employee_number")
|
||||
employeeNumber := metadataEmployeeNumber(user.Metadata)
|
||||
if isHanmacWorksmobileTenant(tenant) && employeeNumber != "" {
|
||||
candidates = append(candidates, employeeNumber+"@hanmaceng.co.kr")
|
||||
}
|
||||
@@ -351,26 +389,21 @@ func normalizeWorksmobileAliasEmails(primaryEmail string, candidates []string) [
|
||||
return result
|
||||
}
|
||||
|
||||
func ValidateWorksmobileAliasLocalParts(primaryEmail string, aliasEmails []string, existingLocalParts map[string]string) error {
|
||||
seen := map[string]string{}
|
||||
primaryLocalPart, err := domain.ExtractNormalizedEmailLocalPart(primaryEmail)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
seen[primaryLocalPart] = primaryEmail
|
||||
func ValidateWorksmobileAliasEmails(primaryEmail string, aliasEmails []string, existingEmails map[string]string) error {
|
||||
seen := map[string]string{strings.ToLower(strings.TrimSpace(primaryEmail)): primaryEmail}
|
||||
|
||||
for _, aliasEmail := range aliasEmails {
|
||||
localPart, err := domain.ExtractNormalizedEmailLocalPart(aliasEmail)
|
||||
if err != nil {
|
||||
normalized := strings.ToLower(strings.TrimSpace(aliasEmail))
|
||||
if _, err := mail.ParseAddress(normalized); err != nil {
|
||||
return err
|
||||
}
|
||||
if previous, ok := seen[localPart]; ok {
|
||||
return fmt.Errorf("worksmobile alias local-part duplicates %s: %s and %s", localPart, previous, aliasEmail)
|
||||
if previous, ok := seen[normalized]; ok {
|
||||
return fmt.Errorf("worksmobile alias email duplicates: %s and %s", previous, aliasEmail)
|
||||
}
|
||||
if owner, ok := existingLocalParts[localPart]; ok {
|
||||
return fmt.Errorf("worksmobile alias local-part %s는 이미 사용 중입니다: %s", localPart, owner)
|
||||
if owner, ok := existingEmails[normalized]; ok {
|
||||
return fmt.Errorf("worksmobile alias email %s는 이미 사용 중입니다: %s", normalized, owner)
|
||||
}
|
||||
seen[localPart] = aliasEmail
|
||||
seen[normalized] = aliasEmail
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -446,6 +479,91 @@ func ResolveWorksmobileDomainIDFromTenant(tenant domain.Tenant, _ domain.JSONMap
|
||||
return 0, fmt.Errorf("worksmobile domain id env is missing for tenant: %s", envKey)
|
||||
}
|
||||
|
||||
func ResolveWorksmobileAccountDomainIDFromEmail(email string, fallbackTenant domain.Tenant, rootConfig domain.JSONMap) (int64, error) {
|
||||
switch worksmobileEmailDomainName(email) {
|
||||
case "samaneng.com":
|
||||
if domainID, ok := worksmobileDomainIDFromEnv("SAMAN_DOMAIN_ID"); ok {
|
||||
return domainID, nil
|
||||
}
|
||||
case "hanmaceng.co.kr":
|
||||
if domainID, ok := worksmobileDomainIDFromEnv("HANMAC_DOMAIN_ID"); ok {
|
||||
return domainID, nil
|
||||
}
|
||||
case "baroncs.co.kr":
|
||||
if domainID, ok := worksmobileDomainIDFromEnv("GPDTDC_DOMAIN_ID"); ok {
|
||||
return domainID, nil
|
||||
}
|
||||
case "brsw.kr":
|
||||
if domainID, ok := worksmobileDomainIDFromEnv("BARONGROUP_DOMAIN_ID"); ok {
|
||||
return domainID, nil
|
||||
}
|
||||
}
|
||||
return ResolveWorksmobileDomainIDFromTenant(fallbackTenant, rootConfig)
|
||||
}
|
||||
|
||||
func worksmobileAccountDomainTenantFromEmail(email string, fallbackTenant domain.Tenant, tenantByID map[string]domain.Tenant) domain.Tenant {
|
||||
envKey := worksmobileDomainIDEnvKeyFromEmail(email)
|
||||
for _, tenant := range tenantByID {
|
||||
if isWorksmobileDomainRootTenant(tenant) && worksmobileTenantDomainIDEnvKey(tenant) == envKey {
|
||||
return tenant
|
||||
}
|
||||
}
|
||||
for _, tenant := range tenantByID {
|
||||
if worksmobileTenantDomainIDEnvKey(tenant) == envKey {
|
||||
return worksmobileDomainClassificationTenant(tenant, tenantByID)
|
||||
}
|
||||
}
|
||||
return worksmobileDomainClassificationTenant(fallbackTenant, tenantByID)
|
||||
}
|
||||
|
||||
func worksmobileDomainIDEnvKeyFromEmail(email string) string {
|
||||
switch worksmobileEmailDomainName(email) {
|
||||
case "samaneng.com":
|
||||
return "SAMAN_DOMAIN_ID"
|
||||
case "hanmaceng.co.kr":
|
||||
return "HANMAC_DOMAIN_ID"
|
||||
case "baroncs.co.kr":
|
||||
return "GPDTDC_DOMAIN_ID"
|
||||
case "brsw.kr":
|
||||
return "BARONGROUP_DOMAIN_ID"
|
||||
default:
|
||||
return worksmobileTenantDomainIDEnvKey(domain.Tenant{})
|
||||
}
|
||||
}
|
||||
|
||||
func worksmobileEmailDomainName(email string) string {
|
||||
address, err := mail.ParseAddress(strings.TrimSpace(email))
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
parts := strings.Split(address.Address, "@")
|
||||
if len(parts) != 2 {
|
||||
return ""
|
||||
}
|
||||
return strings.ToLower(strings.TrimSpace(parts[1]))
|
||||
}
|
||||
|
||||
func worksmobileOrganizationEmail(user domain.User, domainTenant domain.Tenant) string {
|
||||
domainName := worksmobileTenantMailDomain(domainTenant)
|
||||
if domainName == "" {
|
||||
return ""
|
||||
}
|
||||
primaryEmail := strings.ToLower(strings.TrimSpace(user.Email))
|
||||
if worksmobileEmailDomainName(primaryEmail) == domainName {
|
||||
return primaryEmail
|
||||
}
|
||||
for _, alias := range BuildWorksmobileAliasEmails(user, domainTenant) {
|
||||
if worksmobileEmailDomainName(alias) == domainName {
|
||||
return alias
|
||||
}
|
||||
}
|
||||
localPart, err := domain.ExtractNormalizedEmailLocalPart(primaryEmail)
|
||||
if err != nil || localPart == "" {
|
||||
return ""
|
||||
}
|
||||
return localPart + "@" + domainName
|
||||
}
|
||||
|
||||
func worksmobileTenantDomainIDEnvKey(tenant domain.Tenant) string {
|
||||
if tenantHasDomain(tenant, "samaneng.com") || tenantMatchesAny(tenant, "saman", "삼안") {
|
||||
return "SAMAN_DOMAIN_ID"
|
||||
@@ -597,6 +715,70 @@ func metadataString(metadata domain.JSONMap, keys ...string) string {
|
||||
return ""
|
||||
}
|
||||
|
||||
func metadataEmployeeNumber(metadata domain.JSONMap) string {
|
||||
for _, key := range []string{"employee_id", "employeeNumber", "employee_number"} {
|
||||
value, ok := metadata[key]
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
if normalized := normalizeMetadataEmployeeNumber(value); normalized != "" {
|
||||
return normalized
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func normalizeMetadataEmployeeNumber(value any) string {
|
||||
switch v := value.(type) {
|
||||
case string:
|
||||
return strings.TrimSpace(v)
|
||||
case int, int8, int16, int32, int64, uint, uint8, uint16, uint32, uint64, float32, float64:
|
||||
return strings.TrimSpace(fmt.Sprint(v))
|
||||
case map[string]any:
|
||||
return normalizeMetadataCharacterMap(v)
|
||||
case domain.JSONMap:
|
||||
return normalizeMetadataCharacterMap(map[string]any(v))
|
||||
case map[string]string:
|
||||
converted := make(map[string]any, len(v))
|
||||
for key, value := range v {
|
||||
converted[key] = value
|
||||
}
|
||||
return normalizeMetadataCharacterMap(converted)
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
func normalizeMetadataCharacterMap(value map[string]any) string {
|
||||
type characterEntry struct {
|
||||
index int
|
||||
value string
|
||||
}
|
||||
entries := make([]characterEntry, 0, len(value))
|
||||
for key, raw := range value {
|
||||
index, err := strconv.Atoi(key)
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
part, ok := raw.(string)
|
||||
if !ok || part == "" {
|
||||
return ""
|
||||
}
|
||||
entries = append(entries, characterEntry{index: index, value: part})
|
||||
}
|
||||
if len(entries) == 0 {
|
||||
return ""
|
||||
}
|
||||
sort.Slice(entries, func(i, j int) bool {
|
||||
return entries[i].index < entries[j].index
|
||||
})
|
||||
var builder strings.Builder
|
||||
for _, entry := range entries {
|
||||
builder.WriteString(entry.value)
|
||||
}
|
||||
return strings.TrimSpace(builder.String())
|
||||
}
|
||||
|
||||
func metadataBool(metadata domain.JSONMap, keys ...string) bool {
|
||||
value, _ := metadataOptionalBool(metadata, keys...)
|
||||
return value
|
||||
|
||||
Reference in New Issue
Block a user