Authorization¶
Once a user is admitted (Authentication), a resource’s policy
decides what they may do with it, and its scope() decides which
records they see. Both are enforced by the API; the UI only reflects the
result.
Policies¶
policy() names a class implementing ResourcePolicyInterface,
evaluated through kinetis/authorization’s Gate. Extend
ResourcePolicy and override only what you restrict:
final class OrderPolicy extends ResourcePolicy
{
public function update(CurrentUserInterface $user, array $record): bool
{
return $record['status'] !== 'shipped';
}
public function delete(CurrentUserInterface $user, array $record): bool|AuthorizationResponse
{
return AuthorizationResponse::deny('Orders are cancelled, not deleted.');
}
}
A denial with a message shows that message to the user.
Ability |
Decides |
|---|---|
|
Listing the resource, and seeing one record. |
|
Writing. |
|
The trash; both follow |
|
Exporting, as CSV or XLSX. |
|
Running an action, alongside the action’s own |
|
Seeing a field at all. |
|
Adding and removing one record in a many-to-many; both follow |
|
Making a shared view everyone’s default. |
A resource without a policy uses AllowAllPolicy: every user the
access policy admits may do everything.
Listing and reaching¶
A user viewAny() refuses cannot reach a record by identifier either:
show, audit, files, related lists and writes all answer 403, as the
list does. A relation value — a belongs-to choice, a many-to-many list —
must name a record the target resource’s scope() lists for the
writer; one outside it answers not_found, the same as one that does
not exist.
Row scope¶
scope() on the resource narrows the query for a user — a sales rep
sees their own customers. It applies to every read: lists, counts,
summaries, exports, badges and search.
public function scope(Query $query, CurrentUserInterface $user): Query
{
return $user instanceof SalesRep ? $query->where('owner_id', '=', $user->id()) : $query;
}
Field visibility¶
viewField($user, $field) hides a field from a user completely: it
leaves their descriptor, every record, the audit trail, exports and MCP
results, and a filter, sort, grouping, column or written value naming
it is refused as unknown. Search does not reach it.
public function viewField(CurrentUserInterface $user, string $field): bool
{
return $field !== 'phone' || $user instanceof SupportLead;
}
The primary key cannot be hidden; a hidden title field falls back to the key. Hiding a field that a new record requires makes creating impossible for that user, and the descriptor says so.
Membership¶
attach($user, $record, $relation, $related) and detach(...) decide
adding and removing one record in a many-to-many relation; both follow
update unless overridden. They decide the
POST …/{id}/{relation}/attach and …/detach endpoints the UI’s
relation panels use, and an update that writes the whole list: every id
it adds is an attach, every id it drops a detach, and one refusal
refuses the update.
Everyone’s default view¶
A saved view that is shared can also be flagged resourceDefault. The
list then opens with it for every user who has no default view of their
own. A resource has at most one such view: flagging another view clears
the first. setDefaultView($user) decides who may set the flag, or save
a view that keeps it. Anyone may drop it, and the schema publishes the
answer as permissions.setDefaultView.
Per-record permissions¶
Every record the API returns carries what this user may do with it —
update, delete, restore, forceDelete and the actions they may
run — and the UI shows exactly those buttons.
See also¶
Authentication — who is admitted.
Audit trail and impersonation — what is recorded.