This package implements object-level, model-level and simple permissions for Laravel.
Once installed you can do stuff like this:
$permission = $article::getPermission("edit");
$user->permissions->grant($permission, $article);
$user->permissions->revoke($permission);
$user->permissions->revokeAll();
If you want to add permissions to your users, you will need to add an attribute to store the permissions. You can call it anything you want:
$table->jsonb('permissions');
You need to cast it to AsPermissions
or AsScopedPermissions
depending on
whether you need permissions to be global or scoped (e.g. to a tenant):
use FossHaas\LaravelPermissionObjects\AsPermissions;
protected function casts(): array
{
return [
'permissions' => AsPermissions::class,
];
}
You can define permissions in your service provider's boot
method:
use App\Models\User;
use FossHaas\LaravelPermissionObjects\Permission;
Permission::register(User::class, [
'manage' => fn() => __('Manage users'),
'change-passwords' => fn() => __('Change user passwords'),
]);
By default permission names will be qualified with the full name of the model class they're defined for. If you want nicer looking (shorter) names, you can use morph maps. Make sure to define your morph maps before looking up permissions:
use Illuminate\Database\Eloquent\Relations\Relation;
Relation::morphMap([
'user' => \App\Models\User::class,
]);
You can also define permissions for classes that are not models:
Permission::register(Permission::class, [
'assign' => fn() => __('Assign permissions'),
]);
You can look up permissions you have defined using the short name and the name of the class they were defined for:
use App\Models\User;
$manageUsersPermission = Permission::resolve('manage', User::class);
// This also works if your morph map is set up correctly:
$manageUsersPermission = Permission::resolve('manage', 'user');
You can also look them up using the fully qualified name:
use App\Models\User;
$manageUsersPermission = Permission::find(User::class . '.' . 'manage');
// This also works if your morph map is set up correctly:
$manageUsersPermission = Permission::find('user.manage');
If you want to look up permission objects from the models you define them for,
you can also add the HasPermissions
trait to them:
use FossHaas\LaravelPermissionObjects\Traits\HasPermissions;
class User extends Authenticatable {
use HasPermissions;
// ...
}
Now you can look up permissions defined for your model on the model:
$manageUsersPermission = User::getPermission('manage');
The Permission
class comes with this trait already baked in so there is no
need to extend it if you want to define permissions for it:
use FossHaas\LaravelPermissionObjects\Permission;
$assignPermissionsPermission = Permission::getPermission('assign');
You can use the methods on your model's permissions attribute to manage its
assigned permissions. You can either pass the key (or ID) of the instance (object)
you want the permission to be restricted to or null
if you want the
permission to be valid for any instance of its type.
When passing an ID make sure it is cast to a string if it isn't one already:
use App\Models\User;
use FossHaas\LaravelPermissionObjects\Permission;
$permission = Permission::find('article.edit');
// grant permission to edit only a specific article
$user->permissions->grant($permission, (string) $article->id);
// grant permission to edit any article
$user->permissions->grant($permission, null);
// revoke only permission to edit a specific article, if it was granted
$user->permissions->revoke($permission, (string) $article->id);
// revoke only permission to edit any article, if it was granted
$user->permissions->revoke($permission, null);
Granting a model level permission (using null
) will override any existing
object level permissions (using object IDs) of the same permission type.
Revoking a model level permission has no effect if the user was only granted
object level permissions. In this case revokeAll
can be used to revoke any
model or object level permissions of the permission type:
// revoke all permissions of the given permission type
$user->permissions->revokeAll($permission);
// revoke all permissions ever granted to the user
$user->permissions->revokeAll();
The permissions attribute supports a simple presence check:
use App\Models\User;
use FossHaas\LaravelPermissionObjects\Permission;
$permission = Permission::find('article.edit');
// Check if the user has the permission for this instance
$user->permissions->has($permission, (string) $article->id);
// Check if the user has the permission for all instances
$user->permissions->has($permission, null);
If a permission was granted at the model level (using null
), any instance
level checks will also pass:
$user->permissions->grant($permission, null);
// This will always pass
$user->permissions->has($permission, (string) $article->id);
The permissions attribute also provides a can
method which can be used in a
Gate::after
fallback in your service provider if you don't want to set up
gates or policies yourself:
use App\Models\User;
use Illuminate\Support\Facades\Gate;
Gate::after(function (User $user, string $ability, bool|null $result, mixed $arguments) {
if ($result !== null) return $result;
$object = isset($arguments[0]) ? $arguments[0] : null;
return $user->permissions->can($ability, $object);
});
// Elsewhere ...
Gate::authorize('edit', [$article]);
This also works when using AsScopedPermissions
:
Gate::after(function (User $user, string $ability, bool|null $result, mixed $arguments) {
if ($result !== null) return $result;
$object = isset($arguments[0]) ? $arguments[0] : null;
$scopes = isset($arguments[1]) ? $arguments[1] : "";
return $user->permissions->can($ability, $object, $scopes);
});
// Elsewhere ...
Gate::authorize('edit', [$article, $scope]);
Note that the can
method returns null
when passed a permission name it does
not recognize or that can't be resolved using the object or object type it is
passed.
Permissions don't have to be tied to specific models or classes. You can
define simple permissions by passing null
instead of a class when registering
them:
Permission::register(null, [
'self-destruct' => fn() => __('Initiate self-destruct')
]);
Note that you will still need to pass null
as an object ID when using this
permission as this argument is intentionally not optional to avoid mistakes:
$permission = Permission::find('self-destruct');
$user->permissions->has($permission, null);
// This also works:
$user->permissions->can('self-destruct', null);
If you want to misuse the object ID for your own purposes, keep in mind that
the can
method will not work correctly as it expects the string
argument
to be a class name and will attempt to resolve the permission name using it:
// This DOES NOT work:
$user->permissions->can('self-destruct', '1234'); // Always returns null!
Although not built for this purpose, simple permissions can be used to
implement a "super admin" flag that will pass all Gate
or Policy
checks:
use App\Models\User;
use Illuminate\Support\Facades\Gate;
use FossHaas\LaravelPermissionObjects\Permission;
Permission::register(null, [
'is-super-admin' => fn() => __('Is Super Admin'),
]);
// Use as a fallback check if no other Gate or Policy applied
Gate::after(function (User $user): bool {
return $user->permissions->has('is-super-admin', null);
});
// Alternatively, if super admins should always bypass all rules
Gate::before(function (User $user): bool|null {
return $user->permissions->has('is-super-admin', null) ?: null;
});
When using AsScopedPermissions
, you can pass in an additional scopes
parameter to method calls to define which scope or scopes the method should
consider:
$user->permissions->grant($permission, $objectId, $scope);
Alternatively, you can use the scope
method to access the AsPermissions
for that scope directly:
$user->permissions->scope($scope)->grant($permission, $objectId);
Scopes are identified by their name as string values. The meaning of scopes is up to your application's needs but could range from organizational units of your company to different customers in a poor man's single-database multi-tenancy implementation.
The default or global scope is identified by AsScopedPermissions::DEFAULT_SCOPE
(which is set to the empty string) and will be used if no scope is passed
explicitly.
All permission checks using has
or can
will always also check the default
scope in addition to any scopes passed explicitly.
You can also use AsScopedPermissions::ALL_SCOPES
(which is set to '*'
) to
refer to all scopes, e.g. to revoke a given permission across all scopes:
// This is also the default behavior of `revokeAll` if no scopes are specified
$user->permissions->revokeAll($permission, AsScopedPermissions::ALL_SCOPES);
If you want to implement role-based authorization, you can create a role model
and give it an AsPermissions
attribute just as you would for a user model. As
this package aims to be unopinionated, how you use this model is up to you,
but a possible schema could look like this:
Schema::create('roles', function (Blueprint $table) {
$table->id();
$table->string('name');
$table->json('permissions');
});
Schema::create('user_roles', function (Blueprint $table) {
$table->foreignIdFor(User::class)
->constrained('users')->cascadeOnDelete();
$table->foreignIdFor(Role::class)
->constrained('roles')->cascadeOnDelete();
});
Copyright (c) 2004 Foss & Haas GmbH.
This package is licensed under the terms of the MIT license.