# grants (a link that opens one row)

> A `grant` is a link the platform signs that lets whoever holds it reach ONE row of ONE entity, for a while, without signing in — the "manage your mail" link at the foot of a newsletter, a confirmation link, a pay-this-order link. Declare it once (`grant G for E { Expires = … }`), admit it in the entity's rules (`allow read by G;`), mint it (`G.For(row)`), and take it as a typed parameter (`void Leave(G link)`, `component Page(G link)`). The platform checks the signature, the app, the grant, the expiry and the revocation stamp before any of your code runs.

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

## Summary        {#summary}
Some pages are for people who will never sign in: the reader who clicks "manage your mail" at the foot of a letter,
the customer paying an order from a mail, the visitor confirming an address. What they hold is a LINK, and the link is
the permission. A `grant` is that link, declared as a type:

```osy title="a letter's preferences link, end to end" test app=security-grants
using Osysharp.Ui;

entity Subscriber {
  [MaxLength(200)] string Email;
  bool Weekly;
  security {
    allow read by Preferences;              // a Preferences link reads the row it was made for
    allow update Weekly by Preferences;     // …and may change this one column of it
  }
}

/// What the foot of every letter links to.
grant Preferences for Subscriber { Expires = TimeSpan.FromDays(30); }

/// Made when the letter is sent, by code that can read the subscriber.
string PreferencesLink(Subscriber s) { return $"{App.BaseUrl}/preferences/{Preferences.For(s)}"; }

// Called by the page. The platform has checked `link` before the first line runs.
[AllowAnonymous] string AddressOf(Preferences link) { return link.Row.Email; }
[AllowAnonymous] bool GetsWeekly(Preferences link) { return link.Row.Weekly; }
[AllowAnonymous] void SetWeekly(Preferences link, bool on) { link.Row.Weekly = on; UnitOfWork.Commit(); }

[Route("/preferences/{link}")]
[AllowAnonymous]
component PreferencesPage(Preferences link) {
  string address = "";
  bool weekly = false;
  on mount { address = AddressOf(link); weekly = GetsWeekly(link); }
  // The switch has already written `weekly` when this runs — the action only saves it.
  action Save() { SetWeekly(link, weekly); }
  render {
    Stack(gap: 4) {
      Text($"Mail for {address}");
      Switch("The weekly letter", value: weekly, onToggle: Save);
    }
  }
}
```

Nobody signs in and nobody needs to. The link reads Subscriber rows exactly as far as the rule says — the one row it
was made for — and nothing else in the app: the visitor is still anonymous for every other entity.

## Signature      {#signature}
```osy syntax
grant G for Entity { Expires = TimeSpan.FromDays(n); }    // also FromHours / FromMinutes / FromSeconds; at most 400 days

security {
  allow read by G;                                        // the row G was made for
  allow read by G where Subscriber == G.Row;              // rows of ANOTHER entity, reached from G's row
  allow update Col1, Col2 by G;                           // writes are opt-in, per column
}

G.For(row)              // mint a link (a G value; "$"{…}"" gives the text to put in a URL)
G.RevokeAll(row)        // every link ever made for this row stops working, at once
G.From(text)            // check link text you received some other way; throws the refusal if it is not valid
void F(G link) { … }    // a parameter: checked before the body runs; `link.Row` is the row
[Route("/x/{link}")] component P(G link) { … }             // a page: checked in routing, before the page is served
app.Ui = new AppUi { InvalidLinkSurface = LinkGone };      // what a refused link shows (optional)
```

## Description    {#description}

### What a grant admits   {#admits}
A grant admits **one row of one entity**, and only where a rule says so. `allow read by Preferences;` on `Subscriber`
means: a caller holding a valid `Preferences` link reads the Subscriber row that link was made for. It does not read
other subscribers — a `Subscriber.Count()` under the link answers 1, not the size of the table — and a grant of another
kind for the same row (a `Receipt` link) satisfies nothing that names `Preferences`.

To reach rows of *another* entity from the grant's row, name the column that connects them:

```osy title="the memberships of the subscriber the link was made for" test app=security-grants-where
entity Subscriber {
  [MaxLength(200)] string Email;
  [ForeignKey(Subscriber)] Membership[] Memberships;
  security { allow read by Preferences; }
}

entity Membership {
  [Required] Subscriber Subscriber;
  [MaxLength(60)] string List;
  security { allow read by Preferences where Subscriber == Preferences.Row; }
}

grant Preferences for Subscriber { Expires = TimeSpan.FromDays(365); }

[AllowAnonymous] List<string> ListsOf(Preferences link) {
  return link.Row.Memberships.Select(m => m.List).ToList();
}
```

A `by` rule may carry any `where` you like; if it does not name `G.Row` itself, the platform adds `Id == G.Row`
for you, so `allow read by G where Active;` still reads only the grant's row.

