forked from baron/baron-sso
네이버 계정 정합성 맞춤
This commit is contained in:
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user