forked from baron/baron-sso
260 lines
8.3 KiB
Go
260 lines
8.3 KiB
Go
package service
|
|
|
|
import (
|
|
"baron-sso-backend/internal/domain"
|
|
"baron-sso-backend/internal/repository"
|
|
"baron-sso-backend/internal/utils"
|
|
"context"
|
|
"errors"
|
|
"log/slog"
|
|
"strings"
|
|
|
|
"gorm.io/gorm"
|
|
)
|
|
|
|
type TenantService interface {
|
|
RegisterTenant(ctx context.Context, name, slug, description string, domains []string) (*domain.Tenant, error)
|
|
RequestRegistration(ctx context.Context, name, slug, description string, domainName string, adminEmail string) (*domain.Tenant, error)
|
|
GetTenantByDomain(ctx context.Context, emailDomain string) (*domain.Tenant, error)
|
|
GetTenantBySlug(ctx context.Context, slug string) (*domain.Tenant, error)
|
|
GetTenant(ctx context.Context, id string) (*domain.Tenant, error)
|
|
ListManageableTenants(ctx context.Context, userID string) ([]domain.Tenant, error)
|
|
ApproveTenant(ctx context.Context, id string) error
|
|
SetKetoService(keto KetoService) // 추가
|
|
AddTenantAdmin(ctx context.Context, tenantID, userID string) error
|
|
RemoveTenantAdmin(ctx context.Context, tenantID, userID string) error
|
|
ListTenantAdmins(ctx context.Context, tenantID string) ([]string, error)
|
|
}
|
|
|
|
type tenantService struct {
|
|
repo repository.TenantRepository
|
|
keto KetoService
|
|
}
|
|
|
|
func NewTenantService(repo repository.TenantRepository) TenantService {
|
|
return &tenantService{repo: repo}
|
|
}
|
|
|
|
func (s *tenantService) SetKetoService(keto KetoService) {
|
|
s.keto = keto
|
|
}
|
|
|
|
func (s *tenantService) GetTenant(ctx context.Context, id string) (*domain.Tenant, error) {
|
|
return s.repo.FindByID(ctx, id)
|
|
}
|
|
|
|
func (s *tenantService) ListManageableTenants(ctx context.Context, userID string) ([]domain.Tenant, error) {
|
|
if s.keto == nil {
|
|
return nil, errors.New("keto service not initialized")
|
|
}
|
|
|
|
// 1. 직접 관리자인 테넌트 ID 목록 (Tenant:ID#admins@User:ID)
|
|
directTenantIDs, err := s.keto.ListObjects(ctx, "Tenant", "admins", "User:"+userID)
|
|
if err != nil {
|
|
slog.Error("Failed to list direct tenants", "userID", userID, "error", err)
|
|
}
|
|
|
|
// 2. 관리 권한이 있는 유저 그룹 목록 (UserGroup:ID#owners@User:ID)
|
|
// 정책: 그룹장은 해당 그룹(테넌트)의 어드민이 된다.
|
|
ownedGroupIDs, err := s.keto.ListObjects(ctx, "UserGroup", "owners", "User:"+userID)
|
|
if err != nil {
|
|
slog.Error("Failed to list owned groups", "userID", userID, "error", err)
|
|
}
|
|
|
|
// 3. 멤버로 속한 유저 그룹 목록 (UserGroup:ID#members@User:ID)
|
|
memberGroupIDs, err := s.keto.ListObjects(ctx, "UserGroup", "members", "User:"+userID)
|
|
if err != nil {
|
|
slog.Error("Failed to list group memberships", "userID", userID, "error", err)
|
|
}
|
|
|
|
// 4. 유저 그룹을 통해 상속받은 테넌트 목록 조회 (Tenant:ID#manage@UserGroup:ID#members)
|
|
var inheritedTenantIDs []string
|
|
allMyGroups := append(ownedGroupIDs, memberGroupIDs...)
|
|
for _, groupID := range allMyGroups {
|
|
// 해당 그룹에 부여된 테넌트 관리 권한 역추적
|
|
relations, err := s.keto.ListRelations(ctx, "Tenant", "", "manage", "UserGroup:"+groupID+"#members")
|
|
if err == nil {
|
|
for _, r := range relations {
|
|
inheritedTenantIDs = append(inheritedTenantIDs, r.Object)
|
|
}
|
|
}
|
|
// view 권한도 관리 가능 목록에 포함 (필요 시)
|
|
relationsView, err := s.keto.ListRelations(ctx, "Tenant", "", "view", "UserGroup:"+groupID+"#members")
|
|
if err == nil {
|
|
for _, r := range relationsView {
|
|
inheritedTenantIDs = append(inheritedTenantIDs, r.Object)
|
|
}
|
|
}
|
|
}
|
|
|
|
// 합산 및 중복 제거
|
|
allIDsMap := make(map[string]bool)
|
|
for _, id := range directTenantIDs {
|
|
allIDsMap[id] = true
|
|
}
|
|
for _, id := range ownedGroupIDs {
|
|
allIDsMap[id] = true // 그룹 자체도 테넌트이므로 포함
|
|
}
|
|
for _, id := range inheritedTenantIDs {
|
|
allIDsMap[id] = true
|
|
}
|
|
|
|
allIDs := make([]string, 0, len(allIDsMap))
|
|
for id := range allIDsMap {
|
|
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, description string, domains []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")
|
|
}
|
|
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
|
|
return nil, err
|
|
}
|
|
|
|
// 2. Create Tenant
|
|
tenant := &domain.Tenant{
|
|
Name: name,
|
|
Slug: slug,
|
|
Description: description,
|
|
Status: domain.TenantStatusActive,
|
|
}
|
|
|
|
if err := s.repo.Create(ctx, tenant); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// 3. Add Domains (Auto-verify for manual admin registration)
|
|
for _, d := range domains {
|
|
if err := s.repo.AddDomain(ctx, tenant.ID, d); err != nil {
|
|
slog.Error("Failed to add domain to tenant", "tenant", slug, "domain", d, "error", err)
|
|
}
|
|
}
|
|
|
|
return s.repo.FindBySlug(ctx, slug)
|
|
}
|
|
|
|
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")
|
|
}
|
|
|
|
tenant := &domain.Tenant{
|
|
Name: name,
|
|
Slug: slug,
|
|
Description: description,
|
|
Status: domain.TenantStatusPending,
|
|
Config: domain.JSONMap{"adminEmail": adminEmail},
|
|
}
|
|
|
|
if err := s.repo.Create(ctx, tenant); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Add Domain as unverified
|
|
// TODO: Create a more nuanced AddDomain that takes 'verified' param
|
|
// For now, Repo.AddDomain sets verified=true. I should fix Repo or just manually do it here if needed.
|
|
// Let's fix Repo later.
|
|
if err := s.repo.AddDomain(ctx, tenant.ID, domainName); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return tenant, nil
|
|
}
|
|
|
|
func (s *tenantService) ApproveTenant(ctx context.Context, id string) error {
|
|
tenant, err := s.repo.FindByID(ctx, id)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
tenant.Status = domain.TenantStatusActive
|
|
if err := s.repo.Update(ctx, tenant); err != nil {
|
|
return err
|
|
}
|
|
|
|
// [Keto] Sync relation
|
|
if s.keto != nil {
|
|
// 테넌트 자체를 정의 (Zanzibar style)
|
|
// 만약 신청 시 관리자 이메일이 있었다면 해당 사용자를 찾아 admin 권한 부여 시도
|
|
if adminEmail, ok := tenant.Config["adminEmail"].(string); ok && adminEmail != "" {
|
|
slog.Info("Syncing tenant admin to Keto", "tenant", tenant.Slug, "adminEmail", adminEmail)
|
|
// 여기서는 나중에 사용자가 가입할 때 처리하거나, 이미 가입된 사용자인지 확인 필요
|
|
// 우선 테넌트 관리자 관계 생성 로직은 사용자 가입/역할 변경 시점에 주로 발생하도록 설계
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (s *tenantService) GetTenantByDomain(ctx context.Context, emailDomain string) (*domain.Tenant, error) {
|
|
tenant, err := s.repo.FindByDomain(ctx, emailDomain)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Only return ACTIVE tenants for auto-assignment
|
|
if tenant.Status != domain.TenantStatusActive {
|
|
return nil, errors.New("tenant is not active")
|
|
}
|
|
|
|
return tenant, nil
|
|
}
|
|
|
|
func (s *tenantService) GetTenantBySlug(ctx context.Context, slug string) (*domain.Tenant, error) {
|
|
return s.repo.FindBySlug(ctx, slug)
|
|
}
|
|
|
|
func (s *tenantService) AddTenantAdmin(ctx context.Context, tenantID, userID string) error {
|
|
if s.keto == nil {
|
|
return errors.New("keto service not initialized")
|
|
}
|
|
return s.keto.CreateRelation(ctx, "Tenant", tenantID, "admins", "User:"+userID)
|
|
}
|
|
|
|
func (s *tenantService) RemoveTenantAdmin(ctx context.Context, tenantID, userID string) error {
|
|
if s.keto == nil {
|
|
return errors.New("keto service not initialized")
|
|
}
|
|
return s.keto.DeleteRelation(ctx, "Tenant", tenantID, "admins", "User:"+userID)
|
|
}
|
|
|
|
func (s *tenantService) ListTenantAdmins(ctx context.Context, tenantID string) ([]string, error) {
|
|
if s.keto == nil {
|
|
return nil, errors.New("keto service not initialized")
|
|
}
|
|
tuples, err := s.keto.ListRelations(ctx, "Tenant", tenantID, "admins", "")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
userIDs := make([]string, 0, len(tuples))
|
|
for _, t := range tuples {
|
|
if len(t.SubjectID) > 5 && t.SubjectID[:5] == "User:" {
|
|
userIDs = append(userIDs, t.SubjectID[5:])
|
|
}
|
|
}
|
|
return userIDs, nil
|
|
}
|