1
0
forked from baron/baron-sso

feat: implement dynamic tenant provisioning and remove hardcoded company mappings

This commit is contained in:
2026-04-06 16:13:03 +09:00
parent 003f12f008
commit c78604df06
9 changed files with 125 additions and 67 deletions

View File

@@ -22,6 +22,7 @@ type TenantService interface {
ListManageableTenants(ctx context.Context, userID string) ([]domain.Tenant, error)
IsDomainAllowed(ctx context.Context, domainName string) (bool, error)
ApproveTenant(ctx context.Context, id string) error
ProvisionTenantByDomain(ctx context.Context, domainName string) (*domain.Tenant, error) // 추가
SetKetoService(keto KetoService) // 추가
}
@@ -311,3 +312,51 @@ func (s *tenantService) IsDomainAllowed(ctx context.Context, domainName string)
}
return tenant != nil && tenant.Status == domain.TenantStatusActive, nil
}
func (s *tenantService) ProvisionTenantByDomain(ctx context.Context, domainName string) (*domain.Tenant, error) {
// 1. Find all COMPANY_GROUP tenants
groups, err := s.repo.ListByType(ctx, domain.TenantTypeCompanyGroup)
if err != nil {
return nil, err
}
for _, g := range groups {
// 2. Check autoProvisioning config
rawConfig, ok := g.Config["autoProvisioning"].(map[string]interface{})
if !ok {
continue
}
enabled, _ := rawConfig["enabled"].(bool)
if !enabled {
continue
}
mapping, ok := rawConfig["mappingRules"].(map[string]interface{})
if !ok {
continue
}
// 3. Find rule for this domain
rule, ok := mapping[domainName].(map[string]interface{})
if !ok {
continue
}
slug, _ := rule["slug"].(string)
name, _ := rule["name"].(string)
if slug == "" || name == "" {
continue
}
// 4. Create new sub-tenant under this group
slog.Info("[Provisioning] Found rule for domain, creating sub-tenant", "domain", domainName, "parent", g.Slug, "newTenant", slug)
// Use RegisterTenant to handle DB creation and Keto Outbox sync
// creatorID is empty as per security policy (manual delegation later)
return s.RegisterTenant(ctx, name, slug, domain.TenantTypeCompany, "Automatically provisioned via group policy", []string{domainName}, &g.ID, "")
}
return nil, gorm.ErrRecordNotFound
}

View File

@@ -64,6 +64,14 @@ func (m *MockTenantRepoForSvc) List(ctx context.Context, limit, offset int, pare
return args.Get(0).([]domain.Tenant), int64(args.Int(1)), args.Error(2)
}
func (m *MockTenantRepoForSvc) ListByType(ctx context.Context, tenantType string) ([]domain.Tenant, error) {
args := m.Called(ctx, tenantType)
if args.Get(0) == nil {
return nil, args.Error(1)
}
return args.Get(0).([]domain.Tenant), args.Error(1)
}
type MockKetoSvcForTenant struct {
mock.Mock
}

View File

@@ -158,14 +158,18 @@ func (m *MockTenantRepository) FindByDomain(ctx context.Context, domainName stri
return nil, nil
}
func (m *MockTenantRepository) AddDomain(ctx context.Context, tenantID string, domainName string, verified bool) error {
return nil
}
func (m *MockTenantRepository) List(ctx context.Context, limit, offset int, parentID string) ([]domain.Tenant, int64, error) {
return nil, 0, nil
}
func (m *MockTenantRepository) ListByType(ctx context.Context, tenantType string) ([]domain.Tenant, error) {
return nil, nil
}
func (m *MockTenantRepository) AddDomain(ctx context.Context, tenantID string, domainName string, verified bool) error {
return nil
}
func TestUserGroupService_Create(t *testing.T) {
mockRepo := new(MockUserGroupRepository)
mockTenantRepo := new(MockTenantRepository)