# policy

> Names an authorization rule once so every place that needs it can say the name. A policy is a boolean about the caller, usable where security is declared and callable in ordinary code.

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

## Summary        {#summary}

A `policy` gives an authorization rule a name. Declare it once, then say the name wherever it applies — in an
entity's `security { }`, on a page, and inside a function as an ordinary boolean. A policy may take a parameter, which
is how you say *"may they manage **this** one"* rather than *"are they a manager of something"*.

## Signature      {#signature}

```osy syntax
policy Name => <predicate over `user`>;
policy Name(Type parameter) => <predicate over `user` and that parameter>;
```

## Description    {#description}

The predicate is about the **caller**. `user` is whoever is asking, and the rule may look at any data it needs to
decide — including rows the caller could not read themselves. That is deliberate: whether you *may* do a thing must
not depend on which rows you happen to be allowed to *see*, or every grant becomes circular.

```osy title="one rule, named" test app=security-naming-a-policy
[Principal] entity Person {
  [Required, MaxLength(200), Unique] string Email;
  security { allow read when IsAuthenticated; }
}

entity Team {
  [Required, MaxLength(80)] string Name;
  security { allow read when IsAuthenticated; }
}

entity TeamLead {
  [Required] Person Person;
  [Required] Team Team;
  security { allow read when IsAuthenticated; }
}

// "is a lead of some team" — a fact about the caller alone.
policy IsALead => TeamLead.Any(l => l.Person == user);
```

### Call it like the boolean it is   {#calling}

A policy reads as a boolean, so you can use it as one. This is the alternative to copying the predicate into every
function that needs it — one rule in as many copies as your app has entry points, each able to drift, in the one
category where drift is a hole rather than a bug.

```osy title="named, not re-written" test app=security-naming-a-policy
string LeadsOnly() {
  if (!IsALead) { throw new NotAuthorized("Only a team lead may do this."); }
  return "done";
}
```

### A parameter is what makes it about **this** one   {#parameters}

`IsALead` asks whether you lead *anything*. Most real authority is narrower — may you manage **this** team? Give the
policy a parameter and pass the row:

```osy title="authority about a particular row" test app=security-naming-a-policy
policy CanManageTeam(Team t) => TeamLead.Any(l => l.Person == user && l.Team == t);

string Rename(Guid teamId, string name) {
  var team = Team.Where(t => t.Id == teamId).FirstOrDefault();
  if (!CanManageTeam(team)) { throw new NotAuthorized("Only a lead of THIS team may rename it."); }
  team.Name = name;
  return name;
}
```

The difference matters more than it looks. A lead of *another* team passes `IsALead` and fails `CanManageTeam(team)` —
and that is usually the only case that tells a working rule from a broken one, because a manager and a non-member
behave the same under both.

### The rule the engine enforces can be composed too   {#in-a-where}

A `where` may CALL a parameterised policy, passing the row it is about. This is where naming rules stops being tidy
and starts being the difference between a rule you can read and one nobody dares touch.

Take a grant-handling system. Whether a caseworker may read a case is **four independent rules at once**:

- they work for the unit that owns the scheme it was applied under;
- they have not **recused** themselves from this particular case;
- if the case is **restricted**, they are senior enough to see one;
- and they are signed in at all.

Written inline, that is one welded expression — and it has to be repeated on every entity that hangs off a case:
the attachments, the assessments, the notes, the decision, the payment.

```osy title="the welded form — four rules, no names, copied five times" syntax
entity Case {
  security {
    allow read where RoleGrant.Any(g => g.Holder == user && g.Unit == Scheme.OwningUnit)
                  && !Recusal.Any(r => r.Person == user && r.Case.Id == Id && r.Lifted == false)
                  && (!Restricted || RoleGrant.Any(g => g.Holder == user && g.Level == Level.Senior));
  }
}
```

⚠ **The problem is not that it is long — it is that the fifth copy is where a clause goes missing**, and nothing
catches that. It compiles, that entity's own tests pass, and the defect is *"a caseworker read a case they had
recused themselves from"*. Nobody sees it until an audit.

