1
0
forked from baron/baron-sso

테넌트 접근 제한 로직 보강

This commit is contained in:
2026-04-28 10:57:16 +09:00
parent 367368805a
commit 955128a25a
3 changed files with 375 additions and 48 deletions

View File

@@ -2,6 +2,8 @@ package handler
import (
"baron-sso-backend/internal/domain"
"baron-sso-backend/internal/response"
"baron-sso-backend/internal/service"
"encoding/json"
"errors"
"sort"
@@ -133,8 +135,32 @@ func clientTenantAccessAllowed(profile *domain.UserProfileResponse, client domai
return false
}
func tenantNotAllowedError(c *fiber.Ctx) error {
return errorJSONCode(c, fiber.StatusForbidden, "tenant_not_allowed", "허용되지 않은 테넌트입니다.")
type tenantAccessDeniedDetails struct {
Account tenantAccessDeniedAccount `json:"account"`
CurrentTenant tenantAccessDeniedTenant `json:"current_tenant"`
AffiliatedTenants []tenantAccessDeniedTenant `json:"affiliated_tenants,omitempty"`
AllowedTenants []tenantAccessDeniedTenant `json:"allowed_tenants,omitempty"`
}
type tenantAccessDeniedAccount struct {
Email string `json:"email,omitempty"`
}
type tenantAccessDeniedTenant struct {
ID string `json:"id,omitempty"`
Slug string `json:"slug,omitempty"`
Name string `json:"name,omitempty"`
Identifier string `json:"identifier,omitempty"`
}
func tenantNotAllowedError(c *fiber.Ctx, details tenantAccessDeniedDetails) error {
return response.ErrorWithDetails(
c,
fiber.StatusForbidden,
"tenant_not_allowed",
"허용되지 않은 테넌트입니다.",
details,
)
}
func isClientTenantAccessAllowed(profile *domain.UserProfileResponse, client domain.HydraClient) bool {
@@ -144,17 +170,162 @@ func isClientTenantAccessAllowed(profile *domain.UserProfileResponse, client dom
return clientTenantAccessAllowed(profile, client)
}
func enforceClientTenantAccess(c *fiber.Ctx, client domain.HydraClient, profile *domain.UserProfileResponse, resolveErr error) error {
func enforceClientTenantAccess(c *fiber.Ctx, tenantSvc service.TenantService, client domain.HydraClient, profile *domain.UserProfileResponse, resolveErr error) bool {
if !clientTenantAccessRestricted(client.Metadata) {
return nil
return false
}
details := buildTenantAccessDeniedDetails(c, tenantSvc, client, profile)
if resolveErr != nil || profile == nil {
return tenantNotAllowedError(c)
_ = tenantNotAllowedError(c, details)
return true
}
if !clientTenantAccessAllowed(profile, client) {
return tenantNotAllowedError(c)
_ = tenantNotAllowedError(c, details)
return true
}
return nil
return false
}
func buildTenantAccessDeniedDetails(c *fiber.Ctx, tenantSvc service.TenantService, client domain.HydraClient, profile *domain.UserProfileResponse) tenantAccessDeniedDetails {
details := tenantAccessDeniedDetails{
Account: tenantAccessDeniedAccount{Email: strings.TrimSpace(profileEmail(profile))},
CurrentTenant: resolveCurrentTenantDetails(c, tenantSvc, profile),
AffiliatedTenants: resolveAffiliatedTenantDetails(c, tenantSvc, profile),
}
for _, identifier := range clientAllowedTenants(client.Metadata) {
details.AllowedTenants = append(details.AllowedTenants, resolveAllowedTenantDetails(c, tenantSvc, identifier))
}
return details
}
func resolveAffiliatedTenantDetails(c *fiber.Ctx, tenantSvc service.TenantService, profile *domain.UserProfileResponse) []tenantAccessDeniedTenant {
if profile == nil {
return nil
}
seen := make(map[string]struct{})
out := make([]tenantAccessDeniedTenant, 0, len(profile.JoinedTenants)+1)
appendTenant := func(tenant tenantAccessDeniedTenant) {
key := strings.ToLower(firstNonEmptyString(tenant.ID, tenant.Slug, tenant.Identifier, tenant.Name))
if key == "" {
return
}
if _, ok := seen[key]; ok {
return
}
seen[key] = struct{}{}
out = append(out, tenant)
}
appendTenant(resolveCurrentTenantDetails(c, tenantSvc, profile))
for _, joined := range profile.JoinedTenants {
appendTenant(tenantAccessDeniedTenant{
ID: strings.TrimSpace(joined.ID),
Slug: strings.TrimSpace(joined.Slug),
Name: strings.TrimSpace(joined.Name),
Identifier: firstNonEmptyString(strings.TrimSpace(joined.Slug), strings.TrimSpace(joined.ID)),
})
}
return out
}
func resolveCurrentTenantDetails(c *fiber.Ctx, tenantSvc service.TenantService, profile *domain.UserProfileResponse) tenantAccessDeniedTenant {
if profile == nil {
return tenantAccessDeniedTenant{}
}
if profile.Tenant != nil {
return tenantAccessDeniedTenant{
ID: strings.TrimSpace(profile.Tenant.ID),
Slug: strings.TrimSpace(profile.Tenant.Slug),
Name: strings.TrimSpace(profile.Tenant.Name),
Identifier: firstNonEmptyString(strings.TrimSpace(profile.Tenant.Slug), strings.TrimSpace(profile.Tenant.ID)),
}
}
if tenantSvc != nil {
if profile.TenantID != nil && strings.TrimSpace(*profile.TenantID) != "" {
if tenant, err := tenantSvc.GetTenant(c.Context(), strings.TrimSpace(*profile.TenantID)); err == nil && tenant != nil {
return tenantAccessDeniedTenant{
ID: strings.TrimSpace(tenant.ID),
Slug: strings.TrimSpace(tenant.Slug),
Name: strings.TrimSpace(tenant.Name),
Identifier: firstNonEmptyString(strings.TrimSpace(tenant.Slug), strings.TrimSpace(tenant.ID)),
}
}
}
if strings.TrimSpace(profile.CompanyCode) != "" {
if tenant, err := tenantSvc.GetTenantBySlug(c.Context(), strings.TrimSpace(profile.CompanyCode)); err == nil && tenant != nil {
return tenantAccessDeniedTenant{
ID: strings.TrimSpace(tenant.ID),
Slug: strings.TrimSpace(tenant.Slug),
Name: strings.TrimSpace(tenant.Name),
Identifier: firstNonEmptyString(strings.TrimSpace(tenant.Slug), strings.TrimSpace(tenant.ID)),
}
}
}
}
return tenantAccessDeniedTenant{
ID: strings.TrimSpace(pointerValue(profile.TenantID)),
Slug: strings.TrimSpace(profile.CompanyCode),
Identifier: firstNonEmptyString(strings.TrimSpace(profile.CompanyCode), strings.TrimSpace(pointerValue(profile.TenantID))),
}
}
func resolveAllowedTenantDetails(c *fiber.Ctx, tenantSvc service.TenantService, identifier string) tenantAccessDeniedTenant {
identifier = strings.TrimSpace(identifier)
if identifier == "" {
return tenantAccessDeniedTenant{}
}
if tenantSvc != nil {
if tenant, err := tenantSvc.GetTenant(c.Context(), identifier); err == nil && tenant != nil {
return tenantAccessDeniedTenant{
ID: strings.TrimSpace(tenant.ID),
Slug: strings.TrimSpace(tenant.Slug),
Name: strings.TrimSpace(tenant.Name),
Identifier: firstNonEmptyString(strings.TrimSpace(tenant.Slug), strings.TrimSpace(tenant.ID), identifier),
}
}
if tenant, err := tenantSvc.GetTenantBySlug(c.Context(), identifier); err == nil && tenant != nil {
return tenantAccessDeniedTenant{
ID: strings.TrimSpace(tenant.ID),
Slug: strings.TrimSpace(tenant.Slug),
Name: strings.TrimSpace(tenant.Name),
Identifier: firstNonEmptyString(strings.TrimSpace(tenant.Slug), strings.TrimSpace(tenant.ID), identifier),
}
}
}
return tenantAccessDeniedTenant{Identifier: identifier}
}
func profileEmail(profile *domain.UserProfileResponse) string {
if profile == nil {
return ""
}
return profile.Email
}
func pointerValue(value *string) string {
if value == nil {
return ""
}
return *value
}
func firstNonEmptyString(values ...string) string {
for _, value := range values {
if strings.TrimSpace(value) != "" {
return strings.TrimSpace(value)
}
}
return ""
}
type clientStructuredScope struct {