# secure by default (deny-all)

> Deny-all is the posture, and it is the only one: an entity that declares no `security { }` block is denied to every user request, and you grant access explicitly where you want it. There is no setting, no opt-out and no app that runs open — the one thing that genuinely has to run before anyone is signed in is declared as an auth-bootstrap. System, designer, and bootstrap contexts are never affected.

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

## Summary        {#summary}
An entity with **no `security { }` block** is **denied to every user request** — access is granted only where you
write it. This is the *secure-by-default* posture: you cannot forget to lock down a table, because an unmentioned
table is already locked. It is not a setting you turn on and there is no way to turn it off, so there is nothing to
write and nothing to remember: every app is deny-all, every entity starts closed, and the only question left is
which access you grant back.

```osy syntax
// Deny-all is the default — no security block → denied to every user request.
entity Ledger { string Entry; }

// Grant access back explicitly — anyone may read the catalog.
entity Product {
  string Name;
  security { allow read when IsAuthenticated || IsAnonymous; }
}
```

## Signature      {#signature}
```osy syntax
entity Ledger { string Entry; }                              // no block  → denied to every user request
security { allow read when IsAuthenticated; }                // a block   → a LIST OF GRANTS, and the only way in
```

There is nothing app-level to write. The posture is a property of the language, not a configuration value, so it
cannot be relaxed for an app, a file or an entity — the only lever is which grants you write in a `security { }`
block. The one thing that genuinely has to run before anyone is signed in — login, signup, a password reset — is
declared as an [auth-bootstrap](https://osysharp.com/reference/security/auth-bootstrap/) and runs under an identity you defined, on a leash of the
grants you gave that role.

## Description    {#description}
The posture changes only what a **user request** may touch. It does **not** change how the platform runs itself:

- **User requests** (a logged-in or anonymous caller reading/writing app data) are subject to the posture. An entity
  with no `security { }` block is denied; an entity **with** a block is governed by exactly what that block grants.
- **System, designer, and bootstrap** work (schema evolution, seeding, the metadata designer) is **never** affected —
  it does not run through a user's security context, so the posture never denies the platform's own operations.
- **Platform (`osy.*`) entities are exempt** — their access is gated by other layers, so the posture never denies a
  built-in read.

Grant access back with ordinary [security { }](https://osysharp.com/reference/security/entity-security/) rules — `allow read where Owner == user;`, a role check,
or one of the built-in [principal predicates (IsAuthenticated / IsAnonymous) and open reads](https://osysharp.com/reference/security/principal-predicates/) (`allow read when IsAuthenticated`, `when IsAnonymous`, or
`when IsAuthenticated || IsAnonymous` for a genuinely public read).

- **Every `allow` must say WHO — a rule with no `when` and no `where` is a compile error.** Deny-all decides what
  happens when you say *nothing*; this decides what happens when you open a block and then forget to qualify a line
  inside it. An unqualified `allow` grants that verb to every caller, anonymous included, and the runtime consults
  nothing — so it is not a weaker rule, it is the **absence** of one wearing the syntax of one, and it is worse than
  no block at all because deny-all would have caught the omission. It applies to all four verbs and to every app,
  whether or not it declares a `[Principal]`.

  The remedy is never to remove the grant — it is to name who holds it:

  | you mean | you write |
  |---|---|
  | the owner of the row | `allow update where Owner == user;` |
  | anyone signed in | `allow read when IsAuthenticated;` |
  | signed-out visitors (signup, a public form) | `allow create when IsAnonymous;` |
  | genuinely everyone | `allow read when IsAuthenticated \|\| IsAnonymous;` |

  That last row is supported and is the right answer for a store front — **"everyone" is a legitimate decision, and
  spelling it out is the whole point.** What is refused is leaving it unsaid. `when true` is a compile error for the
  same reason: it says *yes* without saying *who*, so a reader cannot tell a public surface from an unfinished one.
  The diagnostic names the form that does mean everyone.

- **An entity-level `deny` must say WHEN as well.** "Denies more, never less" is the reasoning of an
  allow-by-default system; under deny-all there is nothing broader to narrow. A bare `deny update;` on an entity is
  either dead text — the verb was already denied — or, when a sibling `allow` names the same verb, a silent kill
  switch: an unconditional deny is checked FIRST, so it cancels the grant written above it. Both readings are worse
  than deleting the line, which is the same argument that refuses `default deny;`.

- **A FIELD-scoped `deny` needs no condition — except on a CREDENTIAL.** `deny read PasswordHash when !IsAuthenticator;` subtracts one column
  from whatever the entity grants, because property rules REPLACE the entity's rather than layering on them — it is
  the only way to say "nobody, ever", and no `when` states it as cleanly.

- **A `partial entity` is exempt too**, since narrowing a declaration made elsewhere is the whole reason it
  exists.

- **There is no way out, and that is the point.** Deny-all holds for every app, always — so "we will turn security on
  before we ship" is not a state this platform can be in, and an app that appears to work is an app whose grants you
  actually wrote. The only sanctioned path past a locked table is the one that must exist: login and signup have to
  reach a user row before a principal exists, so you declare them in `app.AuthBootstrap` and the engine runs them as
  an ephemeral principal bearing a role you chose — which may touch exactly what your `security { }` blocks grant
  that role, and nothing else. See [auth bootstrap (login, before anyone is signed in)](https://osysharp.com/reference/security/auth-bootstrap/), and [page authorization (policies)](https://osysharp.com/reference/ui/authorize/) for page-level authorization,
  the request-time complement to entity-level deny-all.

## Examples       {#examples}
A public catalog an anonymous visitor may browse, alongside a private table only an owner sees — under the posture,
the untouched `Ledger` is denied to all:

```osy title="granted, filtered, and denied — in one app" test app=security-secure-by-default
[Principal] entity User { string Name; }

entity Product {                                   // anonymous visitors may browse the catalog
  string Name;
  security { allow read when IsAnonymous || IsAuthenticated; }
}

entity Order {                                     // a buyer sees only their own orders
  User Buyer;
  string Item;
  security { allow read where Buyer == user; }
}

entity Ledger { string Entry; }                    // no security block → denied to everyone (the default)
```

## See also       {#see-also}
- [security { }](https://osysharp.com/reference/security/entity-security/) — the `security { }` block that grants access back
- [rows that are part of another row](https://osysharp.com/reference/security/part-of-derived-access/) — the exception: a row that is PART OF another takes that row's rule, with no block of its own
- [principal predicates (IsAuthenticated / IsAnonymous) and open reads](https://osysharp.com/reference/security/principal-predicates/) — `IsAuthenticated`/`IsAnonymous`, and why every `allow` must say who
- [page authorization (policies)](https://osysharp.com/reference/ui/authorize/) — page-level authorization, the request-time complement to deny-all
