package cabana import ( "testing" "git.golem15.com/golem15/summercms/modules/bouncer" ) func TestScopeMatch(t *testing.T) { cases := []struct { name string pattern string required string want int }{ {"exact full same", "a.b.c", "a.b.c", matchFull}, {"exact full via a.b.*", "a.b.*", "a.b.c", matchFull}, {"exact full via a.*", "a.*", "a.b.c", matchFull}, {"exact none a.x.*", "a.x.*", "a.b.c", matchNone}, {"exact none a.b.d", "a.b.d", "a.b.c", matchNone}, {"wild full same", "a.b.*", "a.b.*", matchFull}, {"wild full broader a.*", "a.*", "a.b.*", matchFull}, {"wild partial narrower pattern", "a.b.*", "a.*", matchPartial}, {"wild partial exact narrower", "a.b.c", "a.*", matchPartial}, {"wild none a.x.*", "a.x.*", "a.b.*", matchNone}, {"wild none x.*", "x.*", "a.b.*", matchNone}, {"leading partial exact", "*.manage", "a.manage", matchPartial}, {"leading none wild required", "*.manage", "a.manage.*", matchNone}, {"leading none wild pattern required", "*.manage", "*.manage", matchNone}, {"empty pattern", "", "a.b.c", matchNone}, {"empty required", "a.b.c", "", matchNone}, } for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { if got := scopeMatch(tc.pattern, tc.required); got != tc.want { t.Fatalf("scopeMatch(%q, %q) = %d, want %d", tc.pattern, tc.required, got, tc.want) } }) } } func TestAllowsScope(t *testing.T) { dev := &bouncer.Principal{ ID: 1, Backend: true, PermissionGrants: map[string]bool{ "acme.demo.access": true, "acme.demo.reports": true, }, } super := &bouncer.Principal{ID: 2, Backend: true, IsSuperuser: true} cases := []struct { name string p *bouncer.Principal scope *bouncer.ScopeLimit required []string want bool }{ { name: "JWT nil scope keeps empty-list shortcut", p: dev, scope: nil, required: nil, want: true, }, { name: "JWT nil scope permission grant", p: dev, scope: nil, required: []string{"acme.demo.access"}, want: true, }, { name: "JWT nil scope missing grant", p: dev, scope: nil, required: []string{"acme.other.x"}, want: false, }, { name: "JWT superuser empty list", p: super, scope: nil, required: nil, want: true, }, { name: "full * every method empty list", p: dev, scope: &bouncer.ScopeLimit{Full: true}, required: nil, want: true, }, { name: "full * permission as owner", p: dev, scope: &bouncer.ScopeLimit{Full: true}, required: []string{"acme.demo.access"}, want: true, }, { name: "full admin:read empty list", p: dev, scope: &bouncer.ScopeLimit{Full: true}, required: nil, want: true, }, { name: "plugin scope with owner grant", p: dev, scope: &bouncer.ScopeLimit{Scopes: [][]string{{"acme.demo.*"}}}, required: []string{"acme.demo.access"}, want: true, }, { name: "plugin scope without owner grant", p: &bouncer.Principal{ ID: 3, Backend: true, PermissionGrants: map[string]bool{"acme.other.x": true}, }, scope: &bouncer.ScopeLimit{Scopes: [][]string{{"acme.demo.*"}}}, required: []string{"acme.demo.access"}, want: false, }, { name: "superuser with scoped token limited", p: super, scope: &bouncer.ScopeLimit{Scopes: [][]string{{"acme.demo.reports"}}}, required: []string{"acme.demo.access"}, want: false, }, { name: "superuser scoped token matching scope code", p: super, scope: &bouncer.ScopeLimit{Scopes: [][]string{{"acme.demo.reports"}}}, required: []string{"acme.demo.reports"}, want: true, }, { name: "empty requirement list refused unless full", p: dev, scope: &bouncer.ScopeLimit{Scopes: [][]string{{"acme.demo.*"}}}, required: nil, want: false, }, { name: "multi-code literal: one scope must match every code", p: dev, scope: &bouncer.ScopeLimit{Scopes: [][]string{{"acme.demo.*"}}}, required: []string{"acme.demo.access", "acme.demo.reports"}, want: true, }, { name: "multi-code literal: two scopes each matching part fails", p: dev, scope: &bouncer.ScopeLimit{Scopes: [][]string{ {"acme.demo.access"}, {"acme.demo.reports"}, }}, required: []string{"acme.demo.access", "acme.demo.reports"}, want: false, }, { name: "wildcard requirement broader pattern partial via ownerHas(pattern)", p: &bouncer.Principal{ ID: 4, Backend: true, PermissionGrants: map[string]bool{"acme.demo.*": true}, }, scope: &bouncer.ScopeLimit{Scopes: [][]string{{"acme.demo.access"}}}, required: []string{"acme.demo.*"}, want: true, }, { name: "wildcard requirement equal pattern full", p: dev, scope: &bouncer.ScopeLimit{Scopes: [][]string{{"acme.demo.*"}}}, required: []string{"acme.demo.*"}, want: true, }, { name: "wildcard requirement narrower pattern none for Allows owner side", p: &bouncer.Principal{ ID: 5, Backend: true, PermissionGrants: map[string]bool{"acme.demo.access": true}, }, scope: &bouncer.ScopeLimit{Scopes: [][]string{{"acme.demo.access.extra"}}}, required: []string{"acme.demo.*"}, want: false, }, } for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { p := *tc.p p.Scope = tc.scope if got := Allows(&p, tc.required); got != tc.want { t.Fatalf("Allows = %v, want %v", got, tc.want) } }) } }