Appearance
Permissions
Introduction
Access control in kitloom has two layers:
kitloom/access— a small contract package. Modules use it to declare the permissions that open their admin pages. It has no storage and no opinion about roles.kitloom/permissions(+-filament/-nova) — the roles engine: roles with their permissions, the Users → Roles screen, the Role field on the user form, and the answers to every check.
Every check goes through Laravel's Gate. Without the roles engine nobody answers the capabilities an admin page asks for, and the admin stays closed: a forgotten declaration must not open a page to every signed-in user, an API or an agent. A site that runs without roles on purpose says so:
ini
KITLOOM_UNDECIDED_ABILITIES=allowkitloom-access.undecided (for admin sections) and kitloom-wp-schema.capabilities.undecided (for WordPress capabilities) both read this variable; the default is deny.
Content capabilities
Posts, pages, taxonomies, comments, media, users and the site settings use WordPress's capability names — edit_posts, edit_others_posts, publish_pages, upload_files, list_users, manage_options, edit_theme_options. Check them through the Gate as usual:
php
Gate::allows('edit_post', $post);
$user->can('publish_posts');Capabilities about one record are meta capabilities, mapped the way WordPress's map_meta_cap() maps them (Kitloom\WpSchema\Capabilities\MetaCapabilities): edit_post of a post depends on who wrote it and on its status, edit_user on whether it is yourself.
| Meta capability | Asks for |
|---|---|
edit_post, delete_post, read_post, publish_post | the post type's capabilities, by author and status (edit_others_posts, edit_published_posts…) |
edit_term, delete_term, assign_term | the taxonomy's edit_terms, delete_terms, assign_terms |
edit_user, delete_user, promote_user, remove_user | edit_users (nothing for yourself), delete_users, promote_users, remove_users |
edit_comment | edit_post of the comment's post; moderate_comments when it has none |
A post type gets its capability names from its capability_type, as register_post_type() gives them: a type of book/books is guarded by edit_books, edit_others_books, publish_books. Kitloom\WpSchema\Capabilities\Capabilities builds them:
php
$capabilities = app(Capabilities::class);
$capabilities->postType('book', 'edit_others_posts'); // 'edit_others_books'
$capabilities->taxonomy('genre', 'assign_terms'); // 'assign_genre_terms' with the roles engineWhoever registers a type says which capabilities guard it with a Kitloom\WpSchema\Contracts\CapabilityTypeProvider tagged Capabilities::PROVIDER_TAG. A type nobody describes is guarded by the capabilities of posts — unless the roles engine is installed: then every custom post type and taxonomy gets capabilities of its own (edit_books, manage_genre_terms), so each can be given to a role separately.
The writers of kitloom/wp-schema ask these capabilities for you: PostWriter and TermWriter refuse, as the acting user, what the user may not do — see Data.
Declaring a module's permissions
Every other admin page asks about itself: Gate::allows(static::class). The module declares which permission opens that page — one permission per menu item — through a Kitloom\Access\Contracts\PermissionProvider:
php
<?php
declare(strict_types=1);
namespace Acme\Reviews\Filament;
use Acme\Reviews\Filament\Resources\ReviewResource;
use Acme\Reviews\Filament\Settings\ReviewsSettingsPage;
use Kitloom\Access\Contracts\PermissionProvider;
use Kitloom\Access\Permission;
final class ReviewsPermissions implements PermissionProvider
{
public function permissions(): iterable
{
yield new Permission(
name: 'manage_reviews',
group: 'Reviews',
covers: [ReviewResource::class, ReviewsSettingsPage::class],
label: 'Manage reviews',
roles: ['administrator', 'editor'],
);
}
}Tag it in the admin package's register():
php
use Kitloom\Access\Permission;
$this->app->tag([ReviewsPermissions::class], Permission::PROVIDER_TAG);| Argument | Meaning |
|---|---|
name | Capability name, WordPress style: manage_reviews |
group | Heading on the roles screen, usually the module name |
covers | Classes of the pages, resources or tools this permission opens |
label | Human-readable text next to the checkbox |
roles | Roles that receive it the first time the site sees it. Default: ['administrator'] |
like | Roles that hold this other permission receive it too — for a permission split off an existing one |
A section nobody declared needs manage_options once the roles engine is installed, so a forgotten declaration closes the page instead of opening it. A link or an action that leads to another menu item checks that item too (ReviewResource::canViewAny()).
A resource on a table of the module's own asks Gate::allows(static::class) in its can…() methods. A resource on a post type goes by the type's capabilities (edit_posts and the rest), as in WordPress — unless the post type is the module's section (contact forms, sliders): then one permission opens all of it, through a trait of the admin base packages:
php
use Kitloom\WpSchema\Filament\Resources\Concerns\GuardedBySection;
use Kitloom\WpSchema\Filament\Resources\PostTypeResource;
// canViewAny, canCreate, canEdit, canView, canDelete, canDeleteAny
// all ask Gate::allows(static::class)
class ReviewResource extends PostTypeResource
{
use GuardedBySection;
}php
use Kitloom\WpSchema\Nova\Resources\Concerns\GuardedBySection;
use Kitloom\WpSchema\Nova\Resources\PostResource;
// every Nova ability asks Gate::allows(static::class)
class ReviewResource extends PostResource
{
use GuardedBySection;
}kitloom:make-module generates the permission provider for the module's settings page, and its resources ask the Gate as described.
Installing the roles engine
bash
composer require kitloom/permissions kitloom/permissions-filament
# or
composer require kitloom/permissions kitloom/permissions-novaThe skeletons have it installed. On another application, give the user model roles:
php
use Kitloom\Permissions\Concerns\HasRoles;
use Kitloom\WpSchema\Models\Concerns\IsWordPressUser;
class User extends Authenticatable
{
use HasRoles, IsWordPressUser;
}Run the migrations — they create the role tables and the default roles (administrator, editor, author, contributor, subscriber), and make the first user an administrator if nobody is one:
bash
php artisan migrateLet into the admin whoever has something to do in it — a permission beyond reading the site (Kitloom\Permissions\AdminAccess). Customers and subscribers stay out, as WooCommerce keeps them out of wp-admin, and use their account on the site:
php
public function canAccessPanel(Panel $panel): bool
{
return app(AdminAccess::class)->allows($this);
}php
Gate::define('viewNova', fn (User $user): bool => app(AdminAccess::class)->allows($user));| Switch | Turns off |
|---|---|
kitloom-permissions.filament.register | the Roles screens in Filament |
kitloom-permissions.filament.user_role | the Role field on Filament's user form |
kitloom-permissions.nova.register | the Roles tool, the Role field and filter in Nova |
How roles behave
- A permission a module declares reaches the roles named in
rolesonce, the first time the site sees it. A post type with capabilities of its own gives them to the roles that hold the same ones for posts. After that, roles belong to the site's managers: a permission they remove is not given back. - A permission from a deactivated module is kept, so activating the module restores access as it was.
- Roles are shared with WordPress. A role given here is written where WordPress reads it —
{prefix}capabilitiesand{prefix}user_levelin user meta, the role's definition in the{prefix}user_rolesoption — and a role given in WordPress is taken over when the user signs in. A site moved from WordPress keeps its users' roles. - A user who signs up gets the site's New User Default Role (Settings → General), as in WordPress.
- Roles are stored with spatie/laravel-permission, but only behind kitloom's own contracts (
Kitloom\Permissions\Contracts\RoleRepository,UserRoles,PermissionChecker). Keep it replaceable:- check access through the Gate only —
Gate::allows(),$user->can(); - do not call spatie methods (
hasRole(),hasPermissionTo()) or itsrole:/permission:middleware; - do not publish spatie's migration — the package ships its own.
- check access through the Gate only —
To assign a role in code — a seeder, a factory — use the contract, and pass the user who does it when there is one:
php
use Kitloom\Permissions\Contracts\UserRoles;
app(UserRoles::class)->setRole($user, 'editor');
app(UserRoles::class)->setRole($user, 'editor', actor: $admin);Kitloom\Permissions\RoleEditor is what a roles screen does, whatever admin draws it: save($key, $name, $permissions, $creating, $actor), delete($key, $actor), deletable($key) — the administrator role and the default role for new users stay.
Events
| Event | When |
|---|---|
Kitloom\Permissions\Events\RoleSaved | a role was created, renamed or given other permissions through RoleEditor; previous is the role before, addedPermissions() and removedPermissions() say what changed |
Kitloom\Permissions\Events\RoleDeleted | a role was deleted; its users stay without it |
Kitloom\Permissions\Events\UserRoleChanged | a user was given another role or none (UserRoles::setRole()): on a user's screen, as the default role of a new account, or taken over from WordPress at sign-in; old are the roles before |
Each carries the actor — the user who did it, null for the system. Permissions that reach roles on their own, when a module declares them, are not announced. kitloom/activity-log writes these events to the site's activity log.