173 lines
6.7 KiB
Go
173 lines
6.7 KiB
Go
package test
|
|
|
|
import (
|
|
"RenewCMS/internal/domain/role"
|
|
"RenewCMS/internal/domain/user"
|
|
"RenewCMS/internal/infrastructure/useCases"
|
|
"errors"
|
|
"testing"
|
|
)
|
|
|
|
func missingKeyOf(t *testing.T, err error) string {
|
|
t.Helper()
|
|
var missing role.MissingPermissionError
|
|
if !errors.As(err, &missing) {
|
|
t.Fatalf("expected a MissingPermissionError, got %v", err)
|
|
}
|
|
return missing.Key
|
|
}
|
|
|
|
func TestCheckPermissionUseCase(t *testing.T) {
|
|
newCheck := func() (*useCases.CheckPermissionUseCase, *mockRoleRepo) {
|
|
roles := newSeededMockRoleRepo()
|
|
return useCases.NewCheckPermissionUseCase(roles), roles
|
|
}
|
|
owner := user.User{ID: 3, RoleID: ownerRoleID}
|
|
admin := user.User{ID: 5, RoleID: adminRoleID}
|
|
writer := user.User{ID: 9, RoleID: writerRoleID}
|
|
|
|
t.Run("Require passes for held keys and refuses the others", func(t *testing.T) {
|
|
check, _ := newCheck()
|
|
cases := []struct {
|
|
name string
|
|
account user.User
|
|
key string
|
|
allowed bool
|
|
}{
|
|
{"owner holds an owner-only key", owner, "system.apply_updates", true},
|
|
{"admin lacks an owner-only key", admin, "system.apply_updates", false},
|
|
{"admin holds users.invite", admin, "users.invite", true},
|
|
{"writer lacks users.invite", writer, "users.invite", false},
|
|
{"writer holds schema.view", writer, "schema.view", true},
|
|
{"writer holds a scoped key at own", writer, "activity.view", true},
|
|
{"an unknown key is refused", owner, "no.such_key", false},
|
|
}
|
|
for _, c := range cases {
|
|
t.Run(c.name, func(t *testing.T) {
|
|
_, err := check.Require(c.account, c.key)
|
|
if c.allowed && err != nil {
|
|
t.Fatalf("expected pass, got %v", err)
|
|
}
|
|
if !c.allowed {
|
|
if !errors.Is(err, role.ErrMissingPermission) || missingKeyOf(t, err) != c.key {
|
|
t.Fatalf("expected missing permission for %s, got %v", c.key, err)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
})
|
|
|
|
t.Run("RequireAll refuses a scoped key held only at own", func(t *testing.T) {
|
|
check, _ := newCheck()
|
|
if _, err := check.RequireAll(writer, "activity.view"); !errors.Is(err, role.ErrMissingPermission) {
|
|
t.Fatalf("expected refusal, got %v", err)
|
|
}
|
|
if _, err := check.RequireAll(admin, "activity.view"); err != nil {
|
|
t.Fatalf("expected admin to pass, got %v", err)
|
|
}
|
|
})
|
|
|
|
t.Run("Require returns the resolved grants", func(t *testing.T) {
|
|
check, _ := newCheck()
|
|
grants, err := check.Require(writer, "authors.view")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if grants.Scope("authors.edit") != role.ScopeOwn || grants.Scope("users.view") != role.ScopeNone {
|
|
t.Errorf("unexpected grants %v", grants)
|
|
}
|
|
})
|
|
|
|
t.Run("A custom role is judged by its grants only, owner-only keys included", func(t *testing.T) {
|
|
check, roles := newCheck()
|
|
custom, _ := roles.Create(role.Role{Name: "Hand made", Version: 1, Grants: []role.Grant{{Key: "system.apply_updates", Scope: role.ScopeAll}}})
|
|
account := user.User{ID: 20, RoleID: custom.ID}
|
|
if _, err := check.Require(account, "system.apply_updates"); err != nil {
|
|
t.Errorf("expected the hand-placed owner-only key to be honoured, got %v", err)
|
|
}
|
|
if _, err := check.Require(account, "schema.view"); !errors.Is(err, role.ErrMissingPermission) {
|
|
t.Errorf("expected no implied keys, got %v", err)
|
|
}
|
|
})
|
|
|
|
t.Run("An unresolvable role is refused as a missing permission", func(t *testing.T) {
|
|
check, _ := newCheck()
|
|
for name, account := range map[string]user.User{
|
|
"unknown role_id": {ID: 13, RoleID: 99},
|
|
"empty legacy and no id": {ID: 14, Role: ""},
|
|
"editor legacy and no id": {ID: 15, Role: "editor"},
|
|
"legacy owner string ignored when role_id names Writer": {ID: 16, Role: user.RoleOwner, RoleID: writerRoleID},
|
|
} {
|
|
_, err := check.Require(account, "users.invite")
|
|
if !errors.Is(err, role.ErrMissingPermission) || missingKeyOf(t, err) != "users.invite" {
|
|
t.Errorf("%s: got %v", name, err)
|
|
}
|
|
}
|
|
})
|
|
|
|
t.Run("The legacy string resolves only owner and admin when there is no role_id", func(t *testing.T) {
|
|
check, _ := newCheck()
|
|
if _, err := check.Require(user.User{ID: 17, Role: user.RoleOwner}, "system.apply_updates"); err != nil {
|
|
t.Errorf("legacy owner: %v", err)
|
|
}
|
|
if _, err := check.Require(user.User{ID: 18, Role: user.RoleAdmin}, "system.apply_updates"); !errors.Is(err, role.ErrMissingPermission) {
|
|
t.Errorf("legacy admin should lack owner-only keys, got %v", err)
|
|
}
|
|
})
|
|
|
|
t.Run("A repository failure is not a missing permission", func(t *testing.T) {
|
|
check, roles := newCheck()
|
|
roles.fail.failNext("Get")
|
|
_, err := check.Require(owner, "users.view")
|
|
if err == nil || errors.Is(err, role.ErrMissingPermission) {
|
|
t.Fatalf("expected a plain failure, got %v", err)
|
|
}
|
|
roles.fail.failNext("ListGrants")
|
|
_, err = check.Require(owner, "users.view")
|
|
if err == nil || errors.Is(err, role.ErrMissingPermission) {
|
|
t.Fatalf("expected a plain failure, got %v", err)
|
|
}
|
|
})
|
|
|
|
t.Run("Grant changes apply to the next call with no cache", func(t *testing.T) {
|
|
check, roles := newCheck()
|
|
if _, err := check.Require(writer, "settings.view"); !errors.Is(err, role.ErrMissingPermission) {
|
|
t.Fatalf("expected refusal first, got %v", err)
|
|
}
|
|
stored := roles.roles[writerRoleID]
|
|
stored.Grants = append(stored.Grants, role.Grant{Key: "settings.view", Scope: role.ScopeAll})
|
|
roles.roles[writerRoleID] = stored
|
|
if _, err := check.Require(writer, "settings.view"); err != nil {
|
|
t.Fatalf("expected pass after the edit, got %v", err)
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestGrantSet(t *testing.T) {
|
|
t.Run("an absent key is none and a nil set holds nothing", func(t *testing.T) {
|
|
var empty role.GrantSet
|
|
if empty.Scope("users.view") != role.ScopeNone || empty.Holds("users.view") || empty.HoldsAll("users.view") {
|
|
t.Error("a nil set must hold nothing")
|
|
}
|
|
})
|
|
t.Run("Holds is true for own and all, HoldsAll only for all", func(t *testing.T) {
|
|
set := role.NewGrantSet([]role.Grant{{Key: "a", Scope: role.ScopeOwn}, {Key: "b", Scope: role.ScopeAll}, {Key: "c", Scope: role.ScopeNone}})
|
|
if !set.Holds("a") || set.HoldsAll("a") || !set.Holds("b") || !set.HoldsAll("b") || set.Holds("c") {
|
|
t.Errorf("unexpected %v", set)
|
|
}
|
|
})
|
|
t.Run("MissingPermissionError matches the sentinel and carries the key", func(t *testing.T) {
|
|
err := error(role.MissingPermissionError{Key: "users.invite"})
|
|
if !errors.Is(err, role.ErrMissingPermission) || missingKeyOf(t, err) != "users.invite" {
|
|
t.Errorf("unexpected %v", err)
|
|
}
|
|
})
|
|
}
|
|
|
|
var (
|
|
activityViewAllGrants = role.NewGrantSet([]role.Grant{{Key: role.KeyActivityView, Scope: role.ScopeAll}})
|
|
activityViewOwnGrants = role.NewGrantSet([]role.Grant{{Key: role.KeyActivityView, Scope: role.ScopeOwn}})
|
|
authorsEditOwnGrants = role.NewGrantSet([]role.Grant{{Key: role.KeyAuthorsEdit, Scope: role.ScopeOwn}})
|
|
authorsEditAllGrants = role.NewGrantSet([]role.Grant{{Key: role.KeyAuthorsEdit, Scope: role.ScopeAll}})
|
|
)
|