yuriitatur/permissions

simple permission manager

Maintainers

Package info

bitbucket.org/yurii_tatur/permissions

pkg:composer/yuriitatur/permissions

Statistics

Installs: 1

Dependents: 0

Suggesters: 0

dev-master 2026-05-16 19:46 UTC

This package is auto-updated.

Last update: 2026-05-16 19:46:32 UTC


README

A standalone PHP 8.4 RBAC/ACL library with a full Laravel adapter, Filament v5 admin UI, and optional REST API.

Features

  • Role-based access control — assign roles to users, check roles with full hierarchy traversal
  • Structural and named permissionsread:post:42, read:post:*, or a plain edit-posts string
  • Direct grants — assign a permission directly to a user, bypassing the role system
  • Multiple-inheritance hierarchy — roles and permissions can extend multiple parents via a materialized-path tree
  • Two storage backendslocal (PHP config arrays, read-only at runtime) and eloquent (database)
  • Command/Handler pattern — every mutation is a typed DTO dispatched to a dedicated handler with PSR-14 events
  • Filament v5 plugin — full admin UI with visual hierarchy trees, relation managers, and read-only list pages for local-driver resources
  • REST API — optional api-routes that expose the full management surface over HTTP

Installation

composer require yuriitatur/permissions

Publish the config and migrations:

php artisan vendor:publish --tag=permission-config
php artisan vendor:publish --tag=permission-migrations
php artisan migrate

Configuration

config/permissions.php controls every aspect of the package.

Drivers

'driver' => [
    'role'                 => 'eloquent', // or 'local'
    'permission'           => 'eloquent',
    'action'               => 'eloquent',
    'grant'                => 'eloquent',
    'role-hierarchy'       => 'eloquent', // null = disabled
    'permission-hierarchy' => 'eloquent', // null = disabled
],

Switch any resource to 'local' to serve it from a PHP array in config (read-only, ideal for roles that are fixed at deploy time).

Local driver data

'local' => [
    'roles' => [
        ['name' => 'admin',  'ui_name' => 'Administrator', 'permissions' => ['edit-posts']],
        ['name' => 'editor', 'ui_name' => 'Editor'],
    ],
    'permissions' => [
        ['name' => 'edit-posts', 'ui_name' => 'Edit Posts'],
    ],
    'role-hierarchy' => [
        'admin' => ['editor'], // admin inherits everything editor has
    ],
],

Almighty role

'almighty-role' => 'admin', // this role bypasses ALL permission checks

Set to null to disable the superuser shortcut.

Eloquent models

'eloquent' => [
    'models' => [
        'user'       => App\Models\User::class,
        'role'       => \YuriiTatur\Permissions\Laravel\Models\Role::class,
        'permission' => \YuriiTatur\Permissions\Laravel\Models\Permission::class,
        'action'     => \YuriiTatur\Permissions\Laravel\Models\Action::class,
        'grant'      => \YuriiTatur\Permissions\Laravel\Models\Grant::class,
    ],
],

User model

Add the helper traits to your User model:

use YuriiTatur\Permissions\Laravel\Models\Utils\HasRoles;
use YuriiTatur\Permissions\Laravel\Models\Utils\HasGrants;

class User extends Authenticatable
{
    use HasRoles;  // adds $user->roles() belongsToMany
    use HasGrants; // adds $user->grants() hasMany
}

To make your own Eloquent models usable as permission targets, implement ResourceInterface via the IsResource trait:

use YuriiTatur\Permissions\Laravel\Models\Utils\IsResource;

class Post extends Model implements \YuriiTatur\Permissions\Entities\ResourceInterface
{
    use IsResource;
}

Superuser model (SystemUserInterface)

If your application has a separate admin model (e.g. a dedicated Admin Eloquent model), implement SystemUserInterface on it to bypass all permission and role checks unconditionally:

use YuriiTatur\Permissions\Entities\SystemUserInterface;
use YuriiTatur\Permissions\Entities\ActorInterface;

class Admin extends Authenticatable implements ActorInterface, SystemUserInterface
{
    public function getId(): string|int
    {
        return $this->id;
    }
}

Any actor that implements SystemUserInterface will always pass hasRole(), hasPermission(), and every gate check — no role or grant lookup is performed. This is the correct way to give an admin-level model blanket access without littering your code with superuser shortcuts.

Access checks

Inject AccessManager from the container, or use the AclBuilder for fluent multi-check pipelines.

use YuriiTatur\Permissions\Services\AccessManager;
use YuriiTatur\Permissions\ValueObjects\PermissionKey;

class PostController
{
    public function __construct(private AccessManager $acl) {}

    public function update(Request $request, Post $post): Response
    {
        // Single check — throws AccessDeniedException on failure
        $this->acl->requirePermission(
            $request->user(),
            new PermissionKey(null, $post, $post->getWriteAction())
        );

        // Named permission check
        $this->acl->requirePermission($request->user(), new PermissionKey('edit-posts', null, null));

        // Role check (includes hierarchy descendants)
        $this->acl->requireRole($request->user(), 'editor');

        // Boolean form
        if ($this->acl->hasRole($request->user(), 'admin')) { ... }

        // Fluent multi-check pipeline — all checks evaluated, throws if any fail
        $this->acl->createBuilder()
            ->requireRole($request->user(), 'editor')
            ->requirePermission($request->user(), new PermissionKey('edit-posts', null, null))
            ->check();
    }
}

Route middleware

Route::put('/posts/{post}', [PostController::class, 'update'])
    ->middleware('any-role:editor')
    ->middleware('any-ability:edit-posts');

Mutations (command handlers)

