- classes: every admin action for its changed-row count and idempotency, the ban across several throttle rows, the cleanup of a permanent delete - classes: the privileged list and IsPrivilegedMember (D-30), also through a handle that carries conditions - classes: the five-minute boundary of last_seen - models: PermissionSet round trip, the slug table shared with the SPA, Fillable and Rules, fields that never marshal, the group filter value - TestAdminRegistration: permissions, navigation, embedded files and phrase keys in en and pl; the admin secret is required
208 lines
7.6 KiB
Go
208 lines
7.6 KiB
Go
package classes
|
|
|
|
import (
|
|
"os"
|
|
"path/filepath"
|
|
"reflect"
|
|
"testing"
|
|
|
|
"git.golem15.com/golem15/sm-user-plugin/models"
|
|
"git.golem15.com/golem15/summercms/modules/compass"
|
|
)
|
|
|
|
// privilegedConfig opens a config whose golem15.user.privileged_groups is
|
|
// value; a nil value leaves the key out.
|
|
func privilegedConfig(t *testing.T, value any, environ ...string) *compass.Config {
|
|
t.Helper()
|
|
dir := t.TempDir()
|
|
if err := os.WriteFile(filepath.Join(dir, "app.yaml"), []byte("name: privileged\n"), 0o644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
cfg, err := compass.Open(compass.Options{Dir: dir, Environ: append([]string{"SUMMER_ENV=development"}, environ...)})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if value != nil {
|
|
if err := cfg.Set(privilegedGroupsKey, value); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
return cfg
|
|
}
|
|
|
|
// TestPrivilegedGroupCodes pins how the privileged list is read (D-05): the
|
|
// default, an override, blank entries and the comma-separated form of an
|
|
// environment variable.
|
|
func TestPrivilegedGroupCodes(t *testing.T) {
|
|
if got := PrivilegedGroupCodes(nil); !reflect.DeepEqual(got, []string{"admin"}) {
|
|
t.Fatalf("nil config = %v", got)
|
|
}
|
|
for _, tc := range []struct {
|
|
name string
|
|
value any
|
|
want []string
|
|
}{
|
|
{"a missing key", nil, []string{"admin"}},
|
|
{"a list", []string{"admin", "moderator"}, []string{"admin", "moderator"}},
|
|
{"a list of any", []any{"owner", 7, "staff"}, []string{"owner", "staff"}},
|
|
{"blank entries", []string{" admin ", "", " ", "staff"}, []string{"admin", "staff"}},
|
|
{"an empty list", []string{}, []string{}},
|
|
{"a comma-separated string", "admin, staff ,,owner", []string{"admin", "staff", "owner"}},
|
|
{"an empty string", "", []string{}},
|
|
{"a value of another type", 5, []string{}},
|
|
} {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
got := PrivilegedGroupCodes(privilegedConfig(t, tc.value))
|
|
if got == nil || !reflect.DeepEqual(got, tc.want) {
|
|
t.Fatalf("codes = %#v, want %#v", got, tc.want)
|
|
}
|
|
})
|
|
}
|
|
// The key is read on every call: a change applies without a new config.
|
|
cfg := privilegedConfig(t, []string{"first"})
|
|
if got := PrivilegedGroupCodes(cfg); !reflect.DeepEqual(got, []string{"first"}) {
|
|
t.Fatalf("codes = %v", got)
|
|
}
|
|
if err := cfg.Set(privilegedGroupsKey, []string{"second"}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if got := PrivilegedGroupCodes(cfg); !reflect.DeepEqual(got, []string{"second"}) {
|
|
t.Fatalf("codes after the change = %v", got)
|
|
}
|
|
if PermissionManagePrivilegedGroups != "golem15.users.manage_privileged_groups" {
|
|
t.Fatalf("permission code = %q", PermissionManagePrivilegedGroups)
|
|
}
|
|
}
|
|
|
|
// TestIsPrivilegedCode: codes compare case-sensitively and a group without a
|
|
// code is never privileged.
|
|
func TestIsPrivilegedCode(t *testing.T) {
|
|
str := func(s string) *string { return &s }
|
|
codes := []string{"admin", "Staff"}
|
|
for _, tc := range []struct {
|
|
code *string
|
|
want bool
|
|
}{
|
|
{str("admin"), true}, {str("Staff"), true},
|
|
{str("Admin"), false}, {str("ADMIN"), false}, {str("staff"), false},
|
|
{str("admin "), false}, {str(" admin"), false}, {str("administrator"), false},
|
|
{str(""), false}, {nil, false},
|
|
} {
|
|
if got := IsPrivilegedCode(codes, tc.code); got != tc.want {
|
|
label := "<nil>"
|
|
if tc.code != nil {
|
|
label = *tc.code
|
|
}
|
|
t.Errorf("IsPrivilegedCode(%q) = %v, want %v", label, got, tc.want)
|
|
}
|
|
}
|
|
if IsPrivilegedCode(nil, str("admin")) || IsPrivilegedCode([]string{}, str("admin")) {
|
|
t.Error("an empty list has a privileged code")
|
|
}
|
|
// An empty entry in the list never matches a group without a code.
|
|
if IsPrivilegedCode([]string{""}, str("")) || IsPrivilegedCode([]string{""}, nil) {
|
|
t.Error("an empty code is privileged")
|
|
}
|
|
}
|
|
|
|
// TestIsPrivilegedMember covers the membership question behind the takeover
|
|
// guard (D-30) and PrivilegedGroupIDs behind the groups lock (D-07).
|
|
func TestIsPrivilegedMember(t *testing.T) {
|
|
ctx := t.Context()
|
|
db := throttleDB(t)
|
|
str := func(s string) *string { return &s }
|
|
admins := models.UserGroup{Name: "Admins", Code: str("admin")}
|
|
second := models.UserGroup{Name: "Second admins", Code: str("admin2")}
|
|
ordinary := models.UserGroup{Name: "Ordinary", Code: str("ordinary")}
|
|
upper := models.UserGroup{Name: "Upper", Code: str("ADMIN")}
|
|
uncoded := models.UserGroup{Name: "Uncoded"}
|
|
for _, group := range []*models.UserGroup{&admins, &second, &ordinary, &upper, &uncoded} {
|
|
if err := db.Create(group).Error; err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
user := func(label string, groups ...*models.UserGroup) uint {
|
|
t.Helper()
|
|
u := models.User{Email: "privileged-" + label + "@example.test", IsActivated: true}
|
|
if err := db.Omit("Groups").Create(&u).Error; err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
for _, group := range groups {
|
|
if err := db.Create(&models.UsersGroup{UserID: u.ID, UserGroupID: group.ID}).Error; err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
return u.ID
|
|
}
|
|
nobody := user("nobody")
|
|
plain := user("plain", &ordinary)
|
|
codeless := user("codeless", &uncoded, &upper)
|
|
member := user("member", &ordinary, &admins)
|
|
gone := user("gone", &admins)
|
|
if err := db.Exec(`UPDATE users SET deleted_at = NOW() WHERE id = ?`, gone).Error; err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
defaults := PrivilegedGroupCodes(nil)
|
|
for _, tc := range []struct {
|
|
name string
|
|
codes []string
|
|
user uint
|
|
want bool
|
|
}{
|
|
{"a user in no group", defaults, nobody, false},
|
|
{"an ordinary group only", defaults, plain, false},
|
|
{"a group without a code and a case variant", defaults, codeless, false},
|
|
{"a member of a listed group", defaults, member, true},
|
|
{"a deactivated member", defaults, gone, true},
|
|
{"an overridden list without admin", []string{"ordinary"}, gone, false},
|
|
{"an overridden list with the ordinary code", []string{"ordinary"}, plain, true},
|
|
{"an overridden list, a member of both", []string{"ordinary"}, member, true},
|
|
{"a case variant in the list", []string{"ADMIN"}, codeless, true},
|
|
{"an empty list", []string{}, member, false},
|
|
{"a nil list", nil, member, false},
|
|
{"an unknown user", defaults, 987654, false},
|
|
{"the zero user", defaults, 0, false},
|
|
} {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
got, err := IsPrivilegedMember(ctx, db, tc.codes, tc.user)
|
|
if err != nil || got != tc.want {
|
|
t.Fatalf("IsPrivilegedMember = %v %v, want %v", got, err, tc.want)
|
|
}
|
|
})
|
|
}
|
|
if _, err := IsPrivilegedMember(ctx, nil, defaults, member); err == nil {
|
|
t.Fatal("IsPrivilegedMember without a handle")
|
|
}
|
|
// The handle's own conditions do not leak into the query.
|
|
scoped := db.Where("1 = 0")
|
|
if got, err := IsPrivilegedMember(ctx, scoped, defaults, member); err != nil || !got {
|
|
t.Fatalf("with a scoped handle = %v %v", got, err)
|
|
}
|
|
// A failing query is an error, never "not a member".
|
|
cancelled, cancel := contextCancelled(t)
|
|
defer cancel()
|
|
if got, err := IsPrivilegedMember(cancelled, db, defaults, member); err == nil || got {
|
|
t.Fatalf("a failed query answered %v %v", got, err)
|
|
}
|
|
|
|
ids, err := PrivilegedGroupIDs(ctx, db, []string{"admin", "admin2", "missing"})
|
|
if err != nil || !reflect.DeepEqual(ids, []uint{admins.ID, second.ID}) {
|
|
t.Fatalf("PrivilegedGroupIDs = %v %v", ids, err)
|
|
}
|
|
if ids, err = PrivilegedGroupIDs(ctx, scoped, defaults); err != nil || !reflect.DeepEqual(ids, []uint{admins.ID}) {
|
|
t.Fatalf("PrivilegedGroupIDs with a scoped handle = %v %v", ids, err)
|
|
}
|
|
for _, codes := range [][]string{nil, {}, {"nothing"}} {
|
|
if ids, err = PrivilegedGroupIDs(ctx, db, codes); err != nil || ids == nil || len(ids) != 0 {
|
|
t.Fatalf("PrivilegedGroupIDs(%v) = %#v %v", codes, ids, err)
|
|
}
|
|
}
|
|
if _, err = PrivilegedGroupIDs(ctx, nil, defaults); err == nil {
|
|
t.Fatal("PrivilegedGroupIDs without a handle")
|
|
}
|
|
if _, err = PrivilegedGroupIDs(cancelled, db, defaults); err == nil {
|
|
t.Fatal("PrivilegedGroupIDs swallowed a failed query")
|
|
}
|
|
}
|