forked from baron/baron-sso
chore: consolidate local integration changes
This commit is contained in:
@@ -56,6 +56,11 @@ func main() {
|
||||
slog.Error("clear-orphan-user-tenant-memberships failed", "error", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
case "worksmobile-sync":
|
||||
if err := runWorksmobileSync(os.Args[2:]); err != nil {
|
||||
slog.Error("worksmobile-sync failed", "error", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
default:
|
||||
printUsage()
|
||||
os.Exit(2)
|
||||
@@ -227,4 +232,5 @@ func printUsage() {
|
||||
fmt.Fprintln(os.Stderr, "usage:")
|
||||
fmt.Fprintln(os.Stderr, " adminctl create-super-admin [--email EMAIL] [--password PASSWORD] [--name NAME] [--update-password]")
|
||||
fmt.Fprintln(os.Stderr, " adminctl clear-orphan-user-tenant-memberships [--dry-run]")
|
||||
fmt.Fprintln(os.Stderr, " adminctl worksmobile-sync [--orgunits] [--users-csv PATH] [--credential-batch-id ID] [--process] [--serialize-orgunits] [--serialize-users-batch ID] [--batch-size N] [--delay DURATION]")
|
||||
}
|
||||
|
||||
@@ -1,6 +1,11 @@
|
||||
package main
|
||||
|
||||
import "testing"
|
||||
import (
|
||||
"baron-sso-backend/internal/service"
|
||||
"context"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestResolveCreateSuperAdminConfigUsesEnvDefaults(t *testing.T) {
|
||||
t.Setenv("ADMIN_EMAIL", "admin@example.com")
|
||||
@@ -71,3 +76,66 @@ func TestResolveClearOrphanUserTenantMembershipsConfig(t *testing.T) {
|
||||
t.Fatal("dry-run flag was not set")
|
||||
}
|
||||
}
|
||||
|
||||
func TestAuditWorksmobileDuplicatePhoneCountryCodesReportsAndFixes(t *testing.T) {
|
||||
client := &fakeWorksmobilePhoneAuditClient{
|
||||
users: []service.WorksmobileRemoteUser{
|
||||
{
|
||||
ID: "works-user-1",
|
||||
ExternalID: "baron-user-1",
|
||||
Email: "one@example.com",
|
||||
DisplayName: "One",
|
||||
CellPhone: "+82 +821091917771",
|
||||
DomainID: 1001,
|
||||
DomainName: "samaneng.com",
|
||||
},
|
||||
{
|
||||
ID: "works-user-2",
|
||||
Email: "two@example.com",
|
||||
CellPhone: "+821012345678",
|
||||
DomainID: 1001,
|
||||
},
|
||||
},
|
||||
}
|
||||
output := &strings.Builder{}
|
||||
|
||||
count, err := auditWorksmobileDuplicatePhoneCountryCodes(context.Background(), output, true, client)
|
||||
|
||||
if err != nil {
|
||||
t.Fatalf("auditWorksmobileDuplicatePhoneCountryCodes returned error: %v", err)
|
||||
}
|
||||
if count != 1 {
|
||||
t.Fatalf("count=%d, want 1", count)
|
||||
}
|
||||
if !strings.Contains(output.String(), "one@example.com") || !strings.Contains(output.String(), "+821091917771") {
|
||||
t.Fatalf("audit output did not include normalized duplicate phone row: %s", output.String())
|
||||
}
|
||||
if len(client.patches) != 1 {
|
||||
t.Fatalf("patch count=%d, want 1", len(client.patches))
|
||||
}
|
||||
if client.patches[0].identifier != "works-user-1" {
|
||||
t.Fatalf("patch identifier=%q, want works-user-1", client.patches[0].identifier)
|
||||
}
|
||||
if client.patches[0].payload.CellPhone != "+821091917771" {
|
||||
t.Fatalf("patch cellPhone=%q, want +821091917771", client.patches[0].payload.CellPhone)
|
||||
}
|
||||
}
|
||||
|
||||
type fakeWorksmobilePhoneAuditClient struct {
|
||||
users []service.WorksmobileRemoteUser
|
||||
patches []fakeWorksmobilePhonePatch
|
||||
}
|
||||
|
||||
type fakeWorksmobilePhonePatch struct {
|
||||
identifier string
|
||||
payload service.WorksmobileUserPatchPayload
|
||||
}
|
||||
|
||||
func (f *fakeWorksmobilePhoneAuditClient) ListUsers(ctx context.Context) ([]service.WorksmobileRemoteUser, error) {
|
||||
return f.users, nil
|
||||
}
|
||||
|
||||
func (f *fakeWorksmobilePhoneAuditClient) PatchUser(ctx context.Context, identifier string, payload service.WorksmobileUserPatchPayload) error {
|
||||
f.patches = append(f.patches, fakeWorksmobilePhonePatch{identifier: identifier, payload: payload})
|
||||
return nil
|
||||
}
|
||||
|
||||
1709
backend/cmd/adminctl/worksmobile_sync.go
Normal file
1709
backend/cmd/adminctl/worksmobile_sync.go
Normal file
File diff suppressed because it is too large
Load Diff
38
backend/cmd/adminctl/worksmobile_sync_test.go
Normal file
38
backend/cmd/adminctl/worksmobile_sync_test.go
Normal file
@@ -0,0 +1,38 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"baron-sso-backend/internal/service"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestClassifyWorksmobileAlignFromWorksAllowsDomainOnlyEmailMismatch(t *testing.T) {
|
||||
item := service.WorksmobileComparisonItem{
|
||||
BaronEmail: "user@typo.example.com",
|
||||
WorksmobileEmail: "user@example.com",
|
||||
}
|
||||
|
||||
status, ok := classifyWorksmobileAlignFromWorks(item)
|
||||
|
||||
if !ok {
|
||||
t.Fatalf("expected domain-only email mismatch to be alignable, status=%s", status)
|
||||
}
|
||||
if status != "updated" {
|
||||
t.Fatalf("expected updated status, got %s", status)
|
||||
}
|
||||
}
|
||||
|
||||
func TestClassifyWorksmobileAlignFromWorksSkipsLocalPartChange(t *testing.T) {
|
||||
item := service.WorksmobileComparisonItem{
|
||||
BaronEmail: "old@example.com",
|
||||
WorksmobileEmail: "new@example.com",
|
||||
}
|
||||
|
||||
status, ok := classifyWorksmobileAlignFromWorks(item)
|
||||
|
||||
if ok {
|
||||
t.Fatalf("expected local-part change to be skipped")
|
||||
}
|
||||
if status != "skipped_email_local_part_changed" {
|
||||
t.Fatalf("expected skipped_email_local_part_changed status, got %s", status)
|
||||
}
|
||||
}
|
||||
@@ -4,11 +4,21 @@ import (
|
||||
"baron-sso-backend/internal/domain"
|
||||
"baron-sso-backend/internal/service"
|
||||
"context"
|
||||
"flag"
|
||||
"fmt"
|
||||
"log"
|
||||
)
|
||||
|
||||
func main() {
|
||||
dryRun := flag.Bool("dry-run", true, "변경 대상만 출력하고 Kratos identity를 수정하지 않습니다")
|
||||
maintenanceWindow := flag.Bool("maintenance-window", false, "승인된 정비 시간에만 실제 변경을 허용합니다")
|
||||
markMirrorStale := flag.Bool("mark-mirror-stale", false, "실행 전 Redis identity mirror를 stale로 표시했음을 확인합니다")
|
||||
flag.Parse()
|
||||
|
||||
if !*dryRun && (!*maintenanceWindow || !*markMirrorStale) {
|
||||
log.Fatal("refusing to update Kratos identities: pass --dry-run=false --maintenance-window --mark-mirror-stale after marking identity mirror stale")
|
||||
}
|
||||
|
||||
kratosAdmin := service.NewKratosAdminService()
|
||||
ctx := context.Background()
|
||||
|
||||
@@ -37,6 +47,11 @@ func main() {
|
||||
}
|
||||
|
||||
if changed {
|
||||
if *dryRun {
|
||||
count++
|
||||
fmt.Printf("Would update %s\n", id.ID)
|
||||
continue
|
||||
}
|
||||
_, err := kratosAdmin.UpdateIdentity(ctx, id.ID, traits, id.State)
|
||||
if err != nil {
|
||||
log.Printf("Failed to update %s: %v", id.ID, err)
|
||||
@@ -46,5 +61,10 @@ func main() {
|
||||
}
|
||||
}
|
||||
}
|
||||
fmt.Printf("Total updated: %d\n", count)
|
||||
if *dryRun {
|
||||
fmt.Printf("Total candidates: %d\n", count)
|
||||
} else {
|
||||
fmt.Printf("Total updated: %d\n", count)
|
||||
fmt.Println("Identity mirror was marked stale before maintenance; run full mirror refresh and drift report before trusting cached user lists.")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -336,7 +336,12 @@ func main() {
|
||||
)
|
||||
configureWorksmobileClientFromEnv(worksmobileClient)
|
||||
worksmobileService := service.NewWorksmobileSyncService(tenantService, userRepo, worksmobileOutboxRepo, worksmobileClient)
|
||||
worksmobileRelayWorker := service.NewWorksmobileRelayWorker(worksmobileOutboxRepo, worksmobileClient)
|
||||
worksmobileRelayClient := *worksmobileClient
|
||||
worksmobileRelayClient.RateLimiter = service.NewWorksmobileAPIRateLimiter(240, time.Minute)
|
||||
worksmobileRelayWorker := service.NewWorksmobileRelayWorker(worksmobileOutboxRepo, &worksmobileRelayClient)
|
||||
if lock := service.NewWorksmobileRedisRelayLeaderLock(redisService); lock != nil {
|
||||
worksmobileRelayWorker.SetLeaderLock(lock)
|
||||
}
|
||||
go worksmobileRelayWorker.Start(context.Background())
|
||||
slog.Info("✅ Worksmobile Relay Worker started")
|
||||
rpUsageEmitter := service.NewRPUsageEventEmitter(rpUsageOutboxRepo)
|
||||
@@ -370,12 +375,13 @@ func main() {
|
||||
authHandler.RPUserMetadataRepo = rpUserMetadataRepo
|
||||
authHandler.RPUsageSink = rpUsageEmitter
|
||||
adminHandler := handler.NewAdminHandler(ketoService, ketoOutboxRepo)
|
||||
adminHandler.DB = db
|
||||
adminHandler.RPUsageQueries = rpUsageQueryRepo
|
||||
adminHandler.TenantRepo = tenantRepo
|
||||
adminHandler.Hydra = hydraService
|
||||
adminHandler.AuditRepo = auditRepo
|
||||
adminHandler.UserProjectionRepo = userProjectionRepo
|
||||
adminHandler.UserProjectionSyncer = userProjectionSyncer
|
||||
adminHandler.IdentityCache = redisService
|
||||
adminHandler.IntegrityChecker = repository.NewDataIntegrityChecker(db)
|
||||
devHandler := handler.NewDevHandler(redisService, secretRepo, consentRepo, relyingPartyService, ketoService, ketoOutboxRepo, tenantService, developerService, authHandler)
|
||||
devHandler.HeadlessJWKS = headlessJWKSCache
|
||||
@@ -383,6 +389,7 @@ func main() {
|
||||
devHandler.RPUserMetadataRepo = rpUserMetadataRepo
|
||||
devHandler.RPUsageQueries = rpUsageQueryRepo
|
||||
tenantHandler := handler.NewTenantHandler(db, tenantService, userRepo, userProjectionRepo, ketoService, ketoOutboxRepo, kratosAdminService, sharedLinkService, hydraService, consentRepo)
|
||||
tenantHandler.OrgChartCache = redisService
|
||||
userGroupHandler := handler.NewUserGroupHandler(userGroupService)
|
||||
relyingPartyHandler := handler.NewRelyingPartyHandler(relyingPartyService, kratosAdminService)
|
||||
userHandler := handler.NewUserHandler(kratosAdminService, oryAdminProvider, tenantService, ketoService, ketoOutboxRepo, userRepo, userGroupRepo, auditRepo)
|
||||
@@ -718,12 +725,15 @@ func main() {
|
||||
admin.Get("/integrity/orphan-user-login-ids", requireSuperAdmin, adminHandler.ListOrphanUserLoginIDs)
|
||||
admin.Delete("/integrity/orphan-user-login-ids", requireSuperAdmin, adminHandler.DeleteOrphanUserLoginIDs)
|
||||
admin.Get("/projections/users", requireSuperAdmin, adminHandler.GetUserProjectionStatus)
|
||||
admin.Post("/projections/users/reconcile", requireSuperAdmin, adminHandler.ReconcileUserProjection)
|
||||
admin.Post("/projections/users/reset", requireSuperAdmin, adminHandler.ResetUserProjection)
|
||||
admin.Get("/ory/ssot", requireSuperAdmin, adminHandler.GetOrySSOTSystemStatus)
|
||||
admin.Post("/ory/ssot/identity-cache/flush", requireSuperAdmin, adminHandler.FlushIdentityCache)
|
||||
admin.Get("/rp-usage/daily", requireAdmin, adminHandler.GetRPUsageDaily)
|
||||
admin.Get("/global-custom-claims", requireSuperAdmin, adminHandler.GetGlobalCustomClaimDefinitions)
|
||||
admin.Put("/global-custom-claims", requireSuperAdmin, adminHandler.UpdateGlobalCustomClaimDefinitions)
|
||||
|
||||
// Tenant Management (Mixed roles, handler filters results)
|
||||
admin.Get("/tenants", requireAnyUser, tenantHandler.ListTenants)
|
||||
admin.Get("/orgchart/snapshot", requireAnyUser, tenantHandler.GetOrgChartSnapshot)
|
||||
admin.Get("/tenants/export", requireSuperAdmin, tenantHandler.ExportTenantsCSV)
|
||||
admin.Post("/tenants/import", requireSuperAdmin, tenantHandler.ImportTenantsCSV)
|
||||
admin.Post("/tenants", requireSuperAdmin, tenantHandler.CreateTenant)
|
||||
|
||||
Reference in New Issue
Block a user