1
0
forked from baron/baron-sso

네이버 계정 정합성 맞춤

This commit is contained in:
2026-06-15 19:54:09 +09:00
parent 8e9d015443
commit 4d468cd39f
97 changed files with 5837 additions and 2031 deletions

View File

@@ -16,14 +16,18 @@ import (
)
const (
HanmacFamilyTenantSlug = "hanmac-family"
worksmobileExcludedConfigKey = "worksmobileExcluded"
HanmacFamilyTenantSlug = "hanmac-family"
worksmobileExcludedConfigKey = "worksmobileExcluded"
worksmobileIdentityMirrorVersion = "kratos-full-pagination-v1"
worksmobileProvisioningModeKey = "provisioningMode"
worksmobileProvisioningUpdateOnly = "update_only"
)
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
EnqueueUserUpdateIfInScope(ctx context.Context, user domain.User) error
EnqueueUserDeleteIfInScope(ctx context.Context, user domain.User) error
}
@@ -103,55 +107,62 @@ type WorksmobileComparison struct {
}
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"`
BaronPhone string `json:"baronPhone,omitempty"`
BaronEmployeeNumber string `json:"baronEmployeeNumber,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"`
WorksmobilePhone string `json:"worksmobilePhone,omitempty"`
WorksmobileEmployeeNumber string `json:"worksmobileEmployeeNumber,omitempty"`
WorksmobileAccountStatus string `json:"worksmobileAccountStatus,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"`
WorksmobileJobStatus string `json:"worksmobileJobStatus,omitempty"`
WorksmobileJobRetryCount int `json:"worksmobileJobRetryCount,omitempty"`
WorksmobileLastError string `json:"worksmobileLastError,omitempty"`
WorksmobileLastAttemptAt string `json:"worksmobileLastAttemptAt,omitempty"`
Status string `json:"status"`
ResourceType string `json:"resourceType"`
BaronID string `json:"baronId,omitempty"`
BaronSlug string `json:"baronSlug,omitempty"`
BaronName string `json:"baronName,omitempty"`
BaronEmail string `json:"baronEmail,omitempty"`
BaronPhone string `json:"baronPhone,omitempty"`
BaronEmployeeNumber string `json:"baronEmployeeNumber,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"`
WorksmobilePhone string `json:"worksmobilePhone,omitempty"`
WorksmobileEmployeeNumber string `json:"worksmobileEmployeeNumber,omitempty"`
WorksmobileAccountStatus string `json:"worksmobileAccountStatus,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"`
WorksmobileJobStatus string `json:"worksmobileJobStatus,omitempty"`
WorksmobileJobRetryCount int `json:"worksmobileJobRetryCount,omitempty"`
WorksmobileLastError string `json:"worksmobileLastError,omitempty"`
WorksmobileLastAttemptAt string `json:"worksmobileLastAttemptAt,omitempty"`
UpdateReasons []string `json:"updateReasons,omitempty"`
Status string `json:"status"`
}
type worksmobileSyncService struct {
tenantService TenantService
userRepo repository.UserRepository
outboxRepo repository.WorksmobileOutboxRepository
client WorksmobileDirectoryClient
tenantService TenantService
userRepo repository.UserRepository
outboxRepo repository.WorksmobileOutboxRepository
client WorksmobileDirectoryClient
identityMirror WorksmobileIdentityMirror
}
type WorksmobileIdentityMirror interface {
GetIdentityCacheStatus(ctx context.Context) (domain.IdentityCacheStatus, error)
ListIdentityMirrors(ctx context.Context) ([]KratosIdentity, error)
}
func NewWorksmobileSyncService(tenantService TenantService, userRepo repository.UserRepository, outboxRepo repository.WorksmobileOutboxRepository, client WorksmobileDirectoryClient) *worksmobileSyncService {
@@ -163,6 +174,13 @@ func NewWorksmobileSyncService(tenantService TenantService, userRepo repository.
}
}
func (s *worksmobileSyncService) SetIdentityMirror(source WorksmobileIdentityMirror) {
if s == nil {
return
}
s.identityMirror = source
}
func (s *worksmobileSyncService) GetTenantOverview(ctx context.Context, tenantID string) (WorksmobileTenantOverview, error) {
tenant, err := s.tenantService.GetTenant(ctx, tenantID)
if err != nil {
@@ -344,7 +362,7 @@ func (s *worksmobileSyncService) GetComparison(ctx context.Context, tenantID str
tenantIDs = append(tenantIDs, tenant.ID)
}
}
users, err := s.userRepo.FindByTenantIDs(ctx, tenantIDs)
users, err := s.comparisonUsers(ctx, tenantIDs)
if err != nil {
return WorksmobileComparison{}, err
}
@@ -360,11 +378,96 @@ func (s *worksmobileSyncService) GetComparison(ctx context.Context, tenantID str
recentJobs, _ := s.outboxRepo.ListRecent(ctx, 1000)
return WorksmobileComparison{
Users: compareWorksmobileUsers(users, remoteUsers, includeMatched, tenantByID, worksmobileUserJobSummaries(recentJobs)),
Users: compareWorksmobileUsersWithRemoteGroups(users, remoteUsers, includeMatched, tenantByID, remoteGroups, worksmobileUserJobSummaries(recentJobs)),
Groups: compareWorksmobileGroups(append([]domain.Tenant{*root}, tenants...), remoteGroups, includeMatched),
}, nil
}
func (s *worksmobileSyncService) comparisonUsers(ctx context.Context, tenantIDs []string) ([]domain.User, error) {
if s.identityMirror != nil {
status, err := s.identityMirror.GetIdentityCacheStatus(ctx)
if err == nil &&
status.RedisReady &&
status.Status == "ready" &&
status.MirrorVersion == worksmobileIdentityMirrorVersion {
identities, err := s.identityMirror.ListIdentityMirrors(ctx)
if err != nil {
return nil, err
}
return worksmobileUsersFromIdentityMirror(identities, tenantIDs), nil
}
}
return s.userRepo.FindByTenantIDs(ctx, tenantIDs)
}
func worksmobileUsersFromIdentityMirror(identities []KratosIdentity, tenantIDs []string) []domain.User {
allowed := make(map[string]bool, len(tenantIDs))
for _, tenantID := range tenantIDs {
allowed[strings.TrimSpace(tenantID)] = true
}
users := make([]domain.User, 0, len(identities))
for _, identity := range identities {
tenantID := traitString(identity.Traits, "tenant_id")
if tenantID == "" || !allowed[tenantID] {
continue
}
user := worksmobileUserFromIdentity(identity)
users = append(users, user)
}
return users
}
func worksmobileUserFromIdentity(identity KratosIdentity) domain.User {
metadata := domain.JSONMap{}
for key, value := range identity.Traits {
metadata[key] = value
}
tenantID := traitString(identity.Traits, "tenant_id")
status := domain.UserStatusActive
if identity.State == "inactive" {
status = domain.UserStatusArchived
}
if traitStatus := traitString(identity.Traits, "status"); traitStatus != "" {
status = domain.NormalizeUserStatus(traitStatus)
}
user := domain.User{
ID: strings.TrimSpace(identity.ID),
Email: traitString(identity.Traits, "email"),
Name: traitString(identity.Traits, "name"),
Phone: traitString(identity.Traits, "phone_number"),
Role: domain.NormalizeRole(traitString(identity.Traits, "role")),
AffiliationType: traitString(identity.Traits, "affiliationType"),
Department: traitString(identity.Traits, "department"),
Grade: traitString(identity.Traits, "grade"),
Position: traitString(identity.Traits, "position"),
JobTitle: traitString(identity.Traits, "jobTitle"),
Metadata: metadata,
Status: status,
CreatedAt: identity.CreatedAt,
UpdatedAt: identity.UpdatedAt,
}
if tenantID != "" {
user.TenantID = &tenantID
}
return user
}
func traitString(traits map[string]any, key string) string {
if traits == nil {
return ""
}
value, ok := traits[key]
if !ok || value == nil {
return ""
}
switch typed := value.(type) {
case string:
return strings.TrimSpace(typed)
default:
return strings.TrimSpace(fmt.Sprint(typed))
}
}
func (s *worksmobileSyncService) EnqueueBackfillDryRun(ctx context.Context, tenantID string) (WorksmobileBackfillDryRun, error) {
root, err := s.hanmacRoot(ctx, tenantID)
if err != nil {
@@ -545,35 +648,32 @@ func (s *worksmobileSyncService) EnqueueUserSync(ctx context.Context, tenantID,
return nil, err
}
tenantByID := worksmobileTenantByID(append([]domain.Tenant{*root}, scopeTenants...))
if _, ok := tenantByID[tenant.ID]; !ok {
return nil, errors.New("target user tenant is excluded from Worksmobile sync")
}
_, tenantInScope := tenantByID[tenant.ID]
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")
}
payload, err := BuildWorksmobileUserPayloadForDomainTenants(
*user,
*tenant,
tenantByID,
root.Config,
)
if err != nil {
err := errors.New("target user status is excluded from Worksmobile sync")
if recordErr := s.recordRejectedUserSync(ctx, root.ID, *user, *tenant, err); recordErr != nil {
return nil, errors.Join(err, recordErr)
}
return nil, err
}
initialPassword = strings.TrimSpace(initialPassword)
if initialPassword != "" {
payload.PasswordConfig = WorksmobilePasswordConfig{
PasswordCreationType: "ADMIN",
Password: initialPassword,
}
buildPayload := BuildWorksmobileUserPayloadForDomainTenants
if !tenantInScope {
buildPayload = BuildWorksmobileUserPayloadForScopedDomainTenants
}
payload, err := buildPayload(*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)
if action == domain.WorksmobileActionUpsert {
payload.PasswordConfig = worksmobileAdminInitialPasswordConfig(initialPassword)
}
item := &domain.WorksmobileOutbox{
ResourceType: domain.WorksmobileResourceUser,
ResourceID: user.ID,
@@ -594,10 +694,48 @@ func (s *worksmobileSyncService) EnqueueUserSync(ctx context.Context, tenantID,
return item, nil
}
func (s *worksmobileSyncService) recordRejectedUserSync(ctx context.Context, rootID string, user domain.User, tenant domain.Tenant, reason error) error {
payload := WorksmobileUserPayload{
Email: strings.TrimSpace(user.Email),
UserExternalKey: user.ID,
UserName: WorksmobileUserName{LastName: strings.TrimSpace(user.Name)},
CellPhone: domain.NormalizePhoneNumber(user.Phone),
EmployeeNumber: metadataEmployeeNumber(user.Metadata),
Locale: "ko_KR",
Task: strings.TrimSpace(user.JobTitle),
}
outboxPayload := worksmobileUserOutboxPayload(rootID, payload, user.Status)
outboxPayload["displayName"] = strings.TrimSpace(user.Name)
outboxPayload["primaryLeafOrgName"] = strings.TrimSpace(tenant.Name)
item := &domain.WorksmobileOutbox{
ResourceType: domain.WorksmobileResourceUser,
ResourceID: user.ID,
Action: WorksmobileUserStatusAction(user.Status),
DedupeKey: worksmobileUserSyncDedupeKey("rejected", user.ID),
Payload: outboxPayload,
Status: domain.WorksmobileOutboxStatusFailed,
LastError: reason.Error(),
}
return s.outboxRepo.Create(ctx, item)
}
func worksmobileUserSyncDedupeKey(action, userID string) string {
return "user:" + strings.ToLower(action) + ":" + userID + ":" + uuid.NewString()
}
func worksmobileAdminInitialPasswordConfig(password string) WorksmobilePasswordConfig {
password = strings.TrimSpace(password)
if password == "" {
password = GenerateWorksmobileInitialPassword()
}
changePasswordAtNextLogin := true
return WorksmobilePasswordConfig{
PasswordCreationType: "ADMIN",
Password: password,
ChangePasswordAtNextLogin: &changePasswordAtNextLogin,
}
}
func (s *worksmobileSyncService) EnqueueUserPasswordReset(ctx context.Context, tenantID, userID, credentialBatchID string) (*domain.WorksmobileOutbox, error) {
root, err := s.hanmacRoot(ctx, tenantID)
if err != nil {
@@ -629,10 +767,11 @@ func (s *worksmobileSyncService) EnqueueUserPasswordReset(ctx context.Context, t
return nil, err
}
tenantByID := worksmobileTenantByID(append([]domain.Tenant{*root}, scopeTenants...))
buildPayload := BuildWorksmobileUserPayloadForDomainTenants
if _, ok := tenantByID[tenant.ID]; !ok {
return nil, errors.New("target user tenant is excluded from Worksmobile sync")
buildPayload = BuildWorksmobileUserPayloadForScopedDomainTenants
}
payload, err := BuildWorksmobileUserPayloadForDomainTenants(*user, *tenant, tenantByID, root.Config)
payload, err := buildPayload(*user, *tenant, tenantByID, root.Config)
if err != nil {
return nil, err
}
@@ -848,6 +987,14 @@ func (s *worksmobileSyncService) EnqueueTenantDeleteIfInScope(ctx context.Contex
}
func (s *worksmobileSyncService) EnqueueUserUpsertIfInScope(ctx context.Context, user domain.User) error {
return s.enqueueUserUpsertIfInScope(ctx, user, false)
}
func (s *worksmobileSyncService) EnqueueUserUpdateIfInScope(ctx context.Context, user domain.User) error {
return s.enqueueUserUpsertIfInScope(ctx, user, true)
}
func (s *worksmobileSyncService) enqueueUserUpsertIfInScope(ctx context.Context, user domain.User, updateOnly bool) error {
if user.TenantID == nil || *user.TenantID == "" {
return nil
}
@@ -887,12 +1034,19 @@ func (s *worksmobileSyncService) EnqueueUserUpsertIfInScope(ctx context.Context,
return err
}
action := WorksmobileUserStatusAction(user.Status)
if action == domain.WorksmobileActionUpsert && !updateOnly {
payload.PasswordConfig = worksmobileAdminInitialPasswordConfig("")
}
outboxPayload := worksmobileUserOutboxPayload(root.ID, payload, user.Status)
if action == domain.WorksmobileActionUpsert && updateOnly {
outboxPayload[worksmobileProvisioningModeKey] = worksmobileProvisioningUpdateOnly
}
return s.outboxRepo.Create(ctx, &domain.WorksmobileOutbox{
ResourceType: domain.WorksmobileResourceUser,
ResourceID: user.ID,
Action: action,
DedupeKey: worksmobileUserSyncDedupeKey(action, user.ID),
Payload: worksmobileUserOutboxPayload(root.ID, payload, user.Status),
Payload: outboxPayload,
})
}
@@ -1429,10 +1583,15 @@ func worksmobileUserJobSummaries(jobs []domain.WorksmobileOutbox) map[string]wor
}
func compareWorksmobileUsers(localUsers []domain.User, remoteUsers []WorksmobileRemoteUser, includeMatched bool, localTenants map[string]domain.Tenant, jobSummaries ...map[string]worksmobileUserJobSummary) []WorksmobileComparisonItem {
return compareWorksmobileUsersWithRemoteGroups(localUsers, remoteUsers, includeMatched, localTenants, nil, jobSummaries...)
}
func compareWorksmobileUsersWithRemoteGroups(localUsers []domain.User, remoteUsers []WorksmobileRemoteUser, includeMatched bool, localTenants map[string]domain.Tenant, remoteGroups []WorksmobileRemoteGroup, jobSummaries ...map[string]worksmobileUserJobSummary) []WorksmobileComparisonItem {
jobSummaryByUserID := map[string]worksmobileUserJobSummary{}
if len(jobSummaries) > 0 && jobSummaries[0] != nil {
jobSummaryByUserID = jobSummaries[0]
}
remoteOrgUnitByExternalID := worksmobileRemoteOrgUnitByExternalID(remoteGroups)
remoteByExternalID := map[string]WorksmobileRemoteUser{}
remoteByEmail := map[string]WorksmobileRemoteUser{}
for _, remote := range remoteUsers {
@@ -1462,7 +1621,11 @@ func compareWorksmobileUsers(localUsers []domain.User, remoteUsers []Worksmobile
if !matched {
remote, matched = remoteByEmail[strings.ToLower(strings.TrimSpace(user.Email))]
}
needsUpdate := matched && worksmobileUserNeedsUpdate(user, remote, localTenants)
updateReasons := []string(nil)
if matched {
updateReasons = worksmobileUserUpdateReasons(user, remote, localTenants, remoteOrgUnitByExternalID)
}
needsUpdate := len(updateReasons) > 0
if matched && !includeMatched && !needsUpdate {
matchedRemoteIDs[remote.ID] = true
continue
@@ -1491,6 +1654,7 @@ func compareWorksmobileUsers(localUsers []domain.User, remoteUsers []Worksmobile
item.Status = "matched"
if needsUpdate {
item.Status = "needs_update"
item.UpdateReasons = updateReasons
}
item.WorksmobileID = remote.ID
item.ExternalKey = remote.ExternalID
@@ -1571,6 +1735,18 @@ func compareWorksmobileUsers(localUsers []domain.User, remoteUsers []Worksmobile
return result
}
func worksmobileRemoteOrgUnitByExternalID(remoteGroups []WorksmobileRemoteGroup) map[string]WorksmobileRemoteGroup {
result := make(map[string]WorksmobileRemoteGroup, len(remoteGroups))
for _, remote := range remoteGroups {
externalID := strings.TrimSpace(remote.ExternalID)
if externalID == "" {
continue
}
result[externalID] = remote
}
return result
}
func worksmobileRemoteAccountStatus(remote WorksmobileRemoteUser) string {
return normalizeWorksmobileAccountStatus(
remote.AccountStatus,
@@ -1582,29 +1758,40 @@ func worksmobileRemoteAccountStatus(remote WorksmobileRemoteUser) string {
)
}
func worksmobileUserNeedsUpdate(user domain.User, remote WorksmobileRemoteUser, localTenants map[string]domain.Tenant) bool {
func worksmobileUserNeedsUpdate(user domain.User, remote WorksmobileRemoteUser, localTenants map[string]domain.Tenant, remoteOrgUnitByExternalID map[string]WorksmobileRemoteGroup) bool {
return len(worksmobileUserUpdateReasons(user, remote, localTenants, remoteOrgUnitByExternalID)) > 0
}
func worksmobileUserUpdateReasons(user domain.User, remote WorksmobileRemoteUser, localTenants map[string]domain.Tenant, remoteOrgUnitByExternalID map[string]WorksmobileRemoteGroup) []string {
reasons := []string{}
if strings.TrimSpace(remote.ExternalID) != strings.TrimSpace(user.ID) {
return true
reasons = append(reasons, "external_key")
}
if strings.TrimSpace(remote.DisplayName) != strings.TrimSpace(user.Name) {
return true
reasons = append(reasons, "name")
}
if strings.ToLower(strings.TrimSpace(remote.Email)) != strings.ToLower(strings.TrimSpace(user.Email)) {
return true
reasons = append(reasons, "email")
}
if worksmobileUserPhoneNeedsUpdate(user, remote) {
return true
reasons = append(reasons, "phone")
}
if worksmobileUserEmployeeNumberNeedsUpdate(user, remote) {
return true
reasons = append(reasons, "employee_number")
}
return false
if worksmobileUserOrganizationsNeedUpdate(user, remote, localTenants, remoteOrgUnitByExternalID) {
reasons = append(reasons, "organization")
}
if worksmobileUserManagerNeedsUpdate(user, remote) {
reasons = append(reasons, "manager")
}
return reasons
}
func worksmobileUserPhoneNeedsUpdate(user domain.User, remote WorksmobileRemoteUser) bool {
localPhone := normalizeWorksmobilePhoneForCompare(user.Phone)
remotePhone := normalizeWorksmobilePhoneForCompare(remote.CellPhone)
if localPhone == "" && remotePhone == "" {
if localPhone == "" {
return false
}
if localPhone != remotePhone {
@@ -1636,11 +1823,11 @@ func worksmobileUserEmployeeNumberNeedsUpdate(user domain.User, remote Worksmobi
return localEmployeeNumber != remoteEmployeeNumber
}
func worksmobileUserOrganizationsNeedUpdate(user domain.User, remote WorksmobileRemoteUser, localTenants map[string]domain.Tenant) bool {
if len(remote.Organizations) == 0 || user.TenantID == nil || localTenants == nil {
func worksmobileUserOrganizationsNeedUpdate(user domain.User, remote WorksmobileRemoteUser, localTenants map[string]domain.Tenant, remoteOrgUnitByExternalID map[string]WorksmobileRemoteGroup) bool {
if localTenants == nil {
return false
}
tenantID := strings.TrimSpace(*user.TenantID)
tenantID := worksmobileUserComparisonTenantID(user, localTenants)
if tenantID == "" {
return false
}
@@ -1648,11 +1835,34 @@ func worksmobileUserOrganizationsNeedUpdate(user domain.User, remote Worksmobile
if !ok {
return false
}
expected, err := BuildWorksmobileUserPayloadForDomainTenants(user, tenant, localTenants, worksmobileComparisonRootConfig(localTenants))
if err != nil {
expectedOrganizations, _, err := buildWorksmobileUserOrganizations(user, tenant, localTenants, worksmobileComparisonRootConfig(localTenants))
if err != nil || len(expectedOrganizations) == 0 {
return worksmobileUserPrimaryOrganizationNeedsUpdate(user, remote, localTenants, remoteOrgUnitByExternalID)
}
remoteOrganizations := remote.Organizations
if len(remoteOrganizations) == 0 {
remoteOrganizations = worksmobileRemoteUserLegacyOrganizations(remote, remoteOrgUnitByExternalID)
} else {
remoteOrganizations = worksmobileRemoteUserOrganizationsForCompare(remote, remoteOrgUnitByExternalID)
}
return !worksmobileUserOrganizationsEqual(expectedOrganizations, remoteOrganizations)
}
func worksmobileUserPrimaryOrganizationNeedsUpdate(user domain.User, remote WorksmobileRemoteUser, localTenants map[string]domain.Tenant, remoteOrgUnitByExternalID map[string]WorksmobileRemoteGroup) bool {
tenantID := worksmobileUserComparisonPrimaryTenantID(user)
if tenantID == "" {
return false
}
return !worksmobileUserOrganizationsEqual(expected.Organizations, remote.Organizations)
tenant, ok := localTenants[tenantID]
if !ok || isWorksmobileDomainRootTenant(tenant) {
return false
}
expectedPrimary := "externalKey:" + tenantID
if remoteOrgUnit, ok := remoteOrgUnitByExternalID[tenantID]; ok && strings.TrimSpace(remoteOrgUnit.ID) != "" {
expectedPrimary = strings.TrimSpace(remoteOrgUnit.ID)
}
remotePrimaryOrgUnits := worksmobileRemotePrimaryOrgUnitIDs(remote)
return !worksmobileOrgUnitIDContains(remotePrimaryOrgUnits, expectedPrimary)
}
func worksmobileComparisonRootConfig(localTenants map[string]domain.Tenant) domain.JSONMap {
@@ -1664,9 +1874,131 @@ func worksmobileComparisonRootConfig(localTenants map[string]domain.Tenant) doma
return nil
}
func worksmobileUserComparisonPrimaryTenantID(user domain.User) string {
for _, appointment := range worksmobileAppointmentsFromMetadata(user.Metadata) {
if appointment.IsPrimary && strings.TrimSpace(appointment.TenantID) != "" {
return strings.TrimSpace(appointment.TenantID)
}
}
if user.TenantID == nil {
return ""
}
return strings.TrimSpace(*user.TenantID)
}
func worksmobileUserComparisonTenantID(user domain.User, localTenants map[string]domain.Tenant) string {
if user.TenantID != nil {
tenantID := strings.TrimSpace(*user.TenantID)
if _, ok := localTenants[tenantID]; ok {
return tenantID
}
}
for _, appointment := range worksmobileAppointmentsFromMetadata(user.Metadata) {
tenantID := strings.TrimSpace(appointment.TenantID)
if tenantID == "" {
continue
}
if _, ok := localTenants[tenantID]; ok {
return tenantID
}
}
return ""
}
func worksmobileRemoteUserLegacyOrganizations(remote WorksmobileRemoteUser, remoteOrgUnitByExternalID map[string]WorksmobileRemoteGroup) []WorksmobileUserOrganization {
if strings.TrimSpace(remote.PrimaryOrgUnitID) == "" {
return nil
}
return []WorksmobileUserOrganization{
{
DomainID: remote.DomainID,
Email: strings.TrimSpace(remote.Email),
Primary: true,
OrgUnits: []WorksmobileUserOrgUnit{
{
OrgUnitID: worksmobileCanonicalRemoteOrgUnitID(strings.TrimSpace(remote.PrimaryOrgUnitID), remoteOrgUnitByExternalID),
Primary: true,
PositionID: strings.TrimSpace(remote.PrimaryOrgUnitPositionID),
IsManager: remote.PrimaryOrgUnitIsManager,
},
},
},
}
}
func worksmobileRemoteUserOrganizationsForCompare(remote WorksmobileRemoteUser, remoteOrgUnitByExternalID map[string]WorksmobileRemoteGroup) []WorksmobileUserOrganization {
if len(remote.Organizations) == 0 {
return nil
}
primaryOrgUnitID := strings.TrimSpace(remote.PrimaryOrgUnitID)
result := make([]WorksmobileUserOrganization, len(remote.Organizations))
for i, organization := range remote.Organizations {
result[i] = organization
if result[i].DomainID == 0 {
result[i].DomainID = remote.DomainID
}
result[i].OrgUnits = make([]WorksmobileUserOrgUnit, len(organization.OrgUnits))
copy(result[i].OrgUnits, organization.OrgUnits)
for j, orgUnit := range result[i].OrgUnits {
result[i].OrgUnits[j].OrgUnitID = worksmobileCanonicalRemoteOrgUnitID(orgUnit.OrgUnitID, remoteOrgUnitByExternalID)
}
if primaryOrgUnitID == "" {
continue
}
for j, orgUnit := range result[i].OrgUnits {
if strings.TrimSpace(orgUnit.OrgUnitID) != worksmobileCanonicalRemoteOrgUnitID(primaryOrgUnitID, remoteOrgUnitByExternalID) {
continue
}
result[i].Primary = true
result[i].OrgUnits[j].Primary = true
}
}
return result
}
func worksmobileCanonicalRemoteOrgUnitID(orgUnitID string, remoteOrgUnitByExternalID map[string]WorksmobileRemoteGroup) string {
orgUnitID = strings.TrimSpace(orgUnitID)
if orgUnitID == "" || strings.HasPrefix(orgUnitID, "externalKey:") {
return orgUnitID
}
for externalID, remoteOrgUnit := range remoteOrgUnitByExternalID {
if strings.TrimSpace(remoteOrgUnit.ID) == orgUnitID && strings.TrimSpace(externalID) != "" {
return "externalKey:" + strings.TrimSpace(externalID)
}
}
return orgUnitID
}
func worksmobileRemotePrimaryOrgUnitIDs(remote WorksmobileRemoteUser) []string {
result := make([]string, 0, 1)
for _, organization := range remote.Organizations {
if !organization.Primary {
continue
}
for _, orgUnit := range organization.OrgUnits {
if orgUnit.Primary {
result = append(result, orgUnit.OrgUnitID)
}
}
}
if len(result) == 0 && strings.TrimSpace(remote.PrimaryOrgUnitID) != "" {
result = append(result, remote.PrimaryOrgUnitID)
}
return result
}
func worksmobileOrgUnitIDContains(values []string, expected string) bool {
expected = strings.TrimSpace(expected)
for _, value := range values {
if strings.TrimSpace(value) == expected {
return true
}
}
return false
}
type worksmobileComparableOrgUnit struct {
organizationPrimary bool
organizationEmail string
unitPrimary bool
positionID string
comparePosition bool
@@ -1688,9 +2020,6 @@ func worksmobileUserOrganizationsEqual(expected []WorksmobileUserOrganization, r
if expectedUnit.organizationPrimary != remoteUnit.organizationPrimary {
return false
}
if strings.ToLower(expectedUnit.organizationEmail) != strings.ToLower(remoteUnit.organizationEmail) {
return false
}
if expectedUnit.unitPrimary != remoteUnit.unitPrimary {
return false
}
@@ -1704,6 +2033,23 @@ func worksmobileUserOrganizationsEqual(expected []WorksmobileUserOrganization, r
return true
}
func worksmobilePrimaryOrgUnitCompareKey(organizations []WorksmobileUserOrganization) string {
for _, organization := range organizations {
if !organization.Primary {
continue
}
for _, orgUnit := range organization.OrgUnits {
if !orgUnit.Primary {
continue
}
if key := worksmobileComparableOrgUnitKey(organization.DomainID, orgUnit.OrgUnitID); key != "" {
return key
}
}
}
return ""
}
func flattenExpectedWorksmobileUserOrganizations(organizations []WorksmobileUserOrganization) map[string]worksmobileComparableOrgUnit {
result := map[string]worksmobileComparableOrgUnit{}
for _, organization := range organizations {
@@ -1714,7 +2060,6 @@ func flattenExpectedWorksmobileUserOrganizations(organizations []WorksmobileUser
}
result[key] = worksmobileComparableOrgUnit{
organizationPrimary: organization.Primary,
organizationEmail: strings.TrimSpace(organization.Email),
unitPrimary: orgUnit.Primary,
positionID: strings.TrimSpace(orgUnit.PositionID),
comparePosition: strings.TrimSpace(orgUnit.PositionID) != "",
@@ -1736,7 +2081,6 @@ func flattenRemoteWorksmobileUserOrganizations(organizations []WorksmobileUserOr
}
result[key] = worksmobileComparableOrgUnit{
organizationPrimary: organization.Primary,
organizationEmail: strings.TrimSpace(organization.Email),
unitPrimary: orgUnit.Primary,
positionID: strings.TrimSpace(orgUnit.PositionID),
manager: orgUnit.IsManager,
@@ -1766,22 +2110,43 @@ func worksmobileUserManagerNeedsUpdate(user domain.User, remote WorksmobileRemot
if len(localManagers) == 0 {
return false
}
remoteManagers := remote.OrgUnitManagers
if len(remoteManagers) == 0 && remote.PrimaryOrgUnitID != "" {
remoteManagers = map[string]*bool{remote.PrimaryOrgUnitID: remote.PrimaryOrgUnitIsManager}
}
for remoteOrgUnitID, remoteManager := range remoteManagers {
if remoteManager == nil {
continue
remoteManagers := worksmobileRemoteOrgUnitManagerMap(remote)
for localOrgUnitID, localManager := range localManagers {
remoteManager := false
if value, ok := remoteManagers[localOrgUnitID]; ok && value != nil {
remoteManager = *value
}
localManager, ok := localManagers[worksmobileOrgUnitLocalExternalKey(remoteOrgUnitID)]
if ok && localManager != *remoteManager {
if localManager != remoteManager {
return true
}
}
return false
}
func worksmobileRemoteOrgUnitManagerMap(remote WorksmobileRemoteUser) map[string]*bool {
result := map[string]*bool{}
for orgUnitID, isManager := range remote.OrgUnitManagers {
normalized := worksmobileOrgUnitLocalExternalKey(orgUnitID)
if normalized == "" {
continue
}
result[normalized] = isManager
}
for _, organization := range remote.Organizations {
for _, orgUnit := range organization.OrgUnits {
normalized := worksmobileOrgUnitLocalExternalKey(orgUnit.OrgUnitID)
if normalized == "" {
continue
}
result[normalized] = orgUnit.IsManager
}
}
if strings.TrimSpace(remote.PrimaryOrgUnitID) != "" {
result[worksmobileOrgUnitLocalExternalKey(remote.PrimaryOrgUnitID)] = remote.PrimaryOrgUnitIsManager
}
return result
}
func worksmobileUserExplicitOrgUnitManagers(user domain.User) map[string]bool {
managers := map[string]bool{}
for _, appointment := range worksmobileAppointmentsFromMetadata(user.Metadata) {