forked from baron/baron-sso
882 lines
30 KiB
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 ¤t, current.Slug == HanmacFamilyTenantSlug, nil
|
|
}
|
|
|
|
func (s *worksmobileSyncService) validateUserAliasLocalParts(ctx context.Context, root *domain.Tenant, user domain.User, payload WorksmobileUserPayload) error {
|
|
if len(payload.AliasEmails) == 0 {
|
|
return nil
|
|
}
|
|
tenants, err := s.hanmacSubtree(ctx, root.ID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
tenantByID := make(map[string]domain.Tenant, len(tenants)+1)
|
|
tenantByID[root.ID] = *root
|
|
tenantIDs := make([]string, 0, len(tenants)+1)
|
|
tenantIDs = append(tenantIDs, root.ID)
|
|
for _, tenant := range tenants {
|
|
tenantByID[tenant.ID] = tenant
|
|
if isWorksmobileUserScopeTenant(tenant) {
|
|
tenantIDs = append(tenantIDs, tenant.ID)
|
|
}
|
|
}
|
|
users, err := s.userRepo.FindByTenantIDs(ctx, tenantIDs)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
existing := map[string]string{}
|
|
for _, existingUser := range users {
|
|
if existingUser.ID == user.ID {
|
|
continue
|
|
}
|
|
addWorksmobileLocalPart(existing, existingUser.Email, existingUser.ID)
|
|
if existingUser.TenantID == nil {
|
|
continue
|
|
}
|
|
tenant, ok := tenantByID[*existingUser.TenantID]
|
|
if !ok {
|
|
continue
|
|
}
|
|
for _, alias := range BuildWorksmobileAliasEmails(existingUser, tenant) {
|
|
addWorksmobileLocalPart(existing, alias, existingUser.ID)
|
|
}
|
|
}
|
|
return ValidateWorksmobileAliasLocalParts(payload.Email, payload.AliasEmails, existing)
|
|
}
|
|
|
|
func addWorksmobileLocalPart(target map[string]string, email string, owner string) {
|
|
localPart, err := domain.ExtractNormalizedEmailLocalPart(email)
|
|
if err == nil && localPart != "" {
|
|
target[localPart] = owner
|
|
}
|
|
}
|
|
|
|
func 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)
|
|
}
|