Skip to content

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=allow

kitloom-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 capabilityAsks for
edit_post, delete_post, read_post, publish_postthe post type's capabilities, by author and status (edit_others_posts, edit_published_posts…)
edit_term, delete_term, assign_termthe taxonomy's edit_terms, delete_terms, assign_terms
edit_user, delete_user, promote_user, remove_useredit_users (nothing for yourself), delete_users, promote_users, remove_users
edit_commentedit_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 engine

Whoever 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);
ArgumentMeaning
nameCapability name, WordPress style: manage_reviews
groupHeading on the roles screen, usually the module name
coversClasses of the pages, resources or tools this permission opens
labelHuman-readable text next to the checkbox
rolesRoles that receive it the first time the site sees it. Default: ['administrator']
likeRoles 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-nova

The 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 migrate

Let 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));
SwitchTurns off
kitloom-permissions.filament.registerthe Roles screens in Filament
kitloom-permissions.filament.user_rolethe Role field on Filament's user form
kitloom-permissions.nova.registerthe Roles tool, the Role field and filter in Nova

How roles behave ​

  • A permission a module declares reaches the roles named in roles once, 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}capabilities and {prefix}user_level in user meta, the role's definition in the {prefix}user_roles option — 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 its role: / permission: middleware;
    • do not publish spatie's migration — the package ships its own.

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 ​

EventWhen
Kitloom\Permissions\Events\RoleSaveda role was created, renamed or given other permissions through RoleEditor; previous is the role before, addedPermissions() and removedPermissions() say what changed
Kitloom\Permissions\Events\RoleDeleteda role was deleted; its users stay without it
Kitloom\Permissions\Events\UserRoleChangeda 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.

kitloom — modules and themes for Laravel.
Pricing · Services · License · Refunds · Privacy · Community · [email protected]