Osy#betaRun it on Osyrin: osyrin.com

Reference / Security

deny on an inherited field

entity <Sub> : <Base> { security { deny read|update <InheritedField> [when …] [where …] [except via F]; } }

A subtype may NARROW a field it inherits — `deny read Notes when !IsReviewer;`, `deny update Code except via Recode;` — and never widen one: an `allow` that names an inherited field is refused. The deny holds on the subtype's rows on every path, including a read written over the base type. A field follows the rules of the type that declares it, so a kit extending another kit decides its own fields and narrows the ones it inherits.

stable1 example compiled by CIsecurityinheritancepackageskits

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 Bay row.
  • kit B's own fields follow kit B's rules once kit B's block names them in an allow update rule (a deny naming 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 of Bay too.
  • kit B may deny a field it inherits from kit A, and its except via names 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 Bay carries because Shelf declared 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's usings. Kit B writes use 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#

Related

entity Sub : Base

Derives one entity from another. The subtype is its own type with its own name, its own security and its own workflows…

security { }

The rules that decide who may read and write an entity's rows. A where clause filters by the row (the owner sees their…

deny … except via

Names the ONLY functions whose own code may change something a deny withholds: `deny update UnitsLeft except via…

[Authority]

What an app that imports your entity may do to the `security { }` you declared on it. The default is `Narrowable` — the…

The UserAccounts kit — sign-in done properly, once

`Osysharp.UserAccounts` is the default way into an app: sign-in with a password or a provider (Google, GitHub, …)…