1
0
forked from baron/baron-sso

가입 전략 수립

This commit is contained in:
2026-02-19 15:10:36 +09:00
parent e6bfcf465f
commit 5cb713a009
7 changed files with 95 additions and 35 deletions

View File

@@ -249,7 +249,7 @@ func main() {
kratosAdminService := service.NewKratosAdminService()
oryAdminProvider := service.NewOryProvider()
tenantService := service.NewTenantService(tenantRepo)
tenantService := service.NewTenantService(tenantRepo, userRepo)
userGroupService := service.NewUserGroupService(userGroupRepo, userRepo, tenantRepo, ketoService, kratosAdminService)
tenantService.SetKetoService(ketoService) // Keto 주입

View File

@@ -30,7 +30,8 @@ var defaultTenants = []InitialTenantConfig{
func SeedTenants(db *gorm.DB) error {
slog.Info("[Bootstrap] Seeding initial tenants...")
repo := repository.NewTenantRepository(db)
svc := service.NewTenantService(repo)
userRepo := repository.NewUserRepository(db)
svc := service.NewTenantService(repo, userRepo)
ctx := context.Background()
for _, config := range defaultTenants {
@@ -48,7 +49,7 @@ func SeedTenants(db *gorm.DB) error {
}
if !found {
slog.Info("[Bootstrap] Adding missing domain to tenant", "slug", config.Slug, "domain", d)
if err := repo.AddDomain(ctx, existing.ID, d); err != nil {
if err := repo.AddDomain(ctx, existing.ID, d, true); err != nil {
slog.Error("Failed to add domain", "error", err)
}
}

View File

@@ -388,8 +388,7 @@ func (h *AuthHandler) Signup(c *fiber.Ctx) error {
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "Identity provider unavailable"})
}
// [Strict] Enforce Tenant Auto-Assignment by Domain ONLY
// Manual companyCode from request is ignored to prevent unauthorized tenant joining
// [Strict] Enforce Tenant Auto-Assignment
companyCode := ""
var tenantID *string
@@ -399,19 +398,36 @@ func (h *AuthHandler) Signup(c *fiber.Ctx) error {
tenant, err := h.TenantService.GetTenantByDomain(c.Context(), domainName)
if err == nil && tenant != nil {
if tenant.Status == domain.TenantStatusActive {
slog.Info("[Signup] Auto-assigning tenant", "email", req.Email, "tenant", tenant.Slug)
slog.Info("[Signup] Auto-assigning tenant by domain", "email", req.Email, "tenant", tenant.Slug)
companyCode = tenant.Slug
tenantID = &tenant.ID
} else {
slog.Warn("[Signup] Attempted to join non-active tenant", "email", req.Email, "tenant", tenant.Slug, "status", tenant.Status)
// Policy: If tenant exists but not active, reject signup or allow as general?
// For now, let's allow as general but log it.
// Or return error if we want strict domain locking.
slog.Warn("[Signup] Attempted to join non-active tenant by domain", "email", req.Email, "tenant", tenant.Slug, "status", tenant.Status)
return c.Status(fiber.StatusForbidden).JSON(fiber.Map{"error": "Your organization's tenant is currently not active."})
}
}
}
// Fallback/Validation for manually provided CompanyCode if domain lookup didn't yield a tenant
if tenantID == nil && req.CompanyCode != "" {
tenant, err := h.TenantService.GetTenantBySlug(c.Context(), req.CompanyCode)
if err == nil && tenant != nil {
if tenant.Status == domain.TenantStatusActive {
// Policy: Should we allow manual joining by Slug?
// For now, let's allow it but log it as manual.
slog.Info("[Signup] Assigning tenant by manual slug", "email", req.Email, "tenant", tenant.Slug)
companyCode = tenant.Slug
tenantID = &tenant.ID
} else {
return c.Status(fiber.StatusForbidden).JSON(fiber.Map{"error": "The specified organization is not active."})
}
} else {
// If companyCode provided but not found, we should probably reject if we want strictness,
// or just treat as GENERAL user. Given the risk "존재하지 않는 테넌트도 저장됨", we should reject.
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "Invalid company code."})
}
}
// Normalize Phone (E.164 형태로 보관)
normalizedPhone := strings.ReplaceAll(req.Phone, "-", "")
normalizedPhone = strings.ReplaceAll(normalizedPhone, " ", "")

View File

@@ -2,6 +2,7 @@ package handler
import (
"baron-sso-backend/internal/domain"
"baron-sso-backend/internal/repository"
"baron-sso-backend/internal/service"
"errors"
"strings"
@@ -272,14 +273,8 @@ func (h *TenantHandler) UpdateTenant(c *fiber.Ctx) error {
if strings.TrimSpace(d) == "" {
continue
}
td := domain.TenantDomain{
TenantID: tenant.ID,
Domain: strings.TrimSpace(d),
Verified: true,
}
if err := h.DB.Create(&td).Error; err != nil {
// Log and continue or return error?
// For now return error to be safe.
// Use repository for consistency
if err := repository.NewTenantRepository(h.DB).AddDomain(c.Context(), tenant.ID, strings.TrimSpace(d), true); err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "failed to add domain: " + d})
}
}
@@ -301,7 +296,21 @@ func (h *TenantHandler) DeleteTenant(c *fiber.Ctx) error {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "tenant id is required"})
}
if err := h.DB.Delete(&domain.Tenant{}, "id = ?", tenantID).Error; err != nil {
var tenant domain.Tenant
if err := h.DB.First(&tenant, "id = ?", tenantID).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return c.Status(fiber.StatusNotFound).JSON(fiber.Map{"error": "tenant not found"})
}
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()})
}
// Rename slug to release it for reuse before soft delete
deletedSlug := tenant.Slug + "-deleted-" + time.Now().Format("20060102150405")
if err := h.DB.Model(&tenant).Update("slug", deletedSlug).Error; err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "failed to release slug"})
}
if err := h.DB.Delete(&tenant).Error; err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()})
}

