# public pages (what a signed-out visitor can see and do)

> A public page, its public data and its public actions are three separate declarations. `[AllowAnonymous]` on a component says a signed-out visitor may see the PAGE; whether they get its ROWS is decided by the entity's own `security { }` block; whether they may RUN an action is a grant on the function it calls. Grant only the page and it renders perfectly with nothing in it, and its buttons do nothing — the quietest failures in a public app.

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

## Summary        {#summary}
A shop's catalogue, a published article, a price list: some data is meant for people who have not signed in. Making
that work takes **two declarations, on two different things**, because the page and its data are gated separately:

- **`[AllowAnonymous]` on the component** — a signed-out visitor may load the PAGE. Without it, routing sends them to
  your login route and they never see it.
- **an anonymous read grant on the ENTITY** — a signed-out visitor may read its ROWS. Without it, the page renders
  and every query on it comes back refused.

If the page also lets a visitor *do* something, there is a **third**: `[AllowAnonymous]` on the function the action
calls. See *Writing is a THIRD grant*, below.

```osy title="both halves, and the page works signed out" test app=security-public-reads
[Principal] entity User { [MaxLength(200)] string Email; }   // who "signed in" means, for the grants below

entity Product {
  [MaxLength(80)] string Name;
  decimal Price;
  security {
    allow read when IsAnonymous || IsAuthenticated;   // ← the DATA half: anyone may read the catalogue
    allow create, update, delete when IsAuthenticated;
  }
}

[Page("/")]
[AllowAnonymous]                                      // ← the PAGE half: anyone may load this route
component Catalog() {
  live var products = Product.ToList();
  render { foreach (var p in products) { Text(p.Name); } }
}
```

The two do not inherit from each other, in either direction. That is deliberate — a public page composed of private
data is a real design, and so is a private page over public data — but it means **granting one and forgetting the
other is a thing you can do**, and the failure is silent.

## Signature      {#signature}
```osy syntax
security {
  allow read when IsAnonymous;                    // signed-out visitors only
  allow read when IsAnonymous || IsAuthenticated; // everyone, said out loud
}
```

## Description    {#description}

