package service import ( "baron-sso-backend/internal/domain" "baron-sso-backend/internal/repository" "context" "errors" "os" "sort" "strings" ) const HanmacFamilyTenantSlug = "hanmac-family" type WorksmobileSyncer interface { EnqueueTenantUpsertIfInScope(ctx context.Context, tenant domain.Tenant) error EnqueueTenantDeleteIfInScope(ctx context.Context, tenant domain.Tenant) error EnqueueUserUpsertIfInScope(ctx context.Context, user domain.User) error EnqueueUserDeleteIfInScope(ctx context.Context, user domain.User) error } type WorksmobileAdminService interface { GetTenantOverview(ctx context.Context, tenantID string) (WorksmobileTenantOverview, error) GetComparison(ctx context.Context, tenantID string, includeMatched bool) (WorksmobileComparison, error) EnqueueBackfillDryRun(ctx context.Context, tenantID string) (WorksmobileBackfillDryRun, error) EnqueueOrgUnitSync(ctx context.Context, tenantID, orgUnitID string) (*domain.WorksmobileOutbox, error) EnqueueOrgUnitDelete(ctx context.Context, tenantID, worksmobileOrgUnitID string) (*domain.WorksmobileOutbox, error) EnqueueUserSync(ctx context.Context, tenantID, userID string) (*domain.WorksmobileOutbox, error) RetryJob(ctx context.Context, tenantID, jobID string) (*domain.WorksmobileOutbox, error) ListInitialPasswordCredentials(ctx context.Context, tenantID string) ([]WorksmobileInitialPasswordCredential, error) } type WorksmobileConfigSummary struct { Enabled bool `json:"enabled"` DomainMappings map[string]int64 `json:"domainMappings"` TokenConfigured bool `json:"tokenConfigured"` AdminTenantID string `json:"adminTenantId,omitempty"` } type WorksmobileTenantOverview struct { Tenant domain.Tenant `json:"tenant"` Config WorksmobileConfigSummary `json:"config"` RecentJobs []domain.WorksmobileOutbox `json:"recentJobs"` } type WorksmobileBackfillDryRun struct { OrgUnitCount int `json:"orgUnitCount"` UserCount int `json:"userCount"` } type WorksmobileInitialPasswordCredential struct { Email string `json:"email"` InitialPassword string `json:"initialPassword"` Status string `json:"status"` LastError string `json:"lastError,omitempty"` } type WorksmobileComparison struct { Users []WorksmobileComparisonItem `json:"users"` Groups []WorksmobileComparisonItem `json:"groups"` } type WorksmobileComparisonItem struct { ResourceType string `json:"resourceType"` BaronID string `json:"baronId,omitempty"` BaronSlug string `json:"baronSlug,omitempty"` BaronName string `json:"baronName,omitempty"` BaronEmail string `json:"baronEmail,omitempty"` BaronPrimaryOrgID string `json:"baronPrimaryOrgId,omitempty"` BaronPrimaryOrgSlug string `json:"baronPrimaryOrgSlug,omitempty"` BaronPrimaryOrgName string `json:"baronPrimaryOrgName,omitempty"` BaronParentID string `json:"baronParentId,omitempty"` BaronParentSlug string `json:"baronParentSlug,omitempty"` BaronParentName string `json:"baronParentName,omitempty"` WorksmobileID string `json:"worksmobileId,omitempty"` ExternalKey string `json:"externalKey,omitempty"` WorksmobileName string `json:"worksmobileName,omitempty"` WorksmobileEmail string `json:"worksmobileEmail,omitempty"` WorksmobileLevelID string `json:"worksmobileLevelId,omitempty"` WorksmobileLevelName string `json:"worksmobileLevelName,omitempty"` WorksmobileTask string `json:"worksmobileTask,omitempty"` WorksmobileDomainID int64 `json:"worksmobileDomainId,omitempty"` WorksmobileDomainName string `json:"worksmobileDomainName,omitempty"` WorksmobilePrimaryOrgID string `json:"worksmobilePrimaryOrgId,omitempty"` WorksmobilePrimaryOrgName string `json:"worksmobilePrimaryOrgName,omitempty"` WorksmobilePrimaryOrgPositionID string `json:"worksmobilePrimaryOrgPositionId,omitempty"` WorksmobilePrimaryOrgPositionName string `json:"worksmobilePrimaryOrgPositionName,omitempty"` WorksmobilePrimaryOrgIsManager *bool `json:"worksmobilePrimaryOrgIsManager,omitempty"` BaronParentWorksmobileID string `json:"baronParentWorksmobileId,omitempty"` BaronParentWorksmobileName string `json:"baronParentWorksmobileName,omitempty"` BaronParentWorksmobileEmail string `json:"baronParentWorksmobileEmail,omitempty"` WorksmobileParentID string `json:"worksmobileParentId,omitempty"` WorksmobileParentName string `json:"worksmobileParentName,omitempty"` WorksmobileParentEmail string `json:"worksmobileParentEmail,omitempty"` WorksmobileParentExternalKey string `json:"worksmobileParentExternalKey,omitempty"` Status string `json:"status"` } type worksmobileSyncService struct { tenantService TenantService userRepo repository.UserRepository outboxRepo repository.WorksmobileOutboxRepository client WorksmobileDirectoryClient } func NewWorksmobileSyncService(tenantService TenantService, userRepo repository.UserRepository, outboxRepo repository.WorksmobileOutboxRepository, client WorksmobileDirectoryClient) *worksmobileSyncService { return &worksmobileSyncService{ tenantService: tenantService, userRepo: userRepo, outboxRepo: outboxRepo, client: client, } } func (s *worksmobileSyncService) GetTenantOverview(ctx context.Context, tenantID string) (WorksmobileTenantOverview, error) { tenant, err := s.tenantService.GetTenant(ctx, tenantID) if err != nil { return WorksmobileTenantOverview{}, err } jobs, _ := s.outboxRepo.ListRecent(ctx, 50) jobs = redactWorksmobileOutboxPayloads(jobs) return WorksmobileTenantOverview{ Tenant: *tenant, Config: WorksmobileConfigSummary{ Enabled: WorksmobileEnabled(tenant.Config), DomainMappings: WorksmobileDomainMappings(tenant.Config), TokenConfigured: worksmobileDirectoryAuthConfigured(), AdminTenantID: strings.TrimSpace(os.Getenv("WORKS_ADMIN_TENANT_ID")), }, RecentJobs: jobs, }, nil } func worksmobileDirectoryAuthConfigured() bool { if strings.TrimSpace(os.Getenv("WORKS_ADMIN_ACCESS_TOKEN")) != "" || strings.TrimSpace(os.Getenv("WORKS_ADMIN_OAUTH_ACCESS_TOKEN")) != "" { return true } return strings.TrimSpace(os.Getenv("WORKS_ADMIN_OAUTH_CLIENT_ID")) != "" && strings.TrimSpace(os.Getenv("WORKS_ADMIN_OAUTH_CLIENT_SECRET")) != "" && strings.TrimSpace(os.Getenv("WORKS_ADMIN_OAUTH_CLIENT_SERVICE_ACCOUNT")) != "" && (strings.TrimSpace(os.Getenv("WORKS_ADMIN_OAUTH_CLIENT_PRIVATE_KEY")) != "" || strings.TrimSpace(os.Getenv("WORKS_ADMIN_OAUTH_CLIENT_PRIVATE_KEY_FILE")) != "") } func redactWorksmobileOutboxPayloads(jobs []domain.WorksmobileOutbox) []domain.WorksmobileOutbox { for i := range jobs { if jobs[i].Payload != nil { jobs[i].Payload = nil } } return jobs } func (s *worksmobileSyncService) GetComparison(ctx context.Context, tenantID string, includeMatched bool) (WorksmobileComparison, error) { root, err := s.hanmacRoot(ctx, tenantID) if err != nil { return WorksmobileComparison{}, err } if s.client == nil { return WorksmobileComparison{}, errors.New("worksmobile client is not configured") } tenants, err := s.hanmacSubtree(ctx, root.ID) if err != nil { return WorksmobileComparison{}, err } tenantByID := worksmobileTenantByID(tenants) tenantByID[root.ID] = *root tenantIDs := make([]string, 0, len(tenants)) for _, tenant := range tenants { if isWorksmobileUserScopeTenant(tenant) { tenantIDs = append(tenantIDs, tenant.ID) } } users, err := s.userRepo.FindByTenantIDs(ctx, tenantIDs) if err != nil { return WorksmobileComparison{}, err } remoteUsers, err := s.client.ListUsers(ctx) if err != nil { return WorksmobileComparison{}, err } remoteGroups, err := s.client.ListGroups(ctx) if err != nil { return WorksmobileComparison{}, err } return WorksmobileComparison{ Users: compareWorksmobileUsers(users, remoteUsers, includeMatched, tenantByID), Groups: compareWorksmobileGroups(append([]domain.Tenant{*root}, tenants...), remoteGroups, includeMatched), }, nil } func (s *worksmobileSyncService) EnqueueBackfillDryRun(ctx context.Context, tenantID string) (WorksmobileBackfillDryRun, error) { root, err := s.hanmacRoot(ctx, tenantID) if err != nil { return WorksmobileBackfillDryRun{}, err } tenants, err := s.hanmacSubtree(ctx, root.ID) if err != nil { return WorksmobileBackfillDryRun{}, err } orgUnitTenantIDs := make([]string, 0, len(tenants)) userTenantIDs := make([]string, 0, len(tenants)) tenantByID := worksmobileTenantByID(append([]domain.Tenant{*root}, tenants...)) for _, tenant := range tenants { if isWorksmobileOrgUnitTenant(tenant, tenantByID) { orgUnitTenantIDs = append(orgUnitTenantIDs, tenant.ID) } if isWorksmobileUserScopeTenant(tenant) { userTenantIDs = append(userTenantIDs, tenant.ID) } } users, err := s.userRepo.FindByTenantIDs(ctx, userTenantIDs) if err != nil { return WorksmobileBackfillDryRun{}, err } users = worksmobileSyncScopeUsers(users) _ = s.outboxRepo.Create(ctx, &domain.WorksmobileOutbox{ ResourceType: domain.WorksmobileResourceOrgUnit, ResourceID: root.ID, Action: domain.WorksmobileActionDryRun, DedupeKey: "backfill:dry-run:" + root.ID, Payload: domain.JSONMap{ "tenantIds": orgUnitTenantIDs, "userCount": len(users), }, }) return WorksmobileBackfillDryRun{OrgUnitCount: len(orgUnitTenantIDs), UserCount: len(users)}, nil } func (s *worksmobileSyncService) EnqueueOrgUnitSync(ctx context.Context, tenantID, orgUnitID string) (*domain.WorksmobileOutbox, error) { root, err := s.hanmacRoot(ctx, tenantID) if err != nil { return nil, err } tenant, err := s.tenantService.GetTenant(ctx, orgUnitID) if err != nil { return nil, err } tenantRoot, ok, err := s.rootForTenant(ctx, *tenant) if err != nil { return nil, err } if !ok || tenantRoot.ID != root.ID { return nil, errors.New("target orgunit is outside hanmac-family subtree") } scopeTenants, err := s.hanmacSubtree(ctx, root.ID) if err != nil { return nil, err } tenantByID := worksmobileTenantByID(append([]domain.Tenant{*root}, scopeTenants...)) if !isWorksmobileOrgUnitTenant(*tenant, tenantByID) { return nil, errors.New("target tenant is not a worksmobile orgunit tenant") } return s.enqueueOrgUnitUpsert(ctx, root, *tenant, scopeTenants) } func (s *worksmobileSyncService) enqueueOrgUnitUpsert(ctx context.Context, root *domain.Tenant, tenant domain.Tenant, scopeTenants []domain.Tenant) (*domain.WorksmobileOutbox, error) { tenantByID := worksmobileTenantByID(append([]domain.Tenant{*root}, scopeTenants...)) payload, err := BuildWorksmobileOrgUnitPayloadForDomainTenant( tenant, worksmobileDomainClassificationTenant(tenant, tenantByID), root.Config, 0, ) if err != nil { return nil, err } payload = normalizeWorksmobileOrgUnitParent(payload, tenant, tenantByID, root.ID) item := &domain.WorksmobileOutbox{ ResourceType: domain.WorksmobileResourceOrgUnit, ResourceID: tenant.ID, Action: domain.WorksmobileActionUpsert, DedupeKey: "orgunit:upsert:" + tenant.ID, Payload: domain.JSONMap{ "request": payload, "matchLocalPart": tenant.Slug, }, } if err := s.outboxRepo.Create(ctx, item); err != nil { return nil, err } return item, nil } func (s *worksmobileSyncService) EnqueueOrgUnitDelete(ctx context.Context, tenantID, worksmobileOrgUnitID string) (*domain.WorksmobileOutbox, error) { root, err := s.hanmacRoot(ctx, tenantID) if err != nil { return nil, err } if s.client == nil { return nil, errors.New("worksmobile client is not configured") } scopeTenants, err := s.hanmacSubtree(ctx, root.ID) if err != nil { return nil, err } worksmobileOrgUnitID = strings.TrimSpace(worksmobileOrgUnitID) if worksmobileOrgUnitID == "" { return nil, errors.New("worksmobile orgunit id is required") } groups, err := s.client.ListGroups(ctx) if err != nil { return nil, err } var target *WorksmobileRemoteGroup for i := range groups { if strings.TrimSpace(groups[i].ID) == worksmobileOrgUnitID { target = &groups[i] break } } if target == nil { return nil, errors.New("worksmobile orgunit not found") } tenantByID := worksmobileTenantByID(append([]domain.Tenant{*root}, scopeTenants...)) if tenant, ok := findWorksmobileOrgUnitTenantByRemoteLocalPart(*target, scopeTenants, tenantByID); ok { return s.enqueueOrgUnitUpsert(ctx, root, tenant, scopeTenants) } if isProtectedWorksmobileRemoteOrgUnit(*root, scopeTenants, *target) { return nil, errors.New("protected worksmobile domain root orgunit cannot be deleted") } item := &domain.WorksmobileOutbox{ ResourceType: domain.WorksmobileResourceOrgUnit, ResourceID: worksmobileOrgUnitID, Action: domain.WorksmobileActionDelete, DedupeKey: "orgunit:delete:works:" + worksmobileOrgUnitID, Payload: domain.JSONMap{ "worksmobileId": worksmobileOrgUnitID, "externalKey": target.ExternalID, "domainId": target.DomainID, "name": target.DisplayName, "email": target.Email, }, } if err := s.outboxRepo.Create(ctx, item); err != nil { return nil, err } return item, nil } func (s *worksmobileSyncService) EnqueueUserSync(ctx context.Context, tenantID, userID string) (*domain.WorksmobileOutbox, error) { root, err := s.hanmacRoot(ctx, tenantID) if err != nil { return nil, err } user, err := s.userRepo.FindByID(ctx, userID) if err != nil { return nil, err } if user.TenantID == nil { return nil, errors.New("target user has no tenant") } tenant, err := s.tenantService.GetTenant(ctx, *user.TenantID) if err != nil { return nil, err } tenantRoot, ok, err := s.rootForTenant(ctx, *tenant) if err != nil { return nil, err } if !ok || tenantRoot.ID != root.ID { return nil, errors.New("target user is outside hanmac-family subtree") } scopeTenants, err := s.hanmacSubtree(ctx, root.ID) if err != nil { return nil, err } if domain.IsWorksDeprovisionUserStatus(user.Status) { return s.enqueueUserDelete(ctx, *user, "user:delete:"+user.ID, root.ID) } if !domain.IsWorksProvisionedUserStatus(user.Status) { return nil, errors.New("target user status is excluded from Worksmobile sync") } tenantByID := worksmobileTenantByID(append([]domain.Tenant{*root}, scopeTenants...)) payload, err := BuildWorksmobileUserPayloadForDomainTenants( *user, *tenant, tenantByID, root.Config, ) if err != nil { return nil, err } if err := s.validateUserAliasLocalParts(ctx, root, *user, payload); err != nil { return nil, err } action := WorksmobileUserStatusAction(user.Status) item := &domain.WorksmobileOutbox{ ResourceType: domain.WorksmobileResourceUser, ResourceID: user.ID, Action: action, DedupeKey: "user:" + strings.ToLower(action) + ":" + user.ID, Payload: worksmobileUserOutboxPayload(root.ID, payload, user.Status), } if err := s.outboxRepo.Create(ctx, item); err != nil { return nil, err } return item, nil } func (s *worksmobileSyncService) ListInitialPasswordCredentials(ctx context.Context, tenantID string) ([]WorksmobileInitialPasswordCredential, error) { root, err := s.hanmacRoot(ctx, tenantID) if err != nil { return nil, err } jobs, err := s.outboxRepo.ListRecent(ctx, 1000) if err != nil { return nil, err } credentials := make([]WorksmobileInitialPasswordCredential, 0) seen := map[string]bool{} for _, job := range jobs { if job.ResourceType != domain.WorksmobileResourceUser { continue } if stringValue(job.Payload["tenantRootId"]) != root.ID { continue } email := stringValue(job.Payload["loginEmail"]) password := stringValue(job.Payload["initialPassword"]) if email == "" || password == "" || seen[email] { continue } seen[email] = true credentials = append(credentials, WorksmobileInitialPasswordCredential{ Email: email, InitialPassword: password, Status: job.Status, LastError: job.LastError, }) } return credentials, nil } func (s *worksmobileSyncService) RetryJob(ctx context.Context, tenantID, jobID string) (*domain.WorksmobileOutbox, error) { if _, err := s.hanmacRoot(ctx, tenantID); err != nil { return nil, err } if err := s.outboxRepo.MarkRetry(ctx, jobID); err != nil { return nil, err } return s.outboxRepo.FindByID(ctx, jobID) } func (s *worksmobileSyncService) EnqueueTenantUpsertIfInScope(ctx context.Context, tenant domain.Tenant) error { root, ok, err := s.rootForTenant(ctx, tenant) if err != nil || !ok { return err } scopeTenants, err := s.hanmacSubtree(ctx, root.ID) if err != nil { return err } tenantByID := worksmobileTenantByID(append([]domain.Tenant{*root}, scopeTenants...)) if !isWorksmobileOrgUnitTenant(tenant, tenantByID) { return nil } payload, err := BuildWorksmobileOrgUnitPayloadForDomainTenant( tenant, worksmobileDomainClassificationTenant(tenant, tenantByID), root.Config, 0, ) if err != nil { return err } payload = normalizeWorksmobileOrgUnitParent(payload, tenant, tenantByID, root.ID) return s.outboxRepo.Create(ctx, &domain.WorksmobileOutbox{ ResourceType: domain.WorksmobileResourceOrgUnit, ResourceID: tenant.ID, Action: domain.WorksmobileActionUpsert, DedupeKey: "orgunit:upsert:" + tenant.ID, Payload: domain.JSONMap{ "request": payload, "matchLocalPart": tenant.Slug, }, }) } func (s *worksmobileSyncService) EnqueueTenantDeleteIfInScope(ctx context.Context, tenant domain.Tenant) error { root, ok, err := s.rootForTenant(ctx, tenant) if err != nil || !ok { return err } scopeTenants, err := s.hanmacSubtree(ctx, root.ID) if err != nil { return err } tenantByID := worksmobileTenantByID(append([]domain.Tenant{*root}, scopeTenants...)) if !isWorksmobileOrgUnitTenant(tenant, tenantByID) { return nil } return s.outboxRepo.Create(ctx, &domain.WorksmobileOutbox{ ResourceType: domain.WorksmobileResourceOrgUnit, ResourceID: tenant.ID, Action: domain.WorksmobileActionDelete, DedupeKey: "orgunit:delete:" + tenant.ID, Payload: domain.JSONMap{"orgUnitExternalKey": tenant.ID}, }) } func (s *worksmobileSyncService) EnqueueUserUpsertIfInScope(ctx context.Context, user domain.User) error { if user.TenantID == nil || *user.TenantID == "" { return nil } tenant, err := s.tenantService.GetTenant(ctx, *user.TenantID) if err != nil { return err } root, ok, err := s.rootForTenant(ctx, *tenant) if err != nil || !ok { return err } scopeTenants, err := s.hanmacSubtree(ctx, root.ID) if err != nil { return err } if domain.IsWorksDeprovisionUserStatus(user.Status) { _, err := s.enqueueUserDelete(ctx, user, "user:delete:"+user.ID, root.ID) return err } if !domain.IsWorksProvisionedUserStatus(user.Status) { return nil } tenantByID := worksmobileTenantByID(append([]domain.Tenant{*root}, scopeTenants...)) payload, err := BuildWorksmobileUserPayloadForDomainTenants( user, *tenant, tenantByID, root.Config, ) if err != nil { return err } if err := s.validateUserAliasLocalParts(ctx, root, user, payload); err != nil { return err } action := WorksmobileUserStatusAction(user.Status) return s.outboxRepo.Create(ctx, &domain.WorksmobileOutbox{ ResourceType: domain.WorksmobileResourceUser, ResourceID: user.ID, Action: action, DedupeKey: "user:" + strings.ToLower(action) + ":" + user.ID, Payload: worksmobileUserOutboxPayload(root.ID, payload, user.Status), }) } func (s *worksmobileSyncService) EnqueueUserDeleteIfInScope(ctx context.Context, user domain.User) error { if user.TenantID == nil || *user.TenantID == "" { return nil } tenant, err := s.tenantService.GetTenant(ctx, *user.TenantID) if err != nil { return err } _, ok, err := s.rootForTenant(ctx, *tenant) if err != nil || !ok { return err } _, err = s.enqueueUserDelete(ctx, user, "user:delete:"+user.ID, "") return err } func (s *worksmobileSyncService) enqueueUserDelete(ctx context.Context, user domain.User, dedupeKey string, rootID string) (*domain.WorksmobileOutbox, error) { payload := domain.JSONMap{ "userExternalKey": user.ID, "loginEmail": user.Email, } if rootID != "" { payload["tenantRootId"] = rootID } if status := domain.NormalizeUserStatus(user.Status); status != "" { payload["baronStatus"] = status } item := &domain.WorksmobileOutbox{ ResourceType: domain.WorksmobileResourceUser, ResourceID: user.ID, Action: domain.WorksmobileActionDelete, DedupeKey: dedupeKey, Payload: payload, } if err := s.outboxRepo.Create(ctx, item); err != nil { return nil, err } return item, nil } func (s *worksmobileSyncService) hanmacRoot(ctx context.Context, tenantID string) (*domain.Tenant, error) { tenant, err := s.tenantService.GetTenant(ctx, tenantID) if err != nil { return nil, err } if tenant.Slug != HanmacFamilyTenantSlug || tenant.ParentID != nil { return nil, errors.New("worksmobile is only available for hanmac-family root tenant") } return tenant, nil } func (s *worksmobileSyncService) hanmacSubtree(ctx context.Context, rootID string) ([]domain.Tenant, error) { all, _, err := s.tenantService.ListTenants(ctx, 10000, 0, "") if err != nil { return nil, err } byParent := map[string][]domain.Tenant{} for _, tenant := range all { if tenant.ParentID != nil { byParent[*tenant.ParentID] = append(byParent[*tenant.ParentID], tenant) } } result := []domain.Tenant{} var visit func(id string) visit = func(id string) { for _, child := range byParent[id] { result = append(result, child) visit(child.ID) } } visit(rootID) sort.Slice(result, func(i, j int) bool { return result[i].Name < result[j].Name }) return result, nil } func (s *worksmobileSyncService) rootForTenant(ctx context.Context, tenant domain.Tenant) (*domain.Tenant, bool, error) { current := tenant for current.ParentID != nil && *current.ParentID != "" { parent, err := s.tenantService.GetTenant(ctx, *current.ParentID) if err != nil { return nil, false, err } current = *parent } return ¤t, current.Slug == HanmacFamilyTenantSlug, nil } func (s *worksmobileSyncService) validateUserAliasLocalParts(ctx context.Context, root *domain.Tenant, user domain.User, payload WorksmobileUserPayload) error { if len(payload.AliasEmails) == 0 { return nil } tenants, err := s.hanmacSubtree(ctx, root.ID) if err != nil { return err } tenantByID := make(map[string]domain.Tenant, len(tenants)+1) tenantByID[root.ID] = *root tenantIDs := make([]string, 0, len(tenants)+1) tenantIDs = append(tenantIDs, root.ID) for _, tenant := range tenants { tenantByID[tenant.ID] = tenant if isWorksmobileUserScopeTenant(tenant) { tenantIDs = append(tenantIDs, tenant.ID) } } users, err := s.userRepo.FindByTenantIDs(ctx, tenantIDs) if err != nil { return err } existing := map[string]string{} for _, existingUser := range users { if existingUser.ID == user.ID { continue } addWorksmobileLocalPart(existing, existingUser.Email, existingUser.ID) if existingUser.TenantID == nil { continue } tenant, ok := tenantByID[*existingUser.TenantID] if !ok { continue } for _, alias := range BuildWorksmobileAliasEmails(existingUser, tenant) { addWorksmobileLocalPart(existing, alias, existingUser.ID) } } return ValidateWorksmobileAliasLocalParts(payload.Email, payload.AliasEmails, existing) } func addWorksmobileLocalPart(target map[string]string, email string, owner string) { localPart, err := domain.ExtractNormalizedEmailLocalPart(email) if err == nil && localPart != "" { target[localPart] = owner } } func findWorksmobileOrgUnitTenantByRemoteLocalPart(remote WorksmobileRemoteGroup, localTenants []domain.Tenant, tenantByID map[string]domain.Tenant) (domain.Tenant, bool) { candidates := worksmobileRemoteGroupLocalPartCandidates(remote) for _, tenant := range localTenants { if !isWorksmobileOrgUnitTenant(tenant, tenantByID) { continue } if candidates[normalizeWorksmobileSlugLocalPart(tenant.Slug)] { return tenant, true } } return domain.Tenant{}, false } func isProtectedWorksmobileRemoteOrgUnit(root domain.Tenant, localTenants []domain.Tenant, remote WorksmobileRemoteGroup) bool { if strings.TrimSpace(remote.ParentID) == "" { return true } candidates := worksmobileRemoteGroupLocalPartCandidates(remote) if len(candidates) == 0 { return false } for _, tenant := range localTenants { if tenant.ParentID == nil || *tenant.ParentID != root.ID || tenant.Type != domain.TenantTypeCompany { continue } if candidates[normalizeWorksmobileSlugLocalPart(tenant.Slug)] { return true } } return false } func worksmobileRemoteGroupLocalPartCandidates(remote WorksmobileRemoteGroup) map[string]bool { result := map[string]bool{} if localPart := normalizeWorksmobileSlugLocalPart(remote.MailLocalPart); localPart != "" { result[localPart] = true } if localPart, err := domain.ExtractNormalizedEmailLocalPart(remote.Email); err == nil && localPart != "" { result[localPart] = true } return result } func normalizeWorksmobileSlugLocalPart(value string) string { return strings.ToLower(strings.TrimSpace(value)) } func isWorksmobileOrgUnitTenant(tenant domain.Tenant, tenantByID map[string]domain.Tenant) bool { if tenant.Type == domain.TenantTypeOrganization { return true } if tenant.Type == domain.TenantTypeUserGroup { return true } if tenant.Type == domain.TenantTypeCompany { return isWorksmobileBarongroupChildCompany(tenant, tenantByID) } return false } func isWorksmobileUserScopeTenant(tenant domain.Tenant) bool { return tenant.Type == domain.TenantTypeCompany || tenant.Type == domain.TenantTypeOrganization || tenant.Type == domain.TenantTypeUserGroup } func worksmobileDomainClassificationTenant(tenant domain.Tenant, tenantByID map[string]domain.Tenant) domain.Tenant { current := tenant for { if isWorksmobileDomainRootTenant(current) { return current } parentID := worksmobileTenantParentID(current) if parentID == "" { return tenant } parent, ok := tenantByID[parentID] if !ok { return tenant } current = parent } } func isWorksmobileDomainRootTenant(tenant domain.Tenant) bool { slug := strings.ToLower(strings.TrimSpace(tenant.Slug)) switch slug { case "saman", "hanmac", "gpdtdc", "baron-group": return true } if tenantHasDomain(tenant, "samaneng.com") || tenantHasDomain(tenant, "hanmaceng.co.kr") || tenantHasDomain(tenant, "baroncs.co.kr") || tenantHasDomain(tenant, "brsw.kr") { return true } name := strings.TrimSpace(tenant.Name) return name == "삼안" || name == "한맥기술" || name == "총괄기획&기술개발센터" || name == "바론그룹" } func isWorksmobileBarongroupChildCompany(tenant domain.Tenant, tenantByID map[string]domain.Tenant) bool { if tenant.Type != domain.TenantTypeCompany || tenant.Slug == "baron-group" { return false } parentID := worksmobileTenantParentID(tenant) for parentID != "" { parent, ok := tenantByID[parentID] if !ok { return false } if parent.Slug == "baron-group" { return true } parentID = worksmobileTenantParentID(parent) } return false } func normalizeWorksmobileOrgUnitParent(payload WorksmobileOrgUnitPayload, tenant domain.Tenant, tenantByID map[string]domain.Tenant, rootID string) WorksmobileOrgUnitPayload { if tenant.ParentID != nil && *tenant.ParentID == rootID { payload.ParentOrgUnitID = "" } if tenant.ParentID != nil { if parent, ok := tenantByID[*tenant.ParentID]; ok { if parent.Slug == "baron-group" || !isWorksmobileOrgUnitTenant(parent, tenantByID) { payload.ParentOrgUnitID = "" } } } return payload } func worksmobileUserOutboxPayload(rootID string, payload WorksmobileUserPayload, statuses ...string) domain.JSONMap { outboxPayload := domain.JSONMap{ "request": payload, "tenantRootId": rootID, "loginEmail": payload.Email, "initialPassword": payload.PasswordConfig.Password, } if len(statuses) > 0 { if status := strings.TrimSpace(statuses[0]); status != "" { outboxPayload["baronStatus"] = status } } return outboxPayload } func stringValue(value any) string { switch v := value.(type) { case string: return strings.TrimSpace(v) default: return "" } } func compareWorksmobileUsers(localUsers []domain.User, remoteUsers []WorksmobileRemoteUser, includeMatched bool, localTenants map[string]domain.Tenant) []WorksmobileComparisonItem { remoteByExternalID := map[string]WorksmobileRemoteUser{} remoteByEmail := map[string]WorksmobileRemoteUser{} for _, remote := range remoteUsers { if remote.ExternalID != "" { remoteByExternalID[remote.ExternalID] = remote } if normalizedEmail := strings.ToLower(strings.TrimSpace(remote.Email)); normalizedEmail != "" { remoteByEmail[normalizedEmail] = remote } } localByID := map[string]domain.User{} matchedRemoteIDs := map[string]bool{} excludedLocalIDs := map[string]bool{} result := make([]WorksmobileComparisonItem, 0) for _, user := range localUsers { if !domain.IsWorksProvisionedUserStatus(user.Status) { excludedLocalIDs[user.ID] = true if remote, ok := remoteByExternalID[user.ID]; ok { matchedRemoteIDs[remote.ID] = true } else if remote, ok := remoteByEmail[strings.ToLower(strings.TrimSpace(user.Email))]; ok { matchedRemoteIDs[remote.ID] = true } continue } localByID[user.ID] = user remote, matched := remoteByExternalID[user.ID] if !matched { remote, matched = remoteByEmail[strings.ToLower(strings.TrimSpace(user.Email))] } if matched && !includeMatched { matchedRemoteIDs[remote.ID] = true continue } item := WorksmobileComparisonItem{ ResourceType: "USER", BaronID: user.ID, BaronName: user.Name, BaronEmail: user.Email, BaronPrimaryOrgID: worksmobileUserPrimaryOrgID(user), BaronPrimaryOrgSlug: worksmobileUserPrimaryOrgSlug(user, localTenants), BaronPrimaryOrgName: worksmobileUserPrimaryOrgName(user, localTenants), Status: "missing_in_worksmobile", } if matched { item.Status = "matched" item.WorksmobileID = remote.ID item.ExternalKey = remote.ExternalID item.WorksmobileName = remote.DisplayName item.WorksmobileEmail = remote.Email item.WorksmobileLevelID = remote.LevelID item.WorksmobileLevelName = remote.LevelName item.WorksmobileTask = remote.Task item.WorksmobileDomainID = remote.DomainID item.WorksmobileDomainName = remote.DomainName item.WorksmobilePrimaryOrgID = remote.PrimaryOrgUnitID item.WorksmobilePrimaryOrgName = remote.PrimaryOrgUnitName item.WorksmobilePrimaryOrgPositionID = remote.PrimaryOrgUnitPositionID item.WorksmobilePrimaryOrgPositionName = remote.PrimaryOrgUnitPositionName item.WorksmobilePrimaryOrgIsManager = remote.PrimaryOrgUnitIsManager matchedRemoteIDs[remote.ID] = true } result = append(result, item) } for _, remote := range remoteUsers { if matchedRemoteIDs[remote.ID] { continue } if excludedLocalIDs[remote.ExternalID] { continue } if remote.ExternalID == "" { result = append(result, WorksmobileComparisonItem{ ResourceType: "USER", WorksmobileID: remote.ID, ExternalKey: remote.ExternalID, WorksmobileName: remote.DisplayName, WorksmobileEmail: remote.Email, WorksmobileLevelID: remote.LevelID, WorksmobileLevelName: remote.LevelName, WorksmobileTask: remote.Task, WorksmobileDomainID: remote.DomainID, WorksmobileDomainName: remote.DomainName, WorksmobilePrimaryOrgID: remote.PrimaryOrgUnitID, WorksmobilePrimaryOrgName: remote.PrimaryOrgUnitName, WorksmobilePrimaryOrgPositionID: remote.PrimaryOrgUnitPositionID, WorksmobilePrimaryOrgPositionName: remote.PrimaryOrgUnitPositionName, WorksmobilePrimaryOrgIsManager: remote.PrimaryOrgUnitIsManager, Status: "missing_external_key", }) continue } if _, ok := localByID[remote.ExternalID]; !ok { result = append(result, WorksmobileComparisonItem{ ResourceType: "USER", WorksmobileID: remote.ID, ExternalKey: remote.ExternalID, WorksmobileName: remote.DisplayName, WorksmobileEmail: remote.Email, WorksmobileLevelID: remote.LevelID, WorksmobileLevelName: remote.LevelName, WorksmobileTask: remote.Task, WorksmobileDomainID: remote.DomainID, WorksmobileDomainName: remote.DomainName, WorksmobilePrimaryOrgID: remote.PrimaryOrgUnitID, WorksmobilePrimaryOrgName: remote.PrimaryOrgUnitName, WorksmobilePrimaryOrgPositionID: remote.PrimaryOrgUnitPositionID, WorksmobilePrimaryOrgPositionName: remote.PrimaryOrgUnitPositionName, WorksmobilePrimaryOrgIsManager: remote.PrimaryOrgUnitIsManager, Status: "missing_in_baron", }) } } return result } func worksmobileUserPrimaryOrgID(user domain.User) string { if user.TenantID == nil { return "" } return strings.TrimSpace(*user.TenantID) } func worksmobileUserPrimaryOrgName(user domain.User, localTenants map[string]domain.Tenant) string { tenantID := worksmobileUserPrimaryOrgID(user) if tenantID == "" { return "" } if tenant, ok := localTenants[tenantID]; ok { return strings.TrimSpace(tenant.Name) } if user.Tenant != nil && user.Tenant.ID == tenantID { return strings.TrimSpace(user.Tenant.Name) } return "" } func worksmobileUserPrimaryOrgSlug(user domain.User, localTenants map[string]domain.Tenant) string { tenantID := worksmobileUserPrimaryOrgID(user) if tenantID == "" { return "" } if tenant, ok := localTenants[tenantID]; ok { return strings.TrimSpace(tenant.Slug) } if user.Tenant != nil && user.Tenant.ID == tenantID { return strings.TrimSpace(user.Tenant.Slug) } return "" } func compareWorksmobileGroups(localTenants []domain.Tenant, remoteGroups []WorksmobileRemoteGroup, includeMatched bool) []WorksmobileComparisonItem { remoteByExternalID := map[string][]WorksmobileRemoteGroup{} remoteByID := map[string]WorksmobileRemoteGroup{} for _, remote := range remoteGroups { if remote.ID != "" { remoteByID[remote.ID] = remote } if remote.ExternalID != "" { remoteByExternalID[remote.ExternalID] = append(remoteByExternalID[remote.ExternalID], remote) } } tenantByID := worksmobileTenantByID(localTenants) localByID := map[string]domain.Tenant{} ignoredLocalByID := map[string]bool{} matchedRemoteIDs := map[string]bool{} result := make([]WorksmobileComparisonItem, 0) for _, tenant := range localTenants { if !isWorksmobileOrgUnitTenant(tenant, tenantByID) { ignoredLocalByID[tenant.ID] = true continue } localByID[tenant.ID] = tenant remote, matched := matchingWorksmobileRemoteGroupForTenant(tenant, remoteByExternalID[tenant.ID], tenantByID) item := WorksmobileComparisonItem{ ResourceType: "GROUP", BaronID: tenant.ID, BaronSlug: tenant.Slug, BaronName: tenant.Name, BaronParentID: worksmobileTenantParentID(tenant), BaronParentSlug: worksmobileTenantParentSlug(tenant, tenantByID), BaronParentName: worksmobileTenantParentName(tenant, tenantByID), Status: "missing_in_worksmobile", } if matched { item.Status = "matched" item.WorksmobileID = remote.ID item.ExternalKey = remote.ExternalID item.WorksmobileName = remote.DisplayName item.WorksmobileEmail = remote.Email item.WorksmobileDomainID = remote.DomainID item.WorksmobileDomainName = remote.DomainName item.WorksmobileParentID = remote.ParentID item.WorksmobileParentName = remote.ParentName if parent, ok := tenantByID[item.BaronParentID]; ok { if parentRemote, ok := matchingWorksmobileRemoteGroupForTenant(parent, remoteByExternalID[item.BaronParentID], tenantByID); ok { item.BaronParentWorksmobileID = parentRemote.ID item.BaronParentWorksmobileName = parentRemote.DisplayName item.BaronParentWorksmobileEmail = parentRemote.Email } } else if parentRemote, ok := firstWorksmobileRemoteGroup(remoteByExternalID[item.BaronParentID]); ok { item.BaronParentWorksmobileID = parentRemote.ID item.BaronParentWorksmobileName = parentRemote.DisplayName item.BaronParentWorksmobileEmail = parentRemote.Email } if parentRemote, ok := remoteByID[remote.ParentID]; ok { if item.WorksmobileParentName == "" { item.WorksmobileParentName = parentRemote.DisplayName } item.WorksmobileParentEmail = parentRemote.Email item.WorksmobileParentExternalKey = parentRemote.ExternalID } item = fillWorksmobileParentFromBaronParentMatch(item) if worksmobileGroupNeedsUpdate(tenant, remote, remoteByID, remoteByExternalID, tenantByID) { item.Status = "needs_update" } matchedRemoteIDs[remote.ID] = true } if matched && item.Status == "matched" && !includeMatched { continue } result = append(result, item) } for _, remote := range remoteGroups { if matchedRemoteIDs[remote.ID] { continue } if remote.ExternalID == "" { result = append(result, WorksmobileComparisonItem{ ResourceType: "GROUP", WorksmobileID: remote.ID, ExternalKey: remote.ExternalID, WorksmobileName: remote.DisplayName, WorksmobileEmail: remote.Email, WorksmobileDomainID: remote.DomainID, WorksmobileDomainName: remote.DomainName, WorksmobileParentID: remote.ParentID, WorksmobileParentName: remote.ParentName, Status: "missing_external_key", }) if parentRemote, ok := remoteByID[remote.ParentID]; ok { last := &result[len(result)-1] if last.WorksmobileParentName == "" { last.WorksmobileParentName = parentRemote.DisplayName } last.WorksmobileParentEmail = parentRemote.Email last.WorksmobileParentExternalKey = parentRemote.ExternalID } continue } if ignoredLocalByID[remote.ExternalID] { continue } if _, ok := localByID[remote.ExternalID]; !ok { result = append(result, WorksmobileComparisonItem{ ResourceType: "GROUP", WorksmobileID: remote.ID, ExternalKey: remote.ExternalID, WorksmobileName: remote.DisplayName, WorksmobileEmail: remote.Email, WorksmobileDomainID: remote.DomainID, WorksmobileDomainName: remote.DomainName, WorksmobileParentID: remote.ParentID, WorksmobileParentName: remote.ParentName, Status: "missing_in_baron", }) if parentRemote, ok := remoteByID[remote.ParentID]; ok { last := &result[len(result)-1] if last.WorksmobileParentName == "" { last.WorksmobileParentName = parentRemote.DisplayName } last.WorksmobileParentEmail = parentRemote.Email last.WorksmobileParentExternalKey = parentRemote.ExternalID } } } return result } func matchingWorksmobileRemoteGroupForTenant(tenant domain.Tenant, remotes []WorksmobileRemoteGroup, tenantByID map[string]domain.Tenant) (WorksmobileRemoteGroup, bool) { if len(remotes) == 0 { return WorksmobileRemoteGroup{}, false } expectedDomainID, hasExpectedDomainID := expectedWorksmobileDomainIDForTenant(tenant, tenantByID) if !hasExpectedDomainID { return remotes[0], true } var unknownDomain WorksmobileRemoteGroup hasUnknownDomain := false for i := range remotes { remote := remotes[i] if remote.DomainID == expectedDomainID { return remote, true } if remote.DomainID == 0 && !hasUnknownDomain { unknownDomain = remote hasUnknownDomain = true } } if hasUnknownDomain { return unknownDomain, true } return WorksmobileRemoteGroup{}, false } func firstWorksmobileRemoteGroup(remotes []WorksmobileRemoteGroup) (WorksmobileRemoteGroup, bool) { if len(remotes) == 0 { return WorksmobileRemoteGroup{}, false } return remotes[0], true } func expectedWorksmobileDomainIDForTenant(tenant domain.Tenant, tenantByID map[string]domain.Tenant) (int64, bool) { domainTenant := worksmobileDomainClassificationTenant(tenant, tenantByID) domainID, err := ResolveWorksmobileDomainIDFromTenant(domainTenant, nil) if err != nil || domainID <= 0 { return 0, false } return domainID, true } func worksmobileGroupNeedsUpdate(tenant domain.Tenant, remote WorksmobileRemoteGroup, remoteByID map[string]WorksmobileRemoteGroup, remoteByExternalID map[string][]WorksmobileRemoteGroup, tenantByID map[string]domain.Tenant) bool { if strings.TrimSpace(tenant.Name) != strings.TrimSpace(remote.DisplayName) { return true } expectedParentExternalKey := expectedWorksmobileParentExternalKey(tenant, remoteByExternalID, tenantByID) actualParentExternalKey := "" if remote.ParentID != "" { actualParentExternalKey = strings.TrimSpace(remoteByID[remote.ParentID].ExternalID) } return expectedParentExternalKey != actualParentExternalKey } func expectedWorksmobileParentExternalKey(tenant domain.Tenant, remoteByExternalID map[string][]WorksmobileRemoteGroup, tenantByID map[string]domain.Tenant) string { parentID := worksmobileTenantParentID(tenant) if parentID == "" { return "" } if parent, ok := tenantByID[parentID]; ok && parent.Slug == "baron-group" { return "" } parent, ok := tenantByID[parentID] if !ok { return "" } if _, ok := matchingWorksmobileRemoteGroupForTenant(parent, remoteByExternalID[parentID], tenantByID); !ok { return "" } return parentID } func fillWorksmobileParentFromBaronParentMatch(item WorksmobileComparisonItem) WorksmobileComparisonItem { if item.WorksmobileParentID == "" || item.WorksmobileParentID != item.BaronParentWorksmobileID { return item } if item.WorksmobileParentName == "" { item.WorksmobileParentName = item.BaronParentWorksmobileName } if item.WorksmobileParentEmail == "" { item.WorksmobileParentEmail = item.BaronParentWorksmobileEmail } if item.WorksmobileParentExternalKey == "" { item.WorksmobileParentExternalKey = item.BaronParentID } return item } func worksmobileTenantByID(tenants []domain.Tenant) map[string]domain.Tenant { result := make(map[string]domain.Tenant, len(tenants)) for _, tenant := range tenants { result[tenant.ID] = tenant } return result } func worksmobileTenantParentID(tenant domain.Tenant) string { if tenant.ParentID == nil { return "" } return strings.TrimSpace(*tenant.ParentID) } func worksmobileTenantParentName(tenant domain.Tenant, tenantByID map[string]domain.Tenant) string { parentID := worksmobileTenantParentID(tenant) if parentID == "" { return "" } return strings.TrimSpace(tenantByID[parentID].Name) } func worksmobileTenantParentSlug(tenant domain.Tenant, tenantByID map[string]domain.Tenant) string { parentID := worksmobileTenantParentID(tenant) if parentID == "" { return "" } return strings.TrimSpace(tenantByID[parentID].Slug) } func worksmobileSyncScopeUsers(users []domain.User) []domain.User { if len(users) == 0 { return users } filtered := make([]domain.User, 0, len(users)) for _, user := range users { if !domain.IsWorksProvisionedUserStatus(user.Status) { continue } filtered = append(filtered, user) } return filtered }