1
0
forked from baron/baron-sso

feat: improve Worksmobile tenant sync handling

This commit is contained in:
2026-06-02 18:05:36 +09:00
parent d6d39ca300
commit d32ca69eee
58 changed files with 4035 additions and 1400 deletions

View File

@@ -37,6 +37,7 @@ type InitialTenantConfig struct {
ParentSlug string
Description string
Domains []string
Config domain.JSONMap
}
func SeedTenants(db *gorm.DB) error {
@@ -149,6 +150,9 @@ func seedTenantConfigs(db *gorm.DB, configs []InitialTenantConfig) error {
return err
}
tenant.Status = domain.TenantStatusActive
if len(config.Config) > 0 {
tenant.Config = config.Config
}
if err := db.Save(tenant).Error; err != nil {
return err
}
@@ -265,6 +269,11 @@ func parseSeedTenantCSV(r io.Reader) ([]InitialTenantConfig, error) {
return nil, fmt.Errorf("row %d: slug is required", i+2)
}
config, err := seedTenantCSVRecordConfig(row, header)
if err != nil {
return nil, fmt.Errorf("row %d: %w", i+2, err)
}
configs = append(configs, InitialTenantConfig{
TenantID: seedTenantCSVValue(row, header, "tenant_id"),
Name: name,
@@ -273,6 +282,7 @@ func parseSeedTenantCSV(r io.Reader) ([]InitialTenantConfig, error) {
Slug: slug,
Description: seedTenantCSVValue(row, header, "memo"),
Domains: splitSeedTenantCSVDomains(seedTenantCSVValue(row, header, "email_domain")),
Config: config,
})
}
@@ -298,6 +308,18 @@ func seedTenantCSVHeaderIndex(header []string) map[string]int {
"email_domain": "email_domain",
"domain": "email_domain",
"domains": "email_domain",
"visibility": "visibility",
"public_setting": "visibility",
"publicsetting": "visibility",
"org_unit_type": "org_unit_type",
"orgunittype": "org_unit_type",
"organization_type": "org_unit_type",
"organizationtype": "org_unit_type",
"worksmobile": "worksmobile_sync",
"worksmobilesync": "worksmobile_sync",
"worksmobile_sync": "worksmobile_sync",
"works_sync": "worksmobile_sync",
"works": "worksmobile_sync",
}
for i, column := range header {
key := strings.ToLower(strings.TrimSpace(column))
@@ -317,6 +339,67 @@ func seedTenantCSVValue(row []string, header map[string]int, key string) string
return strings.TrimSpace(row[idx])
}
func seedTenantCSVRecordConfig(row []string, header map[string]int) (domain.JSONMap, error) {
config := domain.JSONMap{}
visibility := strings.TrimSpace(seedTenantCSVValue(row, header, "visibility"))
if visibility != "" {
normalizedVisibility, err := normalizeSeedTenantVisibility(visibility)
if err != nil {
return nil, err
}
config["visibility"] = normalizedVisibility
}
orgUnitType := strings.TrimSpace(seedTenantCSVValue(row, header, "org_unit_type"))
if orgUnitType != "" {
if !isAllowedSeedTenantOrgUnitType(orgUnitType) {
return nil, errors.New("orgUnitType must be one of 실, 팀, TF, TF팀, 센터, 디비전, 셀, 본부, 지역본부, 부, 임원직속")
}
config["orgUnitType"] = orgUnitType
}
if worksmobileSync := strings.TrimSpace(seedTenantCSVValue(row, header, "worksmobile_sync")); worksmobileSync != "" {
excluded, err := normalizeSeedTenantWorksmobileExcluded(worksmobileSync)
if err != nil {
return nil, err
}
config["worksmobileExcluded"] = excluded
}
if len(config) == 0 {
return nil, nil
}
return config, nil
}
func normalizeSeedTenantWorksmobileExcluded(value string) (bool, error) {
switch strings.ToLower(strings.TrimSpace(value)) {
case "", "yes", "y", "true", "1", "on", "sync", "linked", "연동":
return false, nil
case "no", "n", "false", "0", "off", "none", "excluded", "exclude", "not_sync", "not-synced", "미연동", "연동안함", "제외":
return true, nil
default:
return false, errors.New("worksmobile_sync must be yes or no")
}
}
func normalizeSeedTenantVisibility(value string) (string, error) {
visibility := strings.ToLower(strings.TrimSpace(value))
if visibility == "" || visibility == "public" {
return "public", nil
}
if visibility != "internal" && visibility != "private" {
return "", errors.New("visibility must be public, internal, or private")
}
return visibility, nil
}
func isAllowedSeedTenantOrgUnitType(value string) bool {
switch strings.TrimSpace(value) {
case "실", "팀", "TF", "TF팀", "센터", "디비전", "셀", "본부", "지역본부", "부", "임원직속":
return true
default:
return false
}
}
func seedTenantCSVRowIsEmpty(row []string) bool {
for _, value := range row {
if strings.TrimSpace(value) != "" {
@@ -405,6 +488,7 @@ func createSeedTenant(
Description: config.Description,
Status: domain.TenantStatusActive,
ParentID: parentID,
Config: config.Config,
}
if err := repo.Create(ctx, tenant); err != nil {

View File

@@ -61,6 +61,7 @@ func TestSeedTenantCSVDefinesWorksmobileDomainClassTenants(t *testing.T) {
slug: "baron-group",
tenantType: domain.TenantTypeCompanyGroup,
parentSlug: "hanmac-family",
domains: []string{"brsw.kr"},
},
{
name: "(주)장헌",
@@ -78,10 +79,10 @@ func TestSeedTenantCSVDefinesWorksmobileDomainClassTenants(t *testing.T) {
},
{
name: "한라산업개발",
slug: "hanlla",
slug: "halla",
tenantType: domain.TenantTypeCompany,
parentSlug: "baron-group",
domains: []string{"hanllasanup.co.kr"},
parentSlug: "hanmac-family",
domains: []string{"hallasanup.com"},
},
{
name: "(주)피티씨",
@@ -97,30 +98,64 @@ func TestSeedTenantCSVDefinesWorksmobileDomainClassTenants(t *testing.T) {
},
}
if len(configs) != len(expected) {
t.Fatalf("expected %d seed tenants, got %d", len(expected), len(configs))
if len(configs) < len(expected) {
t.Fatalf("expected at least %d seed tenants, got %d", len(expected), len(configs))
}
for i, want := range expected {
got := configs[i]
wantFamilyChildOrder := []string{
"gpdtdc",
"saman",
"hanmac",
"baron-group",
"halla",
}
policyFamilyChildSlugs := map[string]bool{}
for _, slug := range wantFamilyChildOrder {
policyFamilyChildSlugs[slug] = true
}
gotFamilyChildOrder := make([]string, 0, len(wantFamilyChildOrder))
for _, config := range configs {
if config.ParentSlug == "hanmac-family" && policyFamilyChildSlugs[config.Slug] {
gotFamilyChildOrder = append(gotFamilyChildOrder, config.Slug)
}
}
if len(gotFamilyChildOrder) != len(wantFamilyChildOrder) {
t.Fatalf("hanmac-family child order = %#v, want %#v", gotFamilyChildOrder, wantFamilyChildOrder)
}
for i, wantSlug := range wantFamilyChildOrder {
if gotFamilyChildOrder[i] != wantSlug {
t.Fatalf("hanmac-family child order[%d] = %q, want %q", i, gotFamilyChildOrder[i], wantSlug)
}
}
configBySlug := make(map[string]InitialTenantConfig, len(configs))
for _, config := range configs {
configBySlug[config.Slug] = config
}
for _, want := range expected {
got, ok := configBySlug[want.slug]
if !ok {
t.Fatalf("tenant slug %q not found in seed configs", want.slug)
}
if got.Name != want.name {
t.Fatalf("tenant[%d] name = %q, want %q", i, got.Name, want.name)
t.Fatalf("tenant[%s] name = %q, want %q", want.slug, got.Name, want.name)
}
if got.Slug != want.slug {
t.Fatalf("tenant[%d] slug = %q, want %q", i, got.Slug, want.slug)
t.Fatalf("tenant[%s] slug = %q, want %q", want.slug, got.Slug, want.slug)
}
if got.Type != want.tenantType {
t.Fatalf("tenant[%d] type = %q, want %q", i, got.Type, want.tenantType)
t.Fatalf("tenant[%s] type = %q, want %q", want.slug, got.Type, want.tenantType)
}
if got.ParentSlug != want.parentSlug {
t.Fatalf("tenant[%d] parent slug = %q, want %q", i, got.ParentSlug, want.parentSlug)
t.Fatalf("tenant[%s] parent slug = %q, want %q", want.slug, got.ParentSlug, want.parentSlug)
}
if len(got.Domains) != len(want.domains) {
t.Fatalf("tenant[%d] domains = %#v, want %#v", i, got.Domains, want.domains)
t.Fatalf("tenant[%s] domains = %#v, want %#v", want.slug, got.Domains, want.domains)
}
for j, wantDomain := range want.domains {
if got.Domains[j] != wantDomain {
t.Fatalf("tenant[%d] domain[%d] = %q, want %q", i, j, got.Domains[j], wantDomain)
t.Fatalf("tenant[%s] domain[%d] = %q, want %q", want.slug, j, got.Domains[j], wantDomain)
}
}
}
@@ -135,9 +170,9 @@ func TestNormalizeSeedTenantTypeAllowsOrganization(t *testing.T) {
func TestLoadSeedTenantConfigsUsesConfiguredCSVPath(t *testing.T) {
dir := t.TempDir()
path := filepath.Join(dir, "seed-tenant.csv")
csv := "name,type,parent_tenant_slug,slug,memo,email_domain\n" +
"Root,COMPANY_GROUP,,root,Root memo,\n" +
"Child,COMPANY,root,child,Child memo,child.example.com\n"
csv := "name,type,parent_tenant_slug,slug,memo,email_domain,visibility,org_unit_type,worksmobile_sync\n" +
"Root,COMPANY_GROUP,,root,Root memo,,,,\n" +
"Child,USER_GROUP,root,child,Child memo,child.example.com,private,팀,no\n"
if err := os.WriteFile(path, []byte(csv), 0o600); err != nil {
t.Fatalf("failed to write seed csv: %v", err)
}
@@ -156,6 +191,41 @@ func TestLoadSeedTenantConfigsUsesConfiguredCSVPath(t *testing.T) {
if len(configs[1].Domains) != 1 || configs[1].Domains[0] != "child.example.com" {
t.Fatalf("child domains = %#v, want child.example.com", configs[1].Domains)
}
if configs[1].Config["visibility"] != "private" {
t.Fatalf("child visibility = %#v, want private", configs[1].Config["visibility"])
}
if configs[1].Config["orgUnitType"] != "팀" {
t.Fatalf("child orgUnitType = %#v, want 팀", configs[1].Config["orgUnitType"])
}
if configs[1].Config["worksmobileExcluded"] != true {
t.Fatalf("child worksmobileExcluded = %#v, want true", configs[1].Config["worksmobileExcluded"])
}
}
func TestSeedTenantCSVDefinesMHDAsPrivateUserGroup(t *testing.T) {
configs, err := loadSeedTenantConfigs()
if err != nil {
t.Fatalf("loadSeedTenantConfigs returned error: %v", err)
}
configBySlug := make(map[string]InitialTenantConfig, len(configs))
for _, config := range configs {
configBySlug[config.Slug] = config
}
mhd, ok := configBySlug["mhd"]
if !ok {
t.Fatal("mhd seed tenant not found")
}
if mhd.Type != domain.TenantTypeUserGroup {
t.Fatalf("mhd type = %q, want %q", mhd.Type, domain.TenantTypeUserGroup)
}
if mhd.Config["visibility"] != "private" {
t.Fatalf("mhd visibility = %#v, want private", mhd.Config["visibility"])
}
if mhd.Config["worksmobileExcluded"] != true {
t.Fatalf("mhd worksmobileExcluded = %#v, want true", mhd.Config["worksmobileExcluded"])
}
}
func TestIsSeedTenantSlugUsesConfiguredCSVPath(t *testing.T) {