Skip to content

Authorization

All authorization decisions go through Anvil\Security\Authorization\Gate (service('gate')). The gate denies by default.

A permission is a string like orders.view. Access is granted only when the current actor holds the permission.

$gate = service('gate');
if ($gate->allows('orders.view')) {
// ...
}
$gate->authorize('orders.update'); // throws AuthorizationException (403)

Permissions Anvil checks by itself:

Permission Checked for
<resource slug>.view, .create, .update, .delete, .history the pages and operations of a resource
<resource slug>.export exports, when the resource is exportable
<resource slug>.update actions and bulk actions without own permission (->permission('orders.cancel'))
Panel::permission() entering the panel at all (null: every logged in user)
Page::permission() a custom page (default <panel id>.page.<page slug>, null for every logged in user)
Gate::AUTHENTICATED (@authenticated) pseudo permission of every logged in user, only for data of the users themselves (profile, notifications)

All of them are listed in the role editor and by php spark anvil:permissions.

Where permissions come from is decided by a PermissionChecker (service('permissionChecker')):

Checker Use
Anvil\Access\RbacPermissionChecker default (Config\Anvil::$rbac = true): database roles of the user in the current tenant, optionally with Shield groups as fallback
ShieldPermissionChecker $rbac = false: asks Shield for the logged in user
ArrayPermissionChecker fixed permissions, for tests or system jobs

Roles are stored in the database and maintained in the panel, see Users and roles.

Config\Anvil::$shieldGroupFallback (default true) also grants the permissions of the Shield groups in Config\AuthGroups. These apply in every tenant. Turn the fallback off once roles are maintained in Anvil, so that all permissions are tenant bound and visible in the role editor.

A policy restricts access to single records, e.g. “agents may only edit orders of their own region”. It is asked after the permission check and can only restrict, never grant.

use Anvil\Security\Authorization\Policy;
/**
* @implements Policy<Order>
*/
final class OrderPolicy implements Policy
{
public function allows(string $permission, object $order): bool
{
return $order->region === current_user_region();
}
}
service('gate')->allows('orders.update', $order);

Register policies in app/Config/Anvil.php, so that they apply everywhere: panels, API clients and commands.

public array $policies = [\App\Entities\Order::class => \App\Policies\OrderPolicy::class];

service('gate')->policy(Order::class, new OrderPolicy()) registers one at runtime, e.g. in tests. API requests keep the policies of the gate (Gate::withPermissions() swaps only the permissions).

A policy registered for a class also applies to its subclasses.

Where policies apply: detail and edit pages, single and bulk actions, deleting, lists, related lists and exports (records the policy refuses are left out), selects of related records, relations (attach, detach, create), file downloads and the API.

  • Entities required: a policy is found by the class of the record. Models returning arrays ($returnType = 'array') give plain objects no policy can be registered for; such records are allowed, and a warning is logged when policies exist. Use entities (the generators create them).
  • Lists: rows that should never be listed belong into Resource::scopeQuery() as well; the totals and pages count the rows of the query.

Outside of a request, e.g. in a queue job, Anvil\Security\RunAs runs code with the permissions, tenant and audit actor of a user. The user must exist and be active; they are logged in in memory only.

use Anvil\Security\RunAs;
RunAs::user($userId, $tenantId, function ($user): void {
service('gate')->authorize('orders.update');
// ...
});