forked from baron/baron-sso
Merge pull request 'featur/tenantsign' (#278) from featur/tenantsign into dev
Reviewed-on: baron/baron-sso#278
This commit is contained in:
@@ -249,7 +249,7 @@ func main() {
|
|||||||
kratosAdminService := service.NewKratosAdminService()
|
kratosAdminService := service.NewKratosAdminService()
|
||||||
oryAdminProvider := service.NewOryProvider()
|
oryAdminProvider := service.NewOryProvider()
|
||||||
|
|
||||||
tenantService := service.NewTenantService(tenantRepo)
|
tenantService := service.NewTenantService(tenantRepo, userRepo)
|
||||||
userGroupService := service.NewUserGroupService(userGroupRepo, userRepo, tenantRepo, ketoService, kratosAdminService)
|
userGroupService := service.NewUserGroupService(userGroupRepo, userRepo, tenantRepo, ketoService, kratosAdminService)
|
||||||
tenantService.SetKetoService(ketoService) // Keto 주입
|
tenantService.SetKetoService(ketoService) // Keto 주입
|
||||||
|
|
||||||
|
|||||||
@@ -30,7 +30,8 @@ var defaultTenants = []InitialTenantConfig{
|
|||||||
func SeedTenants(db *gorm.DB) error {
|
func SeedTenants(db *gorm.DB) error {
|
||||||
slog.Info("[Bootstrap] Seeding initial tenants...")
|
slog.Info("[Bootstrap] Seeding initial tenants...")
|
||||||
repo := repository.NewTenantRepository(db)
|
repo := repository.NewTenantRepository(db)
|
||||||
svc := service.NewTenantService(repo)
|
userRepo := repository.NewUserRepository(db)
|
||||||
|
svc := service.NewTenantService(repo, userRepo)
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
|
|
||||||
for _, config := range defaultTenants {
|
for _, config := range defaultTenants {
|
||||||
@@ -48,7 +49,7 @@ func SeedTenants(db *gorm.DB) error {
|
|||||||
}
|
}
|
||||||
if !found {
|
if !found {
|
||||||
slog.Info("[Bootstrap] Adding missing domain to tenant", "slug", config.Slug, "domain", d)
|
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)
|
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"})
|
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "Identity provider unavailable"})
|
||||||
}
|
}
|
||||||
|
|
||||||
// [Strict] Enforce Tenant Auto-Assignment by Domain ONLY
|
// [Strict] Enforce Tenant Auto-Assignment
|
||||||
// Manual companyCode from request is ignored to prevent unauthorized tenant joining
|
|
||||||
companyCode := ""
|
companyCode := ""
|
||||||
var tenantID *string
|
var tenantID *string
|
||||||
|
|
||||||
@@ -399,19 +398,36 @@ func (h *AuthHandler) Signup(c *fiber.Ctx) error {
|
|||||||
tenant, err := h.TenantService.GetTenantByDomain(c.Context(), domainName)
|
tenant, err := h.TenantService.GetTenantByDomain(c.Context(), domainName)
|
||||||
if err == nil && tenant != nil {
|
if err == nil && tenant != nil {
|
||||||
if tenant.Status == domain.TenantStatusActive {
|
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
|
companyCode = tenant.Slug
|
||||||
tenantID = &tenant.ID
|
tenantID = &tenant.ID
|
||||||
} else {
|
} else {
|
||||||
slog.Warn("[Signup] Attempted to join non-active tenant", "email", req.Email, "tenant", tenant.Slug, "status", tenant.Status)
|
slog.Warn("[Signup] Attempted to join non-active tenant by domain", "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.
|
|
||||||
return c.Status(fiber.StatusForbidden).JSON(fiber.Map{"error": "Your organization's tenant is currently not active."})
|
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 형태로 보관)
|
// Normalize Phone (E.164 형태로 보관)
|
||||||
normalizedPhone := strings.ReplaceAll(req.Phone, "-", "")
|
normalizedPhone := strings.ReplaceAll(req.Phone, "-", "")
|
||||||
normalizedPhone = strings.ReplaceAll(normalizedPhone, " ", "")
|
normalizedPhone = strings.ReplaceAll(normalizedPhone, " ", "")
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package handler
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"baron-sso-backend/internal/domain"
|
"baron-sso-backend/internal/domain"
|
||||||
|
"baron-sso-backend/internal/repository"
|
||||||
"baron-sso-backend/internal/service"
|
"baron-sso-backend/internal/service"
|
||||||
"errors"
|
"errors"
|
||||||
"strings"
|
"strings"
|
||||||
@@ -272,14 +273,8 @@ func (h *TenantHandler) UpdateTenant(c *fiber.Ctx) error {
|
|||||||
if strings.TrimSpace(d) == "" {
|
if strings.TrimSpace(d) == "" {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
td := domain.TenantDomain{
|
// Use repository for consistency
|
||||||
TenantID: tenant.ID,
|
if err := repository.NewTenantRepository(h.DB).AddDomain(c.Context(), tenant.ID, strings.TrimSpace(d), true); err != nil {
|
||||||
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.
|
|
||||||
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "failed to add domain: " + d})
|
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"})
|
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()})
|
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)
|
FindByName(ctx context.Context, name string) (*domain.Tenant, error)
|
||||||
FindByDomain(ctx context.Context, domainName string) (*domain.Tenant, error)
|
FindByDomain(ctx context.Context, domainName string) (*domain.Tenant, error)
|
||||||
FindByIDs(ctx context.Context, ids []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 {
|
type tenantRepository struct {
|
||||||
@@ -82,11 +82,11 @@ func (r *tenantRepository) FindByIDs(ctx context.Context, ids []string) ([]domai
|
|||||||
return tenants, nil
|
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{
|
td := domain.TenantDomain{
|
||||||
TenantID: tenantID,
|
TenantID: tenantID,
|
||||||
Domain: domainName,
|
Domain: domainName,
|
||||||
Verified: true, // Auto-verify for internal init/admin usage for now
|
Verified: verified,
|
||||||
}
|
}
|
||||||
return r.db.WithContext(ctx).Create(&td).Error
|
return r.db.WithContext(ctx).Create(&td).Error
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,11 +24,12 @@ type TenantService interface {
|
|||||||
|
|
||||||
type tenantService struct {
|
type tenantService struct {
|
||||||
repo repository.TenantRepository
|
repo repository.TenantRepository
|
||||||
|
userRepo repository.UserRepository
|
||||||
keto KetoService
|
keto KetoService
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewTenantService(repo repository.TenantRepository) TenantService {
|
func NewTenantService(repo repository.TenantRepository, userRepo repository.UserRepository) TenantService {
|
||||||
return &tenantService{repo: repo}
|
return &tenantService{repo: repo, userRepo: userRepo}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *tenantService) SetKetoService(keto KetoService) {
|
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)
|
// 3. Add Domains (Auto-verify for manual admin registration)
|
||||||
for _, d := range domains {
|
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)
|
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
|
// Add Domain as unverified
|
||||||
// TODO: Create a more nuanced AddDomain that takes 'verified' param
|
if err := s.repo.AddDomain(ctx, tenant.ID, domainName, false); err != nil {
|
||||||
// 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 nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -192,12 +190,23 @@ func (s *tenantService) ApproveTenant(ctx context.Context, id string) error {
|
|||||||
|
|
||||||
// [Keto] Sync relation
|
// [Keto] Sync relation
|
||||||
if s.keto != nil {
|
if s.keto != nil {
|
||||||
// 테넌트 자체를 정의 (Zanzibar style)
|
|
||||||
// 만약 신청 시 관리자 이메일이 있었다면 해당 사용자를 찾아 admin 권한 부여 시도
|
|
||||||
if adminEmail, ok := tenant.Config["adminEmail"].(string); ok && adminEmail != "" {
|
if adminEmail, ok := tenant.Config["adminEmail"].(string); ok && adminEmail != "" {
|
||||||
slog.Info("Syncing tenant admin to Keto", "tenant", tenant.Slug, "adminEmail", 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -94,7 +94,7 @@ func (m *MockTenantRepository) FindByDomain(ctx context.Context, domainName stri
|
|||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *MockTenantRepository) AddDomain(ctx context.Context, tenantID string, domainName string) error {
|
func (m *MockTenantRepository) AddDomain(ctx context.Context, tenantID string, domainName string, verified bool) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -25,6 +25,31 @@ var (
|
|||||||
"tenant": true,
|
"tenant": true,
|
||||||
"user": true,
|
"user": true,
|
||||||
"dev": 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,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import type { AuthProviderProps } from "react-oidc-context";
|
|||||||
|
|
||||||
export const oidcConfig: AuthProviderProps = {
|
export const oidcConfig: AuthProviderProps = {
|
||||||
authority: import.meta.env.VITE_OIDC_AUTHORITY || "http://localhost:5000/oidc", // Gateway Proxy URL
|
authority: import.meta.env.VITE_OIDC_AUTHORITY || "http://localhost:5000/oidc", // Gateway Proxy URL
|
||||||
client_id: import.meta.env.VITE_OIDC_CLIENT_ID || "devfront-client",
|
client_id: import.meta.env.VITE_OIDC_CLIENT_ID || "devfront",
|
||||||
redirect_uri: `${window.location.origin}/callback`,
|
redirect_uri: `${window.location.origin}/callback`,
|
||||||
response_type: "code",
|
response_type: "code",
|
||||||
scope: "openid offline_access profile email", // offline_access for refresh token
|
scope: "openid offline_access profile email", // offline_access for refresh token
|
||||||
|
|||||||
Reference in New Issue
Block a user