### Read-only unless you say otherwise   {#writes}
A grant reads. It writes only the columns an `allow update … by G` names — `allow update Weekly by Preferences;` lets
the link change `Weekly` and nothing else on the row; a write to `Email` through the same link is refused. A grant
never creates or deletes the row it names: a link is permission to act on that row, and "remove it" is the app's
decision, taken by a function the link calls (the mailing-lists kit's `LeaveWithLink` is one).

### Adding a row to the row a link names   {#adds}
A link may ADD a row of ANOTHER entity that hangs off its row — a reader's message to the desk from the link in their
mail, the browser a continue link is opened in — when that entity's rule says so, naming the column that reaches the
granted row:

```osy title="a comment, added by whoever holds the reader's link — to that reader, and to no one else" test app=security-grants-adds
entity Reader {
  [MaxLength(200)] string Email;
  security { allow read by Preferences; }
}
entity Comment {
  [Required] Reader Reader;
  [MaxLength(500)] string Text;
  security {
    allow create by Preferences where Reader == Preferences.Row;   // only on the row the link was made for
    allow read by Preferences where Reader == Preferences.Row;
  }
}
grant Preferences for Reader { Expires = TimeSpan.FromDays(30); }

[AllowAnonymous] void Comment(Preferences link, string text) {
  new Comment { Reader = link.Row, Text = text };
  UnitOfWork.Commit();
}
```

The new row is judged on its own values like any create: a comment the same holder aims at ANOTHER reader is refused.
`allow create by G` on the grant's OWN entity stays a compile error — that row does not exist yet, so there is nothing
for the link to name.

### Field masks still apply   {#masks}
A grant does not lift a `deny read` — the holder is anonymous, and `deny read Secret when IsAnonymous;` hides `Secret`
from them exactly as it hides it from any other visitor. The link reaches the ROW, never past the row's own rules.

A rule that compares with `user` covers the holder too: with nobody signed in `Owner != user` is unknown, and a
`deny` whose condition is unknown applies ([[security-entity-security#unknown-deny]]). So when the same entity is also
read by ordinary visitors — a bag that belongs to a visit until somebody signs in — one rule masks what an owned row
says about its owner, and leaves an unowned one alone:

```osy syntax
deny read BornOn when !IsStaff where Owner != null && Owner != user;   // a link's holder, or anyone else not the owner
```

### Does holding a link sign anyone in?   {#not-a-principal}
Holding a link does not sign anybody in. `Session.CurrentUser` is null, `IsAnonymous` is true, and every entity whose
rules do not name the grant is as closed to the holder as to anyone else. So a grant cannot be escalated into an
account, and a page taking one needs `[AllowAnonymous]` like any public page.

### Making a link   {#minting}
`G.For(row)` makes a link for `row`, and only for a row the code making it can read *now* — the link hands the row on,
so only someone who holds the row may hand it on. Typically that is a workflow or a send (which runs as the app), or a
signed-in owner sharing their own row. `$"{G.For(row)}"` is the text to put in a URL; it is URL-safe and starts `g1.`.

A link is signed with the app's own key: nothing to configure, no secret to set, and a link made by one app opens
nothing in another, even for a row with the same id.

### Expiry and taking links back   {#revoking}
Every grant says how long its links work — a grant without `Expires` is a compile error, because "forever" should be
a choice you can see. At most 400 days.

`G.RevokeAll(row)` makes every `G` link ever made for that row stop working, immediately — call it when the row is
erased, when an order is paid, when a reader asks for their links to be reset. It takes back `G`'s links only: a row
may carry links of several grants that end at different moments — an order's payment link is taken back once it is
paid, while the link to the order itself, in the confirmation mail, goes on working — so taking back every kind is
one `RevokeAll` per grant. It is committed on its
own the moment it runs, so a rollback elsewhere cannot undo it. A new `G.For(row)` afterwards works again. And a link to
a row that has been deleted stops working by itself. What `RevokeAll` changes is kept in one small platform table,
`Osysharp.RowGrants`, which an app carries once it (or a package it uses) declares a grant; an app that stops declaring
its grant keeps the table, so a link it took back stays taken back if the grant returns.

### Taking a link   {#taking}
Declare the parameter as the grant: `void Leave(Preferences link)`. The platform checks the link before the first line
of the body — signature, app, grant, expiry, revocation, the row still existing — and anything else throws one
refusal, **"This link has expired or is not valid."**, the same words whatever was wrong (telling "expired" from
"forged" would tell a guesser which guess was nearly right). Inside the body, `link.Row` is the row.

The link counts for **that call**. A function that hands a link to a helper does not itself read under it afterwards,
and on a page the next request carries no link unless it passes one again.

Link text that arrives some other way — a webhook body, an HTTP endpoint's path segment — becomes a checked grant with
`G.From(text)`. Its `.Row` is read under the grant; the grant is not held after `From` returns.

### How does a page take a grant?   {#pages}
A page parameter typed as a grant must be a segment of the page's route (`[Route("/preferences/{link}")]`), and the
platform checks it in ROUTING, before the page is served: a link that does not check out never reaches your component.
The visitor gets a 404 carrying the app's own page for it — `app.Ui = new AppUi { InvalidLinkSurface = LinkGone };`,
served inside that page's `[Layout]` — or, when the app names none, a plain page saying *This link has expired or is
not valid.*

```osy title="the app's own page for a link that has run out" test app=security-grants-invalid-link
entity Subscriber {
  [MaxLength(200)] string Email;
  security { allow read by Preferences; }
}
grant Preferences for Subscriber { Expires = TimeSpan.FromDays(30); }

[AllowAnonymous] string AddressOf(Preferences link) { return link.Row.Email; }

[Route("/preferences/{link}")] [AllowAnonymous]
component PreferencesPage(Preferences link) {
  string address = "";
  on mount { address = AddressOf(link); }
  render { Text(address); }
}

[Route("/link-gone")] [AllowAnonymous]
component LinkGone() {
  render { Text("That link has run out. The one at the foot of our latest letter always works."); }
}

app.Ui = new AppUi { InvalidLinkSurface = LinkGone };
```

A component cannot read `link.Row` itself — the browser holds only the link's text. Pass the grant to a server function
that reads it, as `AddressOf` does above; the compiler says so, with that shape, if you write `link.Row` in a component.

Pages that take a grant are served with `Referrer-Policy: no-referrer` and `Cache-Control: no-store`, so the link in
the address does not leave in a Referer header or sit in a shared cache, and the client router always loads them fresh
from the server, so the check above always runs. The platform's own logs never carry a link: every `g1.…` in a
logged path is written `g1.[redacted]`.

### What the compiler refuses   {#errors}
| You wrote | You are told |
|---|---|
| `allow read by Preferencs;` | `` `by Preferencs` names no declared grant. Did you mean `by Preferences`? `` |
| `Preferences.For(order)` where `Preferences` is for `Reader` | `` `Preferences.For` takes the Reader the link is for `` — and the grant an `Order` link would need |
| `grant Pay for Ordr { … }` | `grant 'Pay' is for 'Ordr', and no entity has that name` |
| `component P(Preferences link)` on a route without `{link}` | the parameter `must be a segment of the route` |
| `grant G for E { }` | `says nothing about how long its links work` |
| `new Preferences { … }` | `a grant is not built with new` — `Preferences.For(row)` makes one |
| `allow read by Preferences;` on an entity that is not the grant's | the rule to write: `allow read by Preferences where Reader == Preferences.Row;` |
| `allow create by Preferences;` on the grant's own entity | a create has no row for the link to name — and, to let the link ADD a row, the create to write on the other entity |
| `link.Row` in a component | read it in a server function, with the function to write |

## Examples       {#examples}
The worked example is the mailing-lists kit — its "manage your mail" page and its confirmation link are grants; see
[The MailingLists kit — a newsletter done properly](https://osysharp.com/reference/ui/mailing-lists-kit/). The shop kit uses three more. The link to an ORDER — `grant OrderLink for Order { Expires =
TimeSpan.FromDays(365); }` — is what every mail about an order, the page a payment provider sends the customer back to
(`/paid/{link}/…`) and the receipt carry, instead of the order's reference: the order is read by the account it was
placed from, the visit that placed it, staff, and whoever holds its link, and a reference opens nothing. Several
grants are admitted by ONE rule, beside the rest of who may read the row:

```osy syntax
allow read when IsAnonymous || IsAuthenticated
  where Account == user || VisitId == Visitor.Id || IsShopPacker || Id == OrderLink.Row || Id == Pay.Row;
```

Its payment link (`grant Pay for Order { Expires = TimeSpan.FromDays(7); }`, a `[Route("/pay/{pay}")] component
PayPage(Pay pay)` calling `[AllowAnonymous]` functions that take `Pay pay`) is taken back with `Pay.RevokeAll(order)`
when the order is paid or called off — and the `OrderLink` in the confirmation mail goes on working, because a
`RevokeAll` takes back its own grant's links only. And the abandoned-bag reminder's "Back to your bag" (`grant
ResumeBag for Bag`, read on the bag and its lines, the owner's details on the bag masked as [above](#masks), taken back
once the bag is ordered or emptied).

## See also       {#see-also}
- [security { }](https://osysharp.com/reference/security/entity-security/) — the `security { }` block a `by` rule lives in
- [public pages (what a signed-out visitor can see and do)](https://osysharp.com/reference/security/public-reads/) — the other way to let a signed-out visitor in, for data meant for everyone
- [routes and pages](https://osysharp.com/reference/ui/routing/) — `[Route]` routes and their parameters
- [UI surfaces (app.Ui)](https://osysharp.com/reference/config/ui/) — `app.Ui`, where `InvalidLinkSurface` is set
- [The MailingLists kit — a newsletter done properly](https://osysharp.com/reference/ui/mailing-lists-kit/) — grants in a real kit
