apavliukov/laravel-authorization
A Laravel authorization package for defining and enforcing access rules. It helps organize roles, permissions, and policy-style checks so you can control which users can perform actions in your application.
This is a scope-cut release, not a feature release. v1.6.0 deletes a large slice of public API in a minor version on purpose — the package now only carries what oohire and opiny both need: single-database organizations, roles inside them, and a platform admin. Nothing about the tenancy data layer changed ("the remaining behaviour is unchanged"), so the v1.4.0 BelongsToTenant + TenantScope contract and its dispatch-site audit still stand. What changed is everything around it.
Upgrade path:
composer update apavliukov/laravel-authorization, then delete enterable_scope, fallback_to_owned and areas from a published config/authorization.php. A stale config is the most likely source of confusing boot-time behaviour.grep -rnE "TeamResolver|SetPermissionsTeam|resolveTeamsUsing|BypassStrategy|RoleBypass|NoBypass|BypassGate|bypassUsing|resolveTenantUsing|tenantColumn|systemAbilities|currentTenant\(|UserPermissionMemo|forgetUserPermissions|CurrentArea|choose\(|clearChoice|TenantChoiceDropped|PermissionSync" app/ config/ tests/ routes/authorization:sync, authorization:repair-ownership, or make:authorization-policy. Those commands are gone; a deploy hook referencing them fails at the worst moment.CurrentArea::SESSION_KEY needs its own session key (old sessions will simply miss — decide whether that's a logout or a silent re-route), and any CurrentLayout-style component reading CurrentArea needs its own area tracking.Look first at TenantScopedPolicy — it now compares Tenancy::id() against the model's tenancy.foreign_key, which means your policy wiring depends on that config value being right for every model. That single comparison is the new spine of authorization.
First real use case: an app with teams but no tenancy (the old TeamResolver crowd). You no longer get a resolver — you call setPermissionsTeamId() from your own middleware. That is the whole migration for that pattern:
// app/Http/Middleware/SetPermissionsTeam.php
public function handle($request, Closure $next)
{
setPermissionsTeamId($request->user()?->current_team_id);
return $next($request);
}
Tenanted apps keep using SetCurrentTenant, which now also sets the permissions team — one fewer thing to wire, but also one fewer thing to override.
The team/tenant split is now your job. Without TeamResolver, the mapping from request → team lives in middleware. Two rules: set it before anything that authorizes (a policy run in a service provider or a Gate::before consumer sees whatever was last set — possibly the previous request in an Octane worker), and reset it to null for unauthenticated/platform-admin requests rather than leaving the prior value in place.
One Gate::before, and it must never return false. bypassUsing() is gone; the provider registers a single Gate::before with the same rule — super-admin passes, everyone else falls through. If you previously had your own Gate::before returning false for non-admins, delete it or merge it: false short-circuits every policy in the app. Your replacement hook is:
Gate::before(fn ($user, $ability) => $user->isSuperAdmin() ? true : null);
Under teams, only the global role assignment counts as super-admin — a team-scoped admin assignment does not. If you customized RoleBypass to widen or narrow that, you now own the check entirely.
System abilities become plain gates. systemAbilities() is gone:
Gate::define(SystemAbility::ViewAnyReports->value, static fn (): bool => false);
Returning false (not null) is deliberate — it denies everyone without pretending the ability doesn't exist, which keeps @can and Gate::allows() consistent.
userCan() is now plain $user->can(). The per-user permission verdict memo (UserPermissionMemo, forgetUserPermissions()) is removed, so a loop authorizing 200 rows re-evaluates the permission graph 200 times. If you were relying on the memo for a hot path, cache at the call site — and if you were relying on forgetUserPermissions() for correctness after a role change within the same request, you no longer need it, because there's nothing stale to forget. Role-read memoization and forgetUserRoles() survive; keep calling it after role mutations.
Tenant picker logic moves to your switcher. choose() / clearChoice() / TenantChoiceDropped and the enterable_scope / fallback_to_owned knobs are gone. What remains in TenantResolver: it drops a gone (or soft-deleted) tenant and picks the user's sole existing one. Everything narrower — active-only, owned-first, remembered choice in session — is app code now. That's fine, but it means the "why did I land in tenant X?" question is answered in your codebase, not the package's.
Sync tooling collapses into the seeder. PermissionSync::plan() and apply(prune: true) are gone; AuthorizationSeeder still syncs permissions and roles and re-derives owner rows on every run. Drop authorization:sync from deploy scripts and seed instead — and be aware you've lost the dry-run/plan affordance, so verifying what a prune would delete now requires seeding into a scratch database.
Tenancy patterns from 1.4.0 are unchanged. Still wrap the dispatch (forTenant($tenant)->run(...)), still iterate tenants explicitly in commands and scheduled tasks, still treat authorize() (abilities) and TenantScope (existence) as orthogonal. Nothing in this release softens the security posture; the audit you did for 1.4.0 doesn't need redoing, but don't let the churn of this upgrade tempt anyone into "helpfully" adding withoutGlobalScopes() around restore paths while refactoring.
composer update succeeds, the app boots, and only the code paths you actually exercise explode. Run the full suite, and specifically exercise admin screens, team switching, policy tests, and any @can in Blade — those are where the removed symbols hide.Gate::before returning false is the highest-severity trap here. The package's rule depends on non-admins falling through. A leftover false from a previous BypassGate-era customization silently denies every policy in the app, and it looks like a permissions-seeding problem, not a gate problem.TenantScopedPolicy now hinges on tenancy.foreign_key. If a model's foreign key doesn't match that config, the policy compares against the wrong column and either over-permits or 404s into ModelNotFoundException territory. After the refactor, assert cross-tenant denial for each tenanted model, not just one.setPermissionsTeamId(). Your new middleware is request-scoped; a worker that processes a job after a request can inherit the team ID. Set it at the start of handle() for jobs that authorize, and clear it in terminate() if you're on Octane.UserPermissionMemo can be a performance regression, not a correctness one. Watch query counts on index pages that render per-row @can. The fix is app-side caching keyed on user + team, invalidated alongside forgetUserRoles().authorization:repair-ownership in a release script, a CI job, or a scheduler entry is invisible to your test suite and fatal at deploy time. Grep your docker/, .github/, deploy.php, and Kernel::schedule for the three command names.CurrentArea::SESSION_KEY, define your own constant and decide what happens to sessions carrying the old key — the package won't read it, so users may land on a default area rather than their last one. Usually acceptable; make it a decision rather than a surprise.BelongsToTenant trait (still where the scope attaches on restore — bespoke hydration still needs verifying), forgetUserRoles(), AuthorizationSeeder, TenantScopedPolicy, and the tenant resolver's drop-the-gone-tenant behaviour. Everything else you used to hook into is now yours.Verdict: upgrade, but schedule it as a refactor rather than a patch. The work is a symbol sweep, a config cleanup, a deploy-script edit, and — for teams-without-tenancy apps — new middleware plus a re-homed tenant picker. Budget a day, do it on a branch, and use the opportunity to re-assert cross-tenant denial per model now that TenantScopedPolicy reads tenancy.foreign_key.
How can I help you explore Laravel packages today?