forked from baron/baron-sso
orgfront 권한 정리
This commit is contained in:
@@ -135,6 +135,130 @@ func clientTenantAccessAllowed(profile *domain.UserProfileResponse, client domai
|
|||||||
return false
|
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 {
|
type tenantAccessDeniedDetails struct {
|
||||||
Account tenantAccessDeniedAccount `json:"account"`
|
Account tenantAccessDeniedAccount `json:"account"`
|
||||||
CurrentTenant tenantAccessDeniedTenant `json:"current_tenant"`
|
CurrentTenant tenantAccessDeniedTenant `json:"current_tenant"`
|
||||||
@@ -179,7 +303,7 @@ func enforceClientTenantAccess(c *fiber.Ctx, tenantSvc service.TenantService, cl
|
|||||||
_ = tenantNotAllowedError(c, details)
|
_ = tenantNotAllowedError(c, details)
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
if !clientTenantAccessAllowed(profile, client) {
|
if !clientTenantAccessAllowedForSubtree(c, tenantSvc, profile, client) {
|
||||||
_ = tenantNotAllowedError(c, details)
|
_ = tenantNotAllowedError(c, details)
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -264,17 +264,22 @@ func TestGetConsentRequest_DeniesRestrictedClientWhenProfileResolutionFails(t *t
|
|||||||
ID: "tenant-a",
|
ID: "tenant-a",
|
||||||
Slug: "tenant-a",
|
Slug: "tenant-a",
|
||||||
Name: "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{
|
tenantSvc.On("ListJoinedTenants", mock.Anything, "user-123").Return([]domain.Tenant{
|
||||||
{ID: "tenant-a", Slug: "tenant-a", Name: "Tenant A"},
|
{ID: "tenant-a", Slug: "tenant-a", Name: "Tenant A"},
|
||||||
{ID: "tenant-c", Slug: "tenant-c", Name: "Tenant C"},
|
{ID: "tenant-c", Slug: "tenant-c", Name: "Tenant C"},
|
||||||
}, nil).Once()
|
}, 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{
|
tenantSvc.On("GetTenantBySlug", mock.Anything, "tenant-b").Return(&domain.Tenant{
|
||||||
ID: "tenant-b-id",
|
ID: "tenant-b-id",
|
||||||
Slug: "tenant-b",
|
Slug: "tenant-b",
|
||||||
Name: "Tenant B",
|
Name: "Tenant B",
|
||||||
}, nil).Once()
|
}, nil)
|
||||||
return tenantSvc
|
return tenantSvc
|
||||||
}(),
|
}(),
|
||||||
ConsentRepo: &mockConsentRepo{
|
ConsentRepo: &mockConsentRepo{
|
||||||
@@ -343,13 +348,18 @@ func TestAcceptOidcLoginRequest_DeniesTenantAccess(t *testing.T) {
|
|||||||
ID: "tenant-a",
|
ID: "tenant-a",
|
||||||
Slug: "tenant-a",
|
Slug: "tenant-a",
|
||||||
Name: "Tenant A",
|
Name: "Tenant A",
|
||||||
}, nil).Twice()
|
}, nil)
|
||||||
tenantSvc.On("GetTenant", mock.Anything, "tenant-b").Return(nil, assert.AnError).Once()
|
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{
|
tenantSvc.On("GetTenantBySlug", mock.Anything, "tenant-b").Return(&domain.Tenant{
|
||||||
ID: "tenant-b-id",
|
ID: "tenant-b-id",
|
||||||
Slug: "tenant-b",
|
Slug: "tenant-b",
|
||||||
Name: "Tenant B",
|
Name: "Tenant B",
|
||||||
}, nil).Once()
|
}, nil)
|
||||||
enforceClientTenantAccess(c, tenantSvc, client, profile, nil)
|
enforceClientTenantAccess(c, tenantSvc, client, profile, nil)
|
||||||
return nil
|
return nil
|
||||||
})
|
})
|
||||||
@@ -384,3 +394,65 @@ func TestAcceptOidcLoginRequest_DeniesTenantAccess(t *testing.T) {
|
|||||||
assert.True(t, ok)
|
assert.True(t, ok)
|
||||||
assert.Equal(t, "Tenant B", allowedTenant["name"])
|
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)
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,11 @@
|
|||||||
|
ac448f31cdae39bc93dc1d1274c30eb8a4ab79ea4b8625ab9b1210a11b151a82 ./manifest.json
|
||||||
|
0d43248e95c0d94fee588a2ae17d7607cd4053711dc0e513aa79634fc0c237d6 ./postgres/baron.dump
|
||||||
|
84bf1e08bce8542f9fdefadd493ba0dd8da669198f8d9dede605f383b159771a ./postgres/globals.sql
|
||||||
|
791cbc1a578e75c306c355cd281ab5aa92e687aacefaeee60354017823bf8ef4 ./postgres/ory_hydra.dump
|
||||||
|
ee0e254fd5eda4b3d668ab5d7ada6829de7fde933db43c4518e6d2dfae5f79fd ./postgres/ory_keto.dump
|
||||||
|
4512f6b1f1d395b06c6100c9ee77ace140ba7ac24a35179390b9f3a21126a9bd ./postgres/ory_kratos.dump
|
||||||
|
4ceeb2434aa1b3f37732bec2285d3f981f1ae2ffed3638b93752f337fcd923a1 ./reports/backup-report.md
|
||||||
|
e5a679f9d24f1ebad6c46a17545d693545db86506cb22bc24eddb6ef01676410 ./reports/baron-postgres-row-counts.txt
|
||||||
|
0159c03b5dffa4e1d784018425fc12e153dcce7a659cc5a7a0bcc9cf9ebe5bfe ./reports/ory_hydra-row-counts.txt
|
||||||
|
cd97643bb6a7dd4515ae32b9ef5d658b4f30fa9d4f8b17212d287891cfe29c2b ./reports/ory_keto-row-counts.txt
|
||||||
|
088c7eb5879f8462f7ba1a82cedd8b186c9692a6b6d7d2bd3d08b4921d6ce3ae ./reports/ory_kratos-row-counts.txt
|
||||||
14
backups/pre-saman-works-users-20260608-063605Z/manifest.json
Normal file
14
backups/pre-saman-works-users-20260608-063605Z/manifest.json
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
{
|
||||||
|
"format_version": "1",
|
||||||
|
"created_at": "2026-06-08T06:36:07Z",
|
||||||
|
"git_commit": "aa2848c3b6ce",
|
||||||
|
"mode": "maintenance",
|
||||||
|
"environment_scope": "same-env-only",
|
||||||
|
"services": ["postgres", "ory-postgres"],
|
||||||
|
"restore_policy": {
|
||||||
|
"requires_empty_target": true,
|
||||||
|
"requires_confirmation": "baron-sso",
|
||||||
|
"auto_run_migrations": false,
|
||||||
|
"works_relay_auto_resume": false
|
||||||
|
}
|
||||||
|
}
|
||||||
Binary file not shown.
@@ -0,0 +1,35 @@
|
|||||||
|
--
|
||||||
|
-- PostgreSQL database cluster dump
|
||||||
|
--
|
||||||
|
|
||||||
|
\restrict yrvKzJaekZ6C4tG4mnnuA9bxZKiekltyhGrA1fnUC0e979Md2f3UnQh4TsmeqnV
|
||||||
|
|
||||||
|
SET default_transaction_read_only = off;
|
||||||
|
|
||||||
|
SET client_encoding = 'UTF8';
|
||||||
|
SET standard_conforming_strings = on;
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Roles
|
||||||
|
--
|
||||||
|
|
||||||
|
CREATE ROLE ory;
|
||||||
|
ALTER ROLE ory WITH SUPERUSER INHERIT CREATEROLE CREATEDB LOGIN REPLICATION BYPASSRLS PASSWORD 'SCRAM-SHA-256$4096:MsUfyYDDHLEuU7R26Goauw==$VF6QHNq8fhkEWH4ZAM9daFbYrd6BzTyrg7ovbcPEZig=:4CpeffAwyfHv1hJjEvVj1XI2X6KRASciYL9TdXnoVSY=';
|
||||||
|
|
||||||
|
--
|
||||||
|
-- User Configurations
|
||||||
|
--
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
\unrestrict yrvKzJaekZ6C4tG4mnnuA9bxZKiekltyhGrA1fnUC0e979Md2f3UnQh4TsmeqnV
|
||||||
|
|
||||||
|
--
|
||||||
|
-- PostgreSQL database cluster dump complete
|
||||||
|
--
|
||||||
|
|
||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,11 @@
|
|||||||
|
e821774288e954f12201c06f370f5948a197dbf34d9b188a349cedd98f3eb745 ./manifest.json
|
||||||
|
8bffd96eac628997bb8b3f0d35508075521341e59d11ba16ea696f3c1e3ed315 ./postgres/baron.dump
|
||||||
|
1381aa12f1fb4265ac5e138e785a4ee6c97be2715cb0889d11855a78c027e44e ./postgres/globals.sql
|
||||||
|
7f6311aa3ba5eedd4cde688361204d42b6c933fdfd86b5bc32a2ab6256149784 ./postgres/ory_hydra.dump
|
||||||
|
7794fd834b68e2fbc4c3380724150ea0b721957f1bec0e7b63964b4f905d0179 ./postgres/ory_keto.dump
|
||||||
|
345606e4ba6e7eef649ccc36320d79df8a55d3903f1a9aeb090d699484a998e6 ./postgres/ory_kratos.dump
|
||||||
|
b835af54b4fc0673d9bfa77f33e8ef7db1f76a3e4de11c6c6323615030362d9a ./reports/backup-report.md
|
||||||
|
9f179b47b977cb983e4f69e58c9833b8ea66f18e9bab424ba2bc3409ae4071e4 ./reports/baron-postgres-row-counts.txt
|
||||||
|
bcd5721f029b961d97ef892619fa2a2a09830421ae3db443f8cecdb60986c2c4 ./reports/ory_hydra-row-counts.txt
|
||||||
|
2554588c3db620f3909ad8078cb66d5da6cc7937a4edac2303c5371ae286dc92 ./reports/ory_keto-row-counts.txt
|
||||||
|
0ac6b9ccef5eddd313866dce5bd1d8918d00f7f0c491bf8222dda35da20916d4 ./reports/ory_kratos-row-counts.txt
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
{
|
||||||
|
"format_version": "1",
|
||||||
|
"created_at": "2026-06-08T23:31:17Z",
|
||||||
|
"git_commit": "aa2848c3b6ce",
|
||||||
|
"mode": "maintenance",
|
||||||
|
"environment_scope": "same-env-only",
|
||||||
|
"services": ["postgres", "ory-postgres"],
|
||||||
|
"restore_policy": {
|
||||||
|
"requires_empty_target": true,
|
||||||
|
"requires_confirmation": "baron-sso",
|
||||||
|
"auto_run_migrations": false,
|
||||||
|
"works_relay_auto_resume": false
|
||||||
|
}
|
||||||
|
}
|
||||||
Binary file not shown.
@@ -0,0 +1,35 @@
|
|||||||
|
--
|
||||||
|
-- PostgreSQL database cluster dump
|
||||||
|
--
|
||||||
|
|
||||||
|
\restrict YNM8RQaLG0dfEEoUoHaBy7DQvjAb6BrtCCTSFdfauPgDYEdEZSsWvWVUjFc0aHA
|
||||||
|
|
||||||
|
SET default_transaction_read_only = off;
|
||||||
|
|
||||||
|
SET client_encoding = 'UTF8';
|
||||||
|
SET standard_conforming_strings = on;
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Roles
|
||||||
|
--
|
||||||
|
|
||||||
|
CREATE ROLE ory;
|
||||||
|
ALTER ROLE ory WITH SUPERUSER INHERIT CREATEROLE CREATEDB LOGIN REPLICATION BYPASSRLS PASSWORD 'SCRAM-SHA-256$4096:MsUfyYDDHLEuU7R26Goauw==$VF6QHNq8fhkEWH4ZAM9daFbYrd6BzTyrg7ovbcPEZig=:4CpeffAwyfHv1hJjEvVj1XI2X6KRASciYL9TdXnoVSY=';
|
||||||
|
|
||||||
|
--
|
||||||
|
-- User Configurations
|
||||||
|
--
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
\unrestrict YNM8RQaLG0dfEEoUoHaBy7DQvjAb6BrtCCTSFdfauPgDYEdEZSsWvWVUjFc0aHA
|
||||||
|
|
||||||
|
--
|
||||||
|
-- PostgreSQL database cluster dump complete
|
||||||
|
--
|
||||||
|
|
||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
devfront/e2e-evidence/tenant-access-allowed-tenant-added.png
Normal file
BIN
devfront/e2e-evidence/tenant-access-allowed-tenant-added.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 295 KiB |
BIN
devfront/e2e-evidence/tenant-access-allowed-tenant-deleted.png
Normal file
BIN
devfront/e2e-evidence/tenant-access-allowed-tenant-deleted.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 299 KiB |
@@ -501,6 +501,67 @@ test("org chart places multi-tenant users only on leaf memberships without dupli
|
|||||||
await expect(svg.getByText(/^1$/)).toHaveCount(4);
|
await expect(svg.getByText(/^1$/)).toHaveCount(4);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("org chart allows a user in a hanmac-family descendant tenant", async ({
|
||||||
|
page,
|
||||||
|
}) => {
|
||||||
|
await page.addInitScript(() => {
|
||||||
|
window.localStorage.setItem("playwright_auth_bypass", "1");
|
||||||
|
window.localStorage.setItem("dev_tenant_id", "saman-id");
|
||||||
|
});
|
||||||
|
|
||||||
|
await page.route("**/api/v1/admin/orgchart/snapshot**", async (route) => {
|
||||||
|
expect(route.request().headers()["x-tenant-id"]).toBe("saman-id");
|
||||||
|
await route.fulfill({
|
||||||
|
contentType: "application/json",
|
||||||
|
body: JSON.stringify({
|
||||||
|
tenants: [
|
||||||
|
{
|
||||||
|
...tenant("hanmac-family-id", "한맥가족", "hanmac-family"),
|
||||||
|
type: "COMPANY_GROUP",
|
||||||
|
},
|
||||||
|
tenant("saman-id", "삼안", "saman", "hanmac-family-id", "COMPANY"),
|
||||||
|
tenant("saman-platform-id", "플랫폼팀", "saman-platform", "saman-id"),
|
||||||
|
],
|
||||||
|
users: [
|
||||||
|
{
|
||||||
|
...user("u-saman", "Saman Descendant User", "saman-platform"),
|
||||||
|
tenantSlug: "saman",
|
||||||
|
tenant: tenant(
|
||||||
|
"saman-id",
|
||||||
|
"삼안",
|
||||||
|
"saman",
|
||||||
|
"hanmac-family-id",
|
||||||
|
"COMPANY",
|
||||||
|
),
|
||||||
|
joinedTenants: [
|
||||||
|
tenant(
|
||||||
|
"saman-platform-id",
|
||||||
|
"플랫폼팀",
|
||||||
|
"saman-platform",
|
||||||
|
"saman-id",
|
||||||
|
),
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
cache: { source: "redis", hit: true },
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
await page.goto("/chart");
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
page.getByText("조직도를 불러올 수 없거나 만료된 링크입니다."),
|
||||||
|
).toHaveCount(0);
|
||||||
|
await expect(
|
||||||
|
page.getByRole("button", { name: "조직: 한맥가족" }),
|
||||||
|
).toBeVisible();
|
||||||
|
const svg = page.locator('[data-testid="orgchart-vector-svg"]');
|
||||||
|
await expect(svg.getByText("한맥가족", { exact: true })).toBeVisible();
|
||||||
|
await expect(svg.getByText("삼안", { exact: true })).toBeVisible();
|
||||||
|
await expect(svg.getByText(/Saman Descendant User/)).toBeVisible();
|
||||||
|
});
|
||||||
|
|
||||||
test("org chart places GPDTDC representative users on visible leaf appointments", async ({
|
test("org chart places GPDTDC representative users on visible leaf appointments", async ({
|
||||||
page,
|
page,
|
||||||
}) => {
|
}) => {
|
||||||
|
|||||||
1887
saman_works_users.CSV
Normal file
1887
saman_works_users.CSV
Normal file
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user