Summary#
A subtype carries every field of its base, and its own security { } may take some of them further away: a deny
naming an inherited field is a rule like any other field deny, decided on the subtype's rows. An allow naming an
inherited field is refused — permissions only narrow on the way down. What the base type grants is the floor of what
anyone gets; a subtype can say less, never more.
Signature#
entity <Sub> : <Base> {
security {
deny read <InheritedField> [when <who>] [where <which rows>];
deny update <InheritedField> [when <who>] [where <which rows>] [except via F];
// allow … <InheritedField> …; ✗ refused — only a deny may name an inherited field
}
}Description#
What does a deny on an inherited field do?#
Exactly what a deny on one of the subtype's own fields does, on the subtype's rows: deny read masks the column for
the callers and rows it names (the row still comes back, the value reads as its type's default — null, or 0 for a number; Security.IsMasked — was this value withheld from me?), and deny update refuses the write.
The base type's own rows are untouched — the rule is the subtype's.
It is decided on the server, on every path a row can be read or written by: a function, a client's unit of work, a
REST or MCP call, an import. A read written over the base type is masked by each row's own type, so
Document.Where(…).Select(d => d.Notes) returns null for a Memo row whose Notes the Memo type withholds, just
as Memo.Where(…) does.
[Role] enum Duty { Reviewer, Clerk }
[RoleGrant] entity DutyGrant { User Grantee; [Required] Duty Level; }
[Principal] entity User {
[Required, MaxLength(200)] string Email;
security { allow read when IsAuthenticated; }
}
policy IsReviewer => DutyGrant.Any(g => g.Grantee == user && g.Level == Duty.Reviewer);
entity Document {
[Required, MaxLength(200)] string Title;
[MaxLength(2000)] string Notes = "";
security { allow read, create, update when IsAuthenticated; }
}
entity Memo : Document {
bool Filed = false;
security {
allow read, create, update when IsAuthenticated;
deny read Notes when !IsReviewer; // inherited from Document — narrowed here
deny update Title where Filed; // inherited too — fixed once the memo is filed
}
}Why is an allow on an inherited field refused?#
Because the subtype would be handing out a column of its base to callers the base never granted it to. The rule is
the one an organisation's policy sets: the base type is the floor, and everything below it can only narrow. So the
compiler refuses allow update Title when IsClerk; on Memo and says what to write instead — a deny that takes
Title from the callers who should not change it, while the subtype's entity-level rules (allow update when …)
already cover every field it inherits.
If a field genuinely needs different rules on the subtype, it is a different field: declare one of the subtype's own and grant that.
Does this work on a kit's type?#
Yes, and that is where it matters most. An app's subtype of a kit's entity may deny a field the kit declares:
entity Member : User { // User comes from the UserAccounts kit
security {
allow read when IsAuthenticated || IsAuthenticator;
allow update when IsAuthenticated where Id == user.Id;
deny update DisplayName when IsSuspended; // the kit's field, narrowed for this app's members
}
}The kit's own rules still cap everything — its grants are a ceiling no subtype widens — so the deny can only ever take something away from what the kit allowed.
What happens when a kit extends another kit?#
A field follows the rules of the type that declares it. When kit B's Bay derives from kit A's Shelf:
- kit A's fields stay kit A's. Its rules (its own-row rule, say) decide them on every
Bayrow. - kit B's own fields follow kit B's rules once kit B's block names them in an
allow updaterule (adenynaming one only narrows, and leaves it under kit A's rules) — exactly as an app's own fields on its subtype do ([[entity-inheritance#own-fields]]). A field kit B adds and does not name keeps kit A's rules. This holds on an app's subtype ofBaytoo. - kit B may deny a field it inherits from kit A, and its
except vianames kit B's own functions — a kit vouches only for its own code. - kit B imports only what kit B's own lines name. Everything
Baycarries becauseShelfdeclared it — kit A's rules, with the app's answers to kit A's policies, and its defaults, computed members and invariants — is read in kit A's file, with kit A'susings. Kit B writesuse Acme.Shelves;and nothing for what those lines mention.
namespace Acme.Bays;
using Acme.Shelves;
policy TagsBays;
policy RecodesBays;
public entity Bay : Shelf { // Shelf: kit A's, with Label, Code and an own-row update rule
[MaxLength(80)] string Tag = "";
security {
allow read, create when IsAuthenticated;
allow update when IsAuthenticated where OwnerId == user.Id;
allow update Tag when TagsBays; // Bay's own field — Bay decides it
deny update Code except via Recode; // Shelf's field — Bay may only narrow it
}
}
[Server, Authorize(RecodesBays)]
public void Recode(Bay b, string code) { b.Code = code; }See also#
- entity Sub : Base — grants are never inherited, denies are; who decides a subtype's own fields
- deny … except via — naming the only functions that write past a deny
- [Authority] — a kit's rules as the floor an app stands on
- security { } — the
security { }block