- classes.FrontendPermissionCatalog: the seeded rows, then codes activated plugins declare through pact.HasFrontendPermissions (seeded row wins); both permission editors offer it - classes.MCPUserGuard (guard "user"): reads user.api_token, user.scope:* and user.permission:*, allows with the route's checks (session scopes, group grants, every code), dispatches with an in-memory token credential, resolves usr_ personal tokens; registered in *pact.MCPGuards and published as pact.MCPPersonalTokenResolver - GET /_user/api/v1/token/me (user.api_token, user.scope:read) and the MCP tool me (user_me) - pact.MCPSubjectChanged on token revoke, Users saves and User Groups saves and deletes
113 lines
4.2 KiB
Go
113 lines
4.2 KiB
Go
package user
|
|
|
|
import (
|
|
"net/http"
|
|
"testing"
|
|
|
|
"git.golem15.com/golem15/summercms/modules/backpack"
|
|
"git.golem15.com/golem15/summercms/modules/pact"
|
|
"git.golem15.com/golem15/summercms/modules/party"
|
|
)
|
|
|
|
// The frontend permission codes the acmeBlogPermissions test plugin declares.
|
|
const (
|
|
acmeBlogManage = "acme.blog.manage"
|
|
// acmeBlogSeeded is also a seeded row of golem15_user_frontend_permissions
|
|
// in the smoke, with another label: the seeded row wins.
|
|
acmeBlogSeeded = "acme.blog.seeded"
|
|
)
|
|
|
|
// acmeBlogPermissions is a plugin that only declares frontend permission
|
|
// codes (pact.HasFrontendPermissions). It is registered for the test binary
|
|
// and activated only by the tests that name it.
|
|
type acmeBlogPermissions struct{}
|
|
|
|
func (acmeBlogPermissions) ID() string { return "acme.blog" }
|
|
func (acmeBlogPermissions) Requires() []string { return nil }
|
|
func (acmeBlogPermissions) Register(*backpack.App) error { return nil }
|
|
func (acmeBlogPermissions) Boot(*backpack.App) error { return nil }
|
|
|
|
func (acmeBlogPermissions) FrontendPermissions() []pact.FrontendPermission {
|
|
return []pact.FrontendPermission{
|
|
{Code: acmeBlogManage, Label: "Manage blog posts", Tab: "Blog", Comment: "Create, edit and delete posts."},
|
|
{Code: acmeBlogSeeded, Label: "Plugin label", Tab: "Blog"},
|
|
{Code: acmeBlogManage, Label: "Declared twice", Tab: "Blog"},
|
|
}
|
|
}
|
|
|
|
func init() { party.Register(acmeBlogPermissions{}) }
|
|
|
|
type offeredPermission struct {
|
|
Code string `json:"code"`
|
|
Label string `json:"label"`
|
|
Tab string `json:"tab"`
|
|
Comment string `json:"comment"`
|
|
}
|
|
|
|
// permissionOptions reads the options of the permissions field of a form
|
|
// schema.
|
|
func (e *adminEnv) permissionOptions(t *testing.T, token, path string) []offeredPermission {
|
|
t.Helper()
|
|
var form struct {
|
|
Fields []struct {
|
|
Name string `json:"name"`
|
|
Options []offeredPermission `json:"permissionOptions"`
|
|
} `json:"fields"`
|
|
}
|
|
adminInto(t, e.expect(t, http.StatusOK, http.MethodGet, adminAPI(path+"/schema/form"), token, nil), &form)
|
|
for _, field := range form.Fields {
|
|
if field.Name == "permissions" {
|
|
return field.Options
|
|
}
|
|
}
|
|
t.Fatalf("%s has no permissions field", path)
|
|
return nil
|
|
}
|
|
|
|
// TestFrontendPermissionCatalogSmoke: both permission editors offer the
|
|
// seeded rows first, then the codes an activated plugin declares; a seeded
|
|
// row wins over a declared code and a code is offered once.
|
|
func TestFrontendPermissionCatalogSmoke(t *testing.T) {
|
|
env := newAdminEnvWith(t, []string{"acme.blog"})
|
|
if err := env.gdb.Exec(`INSERT INTO golem15_user_frontend_permissions (code, label, tab) VALUES (?, 'Seeded label', 'Blog') ON CONFLICT (code) DO NOTHING`, acmeBlogSeeded).Error; err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
var seeded int64
|
|
if err := env.gdb.Raw(`SELECT COUNT(*) FROM golem15_user_frontend_permissions`).Scan(&seeded).Error; err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
token := env.admin(t, permAccessUsers, permAccessGroups)
|
|
for _, path := range []string{adminUsersPath, adminGroupsPath} {
|
|
options := env.permissionOptions(t, token, path)
|
|
if int64(len(options)) != seeded+1 {
|
|
t.Fatalf("%s offers %d options, want the %d seeded rows and one declared code: %+v", path, len(options), seeded, options)
|
|
}
|
|
last := options[len(options)-1]
|
|
if last != (offeredPermission{Code: acmeBlogManage, Label: "Manage blog posts", Tab: "Blog", Comment: "Create, edit and delete posts."}) {
|
|
t.Fatalf("%s: last option = %+v, want the declared acme.blog.manage", path, last)
|
|
}
|
|
found := 0
|
|
for _, option := range options[:len(options)-1] {
|
|
if option.Code == acmeBlogManage {
|
|
t.Fatalf("%s offers %s twice", path, acmeBlogManage)
|
|
}
|
|
if option.Code == acmeBlogSeeded {
|
|
found++
|
|
if option.Label != "Seeded label" {
|
|
t.Fatalf("%s: duplicate code label = %q, want the seeded row's", path, option.Label)
|
|
}
|
|
}
|
|
}
|
|
if found != 1 {
|
|
t.Fatalf("%s offers %s %d times", path, acmeBlogSeeded, found)
|
|
}
|
|
}
|
|
|
|
// Without the declaring plugin the editors offer the seeded rows only.
|
|
plain := newAdminEnv(t)
|
|
plainToken := plain.admin(t, permAccessUsers)
|
|
if options := plain.permissionOptions(t, plainToken, adminUsersPath); int64(len(options)) != seeded {
|
|
t.Fatalf("without acme.blog: %d options, want %d", len(options), seeded)
|
|
}
|
|
}
|