forked from baron/baron-sso
가입 전략 수립
This commit is contained in:
@@ -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 주입
|
||||
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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, " ", "")
|
||||
|
||||
@@ -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()})
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user