forked from baron/baron-sso
Merge branch 'dev' into feature/rbac-simplification-and-remove-dev-switcher
This commit is contained in:
@@ -117,16 +117,18 @@ type tenantDomainConflict struct {
|
||||
}
|
||||
|
||||
type tenantCSVRecord struct {
|
||||
TenantID string
|
||||
Name string
|
||||
Type string
|
||||
ParentTenantID *string
|
||||
ParentTenantSlug string
|
||||
Slug string
|
||||
Memo string
|
||||
Domains []string
|
||||
Visibility string
|
||||
OrgUnitType string
|
||||
TenantID string
|
||||
Name string
|
||||
Type string
|
||||
ParentTenantID *string
|
||||
ParentTenantSlug string
|
||||
Slug string
|
||||
Memo string
|
||||
Domains []string
|
||||
Visibility string
|
||||
OrgUnitType string
|
||||
WorksmobileSync string
|
||||
WorksmobileSyncSet bool
|
||||
}
|
||||
|
||||
type orgContextTenant struct {
|
||||
@@ -420,10 +422,10 @@ func (h *TenantHandler) ExportTenantsCSV(c *fiber.Ctx) error {
|
||||
writer := csv.NewWriter(&buf)
|
||||
includeIDs := includeCSVIds(c)
|
||||
if includeIDs {
|
||||
if err := writer.Write([]string{"tenant_id", "name", "type", "parent_tenant_id", "parent_tenant_slug", "slug", "memo", "email_domain", "visibility", "org_unit_type"}); err != nil {
|
||||
if err := writer.Write([]string{"tenant_id", "name", "type", "parent_tenant_id", "parent_tenant_slug", "slug", "memo", "email_domain", "visibility", "org_unit_type", "worksmobile_sync"}); err != nil {
|
||||
return errorJSON(c, fiber.StatusInternalServerError, err.Error())
|
||||
}
|
||||
} else if err := writer.Write([]string{"name", "type", "parent_tenant_slug", "slug", "memo", "email_domain", "visibility", "org_unit_type"}); err != nil {
|
||||
} else if err := writer.Write([]string{"name", "type", "parent_tenant_slug", "slug", "memo", "email_domain", "visibility", "org_unit_type", "worksmobile_sync"}); err != nil {
|
||||
return errorJSON(c, fiber.StatusInternalServerError, err.Error())
|
||||
}
|
||||
slugByID := make(map[string]string, len(allTenants))
|
||||
@@ -444,7 +446,7 @@ func (h *TenantHandler) ExportTenantsCSV(c *fiber.Ctx) error {
|
||||
domains = append(domains, domainName)
|
||||
}
|
||||
}
|
||||
visibility, orgUnitType := tenantCSVOrgConfigValues(tenant.Config)
|
||||
visibility, orgUnitType, worksmobileSync := tenantCSVOrgConfigValues(tenant.Config)
|
||||
row := []string{
|
||||
tenant.Name,
|
||||
tenant.Type,
|
||||
@@ -454,6 +456,7 @@ func (h *TenantHandler) ExportTenantsCSV(c *fiber.Ctx) error {
|
||||
strings.Join(domains, ";"),
|
||||
visibility,
|
||||
orgUnitType,
|
||||
worksmobileSync,
|
||||
}
|
||||
if includeIDs {
|
||||
row = []string{
|
||||
@@ -467,6 +470,7 @@ func (h *TenantHandler) ExportTenantsCSV(c *fiber.Ctx) error {
|
||||
strings.Join(domains, ";"),
|
||||
visibility,
|
||||
orgUnitType,
|
||||
worksmobileSync,
|
||||
}
|
||||
}
|
||||
if err := writer.Write(row); err != nil {
|
||||
@@ -683,17 +687,20 @@ func parseTenantCSVRecords(r io.Reader) ([]tenantCSVRecord, error) {
|
||||
parentID = &parentValue
|
||||
}
|
||||
|
||||
worksmobileSync, worksmobileSyncSet := tenantCSVWorksmobileSyncValue(row, header)
|
||||
records = append(records, tenantCSVRecord{
|
||||
TenantID: tenantCSVValue(row, header, "tenant_id"),
|
||||
Name: name,
|
||||
Type: tenantType,
|
||||
ParentTenantID: parentID,
|
||||
ParentTenantSlug: tenantCSVValue(row, header, "parent_tenant_slug"),
|
||||
Slug: slug,
|
||||
Memo: tenantCSVValue(row, header, "memo"),
|
||||
Domains: splitTenantCSVDomains(tenantCSVValue(row, header, "email_domain")),
|
||||
Visibility: tenantCSVValue(row, header, "visibility"),
|
||||
OrgUnitType: tenantCSVValue(row, header, "org_unit_type"),
|
||||
TenantID: tenantCSVValue(row, header, "tenant_id"),
|
||||
Name: name,
|
||||
Type: tenantType,
|
||||
ParentTenantID: parentID,
|
||||
ParentTenantSlug: tenantCSVValue(row, header, "parent_tenant_slug"),
|
||||
Slug: slug,
|
||||
Memo: tenantCSVValue(row, header, "memo"),
|
||||
Domains: splitTenantCSVDomains(tenantCSVValue(row, header, "email_domain")),
|
||||
Visibility: tenantCSVValue(row, header, "visibility"),
|
||||
OrgUnitType: tenantCSVValue(row, header, "org_unit_type"),
|
||||
WorksmobileSync: worksmobileSync,
|
||||
WorksmobileSyncSet: worksmobileSyncSet,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -703,35 +710,42 @@ func parseTenantCSVRecords(r io.Reader) ([]tenantCSVRecord, error) {
|
||||
func tenantCSVHeaderIndex(header []string) map[string]int {
|
||||
index := make(map[string]int, len(header))
|
||||
aliases := map[string]string{
|
||||
"id": "tenant_id",
|
||||
"tenantid": "tenant_id",
|
||||
"tenant_id": "tenant_id",
|
||||
"name": "name",
|
||||
"type": "type",
|
||||
"parentid": "parent_tenant_id",
|
||||
"parent_id": "parent_tenant_id",
|
||||
"parenttenantid": "parent_tenant_id",
|
||||
"parent_tenant_id": "parent_tenant_id",
|
||||
"parenttenantslug": "parent_tenant_slug",
|
||||
"parent_tenant_slug": "parent_tenant_slug",
|
||||
"slug": "slug",
|
||||
"memo": "memo",
|
||||
"description": "memo",
|
||||
"email-domain": "email_domain",
|
||||
"emaildomain": "email_domain",
|
||||
"email_domain": "email_domain",
|
||||
"domain": "email_domain",
|
||||
"domains": "email_domain",
|
||||
"visibility": "visibility",
|
||||
"public_setting": "visibility",
|
||||
"publicsetting": "visibility",
|
||||
"orgunittype": "org_unit_type",
|
||||
"org_unit_type": "org_unit_type",
|
||||
"org-unit-type": "org_unit_type",
|
||||
"organizationtype": "org_unit_type",
|
||||
"organization_type": "org_unit_type",
|
||||
"orgtype": "org_unit_type",
|
||||
"org_type": "org_unit_type",
|
||||
"id": "tenant_id",
|
||||
"tenantid": "tenant_id",
|
||||
"tenant_id": "tenant_id",
|
||||
"name": "name",
|
||||
"type": "type",
|
||||
"parentid": "parent_tenant_id",
|
||||
"parent_id": "parent_tenant_id",
|
||||
"parenttenantid": "parent_tenant_id",
|
||||
"parent_tenant_id": "parent_tenant_id",
|
||||
"parenttenantslug": "parent_tenant_slug",
|
||||
"parent_tenant_slug": "parent_tenant_slug",
|
||||
"slug": "slug",
|
||||
"memo": "memo",
|
||||
"description": "memo",
|
||||
"email-domain": "email_domain",
|
||||
"emaildomain": "email_domain",
|
||||
"email_domain": "email_domain",
|
||||
"domain": "email_domain",
|
||||
"domains": "email_domain",
|
||||
"visibility": "visibility",
|
||||
"public_setting": "visibility",
|
||||
"publicsetting": "visibility",
|
||||
"orgunittype": "org_unit_type",
|
||||
"org_unit_type": "org_unit_type",
|
||||
"org-unit-type": "org_unit_type",
|
||||
"organizationtype": "org_unit_type",
|
||||
"organization_type": "org_unit_type",
|
||||
"orgtype": "org_unit_type",
|
||||
"org_type": "org_unit_type",
|
||||
"worksmobile": "worksmobile_sync",
|
||||
"worksmobilesync": "worksmobile_sync",
|
||||
"worksmobile_sync": "worksmobile_sync",
|
||||
"works_sync": "worksmobile_sync",
|
||||
"works": "worksmobile_sync",
|
||||
"worksmobileexcluded": "worksmobile_excluded",
|
||||
"worksmobile_excluded": "worksmobile_excluded",
|
||||
}
|
||||
for i, column := range header {
|
||||
key := strings.ToLower(strings.TrimSpace(column))
|
||||
@@ -751,6 +765,28 @@ func tenantCSVValue(row []string, header map[string]int, key string) string {
|
||||
return strings.TrimSpace(row[idx])
|
||||
}
|
||||
|
||||
func tenantCSVWorksmobileSyncValue(row []string, header map[string]int) (string, bool) {
|
||||
if _, ok := header["worksmobile_sync"]; ok {
|
||||
value := tenantCSVValue(row, header, "worksmobile_sync")
|
||||
if value == "" {
|
||||
return "yes", true
|
||||
}
|
||||
return value, true
|
||||
}
|
||||
if _, ok := header["worksmobile_excluded"]; ok {
|
||||
value := tenantCSVValue(row, header, "worksmobile_excluded")
|
||||
excluded, err := normalizeTenantWorksmobileExcluded(value)
|
||||
if err == nil && excluded {
|
||||
return "no", true
|
||||
}
|
||||
if err == nil {
|
||||
return "yes", true
|
||||
}
|
||||
return value, true
|
||||
}
|
||||
return "", false
|
||||
}
|
||||
|
||||
func tenantCSVRowIsEmpty(row []string) bool {
|
||||
for _, value := range row {
|
||||
if strings.TrimSpace(value) != "" {
|
||||
@@ -872,11 +908,38 @@ func normalizeTenantConfig(config map[string]any) (domain.JSONMap, error) {
|
||||
normalized[key] = orgUnitType
|
||||
continue
|
||||
}
|
||||
if key == "worksmobileExcluded" {
|
||||
excluded, err := normalizeTenantWorksmobileExcluded(value)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
normalized[key] = excluded
|
||||
continue
|
||||
}
|
||||
normalized[key] = value
|
||||
}
|
||||
return normalized, nil
|
||||
}
|
||||
|
||||
func normalizeTenantWorksmobileExcluded(value any) (bool, error) {
|
||||
switch typed := value.(type) {
|
||||
case bool:
|
||||
return typed, nil
|
||||
case string:
|
||||
normalized := strings.ToLower(strings.TrimSpace(typed))
|
||||
switch normalized {
|
||||
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, fmt.Errorf("worksmobile_sync must be yes or no")
|
||||
}
|
||||
default:
|
||||
return false, fmt.Errorf("worksmobile_sync must be yes or no")
|
||||
}
|
||||
}
|
||||
|
||||
func isAllowedOrgUnitType(value string) bool {
|
||||
switch value {
|
||||
case "실", "팀", "TF", "TF팀", "센터", "디비전", "셀", "본부", "지역본부", "부", "임원직속":
|
||||
@@ -948,10 +1011,14 @@ func tenantVisibility(config domain.JSONMap) string {
|
||||
}
|
||||
}
|
||||
|
||||
func tenantCSVOrgConfigValues(config domain.JSONMap) (string, string) {
|
||||
func tenantCSVOrgConfigValues(config domain.JSONMap) (string, string, string) {
|
||||
visibility := tenantVisibility(config)
|
||||
orgUnitType, _ := config["orgUnitType"].(string)
|
||||
return visibility, strings.TrimSpace(orgUnitType)
|
||||
worksmobileSync := "yes"
|
||||
if excluded, err := normalizeTenantWorksmobileExcluded(config["worksmobileExcluded"]); err == nil && excluded {
|
||||
worksmobileSync = "no"
|
||||
}
|
||||
return visibility, strings.TrimSpace(orgUnitType), worksmobileSync
|
||||
}
|
||||
|
||||
func tenantCSVRecordConfig(record tenantCSVRecord) (domain.JSONMap, error) {
|
||||
@@ -962,6 +1029,9 @@ func tenantCSVRecordConfig(record tenantCSVRecord) (domain.JSONMap, error) {
|
||||
if strings.TrimSpace(record.OrgUnitType) != "" {
|
||||
config["orgUnitType"] = record.OrgUnitType
|
||||
}
|
||||
if record.WorksmobileSyncSet {
|
||||
config["worksmobileExcluded"] = record.WorksmobileSync
|
||||
}
|
||||
if len(config) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
@@ -2319,7 +2389,7 @@ func mapOrgContextTenant(tenant domain.Tenant) orgContextTenant {
|
||||
for _, domain := range tenant.Domains {
|
||||
domains = append(domains, domain.Domain)
|
||||
}
|
||||
visibility, orgUnitType := tenantCSVOrgConfigValues(tenant.Config)
|
||||
visibility, orgUnitType, _ := tenantCSVOrgConfigValues(tenant.Config)
|
||||
return orgContextTenant{
|
||||
ID: tenant.ID,
|
||||
Type: tenant.Type,
|
||||
|
||||
@@ -991,8 +991,8 @@ func TestTenantHandler_ExportTenantsCSV(t *testing.T) {
|
||||
assert.Equal(t, http.StatusOK, resp.StatusCode)
|
||||
assert.Contains(t, resp.Header.Get("Content-Disposition"), "tenants.csv")
|
||||
assert.Equal(t, "text/csv", strings.Split(resp.Header.Get("Content-Type"), ";")[0])
|
||||
assert.Contains(t, string(body), "tenant_id,name,type,parent_tenant_id,parent_tenant_slug,slug,memo,email_domain,visibility,org_unit_type")
|
||||
assert.Contains(t, string(body), "t1,Tenant A,COMPANY,parent-1,,tenant-a,Primary tenant,tenant-a.example.com;login.tenant-a.example.com,internal,센터")
|
||||
assert.Contains(t, string(body), "tenant_id,name,type,parent_tenant_id,parent_tenant_slug,slug,memo,email_domain,visibility,org_unit_type,worksmobile_sync")
|
||||
assert.Contains(t, string(body), "t1,Tenant A,COMPANY,parent-1,,tenant-a,Primary tenant,tenant-a.example.com;login.tenant-a.example.com,internal,센터,yes")
|
||||
}
|
||||
|
||||
func TestTenantHandler_ExportTenantsCSV_OmitsIDsAndUsesParentSlug(t *testing.T) {
|
||||
@@ -1027,7 +1027,7 @@ func TestTenantHandler_ExportTenantsCSV_OmitsIDsAndUsesParentSlug(t *testing.T)
|
||||
text := string(body)
|
||||
|
||||
assert.Equal(t, http.StatusOK, resp.StatusCode)
|
||||
assert.Contains(t, text, "name,type,parent_tenant_slug,slug,memo,email_domain,visibility,org_unit_type")
|
||||
assert.Contains(t, text, "name,type,parent_tenant_slug,slug,memo,email_domain,visibility,org_unit_type,worksmobile_sync")
|
||||
assert.Contains(t, text, "Child Tenant,USER_GROUP,parent-tenant,child-tenant,,")
|
||||
assert.NotContains(t, text, "tenant_id")
|
||||
assert.NotContains(t, text, "parent_tenant_id")
|
||||
@@ -1114,7 +1114,7 @@ func TestTenantHandler_ExportTenantsCSV_FiltersDescendantsByParentIDWithIDs(t *t
|
||||
text := string(body)
|
||||
|
||||
assert.Equal(t, http.StatusOK, resp.StatusCode)
|
||||
assert.Contains(t, text, "tenant_id,name,type,parent_tenant_id,parent_tenant_slug,slug,memo,email_domain,visibility,org_unit_type")
|
||||
assert.Contains(t, text, "tenant_id,name,type,parent_tenant_id,parent_tenant_slug,slug,memo,email_domain,visibility,org_unit_type,worksmobile_sync")
|
||||
assert.Contains(t, text, childID+",Child Org,ORGANIZATION,"+parentID+",parent-org,child-org,")
|
||||
assert.Contains(t, text, grandchildID+",Leaf Team,USER_GROUP,"+childID+",child-org,leaf-team,")
|
||||
assert.NotContains(t, text, unrelatedID)
|
||||
@@ -1309,8 +1309,8 @@ func TestNormalizeTenantTypeAllowsOrganization(t *testing.T) {
|
||||
|
||||
func TestTenantCSVAllowedDomainsRoundTrip(t *testing.T) {
|
||||
records, err := parseTenantCSVRecords(strings.NewReader(
|
||||
"name,type,parent_tenant_slug,slug,memo,email_domain,visibility,org_unit_type\n" +
|
||||
"Hanmac,COMPANY,,hanmac,,\"samaneng.com, hanmaceng.co.kr;login.hmac.kr\",internal,센터\n",
|
||||
"name,type,parent_tenant_slug,slug,memo,email_domain,visibility,org_unit_type,worksmobile_sync\n" +
|
||||
"Hanmac,COMPANY,,hanmac,,\"samaneng.com, hanmaceng.co.kr;login.hmac.kr\",internal,센터,no\n",
|
||||
))
|
||||
|
||||
assert.NoError(t, err)
|
||||
@@ -1318,6 +1318,10 @@ func TestTenantCSVAllowedDomainsRoundTrip(t *testing.T) {
|
||||
assert.Equal(t, []string{"samaneng.com", "hanmaceng.co.kr", "login.hmac.kr"}, records[0].Domains)
|
||||
assert.Equal(t, "internal", records[0].Visibility)
|
||||
assert.Equal(t, "센터", records[0].OrgUnitType)
|
||||
assert.Equal(t, "no", records[0].WorksmobileSync)
|
||||
config, err := tenantCSVRecordConfig(records[0])
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, true, config["worksmobileExcluded"])
|
||||
}
|
||||
|
||||
func TestNormalizeTenantDomainInputsSplitsCommaAndWhitespace(t *testing.T) {
|
||||
@@ -1378,13 +1382,15 @@ func TestNormalizeTenantConfigRejectsNonTextLoginIDFields(t *testing.T) {
|
||||
|
||||
func TestNormalizeTenantConfigAcceptsTenantVisibilityAndOrgUnitType(t *testing.T) {
|
||||
config, err := normalizeTenantConfig(map[string]any{
|
||||
"visibility": "internal",
|
||||
"orgUnitType": "센터",
|
||||
"visibility": "internal",
|
||||
"orgUnitType": "센터",
|
||||
"worksmobileExcluded": true,
|
||||
})
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "internal", config["visibility"])
|
||||
assert.Equal(t, "센터", config["orgUnitType"])
|
||||
assert.Equal(t, true, config["worksmobileExcluded"])
|
||||
}
|
||||
|
||||
func TestNormalizeTenantConfigAcceptsTaskForceAndExecutiveOrgUnitTypes(t *testing.T) {
|
||||
|
||||
@@ -2,6 +2,7 @@ package handler
|
||||
|
||||
import (
|
||||
"baron-sso-backend/internal/domain"
|
||||
"baron-sso-backend/internal/service"
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
@@ -53,6 +54,8 @@ func (m *MockUserGroupService) List(ctx context.Context, tenantID string) ([]dom
|
||||
return args.Get(0).([]domain.UserGroup), args.Error(1)
|
||||
}
|
||||
|
||||
func (m *MockUserGroupService) SetWorksmobileSyncer(syncer service.WorksmobileSyncer) {}
|
||||
|
||||
func (m *MockUserGroupService) AddMember(ctx context.Context, groupID, userID string) error {
|
||||
return m.Called(ctx, groupID, userID).Error(0)
|
||||
}
|
||||
|
||||
@@ -99,6 +99,70 @@ func sanitizeUserMetadata(metadata map[string]any) map[string]any {
|
||||
return sanitized
|
||||
}
|
||||
|
||||
func sanitizeUserRepresentativeTenants(ctx context.Context, tenantService service.TenantService, metadata map[string]any, appointments []map[string]any) (bool, error) {
|
||||
if tenantService == nil || metadata == nil {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
cleared := false
|
||||
clearMetadataPrimary := func() {
|
||||
delete(metadata, "primaryTenantId")
|
||||
delete(metadata, "primaryTenantSlug")
|
||||
delete(metadata, "primaryTenantName")
|
||||
delete(metadata, "primaryTenantIsOwner")
|
||||
cleared = true
|
||||
}
|
||||
if isNonPublicRepresentativeTenant(ctx, tenantService, normalizeMetadataString(metadata["primaryTenantId"]), normalizeMetadataString(metadata["primaryTenantSlug"])) {
|
||||
clearMetadataPrimary()
|
||||
}
|
||||
|
||||
clearAppointment := func(appointment map[string]any) {
|
||||
if isPrimary, ok := metadataBoolFromMap(appointment, "isPrimary", "primary", "representative", "isRepresentative"); !ok || !isPrimary {
|
||||
return
|
||||
}
|
||||
tenantID := normalizeMetadataString(appointment["tenantId"])
|
||||
tenantSlug := normalizeMetadataString(appointment["tenantSlug"])
|
||||
if tenantSlug == "" {
|
||||
tenantSlug = normalizeMetadataString(appointment["slug"])
|
||||
}
|
||||
if !isNonPublicRepresentativeTenant(ctx, tenantService, tenantID, tenantSlug) {
|
||||
return
|
||||
}
|
||||
appointment["isPrimary"] = false
|
||||
appointment["primary"] = false
|
||||
appointment["representative"] = false
|
||||
appointment["isRepresentative"] = false
|
||||
clearMetadataPrimary()
|
||||
}
|
||||
|
||||
for _, appointment := range appointments {
|
||||
clearAppointment(appointment)
|
||||
}
|
||||
if rawAppointments, ok := metadata["additionalAppointments"].([]any); ok {
|
||||
for _, rawAppointment := range rawAppointments {
|
||||
if appointment, ok := rawAppointment.(map[string]any); ok {
|
||||
clearAppointment(appointment)
|
||||
}
|
||||
}
|
||||
}
|
||||
return cleared, nil
|
||||
}
|
||||
|
||||
func isNonPublicRepresentativeTenant(ctx context.Context, tenantService service.TenantService, tenantID string, tenantSlug string) bool {
|
||||
var tenant *domain.Tenant
|
||||
var err error
|
||||
if strings.TrimSpace(tenantID) != "" {
|
||||
tenant, err = tenantService.GetTenant(ctx, strings.TrimSpace(tenantID))
|
||||
} else if strings.TrimSpace(tenantSlug) != "" {
|
||||
tenant, err = tenantService.GetTenantBySlug(ctx, strings.TrimSpace(tenantSlug))
|
||||
}
|
||||
if err != nil || tenant == nil {
|
||||
return false
|
||||
}
|
||||
visibility := tenantVisibility(tenant.Config)
|
||||
return visibility == "internal" || visibility == "private"
|
||||
}
|
||||
|
||||
func primaryTenantIDFromRequest(primaryTenantID string, metadata map[string]any, appointments []map[string]any) string {
|
||||
if value := strings.TrimSpace(primaryTenantID); value != "" {
|
||||
return value
|
||||
@@ -651,6 +715,20 @@ func (h *UserHandler) CreateUser(c *fiber.Ctx) error {
|
||||
}
|
||||
req.CompanyCode = tenantSlug
|
||||
req.Metadata = sanitizeUserMetadata(mergeUserAppointmentMetadata(req.Metadata, req.AdditionalAppointments, req.PrimaryTenantID, req.PrimaryTenantName, req.PrimaryTenantIsOwner))
|
||||
representativeCleared := false
|
||||
if h.TenantService != nil {
|
||||
cleared, err := sanitizeUserRepresentativeTenants(c.Context(), h.TenantService, req.Metadata, req.AdditionalAppointments)
|
||||
if err != nil {
|
||||
return errorJSON(c, fiber.StatusBadRequest, err.Error())
|
||||
}
|
||||
representativeCleared = cleared
|
||||
if cleared {
|
||||
req.PrimaryTenantID = ""
|
||||
req.PrimaryTenantName = ""
|
||||
req.PrimaryTenantIsOwner = nil
|
||||
req.CompanyCode = ""
|
||||
}
|
||||
}
|
||||
|
||||
email := strings.TrimSpace(req.Email)
|
||||
if email == "" {
|
||||
@@ -725,7 +803,11 @@ func (h *UserHandler) CreateUser(c *fiber.Ctx) error {
|
||||
|
||||
// [Resolve TenantID and Custom Login IDs before Kratos creation]
|
||||
var tenantID string
|
||||
requestedPrimaryTenantID := primaryTenantIDFromRequest(req.PrimaryTenantID, req.Metadata, req.AdditionalAppointments)
|
||||
primaryAppointments := req.AdditionalAppointments
|
||||
if representativeCleared {
|
||||
primaryAppointments = nil
|
||||
}
|
||||
requestedPrimaryTenantID := primaryTenantIDFromRequest(req.PrimaryTenantID, req.Metadata, primaryAppointments)
|
||||
if req.CompanyCode == "" && h.TenantService != nil {
|
||||
if requestedPrimaryTenantID != "" {
|
||||
if tenant, err := h.TenantService.GetTenant(c.Context(), requestedPrimaryTenantID); err == nil && tenant != nil {
|
||||
@@ -1971,6 +2053,18 @@ func (h *UserHandler) UpdateUser(c *fiber.Ctx) error {
|
||||
}
|
||||
req.CompanyCode = tenantSlug
|
||||
req.Metadata = sanitizeUserMetadata(mergeUserAppointmentMetadata(req.Metadata, req.AdditionalAppointments, req.PrimaryTenantID, req.PrimaryTenantName, req.PrimaryTenantIsOwner))
|
||||
if h.TenantService != nil {
|
||||
cleared, err := sanitizeUserRepresentativeTenants(c.Context(), h.TenantService, req.Metadata, req.AdditionalAppointments)
|
||||
if err != nil {
|
||||
return errorJSON(c, fiber.StatusBadRequest, err.Error())
|
||||
}
|
||||
if cleared {
|
||||
req.PrimaryTenantID = ""
|
||||
req.PrimaryTenantName = ""
|
||||
req.PrimaryTenantIsOwner = nil
|
||||
req.CompanyCode = nil
|
||||
}
|
||||
}
|
||||
if req.Role != nil {
|
||||
if requester == nil || domain.NormalizeRole(requester.Role) != domain.RoleSuperAdmin {
|
||||
return errorJSON(c, fiber.StatusForbidden, "forbidden: only super admin can change user role")
|
||||
|
||||
@@ -205,6 +205,49 @@ func TestSanitizeUserMetadataRemovesLegacyClassificationFlags(t *testing.T) {
|
||||
assert.Contains(t, metadata, "userType")
|
||||
}
|
||||
|
||||
func TestSanitizeUserRepresentativeTenantsClearsNonPublicPrimary(t *testing.T) {
|
||||
mockTenant := new(MockTenantServiceForUser)
|
||||
internalTenantID := "internal-tenant"
|
||||
publicTenantID := "public-tenant"
|
||||
metadata := map[string]any{
|
||||
"primaryTenantId": internalTenantID,
|
||||
"primaryTenantName": "비공개팀",
|
||||
"primaryTenantSlug": "private-team",
|
||||
"additionalAppointments": []any{
|
||||
map[string]any{"tenantId": internalTenantID, "tenantSlug": "private-team", "isPrimary": true},
|
||||
map[string]any{"tenantId": publicTenantID, "tenantSlug": "public-team", "isPrimary": false},
|
||||
},
|
||||
}
|
||||
appointments := []map[string]any{
|
||||
{"tenantId": internalTenantID, "tenantSlug": "private-team", "isPrimary": true},
|
||||
{"tenantId": publicTenantID, "tenantSlug": "public-team", "isPrimary": false},
|
||||
}
|
||||
|
||||
mockTenant.On("GetTenant", mock.Anything, internalTenantID).Return(&domain.Tenant{
|
||||
ID: internalTenantID,
|
||||
Slug: "private-team",
|
||||
Config: domain.JSONMap{"visibility": "private"},
|
||||
}, nil)
|
||||
mockTenant.On("GetTenant", mock.Anything, publicTenantID).Return(&domain.Tenant{
|
||||
ID: publicTenantID,
|
||||
Slug: "public-team",
|
||||
Config: domain.JSONMap{"visibility": "public"},
|
||||
}, nil).Maybe()
|
||||
|
||||
cleared, err := sanitizeUserRepresentativeTenants(context.Background(), mockTenant, metadata, appointments)
|
||||
|
||||
require.NoError(t, err)
|
||||
assert.True(t, cleared)
|
||||
assert.NotContains(t, metadata, "primaryTenantId")
|
||||
assert.NotContains(t, metadata, "primaryTenantName")
|
||||
assert.NotContains(t, metadata, "primaryTenantSlug")
|
||||
assert.Equal(t, false, appointments[0]["isPrimary"])
|
||||
metadataAppointments := metadata["additionalAppointments"].([]any)
|
||||
firstAppointment := metadataAppointments[0].(map[string]any)
|
||||
assert.Equal(t, false, firstAppointment["isPrimary"])
|
||||
mockTenant.AssertExpectations(t)
|
||||
}
|
||||
|
||||
type MockTenantServiceForUser struct {
|
||||
mock.Mock
|
||||
service.TenantService
|
||||
|
||||
@@ -105,6 +105,14 @@ func (h *WorksmobileHandler) RetryJob(c *fiber.Ctx) error {
|
||||
return c.JSON(job)
|
||||
}
|
||||
|
||||
func (h *WorksmobileHandler) DeletePendingJobs(c *fiber.Ctx) error {
|
||||
result, err := h.Service.DeletePendingJobs(c.Context(), strings.TrimSpace(c.Params("tenantId")))
|
||||
if err != nil {
|
||||
return worksmobileGuardError(c, err, "delete_pending_jobs")
|
||||
}
|
||||
return c.JSON(result)
|
||||
}
|
||||
|
||||
func (h *WorksmobileHandler) DownloadInitialPasswordsCSV(c *fiber.Ctx) error {
|
||||
credentials, err := h.Service.ListInitialPasswordCredentials(c.Context(), strings.TrimSpace(c.Params("tenantId")), strings.TrimSpace(c.Query("batchId")))
|
||||
if err != nil {
|
||||
|
||||
@@ -153,6 +153,24 @@ func TestWorksmobileHandlerDeletesCredentialBatchPasswords(t *testing.T) {
|
||||
require.Equal(t, "batch-1", fakeService.deletedCredentialBatchID)
|
||||
}
|
||||
|
||||
func TestWorksmobileHandlerDeletesPendingJobs(t *testing.T) {
|
||||
fakeService := &fakeWorksmobileAdminService{
|
||||
pendingJobsDeleteResult: service.WorksmobilePendingJobDeleteResult{DeletedCount: 3},
|
||||
}
|
||||
h := NewWorksmobileHandler(fakeService)
|
||||
app := fiber.New()
|
||||
app.Delete("/tenants/:tenantId/worksmobile/jobs/pending", h.DeletePendingJobs)
|
||||
|
||||
resp, err := app.Test(httptest.NewRequest("DELETE", "/tenants/hanmac-id/worksmobile/jobs/pending", nil))
|
||||
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, fiber.StatusOK, resp.StatusCode)
|
||||
require.Equal(t, "hanmac-id", fakeService.deletedPendingJobsTenantID)
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
require.NoError(t, err)
|
||||
require.Contains(t, string(body), `"deletedCount":3`)
|
||||
}
|
||||
|
||||
func TestWorksmobileHandlerLogsActionFailures(t *testing.T) {
|
||||
var logs bytes.Buffer
|
||||
previous := slog.Default()
|
||||
@@ -184,6 +202,8 @@ type fakeWorksmobileAdminService struct {
|
||||
resetPasswordCredentialBatchID string
|
||||
downloadCredentialBatchID string
|
||||
deletedCredentialBatchID string
|
||||
deletedPendingJobsTenantID string
|
||||
pendingJobsDeleteResult service.WorksmobilePendingJobDeleteResult
|
||||
credentialBatches []service.WorksmobileCredentialBatch
|
||||
}
|
||||
|
||||
@@ -237,3 +257,8 @@ func (f *fakeWorksmobileAdminService) DeleteCredentialBatchPasswords(ctx context
|
||||
f.deletedCredentialBatchID = credentialBatchID
|
||||
return service.WorksmobileCredentialBatch{BatchID: credentialBatchID}, nil
|
||||
}
|
||||
|
||||
func (f *fakeWorksmobileAdminService) DeletePendingJobs(ctx context.Context, tenantID string) (service.WorksmobilePendingJobDeleteResult, error) {
|
||||
f.deletedPendingJobsTenantID = tenantID
|
||||
return f.pendingJobsDeleteResult, nil
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user