Skip to content

Commit e76cb26

Browse files
committed
Additional tests
1 parent 5f8d5db commit e76cb26

1 file changed

Lines changed: 179 additions & 0 deletions

File tree

server/gql/permission_resolver_test.go

Lines changed: 179 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -492,6 +492,185 @@ func TestPermissionResolver_Users(t *testing.T) {
492492
})
493493
}
494494

495+
// TestPermissionResolver_Filtering verifies that traversing relationships
496+
// (tenant→groups, group→tenant→groups, permissions→children, group→feeds)
497+
// never widens the result set beyond what the querying user is authorized to see.
498+
func TestPermissionResolver_Filtering(t *testing.T) {
499+
// Hierarchy:
500+
// tl-tenant
501+
// ├── CT-group → feed CT (public)
502+
// ├── BA-group → feed BA (public)
503+
// └── HA-group → feed HA (public), feed EG (non-public)
504+
// restricted-tenant
505+
// └── EX-group (no feeds)
506+
//
507+
// Users:
508+
// "full-user" : admin of tl-tenant → sees all groups/feeds under tl-tenant
509+
// "partial-user" : viewer of CT-group only → sees only CT-group
510+
// "multi-user" : viewer of CT-group + editor of EX-group → spans two tenants
511+
// "nobody" : no tuples → sees nothing
512+
filterTuples := []authz.TupleKey{
513+
// tl-tenant groups
514+
{Subject: authz.NewEntityKey(authz.TenantType, "tl-tenant"), Object: authz.NewEntityKey(authz.GroupType, "CT-group"), Relation: authz.ParentRelation},
515+
{Subject: authz.NewEntityKey(authz.TenantType, "tl-tenant"), Object: authz.NewEntityKey(authz.GroupType, "BA-group"), Relation: authz.ParentRelation},
516+
{Subject: authz.NewEntityKey(authz.TenantType, "tl-tenant"), Object: authz.NewEntityKey(authz.GroupType, "HA-group"), Relation: authz.ParentRelation},
517+
// restricted-tenant groups
518+
{Subject: authz.NewEntityKey(authz.TenantType, "restricted-tenant"), Object: authz.NewEntityKey(authz.GroupType, "EX-group"), Relation: authz.ParentRelation},
519+
// Feed assignments
520+
{Subject: authz.NewEntityKey(authz.GroupType, "CT-group"), Object: authz.NewEntityKey(authz.FeedType, "CT"), Relation: authz.ParentRelation},
521+
{Subject: authz.NewEntityKey(authz.GroupType, "BA-group"), Object: authz.NewEntityKey(authz.FeedType, "BA"), Relation: authz.ParentRelation},
522+
{Subject: authz.NewEntityKey(authz.GroupType, "HA-group"), Object: authz.NewEntityKey(authz.FeedType, "HA"), Relation: authz.ParentRelation},
523+
{Subject: authz.NewEntityKey(authz.GroupType, "HA-group"), Object: authz.NewEntityKey(authz.FeedType, "EG"), Relation: authz.ParentRelation},
524+
// full-user: admin of tl-tenant (inherits access to all children)
525+
{Subject: authz.NewEntityKey(authz.UserType, "full-user"), Object: authz.NewEntityKey(authz.TenantType, "tl-tenant"), Relation: authz.AdminRelation},
526+
// partial-user: viewer of CT-group only, member of tl-tenant
527+
{Subject: authz.NewEntityKey(authz.UserType, "partial-user"), Object: authz.NewEntityKey(authz.TenantType, "tl-tenant"), Relation: authz.MemberRelation},
528+
{Subject: authz.NewEntityKey(authz.UserType, "partial-user"), Object: authz.NewEntityKey(authz.GroupType, "CT-group"), Relation: authz.ViewerRelation},
529+
// multi-user: viewer of CT-group + editor of EX-group (spans two tenants)
530+
{Subject: authz.NewEntityKey(authz.UserType, "multi-user"), Object: authz.NewEntityKey(authz.TenantType, "tl-tenant"), Relation: authz.MemberRelation},
531+
{Subject: authz.NewEntityKey(authz.UserType, "multi-user"), Object: authz.NewEntityKey(authz.GroupType, "CT-group"), Relation: authz.ViewerRelation},
532+
{Subject: authz.NewEntityKey(authz.UserType, "multi-user"), Object: authz.NewEntityKey(authz.TenantType, "restricted-tenant"), Relation: authz.MemberRelation},
533+
{Subject: authz.NewEntityKey(authz.UserType, "multi-user"), Object: authz.NewEntityKey(authz.GroupType, "EX-group"), Relation: authz.EditorRelation},
534+
}
535+
opts := testconfig.Options{
536+
FGAEndpoint: testutil.FGAServer(t),
537+
FGAModelFile: testdata.Path("server/authz/tls.json"),
538+
FGAModelTuples: filterTuples,
539+
}
540+
cfg := testconfig.Config(t, opts)
541+
542+
// Helper to collect names from a gjson array
543+
names := func(arr []gjson.Result, key string) []string {
544+
var out []string
545+
for _, r := range arr {
546+
out = append(out, r.Get(key).Str)
547+
}
548+
return out
549+
}
550+
551+
t.Run("partial-user tenant groups filtered", func(t *testing.T) {
552+
c := newPermTestClientFromConfig(cfg, "partial-user")
553+
jj := postQuery(t, c, `{ tenants { name groups { name } } }`, nil)
554+
for _, tenant := range gjson.Get(jj, "tenants").Array() {
555+
if tenant.Get("name").Str != "tl-tenant" {
556+
continue
557+
}
558+
groupNames := names(tenant.Get("groups").Array(), "name")
559+
assert.Contains(t, groupNames, "CT-group")
560+
assert.NotContains(t, groupNames, "BA-group", "partial-user should not see BA-group")
561+
assert.NotContains(t, groupNames, "HA-group", "partial-user should not see HA-group")
562+
return
563+
}
564+
t.Fatal("tl-tenant not found")
565+
})
566+
567+
t.Run("partial-user group tenant groups no leak", func(t *testing.T) {
568+
// Traversing group → tenant → groups must not widen results
569+
c := newPermTestClientFromConfig(cfg, "partial-user")
570+
jj := postQuery(t, c, `{ groups { name tenant { name groups { name } } } }`, nil)
571+
groups := gjson.Get(jj, "groups").Array()
572+
assert.Equal(t, 1, len(groups), "partial-user should see exactly 1 group")
573+
assert.Equal(t, "CT-group", groups[0].Get("name").Str)
574+
tenantGroups := names(groups[0].Get("tenant.groups").Array(), "name")
575+
assert.Contains(t, tenantGroups, "CT-group")
576+
assert.NotContains(t, tenantGroups, "BA-group", "traversal should not widen to BA-group")
577+
assert.NotContains(t, tenantGroups, "HA-group", "traversal should not widen to HA-group")
578+
})
579+
580+
t.Run("partial-user permissions children filtered", func(t *testing.T) {
581+
c := newPermTestClientFromConfig(cfg, "partial-user")
582+
jj := postQuery(t, c, `{ tenants { name permissions { children { type name } } } }`, nil)
583+
for _, tenant := range gjson.Get(jj, "tenants").Array() {
584+
if tenant.Get("name").Str != "tl-tenant" {
585+
continue
586+
}
587+
childNames := names(tenant.Get("permissions.children").Array(), "name")
588+
assert.Contains(t, childNames, "CT-group")
589+
assert.NotContains(t, childNames, "BA-group", "permissions children should not include BA-group")
590+
assert.NotContains(t, childNames, "HA-group", "permissions children should not include HA-group")
591+
return
592+
}
593+
t.Fatal("tl-tenant not found")
594+
})
595+
596+
t.Run("partial-user group feeds", func(t *testing.T) {
597+
c := newPermTestClientFromConfig(cfg, "partial-user")
598+
jj := postQuery(t, c, `{ groups { name feeds { onestop_id } } }`, nil)
599+
groups := gjson.Get(jj, "groups").Array()
600+
assert.Equal(t, 1, len(groups))
601+
feedIDs := names(groups[0].Get("feeds").Array(), "onestop_id")
602+
assert.Contains(t, feedIDs, "CT")
603+
assert.NotContains(t, feedIDs, "BA")
604+
})
605+
606+
t.Run("full-user sees all tenant groups", func(t *testing.T) {
607+
c := newPermTestClientFromConfig(cfg, "full-user")
608+
jj := postQuery(t, c, `{ tenants { name groups { name } } }`, nil)
609+
for _, tenant := range gjson.Get(jj, "tenants").Array() {
610+
if tenant.Get("name").Str != "tl-tenant" {
611+
continue
612+
}
613+
groupNames := names(tenant.Get("groups").Array(), "name")
614+
assert.Contains(t, groupNames, "CT-group")
615+
assert.Contains(t, groupNames, "BA-group")
616+
assert.Contains(t, groupNames, "HA-group")
617+
return
618+
}
619+
t.Fatal("tl-tenant not found")
620+
})
621+
622+
t.Run("full-user sees non-public feed via group", func(t *testing.T) {
623+
c := newPermTestClientFromConfig(cfg, "full-user")
624+
jj := postQuery(t, c, `{ groups { name feeds { onestop_id } } }`, nil)
625+
for _, group := range gjson.Get(jj, "groups").Array() {
626+
if group.Get("name").Str != "HA-group" {
627+
continue
628+
}
629+
feedIDs := names(group.Get("feeds").Array(), "onestop_id")
630+
assert.Contains(t, feedIDs, "HA")
631+
assert.Contains(t, feedIDs, "EG", "admin should see non-public feed EG via HA-group")
632+
return
633+
}
634+
t.Fatal("HA-group not found")
635+
})
636+
637+
t.Run("multi-user cross-tenant isolation", func(t *testing.T) {
638+
c := newPermTestClientFromConfig(cfg, "multi-user")
639+
jj := postQuery(t, c, `{ tenants { name groups { name } } }`, nil)
640+
tenants := gjson.Get(jj, "tenants").Array()
641+
assert.GreaterOrEqual(t, len(tenants), 2, "multi-user should see two tenants")
642+
for _, tenant := range tenants {
643+
groupNames := names(tenant.Get("groups").Array(), "name")
644+
switch tenant.Get("name").Str {
645+
case "tl-tenant":
646+
assert.Contains(t, groupNames, "CT-group")
647+
assert.NotContains(t, groupNames, "BA-group", "multi-user should not see BA-group")
648+
assert.NotContains(t, groupNames, "HA-group", "multi-user should not see HA-group")
649+
case "restricted-tenant":
650+
assert.Contains(t, groupNames, "EX-group")
651+
}
652+
}
653+
})
654+
655+
t.Run("multi-user top-level groups", func(t *testing.T) {
656+
c := newPermTestClientFromConfig(cfg, "multi-user")
657+
jj := postQuery(t, c, `{ groups { name } }`, nil)
658+
groupNames := names(gjson.Get(jj, "groups").Array(), "name")
659+
assert.Contains(t, groupNames, "CT-group")
660+
assert.Contains(t, groupNames, "EX-group")
661+
assert.NotContains(t, groupNames, "BA-group")
662+
assert.NotContains(t, groupNames, "HA-group")
663+
})
664+
665+
t.Run("nobody sees nothing", func(t *testing.T) {
666+
c := newPermTestClientFromConfig(cfg, "nobody")
667+
jj := postQuery(t, c, `{ tenants { id } }`, nil)
668+
assert.Equal(t, 0, len(gjson.Get(jj, "tenants").Array()))
669+
jj = postQuery(t, c, `{ groups { id } }`, nil)
670+
assert.Equal(t, 0, len(gjson.Get(jj, "groups").Array()))
671+
})
672+
}
673+
495674
func TestPermissionResolver_NilPermissionManager(t *testing.T) {
496675
srv, _ := NewServer()
497676
cfg := testconfig.Config(t, testconfig.Options{})

0 commit comments

Comments
 (0)