Name each rule once, and the predicate reads like the sentence it enforces:

```osy title="the same rule, composed" test app=security-policy-in-a-where
[Role] enum Level { Caseworker, Senior }

[Principal] entity Person {
  [Required, MaxLength(200), Unique] string Email;
  security { allow read when IsAuthenticated; }
}
entity Unit { [Required, MaxLength(60)] string Name;
  security { allow read when IsAuthenticated; } }
entity RoleGrant { [Required] Person Holder; [Required] Level Level; Unit? Unit;
  security { allow read when IsAuthenticated; } }
entity Scheme { [Required, MaxLength(60)] string Name; [Required] Unit OwningUnit;
  security { allow read when IsAuthenticated; } }
entity Recusal { [Required] Person Person; [Required] Case Case; bool Lifted;
  security { allow read when IsAuthenticated; } }

// Each rule, named once. The first two are ABOUT a row, so they take one.
policy HandledByUnit(Unit u)   => RoleGrant.Any(g => g.Holder == user && g.Unit == u);
policy HasRecused(Guid caseId) => Recusal.Any(r => r.Person == user && r.Case.Id == caseId && r.Lifted == false);
policy MaySeeRestricted        => RoleGrant.Any(g => g.Holder == user && g.Level == Level.Senior);

entity Case {
  [Required, MaxLength(40)] string Reference;
  [Required] Scheme Scheme;
  bool Restricted;
  security {
    allow read where HandledByUnit(Scheme.OwningUnit)
                  && !HasRecused(Id)
                  && (!Restricted || MaySeeRestricted);
  }
}
```

**Nothing is given up for that.** A policy INLINES — the persisted predicate is byte-for-byte the welded one above,
so the database does the same work and a named rule costs nothing at run time. What changes is everything around it:

| | welded | composed |
|---|---|---|
| the recusal rule lives in | five places | one |
| renaming `Lifted` | five edits, and a miss compiles | one edit |
| a missing clause on entity five | silent | still silent — **but there is only one clause to miss** |
| reading the rule | parse the expression | read the names |

### And a query says nothing about it   {#in-a-query}

The rule lives on the entity, so a query over `Case` is just a query. It filters for what the caller is *looking
for*; who the caller is allowed to *see* is already inside the statement the compiler emits, composed from the four
rules above. Nothing here restates a grant, names a role, or checks anything:

```osy title="a query about cases, and not one word about who may see them" test app=security-policy-in-a-where
List<Case> RestrictedUnder(string scheme) {
  return Case.Where(c => c.Restricted && c.Scheme.Name == scheme)
             .OrderBy(c => c.Reference)
             .ToList();
}
```

A caseworker outside the unit gets an empty list, not an error; a recused one does not see the case they recused
from; a junior one sees no restricted case at all — and the function above is the same function for every one of
them. That is the point of putting the rule on the entity: there is no call site that could have forgotten it.

⚠ **Pass the ID, not the row.** `HasRecused(Id)` — a row predicate has no `this`, and the compiler will tell you so
(that is a fact about predicates, not about policies: the welded form cannot say `this` either).

### `when` or `where` — the argument decides   {#when-or-where}

A rule has two clauses: `when` asks who the **caller** is, once per request, with no row in hand; `where` narrows
**rows**, one at a time. A parameterised policy called with one of this entity's own members can only be about the
row — `IsAdmin(Organization)` written on `RoleGrant` means *this row's* `Organization`, and nothing else a C# reader
could take it for — so it is a row rule **whichever word you wrote**. Both of these compile, and they compile to the
same rule:

