Summary#
Two built-in when predicates describe the request's principal, for use in an entity security { } block:
IsAuthenticated— a signed-in user is making the request.IsAnonymous— no one is signed in (the unauthenticated visitor).IsAuthenticated || IsAnonymous— everyone, stated explicitly.
They exist because a bare unqualified allow — any verb, with no when guard and no where row filter — is a
compile error. It hands that verb to every caller, anonymous included, and the runtime consults nothing: on a read
it is the shape by which a [Principal] leaks its own credential columns, and on a delete it is worse. So you must
say who: a role, a where filter, or one of these predicates.
[Principal] entity User {
string Email;
security {
allow read when IsAuthenticated; // any signed-in user may read the directory (not the world)
}
}Signature#
allow read when IsAuthenticated; // a signed-in user
allow read when IsAnonymous; // an unauthenticated visitor
allow read when IsAuthenticated || IsAnonymous; // everyone, explicitly
allow read when IsAuthenticated || IsStaff; // compose with your own policiesUsable in a when guard or a policy body (a per-request fact about the principal — not a where row filter), and
usable whether or not the app declares a [Principal]: an app with none has exactly one kind of caller, so
IsAnonymous is true there and IsAuthenticated is false. IsAuthenticated, IsAnonymous and IsTheApp are
reserved — a policy may not take those names. The third is not about a caller at all: see
the app itself is acting.
Description#
The rule applies to every app and all four verbs, whether or not a [Principal] is declared. It once fired only
for read, and only on apps that had a principal; both exemptions are gone. The principal exemption in particular was
a trap rather than a kindness — a principal-less app was let off, and then silently inherited every unqualified grant
the day it declared a [Principal], having never been asked. These two predicates are always bound, so an app with
no principal can still say exactly what it means (when IsAnonymous).
deny has its own rule rather than an exemption: an entity-level deny with no when/where is refused too — under deny-all it subtracts from nothing, and a conditioned grant beside it is more specific and overwrites it ([[security-entity-security#layers]]). A FIELD-scoped one (deny read Secret;) stays legal: a field's blanket is more specific than every entity rule, so it is the only way to say "nobody, ever" about one column. ⚠ A CREDENTIAL column is the one exception, and it is refused for a reason worth knowing: masking it from everyone masks it from your own sign-in too, so Security.VerifyPassword compares against null and a correct password is refused exactly like a wrong one. Condition that one on your auth policy — deny read PasswordHash when !IsAuthenticator;.
The predicates read as intent:
IsAuthenticatedgrants the read to anyone signed in, regardless of role — the common "members can see the directory, the public cannot" case, without inventing a role for it.IsAnonymousgrants it to the not-signed-in visitor — a genuinely public page or catalog. (It is the identity of the request, distinct from any role a user account might hold.)IsAuthenticated || IsAnonymousis the honest way to write "everyone": broad, but deliberate, and greppable — a reader sees that the openness was chosen, not forgotten.
Prefer the narrowest that is true. Reach for the || form only when a read really is public; reach for a role or a
where Owner == user filter when it is not.
user in a package's rule, in an app with no [Principal]#
An app with no [Principal] can still use a package whose own security { } names user — Osysharp.Blocks asks
who started a preview with CreatedBy == user. There, user is nobody: the same null principal an anonymous
caller is in an app that has one. So the package's rule answers exactly as it does for a signed-out visitor:
X == userandX != userhold for no row — not even a row whoseXis empty. Anallowbuilt on one grants nothing; adenybuilt on one applies.- A package rule that reads a property of the user (
user.Email) is refused, naming the fix: nobody has an email, so the package needs you to declare a[Principal].
Your OWN rule naming user is still a compile error in such an app — it has no one to mean, and the message says so
and offers the two ways out:
entity Note {
string Words;
[CreatedBy] Guid? CreatedBy;
security { allow read when IsAnonymous where CreatedBy == user; }
// ✗ 'user' is the signed-in caller, and this app declares no `[Principal]` entity — so nobody can ever sign in …
}IsTheApp — the app itself is acting#
Some code runs with nobody behind it: the platform starts it for the app. An [On] handler hears an event after the
request that raised it has ended; a workflow's body runs on a transition, a timer or a reminder; an [OnStart] hook
runs when the app starts. That code has the app's own authority and no user — so IsAnonymous is true there,
exactly as it is for a signed-out visitor. IsTheApp is what tells the two apart:
| who is asking | IsAuthenticated | IsAnonymous | IsTheApp |
|---|---|---|---|
| a signed-in person — a page, an API key, an integration's account, an MCP caller | true | false | false |
| a signed-out visitor | false | true | false |
your sign-in and sign-up functions ([AuthMethod]), while they run | false | true | false |
an [On] handler · a workflow's body (a transition, a timer, a clock, a reminder, a resume after a wait) · an [OnStart] hook | false | true | true |
a runas (Actor) { … } block inside any of those | true | false | false — the block is the actor's ([Actor] — a principal that code may run as) |
a test's body, and a [TestFixture] | as the test's [runas] says | false |
It is true for exactly the code that is already let past an [Authorize] — one fact, set by the platform when it
starts such a body, and never worked out from "nobody is signed in". Everything that arrives from outside is false.
Where it is written.
// In a function's body — the same check, wherever the function is called from.
int PlacesLeft(Session s) {
if (!IsTheApp && !IsAuthenticated && !VisitorsSeePlaces) { throw new NotAuthorized("Sign in to see how full it is."); }
return s.Capacity - Booking.Count(b => b.Session == s);
}
// On a function: only the app's own code reaches it. No request does, signed in or not.
[Authorize(IsTheApp)]
void CloseTheRegister(Session s) { … }
// In a policy, with whatever else admits a caller.
policy MaySeeCounts => IsTheApp || IsAuthenticated || VisitorsSeePlaces;IsAuthenticated and IsAnonymous may be named in a function's body the same way.
In an entity's security { } rule it grants nothing — and says so. A rule is asked of whoever is calling (or of
an actor, inside its block), and a caller is never the app; the app's own code is not judged by an allow at all — it
reads and writes on its own authority. So allow read when IsTheApp; is a readable way to write "no caller reads
this; the app's own code does", and allow read when IsTheApp || IsStaff; is "staff, and the app's own code".
| written | what happens |
|---|---|
allow <verb> when IsTheApp … | allowed; grants no caller anything |
deny <verb> when !IsTheApp — directly or through a policy | refused when compiled. It would hold for everybody, the app's own code under a package's deny included. Write deny <verb> except via <Function>; (deny … except via) and keep that function to the app with [Authorize(IsTheApp)] |
… where IsTheApp | refused, like the other two: it is not a property of a row |
Security.Authorize(person, IsTheApp) | refused: a question about a person, and a person is never the app. A policy that names IsTheApp answers no for any person asked about, whoever asks |
on a page — canSee:, if (Policy) in a component | always no: a page is opened by a person |
policy IsTheApp => … | refused — the name is reserved, like the other two |
[Principal] entity Member {
[Required, MaxLength(120), Unique] string Email;
security { allow read, create when IsAuthenticated || IsAnonymous; }
}
entity Reading {
[Required, MaxLength(40)] string Station;
int Level;
security { allow read, create when IsAuthenticated || IsAnonymous; }
}
/// What the gauge said, and who asked for it to be written down.
entity LogLine {
[Required, MaxLength(40)] string Station;
[Required, MaxLength(20)] string WrittenBy;
security { allow read, create when IsAuthenticated || IsAnonymous; }
}
[Event("Level read")]
public sealed class LevelRead { public Reading Reading; }
// Only the app's own code reaches this one.
[Authorize(IsTheApp)]
void WriteDown(string station, string by) { new LogLine { Station = station, WrittenBy = by }; }
string WhoAsks() { return IsTheApp ? "the app" : IsAnonymous ? "a visitor" : "a member"; }
[AllowAnonymous]
void Read(string station, int level) {
var r = new Reading { Station = station, Level = level };
Raise(new LevelRead { Reading = r });
}
[AllowAnonymous]
string Ask() { return WhoAsks(); }
[AllowAnonymous]
void WriteItMyself(string station) { WriteDown(station, WhoAsks()); }
// Heard after the commit that raised it, with the app's authority and nobody signed in.
[On]
void Log(LevelRead e) { WriteDown(e.Reading.Station, WhoAsks()); }[Test]
void a_visitor_asks_and_the_apps_handler_asks() {
Assert.Equal("a visitor", Ask());
Assert.Throws<NotAuthorized>(() => WriteItMyself("North pier"));
Read("North pier", 212);
UnitOfWork.Commit();
Background.Settle();
Assert.Equal("the app", LogLine.Single(l => l.Station == "North pier").WrittenBy);
}Examples#
A [Principal] with a credential column, a members-only directory read, and a genuinely public catalog:
[Role] enum AppRole { Authenticator }
[RoleGrant] entity RoleGrant { User User; [Required] AppRole Role; }
// The leash the auth flow is checked against — the credential is masked from everyone EXCEPT this.
policy IsAuthenticator => RoleGrant.Any(g => g.User == user && g.Role == AppRole.Authenticator);
[Principal] entity User {
string Email;
security {
allow read when IsAuthenticated; // signed-in users see the directory; the world does not
}
}
entity Announcement {
string Body;
security {
allow read when IsAuthenticated || IsAnonymous; // truly public — everyone, said out loud
}
}An app with no [Principal] says "anyone" the same way — inline or through a policy — because the two
predicates are constants there: every caller is anonymous. (when true is refused in every app: a when asks who
the caller is, and a constant asks nothing. Its refusal names this spelling.)
policy Anyone => IsAnonymous || IsAuthenticated; // true here: every caller is anonymous
policy SignedIn => IsAuthenticated; // false here: nobody ever signs in
entity Notice {
[MaxLength(200)] string Body;
security { allow read, create when Anyone; }
}
entity Draft {
[MaxLength(200)] string Body;
security { allow read, create when SignedIn; }
}
[AllowAnonymous]
void Post(string body) { new Notice { Body = body }; }
[AllowAnonymous]
void Sketch(string body) { new Draft { Body = body }; }[Test]
void anyone_may_post_and_nobody_is_signed_in() {
Post("Closed on Monday");
UnitOfWork.Commit();
Assert.Equal(1, Notice.Count());
Assert.Denied(() => { Sketch("not yet"); UnitOfWork.Commit(); });
}See also#
- security { } — the
security { }block these predicates live in - secure by default (deny-all) — the deny-all posture that makes an unmentioned grant a denial
- auth bootstrap (login, before anyone is signed in) — login/signup before a principal exists, run as a declared role
- [Actor] — a principal that code may run as — a block run as an actor is the actor's, not the app's
- [On] — hear an event, after the commit that raised it — an event handler, which runs as the app
- public pages (what a signed-out visitor can see and do) —
IsAnonymousin practice: what a signed-out visitor actually sees, and the half that is easy to forget