forked from baron/baron-sso
feat: update worksmobile sync and restore planning
This commit is contained in:
@@ -5,9 +5,11 @@ import (
|
||||
"baron-sso-backend/internal/repository"
|
||||
"context"
|
||||
"errors"
|
||||
"net/mail"
|
||||
"os"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
const HanmacFamilyTenantSlug = "hanmac-family"
|
||||
@@ -25,9 +27,12 @@ type WorksmobileAdminService interface {
|
||||
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)
|
||||
EnqueueUserSync(ctx context.Context, tenantID, userID, credentialBatchID string) (*domain.WorksmobileOutbox, error)
|
||||
EnqueueUserPasswordReset(ctx context.Context, tenantID, userID, credentialBatchID string) (*domain.WorksmobileOutbox, error)
|
||||
RetryJob(ctx context.Context, tenantID, jobID string) (*domain.WorksmobileOutbox, error)
|
||||
ListInitialPasswordCredentials(ctx context.Context, tenantID string) ([]WorksmobileInitialPasswordCredential, error)
|
||||
ListInitialPasswordCredentials(ctx context.Context, tenantID, credentialBatchID string) ([]WorksmobileInitialPasswordCredential, error)
|
||||
ListCredentialBatches(ctx context.Context, tenantID string) ([]WorksmobileCredentialBatch, error)
|
||||
DeleteCredentialBatchPasswords(ctx context.Context, tenantID, credentialBatchID string) (WorksmobileCredentialBatch, error)
|
||||
}
|
||||
|
||||
type WorksmobileConfigSummary struct {
|
||||
@@ -49,10 +54,36 @@ type WorksmobileBackfillDryRun struct {
|
||||
}
|
||||
|
||||
type WorksmobileInitialPasswordCredential struct {
|
||||
Email string `json:"email"`
|
||||
InitialPassword string `json:"initialPassword"`
|
||||
Status string `json:"status"`
|
||||
LastError string `json:"lastError,omitempty"`
|
||||
Email string `json:"email"`
|
||||
Name string `json:"name,omitempty"`
|
||||
PrimaryLeafOrgName string `json:"primaryLeafOrgName,omitempty"`
|
||||
InitialPassword string `json:"initialPassword"`
|
||||
Status string `json:"status"`
|
||||
LastError string `json:"lastError,omitempty"`
|
||||
}
|
||||
|
||||
type WorksmobileCredentialBatch struct {
|
||||
BatchID string `json:"batchId"`
|
||||
Operation string `json:"operation,omitempty"`
|
||||
UserCount int `json:"userCount"`
|
||||
PendingCount int `json:"pendingCount"`
|
||||
ProcessingCount int `json:"processingCount"`
|
||||
ProcessedCount int `json:"processedCount"`
|
||||
FailedCount int `json:"failedCount"`
|
||||
HasPasswords bool `json:"hasPasswords"`
|
||||
DeletedAt string `json:"deletedAt,omitempty"`
|
||||
Failures []WorksmobileCredentialBatchFailure `json:"failures,omitempty"`
|
||||
CreatedAt time.Time `json:"createdAt"`
|
||||
UpdatedAt time.Time `json:"updatedAt"`
|
||||
}
|
||||
|
||||
type WorksmobileCredentialBatchFailure struct {
|
||||
UserID string `json:"userId,omitempty"`
|
||||
Email string `json:"email,omitempty"`
|
||||
Status string `json:"status"`
|
||||
RetryCount int `json:"retryCount"`
|
||||
LastError string `json:"lastError,omitempty"`
|
||||
UpdatedAt string `json:"updatedAt,omitempty"`
|
||||
}
|
||||
|
||||
type WorksmobileComparison struct {
|
||||
@@ -93,6 +124,10 @@ type WorksmobileComparisonItem struct {
|
||||
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"`
|
||||
}
|
||||
|
||||
@@ -185,8 +220,10 @@ func (s *worksmobileSyncService) GetComparison(ctx context.Context, tenantID str
|
||||
return WorksmobileComparison{}, err
|
||||
}
|
||||
|
||||
recentJobs, _ := s.outboxRepo.ListRecent(ctx, 1000)
|
||||
|
||||
return WorksmobileComparison{
|
||||
Users: compareWorksmobileUsers(users, remoteUsers, includeMatched, tenantByID),
|
||||
Users: compareWorksmobileUsers(users, remoteUsers, includeMatched, tenantByID, worksmobileUserJobSummaries(recentJobs)),
|
||||
Groups: compareWorksmobileGroups(append([]domain.Tenant{*root}, tenants...), remoteGroups, includeMatched),
|
||||
}, nil
|
||||
}
|
||||
@@ -340,7 +377,7 @@ func (s *worksmobileSyncService) EnqueueOrgUnitDelete(ctx context.Context, tenan
|
||||
return item, nil
|
||||
}
|
||||
|
||||
func (s *worksmobileSyncService) EnqueueUserSync(ctx context.Context, tenantID, userID string) (*domain.WorksmobileOutbox, error) {
|
||||
func (s *worksmobileSyncService) EnqueueUserSync(ctx context.Context, tenantID, userID, credentialBatchID string) (*domain.WorksmobileOutbox, error) {
|
||||
root, err := s.hanmacRoot(ctx, tenantID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -394,18 +431,104 @@ func (s *worksmobileSyncService) EnqueueUserSync(ctx context.Context, tenantID,
|
||||
DedupeKey: "user:" + strings.ToLower(action) + ":" + user.ID,
|
||||
Payload: worksmobileUserOutboxPayload(root.ID, payload, user.Status),
|
||||
}
|
||||
item.Payload["displayName"] = strings.TrimSpace(user.Name)
|
||||
item.Payload["primaryLeafOrgName"] = worksmobileUserPrimaryOrgName(*user, tenantByID)
|
||||
if batchID := strings.TrimSpace(credentialBatchID); batchID != "" {
|
||||
item.Payload["credentialBatchId"] = batchID
|
||||
item.Payload["credentialOperation"] = "worksmobile_user_sync"
|
||||
item.Payload["credentialBatchCreatedAt"] = time.Now().UTC().Format(time.RFC3339Nano)
|
||||
}
|
||||
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) {
|
||||
func (s *worksmobileSyncService) EnqueueUserPasswordReset(ctx context.Context, tenantID, userID, credentialBatchID string) (*domain.WorksmobileOutbox, error) {
|
||||
root, err := s.hanmacRoot(ctx, tenantID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
jobs, err := s.outboxRepo.ListRecent(ctx, 1000)
|
||||
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")
|
||||
}
|
||||
if !domain.IsWorksProvisionedUserStatus(user.Status) {
|
||||
return nil, errors.New("target user status is excluded from Worksmobile password reset")
|
||||
}
|
||||
scopeTenants, err := s.hanmacSubtree(ctx, root.ID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
tenantByID := worksmobileTenantByID(append([]domain.Tenant{*root}, scopeTenants...))
|
||||
payload, err := BuildWorksmobileUserPayloadForDomainTenants(*user, *tenant, tenantByID, root.Config)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
password := GenerateWorksmobileInitialPassword()
|
||||
request := WorksmobilePasswordResetPayload{
|
||||
Email: strings.TrimSpace(payload.Email),
|
||||
PasswordConfig: WorksmobilePasswordConfig{
|
||||
PasswordCreationType: "ADMIN",
|
||||
Password: password,
|
||||
},
|
||||
}
|
||||
batchID := strings.TrimSpace(credentialBatchID)
|
||||
batchCreatedAt := time.Now().UTC().Format(time.RFC3339Nano)
|
||||
dedupeSuffix := batchID
|
||||
if dedupeSuffix == "" {
|
||||
dedupeSuffix = batchCreatedAt
|
||||
}
|
||||
item := &domain.WorksmobileOutbox{
|
||||
ResourceType: domain.WorksmobileResourceUser,
|
||||
ResourceID: user.ID,
|
||||
Action: domain.WorksmobileActionPasswordReset,
|
||||
DedupeKey: "user:password-reset:" + user.ID + ":" + dedupeSuffix,
|
||||
Payload: domain.JSONMap{
|
||||
"tenantRootId": root.ID,
|
||||
"loginEmail": request.Email,
|
||||
"userExternalKey": user.ID,
|
||||
"initialPassword": password,
|
||||
"displayName": strings.TrimSpace(user.Name),
|
||||
"primaryLeafOrgName": worksmobileUserPrimaryOrgName(*user, tenantByID),
|
||||
"credentialBatchId": batchID,
|
||||
"credentialOperation": "worksmobile_password_reset",
|
||||
"credentialBatchCreatedAt": batchCreatedAt,
|
||||
"request": request,
|
||||
},
|
||||
}
|
||||
if err := s.outboxRepo.Create(ctx, item); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return item, nil
|
||||
}
|
||||
|
||||
func (s *worksmobileSyncService) ListInitialPasswordCredentials(ctx context.Context, tenantID, credentialBatchID string) ([]WorksmobileInitialPasswordCredential, error) {
|
||||
root, err := s.hanmacRoot(ctx, tenantID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
credentialBatchID = strings.TrimSpace(credentialBatchID)
|
||||
var jobs []domain.WorksmobileOutbox
|
||||
if credentialBatchID != "" {
|
||||
jobs, err = s.outboxRepo.ListCredentialBatchJobs(ctx, root.ID, credentialBatchID)
|
||||
} else {
|
||||
jobs, err = s.outboxRepo.ListRecent(ctx, 1000)
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -418,6 +541,9 @@ func (s *worksmobileSyncService) ListInitialPasswordCredentials(ctx context.Cont
|
||||
if stringValue(job.Payload["tenantRootId"]) != root.ID {
|
||||
continue
|
||||
}
|
||||
if credentialBatchID != "" && stringValue(job.Payload["credentialBatchId"]) != credentialBatchID {
|
||||
continue
|
||||
}
|
||||
email := stringValue(job.Payload["loginEmail"])
|
||||
password := stringValue(job.Payload["initialPassword"])
|
||||
if email == "" || password == "" || seen[email] {
|
||||
@@ -425,15 +551,60 @@ func (s *worksmobileSyncService) ListInitialPasswordCredentials(ctx context.Cont
|
||||
}
|
||||
seen[email] = true
|
||||
credentials = append(credentials, WorksmobileInitialPasswordCredential{
|
||||
Email: email,
|
||||
InitialPassword: password,
|
||||
Status: job.Status,
|
||||
LastError: job.LastError,
|
||||
Email: email,
|
||||
Name: stringValue(job.Payload["displayName"]),
|
||||
PrimaryLeafOrgName: stringValue(job.Payload["primaryLeafOrgName"]),
|
||||
InitialPassword: password,
|
||||
Status: job.Status,
|
||||
LastError: job.LastError,
|
||||
})
|
||||
}
|
||||
return credentials, nil
|
||||
}
|
||||
|
||||
func (s *worksmobileSyncService) ListCredentialBatches(ctx context.Context, tenantID string) ([]WorksmobileCredentialBatch, error) {
|
||||
root, err := s.hanmacRoot(ctx, tenantID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
jobs, err := s.outboxRepo.ListCredentialBatchJobs(ctx, root.ID, "")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return aggregateWorksmobileCredentialBatches(jobs), nil
|
||||
}
|
||||
|
||||
func (s *worksmobileSyncService) DeleteCredentialBatchPasswords(ctx context.Context, tenantID, credentialBatchID string) (WorksmobileCredentialBatch, error) {
|
||||
root, err := s.hanmacRoot(ctx, tenantID)
|
||||
if err != nil {
|
||||
return WorksmobileCredentialBatch{}, err
|
||||
}
|
||||
credentialBatchID = strings.TrimSpace(credentialBatchID)
|
||||
if credentialBatchID == "" {
|
||||
return WorksmobileCredentialBatch{}, errors.New("credential batch id is required")
|
||||
}
|
||||
jobs, err := s.outboxRepo.ListCredentialBatchJobs(ctx, root.ID, credentialBatchID)
|
||||
if err != nil {
|
||||
return WorksmobileCredentialBatch{}, err
|
||||
}
|
||||
if len(jobs) == 0 {
|
||||
return WorksmobileCredentialBatch{}, errors.New("credential batch not found")
|
||||
}
|
||||
deletedAt := time.Now().UTC().Format(time.RFC3339)
|
||||
for i := range jobs {
|
||||
nextPayload := scrubWorksmobileCredentialPayload(jobs[i].Payload, deletedAt)
|
||||
if err := s.outboxRepo.UpdatePayload(ctx, jobs[i].ID, nextPayload); err != nil {
|
||||
return WorksmobileCredentialBatch{}, err
|
||||
}
|
||||
jobs[i].Payload = nextPayload
|
||||
}
|
||||
batches := aggregateWorksmobileCredentialBatches(jobs)
|
||||
if len(batches) == 0 {
|
||||
return WorksmobileCredentialBatch{}, errors.New("credential batch not found")
|
||||
}
|
||||
return batches[0], 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
|
||||
@@ -663,7 +834,7 @@ func (s *worksmobileSyncService) validateUserAliasLocalParts(ctx context.Context
|
||||
if existingUser.ID == user.ID {
|
||||
continue
|
||||
}
|
||||
addWorksmobileLocalPart(existing, existingUser.Email, existingUser.ID)
|
||||
addWorksmobileEmail(existing, existingUser.Email, existingUser.ID)
|
||||
if existingUser.TenantID == nil {
|
||||
continue
|
||||
}
|
||||
@@ -672,16 +843,16 @@ func (s *worksmobileSyncService) validateUserAliasLocalParts(ctx context.Context
|
||||
continue
|
||||
}
|
||||
for _, alias := range BuildWorksmobileAliasEmails(existingUser, tenant) {
|
||||
addWorksmobileLocalPart(existing, alias, existingUser.ID)
|
||||
addWorksmobileEmail(existing, alias, existingUser.ID)
|
||||
}
|
||||
}
|
||||
return ValidateWorksmobileAliasLocalParts(payload.Email, payload.AliasEmails, existing)
|
||||
return ValidateWorksmobileAliasEmails(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 addWorksmobileEmail(target map[string]string, email string, owner string) {
|
||||
normalized := strings.ToLower(strings.TrimSpace(email))
|
||||
if _, err := mail.ParseAddress(normalized); err == nil && normalized != "" {
|
||||
target[normalized] = owner
|
||||
}
|
||||
}
|
||||
|
||||
@@ -833,6 +1004,196 @@ func worksmobileUserOutboxPayload(rootID string, payload WorksmobileUserPayload,
|
||||
return outboxPayload
|
||||
}
|
||||
|
||||
func aggregateWorksmobileCredentialBatches(jobs []domain.WorksmobileOutbox) []WorksmobileCredentialBatch {
|
||||
byBatchID := map[string]*WorksmobileCredentialBatch{}
|
||||
for _, job := range jobs {
|
||||
batchID := stringValue(job.Payload["credentialBatchId"])
|
||||
if batchID == "" {
|
||||
continue
|
||||
}
|
||||
batch, ok := byBatchID[batchID]
|
||||
if !ok {
|
||||
createdAt := worksmobileCredentialBatchCreatedAt(job)
|
||||
batch = &WorksmobileCredentialBatch{
|
||||
BatchID: batchID,
|
||||
Operation: stringValue(job.Payload["credentialOperation"]),
|
||||
CreatedAt: createdAt,
|
||||
UpdatedAt: job.UpdatedAt,
|
||||
}
|
||||
byBatchID[batchID] = batch
|
||||
}
|
||||
batch.UserCount++
|
||||
if batch.Operation == "" {
|
||||
batch.Operation = stringValue(job.Payload["credentialOperation"])
|
||||
}
|
||||
jobBatchCreatedAt := worksmobileCredentialBatchCreatedAt(job)
|
||||
if jobBatchCreatedAt.Before(batch.CreatedAt) || batch.CreatedAt.IsZero() {
|
||||
batch.CreatedAt = jobBatchCreatedAt
|
||||
}
|
||||
if job.UpdatedAt.After(batch.UpdatedAt) {
|
||||
batch.UpdatedAt = job.UpdatedAt
|
||||
}
|
||||
switch job.Status {
|
||||
case domain.WorksmobileOutboxStatusPending:
|
||||
batch.PendingCount++
|
||||
case domain.WorksmobileOutboxStatusProcessing:
|
||||
batch.ProcessingCount++
|
||||
case domain.WorksmobileOutboxStatusProcessed:
|
||||
batch.ProcessedCount++
|
||||
case domain.WorksmobileOutboxStatusFailed:
|
||||
batch.FailedCount++
|
||||
batch.Failures = append(batch.Failures, WorksmobileCredentialBatchFailure{
|
||||
UserID: job.ResourceID,
|
||||
Email: worksmobileCredentialJobEmail(job),
|
||||
Status: job.Status,
|
||||
RetryCount: job.RetryCount,
|
||||
LastError: strings.TrimSpace(job.LastError),
|
||||
UpdatedAt: job.UpdatedAt.Format(time.RFC3339),
|
||||
})
|
||||
}
|
||||
if worksmobilePayloadHasPassword(job.Payload) {
|
||||
batch.HasPasswords = true
|
||||
}
|
||||
if deletedAt := stringValue(job.Payload["credentialDeletedAt"]); deletedAt != "" {
|
||||
batch.DeletedAt = deletedAt
|
||||
}
|
||||
}
|
||||
batches := make([]WorksmobileCredentialBatch, 0, len(byBatchID))
|
||||
for _, batch := range byBatchID {
|
||||
batches = append(batches, *batch)
|
||||
}
|
||||
sort.Slice(batches, func(i, j int) bool {
|
||||
return batches[i].CreatedAt.After(batches[j].CreatedAt)
|
||||
})
|
||||
return batches
|
||||
}
|
||||
|
||||
func worksmobileCredentialBatchCreatedAt(job domain.WorksmobileOutbox) time.Time {
|
||||
if value := stringValue(job.Payload["credentialBatchCreatedAt"]); value != "" {
|
||||
if parsed, err := time.Parse(time.RFC3339Nano, value); err == nil {
|
||||
return parsed.UTC()
|
||||
}
|
||||
if parsed, err := time.Parse(time.RFC3339, value); err == nil {
|
||||
return parsed.UTC()
|
||||
}
|
||||
}
|
||||
if !job.UpdatedAt.IsZero() && !job.CreatedAt.IsZero() && job.UpdatedAt.After(job.CreatedAt) {
|
||||
return job.UpdatedAt.UTC()
|
||||
}
|
||||
return job.CreatedAt.UTC()
|
||||
}
|
||||
|
||||
func worksmobileCredentialJobEmail(job domain.WorksmobileOutbox) string {
|
||||
if email := stringValue(job.Payload["loginEmail"]); email != "" {
|
||||
return email
|
||||
}
|
||||
switch request := job.Payload["request"].(type) {
|
||||
case WorksmobileUserPayload:
|
||||
return strings.TrimSpace(request.Email)
|
||||
case WorksmobilePasswordResetPayload:
|
||||
return strings.TrimSpace(request.Email)
|
||||
case map[string]any:
|
||||
return stringValue(request["email"])
|
||||
case domain.JSONMap:
|
||||
return stringValue(request["email"])
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
func scrubWorksmobileCredentialPayload(payload domain.JSONMap, deletedAt string) domain.JSONMap {
|
||||
nextPayload := make(domain.JSONMap, len(payload)+1)
|
||||
for key, value := range payload {
|
||||
nextPayload[key] = value
|
||||
}
|
||||
delete(nextPayload, "initialPassword")
|
||||
nextPayload["credentialDeletedAt"] = deletedAt
|
||||
nextPayload["request"] = scrubWorksmobileRequestPassword(nextPayload["request"])
|
||||
return nextPayload
|
||||
}
|
||||
|
||||
func scrubWorksmobileRequestPassword(request any) any {
|
||||
switch v := request.(type) {
|
||||
case WorksmobileUserPayload:
|
||||
v.PasswordConfig.Password = ""
|
||||
return v
|
||||
case WorksmobilePasswordResetPayload:
|
||||
v.PasswordConfig.Password = ""
|
||||
return v
|
||||
case map[string]any:
|
||||
next := make(map[string]any, len(v))
|
||||
for key, value := range v {
|
||||
next[key] = value
|
||||
}
|
||||
next["passwordConfig"] = scrubWorksmobilePasswordConfig(next["passwordConfig"])
|
||||
return next
|
||||
case domain.JSONMap:
|
||||
next := make(domain.JSONMap, len(v))
|
||||
for key, value := range v {
|
||||
next[key] = value
|
||||
}
|
||||
next["passwordConfig"] = scrubWorksmobilePasswordConfig(next["passwordConfig"])
|
||||
return next
|
||||
default:
|
||||
return request
|
||||
}
|
||||
}
|
||||
|
||||
func scrubWorksmobilePasswordConfig(config any) any {
|
||||
switch v := config.(type) {
|
||||
case WorksmobilePasswordConfig:
|
||||
v.Password = ""
|
||||
return v
|
||||
case map[string]any:
|
||||
next := make(map[string]any, len(v))
|
||||
for key, value := range v {
|
||||
next[key] = value
|
||||
}
|
||||
next["password"] = ""
|
||||
return next
|
||||
case domain.JSONMap:
|
||||
next := make(domain.JSONMap, len(v))
|
||||
for key, value := range v {
|
||||
next[key] = value
|
||||
}
|
||||
next["password"] = ""
|
||||
return next
|
||||
default:
|
||||
return config
|
||||
}
|
||||
}
|
||||
|
||||
func worksmobilePayloadHasPassword(payload domain.JSONMap) bool {
|
||||
if stringValue(payload["initialPassword"]) != "" {
|
||||
return true
|
||||
}
|
||||
switch request := payload["request"].(type) {
|
||||
case WorksmobileUserPayload:
|
||||
return strings.TrimSpace(request.PasswordConfig.Password) != ""
|
||||
case WorksmobilePasswordResetPayload:
|
||||
return strings.TrimSpace(request.PasswordConfig.Password) != ""
|
||||
case map[string]any:
|
||||
return worksmobilePasswordConfigHasPassword(request["passwordConfig"])
|
||||
case domain.JSONMap:
|
||||
return worksmobilePasswordConfigHasPassword(request["passwordConfig"])
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func worksmobilePasswordConfigHasPassword(config any) bool {
|
||||
switch v := config.(type) {
|
||||
case WorksmobilePasswordConfig:
|
||||
return strings.TrimSpace(v.Password) != ""
|
||||
case map[string]any:
|
||||
return stringValue(v["password"]) != ""
|
||||
case domain.JSONMap:
|
||||
return stringValue(v["password"]) != ""
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func stringValue(value any) string {
|
||||
switch v := value.(type) {
|
||||
case string:
|
||||
@@ -842,7 +1203,40 @@ func stringValue(value any) string {
|
||||
}
|
||||
}
|
||||
|
||||
func compareWorksmobileUsers(localUsers []domain.User, remoteUsers []WorksmobileRemoteUser, includeMatched bool, localTenants map[string]domain.Tenant) []WorksmobileComparisonItem {
|
||||
type worksmobileUserJobSummary struct {
|
||||
Status string
|
||||
RetryCount int
|
||||
LastError string
|
||||
LastAttemptAt string
|
||||
}
|
||||
|
||||
func worksmobileUserJobSummaries(jobs []domain.WorksmobileOutbox) map[string]worksmobileUserJobSummary {
|
||||
result := map[string]worksmobileUserJobSummary{}
|
||||
for _, job := range jobs {
|
||||
if job.ResourceType != domain.WorksmobileResourceUser {
|
||||
continue
|
||||
}
|
||||
if job.ResourceID == "" {
|
||||
continue
|
||||
}
|
||||
if _, exists := result[job.ResourceID]; exists {
|
||||
continue
|
||||
}
|
||||
result[job.ResourceID] = worksmobileUserJobSummary{
|
||||
Status: job.Status,
|
||||
RetryCount: job.RetryCount,
|
||||
LastError: strings.TrimSpace(job.LastError),
|
||||
LastAttemptAt: job.UpdatedAt.Format(time.RFC3339),
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func compareWorksmobileUsers(localUsers []domain.User, remoteUsers []WorksmobileRemoteUser, includeMatched bool, localTenants map[string]domain.Tenant, jobSummaries ...map[string]worksmobileUserJobSummary) []WorksmobileComparisonItem {
|
||||
jobSummaryByUserID := map[string]worksmobileUserJobSummary{}
|
||||
if len(jobSummaries) > 0 && jobSummaries[0] != nil {
|
||||
jobSummaryByUserID = jobSummaries[0]
|
||||
}
|
||||
remoteByExternalID := map[string]WorksmobileRemoteUser{}
|
||||
remoteByEmail := map[string]WorksmobileRemoteUser{}
|
||||
for _, remote := range remoteUsers {
|
||||
@@ -872,7 +1266,8 @@ func compareWorksmobileUsers(localUsers []domain.User, remoteUsers []Worksmobile
|
||||
if !matched {
|
||||
remote, matched = remoteByEmail[strings.ToLower(strings.TrimSpace(user.Email))]
|
||||
}
|
||||
if matched && !includeMatched {
|
||||
needsUpdate := matched && worksmobileUserNeedsUpdate(user, remote)
|
||||
if matched && !includeMatched && !needsUpdate {
|
||||
matchedRemoteIDs[remote.ID] = true
|
||||
continue
|
||||
}
|
||||
@@ -886,8 +1281,19 @@ func compareWorksmobileUsers(localUsers []domain.User, remoteUsers []Worksmobile
|
||||
BaronPrimaryOrgName: worksmobileUserPrimaryOrgName(user, localTenants),
|
||||
Status: "missing_in_worksmobile",
|
||||
}
|
||||
if summary, ok := jobSummaryByUserID[user.ID]; ok {
|
||||
item.WorksmobileJobStatus = summary.Status
|
||||
item.WorksmobileJobRetryCount = summary.RetryCount
|
||||
item.WorksmobileLastAttemptAt = summary.LastAttemptAt
|
||||
if summary.Status == domain.WorksmobileOutboxStatusFailed {
|
||||
item.WorksmobileLastError = summary.LastError
|
||||
}
|
||||
}
|
||||
if matched {
|
||||
item.Status = "matched"
|
||||
if needsUpdate {
|
||||
item.Status = "needs_update"
|
||||
}
|
||||
item.WorksmobileID = remote.ID
|
||||
item.ExternalKey = remote.ExternalID
|
||||
item.WorksmobileName = remote.DisplayName
|
||||
@@ -958,6 +1364,62 @@ func compareWorksmobileUsers(localUsers []domain.User, remoteUsers []Worksmobile
|
||||
return result
|
||||
}
|
||||
|
||||
func worksmobileUserNeedsUpdate(user domain.User, remote WorksmobileRemoteUser) bool {
|
||||
if strings.TrimSpace(remote.ExternalID) != strings.TrimSpace(user.ID) {
|
||||
return true
|
||||
}
|
||||
if strings.TrimSpace(remote.DisplayName) != strings.TrimSpace(user.Name) {
|
||||
return true
|
||||
}
|
||||
if strings.ToLower(strings.TrimSpace(remote.Email)) != strings.ToLower(strings.TrimSpace(user.Email)) {
|
||||
return true
|
||||
}
|
||||
if worksmobileUserManagerNeedsUpdate(user, remote) {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func worksmobileUserManagerNeedsUpdate(user domain.User, remote WorksmobileRemoteUser) bool {
|
||||
localManagers := worksmobileUserExplicitOrgUnitManagers(user)
|
||||
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
|
||||
}
|
||||
localManager, ok := localManagers[worksmobileOrgUnitLocalExternalKey(remoteOrgUnitID)]
|
||||
if ok && localManager != *remoteManager {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func worksmobileUserExplicitOrgUnitManagers(user domain.User) map[string]bool {
|
||||
managers := map[string]bool{}
|
||||
for _, appointment := range worksmobileAppointmentsFromMetadata(user.Metadata) {
|
||||
if appointment.TenantID == "" || !appointment.HasManager {
|
||||
continue
|
||||
}
|
||||
managers[appointment.TenantID] = appointment.IsManager
|
||||
}
|
||||
return managers
|
||||
}
|
||||
|
||||
func worksmobileOrgUnitLocalExternalKey(orgUnitID string) string {
|
||||
normalized := strings.TrimSpace(orgUnitID)
|
||||
if after, ok := strings.CutPrefix(normalized, "externalKey:"); ok {
|
||||
return strings.TrimSpace(after)
|
||||
}
|
||||
return normalized
|
||||
}
|
||||
|
||||
func worksmobileUserPrimaryOrgID(user domain.User) string {
|
||||
if user.TenantID == nil {
|
||||
return ""
|
||||
|
||||
Reference in New Issue
Block a user