fix(09): WR-17 merge an admin's own permissions over the role's, honouring denies

This commit is contained in:
Jakub Zych
2026-10-01 21:33:07 +02:00
parent 629fac4d29
commit 8479defe53
4 changed files with 100 additions and 1 deletions

View File

@@ -630,3 +630,65 @@ func TestAdminLogoutRevokesExpiredRefreshableToken(t *testing.T) {
}
}
}
// TestBackendUserPermissionsOverrideRole pins WR-17: an administrator's own
// backend_users.permissions are merged over the role's the way Winter merges
// them. A user-level -1 removes a permission the role grants and a user-level 1
// adds one the role lacks, so a row copied from WinterCMS keeps its denies.
func TestBackendUserPermissionsOverrideRole(t *testing.T) {
gdb := adminGorm(t)
role := cabana.BackendUserRole{Name: "WR-17 role", Code: "wr17", Permissions: `{"wr17.a":1,"wr17.b":1,"wr17.wild.*":1}`}
if err := gdb.Create(&role).Error; err != nil {
t.Fatal(err)
}
cases := []struct {
name string
perms string
roleP *uint
want map[string]bool
}{
{name: "no user level permissions", perms: "", roleP: &role.ID, want: map[string]bool{"wr17.a": true, "wr17.b": true, "wr17.wild.*": true}},
{name: "empty object", perms: "{}", roleP: &role.ID, want: map[string]bool{"wr17.a": true, "wr17.b": true, "wr17.wild.*": true}},
{name: "deny removes a role grant and a grant adds one", perms: `{"wr17.b":-1,"wr17.c":1}`, roleP: &role.ID, want: map[string]bool{"wr17.a": true, "wr17.c": true, "wr17.wild.*": true}},
{name: "zero is not a grant", perms: `{"wr17.a":0}`, roleP: &role.ID, want: map[string]bool{"wr17.b": true, "wr17.wild.*": true}},
{name: "deny of a wildcard grant", perms: `{"wr17.wild.*":-1}`, roleP: &role.ID, want: map[string]bool{"wr17.a": true, "wr17.b": true}},
{name: "no role", perms: `{"wr17.only":1}`, roleP: nil, want: map[string]bool{"wr17.only": true}},
{name: "malformed JSON is ignored", perms: `not json`, roleP: &role.ID, want: map[string]bool{"wr17.a": true, "wr17.b": true, "wr17.wild.*": true}},
}
for i, tc := range cases {
hash, err := bouncer.HashPassword(4, "x")
if err != nil {
t.Fatal(err)
}
user := cabana.BackendUser{
Login: "wr17-" + strconv.Itoa(i), Email: "wr17-" + strconv.Itoa(i) + "@example.test", Password: hash,
IsActivated: true, RoleID: tc.roleP, Permissions: tc.perms,
}
if err := gdb.Create(&user).Error; err != nil {
t.Fatal(err)
}
principal, err := (cabana.BackendUsers{DB: gdb}).FindByID(context.Background(), user.ID)
if err != nil || principal == nil {
t.Fatalf("%s: FindByID = %v, %v", tc.name, principal, err)
}
got := map[string]bool{}
for code, on := range principal.PermissionGrants {
if on {
got[code] = true
}
}
if len(got) != len(tc.want) {
t.Fatalf("%s: grants = %v, want %v", tc.name, got, tc.want)
}
for code := range tc.want {
if !got[code] {
t.Fatalf("%s: grants = %v, want %v", tc.name, got, tc.want)
}
}
}
// The check the guard feeds: the denied code no longer passes Allows.
denied := &bouncer.Principal{Backend: true, PermissionGrants: map[string]bool{"wr17.a": true}}
if cabana.Allows(denied, []string{"wr17.b"}) {
t.Fatal("a denied code passed Allows")
}
}