NewWe open-sourced 50+ Laravel packages
Custom AI apps, agents and automation — Roundly ConsultingRoundly
All packages
Permissions for Laravel

Tenant-defined roles

With assignments on, a role can belong to one owner — a tenant, a team — so two tenants can each define reviewer without clashing with each other or with the global reviewer. Permissions stay global code constants; only roles are owned:

use RoundlyConsulting\Permissions\Facades\Permissions;

Permissions::owner($tenant)->role('reviewer')->givePermissionTo('posts.review'); // find or create
Permissions::owner($tenant)->findRole('reviewer');              // ?Role
Permissions::owner($tenant)->roles();                           // Collection<int, Role>, by name
Permissions::owner($tenant)->syncRolesFrom(TenantRole::class);  // SyncResult, additive

Permissions::for($user)->in($tenant)->assignRole('reviewer');   // the tenant's own reviewer

owner()->role() finds or creates the role race-safely under the unique (owner_key, name) index, through your configured role model. syncRolesFrom() registers one owned role per enum case, additively, like the global version.

Which role a name means

Inside a scope, a role name resolves to the scope’s own role first, then the role of an extra owner you register for that scope, then the global role; an unknown name throws RoleDoesNotExist. An owned role is assignable only inside its owner’s scope — passing an owned Role model anywhere else, another scope, an unscoped write or a 2.0 assignRole(), throws PermissionException. Widen it with a resolver:

use Illuminate\Database\Eloquent\Model;
use RoundlyConsulting\Permissions\Facades\Permissions;

// A project uses the roles its tenant defined
Permissions::resolveRoleOwnerUsing(
    fn (Model $scope): ?Model => $scope instanceof Project ? $scope->tenant : null,
);

Permissions::for($user)->in($project)->assignRole('reviewer'); // the tenant's reviewer

Global verbs see global roles

With the flag on, the 2.0 catalog verbs work on global roles only: role(), findRole(), roles(), syncRolesFrom(), Role::findOrCreate(), name resolution in an unscoped assignRole() and the role() query scope all ignore owned roles. A role knows its owner:

$role->owner;      // the owning model (MorphTo), null for a global role
$role->isOwned();  // bool

owner_type and owner_key are hidden when a role is serialized, because they carry a class name; owner_id is not. Permissions::forgetScope($tenant) deletes the roles the tenant owns along with its assignments, and pruneOrphans() prunes roles whose owner no longer exists.

Show your open-source love

This package is free and MIT-licensed. If it saves you time, a one-off donation or a Patreon membership keeps it maintained, tested and documented.

More ways to support, including crypto

By donating, you agree to our donation terms.

Want this built into your product?

We integrate our packages into custom Laravel and AI builds. Tell us what you're working on and we'll reply within 48 hours.