Skip to content

Security and authorization ​

Arrel decides access on the server, at every endpoint. The frontend hides the buttons you cannot use, but that is a convenience: the real protection is that the API refuses them.

Layers of a request ​

A request to a Resource's API goes through, in this order:

  1. The middleware of config('arrel.middleware') (for example, identifying the tenant).
  2. The Sanctum session (auth:sanctum).
  3. Email verification and the second factor, if they are enabled (see Authentication).
  4. The middleware of config('arrel.authenticated_middleware').
  5. The authorization of the Resource, the page or the action.

Resources ​

The five methods canViewAny(), canView(), canCreate(), canEdit() and canDelete() delegate to the model's Policy, or can be overridden (see Resources).

A Resource for which canViewAny() is false is closed at every endpoint: the listing, but also the schema, creation, editing, deletion, actions, field options and relations answer 403. Without that rule, a model with no Policy would leave canCreate() or canEdit() open independently.

This means that, for a model without a Policy, everything is allowed for any authenticated user. If the Resource must be restricted, define a Policy or override canViewAny().

Actions ​

  • An action with authorize() uses that closure, with the same arguments as handle().
  • An action without authorize() requires canEdit() on the record, or canCreate() if it is standalone(). Declare authorize() on actions that must stay open to more users.
  • The built-in actions follow their own ability: ViewAction asks for canView(), EditAction and RestoreAction ask for canEdit(), DeleteAction and ForceDeleteAction ask for canDelete().
  • The options an action offers for its relation fields are only served to users who can run it.

Relations ​

If a Relation field declares query(), the scope is also applied on save: a record that the scope hides from the dropdown is refused with a validation error, even if someone sends its identifier by hand. It works the same in the forms of a Resource, of a Relation manager and of an action.

php
Relation::make('author_id')
    ->related(User::class)
    ->query(fn (Builder $query): Builder => $query->where('team_id', auth()->user()->team_id))

In the query() closure of a field in a Relation manager or an action, the parent record arrives as the third argument.

File uploads ​

  • Uploads are limited to 60 per minute and user.
  • When saving a record, only files that were really uploaded to the temporary directory (arrel-temp) are moved. A value that does not match a real upload is not moved.
  • The content of RichEditor fields is sanitized on the server on every save.

Attempt limits ​

WhatLimit
Sign-in5 failed attempts per email and IP address
Second factor of a pending sign-in5 wrong codes, and it expires after 5 minutes
Sending the verification email3 per minute
Confirming or turning off the second factor10 per minute
File uploads60 per minute and user

Pages ​

Page::canView() decides whether a page is visible and accessible. A page that cannot be viewed leaves the navigation and answers 403 on the API. For a CustomPage, visible() controls the navigation entry; the authorization of the data the page loads must be done at your application's endpoints, which are your code.

Checklist ​

  • Every exposed model has a Policy, or the Resource overrides the can...() methods.
  • Actions that must not be only for editors declare authorize().
  • Relation fields that show other people's records have query().
  • sanctum.stateful contains only your panel's domains.
  • The second factor is mandatory (isRequiredFor()) for the roles with the most privileges.