- Audience-aware mint, verify, refresh, and backend guard keep frontend tokens compatible - Cabana mounts raw admin login, list schema, and record list behind admin.jwt.secret - Framework migration seeds Winter backend users and developer/publisher roles
118 lines
3.0 KiB
Go
118 lines
3.0 KiB
Go
package cabana
|
|
|
|
import (
|
|
"encoding/json"
|
|
"net/http"
|
|
"strings"
|
|
|
|
"git.golem15.com/golem15/summercms/bouncer"
|
|
"git.golem15.com/golem15/summercms/pact"
|
|
)
|
|
|
|
// Option is a dropdown choice shared with later schema plans.
|
|
type Option = pact.Option
|
|
|
|
// ListColumn is one compiled columns.yaml entry, in file order.
|
|
type ListColumn struct {
|
|
Key string `json:"key"`
|
|
Label string `json:"label"`
|
|
Searchable bool `json:"searchable"`
|
|
Sortable bool `json:"sortable"`
|
|
Type string `json:"type,omitempty"`
|
|
}
|
|
|
|
// ListSchema is the boot-compiled list contract for one controller.
|
|
type ListSchema struct {
|
|
Title string `json:"title,omitempty"`
|
|
RecordsPerPage int `json:"recordsPerPage"`
|
|
ShowSearch bool `json:"showSearch"`
|
|
Columns []ListColumn `json:"columns"`
|
|
}
|
|
|
|
// CompiledController is one admin controller after YAML compilation.
|
|
type CompiledController struct {
|
|
PluginID string
|
|
Controller pact.AdminController
|
|
List *ListSchema
|
|
}
|
|
|
|
// Registry is the immutable controller map keyed by controller ID.
|
|
type Registry struct {
|
|
byID map[string]*CompiledController
|
|
}
|
|
|
|
// Get returns the compiled controller for a D-09 id (vendor.plugin.controller).
|
|
func (r *Registry) Get(id string) (*CompiledController, bool) {
|
|
if r == nil {
|
|
return nil, false
|
|
}
|
|
cc, ok := r.byID[id]
|
|
return cc, ok && cc != nil
|
|
}
|
|
|
|
// Allows reports whether principal satisfies every required permission code.
|
|
// A nil principal fails. Superusers pass. An empty requirement list allows
|
|
// any authenticated principal. Grants ending in ".*" match by prefix.
|
|
func Allows(principal *bouncer.Principal, required []string) bool {
|
|
if principal == nil {
|
|
return false
|
|
}
|
|
if principal.IsSuperuser || len(required) == 0 {
|
|
return true
|
|
}
|
|
for _, code := range required {
|
|
if !granted(principal.PermissionGrants, code) {
|
|
return false
|
|
}
|
|
}
|
|
return true
|
|
}
|
|
|
|
func granted(grants map[string]bool, code string) bool {
|
|
if grants[code] {
|
|
return true
|
|
}
|
|
for key, on := range grants {
|
|
if !on || !strings.HasSuffix(key, ".*") {
|
|
continue
|
|
}
|
|
prefix := strings.TrimSuffix(key, "*")
|
|
if strings.HasPrefix(code, prefix) {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
func requiredOf(ctl pact.AdminController) []string {
|
|
if p, ok := ctl.(pact.AdminPermissioned); ok && p != nil {
|
|
return p.RequiredPermissions()
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// WriteData writes a D-10 success envelope.
|
|
func WriteData(w http.ResponseWriter, status int, data, meta any) {
|
|
if meta == nil {
|
|
meta = map[string]any{}
|
|
}
|
|
writeJSON(w, status, map[string]any{"data": data, "meta": meta})
|
|
}
|
|
|
|
// WriteError writes a D-10 error envelope. details is always an object.
|
|
func WriteError(w http.ResponseWriter, status int, code, message string) {
|
|
writeJSON(w, status, map[string]any{
|
|
"error": map[string]any{
|
|
"code": code,
|
|
"message": message,
|
|
"details": map[string]any{},
|
|
},
|
|
})
|
|
}
|
|
|
|
func writeJSON(w http.ResponseWriter, status int, body any) {
|
|
w.Header().Set("Content-Type", "application/json")
|
|
w.WriteHeader(status)
|
|
_ = json.NewEncoder(w).Encode(body)
|
|
}
|