All writes go through typed command DTOs dispatched to handlers. The handlers emit PSR-14 events (AccessCheckEvent, ActionHappenedEvent) before and after each operation.

Roles

use YuriiTatur\Permissions\CommandHandlers\Roles\CreateRoleHandler;
use YuriiTatur\Permissions\Commands\Roles\CreateRoleCommand;
use YuriiTatur\Permissions\Utilities\GenericActor;

app(CreateRoleHandler::class)(new CreateRoleCommand(
    creator: new GenericActor($user->id),
    roleName: 'editor',
    description: 'Can edit posts',
    uiLabel: 'Editor',
    active: true,
));
// Assign role to user
app(AssignRoleHandler::class)(new AssignRoleCommand(new GenericActor($admin->id), $user, 'editor'));

// Sync all permissions on a role (replaces the full set)
app(ConfigureRoleHandler::class)(new ConfigureRoleCommand(
    new GenericActor($admin->id),
    'editor',
    collect(['edit-posts', 'view-posts']),
));

// Delete role
app(DeleteRoleHandler::class)(new DeleteRoleCommand(new GenericActor($admin->id), 'editor'));

Hierarchy

use YuriiTatur\Nested\ValueObjects\ParentPath;

// Make 'editor' a child of 'admin' in the hierarchy
app(AttachRoleHandler::class)(new AttachRoleCommand(
    new GenericActor($admin->id),
    new ParentPath('admin'),   // ordered list of ancestors root → direct parent
    'editor',
));

// Remove that relationship (cascades to the subtree below this path)
app(DetachRoleHandler::class)(new DetachRoleCommand(
    new GenericActor($admin->id),
    new ParentPath('admin'),
    'editor',
));

Permissions

use YuriiTatur\Permissions\ValueObjects\PermissionKey;

// Named permission
app(CreatePermissionHandler::class)(new CreatePermissionCommand(
    new GenericActor($admin->id),
    new PermissionKey('edit-posts', null, null),
    description: 'Allows editing any post',
    uiLabel: 'Edit Posts',
    active: true,
));

// Structural permission: action + resource type + optional ID
$key = new PermissionKey(null, new GenericResource('*', 'post'), $readAction);
app(CreatePermissionHandler::class)(new CreatePermissionCommand(new GenericActor($admin->id), $key, ...));

Direct grants

// Grant access directly to a user (bypasses role system)
app(GrantAccessHandler::class)(new GrantAccessCommand(
    granter: new GenericActor($admin->id),
    grantee: new GenericActor($user->id),
    key: new PermissionKey('edit-posts', null, null),
    grantedAt: now(),
));

// Revoke
app(RevokeGrantHandler::class)(new RevokeGrantCommand(
    new GenericActor($admin->id),
    new GenericActor($user->id),
    new PermissionKey('edit-posts', null, null),
));

Artisan commands

php artisan acl:role:create          # Create a role
php artisan acl:role:delete          # Delete a role
php artisan acl:role:assign          # Assign a role to a user
php artisan acl:role:resign          # Remove a role from a user
php artisan acl:role:configure       # Sync all permissions on a role
php artisan acl:role:attach          # Attach a role under a parent in the hierarchy
php artisan acl:role:detach          # Detach a role from a parent in the hierarchy
php artisan acl:permission:create    # Create a permission
php artisan acl:permission:delete    # Delete a permission
php artisan acl:permission:attach    # Attach a permission under a parent in the hierarchy
php artisan acl:permission:detach    # Detach a permission from a parent in the hierarchy
php artisan acl:action:create        # Create an action verb
php artisan acl:action:delete        # Delete an action verb (cascades to permissions and grants)

All commands accept an --actor=<user-id> option to record who performed the operation in the audit event.

Filament v5 plugin

Requires filament/filament: ^5.0 (dev dependency — add it yourself in your app).

Register the plugin on your Filament panel:

use YuriiTatur\Permissions\Laravel\Filament\PermissionsPlugin;

$panel->plugin(PermissionsPlugin::make());

What the plugin provides

Four resources appear in the "Permissions" navigation group:

ResourceDriver modeCapabilities
RoleslocalRead-only list
RoleseloquentFull CRUD + hierarchy tree + permissions relation manager
PermissionslocalRead-only list
PermissionseloquentFull CRUD + hierarchy tree + roles relation manager
ActionslocalRead-only list
ActionseloquentFull CRUD
GrantslocalRead-only list
GrantseloquentList + Create (grants are immutable — delete only)

Hierarchy tree pages (/roles/hierarchy, /permissions/hierarchy):

  • Renders the full hierarchy as a recursive indented tree
  • Hover over any node to reveal Add child and Remove action buttons (open Filament modal forms)
  • Add root header button; unattached entities are listed below the tree with a one-click Add as root shortcut
  • Pages are automatically hidden when the relevant hierarchy driver is null

Relation managers:

  • Role edit page → Permissions tab: attach/detach permissions (syncs via ConfigureRoleHandler)
  • Permission edit page → Roles tab: see which roles include this permission, and detach them

All writes go through the same command handlers as the Artisan commands and application code — no direct Eloquent writes.

REST API

Enable in config:

'api-routes' => [
    'prefix'     => '/api/acl',
    'middleware' => ['api', 'auth'],
],

This mounts a full CRUD REST API for roles, permissions, actions, grants, and hierarchy management.

Testing

composer test

Coverage output: tests/html-coverage/ and tests/clover-coverage/clover.xml.

Every class in src/ must declare PHPUnit coverage attributes — requireCoverageMetadata=true is enforced in phpunit.xml.

License

MIT — see LICENSE.