*/ public const CRUD_ABILITIES = [ 'view_any', 'view', 'create', 'update', 'delete', 'restore', 'force_delete', ]; /** * Resource key => the abilities that resource actually supports. * * `users` and `roles` are hard-deleted, so they carry no restore or * force_delete permission; granting one would be a permission nothing ever * checks. * * @var array> */ public const RESOURCES = [ 'pages' => [...self::CRUD_ABILITIES, 'publish'], 'categories' => self::CRUD_ABILITIES, 'projects' => [...self::CRUD_ABILITIES, 'publish'], 'members' => self::CRUD_ABILITIES, // What the collective offers, as opposed to what it has built. // Publishing is separate for the same reason it is on projects: an // editor may write the offer without putting it on the website. 'services' => [...self::CRUD_ABILITIES, 'publish'], 'clients' => self::CRUD_ABILITIES, 'testimonials' => self::CRUD_ABILITIES, 'talks' => ['view_any', 'view', 'create', 'update', 'delete'], 'technologies' => ['view_any', 'view', 'create', 'update', 'delete'], 'calls' => ['view_any', 'view', 'create', 'update', 'delete'], // The inbox holds names, e-mail addresses and messages from people // outside the collective, so it is gated more tightly than the content // resources — and nobody creates one by hand: they arrive from the // public site, which is why there is no `create` here. 'applications' => ['view_any', 'view', 'update', 'delete', 'restore', 'force_delete'], // A broken link is a content problem, not an infrastructure one, so // whoever renames a slug is also who fixes what it broke. // Derived from the files themselves: nobody creates one by hand, and // "update" would mean editing something the processor owns. 'media' => ['view_any', 'view', 'delete'], 'redirects' => ['view_any', 'view', 'create', 'update', 'delete'], // `sync` is separate from `update` because triggering a sync spends // the provider's rate limit, which is a shared resource. 'repositories' => ['view_any', 'view', 'create', 'update', 'delete', 'sync'], 'users' => ['view_any', 'view', 'create', 'update', 'delete'], 'roles' => ['view_any', 'view', 'create', 'update', 'delete'], // Taking the public site down is a deliberate, visible act, so it is // its own permission rather than part of general settings editing. 'settings' => ['view', 'update', 'maintenance'], ]; /** * Every permission name the application knows about. * * @return array */ public static function all(): array { return array_merge(...array_values(self::grouped())); } /** * Permissions grouped by resource, for rendering the role editor. * * @return array> */ public static function grouped(): array { $groups = ['general' => [self::PANEL_ACCESS]]; foreach (self::RESOURCES as $resource => $abilities) { $groups[$resource] = array_map( fn (string $ability) => "{$resource}.{$ability}", $abilities, ); } return $groups; } /** * Every permission belonging to one resource. * * @return array */ public static function for(string $resource): array { return self::grouped()[$resource] ?? []; } /** * The roles shipped with the application, and what each one may do. * * `super_admin` is deliberately absent: it is granted everything by a * Gate::before hook in AuthServiceProvider rather than by a permission list, * so it can never drift out of date as the catalogue grows. * * @return array> */ public static function roleDefinitions(): array { $readOnly = fn (string $resource) => [ "{$resource}.view_any", "{$resource}.view", ]; // Everything you need to write something, and nothing that decides // whether the public sees it. $authoring = fn (string $resource) => array_values(array_diff( self::for($resource), ["{$resource}.publish", "{$resource}.force_delete"], )); return [ 'super_admin' => [], 'admin' => self::all(), 'editor' => [ self::PANEL_ACCESS, ...self::for('pages'), ...self::for('categories'), ...self::for('projects'), ...self::for('members'), ...self::for('services'), ...self::for('clients'), ...self::for('testimonials'), ...self::for('talks'), ...self::for('technologies'), ...self::for('calls'), ...self::for('media'), ...self::for('redirects'), // Editors run the calls, so they are the ones who have to // answer the people who reply to them. ...self::for('applications'), ...$readOnly('repositories'), ...$readOnly('users'), 'settings.view', ], /* * Writes content, cannot put it on the site. * * The role the separate `publish` permissions were always for. * Without one, `pages.publish` was a distinction the catalogue * made and no shipped role ever exercised — `editor` holds it * along with everything else — so the handover it implies had * nowhere to happen. This is the other end of it: somebody who * finishes a piece of work and asks for a review, because that is * the only way they have of putting it live. * * Also without `force_delete`: erasing something for good is not * part of writing it, and the bin makes an ordinary delete * recoverable. And without `applications`, for the reason the * viewer does not have it either — writing for the site and * reading strangers' contact details are different jobs. */ 'writer' => [ self::PANEL_ACCESS, ...$authoring('pages'), ...$authoring('projects'), ...$authoring('services'), // Enough to pick one on a form, not to invent one. ...$readOnly('categories'), ...$readOnly('members'), ...$readOnly('clients'), ...$readOnly('technologies'), ...$readOnly('talks'), ...$readOnly('testimonials'), ...$readOnly('media'), ...$readOnly('calls'), ], // Deliberately without `applications`: a viewer reads the site's // content, not the contact details of the people who wrote in. 'viewer' => [ self::PANEL_ACCESS, ...$readOnly('pages'), ...$readOnly('categories'), ...$readOnly('projects'), ...$readOnly('members'), ...$readOnly('services'), ...$readOnly('clients'), ...$readOnly('testimonials'), ...$readOnly('talks'), ...$readOnly('technologies'), ...$readOnly('media'), ...$readOnly('calls'), ...$readOnly('repositories'), ], ]; } /** * Roles that must never be deleted or renamed from the panel, because the * application's own bootstrapping depends on them. * * @return array */ public static function protectedRoles(): array { return array_values(array_unique([ 'super_admin', (string) config('pireon.default_role'), ])); } }