- pact.HasAccountContributions compiled at Activate; GET /account/contributions
and POST /account/contributions/{name}/actions/{action} behind interactiveOnly
(AccountContributionView, AccountContributionActionView, AccountActionResultView)
- RecordAction.prompt (RecordActionPrompt) for a pact.AdminRecordAction with a
Prompt; the record action route accepts payload only for such an action
(trimmed JSON string, non-empty, capped at max_length) and hands it to Run as
Value through CRUDService.RecordActionWithValue; a prompt without a label
fails boot
- fireSubjectChanged: pact.MCPSubjectChanged for revoked tokens, Administrators
saves that changed role, permissions or superuser, Roles saves that changed
grants, deactivation and deletion
- phrases backend::lang.form.action_prompt_required/_too_long (en, pl)
- smoke tests TestAccountContributionsSmoke and TestRecordActionPromptSmoke;
TestRecordActionPromptRun on Postgres; route inventories, CSRF walk and
OpenAPI conformance cases extended for the two routes
- regenerated admin/openapi/admin.json and admin/src/api/schema.d.ts; cabana
README, docs/backend/{admin-controllers,users-and-permissions}.md
124 lines
5.0 KiB
Go
124 lines
5.0 KiB
Go
package cabana
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"strings"
|
|
|
|
"git.golem15.com/golem15/summercms/modules/bouncer"
|
|
"git.golem15.com/golem15/summercms/modules/pact"
|
|
"git.golem15.com/golem15/summercms/modules/party"
|
|
)
|
|
|
|
// compileActionExtensions merges the record actions plugins add to admin
|
|
// controllers through pact.HasAdminActionExtensions (D-14) into the compiled
|
|
// controllers of reg, in plugin slice (activation) order. A contributed
|
|
// action is appended to the target form's offered record actions and to its
|
|
// RecordActions map, so the show response, the record action route and the
|
|
// permission validation of compileContributions treat it like the owner's.
|
|
// It must run after every controller is compiled and before
|
|
// compileContributions, which validates the actions' permissions. Every
|
|
// failure stops boot: an unknown controller ID, a target without a form
|
|
// preview block (record actions are offered on the preview screen), a name
|
|
// that is not an identifier, a reserved name, a name the target already
|
|
// registers, an action without Run or without a label.
|
|
func compileActionExtensions(reg *Registry, plugins []party.Plugin) error {
|
|
if reg == nil {
|
|
return fmt.Errorf("cabana: registry is nil")
|
|
}
|
|
for _, plugin := range plugins {
|
|
if plugin == nil {
|
|
continue
|
|
}
|
|
src, ok := plugin.(pact.HasAdminActionExtensions)
|
|
if !ok || src == nil {
|
|
continue
|
|
}
|
|
pluginID := plugin.ID()
|
|
for _, ext := range src.AdminActionExtensions() {
|
|
if len(ext.RecordActions) == 0 {
|
|
continue
|
|
}
|
|
cc, ok := reg.byID[ext.Controller]
|
|
if !ok || cc == nil {
|
|
return fmt.Errorf("cabana: plugin %s adds record actions to unknown admin controller %q", pluginID, ext.Controller)
|
|
}
|
|
if cc.Form == nil || cc.Form.preview == nil {
|
|
return fmt.Errorf("cabana: plugin %s adds record actions to admin controller %s, which has no preview block (record actions are offered on the preview screen)", pluginID, ext.Controller)
|
|
}
|
|
for _, action := range ext.RecordActions {
|
|
if err := checkContributedAction(cc, pluginID, ext.Controller, action); err != nil {
|
|
return err
|
|
}
|
|
if ext.Controller == AdministratorsControllerID {
|
|
action.Run = wrapAdministratorsContributedRun(reg, action.Run)
|
|
}
|
|
// A new slice and map: the compiled backing array may be
|
|
// shared with another schema value.
|
|
names := make([]string, 0, len(cc.Form.recordActions)+1)
|
|
names = append(names, cc.Form.recordActions...)
|
|
cc.Form.recordActions = append(names, action.Name)
|
|
actions := make(map[string]pact.AdminRecordAction, len(cc.RecordActions)+1)
|
|
for name, registered := range cc.RecordActions {
|
|
actions[name] = registered
|
|
}
|
|
actions[action.Name] = action
|
|
cc.RecordActions = actions
|
|
if cc.contributed == nil {
|
|
cc.contributed = map[string]string{}
|
|
}
|
|
cc.contributed[action.Name] = pluginID
|
|
}
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// checkContributedAction applies compileRecordActions' checks to one
|
|
// contributed record action, plus a label and a name the target does not
|
|
// register yet (the owner's or an earlier contribution).
|
|
func checkContributedAction(cc *CompiledController, pluginID, controller string, action pact.AdminRecordAction) error {
|
|
prefix := fmt.Sprintf("cabana: plugin %s adds record action %q to admin controller %s", pluginID, action.Name, controller)
|
|
if !identifier(action.Name) {
|
|
return fmt.Errorf("%s: the name is not an identifier", prefix)
|
|
}
|
|
if builtinToolbarActions[action.Name] {
|
|
return fmt.Errorf("%s: the name is reserved (create, delete)", prefix)
|
|
}
|
|
if action.Run == nil {
|
|
return fmt.Errorf("%s: it has no Run function", prefix)
|
|
}
|
|
if strings.TrimSpace(action.Label) == "" {
|
|
return fmt.Errorf("%s: it needs a label", prefix)
|
|
}
|
|
if action.Prompt != nil && strings.TrimSpace(action.Prompt.Label) == "" {
|
|
return fmt.Errorf("%s: its prompt needs a label", prefix)
|
|
}
|
|
if _, taken := cc.RecordActions[action.Name]; taken {
|
|
owner := cc.PluginID
|
|
if contributor, ok := cc.contributed[action.Name]; ok {
|
|
owner = contributor
|
|
}
|
|
return fmt.Errorf("%s: plugin %s already registers a record action with that name", prefix, owner)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// wrapAdministratorsContributedRun runs guardOutranked on the locked
|
|
// Administrators record before the contributor's Run (D-19). Contributions
|
|
// to any other controller are stored unchanged. The wrap is not exported:
|
|
// cabana owns the privilege check.
|
|
func wrapAdministratorsContributedRun(reg *Registry, next func(context.Context, pact.AdminRecordActionInput) (pact.AdminRecordActionResult, error)) func(context.Context, pact.AdminRecordActionInput) (pact.AdminRecordActionResult, error) {
|
|
return func(ctx context.Context, in pact.AdminRecordActionInput) (pact.AdminRecordActionResult, error) {
|
|
principal, _ := bouncer.User(ctx)
|
|
user, ok := in.Record.(*BackendUser)
|
|
if !ok || user == nil {
|
|
return pact.AdminRecordActionResult{}, fmt.Errorf("cabana: contributed Administrators action: record is not a backend user")
|
|
}
|
|
if err := guardOutranked(ctx, reg, principal, user); err != nil {
|
|
return pact.AdminRecordActionResult{}, err
|
|
}
|
|
return next(ctx, in)
|
|
}
|
|
}
|