View File

@@ -15,7 +15,7 @@ type TenantRepository interface {
FindByName(ctx context.Context, name string) (*domain.Tenant, error)
FindByDomain(ctx context.Context, domainName string) (*domain.Tenant, error)
FindByIDs(ctx context.Context, ids []string) ([]domain.Tenant, error)
AddDomain(ctx context.Context, tenantID string, domainName string) error
AddDomain(ctx context.Context, tenantID string, domainName string, verified bool) error
}
type tenantRepository struct {
@@ -82,11 +82,11 @@ func (r *tenantRepository) FindByIDs(ctx context.Context, ids []string) ([]domai
return tenants, nil
}
func (r *tenantRepository) AddDomain(ctx context.Context, tenantID string, domainName string) error {
func (r *tenantRepository) AddDomain(ctx context.Context, tenantID string, domainName string, verified bool) error {
td := domain.TenantDomain{
TenantID: tenantID,
Domain: domainName,
Verified: true, // Auto-verify for internal init/admin usage for now
Verified: verified,
}
return r.db.WithContext(ctx).Create(&td).Error
}

View File

@@ -23,12 +23,13 @@ type TenantService interface {
}
type tenantService struct {
repo repository.TenantRepository
keto KetoService
repo repository.TenantRepository
userRepo repository.UserRepository
keto KetoService
}
func NewTenantService(repo repository.TenantRepository) TenantService {
return &tenantService{repo: repo}
func NewTenantService(repo repository.TenantRepository, userRepo repository.UserRepository) TenantService {
return &tenantService{repo: repo, userRepo: userRepo}
}
func (s *tenantService) SetKetoService(keto KetoService) {
@@ -136,7 +137,7 @@ func (s *tenantService) RegisterTenant(ctx context.Context, name, slug, descript
// 3. Add Domains (Auto-verify for manual admin registration)
for _, d := range domains {
if err := s.repo.AddDomain(ctx, tenant.ID, d); err != nil {
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)
}
}
@@ -169,10 +170,7 @@ func (s *tenantService) RequestRegistration(ctx context.Context, name, slug, des
}
// 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 {
if err := s.repo.AddDomain(ctx, tenant.ID, domainName, false); err != nil {
return nil, err
}
@@ -192,12 +190,23 @@ func (s *tenantService) ApproveTenant(ctx context.Context, id string) error {
// [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)
// 여기서는 나중에 사용자가 가입할 때 처리하거나, 이미 가입된 사용자인지 확인 필요
// 우선 테넌트 관리자 관계 생성 로직은 사용자 가입/역할 변경 시점에 주로 발생하도록 설계
// 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 role in Keto
err = s.keto.CreateRelation(ctx, "Tenant", tenant.ID, "admin", "User:"+user.ID)
if err != nil {
slog.Error("Failed to assign tenant admin in Keto", "tenant", tenant.ID, "user", user.ID, "error", err)
} else {
slog.Info("Assigned tenant admin in Keto", "tenant", tenant.ID, "user", user.ID)
}
} else {
slog.Info("Tenant admin user not found in local DB, will need manual sync or sync on signup", "email", adminEmail)
}
}
}
}

View File

@@ -25,6 +25,31 @@ var (
"tenant": true,
"user": true,
"dev": true,
"stage": true,
"prod": true,
"test": true,
"static": true,
"assets": true,
"image": true,
"img": true,
"mail": true,
"smtp": true,
"pop": true,
"imap": true,
"ns": true,
"mx": true,
"webmaster": true,
"security": true,
"support": true,
"help": true,
"billing": true,
"account": true,
"config": true,
"status": true,
"health": true,
"metrics": true,
"grafana": true,
"prometheus": true,
}
)