carlopaa / access-control
Tenant-aware access control with roles, groups, and permissions
Fund package maintenance!
Requires
- php: ^8.3
- illuminate/contracts: ^11.0||^12.0||^13.0
- spatie/laravel-package-tools: ^1.16
Requires (Dev)
- larastan/larastan: ^3.0
- laravel/pint: ^1.14
- nunomaduro/collision: ^8.8
- orchestra/testbench: ^10.0.0||^9.0.0
- pestphp/pest: ^4.0
- pestphp/pest-plugin-arch: ^4.0
- pestphp/pest-plugin-laravel: ^4.0
- phpstan/extension-installer: ^1.4
- phpstan/phpstan-deprecation-rules: ^2.0
- phpstan/phpstan-phpunit: ^2.0
- spatie/laravel-ray: ^1.35
This package is auto-updated.
Last update: 2026-03-28 04:47:00 UTC
README
carlopaa/access-control is a tenant-aware access control package for Laravel.
It combines role-based and permission-based patterns:
- users get roles in an organization context
- roles can map to default groups
- groups aggregate permissions
- users can also receive direct permissions
:denypermissions override allows
Table of contents
- Installation
- Quick start
- Required model setup
- Configuration reference
- Commands
- Using permissions and roles
- Role to default group sync
- Middleware
- Gate integration
- Tenant resolution
- Testing
Installation
Install via Composer:
composer require carlopaa/access-control
Publish migrations and run them:
php artisan vendor:publish --tag="access-control-migrations"
php artisan migrate
Publish config:
php artisan vendor:publish --tag="access-control-config"
Quick start
- Add the trait to your user model:
use Aapolrac\AccessControl\Concerns\HasAccessControl; class User extends Authenticatable { use HasAccessControl; }
- Add a JSON
permissionscolumn to users (for direct permissions):
Schema::table('users', function (Blueprint $table) { $table->json('permissions')->nullable(); });
-
Configure
config/access_control.php(models, enum classes, groups). -
Seed permissions from your enums:
php artisan access-control:sync
- Use checks in code:
$user->hasRole('owner'); $user->hasPermission('member:view-any');
Required model setup
The trait expects roles() and groups() relations on the user model.
use Illuminate\Database\Eloquent\Relations\BelongsToMany; public function roles(): BelongsToMany { return $this->belongsToMany( Role::class, config('access_control.tables.role_user', 'role_user') )->withPivot('organization_id')->withTimestamps(); } public function groups(): BelongsToMany { return $this->belongsToMany( Group::class, config('access_control.tables.group_user', 'group_user') )->withPivot('organization_id')->withTimestamps(); }
Your Group model should have permissions relation:
public function permissions(): BelongsToMany { return $this->belongsToMany( Permission::class, config('access_control.tables.group_permission', 'group_permission') )->withTimestamps(); }
Configuration reference
config/access_control.php:
return [ 'models' => [ 'role' => Aapolrac\AccessControl\Models\Role::class, 'group' => Aapolrac\AccessControl\Models\Group::class, 'permission' => Aapolrac\AccessControl\Models\Permission::class, ], 'tables' => [ 'roles' => 'roles', 'groups' => 'groups', 'permissions' => 'permissions', 'role_user' => 'role_user', 'group_user' => 'group_user', 'group_permission' => 'group_permission', ], 'context_cache' => [ 'enabled' => true, 'key' => 'permissions', ], 'permissions' => [ 'enum_classes' => [ App\Enums\MemberPermission::class, App\Enums\CustomerPermission::class, ], ], 'groups' => [ // role key => default group keys 'owner' => ['owners', 'team-management'], 'manager' => ['team-management'], ], ];
By default, the package ships with these models:
Aapolrac\AccessControl\Models\RoleAapolrac\AccessControl\Models\GroupAapolrac\AccessControl\Models\Permission
In your app, you can override them in models with your own Eloquent classes.
Example override:
'models' => [ 'role' => App\Models\Role::class, 'group' => App\Models\Group::class, 'permission' => App\Models\Permission::class, ],
Commands
Check package installation:
php artisan access-control
Sync permission records from configured enum classes:
php artisan access-control:sync php artisan access-control:sync --only-missing
Using permissions and roles
Permission checks
$user->hasPermission('member:view-any'); $user->hasAnyPermission(['member:view-any', 'member:update']);
Deny override
If a user has member:view-any:deny, then hasPermission('member:view-any') returns false even if the allow exists from groups or direct permissions.
Direct permission API
$user->assignPermission('customer:view-any'); $user->assignPermissions(['member:view-any', 'member:update']); $user->revokePermission('member:update'); $user->syncDirectPermissions(['member:view-any']); $user->clearDirectPermissions(); $direct = $user->getDirectPermissions(); // Collection
Role checks
$user->hasRole('owner'); $user->hasAnyRole(['owner', 'manager']); $user->hasRoleInOrg('owner', $organizationId); $user->hasAnyRoleInOrg(['owner', 'manager'], $organizationId);
Query scopes
User::query()->withRole('owner')->get(); User::query()->withAnyRoles(['owner', 'manager'])->get(); User::query()->withRoleInOrg('owner', $organizationId)->get(); User::query()->withAnyRolesInOrg(['owner', 'manager'], $organizationId)->get();
Role to default group sync
Use RoleGroupSync when role assignment should automatically maintain configured default groups:
use Aapolrac\AccessControl\Support\RoleGroupSync; RoleGroupSync::syncDefaultsForRoles($user, $organizationId, ['owner', 'manager']);
You can also attach explicit groups by key or id:
RoleGroupSync::attach($user, $organizationId, ['team-management', 5]);
Troubleshooting
vendor:publish --tag="access-control-config" not available
If the tag is not found, run:
composer update carlopaa/access-control -W php artisan package:discover --ansi php artisan vendor:publish --provider="Aapolrac\\AccessControl\\AccessControlServiceProvider" --tag="access-control-config"
If config/access_control.php already exists, Laravel will skip it. Use force when you want to overwrite:
php artisan vendor:publish --tag="access-control-config" --force
Middleware
The package auto-registers middleware aliases:
access.permissionaccess.role
Usage:
Route::middleware(['auth', 'access.permission:member:view-any'])->group(function () { // ... }); Route::middleware(['auth', 'access.role:owner,manager'])->group(function () { // ... });
Gate integration
The package registers a Gate::before hook. If the user model has hasPermission(), then:
$user->can('member:view-any'); Gate::allows('member:view-any', $user);
Both resolve through package permissions.
Additionally, enum-based permission abilities can be auto-registered from permissions.enum_classes.
Tenant resolution
If your checks/scopes need an implicit active organization, bind your own resolver:
use Aapolrac\AccessControl\Contracts\TenantResolver; app()->bind(TenantResolver::class, YourTenantResolver::class);
Your resolver must return the current organization id (or null):
public function resolveOrganizationId(?Model $tenant = null): ?int { return $tenant?->getKey() ? (int) $tenant->getKey() : null; }
Testing
Run package tests:
composer test
Run static analysis:
composer analyse
Credits
License
The MIT License (MIT). Please see License File for more information.