# 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 own); a when clause gates by the principal (staff see everything). Rules are compiled into every query.

<!-- id: security-entity-security · area: security · stability: stable · html: https://osysharp.com/reference/security/entity-security/ -->

## Summary        {#summary}
A `security { }` block on an entity decides who may read and write its rows. It is not a check you call — it is
**compiled into every query and every write**, so there is no code path that can go around it. A row you may not see
is not fetched and hidden; it is never in the result at all.

Two kinds of rule, and the distinction is the whole model:
- **`where`** filters by the **row** — *the owner sees their own rows*.
- **`when`** gates by the **principal** — *staff see everything*.

They differ in **what they ask**, never in **when they ask it**. Both are decided against the facts as they stand at
the moment of the read or the write — including a grant your own code committed a line earlier. See
[[#same-freshness|the same freshness]].

A `where` on an **`update`** is asked about the row **twice** — as it is, and as it will be — and both must pass. That
is what makes a status gate a gate: see [[#update-both-images|before and after]].

A rule may also name a single **field**, and that means one thing on reads and a different thing on writes: on `read`
it edits a [[#field-mask|mask]] (which columns are hidden), on a write verb it [[#field-write|replaces]] the entity's
rule for that column. A read mask may take **either** a `when` (principal-only) or a
[[#field-mask-row-scoped|`where`]] (row-scoped, decided per row) — the same two shapes an entity-level rule takes.

## Signature      {#signature}
```osy syntax
entity <E> {
  // Everything is denied already. You do NOT write `default deny` — you only write what is ALLOWED.
  security {
    message "…";                         // what a person reads when a WRITE here is refused (optional)

    allow <verbs> when  <policy>;        // …to principals satisfying this policy
    allow <verbs> where <row predicate>; // …only the rows matching this

    // A rule may name ONE FIELD, and read and write mean different things by that:
    deny  read <Field> when <policy>;    // a MASK, principal-only — the row comes back, the column does not
    deny  read <Field> where <row predicate>;  // a MASK, row-scoped — decided per row, like an entity rule's `where`
    allow read <Field> when <policy>;    // LIFTS that mask again, for these callers
    allow <write verb> <Field> when <policy>;  // REPLACES the entity's rule for this column
  }
}

// <verbs> — one, or several, comma-separated:
//   read · create · update · delete

policy <Name> => <predicate over `user`>;   // a named, reusable principal test
```

## Description    {#description}

### You only ever write what is ALLOWED   {#only-allow}
Everything is denied before you say a word, so a `security { }` block is a list of **grants**. There is nothing to
turn off first, and **you never write `default deny`** — that is the state you are already in.

The clearest demonstration is the locked entity: to make one that nobody may read or create, say **nothing at all**.

```osy title="a locked entity, and a public one" test app=security-entity-security
entity Secret {
  [MaxLength(100)] string Code;
  // No security block. Nobody reads it, nobody creates it. Locked, by saying nothing.
}

entity Article {
  [MaxLength(200)] string Title;
  // Public — and note you must say WHO: a bare `allow read;` with no `when`/`where` is a compile error.
  security { allow read when IsAuthenticated || IsAnonymous; }
}
```

That is the whole posture in one screen: **silence denies, and only a grant opens.** The failure mode of forgetting a
rule is *"nobody can do it"* — reported within the minute — rather than *"everybody can"*, which nobody reports until
it is somebody else's headline.

### `where` — the owner sees their own   {#where}
A `where` clause is a predicate over the **row**, and `user` is the acting principal. It becomes part of the query:

```osy title="each user sees only their own rows" test app=security-entity-security
[Principal] entity User {
  [Required] string Name;
}

entity Doc {
  User Owner;
  [MaxLength(200)] string Title;
  security {
    allow read where Owner == user;      // I see mine; you see yours
  }
}
```

Under this rule `Doc.Count()` returns a *different number* for different users, and both are correct. That is the
point: the filter is part of the query, not a mask applied afterwards.

### Read and write are separate   {#read-vs-write}
They usually differ — a team can all *read* a memo, but only its author may *change* it:

```osy title="everyone reads; only the owner writes" test app=security-entity-security
entity Memo {
  User Owner;
  [MaxLength(200)] string Note;
  security {
    allow read when IsAuthenticated;     // any signed-in colleague can see it
    allow update where Owner == user;    // only the author can change it
  }
}
```

### Which verbs can I grant?   {#verbs}
A grant names what may be done: **`read`** · **`create`** · **`update`** · **`delete`**. Several may share one rule,
comma-separated — and they routinely differ, which is the point of naming them separately:

```osy title="different people may do different things" test app=security-entity-security
entity Organization {
  [Required, MaxLength(200)] string Name;
  [Required, MaxLength(80)] string Slug;
  security {
    allow read when IsStaff;                     // staff can see every org
    allow create, update, delete when IsAdmin;   // only an admin may change the SET of them
  }
}
```

A rule with no verb list is not a thing you can write: you always say what is being allowed. "Access" is not a
permission — reading is, and deleting is, and they are not the same decision.

### A `where` on `create` — checking the row you are writing   {#create-where}
A `where` filters *existing* rows on **`read`** and on **`delete`**. On **`create`** there is no existing row — so the
same `where` is checked against **the row you are writing**: a create is refused unless the new row satisfies the
predicate. (This is a *WITH-CHECK*, the INSERT half of row-level security.) One predicate governs all four verbs —
*you may only bring into existence a row you would be allowed to own*:

```osy title="a create you may not make is refused, and rolled back" test app=security-entity-security
entity Ledger {
  User Owner;
  [MaxLength(200)] string Note;
  security {
    // The SAME `where` governs create: on the commit path it validates the ROW being written.
    allow read, create, update, delete where Owner == user;
  }
}
```

Here a caller may create a `Ledger` **owned by themselves** (the row satisfies `Owner == user`), but a create whose
`Owner` is someone else is **denied at commit and rolled back** — not saved-then-hidden.

This is the *only* way to scope a create by a column **the app sets itself** (an `Owner` ref, an `Organization` an
app supplies). It is different from the auto-stamped ownership idiom — `allow create when IsAuthenticated` +
`allow read, update, delete where CreatedBy == user.Id` — which is safe *only* because `CreatedBy` is stamped by the
platform and cannot be forged. When the scoping column is one the caller writes, a role-only `when` (which never sees
the row) would let them write it for any tenant; the `create where` is what refuses that.

### An `update` is checked against the row BEFORE **and** AFTER   {#update-both-images}
An update has two versions of the row, and the `where` is asked about **both**:

- **the row as it is** — *may you touch this row at all?*
- **the row as it will be** — *may the row become this?*

Both must pass. Read it as the two halves you already know: `read`/`delete` ask only the first, `create` only the
second (there is no "before"), and `update` — which has both — asks both.

This is the rule that surprises people, so here it is on the shape everybody writes. A timesheet is editable only while
it is a draft:

```osy title="editable while Draft — and what that does and does not permit" test app=security-entity-security
enum SheetStatus { Draft, Submitted }

entity Timesheet {
  [Required] User Owner;
  [MaxLength(140)] string Note;
  SheetStatus Status = SheetStatus.Draft;
  security {
    allow read, create where Owner == user;
    allow update where Owner == user && Status == SheetStatus.Draft;
    // Moving the sheet ON is its own grant, scoped to the one column it changes. Without this line the sheet
    // could never leave Draft, because a row that ends up Submitted is not a row the rule above admits.
    allow update Status where Owner == user;
  }
}
```

With that block:

| the write | why |
|---|---|
| edit `Note` while `Draft` | ✅ both versions of the row are `Draft` |
| edit `Note` once `Submitted` | ❌ the row as it is fails the gate |
| set `Status` to `Submitted` | ✅ through the field-scoped grant, which governs `Status` alone |
| set `Status` back to `Draft` **and** rewrite `Note` in one write | ❌ the row as it *is* is `Submitted`, so the entity-level gate refuses the `Note` — you cannot re-enter the gate to get past it |

⚠ **That last row is the point of asking about both.** If only the after-version were checked, a caller could edit any
row at all simply by setting `Status = Draft` in the same write and carrying every other column with it — and a gate
written that way would stop nobody. If only the before-version were checked, a caller inside the gate could write the
row into a state their own rule forbids.

⚠ **And the third row is why a status gate needs a second rule.** `allow update where … Status == Draft` deliberately
does **not** permit `Draft → Submitted`: leaving the gated state is a different decision from editing inside it, so it
gets its own grant. A [[#field-write|field-scoped rule]] is the usual way to say it, because it *replaces* the
entity-level rule for that one column — everything else stays gated.

### The row is fine and one FIELD is not — `deny read <Field>`   {#field-mask}
Sometimes the ROW is fine and one FIELD is not. A password hash is the canonical case: the login flow must read it to
verify a credential, and **nobody else ever should** — not an admin, not the user themselves, not a support tool, not
an export.

`deny read <Field> when <policy>` masks a single field. The row still comes back; the field does not:

```osy title="a field nobody but the auth flow may read" test app=security-entity-security
// `IsAuthenticator` is the ephemeral principal the login flow runs as (see [auth bootstrap (login, before anyone is signed in)](https://osysharp.com/reference/security/auth-bootstrap/)).
policy IsAuthenticator => RoleGrant.Any(g => g.Grantee == user && g.Level == AppRole.Authenticator);

entity Credential {
  [Required] User Owner;
  /// Salted hash of the password — never the plaintext.
  [MaxLength(200)] string PasswordHash;
  security {
    allow read when IsStaff;                        // staff read the directory …
    deny read PasswordHash when !IsAuthenticator;   // … but the hash, only the auth flow, ever
    allow update where Owner == user;               // a user maintains their own credential
  }
}
```

This is worth reaching for more often than people do. A field mask is a **narrow, auditable** statement — *this
column, to these people, never* — and it survives every future query, endpoint, export and tool automatically,
because it is enforced where the data is read rather than wherever someone remembered to be careful.

### A mask can filter by ROW too — `deny read <Field> where <row predicate>`   {#field-mask-row-scoped}
`deny read <Field> when <policy>` answers ONE question for the whole request: *"is this caller the kind of person who
may see the column at all?"* — a `when` has no row in scope, so it can never depend on WHICH row. Sometimes the real
rule needs the row too: *"visible to the account's own person, or to Finance"* is a per-row exception a `when` alone
cannot express.

A field mask takes a `where` for exactly this, evaluated per row the SAME way a row-level rule's `where` is:

```osy title="the account number: its owner sees it, an admin sees every one, nobody else sees any" test app=security-entity-security
entity BankAccount {
  [Required] User Owner;
  [MaxLength(34)] string? Iban;
  security {
    allow read when IsStaff;
    deny read Iban where !IsAdmin && Owner != user;   // an admin, or the account's own person — nobody else
  }
}
```

A declared `policy` binds as a Boolean in a field mask's `where` exactly as it does in a row rule's, so `!IsAdmin &&
Owner != user` reads as ordinary C#-shaped logic: masked unless the caller is privileged, or it is their own row. The
`where` may equally read the row's OWN columns with no policy at all (`deny read Notes where Owner != user;`) — a raw
predicate, nothing to declare.

**The mask is still exclusionary only, never a grant.** It can only ever HIDE more of what the row-level rule already
allowed — it cannot expose a column on a row the row-level rule has already excluded. The row filter decides whether
there is a row to talk about at all; a field mask never runs ahead of it, and there is no caller for whom a mask
"opens" a row nobody's `allow read` granted them.

**When to split the field into its own entity instead.** If the sensitive column wants a genuinely INDEPENDENT
security posture — its own auditing, its own write rule, a lifecycle of its own — give it its own entity with an
ordinary row rule (`allow read where IsAdmin || Owner == user;`) rather than masking it on the parent. Both are
correct; the split earns its keep when the field is not merely hidden but actually governed differently from the
rest of its row.

### `allow read <Field>` — lifting the mask again   {#field-unmask}
A field rule on **`read`** edits ONE thing: the set of columns to hide. **`deny read <Field>` puts a column into that
set; `allow read <Field>` takes it back out.** So the two spell the same idea from opposite ends, and the second is
useful when the honest default is *"nobody"*:

```osy title="hidden from everyone by default, then lifted for the people who may see it" test app=security-entity-security
entity Applicant {
  [Required, MaxLength(200)] string Name;
  /// Interview notes — written about a person, read by the panel and nobody else.
  [MaxLength(2000)] string Notes;
  security {
    allow read when IsAuthenticated;   // the applicant row is ordinary, visible to the company …
    deny  read Notes;                  // … the notes are hidden, from everyone, with no exception …
    allow read Notes when IsStaff;     // … except the panel
  }
}
```

And that is a claim about BEHAVIOUR, so it is executed rather than asserted in prose:

```osy title="proof: the row arrives, the column arrives only for the panel" run app=security-entity-security
principal Panelist  => User.Single(u => u.Name == "Pat");
principal Colleague => User.Single(u => u.Name == "Sam");

[TestFixture]
void SeedApplicants() {
  var pat = new User { Name = "Pat" };
  var sam = new User { Name = "Sam" };
  var panel = new RoleGrant { Grantee = pat, Level = AppRole.Staff };
  var robin = new Applicant { Name = "Robin", Notes = "strong on systems" };
}

[Test(SeedApplicants)]
[runas(Colleague)]
void A_colleague_gets_the_row_but_not_the_notes() {
  var a = Applicant.Single(x => x.Name == "Robin");
  Assert.Equal("Robin", a.Name);       // the ROW comes back …
  Assert.Null(a.Notes);                // … and the masked column does not
}

[Test(SeedApplicants)]
[runas(Panelist)]
void The_panel_gets_the_notes() {
  var a = Applicant.Single(x => x.Name == "Robin");
  Assert.Equal("strong on systems", a.Notes);   // the lift, for the callers it names
}
```

Written the other way round — `deny read Notes when !IsStaff` — that is one line instead of two and it means the same
thing here. Prefer whichever states the intent you would defend in review: an unconditional `deny` plus an explicit
lift says *"nobody, and here is the exception"*, which is the safer sentence when the exception list is likely to
grow.

⚠ **An `allow read <Field>` is NOT a grant, and the compiler will not let you write it as one.** It cannot make a
column appear in a row the entity's `read` rules withhold — whether the ROW comes back is decided by those rules and
by them alone, and a field rule takes no part in it. So an `allow read <Field>` with no `deny read <Field>` beside it
has nothing to lift, and is refused at compile time rather than accepted as an inert line:

```osy title="what the compiler says when the rule can do nothing" syntax
entity Vendor {
  [MaxLength(200)] string Email;
  security {
    allow read when IsStaff;
    allow read Email when IsAnonymous;   // ✗ `allow read Email` does nothing on 'Vendor'.
  }                                      //   There is no `deny read Email` here, so this line has
}                                        //   nothing to lift and no caller's access changes.
```

To open a column to a wider audience you widen the **row** rule (`allow read when …`) and mask what should stay
narrow. That is the only direction reading works in.

### A field rule on a WRITE verb replaces, rather than masks   {#field-write}
On `create`/`update`/`delete` a field rule means something different again: when a column has any rule of its own,
**that rule REPLACES the entity's for that column** — the entity's grant simply does not reach it. This is how you
carve one writable column out of an otherwise read-only row, and it stands alone (there is nothing to lift, so no
sibling `deny` is needed):

```osy title="one column the login flow may stamp, on a row nobody else may touch" test app=security-entity-security
entity DeviceSession {
  [Required] User Owner;
  [MaxLength(80)] string Device;
  DateTime? LastSeenAt;
  security {
    allow read when IsAuthenticated;
    allow update LastSeenAt when IsAuthenticator;   // ONLY this column, ONLY the auth flow
  }
}
```

Nobody may update `Device` — the entity grants no `update` at all — and `LastSeenAt` is governed by its own line
rather than by the (absent) entity rule. `osy explain` prints both halves of this, per column, in the words above.

### `when` — gate by who is asking   {#when}
A `where` asks *"is this row yours?"*. A `when` asks *"are you the kind of person who may do this at all?"*. Name that
test with a `policy` and reuse it:

```osy title="a role policy, and the rule that uses it" test app=security-entity-security
enum AppRole { Staff, Admin, Authenticator }

entity RoleGrant {
  User Grantee;
  [Required] AppRole Level;
}

policy IsStaff => RoleGrant.Any(g => g.Grantee == user && g.Level == AppRole.Staff);
policy IsAdmin => RoleGrant.Any(g => g.Grantee == user && g.Level == AppRole.Admin);

entity Report {
  [MaxLength(200)] string Title;
  security {
    allow read when IsStaff;             // staff read every report; nobody else reads any
  }
}
```

A `policy` is written once and referenced everywhere, so the definition of "staff" lives in one place. When it
changes, it changes everywhere — which is the only way it stays true.

### `when` and `where` have the same freshness   {#same-freshness}
The two ask different **questions**. They do not ask them at different **times**. Whichever you write, the answer is
computed against the grants as they stand when the read or the write happens — so a grant your own code committed a
moment ago is already in force, and a grant it revoked is already gone. The same is true of a
[[#field-mask|field mask]], which is the third way of writing the same authorization.

That matters because the two spellings are often the same rule. `when IsStaff` and
`where RoleGrant.Any(g => g.Grantee == user && g.Level == AppRole.Staff)` say one thing twice — `IsStaff` **is** that
expression, named — and nothing about your app should change depending on which you reached for.

```osy title="a grant committed mid-request is in force for the rest of it" test app=security-entity-security
// Onboarding, founding, an invite accepted: one request writes the grant and then does the work the grant
// authorizes. Both spellings see it, and so does a field mask — there is no snapshot taken when the request began.
void JoinTheStaffAndGetToWork() {
  var g = new RoleGrant { Grantee = Session.CurrentUser, Level = AppRole.Staff };
  UnitOfWork.Commit();          // ← the grant is a fact now …

  var mine = Report.Count();    // … and `allow read when IsStaff`, two blocks up, already knows it
}
```

It runs the other way too, and that direction is the one worth stating: a role **removed** mid-request stops working
for the rest of that request. An admin screen that revokes a membership and then re-renders beneath it is showing the
access the person has now, not the access they had when the page began.

⚑ **This covers ORDINARY ROWS, not just grants — which matters most for a gate that reaches through a reference.**
`allow update where Report.Employee.User == user && Report.Status == ReportStatus.Draft` is decided against the
report as it is *now*, every time it is asked. So the flow an expense app is built out of behaves the way you would
read it: add a line while the report is a draft, submit the report, and the next edit of that line is refused — in
the same request that submitted it, with no re-login, no new page and no second context. Recall the report and the
same edit is allowed again. Nothing is snapshotted when the request begins, and nothing is remembered from the
first time the rule looked at the parent.

⚠ **The bound is the READ, not the request.** A change committed somewhere else — an admin in another session, a
background job, your own API — is in force from the next read that asks. It is not pushed to anything already on
screen: a page does not lose a button the instant a role is revoked; it loses it the next time it asks the server
anything. So this is "authorization follows the facts", not "live revocation across a system".

### Test it, or you have not written it   {#test-it}
A security rule you have not tested is a rule you *believe* you wrote. Prove it, with [`runas`](https://osysharp.com/reference/testing/runas/) and
[`Assert.Denied`](https://osysharp.com/reference/testing/assert/):

```osy title="prove the rule denies the person it should" run app=security-entity-security
// A `principal` names a seeded row so a test can BE that person. It resolves unsecured, which is what makes
// it work: a `[Test]` body outside a `runas` is an anonymous caller, so looking a user up there reads nothing.
principal Bob => User.Single(u => u.Name == "Bob");

[TestFixture]
void Seed() {
  var alice = new User { Name = "Alice" };
  var bob = new User { Name = "Bob" };
  var aliceDoc = new Doc { Owner = alice, Title = "alice-doc" };
}

[Test(Seed)]
[runas(Bob)]
void Bob_cannot_see_Alices_doc() {
  Assert.Equal(0, Doc.Count());         // not "hidden" — for Bob, the row does not exist
}
```

Write one of these for every rule that matters. It is what stops a refactor six months from now from quietly opening a
door nobody notices is open.

### `message` — what a refused person is told   {#message}
A block may open with one line of your own copy, and it is the sentence anyone refused a `create`, `update` or
`delete` on this entity reads:

```osy syntax
security {
  message "Only an organisation admin can change an approval policy.";
  allow create, update, delete when IsOrgAdmin;
}
```

It covers the refusal however it was reached — the row matched none of the `allow` rules, or none of them is active
for this caller — which is the case no per-rule `message` can speak for, because the refusal was made by the ABSENCE
of a matching rule. Usually you do not write it per entity at all: `app.DenialMessage = "…";` says it once for the
whole app and each entity overrides only if it needs different words. Reading is not covered and cannot be — a read
you may not do is filtered, not refused. The whole story is in [what a refused user is told](https://osysharp.com/reference/security/denial-messages/).

## See also       {#see-also}
- [what a refused user is told](https://osysharp.com/reference/security/denial-messages/) — the sentence a refused person reads, and how to write it yourself
- [secure by default (deny-all)](https://osysharp.com/reference/security/secure-by-default/) — what an entity permits before you write any block
- [principal predicates (IsAuthenticated / IsAnonymous) and open reads](https://osysharp.com/reference/security/principal-predicates/) — the `user` a rule compares against
- [runas](https://osysharp.com/reference/testing/runas/) — acting as a principal, so a rule can be tested
- [Assert](https://osysharp.com/reference/testing/assert/) — `Assert.Denied`
