forked from baron/baron-sso
사용자 상태 세분화
This commit is contained in:
@@ -384,14 +384,7 @@ func userGroupTraitStringArray(traits map[string]interface{}, key string) []stri
|
||||
}
|
||||
|
||||
func userGroupIdentityStatus(state string) string {
|
||||
switch state {
|
||||
case "", "active":
|
||||
return domain.UserStatusActive
|
||||
case "inactive":
|
||||
return domain.UserStatusInactive
|
||||
default:
|
||||
return state
|
||||
}
|
||||
return domain.NormalizeUserStatus(state)
|
||||
}
|
||||
|
||||
func (s *userGroupService) RemoveMember(ctx context.Context, groupID, userID string) error {
|
||||
|
||||
@@ -145,14 +145,9 @@ func kratosProjectionTraitStringArray(traits map[string]interface{}, key string)
|
||||
}
|
||||
|
||||
func normalizeProjectionStatus(state string) string {
|
||||
switch strings.ToLower(strings.TrimSpace(state)) {
|
||||
case "blocked", domain.UserStatusInactive:
|
||||
return domain.UserStatusInactive
|
||||
case domain.UserStatusSuspended:
|
||||
return domain.UserStatusSuspended
|
||||
case domain.UserStatusLeaveOfAbsence:
|
||||
return domain.UserStatusLeaveOfAbsence
|
||||
default:
|
||||
normalized := domain.NormalizeUserStatus(state)
|
||||
if normalized == "" {
|
||||
return domain.UserStatusActive
|
||||
}
|
||||
return normalized
|
||||
}
|
||||
|
||||
@@ -96,3 +96,16 @@ func TestUserProjectionSyncService_ReconcileMarksFailedWhenKratosFails(t *testin
|
||||
assert.Empty(t, repo.replacedUsers)
|
||||
kratos.AssertExpectations(t)
|
||||
}
|
||||
|
||||
func TestMapKratosIdentityToLocalUserPreservesArchivedStatus(t *testing.T) {
|
||||
user := MapKratosIdentityToLocalUser(KratosIdentity{
|
||||
ID: "00000000-0000-0000-0000-000000000201",
|
||||
State: domain.UserStatusArchived,
|
||||
Traits: map[string]interface{}{
|
||||
"email": "archived@example.com",
|
||||
"name": "Archived User",
|
||||
},
|
||||
})
|
||||
|
||||
assert.Equal(t, domain.UserStatusArchived, user.Status)
|
||||
}
|
||||
|
||||
@@ -924,6 +924,7 @@ type fakeWorksmobileDirectoryClient struct {
|
||||
deletedUsers []string
|
||||
activeUsers []string
|
||||
suspendedUsers []string
|
||||
users []WorksmobileRemoteUser
|
||||
orgUnitMatchKeys []string
|
||||
groups []WorksmobileRemoteGroup
|
||||
}
|
||||
@@ -1029,7 +1030,7 @@ func (f *fakeWorksmobileDirectoryClient) SetUserActive(ctx context.Context, user
|
||||
}
|
||||
|
||||
func (f *fakeWorksmobileDirectoryClient) ListUsers(ctx context.Context) ([]WorksmobileRemoteUser, error) {
|
||||
return nil, nil
|
||||
return f.users, nil
|
||||
}
|
||||
|
||||
func (f *fakeWorksmobileDirectoryClient) ListGroups(ctx context.Context) ([]WorksmobileRemoteGroup, error) {
|
||||
|
||||
@@ -402,8 +402,12 @@ func shuffleBytes(values []byte) {
|
||||
}
|
||||
|
||||
func WorksmobileUserStatusAction(status string) string {
|
||||
switch strings.ToLower(strings.TrimSpace(status)) {
|
||||
case domain.UserStatusInactive, domain.UserStatusSuspended, domain.UserStatusLeaveOfAbsence:
|
||||
normalized := domain.NormalizeUserStatus(status)
|
||||
if domain.IsWorksDeprovisionUserStatus(normalized) {
|
||||
return domain.WorksmobileActionDelete
|
||||
}
|
||||
switch normalized {
|
||||
case domain.UserStatusSuspended:
|
||||
return WorksmobileUserActionSuspend
|
||||
default:
|
||||
return WorksmobileUserActionUpsert
|
||||
|
||||
@@ -371,9 +371,13 @@ func containsAny(value string, candidates string) bool {
|
||||
|
||||
func TestWorksmobileUserStatusAction(t *testing.T) {
|
||||
require.Equal(t, WorksmobileUserActionUpsert, WorksmobileUserStatusAction(domain.UserStatusActive))
|
||||
require.Equal(t, WorksmobileUserActionSuspend, WorksmobileUserStatusAction(domain.UserStatusInactive))
|
||||
require.Equal(t, WorksmobileUserActionUpsert, WorksmobileUserStatusAction(domain.UserStatusTemporaryLeave))
|
||||
require.Equal(t, WorksmobileUserActionSuspend, WorksmobileUserStatusAction(domain.UserStatusSuspended))
|
||||
require.Equal(t, WorksmobileUserActionSuspend, WorksmobileUserStatusAction(domain.UserStatusLeaveOfAbsence))
|
||||
require.Equal(t, domain.WorksmobileActionDelete, WorksmobileUserStatusAction(domain.UserStatusExtendedLeave))
|
||||
require.Equal(t, domain.WorksmobileActionDelete, WorksmobileUserStatusAction(domain.UserStatusBaronGuest))
|
||||
require.Equal(t, domain.WorksmobileActionDelete, WorksmobileUserStatusAction(domain.UserStatusArchived))
|
||||
require.Equal(t, WorksmobileUserActionUpsert, WorksmobileUserStatusAction(domain.UserStatusLeaveOfAbsence))
|
||||
require.Equal(t, domain.WorksmobileActionDelete, WorksmobileUserStatusAction("baron_only"))
|
||||
}
|
||||
|
||||
func TestValidateWorksmobileExternalKeyRejectsUnsupportedCharacters(t *testing.T) {
|
||||
|
||||
@@ -215,6 +215,7 @@ func (s *worksmobileSyncService) EnqueueBackfillDryRun(ctx context.Context, tena
|
||||
if err != nil {
|
||||
return WorksmobileBackfillDryRun{}, err
|
||||
}
|
||||
users = worksmobileSyncScopeUsers(users)
|
||||
_ = s.outboxRepo.Create(ctx, &domain.WorksmobileOutbox{
|
||||
ResourceType: domain.WorksmobileResourceOrgUnit,
|
||||
ResourceID: root.ID,
|
||||
@@ -366,6 +367,12 @@ func (s *worksmobileSyncService) EnqueueUserSync(ctx context.Context, tenantID,
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if domain.IsWorksDeprovisionUserStatus(user.Status) {
|
||||
return s.enqueueUserDelete(ctx, *user, "user:delete:"+user.ID, root.ID)
|
||||
}
|
||||
if !domain.IsWorksProvisionedUserStatus(user.Status) {
|
||||
return nil, errors.New("target user status is excluded from Worksmobile sync")
|
||||
}
|
||||
tenantByID := worksmobileTenantByID(append([]domain.Tenant{*root}, scopeTenants...))
|
||||
payload, err := BuildWorksmobileUserPayloadForDomainTenants(
|
||||
*user,
|
||||
@@ -510,6 +517,13 @@ func (s *worksmobileSyncService) EnqueueUserUpsertIfInScope(ctx context.Context,
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if domain.IsWorksDeprovisionUserStatus(user.Status) {
|
||||
_, err := s.enqueueUserDelete(ctx, user, "user:delete:"+user.ID, root.ID)
|
||||
return err
|
||||
}
|
||||
if !domain.IsWorksProvisionedUserStatus(user.Status) {
|
||||
return nil
|
||||
}
|
||||
tenantByID := worksmobileTenantByID(append([]domain.Tenant{*root}, scopeTenants...))
|
||||
payload, err := BuildWorksmobileUserPayloadForDomainTenants(
|
||||
user,
|
||||
@@ -545,16 +559,32 @@ func (s *worksmobileSyncService) EnqueueUserDeleteIfInScope(ctx context.Context,
|
||||
if err != nil || !ok {
|
||||
return err
|
||||
}
|
||||
return s.outboxRepo.Create(ctx, &domain.WorksmobileOutbox{
|
||||
_, err = s.enqueueUserDelete(ctx, user, "user:delete:"+user.ID, "")
|
||||
return err
|
||||
}
|
||||
|
||||
func (s *worksmobileSyncService) enqueueUserDelete(ctx context.Context, user domain.User, dedupeKey string, rootID string) (*domain.WorksmobileOutbox, error) {
|
||||
payload := domain.JSONMap{
|
||||
"userExternalKey": user.ID,
|
||||
"loginEmail": user.Email,
|
||||
}
|
||||
if rootID != "" {
|
||||
payload["tenantRootId"] = rootID
|
||||
}
|
||||
if status := domain.NormalizeUserStatus(user.Status); status != "" {
|
||||
payload["baronStatus"] = status
|
||||
}
|
||||
item := &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,
|
||||
},
|
||||
})
|
||||
DedupeKey: dedupeKey,
|
||||
Payload: payload,
|
||||
}
|
||||
if err := s.outboxRepo.Create(ctx, item); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return item, nil
|
||||
}
|
||||
|
||||
func (s *worksmobileSyncService) hanmacRoot(ctx context.Context, tenantID string) (*domain.Tenant, error) {
|
||||
@@ -803,8 +833,18 @@ func compareWorksmobileUsers(localUsers []domain.User, remoteUsers []Worksmobile
|
||||
}
|
||||
localByID := map[string]domain.User{}
|
||||
matchedRemoteIDs := map[string]bool{}
|
||||
excludedLocalIDs := map[string]bool{}
|
||||
result := make([]WorksmobileComparisonItem, 0)
|
||||
for _, user := range localUsers {
|
||||
if !domain.IsWorksProvisionedUserStatus(user.Status) {
|
||||
excludedLocalIDs[user.ID] = true
|
||||
if remote, ok := remoteByExternalID[user.ID]; ok {
|
||||
matchedRemoteIDs[remote.ID] = true
|
||||
} else if remote, ok := remoteByEmail[strings.ToLower(strings.TrimSpace(user.Email))]; ok {
|
||||
matchedRemoteIDs[remote.ID] = true
|
||||
}
|
||||
continue
|
||||
}
|
||||
localByID[user.ID] = user
|
||||
remote, matched := remoteByExternalID[user.ID]
|
||||
if !matched {
|
||||
@@ -848,6 +888,9 @@ func compareWorksmobileUsers(localUsers []domain.User, remoteUsers []Worksmobile
|
||||
if matchedRemoteIDs[remote.ID] {
|
||||
continue
|
||||
}
|
||||
if excludedLocalIDs[remote.ExternalID] {
|
||||
continue
|
||||
}
|
||||
if remote.ExternalID == "" {
|
||||
result = append(result, WorksmobileComparisonItem{
|
||||
ResourceType: "USER",
|
||||
@@ -1094,3 +1137,17 @@ func worksmobileTenantParentSlug(tenant domain.Tenant, tenantByID map[string]dom
|
||||
}
|
||||
return strings.TrimSpace(tenantByID[parentID].Slug)
|
||||
}
|
||||
|
||||
func worksmobileSyncScopeUsers(users []domain.User) []domain.User {
|
||||
if len(users) == 0 {
|
||||
return users
|
||||
}
|
||||
filtered := make([]domain.User, 0, len(users))
|
||||
for _, user := range users {
|
||||
if !domain.IsWorksProvisionedUserStatus(user.Status) {
|
||||
continue
|
||||
}
|
||||
filtered = append(filtered, user)
|
||||
}
|
||||
return filtered
|
||||
}
|
||||
|
||||
@@ -101,6 +101,51 @@ func TestWorksmobileSyncServiceEnqueuesSuspendedUserStatusWithOrganizations(t *t
|
||||
require.Equal(t, "target@samaneng.com", outboxRepo.created[0].Payload["loginEmail"])
|
||||
}
|
||||
|
||||
func TestWorksmobileSyncServiceDeprovisionsArchivedUser(t *testing.T) {
|
||||
t.Setenv("SAMAN_DOMAIN_ID", "1001")
|
||||
rootID := "root-tenant"
|
||||
tenantID := "saman-tenant"
|
||||
root := domain.Tenant{
|
||||
ID: rootID,
|
||||
Slug: HanmacFamilyTenantSlug,
|
||||
Name: "Hanmac Family",
|
||||
}
|
||||
tenant := domain.Tenant{
|
||||
ID: tenantID,
|
||||
Slug: "saman",
|
||||
Name: "Saman",
|
||||
Type: domain.TenantTypeCompany,
|
||||
ParentID: &rootID,
|
||||
Domains: []domain.TenantDomain{{Domain: "samaneng.com"}},
|
||||
}
|
||||
target := domain.User{
|
||||
ID: "archived-user",
|
||||
Email: "archived@samaneng.com",
|
||||
Name: "Archived",
|
||||
Status: domain.UserStatusArchived,
|
||||
TenantID: &tenantID,
|
||||
}
|
||||
outboxRepo := &fakeWorksmobileOutboxRepo{}
|
||||
service := NewWorksmobileSyncService(
|
||||
&fakeWorksmobileTenantService{tenants: map[string]domain.Tenant{rootID: root, tenantID: tenant}, list: []domain.Tenant{root, tenant}},
|
||||
&fakeWorksmobileUserRepo{byID: map[string]domain.User{target.ID: target}, byTenant: []domain.User{target}},
|
||||
outboxRepo,
|
||||
nil,
|
||||
)
|
||||
|
||||
item, err := service.EnqueueUserSync(context.Background(), rootID, target.ID)
|
||||
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, item)
|
||||
require.Equal(t, domain.WorksmobileActionDelete, item.Action)
|
||||
require.Len(t, outboxRepo.created, 1)
|
||||
|
||||
err = service.EnqueueUserUpsertIfInScope(context.Background(), target)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, outboxRepo.created, 2)
|
||||
require.Equal(t, domain.WorksmobileActionDelete, outboxRepo.created[1].Action)
|
||||
}
|
||||
|
||||
func TestWorksmobileSyncServiceOverviewExposesAdminTenantIDForPasswordManageLink(t *testing.T) {
|
||||
t.Setenv("WORKS_ADMIN_TENANT_ID", "works-tenant-1")
|
||||
root := domain.Tenant{
|
||||
@@ -759,6 +804,88 @@ func TestWorksmobileSyncServiceKeepsCompanyUsersInComparisonScope(t *testing.T)
|
||||
require.ElementsMatch(t, []string{companyID, userGroupID}, userRepo.requestedTenantIDs)
|
||||
}
|
||||
|
||||
func TestWorksmobileSyncServiceSkipsArchivedUsersInComparison(t *testing.T) {
|
||||
rootID := "root-tenant"
|
||||
companyID := "company-tenant"
|
||||
root := domain.Tenant{
|
||||
ID: rootID,
|
||||
Slug: HanmacFamilyTenantSlug,
|
||||
Name: "한맥가족",
|
||||
}
|
||||
company := domain.Tenant{
|
||||
ID: companyID,
|
||||
Name: "계열사",
|
||||
Type: domain.TenantTypeCompany,
|
||||
ParentID: &rootID,
|
||||
}
|
||||
archived := domain.User{
|
||||
ID: "archived-user",
|
||||
Email: "archived@samaneng.com",
|
||||
Name: "Archived",
|
||||
TenantID: &companyID,
|
||||
Status: domain.UserStatusArchived,
|
||||
}
|
||||
service := NewWorksmobileSyncService(
|
||||
&fakeWorksmobileTenantService{tenants: map[string]domain.Tenant{rootID: root, companyID: company}, list: []domain.Tenant{root, company}},
|
||||
&fakeWorksmobileUserRepo{byTenant: []domain.User{archived}},
|
||||
&fakeWorksmobileOutboxRepo{},
|
||||
&fakeWorksmobileDirectoryClient{users: []WorksmobileRemoteUser{{
|
||||
ID: "works-archived",
|
||||
ExternalID: archived.ID,
|
||||
Email: archived.Email,
|
||||
}}},
|
||||
)
|
||||
|
||||
comparison, err := service.GetComparison(context.Background(), rootID, true)
|
||||
|
||||
require.NoError(t, err)
|
||||
require.Empty(t, comparison.Users)
|
||||
}
|
||||
|
||||
func TestWorksmobileSyncServiceBackfillDryRunSkipsArchivedUsers(t *testing.T) {
|
||||
rootID := "root-tenant"
|
||||
companyID := "company-tenant"
|
||||
root := domain.Tenant{
|
||||
ID: rootID,
|
||||
Slug: HanmacFamilyTenantSlug,
|
||||
Name: "한맥가족",
|
||||
}
|
||||
company := domain.Tenant{
|
||||
ID: companyID,
|
||||
Name: "계열사",
|
||||
Type: domain.TenantTypeCompany,
|
||||
ParentID: &rootID,
|
||||
}
|
||||
active := domain.User{
|
||||
ID: "active-user",
|
||||
Email: "active@samaneng.com",
|
||||
Name: "Active",
|
||||
TenantID: &companyID,
|
||||
Status: domain.UserStatusActive,
|
||||
}
|
||||
archived := domain.User{
|
||||
ID: "archived-user",
|
||||
Email: "archived@samaneng.com",
|
||||
Name: "Archived",
|
||||
TenantID: &companyID,
|
||||
Status: domain.UserStatusArchived,
|
||||
}
|
||||
outboxRepo := &fakeWorksmobileOutboxRepo{}
|
||||
service := NewWorksmobileSyncService(
|
||||
&fakeWorksmobileTenantService{tenants: map[string]domain.Tenant{rootID: root, companyID: company}, list: []domain.Tenant{root, company}},
|
||||
&fakeWorksmobileUserRepo{byTenant: []domain.User{active, archived}},
|
||||
outboxRepo,
|
||||
nil,
|
||||
)
|
||||
|
||||
dryRun, err := service.EnqueueBackfillDryRun(context.Background(), rootID)
|
||||
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 1, dryRun.UserCount)
|
||||
require.Len(t, outboxRepo.created, 1)
|
||||
require.Equal(t, 1, outboxRepo.created[0].Payload["userCount"])
|
||||
}
|
||||
|
||||
type fakeWorksmobileTenantService struct {
|
||||
tenants map[string]domain.Tenant
|
||||
list []domain.Tenant
|
||||
|
||||
Reference in New Issue
Block a user