### The half that is easy to forget   {#empty-page}
Under [deny-all](https://osysharp.com/reference/security/secure-by-default/), an entity you have not granted is denied to everyone — including the
anonymous visitor. So a page marked `[AllowAnonymous]` over an ungranted entity **renders**: the chrome, the headings,
the empty list, the `0 items` count. No error, no redirect, no console message. It simply shows nothing, and it looks
exactly like a catalogue with no products in it.

This is worth stating plainly because it survives the checks you would expect to catch it. It compiles. Its tests pass
— a test runs under a principal you choose, and the ordinary anonymous read those tests exercise is honoured correctly.
The page renders in every screenshot taken while signed in. **Only opening the app signed out shows the gap.**

The compiler does look for it. An `[AllowAnonymous]` page reading an entity with no anonymous grant raises the
`security-anon-page-reads-ungranted-entity` maturity finding, which names the page, the entity, and the line to add.

### What "reaches an anonymous caller" means   {#grants}
An entity is anonymously readable when its `security { }` block grants a read to a caller who is not signed in:

| Grant | Anonymous visitor |
|---|---|
| `allow read when IsAnonymous;` | reads it |
| `allow read when IsAuthenticated \|\| IsAnonymous;` | reads it |
| `allow read when IsAuthenticated;` | **refused** — an anonymous caller is not authenticated |
| `allow read where Owner == user;` | **refused** — there is no `user` to match |
| no `security { }` block at all | **refused** under deny-all |

There is no separate switch and no attribute to add: the grant IS the declaration. Say it once on the entity, and
every surface — the page's query, the boot bundle, the server-side render — follows it.

### Only the rows you granted   {#scope}
Granting an anonymous read opens exactly what the grant says and nothing beside it. The row filter still applies the
predicate per row, field-level `deny read` masks still apply, and an entity you did not grant stays invisible — not
merely unreadable, but absent from the model an anonymous session is served at all. So a public catalogue does not
drag the customer table into public view because they happen to be related.

Prefer the narrowest grant that is true. `IsAnonymous` alone is right when a page is public and the signed-in view is
a different one; the `||` form is right when the answer really is "everyone" — see
[principal predicates (IsAuthenticated / IsAnonymous) and open reads](https://osysharp.com/reference/security/principal-predicates/).

### Writing is a THIRD grant   {#writes}
Reading needs two declarations. A visitor who *does* something — fills a basket, casts a vote, submits a form —
needs three, because an action reaches a server function and **who may call a function is its own grant**:

| | says | if missing |
|---|---|---|
| `[AllowAnonymous]` on the component | a stranger may SEE the page | they are redirected to login |
| an anonymous read/write grant on the entity | a stranger's rows may be read/written | the read is empty; the write is refused at commit |
| `[AllowAnonymous]` on the function | a stranger may CALL it | the click posts and the server refuses it — a **dead button** |

```osy title="the write half" test app=security-public-reads-write
[Principal] entity User { [MaxLength(200)] string Email; }

entity Vote {
  [MaxLength(80)] string Choice;
  security { allow read, create, update when IsAnonymous || IsAuthenticated; }   // the ROWS
}

[AllowAnonymous]                                   // the FUNCTION — a stranger may run it
void CastVote() {
  var v = new Vote { Choice = "yes" };
  UnitOfWork.Commit();
}

[Page("/vote")]
[AllowAnonymous]                                   // the PAGE
component VotePage() {
  action Cast() { CastVote(); }
  render { Button("Vote", onPress: Cast); }
}
```

The third one is the quietest of the three: the page renders, the button is there, it looks enabled, and pressing it
produces nothing at all — the refusal never reaches the screen. The compiler flags it as
`security-anon-page-calls-gated-function`, naming the page, the action and the function.

An `[AuthMethod]` function needs no such mark: it is an anonymous entry point by construction. And an action that
signs someone in and then calls a gated function is fine — by that line the caller is no longer a stranger.

### Look at it signed out   {#verify}
The habit worth forming: open the app **in a private window**, on the real page, and check the data is there. A public
app's most common defect is not a refusal — a refusal is loud. It is a page that renders beautifully and is empty,
and the only reader who ever sees it is the one who is not signed in.

## Examples       {#examples}
A storefront: a public catalogue, a basket keyed to the browser, and a customer record that stays private.

```osy title="public catalogue, private customers" test app=security-public-reads-shop
[Principal] entity Customer {
  [MaxLength(200)] string Email;
  security {
    allow read where Id == user.Id;         // you read your own profile, and nobody reads anyone else's
    allow update where Id == user.Id;

  }
}

entity Product {
  [MaxLength(80)] string Name;
  decimal Price;
  security {
    allow read when IsAnonymous || IsAuthenticated;   // a catalogue is public — that is what a shop is
    allow create, update, delete when IsAuthenticated;
  }
}

[Page("/")]
[AllowAnonymous]
component Catalog() {
  live var products = Product.ToList();
  render {
    Stack(gap: 2) {
      Text(products.Count() + " items");
      foreach (var p in products) { Text(p.Name + " " + p.Price.ToString("C")); }
    }
  }
}
```

Signed out, this page shows the products. `Customer` is granted to nobody anonymous, so it is not readable and not
even described to an anonymous session — the public page cannot leak it by accident.

## See also       {#see-also}
- [principal predicates (IsAuthenticated / IsAnonymous) and open reads](https://osysharp.com/reference/security/principal-predicates/) — `IsAnonymous` / `IsAuthenticated`, and why an open read must say so
- [security { }](https://osysharp.com/reference/security/entity-security/) — the `security { }` block the grant lives in
- [secure by default (deny-all)](https://osysharp.com/reference/security/secure-by-default/) — the deny-all posture that makes an unmentioned grant a denial
- [routes and pages](https://osysharp.com/reference/ui/routing/) — `[AllowAnonymous]`, and protected-by-default routing
- [Visitor](https://osysharp.com/reference/ui/visitor/) — `Visitor.Id`, for work a person begins before they are anybody
