package cabana import ( "fmt" "strings" "git.golem15.com/golem15/summercms/modules/pact" "git.golem15.com/golem15/summercms/modules/party" ) type controllerRef struct { plugin party.Plugin ctl pact.AdminController } func collectControllers(plugins []party.Plugin) ([]controllerRef, error) { var out []controllerRef for _, p := range plugins { src, ok := p.(pact.HasAdminControllers) if !ok || p == nil { continue } for _, ctl := range src.AdminControllers() { if ctl == nil { continue } id := ctl.ID() if id != p.ID() && !strings.HasPrefix(id, p.ID()+".") { return nil, fmt.Errorf("cabana: controller %s is not owned by plugin %s", id, p.ID()) } out = append(out, controllerRef{plugin: p, ctl: ctl}) } } return out, nil } // reservedVendorSegments are the first path segments under the admin prefix // that the SPA and API own. A controller ID maps to /{vendor}/{plugin}/..., so // a vendor with one of these names would collide with them. var reservedVendorSegments = map[string]bool{"api": true, "assets": true, "login": true, "settings": true} func checkReservedSegments(items []controllerRef) error { for _, item := range items { id := item.ctl.ID() vendor, _, _ := strings.Cut(id, ".") if reservedVendorSegments[vendor] { return fmt.Errorf("cabana: controller %s uses the reserved admin path segment %q (reserved: api, assets, login, settings)", id, vendor) } } return nil } func compileRegistry(items []controllerRef) (*Registry, error) { byID := make(map[string]*CompiledController, len(items)) assets := map[string]*pluginAsset{} for _, item := range items { id := item.ctl.ID() if _, exists := byID[id]; exists { return nil, fmt.Errorf("cabana: duplicate admin controller %s", id) } fsys, ok := item.plugin.(pact.AdminAssets) if !ok || fsys == nil || fsys.AdminFS() == nil { return nil, fmt.Errorf("cabana: plugin %s has admin controllers but no AdminFS", item.plugin.ID()) } list, err := compileList(item.plugin.ID(), item.ctl, fsys.AdminFS()) if err != nil { return nil, err } form, err := compileFormIfPresent(item.plugin.ID(), item.ctl, fsys.AdminFS()) if err != nil { return nil, err } relations, err := compileRelations(item.plugin.ID(), item.ctl, fsys.AdminFS(), form) if err != nil { return nil, err } fieldRelations, err := compileFieldRelations(item.plugin.ID(), item.ctl, form) if err != nil { return nil, err } if form == nil && list != nil { // Without a compiled form there is nothing to create (D-14). list.ToolbarButtons = withoutAction(list.ToolbarButtons, "create") } compiled := &CompiledController{ PluginID: item.plugin.ID(), Controller: item.ctl, List: list, Form: form, Relations: relations, FieldRelations: fieldRelations, } if err := BindWritableFields(compiled); err != nil { return nil, err } if err := compileExtension(item.plugin.ID(), compiled, fsys.AdminFS()); err != nil { return nil, err } // Two controllers of one plugin declaring the same file share an entry. for _, file := range append(append([]*pluginAsset(nil), compiled.scripts...), compiled.styles...) { if _, exists := assets[file.key]; !exists { assets[file.key] = file } } byID[id] = compiled } return &Registry{byID: byID, assets: assets}, nil } func withoutAction(actions []string, drop string) []string { out := make([]string, 0, len(actions)) for _, action := range actions { if action != drop { out = append(out, action) } } return out } func compileContributions(reg *Registry, plugins []party.Plugin) error { if reg == nil { return fmt.Errorf("cabana: registry is nil") } reg.permissions = map[string]pact.Permission{} reg.roleGrants = map[string]map[string]bool{} reg.settings = map[string]*CompiledSetting{} seenNavigation := map[string]struct{}{} for _, plugin := range plugins { if plugin == nil { continue } if src, ok := plugin.(pact.HasPermissions); ok && src != nil { for _, permission := range src.Permissions() { if !permissionCode(permission.Code, false) { return fmt.Errorf("cabana: plugin %s registered invalid permission %q", plugin.ID(), permission.Code) } if _, exists := reg.permissions[permission.Code]; exists { return fmt.Errorf("cabana: duplicate permission %s", permission.Code) } reg.permissions[permission.Code] = permission for _, role := range permission.Roles { role = strings.TrimSpace(role) if role == "" { return fmt.Errorf("cabana: permission %s has an empty role", permission.Code) } if reg.roleGrants[role] == nil { reg.roleGrants[role] = map[string]bool{} } reg.roleGrants[role][permission.Code] = true } } } if src, ok := plugin.(pact.HasSettings); ok && src != nil { assets, hasAssets := plugin.(pact.AdminAssets) for _, item := range src.Settings() { if !identifier(item.Code) { return fmt.Errorf("cabana: plugin %s registered invalid setting %q", plugin.ID(), item.Code) } if _, exists := reg.settings[item.Code]; exists { return fmt.Errorf("cabana: duplicate setting %s", item.Code) } if !hasAssets || assets == nil || assets.AdminFS() == nil { return fmt.Errorf("cabana: plugin %s has settings but no AdminFS", plugin.ID()) } compiled, err := compileSetting(plugin.ID(), item, assets.AdminFS()) if err != nil { return err } reg.settings[item.Code] = compiled } } if src, ok := plugin.(pact.HasNavigation); ok && src != nil { for _, item := range src.Navigation() { if err := validateNavigationShape(plugin.ID(), item, seenNavigation); err != nil { return err } reg.navigation = append(reg.navigation, item) } } } for id, controller := range reg.byID { if err := reg.validatePermissions("controller "+id, requiredOf(controller.Controller)); err != nil { return err } for name, relation := range controller.Relations { if err := reg.validatePermissions("relation "+id+"."+name, relation.RequiredPermissions); err != nil { return err } } for name, action := range controller.Actions { if err := reg.validatePermissions("action "+id+"."+name, action.Permissions); err != nil { return err } } } for _, item := range reg.navigation { if err := reg.validateNavigation(item); err != nil { return err } } for code, setting := range reg.settings { if err := reg.validatePermissions("setting "+code, setting.Item.Permissions); err != nil { return err } } return nil } func permissionCode(code string, wildcard bool) bool { parts := strings.Split(code, ".") if len(parts) < 2 { return false } for i, part := range parts { if wildcard && i == len(parts)-1 && part == "*" { return true } if !identifier(part) { return false } } return true } func (r *Registry) validatePermissions(owner string, permissions []string) error { for _, code := range permissions { if !permissionCode(code, true) || !r.permissionExists(code) { return fmt.Errorf("cabana: %s references unknown permission %s", owner, code) } } return nil } func (r *Registry) permissionExists(code string) bool { if r == nil { return false } if _, ok := r.permissions[code]; ok { return true } if !strings.HasSuffix(code, ".*") { return false } prefix := strings.TrimSuffix(code, "*") for candidate := range r.permissions { if strings.HasPrefix(candidate, prefix) { return true } } return false } func validateNavigationShape(pluginID string, item pact.NavigationItem, seen map[string]struct{}) error { if !identifier(item.Code) { return fmt.Errorf("cabana: plugin %s registered invalid navigation code %q", pluginID, item.Code) } if _, exists := seen[item.Code]; exists { return fmt.Errorf("cabana: duplicate navigation code %s", item.Code) } seen[item.Code] = struct{}{} childSeen := map[string]struct{}{} for _, child := range item.SideMenu { if !identifier(child.Code) { return fmt.Errorf("cabana: navigation %s has invalid child code %q", item.Code, child.Code) } if _, exists := childSeen[child.Code]; exists { return fmt.Errorf("cabana: navigation %s has duplicate child %s", item.Code, child.Code) } childSeen[child.Code] = struct{}{} } return nil } func (r *Registry) validateNavigation(item pact.NavigationItem) error { if _, ok := r.byID[item.Controller]; !ok { return fmt.Errorf("cabana: navigation %s references unknown controller %s", item.Code, item.Controller) } if err := r.validatePermissions("navigation "+item.Code, item.Permissions); err != nil { return err } for _, child := range item.SideMenu { if _, ok := r.byID[child.Controller]; !ok { return fmt.Errorf("cabana: navigation %s.%s references unknown controller %s", item.Code, child.Code, child.Controller) } if err := r.validatePermissions("navigation "+item.Code+"."+child.Code, child.Permissions); err != nil { return err } } return nil } func (r *Registry) rolePermissions(role string) map[string]bool { out := map[string]bool{} if r == nil { return out } for code, allowed := range r.roleGrants[role] { if allowed { out[code] = true } } return out }