1
0
forked from baron/baron-sso

사용자 상태 세분화

This commit is contained in:
2026-05-20 10:17:15 +09:00
parent 9112c4fb36
commit 42b49674cc
33 changed files with 876 additions and 590 deletions

View File

@@ -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 {

View File

@@ -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
}

View File

@@ -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)
}

View File

@@ -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) {

View File

@@ -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

View File

@@ -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) {

View File

@@ -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
}

View File

@@ -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