yuriitatur / permissions
simple permission manager
Requires
- php: ^8.4
- yuriitatur/nested-data: dev-master
- yuriitatur/pipeline: ^1.0
- yuriitatur/repository-laravel: dev-master
- yuriitatur/search: dev-master
- zircote/swagger-php: ^6.0
Requires (Dev)
- dg/bypass-finals: ^v1.2.2
- filament/filament: ^5.0
- orchestra/testbench: ^11.0
- phpunit/phpunit: ^13.0
Suggests
- filament/filament: ^5.0 — required for the Filament admin panel integration
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 permissions —
read:post:42,read:post:*, or a plainedit-postsstring - 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 backends —
local(PHP config arrays, read-only at runtime) andeloquent(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-routesthat 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:
| Resource | Driver mode | Capabilities |
|---|---|---|
| Roles | local | Read-only list |
| Roles | eloquent | Full CRUD + hierarchy tree + permissions relation manager |
| Permissions | local | Read-only list |
| Permissions | eloquent | Full CRUD + hierarchy tree + roles relation manager |
| Actions | local | Read-only list |
| Actions | eloquent | Full CRUD |
| Grants | local | Read-only list |
| Grants | eloquent | List + 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.