# deny … except via

> Names the ONLY functions whose own code may change something a deny withholds: `deny update UnitsLeft except via Draw;`. The deny steps aside for exactly that member, that operation and a write the named function itself makes, on the server — never for a write a client sends, and never for a helper the function calls. It is a floor wherever it is written, on a kit's entity and on an app's own: no `allow` opens a way past it and no other code makes the write — not a workflow step, the declaring kit's own included — on a saved row or on one made a moment ago in the same unit of work. Each named function belongs to the entity's owner, runs on the server, writes the member itself, and says who may call it with `[Authorize]`.

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

## Summary        {#summary}
Some things a kit keeps should change in exactly one way. A stock count changes by drawing from it; a record of the
draw is made by the draw itself. A `deny` says nobody may make that change — and `except via` names the kit's
functions that ARE the way it happens:

- the deny steps aside for **the member it names, the operation it names, and a write the named function's own code
  makes** — nothing more;
- "inside the function" is decided by the **server** running it: a client's unit of work, a REST or MCP call, an
  import or a hand-made request is an ordinary write, judged against the deny with no exception;
- a **helper** the function calls does not inherit it, and every OTHER write inside the function is judged as usual;
- it is a **floor**: it is decided after every `allow`, whoever wrote it — a kit on its entity or an app on its own —
  so no grant opens a direct write past it, and no other function, class method, page action, workflow step or
  `runas` block makes the write. That includes the workflow of the kit that wrote the rule: its steps call the named
  function, like everything else.

## Signature      {#signature}
```osy syntax
deny update <Field>, <Field> except via F, G;   // these fields change only inside F or G
deny create except via F;                       // a row of this entity is made only inside F
deny delete except via F;                       // …and removed only inside F
```

`except via` comes after any `when`/`where` and before `message`. It names functions, not strings — a misspelt name
does not compile.

## Description    {#description}

