Summary#
A where row filter is an ordinary query predicate, so it may follow relations to reach the fact that decides
access. The chain can be as long as your model:
entity Note {
[Required] Folder Folder;
security {
// four hops: this note's folder → its workspace → that workspace's region → the region's head.
allow read, update where Folder.Workspace.Region.Head == user;
}
}Nothing about depth changes the rules. A four-hop rule may carry criteria on the rows it passes through, combine several chains, sit inside a collection hop, or be negated — the same as a one-hop rule.
Signature#
allow <verbs> where <chain> <op> <value>; // <chain> is Rel.Rel….Property, any length
allow <verbs> where user.<chain> == <value>; // the principal navigates tooDescription#
Both sides navigate#
The row under evaluation is reached with a bare property name, and the caller with user. Either may navigate:
// the row's chain compared to the principal's chain
allow read where Folder.Workspace.Region.Company == user.HomeRegion.Company;A broken chain denies, it does not hide the row#
If any relation along the chain is null, the comparison is not true — so that row satisfies no allow built on
the chain. It is not removed from consideration, which matters the moment the chain is one arm of something:
// a workspace with no region is admitted by the LEFT arm; one with a region is decided by the right.
allow read where Folder.Workspace.Region == null || Folder.Workspace.Region.Head == user;Both arms get their say for every row. A rule cannot accidentally exclude rows by mentioning a relation they lack.
Read and write give the same answer#
Reads are answered by the database and writes are checked in memory at commit, but they evaluate the same predicate and must return the same verdict — at every depth, on both sides of the comparison. You never have to know which engine is deciding, and you never have to shorten a rule to keep them in agreement.
Do not denormalize a relation just to keep a rule short. Copying a parent's key onto a child so the rule can say
Organizationinstead ofApplication.Organizationbuys nothing here, and costs a column that can drift out of step with the relation it mirrors.
Does a hop resolve against rows the caller cannot read?#
Following a relation resolves against the data as it is, not against the rows the caller may read. Deciding authorization from what the caller can already see would be circular — you cannot read the membership row that grants you the read. Only the final verdict reaches the caller; no row visited along the way is disclosed.
Examples#
[Principal] entity User {
string Email;
security { allow read when IsAuthenticated; }
}
entity Region {
string Name;
[Required] User Head;
security { allow read when IsAuthenticated; }
}
entity Workspace {
string Name;
Region Region; // nullable — a workspace need not sit in a region
[Required] bool Active;
security { allow read when IsAuthenticated; }
}
enum FolderKind { Normal, Archived }
entity Folder {
string Name;
[Required] Workspace Workspace;
[Required] FolderKind Kind;
security { allow read when IsAuthenticated; }
}
entity Note {
string Title;
[Required] Folder Folder;
security {
// four hops to the deciding fact, plus criteria on TWO of the rows along the way.
allow read, update where Folder.Workspace.Region.Head == user
&& Folder.Workspace.Active
&& Folder.Kind != FolderKind.Archived;
}
}// a chain inside a collection hop, correlated to a chain on the row
allow read where RegionMember.Any(m => m.Region == Folder.Workspace.Region && m.User == user);// the shape a denormalized FK used to exist for — now written as it reads
allow update, delete where OrganizationMember.Any(o => o.Organization == Application.Organization
&& o.User == user
&& o.Role != OrgRole.Member);See also#
- security { } — the
security { }block and its verbs - principal predicates (IsAuthenticated / IsAnonymous) and open reads —
IsAuthenticated/IsAnonymous - secure by default (deny-all) — why an unqualified
allowis refused