forked from baron/baron-sso
조직도 기능 추가
This commit is contained in:
@@ -20,10 +20,12 @@ type TenantService interface {
|
||||
GetTenant(ctx context.Context, id string) (*domain.Tenant, error)
|
||||
ListTenants(ctx context.Context, limit, offset int, parentID string) ([]domain.Tenant, int64, error)
|
||||
ListManageableTenants(ctx context.Context, userID string) ([]domain.Tenant, error)
|
||||
ListJoinedTenants(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) // 추가
|
||||
ProvisionTenantByDomain(ctx context.Context, domainName string) (*domain.Tenant, error)
|
||||
SetKetoService(keto KetoService)
|
||||
DeleteTenantsBulk(ctx context.Context, ids []string) error
|
||||
}
|
||||
|
||||
type tenantService struct {
|
||||
@@ -56,8 +58,6 @@ func (s *tenantService) ListManageableTenants(ctx context.Context, userID string
|
||||
return nil, errors.New("keto service not initialized")
|
||||
}
|
||||
|
||||
// [Keto] 'Tenant' 네임스페이스에서 'manage' 권한을 가진 모든 테넌트 ID 조회
|
||||
// OPL(parents 상속 포함) 결과가 반영된 리스트를 가져옵니다.
|
||||
allIDs, err := s.keto.ListObjects(ctx, "Tenant", "manage", "User:"+userID)
|
||||
if err != nil {
|
||||
slog.Error("Failed to list manageable tenants from Keto", "userID", userID, "error", err)
|
||||
@@ -65,7 +65,6 @@ func (s *tenantService) ListManageableTenants(ctx context.Context, userID string
|
||||
}
|
||||
|
||||
if len(allIDs) == 0 {
|
||||
// Fallback: Check direct membership if list objects didn't catch everything
|
||||
directAdminIDs, _ := s.keto.ListObjects(ctx, "Tenant", "admins", "User:"+userID)
|
||||
directOwnerIDs, _ := s.keto.ListObjects(ctx, "Tenant", "owners", "User:"+userID)
|
||||
|
||||
@@ -90,13 +89,42 @@ func (s *tenantService) ListManageableTenants(ctx context.Context, userID string
|
||||
return s.repo.FindByIDs(ctx, allIDs)
|
||||
}
|
||||
|
||||
func (s *tenantService) ListJoinedTenants(ctx context.Context, userID string) ([]domain.Tenant, error) {
|
||||
if s.keto == nil {
|
||||
return nil, errors.New("keto service not initialized")
|
||||
}
|
||||
|
||||
memberIDs, err := s.keto.ListObjects(ctx, "Tenant", "members", "User:"+userID)
|
||||
if err != nil {
|
||||
slog.Error("Failed to list joined tenants from Keto", "userID", userID, "error", err)
|
||||
return []domain.Tenant{}, nil
|
||||
}
|
||||
|
||||
ownerIDs, _ := s.keto.ListObjects(ctx, "Tenant", "owners", "User:"+userID)
|
||||
adminIDs, _ := s.keto.ListObjects(ctx, "Tenant", "admins", "User:"+userID)
|
||||
|
||||
idMap := make(map[string]bool)
|
||||
for _, id := range memberIDs { idMap[id] = true }
|
||||
for _, id := range ownerIDs { idMap[id] = true }
|
||||
for _, id := range adminIDs { idMap[id] = true }
|
||||
|
||||
allIDs := make([]string, 0, len(idMap))
|
||||
for id := range idMap {
|
||||
allIDs = append(allIDs, id)
|
||||
}
|
||||
|
||||
if len(allIDs) == 0 {
|
||||
return []domain.Tenant{}, nil
|
||||
}
|
||||
|
||||
return s.repo.FindByIDs(ctx, allIDs)
|
||||
}
|
||||
|
||||
func (s *tenantService) RegisterTenant(ctx context.Context, name, slug, tenantType, description string, domains []string, parentID *string, creatorID string) (*domain.Tenant, error) {
|
||||
// Validate Slug
|
||||
if ok, msg := utils.ValidateSlug(slug); !ok {
|
||||
return nil, errors.New(msg)
|
||||
}
|
||||
|
||||
// 1. Check if slug exists
|
||||
existing, err := s.repo.FindBySlug(ctx, slug)
|
||||
if err == nil && existing != nil {
|
||||
return nil, errors.New("tenant slug already exists")
|
||||
@@ -105,7 +133,6 @@ func (s *tenantService) RegisterTenant(ctx context.Context, name, slug, tenantTy
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 2. Create Tenant
|
||||
tenant := &domain.Tenant{
|
||||
Type: tenantType,
|
||||
Name: name,
|
||||
@@ -119,9 +146,7 @@ func (s *tenantService) RegisterTenant(ctx context.Context, name, slug, tenantTy
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// [Keto] Sync hierarchy and ownership via Outbox
|
||||
if s.outboxRepo != nil {
|
||||
// Global Super Admin access to every tenant
|
||||
_ = s.outboxRepo.Create(ctx, &domain.KetoOutbox{
|
||||
Namespace: "Tenant",
|
||||
Object: tenant.ID,
|
||||
@@ -130,7 +155,6 @@ func (s *tenantService) RegisterTenant(ctx context.Context, name, slug, tenantTy
|
||||
Action: domain.KetoOutboxActionCreate,
|
||||
})
|
||||
|
||||
// Sync hierarchy
|
||||
if tenant.ParentID != nil {
|
||||
if err := s.outboxRepo.Create(ctx, &domain.KetoOutbox{
|
||||
Namespace: "Tenant",
|
||||
@@ -143,10 +167,8 @@ func (s *tenantService) RegisterTenant(ctx context.Context, name, slug, tenantTy
|
||||
}
|
||||
}
|
||||
|
||||
// Sync creator ownership
|
||||
if creatorID != "" {
|
||||
slog.Info("Creating outbox entries for tenant creator", "tenant", tenant.ID, "creator", creatorID)
|
||||
// Add as owner
|
||||
_ = s.outboxRepo.Create(ctx, &domain.KetoOutbox{
|
||||
Namespace: "Tenant",
|
||||
Object: tenant.ID,
|
||||
@@ -154,7 +176,6 @@ func (s *tenantService) RegisterTenant(ctx context.Context, name, slug, tenantTy
|
||||
Subject: "User:" + creatorID,
|
||||
Action: domain.KetoOutboxActionCreate,
|
||||
})
|
||||
// Add as admin
|
||||
_ = s.outboxRepo.Create(ctx, &domain.KetoOutbox{
|
||||
Namespace: "Tenant",
|
||||
Object: tenant.ID,
|
||||
@@ -162,7 +183,6 @@ func (s *tenantService) RegisterTenant(ctx context.Context, name, slug, tenantTy
|
||||
Subject: "User:" + creatorID,
|
||||
Action: domain.KetoOutboxActionCreate,
|
||||
})
|
||||
// Add as member
|
||||
_ = s.outboxRepo.Create(ctx, &domain.KetoOutbox{
|
||||
Namespace: "Tenant",
|
||||
Object: tenant.ID,
|
||||
@@ -173,7 +193,6 @@ func (s *tenantService) RegisterTenant(ctx context.Context, name, slug, tenantTy
|
||||
}
|
||||
}
|
||||
|
||||
// 3. Add Domains (Auto-verify for manual admin registration)
|
||||
for _, d := range domains {
|
||||
if err := s.repo.AddDomain(ctx, tenant.ID, d, true); err != nil {
|
||||
slog.Error("Failed to add domain to tenant", "tenant", slug, "domain", d, "error", err)
|
||||
@@ -184,12 +203,10 @@ func (s *tenantService) RegisterTenant(ctx context.Context, name, slug, tenantTy
|
||||
}
|
||||
|
||||
func (s *tenantService) RequestRegistration(ctx context.Context, name, slug, description string, domainName string, adminEmail string) (*domain.Tenant, error) {
|
||||
// Validate Slug
|
||||
if ok, msg := utils.ValidateSlug(slug); !ok {
|
||||
return nil, errors.New(msg)
|
||||
}
|
||||
|
||||
// Verify that adminEmail domain matches the requested domainName
|
||||
parts := strings.Split(adminEmail, "@")
|
||||
if len(parts) != 2 || parts[1] != domainName {
|
||||
return nil, errors.New("admin email domain must match the tenant domain")
|
||||
@@ -208,7 +225,6 @@ func (s *tenantService) RequestRegistration(ctx context.Context, name, slug, des
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// [Keto] Global Super Admin access to every tenant (even pending ones)
|
||||
if s.outboxRepo != nil {
|
||||
_ = s.outboxRepo.Create(ctx, &domain.KetoOutbox{
|
||||
Namespace: "Tenant",
|
||||
@@ -219,7 +235,6 @@ func (s *tenantService) RequestRegistration(ctx context.Context, name, slug, des
|
||||
})
|
||||
}
|
||||
|
||||
// Add Domain as unverified
|
||||
if err := s.repo.AddDomain(ctx, tenant.ID, domainName, false); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -238,15 +253,12 @@ func (s *tenantService) ApproveTenant(ctx context.Context, id string) error {
|
||||
return err
|
||||
}
|
||||
|
||||
// [Keto] Sync relation via Outbox
|
||||
if s.outboxRepo != nil {
|
||||
if adminEmail, ok := tenant.Config["adminEmail"].(string); ok && adminEmail != "" {
|
||||
slog.Info("Queueing tenant admin/owner sync to Keto", "tenant", tenant.Slug, "adminEmail", adminEmail)
|
||||
// Check if user already exists in our Read-Model
|
||||
if s.userRepo != nil {
|
||||
user, err := s.userRepo.FindByEmail(ctx, adminEmail)
|
||||
if err == nil && user != nil {
|
||||
// User exists, assign Admin, Owner, and Member roles in Keto via Outbox
|
||||
slog.Info("Queueing tenant ownership/membership sync to Keto", "tenant", tenant.Slug, "userID", user.ID)
|
||||
_ = s.outboxRepo.Create(ctx, &domain.KetoOutbox{
|
||||
Namespace: "Tenant",
|
||||
@@ -285,7 +297,6 @@ func (s *tenantService) GetTenantByDomain(ctx context.Context, emailDomain strin
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Only return ACTIVE tenants for auto-assignment
|
||||
if tenant.Status != domain.TenantStatusActive {
|
||||
return nil, errors.New("tenant is not active")
|
||||
}
|
||||
@@ -298,7 +309,6 @@ func (s *tenantService) GetTenantBySlug(ctx context.Context, slug string) (*doma
|
||||
}
|
||||
|
||||
func (s *tenantService) ListTenants(ctx context.Context, limit, offset int, parentID string) ([]domain.Tenant, int64, error) {
|
||||
// Let the repository handle the query and pagination
|
||||
return s.repo.List(ctx, limit, offset, parentID)
|
||||
}
|
||||
|
||||
@@ -314,14 +324,12 @@ func (s *tenantService) IsDomainAllowed(ctx context.Context, domainName string)
|
||||
}
|
||||
|
||||
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
|
||||
@@ -337,7 +345,6 @@ func (s *tenantService) ProvisionTenantByDomain(ctx context.Context, domainName
|
||||
continue
|
||||
}
|
||||
|
||||
// 3. Find rule for this domain
|
||||
rule, ok := mapping[domainName].(map[string]interface{})
|
||||
if !ok {
|
||||
continue
|
||||
@@ -350,13 +357,32 @@ func (s *tenantService) ProvisionTenantByDomain(ctx context.Context, domainName
|
||||
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
|
||||
}
|
||||
|
||||
func (s *tenantService) DeleteTenantsBulk(ctx context.Context, ids []string) error {
|
||||
if len(ids) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
if err := s.repo.DeleteBulk(ctx, ids); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if s.outboxRepo != nil {
|
||||
for _, id := range ids {
|
||||
_ = s.outboxRepo.Create(ctx, &domain.KetoOutbox{
|
||||
Namespace: "Tenant",
|
||||
Object: id,
|
||||
Relation: "parents",
|
||||
Action: domain.KetoOutboxActionDelete,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user