forked from baron/baron-sso
orgfront 권한 정리
This commit is contained in:
@@ -135,6 +135,130 @@ func clientTenantAccessAllowed(profile *domain.UserProfileResponse, client domai
|
||||
return false
|
||||
}
|
||||
|
||||
func clientTenantAccessAllowedForSubtree(c *fiber.Ctx, tenantSvc service.TenantService, profile *domain.UserProfileResponse, client domain.HydraClient) bool {
|
||||
if clientTenantAccessAllowed(profile, client) {
|
||||
return true
|
||||
}
|
||||
if tenantSvc == nil || profile == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
allowedTenants := make([]domain.Tenant, 0)
|
||||
for _, identifier := range clientAllowedTenants(client.Metadata) {
|
||||
if tenant, ok := resolveTenantAccessTenant(c, tenantSvc, domain.Tenant{ID: identifier, Slug: identifier}); ok {
|
||||
allowedTenants = append(allowedTenants, tenant)
|
||||
}
|
||||
}
|
||||
if len(allowedTenants) == 0 {
|
||||
return false
|
||||
}
|
||||
|
||||
for _, candidate := range tenantAccessProfileTenants(profile) {
|
||||
resolvedCandidate, ok := resolveTenantAccessTenant(c, tenantSvc, candidate)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
for _, allowed := range allowedTenants {
|
||||
if tenantMatchesOrDescendsFrom(c, tenantSvc, resolvedCandidate, allowed) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func tenantAccessProfileTenants(profile *domain.UserProfileResponse) []domain.Tenant {
|
||||
if profile == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
seen := make(map[string]struct{})
|
||||
tenants := make([]domain.Tenant, 0, len(profile.ManageableTenants)+len(profile.JoinedTenants)+2)
|
||||
add := func(tenant domain.Tenant) {
|
||||
key := strings.ToLower(firstNonEmptyString(tenant.ID, tenant.Slug, tenant.Name))
|
||||
if key == "" {
|
||||
return
|
||||
}
|
||||
if _, ok := seen[key]; ok {
|
||||
return
|
||||
}
|
||||
seen[key] = struct{}{}
|
||||
tenants = append(tenants, tenant)
|
||||
}
|
||||
|
||||
if profile.Tenant != nil {
|
||||
add(*profile.Tenant)
|
||||
}
|
||||
if profile.TenantID != nil {
|
||||
add(domain.Tenant{ID: strings.TrimSpace(*profile.TenantID)})
|
||||
}
|
||||
for _, tenant := range profile.ManageableTenants {
|
||||
add(tenant)
|
||||
}
|
||||
for _, tenant := range profile.JoinedTenants {
|
||||
add(tenant)
|
||||
}
|
||||
return tenants
|
||||
}
|
||||
|
||||
func resolveTenantAccessTenant(c *fiber.Ctx, tenantSvc service.TenantService, tenant domain.Tenant) (domain.Tenant, bool) {
|
||||
if tenantSvc == nil {
|
||||
return tenant, firstNonEmptyString(tenant.ID, tenant.Slug) != ""
|
||||
}
|
||||
if strings.TrimSpace(tenant.ID) != "" {
|
||||
if resolved, err := tenantSvc.GetTenant(c.Context(), strings.TrimSpace(tenant.ID)); err == nil && resolved != nil {
|
||||
return *resolved, true
|
||||
}
|
||||
}
|
||||
if strings.TrimSpace(tenant.Slug) != "" {
|
||||
if resolved, err := tenantSvc.GetTenantBySlug(c.Context(), strings.TrimSpace(tenant.Slug)); err == nil && resolved != nil {
|
||||
return *resolved, true
|
||||
}
|
||||
}
|
||||
return tenant, firstNonEmptyString(tenant.ID, tenant.Slug) != ""
|
||||
}
|
||||
|
||||
func tenantMatchesOrDescendsFrom(c *fiber.Ctx, tenantSvc service.TenantService, tenant domain.Tenant, ancestor domain.Tenant) bool {
|
||||
if tenantAccessTenantMatches(tenant, ancestor) {
|
||||
return true
|
||||
}
|
||||
if tenantSvc == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
visited := make(map[string]struct{})
|
||||
current := tenant
|
||||
for current.ParentID != nil && strings.TrimSpace(*current.ParentID) != "" {
|
||||
parentID := strings.TrimSpace(*current.ParentID)
|
||||
if _, ok := visited[parentID]; ok {
|
||||
return false
|
||||
}
|
||||
visited[parentID] = struct{}{}
|
||||
|
||||
parent, err := tenantSvc.GetTenant(c.Context(), parentID)
|
||||
if err != nil || parent == nil {
|
||||
return false
|
||||
}
|
||||
if tenantAccessTenantMatches(*parent, ancestor) {
|
||||
return true
|
||||
}
|
||||
current = *parent
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func tenantAccessTenantMatches(left, right domain.Tenant) bool {
|
||||
leftID := strings.ToLower(strings.TrimSpace(left.ID))
|
||||
rightID := strings.ToLower(strings.TrimSpace(right.ID))
|
||||
if leftID != "" && rightID != "" && leftID == rightID {
|
||||
return true
|
||||
}
|
||||
|
||||
leftSlug := strings.ToLower(strings.TrimSpace(left.Slug))
|
||||
rightSlug := strings.ToLower(strings.TrimSpace(right.Slug))
|
||||
return leftSlug != "" && rightSlug != "" && leftSlug == rightSlug
|
||||
}
|
||||
|
||||
type tenantAccessDeniedDetails struct {
|
||||
Account tenantAccessDeniedAccount `json:"account"`
|
||||
CurrentTenant tenantAccessDeniedTenant `json:"current_tenant"`
|
||||
@@ -179,7 +303,7 @@ func enforceClientTenantAccess(c *fiber.Ctx, tenantSvc service.TenantService, cl
|
||||
_ = tenantNotAllowedError(c, details)
|
||||
return true
|
||||
}
|
||||
if !clientTenantAccessAllowed(profile, client) {
|
||||
if !clientTenantAccessAllowedForSubtree(c, tenantSvc, profile, client) {
|
||||
_ = tenantNotAllowedError(c, details)
|
||||
return true
|
||||
}
|
||||
|
||||
@@ -264,17 +264,22 @@ func TestGetConsentRequest_DeniesRestrictedClientWhenProfileResolutionFails(t *t
|
||||
ID: "tenant-a",
|
||||
Slug: "tenant-a",
|
||||
Name: "Tenant A",
|
||||
}, nil).Twice()
|
||||
}, nil)
|
||||
tenantSvc.On("GetTenant", mock.Anything, "tenant-c").Return(&domain.Tenant{
|
||||
ID: "tenant-c",
|
||||
Slug: "tenant-c",
|
||||
Name: "Tenant C",
|
||||
}, nil)
|
||||
tenantSvc.On("ListJoinedTenants", mock.Anything, "user-123").Return([]domain.Tenant{
|
||||
{ID: "tenant-a", Slug: "tenant-a", Name: "Tenant A"},
|
||||
{ID: "tenant-c", Slug: "tenant-c", Name: "Tenant C"},
|
||||
}, nil).Once()
|
||||
tenantSvc.On("GetTenant", mock.Anything, "tenant-b").Return(nil, assert.AnError).Once()
|
||||
tenantSvc.On("GetTenant", mock.Anything, "tenant-b").Return(nil, assert.AnError)
|
||||
tenantSvc.On("GetTenantBySlug", mock.Anything, "tenant-b").Return(&domain.Tenant{
|
||||
ID: "tenant-b-id",
|
||||
Slug: "tenant-b",
|
||||
Name: "Tenant B",
|
||||
}, nil).Once()
|
||||
}, nil)
|
||||
return tenantSvc
|
||||
}(),
|
||||
ConsentRepo: &mockConsentRepo{
|
||||
@@ -343,13 +348,18 @@ func TestAcceptOidcLoginRequest_DeniesTenantAccess(t *testing.T) {
|
||||
ID: "tenant-a",
|
||||
Slug: "tenant-a",
|
||||
Name: "Tenant A",
|
||||
}, nil).Twice()
|
||||
tenantSvc.On("GetTenant", mock.Anything, "tenant-b").Return(nil, assert.AnError).Once()
|
||||
}, nil)
|
||||
tenantSvc.On("GetTenant", mock.Anything, "tenant-c").Return(&domain.Tenant{
|
||||
ID: "tenant-c",
|
||||
Slug: "tenant-c",
|
||||
Name: "Tenant C",
|
||||
}, nil)
|
||||
tenantSvc.On("GetTenant", mock.Anything, "tenant-b").Return(nil, assert.AnError)
|
||||
tenantSvc.On("GetTenantBySlug", mock.Anything, "tenant-b").Return(&domain.Tenant{
|
||||
ID: "tenant-b-id",
|
||||
Slug: "tenant-b",
|
||||
Name: "Tenant B",
|
||||
}, nil).Once()
|
||||
}, nil)
|
||||
enforceClientTenantAccess(c, tenantSvc, client, profile, nil)
|
||||
return nil
|
||||
})
|
||||
@@ -384,3 +394,65 @@ func TestAcceptOidcLoginRequest_DeniesTenantAccess(t *testing.T) {
|
||||
assert.True(t, ok)
|
||||
assert.Equal(t, "Tenant B", allowedTenant["name"])
|
||||
}
|
||||
|
||||
func TestAcceptOidcLoginRequest_AllowsRestrictedClientForHanmacFamilyDescendant(t *testing.T) {
|
||||
app := fiber.New()
|
||||
app.Get("/allow-descendant", func(c *fiber.Ctx) error {
|
||||
hanmacFamilyID := "hanmac-family-id"
|
||||
samanID := "saman-id"
|
||||
profile := &domain.UserProfileResponse{
|
||||
ID: "user-123",
|
||||
Role: domain.RoleUser,
|
||||
Email: "user@samaneng.com",
|
||||
TenantID: &samanID,
|
||||
Tenant: &domain.Tenant{
|
||||
ID: samanID,
|
||||
Slug: "saman",
|
||||
Name: "삼안",
|
||||
ParentID: &hanmacFamilyID,
|
||||
},
|
||||
JoinedTenants: []domain.Tenant{
|
||||
{
|
||||
ID: samanID,
|
||||
Slug: "saman",
|
||||
Name: "삼안",
|
||||
ParentID: &hanmacFamilyID,
|
||||
},
|
||||
},
|
||||
}
|
||||
client := domain.HydraClient{
|
||||
ClientID: "orgfront",
|
||||
Metadata: map[string]any{
|
||||
"tenant_access_restricted": true,
|
||||
"allowed_tenants": []string{"hanmac-family"},
|
||||
},
|
||||
}
|
||||
tenantSvc := new(MockTenantService)
|
||||
tenantSvc.On("GetTenant", mock.Anything, "hanmac-family").Return(nil, assert.AnError).Maybe()
|
||||
tenantSvc.On("GetTenantBySlug", mock.Anything, "hanmac-family").Return(&domain.Tenant{
|
||||
ID: hanmacFamilyID,
|
||||
Slug: "hanmac-family",
|
||||
Name: "한맥가족",
|
||||
}, nil).Maybe()
|
||||
tenantSvc.On("GetTenant", mock.Anything, samanID).Return(&domain.Tenant{
|
||||
ID: samanID,
|
||||
Slug: "saman",
|
||||
Name: "삼안",
|
||||
ParentID: &hanmacFamilyID,
|
||||
}, nil).Maybe()
|
||||
tenantSvc.On("GetTenant", mock.Anything, hanmacFamilyID).Return(&domain.Tenant{
|
||||
ID: hanmacFamilyID,
|
||||
Slug: "hanmac-family",
|
||||
Name: "한맥가족",
|
||||
}, nil).Maybe()
|
||||
|
||||
blocked := enforceClientTenantAccess(c, tenantSvc, client, profile, nil)
|
||||
assert.False(t, blocked)
|
||||
return c.SendStatus(http.StatusNoContent)
|
||||
})
|
||||
|
||||
req := httptest.NewRequest(http.MethodGet, "/allow-descendant", nil)
|
||||
resp, err := app.Test(req)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, http.StatusNoContent, resp.StatusCode)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user