# The security model

> How authorization works in Osy#, end to end. Everything is denied until you grant it; a grant is compiled into every query rather than checked afterwards; and the only way to know a rule works is to become the user and try.

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

## Summary        {#summary}
Security in Osy# rests on three ideas. Hold these and the rest is detail:

1. **Silence denies.** An entity you say nothing about is readable by nobody. You never turn access off — you only
   ever grant it.
2. **A grant is part of the query.** A row you may not see is not fetched and then hidden; it is never selected. So
   `Order.Count()` honestly means *"how many orders exist **for me**"*, and two users can correctly get different
   numbers from the same function.
3. **A rule you have not tested is a rule you only believe you wrote.** Security is the one area where compiling,
   passing tests and "working" tell you nothing about correctness. The only proof is to become the other user and be
   refused.

The rest of this page is the model those three ideas describe.

## Description    {#description}

### Which page answers which question?   {#shape}

| Question | Answered by | Page |
|---|---|---|
| What may this entity's rows be used for, and by whom? | the `security { }` block | [security { }](https://osysharp.com/reference/security/entity-security/) |
| *Is this row yours?* | a `where` clause — a predicate over the **row** | [security { }](https://osysharp.com/reference/security/entity-security/) |
| *Is it yours through something else?* | a `where` clause that **navigates relations**, any depth | [navigation in security predicates (any depth, either side)](https://osysharp.com/reference/security/navigation-predicates/) |
| *Are you the kind of person who may do this at all?* | a `when` clause — a predicate over the **principal** | [principal predicates (IsAuthenticated / IsAnonymous) and open reads](https://osysharp.com/reference/security/principal-predicates/) |
| Who is `user`? | the `[Principal]` entity | below |
| May this person open this page? | `[Authorize(policy)]` on the component | [page authorization (policies)](https://osysharp.com/reference/ui/authorize/) |
| How does anyone log in, if nothing is readable yet? | `app.AuthBootstrap` | [auth bootstrap (login, before anyone is signed in)](https://osysharp.com/reference/security/auth-bootstrap/) |
| Does any of it actually work? | `runas` + `Assert.Denied` | [runas](https://osysharp.com/reference/testing/runas/) |

### 1. Silence denies   {#silence-denies}
The posture is deny-all ([secure by default (deny-all)](https://osysharp.com/reference/security/secure-by-default/)). An entity with no `security { }` block is denied to every
user request — not "readable by signed-in users", not "readable by its owner". **Denied.**

To lock an entity completely, say nothing:

```osy title="locked, by saying nothing" test app=security-index
entity AuditRecord {
  [MaxLength(200)] string Message;
  // No security block. No user can read it, and no user can create one.
}
```

There is therefore no `default deny` to write, and a `security { }` block is a **list of grants**. This is why the
failure mode of forgetting a rule is *"nobody can do it"* — reported within the minute — rather than *"everybody
can"*, which nobody reports at all. A door that fails shut is a door you can trust.

### 2. Who `user` is   {#principal}
One entity in your app is the **principal** — the thing a logged-in person *is*. Mark it `[Principal]`, and `user`
inside a security rule means a row of it:

```osy title="the principal, and a rule that compares against it" test app=security-index
[Principal] entity User {
  [Required] string Name;
}

entity Doc {
  User Owner;
  [MaxLength(200)] string Title;
  security { allow read where Owner == user; }   // `user` IS the acting principal's row
}
```

The principal is **yours** — your entity, your fields, your extra relationships. The platform does not impose a user
model on you; it only needs to know which of your entities is the one people log in as.

**Roles are yours too, and they are just data.** A role is granted by an ordinary entity: anything that references the
`[Principal]` and carries a member of your `[Role]` enum *is* a grant table, recognised by that shape. So "who may be
an admin" is not a platform setting — it is the question "who may create a row in that table", answered by a
`security { }` block like any other. One rule is worth carrying from the start: **an app has exactly ONE `[Role]`
enum** — the vocabulary the login ticket carries. Every other tier (org membership, project membership, a
team's `Owner`/`Member`) is **ordinary data**, and you query it. Both kinds work in a rule; they are simply answered
differently, and [role grants (and the first admin)](https://osysharp.com/reference/security/role-grants/) lays them out side by side — along with why a *second* `[Role]` enum is a
compile error rather than a convenience.

### 3. Two axes: the row, and the person   {#two-axes}
This is the distinction to internalise, because almost every real rule is one or the other:

- **`where`** filters by the **row**. *The owner sees their own documents.* It narrows **which rows** you get.
- **`when`** gates by the **principal**. *Staff see every document.* It decides **whether you may at all**.

Name the principal test with a `policy` so it is written once and reused everywhere:

```osy title="both axes, and a named policy" test app=security-index
[Role] enum AppRole { Staff, Admin }     // the app's ONE role vocabulary

entity RoleGrant {                        // a [Principal] ref + a [Role] member ⇒ this table grants roles
  User Grantee;
  [Required] AppRole Level;
}

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

entity Report {
  [MaxLength(200)] string Title;
  security {
    allow read when IsStaff;              // by PERSON: staff, and nobody else
  }
}

entity Memo {
  User Owner;
  [MaxLength(200)] string Note;
  security {
    allow read when IsStaff;              // staff read every memo …
    allow read where Owner == user;       // … and everyone reads their own
    allow update where Owner == user;     // but only the author may change one
  }
}
```

Grants **add up**: a staff member who also owns a memo is covered by either rule. And note that read and write are
separate grants — "the team can see it, only the author can change it" is the common case, not an exotic one.

### 4. The grant is inside the query   {#in-the-query}
A rule is compiled into the SQL alongside your own predicate. It is not a filter applied to rows you already
fetched, and it is not a check you are expected to remember to call.

Three consequences worth stating plainly:

- **`Count()` is honest.** Under `allow read where Owner == user`, `Doc.Count()` returns *your* documents' count. Two
  users get different numbers and both are right.
- **You never re-check after a query.** There is no "and now verify they were allowed to see these". If a row came
  back, they were allowed.
- **You cannot leak by forgetting.** There is no code path — a function, an API call, an MCP tool, a UI query — that
  goes around it, because there is no "it" to go around. The rule is the query.

### 5. The bootstrap paradox   {#bootstrap}
If nothing is readable until you are authenticated, how does anyone *log in* — an act that must read the user row of
someone who, by definition, is not yet authenticated?

`app.AuthBootstrap` resolves it: it names a role the engine mints a **user-less ephemeral principal** with, and the
functions (login, signup, password reset) it runs under that principal. Crucially, what that principal may touch is
your ordinary `security { }` grants — **no special access is minted**, so the bootstrap path cannot become a hole you
forgot about. See [auth bootstrap (login, before anyone is signed in)](https://osysharp.com/reference/security/auth-bootstrap/).

### 6. The UI has its own rail   {#ui}
Data security and page security are different questions, and both are deny-first:

- A routed component **requires authentication** unless it says otherwise ([page authorization (policies)](https://osysharp.com/reference/ui/authorize/)).
- `[Authorize(policy)]` requires a named policy — and the reference is **compile-checked**, so `[Authorize(Typo)]` is
  a build error rather than a silent hole.

But understand the layering: **the UI rail decides who may open a page; the entity rules decide what data exists for
them.** A page that forgets `[Authorize]` and a query that correctly filters by owner will still show nothing but the
user's own rows. Defence in depth is not a slogan here; the data path never trusts the UI path.

### 7. Prove it, or you have not done it   {#prove-it}
Compiling proves nothing. Your tests passing proves nothing — they probably ran unrestricted. The only way to know
that Bob cannot read Alice's document is to **become Bob and be refused**:

```osy title="the test that actually proves the rule" run app=security-index
// `principal` names a seeded row so the test can BE that person. It resolves unsecured — a `[Test]` body outside
// a `runas` is an anonymous caller, so a `User.Single(…)` written there would read 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());     // for Bob, Alice's row does not exist — it is not hidden, it is absent
}
```

Note what is being asserted: not that an exception was thrown, but that **the row is not there**. That is the shape of
a correct row-level rule.

Write one of these for every rule that matters. It is what stops a refactor a year from now from quietly opening a
door that nobody notices is open — and "nobody notices" is the entire failure mode of security.

### The mistakes people make   {#mistakes}

| Mistake | What actually happens |
|---|---|
| Writing `default deny;` | Nothing. It is already the default — and typing it teaches you that security exists *where you typed it*. |
| A bare `allow read;` | A **compile error** on an app with a principal. Say *who*: `when IsAuthenticated`, or `IsAuthenticated \|\| IsAnonymous` if you truly mean the whole internet. |
| Fetching rows and filtering them in a `foreach` | Slower, and it is not security — the rows already left the database. Put the predicate in the query. |
| Testing with no `runas` | You tested what an **unrestricted** caller can do, which is everything. You have not tested security. |
| Assuming the UI protects the data | It does not, and it is not supposed to. The data path never trusts the UI path. |

## See also       {#see-also}
- [secure by default (deny-all)](https://osysharp.com/reference/security/secure-by-default/) — the deny-all posture in full, and what it does not cover
- [security { }](https://osysharp.com/reference/security/entity-security/) — the `security { }` block: `allow`, `where`, `when`, `policy`
- [principal predicates (IsAuthenticated / IsAnonymous) and open reads](https://osysharp.com/reference/security/principal-predicates/) — `IsAuthenticated` / `IsAnonymous`, and why an open read must say so
- [navigation in security predicates (any depth, either side)](https://osysharp.com/reference/security/navigation-predicates/) — following relations in a `where`, at any depth and on either side of the comparison
- [public pages (what a signed-out visitor can see and do)](https://osysharp.com/reference/security/public-reads/) — a public page and its public data are two grants; forget the second and the page renders empty
- [capability rows that belong to a user](https://osysharp.com/reference/security/capability-row-ownership/) — capability tables whose rows belong to one signed-in user, and the `[Principal]` they need
- [rows that are part of another row](https://osysharp.com/reference/security/part-of-derived-access/) — rows that are part of another row and take its rule; declare a shape's security once
- [auth bootstrap (login, before anyone is signed in)](https://osysharp.com/reference/security/auth-bootstrap/) — logging in before a principal exists
- [[AuthMethod] — a function an unauthenticated visitor may call](https://osysharp.com/reference/security/auth-method/) — `[AuthMethod]`: the one door an unauthenticated visitor may walk through
- [role grants (and the first admin)](https://osysharp.com/reference/security/role-grants/) — granting a role, the first admin, and never letting a lower tier grant a higher one
- [app.Auth — how the platform authenticates a user of your app](https://osysharp.com/reference/security/password-auth/) — `app.Auth`: how the platform can authenticate a user of your app with no code from you
- [OAuth clients (app.OAuthClients)](https://osysharp.com/reference/config/oauth-clients/) — `app.OAuthClients`: signing users in with an external identity (Google/GitHub/…), and connecting to an external API on their behalf
- [Security.* — hashing, verifying, tickets, random ids](https://osysharp.com/reference/stdlib/security/) — `HashPassword` · `VerifyPassword` · `IssueJwt` · `RandomId`
- [page authorization (policies)](https://osysharp.com/reference/ui/authorize/) — `[Authorize(policy)]` on a page
- [runas](https://osysharp.com/reference/testing/runas/) — becoming a user, so a rule can be proved
