Osy#the first language built for agents
Agents firstAgentic appsWorkflowsDurable Execution — built inSecurityTestingThe editorThe UI modelOne program

Reference / Security

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

security { allow read when IsAnonymous || IsAuthenticated; } // on the ENTITY the public page reads

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.

preview3 examples compiled by CIsecurityauthorizationanonymousui

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.

[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#

security {
  allow read when IsAnonymous;                    // signed-out visitors only
  allow read when IsAnonymous || IsAuthenticated; // everyone, said out loud
}

Description#

The half that is easy to forget#

Under deny-all, 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#

An entity is anonymously readable when its security { } block grants a read to a caller who is not signed in:

GrantAnonymous 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 allrefused 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#

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.

Writing is a THIRD grant#

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:

saysif missing
[AllowAnonymous] on the componenta stranger may SEE the pagethey are redirected to login
an anonymous read/write grant on the entitya stranger's rows may be read/writtenthe read is empty; the write is refused at commit
[AllowAnonymous] on the functiona stranger may CALL itthe click posts and the server refuses it — a dead button
[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#

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#

A storefront: a public catalogue, a basket keyed to the browser, and a customer record that stays private.

[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#

Related

principal predicates (IsAuthenticated / IsAnonymous) and open reads

Two built-in `when` predicates say who a request is: `IsAuthenticated` is a signed-in user, `IsAnonymous` is an…

security { }

The rules that decide who may read and write an entity's rows. A where clause filters by the row (the owner sees their…

secure by default (deny-all)

Deny-all is the posture, and it is the only one: an entity that declares no `security { }` block is denied to every…

routes and pages

How a component becomes a page: it declares a route with `[Page("/catalog/{slug}")]`, and navigating to a matching path…

Visitor

`Visitor.Id` is a stable opaque id for the browser someone is using, minted on their first visit and remembered…