carlopaa/access-control

Tenant-aware access control with roles, groups, and permissions

Maintainers

Package info

github.com/carlopaa/access-control

pkg:composer/carlopaa/access-control

Fund package maintenance!

carlopaa

Statistics

Installs: 1

Dependents: 0

Suggesters: 0

Stars: 0

Open Issues: 0

v0.1.3 2026-03-28 03:11 UTC

This package is auto-updated.

Last update: 2026-03-28 04:47:00 UTC


README

Latest Version on Packagist Total Downloads

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
  • :deny permissions override allows

Table of contents

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

  1. Add the trait to your user model:
use Aapolrac\AccessControl\Concerns\HasAccessControl;

class User extends Authenticatable
{
    use HasAccessControl;
}
  1. Add a JSON permissions column to users (for direct permissions):
Schema::table('users', function (Blueprint $table) {
    $table->json('permissions')->nullable();
});
  1. Configure config/access_control.php (models, enum classes, groups).

  2. Seed permissions from your enums:

php artisan access-control:sync
  1. 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\Role
  • Aapolrac\AccessControl\Models\Group
  • Aapolrac\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.permission
  • access.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.