1
0
forked from baron/baron-sso
Files
baron-sso/backend/internal/service/worksmobile_sync_service.go

882 lines
30 KiB
Go

package service
import (
"baron-sso-backend/internal/domain"
"baron-sso-backend/internal/repository"
"context"
"errors"
"os"
"sort"
"strings"
)
const HanmacFamilyTenantSlug = "hanmac-family"
type WorksmobileSyncer interface {
EnqueueTenantUpsertIfInScope(ctx context.Context, tenant domain.Tenant) error
EnqueueTenantDeleteIfInScope(ctx context.Context, tenant domain.Tenant) error
EnqueueUserUpsertIfInScope(ctx context.Context, user domain.User) error
EnqueueUserDeleteIfInScope(ctx context.Context, user domain.User) error
}
type WorksmobileAdminService interface {
GetTenantOverview(ctx context.Context, tenantID string) (WorksmobileTenantOverview, error)
GetComparison(ctx context.Context, tenantID string, includeMatched bool) (WorksmobileComparison, error)
EnqueueBackfillDryRun(ctx context.Context, tenantID string) (WorksmobileBackfillDryRun, error)
EnqueueOrgUnitSync(ctx context.Context, tenantID, orgUnitID string) (*domain.WorksmobileOutbox, error)
EnqueueUserSync(ctx context.Context, tenantID, userID string) (*domain.WorksmobileOutbox, error)
RetryJob(ctx context.Context, tenantID, jobID string) (*domain.WorksmobileOutbox, error)
ListInitialPasswordCredentials(ctx context.Context, tenantID string) ([]WorksmobileInitialPasswordCredential, error)
}
type WorksmobileConfigSummary struct {
Enabled bool `json:"enabled"`
DomainMappings map[string]int64 `json:"domainMappings"`
TokenConfigured bool `json:"tokenConfigured"`
}
type WorksmobileTenantOverview struct {
Tenant domain.Tenant `json:"tenant"`
Config WorksmobileConfigSummary `json:"config"`
RecentJobs []domain.WorksmobileOutbox `json:"recentJobs"`
}
type WorksmobileBackfillDryRun struct {
OrgUnitCount int `json:"orgUnitCount"`
UserCount int `json:"userCount"`
}
type WorksmobileInitialPasswordCredential struct {
Email string `json:"email"`
InitialPassword string `json:"initialPassword"`
Status string `json:"status"`
LastError string `json:"lastError,omitempty"`
}
type WorksmobileComparison struct {
Users []WorksmobileComparisonItem `json:"users"`
Groups []WorksmobileComparisonItem `json:"groups"`
}
type WorksmobileComparisonItem struct {
ResourceType string `json:"resourceType"`
BaronID string `json:"baronId,omitempty"`
BaronName string `json:"baronName,omitempty"`
BaronEmail string `json:"baronEmail,omitempty"`
BaronPrimaryOrgID string `json:"baronPrimaryOrgId,omitempty"`
BaronPrimaryOrgName string `json:"baronPrimaryOrgName,omitempty"`
BaronParentID string `json:"baronParentId,omitempty"`
BaronParentName string `json:"baronParentName,omitempty"`
WorksmobileID string `json:"worksmobileId,omitempty"`
ExternalKey string `json:"externalKey,omitempty"`
WorksmobileName string `json:"worksmobileName,omitempty"`
WorksmobileEmail string `json:"worksmobileEmail,omitempty"`
WorksmobileLevelID string `json:"worksmobileLevelId,omitempty"`
WorksmobileLevelName string `json:"worksmobileLevelName,omitempty"`
WorksmobileTask string `json:"worksmobileTask,omitempty"`
WorksmobileDomainID int64 `json:"worksmobileDomainId,omitempty"`
WorksmobileDomainName string `json:"worksmobileDomainName,omitempty"`
WorksmobilePrimaryOrgID string `json:"worksmobilePrimaryOrgId,omitempty"`
WorksmobilePrimaryOrgName string `json:"worksmobilePrimaryOrgName,omitempty"`
WorksmobilePrimaryOrgPositionID string `json:"worksmobilePrimaryOrgPositionId,omitempty"`
WorksmobilePrimaryOrgPositionName string `json:"worksmobilePrimaryOrgPositionName,omitempty"`
WorksmobilePrimaryOrgIsManager *bool `json:"worksmobilePrimaryOrgIsManager,omitempty"`
WorksmobileParentID string `json:"worksmobileParentId,omitempty"`
WorksmobileParentName string `json:"worksmobileParentName,omitempty"`
Status string `json:"status"`
}
type worksmobileSyncService struct {
tenantService TenantService
userRepo repository.UserRepository
outboxRepo repository.WorksmobileOutboxRepository
client WorksmobileDirectoryClient
}
func NewWorksmobileSyncService(tenantService TenantService, userRepo repository.UserRepository, outboxRepo repository.WorksmobileOutboxRepository, client WorksmobileDirectoryClient) *worksmobileSyncService {
return &worksmobileSyncService{
tenantService: tenantService,
userRepo: userRepo,
outboxRepo: outboxRepo,
client: client,
}
}
func (s *worksmobileSyncService) GetTenantOverview(ctx context.Context, tenantID string) (WorksmobileTenantOverview, error) {
tenant, err := s.tenantService.GetTenant(ctx, tenantID)
if err != nil {
return WorksmobileTenantOverview{}, err
}
jobs, _ := s.outboxRepo.ListRecent(ctx, 50)
jobs = redactWorksmobileOutboxPayloads(jobs)
return WorksmobileTenantOverview{
Tenant: *tenant,
Config: WorksmobileConfigSummary{
Enabled: WorksmobileEnabled(tenant.Config),
DomainMappings: WorksmobileDomainMappings(tenant.Config),
TokenConfigured: worksmobileDirectoryAuthConfigured(),
},
RecentJobs: jobs,
}, nil
}
func worksmobileDirectoryAuthConfigured() bool {
if strings.TrimSpace(os.Getenv("WORKS_ADMIN_ACCESS_TOKEN")) != "" || strings.TrimSpace(os.Getenv("WORKS_ADMIN_OAUTH_ACCESS_TOKEN")) != "" {
return true
}
return strings.TrimSpace(os.Getenv("WORKS_ADMIN_OAUTH_CLIENT_ID")) != "" &&
strings.TrimSpace(os.Getenv("WORKS_ADMIN_OAUTH_CLIENT_SECRET")) != "" &&
strings.TrimSpace(os.Getenv("WORKS_ADMIN_OAUTH_CLIENT_SERVICE_ACCOUNT")) != "" &&
(strings.TrimSpace(os.Getenv("WORKS_ADMIN_OAUTH_CLIENT_PRIVATE_KEY")) != "" ||
strings.TrimSpace(os.Getenv("WORKS_ADMIN_OAUTH_CLIENT_PRIVATE_KEY_FILE")) != "")
}
func redactWorksmobileOutboxPayloads(jobs []domain.WorksmobileOutbox) []domain.WorksmobileOutbox {
for i := range jobs {
if jobs[i].Payload != nil {
jobs[i].Payload = nil
}
}
return jobs
}
func (s *worksmobileSyncService) GetComparison(ctx context.Context, tenantID string, includeMatched bool) (WorksmobileComparison, error) {
root, err := s.hanmacRoot(ctx, tenantID)
if err != nil {
return WorksmobileComparison{}, err
}
if s.client == nil {
return WorksmobileComparison{}, errors.New("worksmobile client is not configured")
}
tenants, err := s.hanmacSubtree(ctx, root.ID)
if err != nil {
return WorksmobileComparison{}, err
}
tenantByID := worksmobileTenantByID(tenants)
tenantByID[root.ID] = *root
tenantIDs := make([]string, 0, len(tenants))
for _, tenant := range tenants {
if isWorksmobileUserScopeTenant(tenant) {
tenantIDs = append(tenantIDs, tenant.ID)
}
}
users, err := s.userRepo.FindByTenantIDs(ctx, tenantIDs)
if err != nil {
return WorksmobileComparison{}, err
}
remoteUsers, err := s.client.ListUsers(ctx)
if err != nil {
return WorksmobileComparison{}, err
}
remoteGroups, err := s.client.ListGroups(ctx)
if err != nil {
return WorksmobileComparison{}, err
}
return WorksmobileComparison{
Users: compareWorksmobileUsers(users, remoteUsers, includeMatched, tenantByID),
Groups: compareWorksmobileGroups(append([]domain.Tenant{*root}, tenants...), remoteGroups, includeMatched),
}, nil
}
func (s *worksmobileSyncService) EnqueueBackfillDryRun(ctx context.Context, tenantID string) (WorksmobileBackfillDryRun, error) {
root, err := s.hanmacRoot(ctx, tenantID)
if err != nil {
return WorksmobileBackfillDryRun{}, err
}
tenants, err := s.hanmacSubtree(ctx, root.ID)
if err != nil {
return WorksmobileBackfillDryRun{}, err
}
orgUnitTenantIDs := make([]string, 0, len(tenants))
userTenantIDs := make([]string, 0, len(tenants))
tenantByID := worksmobileTenantByID(append([]domain.Tenant{*root}, tenants...))
for _, tenant := range tenants {
if isWorksmobileOrgUnitTenant(tenant, tenantByID) {
orgUnitTenantIDs = append(orgUnitTenantIDs, tenant.ID)
}
if isWorksmobileUserScopeTenant(tenant) {
userTenantIDs = append(userTenantIDs, tenant.ID)
}
}
users, err := s.userRepo.FindByTenantIDs(ctx, userTenantIDs)
if err != nil {
return WorksmobileBackfillDryRun{}, err
}
_ = s.outboxRepo.Create(ctx, &domain.WorksmobileOutbox{
ResourceType: domain.WorksmobileResourceOrgUnit,
ResourceID: root.ID,
Action: domain.WorksmobileActionDryRun,
DedupeKey: "backfill:dry-run:" + root.ID,
Payload: domain.JSONMap{
"tenantIds": orgUnitTenantIDs,
"userCount": len(users),
},
})
return WorksmobileBackfillDryRun{OrgUnitCount: len(orgUnitTenantIDs), UserCount: len(users)}, nil
}
func (s *worksmobileSyncService) EnqueueOrgUnitSync(ctx context.Context, tenantID, orgUnitID string) (*domain.WorksmobileOutbox, error) {
root, err := s.hanmacRoot(ctx, tenantID)
if err != nil {
return nil, err
}
tenant, err := s.tenantService.GetTenant(ctx, orgUnitID)
if err != nil {
return nil, err
}
tenantRoot, ok, err := s.rootForTenant(ctx, *tenant)
if err != nil {
return nil, err
}
if !ok || tenantRoot.ID != root.ID {
return nil, errors.New("target orgunit is outside hanmac-family subtree")
}
scopeTenants, err := s.hanmacSubtree(ctx, root.ID)
if err != nil {
return nil, err
}
tenantByID := worksmobileTenantByID(append([]domain.Tenant{*root}, scopeTenants...))
if !isWorksmobileOrgUnitTenant(*tenant, tenantByID) {
return nil, errors.New("target tenant is not a worksmobile orgunit tenant")
}
payload, err := BuildWorksmobileOrgUnitPayloadForDomainTenant(
*tenant,
worksmobileDomainClassificationTenant(*tenant, tenantByID),
root.Config,
0,
)
if err != nil {
return nil, err
}
payload = normalizeWorksmobileOrgUnitParent(payload, *tenant, tenantByID, root.ID)
item := &domain.WorksmobileOutbox{
ResourceType: domain.WorksmobileResourceOrgUnit,
ResourceID: tenant.ID,
Action: domain.WorksmobileActionUpsert,
DedupeKey: "orgunit:upsert:" + tenant.ID,
Payload: domain.JSONMap{"request": payload},
}
if err := s.outboxRepo.Create(ctx, item); err != nil {
return nil, err
}
return item, nil
}
func (s *worksmobileSyncService) EnqueueUserSync(ctx context.Context, tenantID, userID string) (*domain.WorksmobileOutbox, error) {
root, err := s.hanmacRoot(ctx, tenantID)
if err != nil {
return nil, err
}
user, err := s.userRepo.FindByID(ctx, userID)
if err != nil {
return nil, err
}
if user.TenantID == nil {
return nil, errors.New("target user has no tenant")
}
tenant, err := s.tenantService.GetTenant(ctx, *user.TenantID)
if err != nil {
return nil, err
}
tenantRoot, ok, err := s.rootForTenant(ctx, *tenant)
if err != nil {
return nil, err
}
if !ok || tenantRoot.ID != root.ID {
return nil, errors.New("target user is outside hanmac-family subtree")
}
scopeTenants, err := s.hanmacSubtree(ctx, root.ID)
if err != nil {
return nil, err
}
tenantByID := worksmobileTenantByID(append([]domain.Tenant{*root}, scopeTenants...))
payload, err := BuildWorksmobileUserPayloadForDomainTenants(
*user,
*tenant,
tenantByID,
root.Config,
)
if err != nil {
return nil, err
}
if err := s.validateUserAliasLocalParts(ctx, root, *user, payload); err != nil {
return nil, err
}
action := WorksmobileUserStatusAction(user.Status)
item := &domain.WorksmobileOutbox{
ResourceType: domain.WorksmobileResourceUser,
ResourceID: user.ID,
Action: action,
DedupeKey: "user:" + strings.ToLower(action) + ":" + user.ID,
Payload: worksmobileUserOutboxPayload(root.ID, payload),
}
if err := s.outboxRepo.Create(ctx, item); err != nil {
return nil, err
}
return item, nil
}
func (s *worksmobileSyncService) ListInitialPasswordCredentials(ctx context.Context, tenantID string) ([]WorksmobileInitialPasswordCredential, error) {
root, err := s.hanmacRoot(ctx, tenantID)
if err != nil {
return nil, err
}
jobs, err := s.outboxRepo.ListRecent(ctx, 1000)
if err != nil {
return nil, err
}
credentials := make([]WorksmobileInitialPasswordCredential, 0)
seen := map[string]bool{}
for _, job := range jobs {
if job.ResourceType != domain.WorksmobileResourceUser {
continue
}
if stringValue(job.Payload["tenantRootId"]) != root.ID {
continue
}
email := stringValue(job.Payload["loginEmail"])
password := stringValue(job.Payload["initialPassword"])
if email == "" || password == "" || seen[email] {
continue
}
seen[email] = true
credentials = append(credentials, WorksmobileInitialPasswordCredential{
Email: email,
InitialPassword: password,
Status: job.Status,
LastError: job.LastError,
})
}
return credentials, nil
}
func (s *worksmobileSyncService) RetryJob(ctx context.Context, tenantID, jobID string) (*domain.WorksmobileOutbox, error) {
if _, err := s.hanmacRoot(ctx, tenantID); err != nil {
return nil, err
}
if err := s.outboxRepo.MarkRetry(ctx, jobID); err != nil {
return nil, err
}
return s.outboxRepo.FindByID(ctx, jobID)
}
func (s *worksmobileSyncService) EnqueueTenantUpsertIfInScope(ctx context.Context, tenant domain.Tenant) error {
root, ok, err := s.rootForTenant(ctx, tenant)
if err != nil || !ok {
return err
}
scopeTenants, err := s.hanmacSubtree(ctx, root.ID)
if err != nil {
return err
}
tenantByID := worksmobileTenantByID(append([]domain.Tenant{*root}, scopeTenants...))
if !isWorksmobileOrgUnitTenant(tenant, tenantByID) {
return nil
}
payload, err := BuildWorksmobileOrgUnitPayloadForDomainTenant(
tenant,
worksmobileDomainClassificationTenant(tenant, tenantByID),
root.Config,
0,
)
if err != nil {
return err
}
payload = normalizeWorksmobileOrgUnitParent(payload, tenant, tenantByID, root.ID)
return s.outboxRepo.Create(ctx, &domain.WorksmobileOutbox{
ResourceType: domain.WorksmobileResourceOrgUnit,
ResourceID: tenant.ID,
Action: domain.WorksmobileActionUpsert,
DedupeKey: "orgunit:upsert:" + tenant.ID,
Payload: domain.JSONMap{"request": payload},
})
}
func (s *worksmobileSyncService) EnqueueTenantDeleteIfInScope(ctx context.Context, tenant domain.Tenant) error {
root, ok, err := s.rootForTenant(ctx, tenant)
if err != nil || !ok {
return err
}
scopeTenants, err := s.hanmacSubtree(ctx, root.ID)
if err != nil {
return err
}
tenantByID := worksmobileTenantByID(append([]domain.Tenant{*root}, scopeTenants...))
if !isWorksmobileOrgUnitTenant(tenant, tenantByID) {
return nil
}
return s.outboxRepo.Create(ctx, &domain.WorksmobileOutbox{
ResourceType: domain.WorksmobileResourceOrgUnit,
ResourceID: tenant.ID,
Action: domain.WorksmobileActionDelete,
DedupeKey: "orgunit:delete:" + tenant.ID,
Payload: domain.JSONMap{"orgUnitExternalKey": tenant.ID},
})
}
func (s *worksmobileSyncService) EnqueueUserUpsertIfInScope(ctx context.Context, user domain.User) error {
if user.TenantID == nil || *user.TenantID == "" {
return nil
}
tenant, err := s.tenantService.GetTenant(ctx, *user.TenantID)
if err != nil {
return err
}
root, ok, err := s.rootForTenant(ctx, *tenant)
if err != nil || !ok {
return err
}
scopeTenants, err := s.hanmacSubtree(ctx, root.ID)
if err != nil {
return err
}
tenantByID := worksmobileTenantByID(append([]domain.Tenant{*root}, scopeTenants...))
payload, err := BuildWorksmobileUserPayloadForDomainTenants(
user,
*tenant,
tenantByID,
root.Config,
)
if err != nil {
return err
}
if err := s.validateUserAliasLocalParts(ctx, root, user, payload); err != nil {
return err
}
action := WorksmobileUserStatusAction(user.Status)
return s.outboxRepo.Create(ctx, &domain.WorksmobileOutbox{
ResourceType: domain.WorksmobileResourceUser,
ResourceID: user.ID,
Action: action,
DedupeKey: "user:" + strings.ToLower(action) + ":" + user.ID,
Payload: worksmobileUserOutboxPayload(root.ID, payload),
})
}
func (s *worksmobileSyncService) EnqueueUserDeleteIfInScope(ctx context.Context, user domain.User) error {
if user.TenantID == nil || *user.TenantID == "" {
return nil
}
tenant, err := s.tenantService.GetTenant(ctx, *user.TenantID)
if err != nil {
return err
}
_, ok, err := s.rootForTenant(ctx, *tenant)
if err != nil || !ok {
return err
}
return s.outboxRepo.Create(ctx, &domain.WorksmobileOutbox{
ResourceType: domain.WorksmobileResourceUser,
ResourceID: user.ID,
Action: domain.WorksmobileActionDelete,
DedupeKey: "user:delete:" + user.ID,
Payload: domain.JSONMap{
"userExternalKey": user.ID,
"loginEmail": user.Email,
},
})
}
func (s *worksmobileSyncService) hanmacRoot(ctx context.Context, tenantID string) (*domain.Tenant, error) {
tenant, err := s.tenantService.GetTenant(ctx, tenantID)
if err != nil {
return nil, err
}
if tenant.Slug != HanmacFamilyTenantSlug || tenant.ParentID != nil {
return nil, errors.New("worksmobile is only available for hanmac-family root tenant")
}
return tenant, nil
}
func (s *worksmobileSyncService) hanmacSubtree(ctx context.Context, rootID string) ([]domain.Tenant, error) {
all, _, err := s.tenantService.ListTenants(ctx, 10000, 0, "")
if err != nil {
return nil, err
}
byParent := map[string][]domain.Tenant{}
for _, tenant := range all {
if tenant.ParentID != nil {
byParent[*tenant.ParentID] = append(byParent[*tenant.ParentID], tenant)
}
}
result := []domain.Tenant{}
var visit func(id string)
visit = func(id string) {
for _, child := range byParent[id] {
result = append(result, child)
visit(child.ID)
}
}
visit(rootID)
sort.Slice(result, func(i, j int) bool {
return result[i].Name < result[j].Name
})
return result, nil
}
func (s *worksmobileSyncService) rootForTenant(ctx context.Context, tenant domain.Tenant) (*domain.Tenant, bool, error) {
current := tenant
for current.ParentID != nil && *current.ParentID != "" {
parent, err := s.tenantService.GetTenant(ctx, *current.ParentID)
if err != nil {
return nil, false, err
}
current = *parent
}
return &current, current.Slug == HanmacFamilyTenantSlug, nil
}
func (s *worksmobileSyncService) validateUserAliasLocalParts(ctx context.Context, root *domain.Tenant, user domain.User, payload WorksmobileUserPayload) error {
if len(payload.AliasEmails) == 0 {
return nil
}
tenants, err := s.hanmacSubtree(ctx, root.ID)
if err != nil {
return err
}
tenantByID := make(map[string]domain.Tenant, len(tenants)+1)
tenantByID[root.ID] = *root
tenantIDs := make([]string, 0, len(tenants)+1)
tenantIDs = append(tenantIDs, root.ID)
for _, tenant := range tenants {
tenantByID[tenant.ID] = tenant
if isWorksmobileUserScopeTenant(tenant) {
tenantIDs = append(tenantIDs, tenant.ID)
}
}
users, err := s.userRepo.FindByTenantIDs(ctx, tenantIDs)
if err != nil {
return err
}
existing := map[string]string{}
for _, existingUser := range users {
if existingUser.ID == user.ID {
continue
}
addWorksmobileLocalPart(existing, existingUser.Email, existingUser.ID)
if existingUser.TenantID == nil {
continue
}
tenant, ok := tenantByID[*existingUser.TenantID]
if !ok {
continue
}
for _, alias := range BuildWorksmobileAliasEmails(existingUser, tenant) {
addWorksmobileLocalPart(existing, alias, existingUser.ID)
}
}
return ValidateWorksmobileAliasLocalParts(payload.Email, payload.AliasEmails, existing)
}
func addWorksmobileLocalPart(target map[string]string, email string, owner string) {
localPart, err := domain.ExtractNormalizedEmailLocalPart(email)
if err == nil && localPart != "" {
target[localPart] = owner
}
}
func isWorksmobileOrgUnitTenant(tenant domain.Tenant, tenantByID map[string]domain.Tenant) bool {
if tenant.Type == domain.TenantTypeOrganization {
return true
}
if tenant.Type == domain.TenantTypeCompany {
return isWorksmobileBarongroupChildCompany(tenant, tenantByID)
}
return false
}
func isWorksmobileUserScopeTenant(tenant domain.Tenant) bool {
return tenant.Type == domain.TenantTypeCompany || tenant.Type == domain.TenantTypeOrganization || tenant.Type == domain.TenantTypeUserGroup
}
func worksmobileDomainClassificationTenant(tenant domain.Tenant, tenantByID map[string]domain.Tenant) domain.Tenant {
current := tenant
for {
envKey := worksmobileTenantDomainIDEnvKey(current)
if envKey != "BARONGROUP_DOMAIN_ID" || current.Type == domain.TenantTypeCompany {
return current
}
parentID := worksmobileTenantParentID(current)
if parentID == "" {
return tenant
}
parent, ok := tenantByID[parentID]
if !ok {
return tenant
}
current = parent
}
}
func isWorksmobileBarongroupChildCompany(tenant domain.Tenant, tenantByID map[string]domain.Tenant) bool {
if tenant.Type != domain.TenantTypeCompany || tenant.Slug == "baron-group" {
return false
}
parentID := worksmobileTenantParentID(tenant)
for parentID != "" {
parent, ok := tenantByID[parentID]
if !ok {
return false
}
if parent.Slug == "baron-group" {
return true
}
parentID = worksmobileTenantParentID(parent)
}
return false
}
func normalizeWorksmobileOrgUnitParent(payload WorksmobileOrgUnitPayload, tenant domain.Tenant, tenantByID map[string]domain.Tenant, rootID string) WorksmobileOrgUnitPayload {
if tenant.ParentID != nil && *tenant.ParentID == rootID {
payload.ParentOrgUnitID = ""
}
if tenant.ParentID != nil {
if parent, ok := tenantByID[*tenant.ParentID]; ok && parent.Slug == "baron-group" {
payload.ParentOrgUnitID = ""
}
}
return payload
}
func worksmobileUserOutboxPayload(rootID string, payload WorksmobileUserPayload) domain.JSONMap {
return domain.JSONMap{
"request": payload,
"tenantRootId": rootID,
"loginEmail": payload.Email,
"initialPassword": payload.PasswordConfig.Password,
}
}
func stringValue(value any) string {
switch v := value.(type) {
case string:
return strings.TrimSpace(v)
default:
return ""
}
}
func compareWorksmobileUsers(localUsers []domain.User, remoteUsers []WorksmobileRemoteUser, includeMatched bool, localTenants map[string]domain.Tenant) []WorksmobileComparisonItem {
remoteByExternalID := map[string]WorksmobileRemoteUser{}
remoteByEmail := map[string]WorksmobileRemoteUser{}
for _, remote := range remoteUsers {
if remote.ExternalID != "" {
remoteByExternalID[remote.ExternalID] = remote
}
if normalizedEmail := strings.ToLower(strings.TrimSpace(remote.Email)); normalizedEmail != "" {
remoteByEmail[normalizedEmail] = remote
}
}
localByID := map[string]domain.User{}
matchedRemoteIDs := map[string]bool{}
result := make([]WorksmobileComparisonItem, 0)
for _, user := range localUsers {
localByID[user.ID] = user
remote, matched := remoteByExternalID[user.ID]
if !matched {
remote, matched = remoteByEmail[strings.ToLower(strings.TrimSpace(user.Email))]
}
if matched && !includeMatched {
matchedRemoteIDs[remote.ID] = true
continue
}
item := WorksmobileComparisonItem{
ResourceType: "USER",
BaronID: user.ID,
BaronName: user.Name,
BaronEmail: user.Email,
BaronPrimaryOrgID: worksmobileUserPrimaryOrgID(user),
BaronPrimaryOrgName: worksmobileUserPrimaryOrgName(user, localTenants),
Status: "missing_in_worksmobile",
}
if matched {
item.Status = "matched"
item.WorksmobileID = remote.ID
item.ExternalKey = remote.ExternalID
item.WorksmobileName = remote.DisplayName
item.WorksmobileEmail = remote.Email
item.WorksmobileLevelID = remote.LevelID
item.WorksmobileLevelName = remote.LevelName
item.WorksmobileTask = remote.Task
item.WorksmobileDomainID = remote.DomainID
item.WorksmobileDomainName = remote.DomainName
item.WorksmobilePrimaryOrgID = remote.PrimaryOrgUnitID
item.WorksmobilePrimaryOrgName = remote.PrimaryOrgUnitName
item.WorksmobilePrimaryOrgPositionID = remote.PrimaryOrgUnitPositionID
item.WorksmobilePrimaryOrgPositionName = remote.PrimaryOrgUnitPositionName
item.WorksmobilePrimaryOrgIsManager = remote.PrimaryOrgUnitIsManager
matchedRemoteIDs[remote.ID] = true
}
result = append(result, item)
}
for _, remote := range remoteUsers {
if matchedRemoteIDs[remote.ID] {
continue
}
if remote.ExternalID == "" {
result = append(result, WorksmobileComparisonItem{
ResourceType: "USER",
WorksmobileID: remote.ID,
ExternalKey: remote.ExternalID,
WorksmobileName: remote.DisplayName,
WorksmobileEmail: remote.Email,
WorksmobileLevelID: remote.LevelID,
WorksmobileLevelName: remote.LevelName,
WorksmobileTask: remote.Task,
WorksmobileDomainID: remote.DomainID,
WorksmobileDomainName: remote.DomainName,
WorksmobilePrimaryOrgID: remote.PrimaryOrgUnitID,
WorksmobilePrimaryOrgName: remote.PrimaryOrgUnitName,
WorksmobilePrimaryOrgPositionID: remote.PrimaryOrgUnitPositionID,
WorksmobilePrimaryOrgPositionName: remote.PrimaryOrgUnitPositionName,
WorksmobilePrimaryOrgIsManager: remote.PrimaryOrgUnitIsManager,
Status: "missing_external_key",
})
continue
}
if _, ok := localByID[remote.ExternalID]; !ok {
result = append(result, WorksmobileComparisonItem{
ResourceType: "USER",
WorksmobileID: remote.ID,
ExternalKey: remote.ExternalID,
WorksmobileName: remote.DisplayName,
WorksmobileEmail: remote.Email,
WorksmobileLevelID: remote.LevelID,
WorksmobileLevelName: remote.LevelName,
WorksmobileTask: remote.Task,
WorksmobileDomainID: remote.DomainID,
WorksmobileDomainName: remote.DomainName,
WorksmobilePrimaryOrgID: remote.PrimaryOrgUnitID,
WorksmobilePrimaryOrgName: remote.PrimaryOrgUnitName,
WorksmobilePrimaryOrgPositionID: remote.PrimaryOrgUnitPositionID,
WorksmobilePrimaryOrgPositionName: remote.PrimaryOrgUnitPositionName,
WorksmobilePrimaryOrgIsManager: remote.PrimaryOrgUnitIsManager,
Status: "missing_in_baron",
})
}
}
return result
}
func worksmobileUserPrimaryOrgID(user domain.User) string {
if user.TenantID == nil {
return ""
}
return strings.TrimSpace(*user.TenantID)
}
func worksmobileUserPrimaryOrgName(user domain.User, localTenants map[string]domain.Tenant) string {
tenantID := worksmobileUserPrimaryOrgID(user)
if tenantID == "" {
return ""
}
if tenant, ok := localTenants[tenantID]; ok {
return strings.TrimSpace(tenant.Name)
}
if user.Tenant != nil && user.Tenant.ID == tenantID {
return strings.TrimSpace(user.Tenant.Name)
}
return ""
}
func compareWorksmobileGroups(localTenants []domain.Tenant, remoteGroups []WorksmobileRemoteGroup, includeMatched bool) []WorksmobileComparisonItem {
remoteByExternalID := map[string]WorksmobileRemoteGroup{}
for _, remote := range remoteGroups {
if remote.ExternalID != "" {
remoteByExternalID[remote.ExternalID] = remote
}
}
tenantByID := worksmobileTenantByID(localTenants)
localByID := map[string]domain.Tenant{}
ignoredLocalByID := map[string]bool{}
result := make([]WorksmobileComparisonItem, 0)
for _, tenant := range localTenants {
if !isWorksmobileOrgUnitTenant(tenant, tenantByID) {
ignoredLocalByID[tenant.ID] = true
continue
}
localByID[tenant.ID] = tenant
remote, matched := remoteByExternalID[tenant.ID]
if matched && !includeMatched {
continue
}
item := WorksmobileComparisonItem{
ResourceType: "GROUP",
BaronID: tenant.ID,
BaronName: tenant.Name,
BaronParentID: worksmobileTenantParentID(tenant),
BaronParentName: worksmobileTenantParentName(tenant, tenantByID),
Status: "missing_in_worksmobile",
}
if matched {
item.Status = "matched"
item.WorksmobileID = remote.ID
item.ExternalKey = remote.ExternalID
item.WorksmobileName = remote.DisplayName
item.WorksmobileDomainID = remote.DomainID
item.WorksmobileDomainName = remote.DomainName
item.WorksmobileParentID = remote.ParentID
item.WorksmobileParentName = remote.ParentName
}
result = append(result, item)
}
for _, remote := range remoteGroups {
if remote.ExternalID == "" {
result = append(result, WorksmobileComparisonItem{
ResourceType: "GROUP",
WorksmobileID: remote.ID,
ExternalKey: remote.ExternalID,
WorksmobileName: remote.DisplayName,
WorksmobileDomainID: remote.DomainID,
WorksmobileDomainName: remote.DomainName,
WorksmobileParentID: remote.ParentID,
WorksmobileParentName: remote.ParentName,
Status: "missing_external_key",
})
continue
}
if ignoredLocalByID[remote.ExternalID] {
continue
}
if _, ok := localByID[remote.ExternalID]; !ok {
result = append(result, WorksmobileComparisonItem{
ResourceType: "GROUP",
WorksmobileID: remote.ID,
ExternalKey: remote.ExternalID,
WorksmobileName: remote.DisplayName,
WorksmobileDomainID: remote.DomainID,
WorksmobileDomainName: remote.DomainName,
WorksmobileParentID: remote.ParentID,
WorksmobileParentName: remote.ParentName,
Status: "missing_in_baron",
})
}
}
return result
}
func worksmobileTenantByID(tenants []domain.Tenant) map[string]domain.Tenant {
result := make(map[string]domain.Tenant, len(tenants))
for _, tenant := range tenants {
result[tenant.ID] = tenant
}
return result
}
func worksmobileTenantParentID(tenant domain.Tenant) string {
if tenant.ParentID == nil {
return ""
}
return strings.TrimSpace(*tenant.ParentID)
}
func worksmobileTenantParentName(tenant domain.Tenant, tenantByID map[string]domain.Tenant) string {
parentID := worksmobileTenantParentID(tenant)
if parentID == "" {
return ""
}
return strings.TrimSpace(tenantByID[parentID].Name)
}