# when is who, where is which rows

> A rule's `when` says WHO it is for and its `where` says WHICH ROWS. A rule whose `when` does not hold is never asked, so each kind of caller pays only for its own rules. A caller test inside a `where` is a compile error.

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

## Summary        {#summary}
Every rule answers two questions, and each has its own place:

- **`when`** — **who** is this rule for? A question about the caller alone: a role, a kind of principal, a fact about
  them.
- **`where`** — **which rows** may they have? A question about the row (and, usually, how it relates to the caller).

The engine answers each rule's `when` once for the caller and **never asks the `where` of a rule whose `when` does not
hold**. So a rule for customers costs a staff member nothing — as long as its `when` says it is for customers. A
caller test written inside a `where` is a compile error, and the error prints the rule rewritten.

## Signature      {#signature}
```osy syntax
allow <verbs> when <who> where <which rows>;    // a rule for some callers, over some rows
allow <verbs> when <who>;                       // every row, for those callers
allow <verbs> where <which rows>;               // those rows, for every SIGNED-IN caller (a signed-out one needs a `when`)

policy <Who> => <a question about `user` alone>;              // a role, a kind of principal, a fact about them
policy <Rows>(<Type> row) => <a question about the row and `user`>;
```

## Description    {#description}

### Give each kind of caller its own rule   {#one-rule-per-caller}
A desk where the team reads every case and a client reads the cases they are a party to is two rules, one per kind of
caller. The `when` of each names who it is for:

```osy title="the team reads every case; a client reads their own" test app=security-when-and-where
[Role] enum DeskRole { Staff, Client }

[Principal] entity Person {
  [Required, MaxLength(200), Unique] string Email;
  security { allow read when IsAuthenticated; }
}

[RoleGrant] entity DeskGrant {
  Person Member;
  [Required] DeskRole Role;
  security { allow read when IsStaff; }
}

entity Case {
  [Required, MaxLength(200)] string Title;
  security {
    allow read, create, update when IsStaff;          // the team: every case
    allow read when IsClient where IsPartyTo(Id);     // a client: the cases they are a party to
  }
}

entity CaseParty {
  [Required] Case Case;
  [Required] Person Person;
  security { allow read when IsStaff; }
}

/// WHO: questions about the caller alone.
policy IsStaff => DeskGrant.Any(g => g.Member == user && g.Role == DeskRole.Staff);
policy IsClient => DeskGrant.Any(g => g.Member == user && g.Role == DeskRole.Client);

/// WHICH ROWS: a question about the row and the caller.
policy IsPartyTo(Guid c) => CaseParty.Any(p => p.Case.Id == c && p.Person == user);
```

For a member of the team, the client rule's `when` is false, so `IsPartyTo` is never asked of any row. For a client,
the team's rule is never active. Each caller pays for exactly the rules that are about them.

### A caller test in a `where` is refused, with the fix   {#refused}
Folding the team into the client rule reads naturally and costs everyone: the rule is now live for every caller, and
`IsPartyTo` is asked of every row for the team too. The compiler refuses any part of a `where` that never reads the
row, and prints the rule split by caller:

```osy syntax
allow read when IsAuthenticated where IsPartyTo(Id) || IsStaff;
// ERROR  security `where` on 'Case': `IsStaff` never reads the row — it asks only about the caller, so it belongs in
//        a `when`, not a `where`. … Write it as:
//   allow read when IsAuthenticated where IsPartyTo(Id);
//   allow read when IsAuthenticated && IsStaff;
```

The rewrite is exact — it grants exactly what the refused rule granted, to exactly the same callers:

| you wrote | it becomes |
|---|---|
| `where A \|\| C` (C asks only about the caller) | two rules: `where A` and `when C` |
| `where A && C` | `when C where A` |
| `where C` (nothing reads the row) | `when C` |

A rule's existing `when` is kept in front of each rewritten rule, so tidy it afterwards when it is implied — `IsStaff`
already implies `IsAuthenticated`, so `when IsAuthenticated && IsStaff` is simply `when IsStaff`.

One part is left alone: the constant `false`. It admits no row to anybody, so no caller pays for it and there is
nothing to move. It is what a package's rule comes to while the app has not answered a question whose default is
nobody ([[security-naming-a-policy#supplied-default]]) — `where OwnsListAddress(Email)` over
`virtual policy OwnsListAddress(string email) => false;`. `where true` is still refused: it reads no row and admits
every one.

### `when IsAnonymous || IsAuthenticated where …` names nobody   {#catch-all}
A `when` that admits every caller — none at all, `IsAuthenticated`, or `IsAnonymous || IsAuthenticated` — beside a
`where` that asks the database about the caller is the same cost in a quieter form: the rule is live for everyone, so
callers who already have their own grant ask its row question too. `osy lint` reports it
(`cost-row-rule-asked-of-every-caller`, Should) and lists the policies about the caller that are in scope.

Name who the rule is for. If no policy says it yet, declare one about the caller alone:

| who | a policy that says it |
|---|---|
| a role | `policy IsClient => DeskGrant.Any(g => g.Member == user && g.Role == DeskRole.Client);` |
| the signed-out | `IsAnonymous` (built in) |
| everyone but the team | `policy IsOutsider => IsAnonymous \|\| IsAuthenticated && !IsStaff;` |
| a fact about the caller | `policy HasACase => CaseParty.Any(p => p.Person == user);` |

⚠ **The `when` must hold for every caller the `where` admits**, or narrowing it takes access away. "Everyone but the
team" is right only when the team's own rule already covers what the client rule gave them — a member of staff who is
ALSO a client would otherwise lose a grant they had. When the team's grants do not cover it, name the fact instead
(`HasACase` above): it is true for every caller `IsPartyTo` could admit.

`where Owner == user` is never a finding: comparing a column with the caller costs nothing, and it is the plainest
way to say *the owner sees their own*.

### Why the two places are not interchangeable   {#why}
A `when` is answered once per request, before any row is in hand — and when it reads nothing but the signed-in caller
and the tables it looks in directly, its answer is kept between requests until one of those tables is written. A `where` is a question about each row: it becomes part of every
query, and every write asks it of the row being written. The two are equally fresh (a grant committed a line ago is in
force for both — see [[security-entity-security#same-freshness]]), and they are equally secure. What differs is who
pays: a `when` costs the caller it is about, a `where` costs every caller its rule is live for.

### Rules for different callers on a package's entity   {#package-floor}
A package's entity that an app may only tighten ([[Authority]](https://osysharp.com/reference/security/contribution-authority/)) turns the package's grants into a
floor. Rules that name different kinds of caller — `allow read when IsStaff;` beside `allow read when IsClient where
IsPartyTo(Id);` — make a floor that is exact for each kind of caller: an app's grant can never let one caller in on
another caller's rows. Write a package's rules one per caller, exactly as an app's.

## See also       {#see-also}
- [security { }](https://osysharp.com/reference/security/entity-security/) — the whole `security { }` block
- [policy](https://osysharp.com/reference/security/naming-a-policy/) — declaring the policies a `when` names
- [role grants (and the first admin)](https://osysharp.com/reference/security/role-grants/) — roles, the commonest answer to "who"
- [[Authority]](https://osysharp.com/reference/security/contribution-authority/) — a package's grants as the floor an app may only tighten
