# guest rows (a visit's own rows)

> A row a guest makes before signing in — a basket, a saved choice, what they typed at the checkout — belongs to their VISIT. Inside a `security` rule `Visitor.Id` is the visitor the platform proved for this request (its sealed cookie), so `where Owner == null && VisitId == Visitor.Id` admits a guest to its own rows and to nobody else's, on both the read and the write side, however the id was learnt.

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

## Summary        {#summary}
Every anonymous caller is the same principal to a rule: `user` is null for all of them. So a rule that admits a guest's
row by `Owner == null` admits EVERY guest's row to EVERY guest — the date of birth, the company, the pickup address one
visitor typed at a checkout reads to the next. **`Visitor.Id`** tells guests apart: it is the browser the platform
proved for this request, and a rule compares a row to it exactly as `Owner == user` compares a row to the principal.

```osy title="a guest's basket is its own visit's" test app=security-guest-rows
entity Basket {
  [MaxLength(64)] string? VisitId;
  [MaxLength(200)] string? DeliverTo;
  security {
    allow read, create, update, delete when IsAnonymous || IsAuthenticated where VisitId == Visitor.Id;
  }
}
```

## Signature      {#signature}
```osy syntax
where … VisitId == Visitor.Id …     // inside a security rule: the visitor this request's cookie proves
```

A `string?`. `null` where there is no visitor (a workflow timer, a background job) — and a comparison with it then
matches nothing: an `allow` grants nothing and a `deny` applies.

## Description    {#description}

### Why a rule, and not only the function   {#why}
Write the functions so they never take "which visit" — they read `Visitor.Id` ([Visitor](https://osysharp.com/reference/ui/visitor/)). The rule says the
same thing again, and it is the half that cannot be walked around: a page's own query, a list, a function that forgot,
another package's function over your entity — each reads and writes through the rule. Declared once; after that the
rows simply are the visit's.

### It is proved, not told   {#proved}
The value a rule compares against is the id sealed in the visitor's HttpOnly cookie under the app's own key — never a
value the request states. A caller who KNOWS another guest's visit id (it was on a screen, in a log, in a URL) still
reads none of their rows and cannot create one under their visit: it is not the visitor its cookie proves, and a forged
cookie is answered with a fresh visitor rather than the claimed one.

### Owned rows and staff   {#owned}
A guest row usually becomes an owned one at sign-in, and staff usually see all of them. Say both in the same rule — the
precedence is C#'s, so `&&` binds before `||`:

```osy title="the visit's, then the owner's, and the shop's" test app=security-guest-rows
[Principal] entity Shopper {
  [Unique, MaxLength(200)] string Email;
  security { allow read when IsAuthenticated; }
}

entity Order {
  [MaxLength(64)] string? VisitId;
  Shopper? Owner;
  [MaxLength(200)] string? Note;
  security {
    allow read, create, update when IsAnonymous || IsAuthenticated
      where Owner == null && VisitId == Visitor.Id || Owner == user;
  }
}
```

A child row reaches its parent's visit the ordinary way — `where Basket.Owner == null && Basket.VisitId == Visitor.Id`.

### How does a row get its visit, wherever it is made?   {#stamp}
Give the member the visitor as its **default** — `= Visitor.Id` — and every `new` stamps it, including a row made in
a helper or an overridden method the caller never sees:

```osy title="a note stamped with the browser that wrote it" test app=security-guest-rows
entity Note {
  [MaxLength(64)] string? VisitId = Visitor.Id;
  [MaxLength(500)] string Text = "";
  security {
    allow read, create when IsAnonymous || IsAuthenticated where VisitId == Visitor.Id;
  }
}
Note Jot(string text) { return new Note { Text = text }; }
```

A default rather than an assignment after the fact, because a create rule is judged where the row is MADE — at the
return of the function that made it. A helper that hands a row back for its caller to stamp hands back a row the rule
has already refused.

### Proving it in a test   {#testing}
A test is one browser. `runas (new Visitor { Id = "…" })` makes a block another browser — the principal stays as it
was — which is the whole of a "second guest cannot see the first guest's rows" test:

```osy syntax
[Test]
void a_guest_sees_only_its_own_basket() {
  runas (new Visitor { Id = "guest-a" }) { StartBasket("Home"); }
  runas (new Visitor { Id = "guest-b" }) { Assert.Equal(0, Basket.Count()); }
  runas (new Visitor { Id = "guest-a" }) { Assert.Equal(1, Basket.Count()); }   // the control
}
```

## Examples       {#examples}
The whole shape: a guest starts a basket, sees their own, and nobody else can read it or plant one under their visit.

```osy title="guest-basket" test app=security-guest-rows
[AllowAnonymous]
void StartBasket(string deliverTo) {
  new Basket { VisitId = Visitor.Id, DeliverTo = deliverTo };
  UnitOfWork.Commit();
}

[AllowAnonymous]
int MyBaskets() { return Basket.Count(); }
```

## See also       {#see-also}
- [Visitor](https://osysharp.com/reference/ui/visitor/) — `Visitor.Id` in a page and a function: issued by the platform, the same value everywhere
- [security { }](https://osysharp.com/reference/security/entity-security/) — `allow`/`deny`, `when` and `where`, and how rules combine
- [grants (a link that opens one row)](https://osysharp.com/reference/security/grants/) — a signed link that opens ONE row for someone who is not signed in
- [runas](https://osysharp.com/reference/testing/runas/) — being somebody else in a test
