Version v1.6.5 Older versions: docs on GitHub
Authorization
On this page
Access to the panel and to data is checked on two levels:
- Who can enter the panel: the
viewLarsadmingate. - What the user can do with models: Laravel policies, plus scoping of the visible rows.
Panel access
Every route behind the login screen goes through the Authenticate middleware (the user is logged in on
the panel's guard) and the Authorize middleware (runs the check below). Otherwise the response is 403
(for JSON requests without X-Inertia it is a JSON {"message": "..."}).
401 (not logged in) and 403 responses to JSON requests contain a translated message
(larsadmin::messages.unauthenticated / larsadmin::messages.forbidden) in the panel language.
Authorize calls Larsadmin::check($request). The application provider
(App\Providers\LarsadminServiceProvider) sets the check up like this:
(config('larsadmin.allow_local', true) && app()->environment('local'))
|| Gate::forUser($request->user(Larsadmin::guard()))->check('viewLarsadmin')
The viewLarsadmin gate
The base provider defines a gate that is closed to everyone. Open it by overriding the gate() method
in app/Providers/LarsadminServiceProvider.php:
use Illuminate\Support\Facades\Gate;
use Maxcxam\Larsadmin\LarsadminApplicationServiceProvider;
class LarsadminServiceProvider extends LarsadminApplicationServiceProvider
{
protected function gate(): void
{
Gate::define('viewLarsadmin', function ($user) {
return in_array($user->email, [
'[email protected]',
]);
});
}
}
You can put any logic there: a role, a permission, an is_admin column.
allow_local
With APP_ENV=local and larsadmin.allow_local = true (the default), the gate is not checked: every
authenticated user gets into the panel. This is convenient for development (the demo application the
package is developed against works this way), but dangerous on shared or public servers.
LARSADMIN_ALLOW_LOCAL=false # always require the gate
Custom access check
Instead of the gate, you can register any closure by overriding the authorization() method in
app/Providers/LarsadminServiceProvider.php:
use Illuminate\Http\Request;
use Maxcxam\Larsadmin\Larsadmin;
protected function authorization(): void
{
Larsadmin::auth(fn (Request $request) => $request->user()?->isAdmin() === true);
}
Such a closure fully replaces the base provider's check (allow_local and the gate no longer apply).
Without the application provider, only allow_local in local applies; in other environments there is
no access.
A separate guard
LARSADMIN_GUARD (or guard in the config) sets the panel's authentication guard. The same guard is
used for the gate and for request->user().
Model policies
An Entity checks the policy of its model ($model) for these abilities:
| Action | Ability | Argument |
|---|---|---|
| Navigation, list, access to the Entity | viewAny |
the model class |
Create button and page, store |
create |
the model class |
| Viewing a record | view |
the record |
Editing, update |
update |
the record |
| Deleting | delete |
the record |
Example app/Policies/TagPolicy.php: tags can be viewed and changed, but not deleted.
class TagPolicy
{
public function viewAny(User $user): bool { return true; }
public function view(User $user, Tag $tag): bool { return true; }
public function create(User $user): bool { return true; }
public function update(User $user, Tag $tag): bool { return true; }
/** Tags may never be deleted from the panel. */
public function delete(User $user, Tag $tag): bool { return false; }
}
The UI shows buttons based on the same checks, but the server always checks again.
Policies fail closed
If a policy is registered for the model, the decision is fully delegated to the Laravel gate:
- a missing policy method means denied;
- the policy's
before()hooks (andGate::before) work as usual.
This also applies to the attach/detach permissions on relations (see Relations),
except that a missing method there falls back to the update ability.
Models without a policy: allow_without_policy
When there is no policy, larsadmin.allow_without_policy applies:
| Value | Result |
|---|---|
true (default) |
everything is allowed |
false |
everything is denied |
For production, use false and explicit policies for every managed model.
Scoping rows
A policy answers "is this allowed at all", not "which rows exactly". Rows are scoped by the Entity's static hooks (Entities):
class OwnPost extends Entity
{
public static string $model = \App\Models\Post::class;
public static function indexQuery(Request $request, Builder $query): Builder
{
return $query->where('user_id', $request->user()->getKey());
}
}
indexQuery applies to the list and to the relation panels. detailQuery (equal to indexQuery by
default) applies to show, edit, update, delete and actions on selected records. A record outside the
scope returns 404. relatableQuery decides what can be selected in relations, and also applies during
validation.
Actions on records additionally check update for each record (Actions).
Global search
The global search (Ctrl/⌘+K) follows the same rules as the Entity list:
- An Entity is searched only when it has
$globallySearchable = trueand the user hasviewAny. Otherwise it is silently skipped and not mentioned in the response. - Rows only come from
indexQuery(), also when the search goes through Laravel Scout: the engine only proposes keys, and the records are loaded from the database within the user's scope. - Columns of fields hidden with
canSeeare not searched (SQL path). On the Scout path, the engine's index decides what matches, so a field hidden withcanSeebut present intoSearchableArray()can still make a record match. - The title and subtitle never show fields hidden with
canSeeor related records outside the related Entity's scope. - Each result is checked against
view; records without permission are dropped. - Results are not cached; the endpoint has the
larsadmin-searchrate limiter.
A Scout index knows nothing about canSee: keep sensitive fields out of toSearchableArray() or turn
Scout off for the Entity with usesScout(): false. See Global search.
Audit log
The audit log has its own viewLarsadminAudit gate, which the package does not define: until the
application defines it, nobody sees the log (fail closed). allow_local does not apply to it.
protected function gate(): void
{
Gate::define('viewLarsadmin', fn ($user) => in_array($user->email, ['[email protected]']));
Gate::define('viewLarsadminAudit', fn ($user) => $user->is_admin);
}
- The "Audit log" page shows entries only of the Entities the user has
viewAnyon. - A record's history also requires the record to be within
detailQuery()and the user to haveviewon it. - Changes to fields hidden from the viewer with
canSeeare not shown. - The "Audit log" page is not row-scoped: it does not check
indexQuery()andviewfor each entry, so a holder of the gate sees every entry of each Entity they haveviewAnyon. The gate is the trust boundary: in multi-tenant applications, grant it only to those who may see the data of every tenant.
See Audit log.
Notifications and background actions
- The bell shows only notifications of type
larsadminaddressed to the current user of the panel's guard. Another user's or an unknown notification answers 404. There is no separate gate: panel access is enough. - A background action is run by the queue worker as the user who started it, repeating every check
(fail closed):
viewLarsadmin,viewAnyon the Entity, the action'scanSee(),authorizedToRunStandalone()orauthorizedToRun()for each record,indexQuery()/detailQuery(). Rights revoked between starting and running take effect: records are skipped or the run fails. Before every chunk the user is reloaded and panel access is checked again.session()in the worker is a separate empty session of the run, so session-based scoping never sees another run's data. - Runs are visible to and cancellable by their owner only. Administrators do not see other users'
runs; only the owner downloads an
ActionResponse::file()result. - The panel's realtime channels (
private-larsa_inbox.*) are authorized only by the panel'sPOST {path}/broadcasting/auth; the application's/broadcasting/authdoes not authorize them.
See Notifications and background actions.
Checklist
- Restrict the
viewLarsadmingate to real conditions; turnallow_localoff outside development. - Create policies for the managed models and consider
allow_without_policy => false. - Add
indexQueryif a user may see only some of the rows. - For Entities with
$globallySearchableand Laravel Scout, check thattoSearchableArray()holds no fields hidden withcanSee. - When the audit log is on, grant the
viewLarsadminAuditgate only to those who may see the history of every record of the Entities they can reach. - For background actions, check that
canSee(),canRun(),fields(), policies andindexQuery()do not depend on the session or request headers: on the queue the session is empty and there are no headers. Tenant scoping read from the session does not work there (it gets an empty value). - Do not use the
larsa_prefix (LARSADMIN_CHANNEL_PREFIX) for the application's broadcast channels. - Do not run public servers with
APP_ENV=local.