Osy#betaRun it on Osyrin: osyrin.com

Reference / Security

guest rows (a visit's own rows)

allow read, create, update when IsAnonymous || IsAuthenticated where Owner == null && VisitId == Visitor.Id || Owner == user;

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.

preview4 examples compiled by CIsecurityauthorizationanonymousvisitor

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.

entity Basket {
  [MaxLength(64)] string? VisitId;
  [MaxLength(200)] string? DeliverTo;
  security {
    allow read, create, update, delete when IsAnonymous || IsAuthenticated where VisitId == Visitor.Id;
  }
}

Signature#

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#

Why a rule, and not only the function#

Write the functions so they never take "which visit" — they read Visitor.Id (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#

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#

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 ||:

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

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:

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#

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:

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

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

[AllowAnonymous]
void StartBasket(string deliverTo) {
  new Basket { VisitId = Visitor.Id, DeliverTo = deliverTo };
  UnitOfWork.Commit();
}

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

See also#

  • Visitor — Visitor.Id in a page and a function: issued by the platform, the same value everywhere
  • security { } — allow/deny, when and where, and how rules combine
  • grants (a link that opens one row) — a signed link that opens ONE row for someone who is not signed in
  • runas — being somebody else in a test

Related

Visitor

`Visitor.Id` names the browser someone is using. The platform issues it on their first visit, proves it on every…

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…

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…

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

A public page, its public data, its public actions and its live updates are four separate declarations…

principal predicates (IsAuthenticated / IsAnonymous / IsTheApp) and open reads

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

runas

Runs a block as a given principal, so security rules apply exactly as they would for that user. It is how you test that…