```osy title="organisation-scoped access, said either way" test app=security-policy-in-a-when
[Role] enum OrgRole { Admin, Member }

[Principal] entity User {
  [Required, MaxLength(200), Unique] string Email;
  security { allow read when IsAuthenticated; }
}
entity Organization { [Required, MaxLength(80)] string Name;
  security { allow read when IsAuthenticated; } }
entity Membership { [Required] User User; [Required] Organization Organization; [Required] OrgRole Role;
  security { allow read when IsAuthenticated; } }

// "is an admin of THIS organisation" — a fact about the caller AND a row.
policy IsAdmin(Organization o) => Membership.Any(m => m.User == user && m.Organization == o && m.Role == OrgRole.Admin);

entity RoleGrant {
  [Required] Organization Organization;
  [Required, MaxLength(40)] string Label;
  security {
    allow read when IsAdmin(Organization);                    // read: this row's Organization
    allow create, update, delete where IsAdmin(Organization); // the same rule, the other word
  }
}
```

The `create` line is a **with-check**: a new `RoleGrant` may only be written for an organisation the caller
administers, decided against the row being created. A `when` that mixes a caller fact with a row fact —
`when IsAuthenticated && IsAdmin(Organization)` — keeps the caller half as the `when` and makes the row half the
`where`. And `osy explain` reads either spelling as the one sentence it is: *"only rows of an Organization where the
caller's Membership has Role = Admin"*.

What the compiler still refuses, at the rule, naming what you passed and what would work:

- **A field-level rule's `when`** — `deny read Label when IsAdmin(Organization)` — cannot be about the row: a
  `when` guard runs once per request, before any row is in hand. Guard `when` on the caller alone, or write this
  rule's OWN `where` instead — a field mask may carry a row-scoped condition, exactly like a row rule's
  (`deny read Label where IsAdmin(Organization);`, [[security-entity-security#field-mask-row-scoped]]).
- **An entity that is not a member** — `IsAdmin(Organization)` on an entity with no `Organization` property. There
  the name IS the entity set (every `Organization`), which a policy taking one row cannot take; the refusal says so
  and offers both ways out: pass a specific row, or add the relation and write the row rule.

### What the compiler will not let you write   {#refusals}

- **Naming a parameterised policy without its argument** — `if (!CanManageTeam)` — is an error. There would be nothing
  for it to decide about, and a rule that quietly decides about nothing denies people who should be allowed.
- **Giving the wrong number of arguments** is an error, naming what the policy declares.
- **A lambda variable that shadows the policy's own parameter** — `CanManageTeam(Team t) => …Any(t => …)` — is an
  error. One name would mean two different rows.
- **Gating a page on a parameterised policy** — `[Authorize(CanManageTeam)]` — is an error. A page gate runs before any
  row is loaded, so there is no argument to give it. Gate the page on a plain policy and check the row inside.
- **Two policies that call each other** — `A(u) => B(u)` and `B(u) => A(u)` — is an error. There is no fixed
  predicate to inline, and a rule with no fixed meaning cannot be enforced.

## Examples       {#examples}

```osy title="the same policy, in all three places" test app=security-naming-a-policy
entity Memo {
  [Required] Team Team;
  [MaxLength(200)] string Note;
  security {
    allow read when IsAuthenticated;
    // Declared authority: the POLICY itself, passing the row it is about. This block used to spell the rule out
    // again by hand — the page claimed the policy was used in all three places while one of them was a copy.
    allow update, delete where CanManageTeam(Team);
  }
}

// …and named in code, for the authority the engine cannot carry for you — an elevated operation, an external call,
// anything where the decision is not a row read.
bool MayManage(Guid teamId) {
  var team = Team.Where(t => t.Id == teamId).FirstOrDefault();
  return CanManageTeam(team);
}
```

## See also       {#see-also}

[security { }](https://osysharp.com/reference/security/entity-security/) — declaring who may read and write an entity's rows.

[principal predicates (IsAuthenticated / IsAnonymous) and open reads](https://osysharp.com/reference/security/principal-predicates/) — `IsAuthenticated` and the other built-in facts about the caller.

[secure by default (deny-all)](https://osysharp.com/reference/security/secure-by-default/) — why everything is denied until a rule grants it.
