#$owns
#Import
1import { $owns } from "alepha/security";
#Overview
Resource-scoped authorization gate.
Roles and permissions answer "what kind of user is this?". They cannot answer "does this user own row 42?", so that check ends up inline in every handler — where nothing enforces its presence and a forgotten call is a silent authorization hole.
$owns loads the row named by a route param, checks the caller against it,
and publishes it via OwnedResourceProvider so the handler does not
re-fetch what the gate already read.
Two checks, applied in order:
- Owner —
row[owner] === user.id. - Membership — when
viais set, a row in the join entity links the caller to this resource.
A privileged identity (user.ownership === false) bypasses both, matching
the ownership semantics $secure already applies: an admin whose grant is
not narrowed to rows they own. Note this is deliberately strict — an
undefined ownership does not bypass, because undefined only means
"no permission check ran", not "this caller is privileged".
1class CampaignController { 2 read = $action({ 3 path: "/campaigns/:id", 4 use: [ 5 $secure(), 6 $owns({ 7 repository: () => this.campaigns, 8 param: "id", 9 owner: "createdBy",10 cast: Number,11 via: {12 repository: () => this.characters,13 resource: "campaignId",14 user: "userId",15 },16 }),17 ],18 handler: async () => this.owned.get<Campaign>(),19 });20}
#Options
| Option | Type | Required | Description |
|---|---|---|---|
repository |
Object |
Yes | Repository the guarded resource is loaded from, as a thunk |
param |
string |
Yes | Route param holding the resource id. |
owner |
string |
Yes | Column on the resource holding the owner's user id. |
via |
Object |
No | Membership fallback: a join entity linking users to resources |
cast |
Object |
No | Coerce the raw string route param before querying |
message |
string |
No | Message used for both the owner and the membership denial |
secure |
Omit<SecureOptions, "guard"> |
No | Additional $secure checks layered on top — roles, permissions, issuers. |