Files
summercms/modules/cabana/token_scopes_test.go
Jakub Zych 91830e506f test(16.3-04): pin deny-by-default contract at every layer
Scope match/Allows tables, route classification for every phase09Routes
entry, SPA plaintext lifetime backstops, and Postgres owner lifecycle.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-10-11 13:17:41 +02:00

171 lines
5.2 KiB
Go

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)
}
})
}
}