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
- Installation
- Quick start
- Required model setup
- Configuration reference
- Commands
- Using permissions and roles
- Role to default group sync
- Middleware
- Gate integration
- Tenant resolution
- Testing
Install via Composer:
composer require carlopaa/access-controlPublish migrations and run them:
php artisan vendor:publish --tag="access-control-migrations"
php artisan migratePublish config:
php artisan vendor:publish --tag="access-control-config"- 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');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();
}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,
],Check package installation:
php artisan access-controlSync permission records from configured enum classes:
php artisan access-control:sync
php artisan access-control:sync --only-missing$user->hasPermission('member:view-any');
$user->hasAnyPermission(['member:view-any', 'member:update']);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.
$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$user->hasRole('owner');
$user->hasAnyRole(['owner', 'manager']);
$user->hasRoleInOrg('owner', $organizationId);
$user->hasAnyRoleInOrg(['owner', 'manager'], $organizationId);User::query()->withRole('owner')->get();
User::query()->withAnyRoles(['owner', 'manager'])->get();
User::query()->withRoleInOrg('owner', $organizationId)->get();
User::query()->withAnyRolesInOrg(['owner', 'manager'], $organizationId)->get();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]);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" --forceThe 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 () {
// ...
});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.
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;
}Run package tests:
composer testRun static analysis:
composer analyseThe MIT License (MIT). Please see License File for more information.