### What does `except via` let through?   {#what-passes}
One thing: a write the named function's OWN statements make, to the member (or the row) the rule is about, for the
operation the rule is about. Everything the rest of the block says still applies — `except via` lifts a deny, it never
grants. So the caller still needs a grant for the write (the kit's `allow update when …`), and the named function still
needs its `[Authorize]` satisfied before its first statement runs.

| the write | judged |
|---|---|
| `Draw`'s own `r.UnitsLeft = r.UnitsLeft - 1;` (`Draw` is named) | the deny steps aside |
| `Replan`'s own `old.Delete();` under `deny delete except via Replan;` beside `allow delete where Owner == user;` | the deny steps aside, and the `where` still decides — the caller's own rows go, another person's are refused |
| the same assignment in a function `Draw` calls | refused — a helper is not the named function |
| `Draw`'s own write to a field the rule does not name | judged by that field's rules, as anywhere |
| the same change sent by a browser, an API call, an import | refused — nothing a request carries is "inside `Draw`" |
| the same person assigning it in any other function | refused |
| the same assignment on a row made a moment ago, not yet saved | refused — see [A row that is not saved yet](#pending-rows) |

### Why is a client's write never "inside the function"?   {#server-only}
Because only the server decides it. When the server runs a function some `except via` names, the writes that
function's own code makes are marked as its writes, as they are made; a write that arrives from anywhere else carries
no mark, and there is nothing a request can send to make one. The rule is enforced on the server, at the write and
again at the commit, whichever way the write came in.

### From a page: show the field, and change it through the function   {#from-a-page}
A page cannot save its own write to such a field — its commit is refused by the deny — so a control that two-way
binds one is a control whose edit goes nowhere. The compiler warns (`TWO_WAY_BIND_TO_GUARDED_MEMBER`), naming the
field and the function to call. Show the value with a plain read, and make the change in the press:

```osy title="a checklist tick that only MarkChecked makes" syntax
[Server, Authorize(IsAuthenticated)]
public void Tick(Check k, bool on) { MarkChecked(k, on); UnitOfWork.Commit(); }

component Checklist() {
  live var checks = AllChecks();
  action Toggle(Check k) { Tick(k, !k.Checked); }
  render {
    foreach (var k in checks) {
      // `checked:` is a READ — the box shows the stored tick, and the press asks the function to change it
      Pressable(onClick: () => Toggle(k), role: UiRole.Checkbox, checked: k.Checked, label: k.Label) { Text(k.Label); }
    }
  }
}
```

A row handed to a function carries the page's edits to it ([[ui-data-mutation#handed-row]]) — but never an edit to a
field the caller may not change itself. The function sees such a field as stored, so a `Checkbox(value: k.Checked)`
pressed into `Tick(k, k.Checked)` still lands: `MarkChecked` finds the box unticked and ticks it. What the bound box
cannot do is take its tick back when the call is refused, which is why the read is the shape to write.

### What does the compiler refuse?   {#refused}
Each of these would read as a decision and enforce something else, so each is an error naming the rule:

- **A named function that does not say who may call it.** Writing past a deny is a privilege, so the function carries
  `[Authorize(<policy>)]` — in a kit, a policy the app answers (a `policy MayDraw;` the kit declares, or a
  `virtual policy MayDraw => false;` with a default) — or `[Authorize(IsAuthenticated)]` when any signed-in caller may
  and its body checks the rest. (`[Authorize(IsAuthenticated)]` is refused as redundant on every other function: there
  it says nothing a function does not already require.)
- **A name that is not a top-level function of the same owner** — the same kit for a kit's entity, the app's own code
  for the app's — or **one that does not itself make the write**. A stale `except via` is an error, and a function
  whose helper makes the write excepts nothing.
- **A named function that could run on the client.** A write the browser makes arrives as a client's unit of work,
  which never carries the exception — the function must make its writes on the server.
- **`except via` on an `allow`, on `read`, or in a `partial`** that adds to another package's entity — which functions
  may write past that entity's denies is its own package's to say.

### A field that changes in several ways — one function per way   {#several-functions}
A deny names as many functions as there are ways the field changes. That is the shape for a status that moves through
states: one function per move, each taking only what that move needs and making the assignment itself. A rule the moves
share goes in a helper that CHECKS — answers a reason, or throws — and never writes, because a helper's write is not
excepted:

```osy title="three moves, one shared check, every way the status changes listed on the entity" syntax
deny update Status except via Ship, Deliver, SendBack;

string NotIn(Parcel parcel, ParcelStatus expected) =>
  parcel.Status == expected ? "" : $"{parcel.Reference} is {parcel.Status.Label}.";   // checks, never writes

[Server, Authorize(IsAuthenticated)]
void Ship(Parcel parcel, string tracking) {
  var why = NotIn(parcel, ParcelStatus.Packed);
  if (why != "") { throw new Exception(why); }
  parcel.Tracking = tracking;
  parcel.Status = ParcelStatus.Shipped;                                               // its own write: excepted
}
```

Not one `Advance(string to, …)` that every move goes through: it takes the state as text, and its callers pad the
arguments the other moves need with `""`. A name whose helper makes the write is refused, and the refusal names the
helper and shows this shape. The whole pattern, compiled with its tests, and the workflow that owns such a field
outright, is [An entity's lifecycle — a workflow that tracks its status, or one function per move](https://osysharp.com/reference/workflow/entity-lifecycle/).

### On your app's own entity   {#app-entity}
An app writes it the same way, naming its own functions, and the compiler vets them exactly as it vets a kit's. A kit's
function can never be named for an app's entity, nor an app's for a kit's.

It holds the same way too. `allow create when IsAuthenticated;` beside `deny create except via MakeJar;` does not let
another of your functions make a jar: the deny is decided after every grant, so `MakeJar` is the only way one is made —
from a page, from a class method, from a workflow step, inside a `runas` block, in a function with a unit of work of
its own. The grant still matters: it says who may make one THROUGH `MakeJar`.

### A row that is not saved yet   {#pending-rows}
A row a function has just made is not saved until the unit of work commits, and until then other code in the same
request can reach it. The deny holds there as it does on a saved row:

| the write, on a row made earlier in the same unit of work | judged |
|---|---|
| the code that made the row, filling it in (its initializer, its own later lines) | part of the create |
| a function named for the field (`Fill(jar)` after `MakeJar(…)`) | the deny steps aside |
| any other code writing a field an `except via` names — the caller that got the row back, a helper | refused, at the write |
| any other code writing a field no `except via` names | part of the create — what may be put on a new row is the create rules' to say |

So `var jar = MakeJar("rye"); jar.Level = 77;` is refused exactly as it would be a request later. A
`deny update … where … except via F` cannot be decided on a row with nothing stored yet, and is left to the create's
rules.

### A kit's plain `deny update Field;` on a row that is not saved yet   {#pending-plain-deny}
A kit also writes denies that name no function and carry no condition — `deny update Member;` on a membership means
nobody re-points it, ever. That line holds on the row the kit's function has just handed back, too:

| the write, on a row a kit's function made earlier in the same unit of work | judged |
|---|---|
| the kit's own code — the function that made the row, and any other function of the kit (or of a kit built on it) that goes on filling it | part of the create |
| your app's code writing a field the kit marks `deny update Field;` — the caller that got the row back, a helper, a class method, a workflow step | refused, at the write |
| your app's code writing a field under a CONDITIONAL deny (`deny update Text when !IsStaff`) | part of the create |
| your app's code writing a field no deny covers | part of the create |

The line between the second and third rows is what the rule says. A deny with no `when` and no `where` says the field
does not change once the row exists, whoever asks — so it does not matter that the row is a moment old. A deny with a
condition says WHO may change the row, and the person filling a new row is the person who made it: a visitor who may
create a message and not edit one still has it filled in by a helper before it is saved, and the rule is asked of them
from the commit on.

What the kit's function put on the row is the kit's decision. If your code needs a different value there, pass it to
the kit's function; if the function takes none, the field is not yours to set.

Your app's own plain `deny update Field;` on its own entity is not asked on a pending row — every function that could
write it is yours. Name the function (`deny update Field except via F;`) when one of them must be the only way.

### How does a test make the row?   {#tests}
A test body runs as its `[runas(…)]` person and meets the deny like any caller: make the row through the named
function. A `[TestFixture]` is the system and seeds past every `security { }` block, this one included — put the rows
that are the test's world there. See [Fixtures: built once, cloned for every test, and built on each other](https://osysharp.com/reference/testing/fixtures/).

⚠ **Not a grant instead.** "Let only the desk change it" written as `allow update Level when IsDesk;` lets the desk's
BROWSER write `Level` to anything, directly. `except via` is what makes the function the only way it changes — the
check the function makes is then the check every change passes.

### Does a workflow body follow the same rule?   {#workflows}
Yes, and it is one rule for every body: the exception is the same triple in a workflow body and in a request. A write
`Draw` makes passes `Draw`'s `except via` wherever `Draw` was called from; nothing else in a body does.

A body writes as the system, and for a kit's PLAIN denies that has an exception — the kit's own workflow, and its
functions called from your body, write past them (see [What a workflow body may write](https://osysharp.com/reference/workflow/body-security/)). A deny that names a function has
none:

| the write to a member `deny update UnitsLeft except via Draw;` guards | judged |
|---|---|
| a step of your app's workflow assigning it | refused |
| a step of the kit's OWN workflow assigning it | refused |
| a function of the kit that is not `Draw`, called from either workflow | refused |
| a step of a kit built on the kit assigning it | refused |
| an `[On]` handler — the app's or the kit's — assigning it | refused |
| `Draw`, called from any of them | the deny steps aside |

So a kit that changes a guarded member on a timer writes a function for it and names it: the rule then lists every
way the member changes, and a reader of the `security { }` block sees all of them.

```osy title="a kit's own clock goes through the function the rule names" syntax
deny update Remaining, Expired except via Spend, ExpirePack;

public workflow PackLife {
  …
  state Expiring {
    Expire { Within = UntilExpiry(this.Item); }
    on Expire { ExpirePack(this.Item); goto Ended; }   // not `this.Item.Remaining = 0;` — the step is not the function
  }
}

/// The pack's own clock is the only caller; a workflow body does not ask `[Authorize]`.
[Server, Authorize(ManagesPacks)]
void ExpirePack(Pack pack) { pack.Expired = pack.Remaining; pack.Remaining = 0; }
```

### A statement over many rows   {#set-based}
A set-based update or delete (one statement over every matching row) carries no function's mark, so a deny it meets
does not step aside. Write the rows one at a time inside the named function.

## Examples       {#examples}

### A kit whose stock changes only by drawing it   {#example-kit}
The kit's side — the deny, the policy the app answers, and the functions it names:

```osy title="a stock count and its record, each changed one way" syntax
namespace Acme.Reagents;

/// Who may draw stock — the app answers.
policy DrawsReagents;

public entity Reagent {
  [Required, MaxLength(80)] string Label;
  int UnitsLeft = 0;
  security {
    allow read, create, update when IsAuthenticated;
    deny update UnitsLeft except via Draw;
  }
}

public entity Withdrawal {
  [Required] Reagent Reagent;
  security {
    allow read, create when IsAuthenticated;
    deny create except via Draw;
  }
}

[Server, Authorize(DrawsReagents)]
public void Draw(Reagent r) {
  r.UnitsLeft = r.UnitsLeft - 1;      // passes: Draw's own write, the member it is named for
  new Withdrawal { Reagent = r };     // passes: Draw's own create
}
```

### An app answering a kit's policy   {#example-app}
The UserAccounts kit lets a desk move an account to a new address — `ChangeAddressFor` is the only way a change is made
(`deny create except via ChangeAddressFor`), and it runs only for the people the app names. The app's whole part is the
policy:

```osy title="the desk's leads may move an account; the kit does the rest" test app=security-except-via
app Desk {
  use Osysharp.Ui;
  use Osysharp.Identity;
  use Osysharp.Mail;
  use Osysharp.UserAccounts;
  use Osysharp.EmailTemplates;
  model "model/**/*.osy";
}

using Osysharp.Identity;
using Osysharp.UserAccounts;

[Role] enum AppRole { Authenticator, Agent, Lead }

policy IsLead => RoleGrant.Any(g => g.Member == user && g.Role == AppRole.Lead);
policy ChangesAccountsFor => IsLead;

/// What the lead's screen calls for somebody whose old address is gone.
void MoveForThem(User account, string newAddress) { ChangeAddressFor(account, newAddress, "ID checked at the desk"); }

[Route("/")] component Home() { render { Text("The desk"); } }
```

### An app's own entity, changed only by its own function   {#example-app-entity}

```osy title="a jar is made one way and filled one way" run app=security-except-via-app
[Principal] entity Cook {
  [Unique, MaxLength(80)] string Name;
  security { allow read when IsAuthenticated; }
}

entity Jar {
  [Required, MaxLength(80)] string Label;
  int Level = 0;
  security {
    allow read, create, update when IsAuthenticated;
    deny create except via MakeJar;
    deny update Level except via Fill;
  }
}

/// Any signed-in person may make and fill a jar — said, because each writes past a deny.
[Server, Authorize(IsAuthenticated)]
Jar MakeJar(string label) { return new Jar { Label = label }; }

[Server, Authorize(IsAuthenticated)]
void Fill(Jar jar) { jar.Level = 10; }

void Shortcut(string label) { new Jar { Label = label }; }                 // not MakeJar
void MakeAndOverfill(string label) { var jar = MakeJar(label); jar.Level = 77; }   // not Fill
void MakeAndFill(string label) { var jar = MakeJar(label); Fill(jar); }

[TestFixture]
void Pantry() {
  new Cook { Name = "Ida" };
  new Jar { Label = "oats", Level = 3 };      // the fixture is the system: it seeds past the deny
}

principal Ida => Cook.Single(c => c.Name == "Ida");

[Test(Pantry)]
[runas(Ida)]
void Only_the_named_functions_make_and_fill_a_jar() {
  Assert.Denied(() => Shortcut("rye"));              // the grant beside the deny does not open it
  Assert.Denied(() => MakeAndOverfill("rye"));       // …nor does the row being a moment old
  Assert.Equal(0, Jar.Count(j => j.Label == "rye"));
  MakeAndFill("barley");
  Assert.Equal(10, Jar.Single(j => j.Label == "barley").Level);
  Assert.Equal(3, Jar.Single(j => j.Label == "oats").Level);
}
```

### A kit's plain deny, on the row its function just made   {#example-pending-plain-deny}
The Organizations kit makes a membership with `Join` and writes `deny update Member;` — a membership is one person in
one organisation, never re-pointed. The app below holds the row `Join` returned and tries to put somebody else on it
before anything is saved:

```osy title="a membership is not re-pointed in the moment before it is saved" run app=security-except-via-pending-kit
app Portal {
  use Osysharp.Identity;
  use Osysharp.Organizations;
  model "model/**/*.osy";
}

using Osysharp.Identity;
using Osysharp.Organizations;

[Role] enum PortalRole { Authenticator, Staff }

policy IsStaff => RoleGrant.Any(g => g.Member == user && g.Role == PortalRole.Staff);
/// The kit's question, answered by the app.
policy ManagesOrganizations => IsStaff;

entity Person : Principal {
  security { allow read when IsAuthenticated || IsAuthenticator; }
}

entity Company : Organization {
  security { allow read, create, update when IsStaff; }
}

/// The kit's `Join` makes the membership and hands it back…
void Enrol(Company company, Person who) { Join(company, who); }
/// …and this tries to finish the job its own way.
void EnrolThenSwap(Company company, Person who, Person other) {
  var membership = Join(company, who);
  membership.Member = other;            // refused: the kit wrote `deny update Member;`
}

[TestFixture]
void Portal() {
  var sam = new Person { Email = "sam@portal.test", DisplayName = "Sam" };
  new RoleGrant { Member = sam, Role = PortalRole.Staff };
  new Person { Email = "ada@acme.test", DisplayName = "Ada" };
  new Person { Email = "bo@acme.test", DisplayName = "Bo" };
  new Company { Name = "Acme" };
}

principal Sam => Person.Single(p => p.Email == "sam@portal.test");

[Test(Portal)]
[runas(Sam)]
void A_membership_the_kit_made_is_not_repointed_before_it_is_saved() {
  var acme = Company.Single(c => c.Name == "Acme");
  var ada = Person.Single(p => p.Email == "ada@acme.test");
  var bo = Person.Single(p => p.Email == "bo@acme.test");
  Assert.Denied(() => EnrolThenSwap(acme, ada, bo));
  Assert.Equal(0, Membership.Count(m => m.Organization == acme));
  Enrol(acme, ada);
  UnitOfWork.Commit();
  Assert.True(Membership.Any(m => m.Organization == acme && m.Member == ada));
}
```

## See also       {#see-also}
- [security { }](https://osysharp.com/reference/security/entity-security/) — the `security { }` block this clause belongs to
- [Security.Authorize — does THIS person satisfy a policy](https://osysharp.com/reference/security/authorize/) — the `[Authorize]` every named function carries
- [[Authority]](https://osysharp.com/reference/security/contribution-authority/) — why a kit's deny binds an app's rules and subtypes
- [What a workflow body may write](https://osysharp.com/reference/workflow/body-security/) — what a workflow body may write, and the kit denies that bind it
- [The UserAccounts kit — sign-in done properly, once](https://osysharp.com/reference/security/accounts-kit/) — a kit that uses it for an address change a desk starts
- [Fixtures: built once, cloned for every test, and built on each other](https://osysharp.com/reference/testing/fixtures/) — seeding the rows a test needs behind a deny
- [An entity's lifecycle — a workflow that tracks its status, or one function per move](https://osysharp.com/reference/workflow/entity-lifecycle/) — a status that moves through states: one function per move, or a workflow
- [allow read via](https://osysharp.com/reference/security/read-via/) — the READ counterpart: the functions whose own reads see every row, to compute a value
