1
0
forked from baron/baron-sso

chore: snapshot local state before dev merge

This commit is contained in:
2026-06-17 21:25:42 +09:00
parent b2808759d2
commit 49560e8a8c
107 changed files with 8958 additions and 939 deletions

View File

@@ -107,49 +107,69 @@ 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"`
UpdateReasons []string `json:"updateReasons,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"`
BaronGrade string `json:"baronGrade,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"`
UserMemberships []WorksmobileUserMembershipComparison `json:"userMemberships,omitempty"`
UpdateReasons []string `json:"updateReasons,omitempty"`
Status string `json:"status"`
}
type WorksmobileUserMembershipComparison struct {
BaronOrgID string `json:"baronOrgId,omitempty"`
BaronOrgSlug string `json:"baronOrgSlug,omitempty"`
BaronOrgName string `json:"baronOrgName,omitempty"`
BaronGrade string `json:"baronGrade,omitempty"`
BaronPrimary bool `json:"baronPrimary,omitempty"`
WorksmobileDomainID int64 `json:"worksmobileDomainId,omitempty"`
WorksmobileDomainName string `json:"worksmobileDomainName,omitempty"`
WorksmobileOrgID string `json:"worksmobileOrgId,omitempty"`
WorksmobileOrgName string `json:"worksmobileOrgName,omitempty"`
WorksmobileLevelID string `json:"worksmobileLevelId,omitempty"`
WorksmobileLevelName string `json:"worksmobileLevelName,omitempty"`
WorksmobileOrgPositionID string `json:"worksmobileOrgPositionId,omitempty"`
WorksmobileOrgIsManager *bool `json:"worksmobileOrgIsManager,omitempty"`
WorksmobilePrimary bool `json:"worksmobilePrimary,omitempty"`
GradeNeedsUpdate bool `json:"gradeNeedsUpdate,omitempty"`
}
type worksmobileSyncService struct {
@@ -362,7 +382,7 @@ func (s *worksmobileSyncService) GetComparison(ctx context.Context, tenantID str
tenantIDs = append(tenantIDs, tenant.ID)
}
}
users, err := s.comparisonUsers(ctx, tenantIDs)
users, err := s.comparisonUsers(ctx, tenantIDs, tenantByID)
if err != nil {
return WorksmobileComparison{}, err
}
@@ -383,7 +403,7 @@ func (s *worksmobileSyncService) GetComparison(ctx context.Context, tenantID str
}, nil
}
func (s *worksmobileSyncService) comparisonUsers(ctx context.Context, tenantIDs []string) ([]domain.User, error) {
func (s *worksmobileSyncService) comparisonUsers(ctx context.Context, tenantIDs []string, tenantByID map[string]domain.Tenant) ([]domain.User, error) {
if s.identityMirror != nil {
status, err := s.identityMirror.GetIdentityCacheStatus(ctx)
if err == nil &&
@@ -394,32 +414,100 @@ func (s *worksmobileSyncService) comparisonUsers(ctx context.Context, tenantIDs
if err != nil {
return nil, err
}
return worksmobileUsersFromIdentityMirror(identities, tenantIDs), nil
return worksmobileUsersFromIdentityMirror(identities, tenantIDs, tenantByID), nil
}
}
return s.userRepo.FindByTenantIDs(ctx, tenantIDs)
}
func worksmobileUsersFromIdentityMirror(identities []KratosIdentity, tenantIDs []string) []domain.User {
func worksmobileUsersFromIdentityMirror(identities []KratosIdentity, tenantIDs []string, tenantMaps ...map[string]domain.Tenant) []domain.User {
allowed := make(map[string]bool, len(tenantIDs))
for _, tenantID := range tenantIDs {
allowed[strings.TrimSpace(tenantID)] = true
tenantID = strings.TrimSpace(tenantID)
if tenantID == "" {
continue
}
allowed[strings.ToLower(tenantID)] = true
if len(tenantMaps) > 0 {
if tenant, ok := tenantMaps[0][tenantID]; ok {
if slug := strings.TrimSpace(tenant.Slug); slug != "" {
allowed[strings.ToLower(slug)] = true
}
}
}
}
users := make([]domain.User, 0, len(identities))
for _, identity := range identities {
tenantID := traitString(identity.Traits, "tenant_id")
if tenantID == "" || !allowed[tenantID] {
if !worksmobileIdentityMirrorMatchesTenant(identity.Traits, allowed) {
continue
}
user := worksmobileUserFromIdentity(identity)
if user.TenantID == nil || strings.TrimSpace(*user.TenantID) == "" {
if tenantID := worksmobileIdentityMirrorTenantID(identity.Traits, allowed); tenantID != "" {
user.TenantID = &tenantID
}
}
users = append(users, user)
}
return users
}
func worksmobileIdentityMirrorMatchesTenant(traits map[string]any, allowed map[string]bool) bool {
for _, key := range identityMirrorTenantKeys(traits) {
if allowed[strings.ToLower(strings.TrimSpace(key))] {
return true
}
}
return false
}
func worksmobileIdentityMirrorTenantID(traits map[string]any, allowed map[string]bool) string {
appointments := identityMirrorAppointments(traits["additionalAppointments"])
if len(appointments) == 0 {
if metadata, ok := traits["metadata"].(map[string]any); ok {
appointments = identityMirrorAppointments(metadata["additionalAppointments"])
}
}
for _, appointment := range appointments {
if !metadataBool(domain.JSONMap(appointment), "isPrimary", "primary") {
continue
}
if tenantID := worksmobileIdentityMirrorAllowedAppointmentTenantID(appointment, allowed); tenantID != "" {
return tenantID
}
}
for _, appointment := range appointments {
if tenantID := worksmobileIdentityMirrorAllowedAppointmentTenantID(appointment, allowed); tenantID != "" {
return tenantID
}
}
return ""
}
func worksmobileIdentityMirrorAllowedAppointmentTenantID(appointment map[string]any, allowed map[string]bool) string {
tenantID := strings.TrimSpace(identityMirrorAnyString(appointment["tenantId"]))
if tenantID == "" {
tenantID = strings.TrimSpace(identityMirrorAnyString(appointment["tenant_id"]))
}
if tenantID != "" && allowed[strings.ToLower(tenantID)] {
return tenantID
}
tenantSlug := strings.TrimSpace(identityMirrorAnyString(appointment["tenantSlug"]))
if tenantSlug == "" {
tenantSlug = strings.TrimSpace(identityMirrorAnyString(appointment["slug"]))
}
if tenantSlug != "" && allowed[strings.ToLower(tenantSlug)] {
return tenantID
}
return ""
}
func worksmobileUserFromIdentity(identity KratosIdentity) domain.User {
metadata := domain.JSONMap{}
for key, value := range identity.Traits {
if key == "grade" {
continue
}
metadata[key] = value
}
tenantID := traitString(identity.Traits, "tenant_id")
@@ -438,7 +526,6 @@ func worksmobileUserFromIdentity(identity KratosIdentity) domain.User {
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,
@@ -595,10 +682,8 @@ func (s *worksmobileSyncService) EnqueueOrgUnitDelete(ctx context.Context, tenan
return nil, errors.New("worksmobile orgunit not found")
}
tenantByID := worksmobileTenantByID(append([]domain.Tenant{*root}, scopeTenants...))
if tenant, ok := findWorksmobileOrgUnitTenantByRemoteLocalPart(*target, scopeTenants, tenantByID); ok {
return s.enqueueOrgUnitUpsert(ctx, root, tenant, scopeTenants)
}
if isProtectedWorksmobileRemoteOrgUnit(*root, scopeTenants, *target) {
_, matchedLocalPart := findWorksmobileOrgUnitTenantByRemoteLocalPart(*target, scopeTenants, tenantByID)
if !matchedLocalPart && isProtectedWorksmobileRemoteOrgUnit(*root, scopeTenants, *target) {
return nil, errors.New("protected worksmobile domain root orgunit cannot be deleted")
}
item := &domain.WorksmobileOutbox{
@@ -1622,7 +1707,9 @@ func compareWorksmobileUsersWithRemoteGroups(localUsers []domain.User, remoteUse
remote, matched = remoteByEmail[strings.ToLower(strings.TrimSpace(user.Email))]
}
updateReasons := []string(nil)
gradeComparison := worksmobileUserGradeComparison{}
if matched {
gradeComparison = worksmobileCompareUserGrade(user, remote, localTenants, remoteOrgUnitByExternalID)
updateReasons = worksmobileUserUpdateReasons(user, remote, localTenants, remoteOrgUnitByExternalID)
}
needsUpdate := len(updateReasons) > 0
@@ -1630,6 +1717,10 @@ func compareWorksmobileUsersWithRemoteGroups(localUsers []domain.User, remoteUse
matchedRemoteIDs[remote.ID] = true
continue
}
baronGrade, _ := worksmobileUserComparisonGradeWithTenant(user, remote, localTenants, remoteOrgUnitByExternalID)
if strings.TrimSpace(gradeComparison.LocalGrade) != "" {
baronGrade = gradeComparison.LocalGrade
}
item := WorksmobileComparisonItem{
ResourceType: "USER",
BaronID: user.ID,
@@ -1637,6 +1728,7 @@ func compareWorksmobileUsersWithRemoteGroups(localUsers []domain.User, remoteUse
BaronEmail: user.Email,
BaronPhone: user.Phone,
BaronEmployeeNumber: metadataEmployeeNumber(user.Metadata),
BaronGrade: baronGrade,
BaronPrimaryOrgID: worksmobileUserPrimaryOrgID(user),
BaronPrimaryOrgSlug: worksmobileUserPrimaryOrgSlug(user, localTenants),
BaronPrimaryOrgName: worksmobileUserPrimaryOrgName(user, localTenants),
@@ -1665,6 +1757,10 @@ func compareWorksmobileUsersWithRemoteGroups(localUsers []domain.User, remoteUse
item.WorksmobileAccountStatus = worksmobileRemoteAccountStatus(remote)
item.WorksmobileLevelID = remote.LevelID
item.WorksmobileLevelName = remote.LevelName
if gradeComparison.NeedsUpdate {
item.WorksmobileLevelID = gradeComparison.RemoteLevelID
item.WorksmobileLevelName = gradeComparison.RemoteLevelName
}
item.WorksmobileTask = remote.Task
item.WorksmobileDomainID = remote.DomainID
item.WorksmobileDomainName = remote.DomainName
@@ -1673,6 +1769,7 @@ func compareWorksmobileUsersWithRemoteGroups(localUsers []domain.User, remoteUse
item.WorksmobilePrimaryOrgPositionID = remote.PrimaryOrgUnitPositionID
item.WorksmobilePrimaryOrgPositionName = remote.PrimaryOrgUnitPositionName
item.WorksmobilePrimaryOrgIsManager = remote.PrimaryOrgUnitIsManager
item.UserMemberships = worksmobileUserMembershipComparisons(user, remote, localTenants, remoteOrgUnitByExternalID, gradeComparison)
matchedRemoteIDs[remote.ID] = true
}
result = append(result, item)
@@ -1779,6 +1876,9 @@ func worksmobileUserUpdateReasons(user domain.User, remote WorksmobileRemoteUser
if worksmobileUserEmployeeNumberNeedsUpdate(user, remote) {
reasons = append(reasons, "employee_number")
}
if worksmobileUserGradeNeedsUpdate(user, remote, localTenants, remoteOrgUnitByExternalID) {
reasons = append(reasons, "grade")
}
if worksmobileUserOrganizationsNeedUpdate(user, remote, localTenants, remoteOrgUnitByExternalID) {
reasons = append(reasons, "organization")
}
@@ -1823,6 +1923,229 @@ func worksmobileUserEmployeeNumberNeedsUpdate(user domain.User, remote Worksmobi
return localEmployeeNumber != remoteEmployeeNumber
}
func worksmobileUserGradeNeedsUpdate(user domain.User, remote WorksmobileRemoteUser, localTenants map[string]domain.Tenant, remoteOrgUnitByExternalID map[string]WorksmobileRemoteGroup) bool {
return worksmobileCompareUserGrade(user, remote, localTenants, remoteOrgUnitByExternalID).NeedsUpdate
}
func worksmobileUserComparisonGradeWithTenant(user domain.User, remote WorksmobileRemoteUser, localTenants map[string]domain.Tenant, remoteOrgUnitByExternalID map[string]WorksmobileRemoteGroup) (string, string) {
comparison := worksmobileCompareUserGrade(user, remote, localTenants, remoteOrgUnitByExternalID)
if strings.TrimSpace(comparison.LocalGrade) != "" {
return comparison.LocalGrade, comparison.TenantID
}
tenantID := worksmobileRemotePrimaryTenantID(remote, localTenants, remoteOrgUnitByExternalID)
if tenantID == "" {
tenantID = worksmobileUserComparisonTenantID(user, localTenants)
}
if tenantID == "" {
return "", ""
}
for _, appointment := range worksmobileAppointmentsFromMetadata(user.Metadata) {
if strings.TrimSpace(appointment.TenantID) != tenantID {
continue
}
grade := normalizeWorksmobileGradeForTenant(appointment.Grade, tenantID, localTenants)
if grade == "" {
return "", tenantID
}
return grade, tenantID
}
return "", tenantID
}
type worksmobileUserGradeComparison struct {
NeedsUpdate bool
TenantID string
LocalGrade string
RemoteLevelID string
RemoteLevelName string
}
type worksmobileRemoteOrganizationLevel struct {
levelID string
levelName string
primary bool
}
func worksmobileCompareUserGrade(user domain.User, remote WorksmobileRemoteUser, localTenants map[string]domain.Tenant, remoteOrgUnitByExternalID map[string]WorksmobileRemoteGroup) worksmobileUserGradeComparison {
if localTenants == nil {
return worksmobileUserGradeComparison{}
}
remoteLevelsByTenant := worksmobileRemoteOrganizationLevelsByTenant(remote, localTenants, remoteOrgUnitByExternalID)
if len(remoteLevelsByTenant) == 0 {
return worksmobileUserGradeComparison{}
}
fallback := worksmobileUserGradeComparison{}
for _, appointment := range worksmobileGradeComparisonAppointments(user, localTenants) {
tenantID := strings.TrimSpace(appointment.TenantID)
localGrade := normalizeWorksmobileGradeForTenant(appointment.Grade, tenantID, localTenants)
if tenantID == "" || localGrade == "" {
continue
}
if _, ok := localTenants[tenantID]; !ok {
continue
}
remoteLevel, ok := remoteLevelsByTenant[tenantID]
if !ok {
continue
}
comparison := worksmobileUserGradeComparison{
TenantID: tenantID,
LocalGrade: localGrade,
RemoteLevelID: strings.TrimSpace(remoteLevel.levelID),
RemoteLevelName: strings.TrimSpace(remoteLevel.levelName),
}
if fallback.LocalGrade == "" || remoteLevel.primary {
fallback = comparison
}
if comparison.RemoteLevelName == "" && comparison.RemoteLevelID == "" {
comparison.NeedsUpdate = true
return comparison
}
if !worksmobileRemoteLevelMatchesLocalGrade(localGrade, tenantID, comparison.RemoteLevelID, comparison.RemoteLevelName, localTenants) {
comparison.NeedsUpdate = true
return comparison
}
}
return fallback
}
func worksmobileRemoteLevelMatchesLocalGrade(localGrade, tenantID, remoteLevelID, remoteLevelName string, localTenants map[string]domain.Tenant) bool {
localGrade = strings.TrimSpace(localGrade)
remoteLevelID = strings.TrimSpace(remoteLevelID)
remoteLevelName = strings.TrimSpace(remoteLevelName)
if localGrade == "" {
return remoteLevelID == "" && remoteLevelName == ""
}
if remoteLevelName == localGrade || remoteLevelID == localGrade {
return true
}
expectedLevelID := worksmobileLevelIDForTenant(localGrade, tenantID, localTenants)
return WorksmobileLevelIdentifierMatchesRemote(expectedLevelID, remoteLevelID, remoteLevelName)
}
func worksmobileGradeComparisonAppointments(user domain.User, tenantByID map[string]domain.Tenant) []worksmobileAppointment {
appointments := worksmobileAppointmentsFromMetadata(user.Metadata)
hasGPDTDCGrade := false
for _, appointment := range appointments {
if strings.TrimSpace(appointment.Grade) == "" {
continue
}
if worksmobileTenantIsGPDTDCDescendant(appointment.TenantID, tenantByID) {
hasGPDTDCGrade = true
break
}
}
if !hasGPDTDCGrade {
return appointments
}
filtered := make([]worksmobileAppointment, 0, len(appointments))
for _, appointment := range appointments {
if worksmobileTenantIsGPDTDCDescendant(appointment.TenantID, tenantByID) {
filtered = append(filtered, appointment)
}
}
return filtered
}
func worksmobileRemoteOrganizationLevelsByTenant(remote WorksmobileRemoteUser, localTenants map[string]domain.Tenant, remoteOrgUnitByExternalID map[string]WorksmobileRemoteGroup) map[string]worksmobileRemoteOrganizationLevel {
result := map[string]worksmobileRemoteOrganizationLevel{}
if localTenants == nil {
return result
}
organizations := remote.Organizations
if len(organizations) == 0 {
organizations = worksmobileRemoteUserLegacyOrganizations(remote, remoteOrgUnitByExternalID)
} else {
organizations = worksmobileRemoteUserOrganizationsForCompare(remote, remoteOrgUnitByExternalID)
}
for _, organization := range organizations {
levelID := strings.TrimSpace(organization.LevelID)
levelName := strings.TrimSpace(organization.LevelName)
if levelID == "" && levelName == "" && (len(organizations) == 1 || organization.Primary) {
levelID = strings.TrimSpace(remote.LevelID)
levelName = strings.TrimSpace(remote.LevelName)
}
for _, orgUnit := range organization.OrgUnits {
tenantID := worksmobileOrgUnitLocalExternalKey(orgUnit.OrgUnitID)
if tenantID == "" {
continue
}
if _, ok := localTenants[tenantID]; !ok {
continue
}
current := worksmobileRemoteOrganizationLevel{
levelID: levelID,
levelName: levelName,
primary: organization.Primary || orgUnit.Primary,
}
existing, ok := result[tenantID]
if !ok || (!existing.primary && current.primary) {
result[tenantID] = current
}
}
}
return result
}
func worksmobileRemotePrimaryTenantID(remote WorksmobileRemoteUser, localTenants map[string]domain.Tenant, remoteOrgUnitByExternalID map[string]WorksmobileRemoteGroup) string {
if localTenants == nil {
return ""
}
for _, orgUnitID := range worksmobileRemotePrimaryOrgUnitIDs(remote) {
canonicalOrgUnitID := worksmobileCanonicalRemoteOrgUnitID(orgUnitID, remoteOrgUnitByExternalID)
tenantID := worksmobileOrgUnitLocalExternalKey(canonicalOrgUnitID)
if tenantID == "" {
continue
}
if _, ok := localTenants[tenantID]; ok {
return tenantID
}
}
return ""
}
func worksmobileIsResearchGrade(values ...string) bool {
for _, value := range values {
normalized := strings.ToLower(strings.TrimSpace(value))
if normalized == "" {
continue
}
if strings.Contains(normalized, "연구원") ||
strings.Contains(normalized, "선임") ||
strings.Contains(normalized, "책임") ||
strings.Contains(normalized, "수석") ||
strings.Contains(normalized, "research") ||
strings.Contains(normalized, "principal") {
return true
}
}
return false
}
func worksmobileTenantIsGPDTDCDescendant(tenantID string, tenantByID map[string]domain.Tenant) bool {
tenantID = strings.TrimSpace(tenantID)
if tenantID == "" || tenantByID == nil {
return false
}
visited := map[string]bool{}
currentID := tenantID
for currentID != "" {
if visited[currentID] {
return false
}
visited[currentID] = true
tenant, ok := tenantByID[currentID]
if !ok {
return false
}
if worksmobileTenantDomainIDEnvKey(tenant) == "GPDTDC_DOMAIN_ID" && isWorksmobileDomainRootTenant(tenant) {
return true
}
currentID = worksmobileTenantParentID(tenant)
}
return false
}
func worksmobileUserOrganizationsNeedUpdate(user domain.User, remote WorksmobileRemoteUser, localTenants map[string]domain.Tenant, remoteOrgUnitByExternalID map[string]WorksmobileRemoteGroup) bool {
if localTenants == nil {
return false
@@ -1911,9 +2234,11 @@ func worksmobileRemoteUserLegacyOrganizations(remote WorksmobileRemoteUser, remo
}
return []WorksmobileUserOrganization{
{
DomainID: remote.DomainID,
Email: strings.TrimSpace(remote.Email),
Primary: true,
DomainID: remote.DomainID,
Email: strings.TrimSpace(remote.Email),
Primary: true,
LevelID: strings.TrimSpace(remote.LevelID),
LevelName: strings.TrimSpace(remote.LevelName),
OrgUnits: []WorksmobileUserOrgUnit{
{
OrgUnitID: worksmobileCanonicalRemoteOrgUnitID(strings.TrimSpace(remote.PrimaryOrgUnitID), remoteOrgUnitByExternalID),
@@ -2009,20 +2334,17 @@ type worksmobileComparableOrgUnit struct {
func worksmobileUserOrganizationsEqual(expected []WorksmobileUserOrganization, remote []WorksmobileUserOrganization) bool {
expectedUnits := flattenExpectedWorksmobileUserOrganizations(expected)
remoteUnits := flattenRemoteWorksmobileUserOrganizations(remote)
if len(expectedUnits) != len(remoteUnits) {
if len(expectedUnits) == 0 {
return len(remoteUnits) == 0
}
if len(remoteUnits) == 0 {
return false
}
for key, expectedUnit := range expectedUnits {
remoteUnit, ok := remoteUnits[key]
for key, remoteUnit := range remoteUnits {
expectedUnit, ok := expectedUnits[key]
if !ok {
return false
}
if expectedUnit.organizationPrimary != remoteUnit.organizationPrimary {
return false
}
if expectedUnit.unitPrimary != remoteUnit.unitPrimary {
return false
}
if expectedUnit.comparePosition && strings.TrimSpace(expectedUnit.positionID) != strings.TrimSpace(remoteUnit.positionID) {
return false
}
@@ -2090,6 +2412,145 @@ func flattenRemoteWorksmobileUserOrganizations(organizations []WorksmobileUserOr
return result
}
type worksmobileRemoteMembershipDetail struct {
domainID int64
domainName string
orgUnitID string
orgUnitName string
levelID string
levelName string
positionID string
manager *bool
primary bool
}
func worksmobileUserMembershipComparisons(user domain.User, remote WorksmobileRemoteUser, localTenants map[string]domain.Tenant, remoteOrgUnitByExternalID map[string]WorksmobileRemoteGroup, gradeComparison worksmobileUserGradeComparison) []WorksmobileUserMembershipComparison {
if localTenants == nil {
return nil
}
tenantID := worksmobileUserComparisonTenantID(user, localTenants)
if tenantID == "" {
return nil
}
tenant, ok := localTenants[tenantID]
if !ok {
return nil
}
expectedOrganizations, _, err := buildWorksmobileUserOrganizations(user, tenant, localTenants, worksmobileComparisonRootConfig(localTenants))
if err != nil || len(expectedOrganizations) == 0 {
return nil
}
remoteOrganizations := remote.Organizations
if len(remoteOrganizations) == 0 {
remoteOrganizations = worksmobileRemoteUserLegacyOrganizations(remote, remoteOrgUnitByExternalID)
} else {
remoteOrganizations = worksmobileRemoteUserOrganizationsForCompare(remote, remoteOrgUnitByExternalID)
}
remoteMemberships := worksmobileRemoteMembershipDetailsByKey(remote, remoteOrganizations, remoteOrgUnitByExternalID)
appointments := worksmobileAppointmentsByTenantID(user.Metadata)
result := make([]WorksmobileUserMembershipComparison, 0)
for _, organization := range expectedOrganizations {
for _, orgUnit := range organization.OrgUnits {
baronOrgID := worksmobileOrgUnitLocalExternalKey(orgUnit.OrgUnitID)
if baronOrgID == "" {
continue
}
baronTenant, ok := localTenants[baronOrgID]
if !ok {
continue
}
item := WorksmobileUserMembershipComparison{
BaronOrgID: baronOrgID,
BaronOrgSlug: strings.TrimSpace(baronTenant.Slug),
BaronOrgName: strings.TrimSpace(baronTenant.Name),
BaronPrimary: organization.Primary || orgUnit.Primary,
}
if appointment, ok := appointments[baronOrgID]; ok {
item.BaronGrade = normalizeWorksmobileGradeForTenant(appointment.Grade, baronOrgID, localTenants)
}
key := worksmobileComparableOrgUnitKey(organization.DomainID, orgUnit.OrgUnitID)
if remoteMembership, ok := remoteMemberships[key]; ok {
item.WorksmobileDomainID = remoteMembership.domainID
item.WorksmobileDomainName = remoteMembership.domainName
item.WorksmobileOrgID = remoteMembership.orgUnitID
item.WorksmobileOrgName = remoteMembership.orgUnitName
item.WorksmobileLevelID = remoteMembership.levelID
item.WorksmobileLevelName = remoteMembership.levelName
item.WorksmobileOrgPositionID = remoteMembership.positionID
item.WorksmobileOrgIsManager = remoteMembership.manager
item.WorksmobilePrimary = remoteMembership.primary
}
item.GradeNeedsUpdate = gradeComparison.NeedsUpdate && strings.TrimSpace(gradeComparison.TenantID) == baronOrgID
result = append(result, item)
}
}
return result
}
func worksmobileAppointmentsByTenantID(metadata domain.JSONMap) map[string]worksmobileAppointment {
result := map[string]worksmobileAppointment{}
for _, appointment := range worksmobileAppointmentsFromMetadata(metadata) {
tenantID := strings.TrimSpace(appointment.TenantID)
if tenantID == "" {
continue
}
result[tenantID] = appointment
}
return result
}
func worksmobileRemoteMembershipDetailsByKey(remote WorksmobileRemoteUser, organizations []WorksmobileUserOrganization, remoteOrgUnitByExternalID map[string]WorksmobileRemoteGroup) map[string]worksmobileRemoteMembershipDetail {
result := map[string]worksmobileRemoteMembershipDetail{}
for _, organization := range organizations {
domainID := organization.DomainID
if domainID == 0 {
domainID = remote.DomainID
}
domainName := strings.TrimSpace(remote.DomainName)
levelID := strings.TrimSpace(organization.LevelID)
levelName := strings.TrimSpace(organization.LevelName)
if levelID == "" && levelName == "" && (len(organizations) == 1 || organization.Primary) {
levelID = strings.TrimSpace(remote.LevelID)
levelName = strings.TrimSpace(remote.LevelName)
}
for _, orgUnit := range organization.OrgUnits {
key := worksmobileComparableOrgUnitKey(domainID, orgUnit.OrgUnitID)
if key == "" {
continue
}
localExternalKey := worksmobileOrgUnitLocalExternalKey(orgUnit.OrgUnitID)
orgUnitID := strings.TrimSpace(orgUnit.OrgUnitID)
orgUnitName := ""
if localExternalKey != "" {
if remoteGroup, ok := remoteOrgUnitByExternalID[localExternalKey]; ok {
if strings.TrimSpace(remoteGroup.ID) != "" {
orgUnitID = strings.TrimSpace(remoteGroup.ID)
}
orgUnitName = strings.TrimSpace(remoteGroup.DisplayName)
if domainName == "" {
domainName = strings.TrimSpace(remoteGroup.DomainName)
}
}
}
if orgUnitName == "" && worksmobileOrgUnitIDContains([]string{remote.PrimaryOrgUnitID}, orgUnitID) {
orgUnitName = strings.TrimSpace(remote.PrimaryOrgUnitName)
}
result[key] = worksmobileRemoteMembershipDetail{
domainID: domainID,
domainName: domainName,
orgUnitID: orgUnitID,
orgUnitName: orgUnitName,
levelID: levelID,
levelName: levelName,
positionID: strings.TrimSpace(orgUnit.PositionID),
manager: orgUnit.IsManager,
primary: organization.Primary || orgUnit.Primary,
}
}
}
return result
}
func worksmobileComparableOrgUnitKey(domainID int64, orgUnitID string) string {
orgUnitID = strings.TrimSpace(orgUnitID)
if domainID == 0 || orgUnitID == "" {
@@ -2167,10 +2628,7 @@ func worksmobileOrgUnitLocalExternalKey(orgUnitID string) string {
}
func worksmobileUserPrimaryOrgID(user domain.User) string {
if user.TenantID == nil {
return ""
}
return strings.TrimSpace(*user.TenantID)
return worksmobileUserComparisonPrimaryTenantID(user)
}
func worksmobileUserPrimaryOrgName(user domain.User, localTenants map[string]domain.Tenant) string {