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

Reference / Testing

runas

runas (<principal>) { … }

Runs a block as a given principal, so security rules apply exactly as they would for that user. It is how you test that a rule denies the people it should — the only way to prove security from inside the app. It is TEST-ONLY: a `runas` outside a `[Test]` is a compile error, because app code may not step outside the security it declared.

stable2 examples compiled by CItestingsecurity

Summary#

runas (principal) { … } runs the block as that user. Every security rule inside it evaluates against them: row filters bind to them, role rules activate for their roles, and what they may not do is refused.

It exists so you can test the thing that is hardest to test and worst to get wrong — that your rules deny the people they should.

It works only inside a [Test]. Writing runas anywhere else is a compile error, and that is deliberate — see Why it is test-only below.

Signature#

runas (<principalRow>) {
  … // everything in here acts as that user
}

runas (AuthBootstrap) {
  … // everything in here acts as the platform's ephemeral auth principal
}

Description#

Seeing what a user sees#

A row filter like allow read where Owner == user means different rows exist for different people. runas is how you observe that:

[Principal] entity User {
  [Required] string Name;
}

entity Doc {
  User Owner;
  [MaxLength(200)] string Title;
  security { allow read where Owner == user; }
}

[TestFixture]
void Seed() {
  var alice = new User { Name = "Alice" };
  var bob = new User { Name = "Bob" };
  var aliceDoc = new Doc { Owner = alice, Title = "alice-doc" };
  var bobDoc = new Doc { Owner = bob, Title = "bob-doc" };
}

[Test(Seed)]
void A_user_sees_only_their_own_docs() {
  var alice = User.Single(u => u.Name == "Alice");

  runas(alice) {
    Assert.Equal(1, Doc.Count());                          // Bob's row is not merely hidden — it does not exist for her
    Assert.Equal("alice-doc", Doc.Single(d => true).Title);
  }
}

Read that first assertion carefully. Under runas(alice), Doc.Count() is 1. The filter is not a mask applied after the fact; it is part of the query. Bob's row is not in the result set to be filtered out — it was never in it.

Proving the denial#

Pair runas with Assert.Denied to claim that someone is refused:

[Test(Seed)]
void Bob_cannot_read_Alices_doc() {
  var bob = User.Single(u => u.Name == "Bob");
  var alice = User.Single(u => u.Name == "Alice");

  runas(bob) {
    Assert.Equal(0, Doc.Count(d => d.Owner == alice));   // Alice's doc is not his to see
  }
}

Outside the block — the fixture is unrestricted, the test body is NOBODY#

These are two different things, and reading them as one is a half-hour lost:

  • A [TestFixture] runs unsecured, so it can seed rows across every entity without fighting the rules it is about to test.
  • A [Test] body outside a runas block runs as an anonymous caller — secured, with no principal. Under deny-by-default a read there returns nothing, which is why a Assert.NotNull on it fails rather than passing on unrestricted access.

Until something signs you in. Driving the app's own sign-in — Ui.SignInAs(Alice), or filling and submitting its login form — makes the test body that person for the rest of the test, exactly as it makes the browser that person. Reads after it are Alice's reads and need no runas wrapper; reads written above it are still nobody's. Ui.SignOut() takes it away again.

That asymmetry is deliberate, and it is worth stating plainly: security is only tested inside runas, or as somebody you signed in. A test that does neither has tested what nobody can do.

Reading a credential, as the auth flow#

runas (AuthBootstrap) is the one form whose argument is not a row. It becomes the same user-less ephemeral principal the platform arms an [AuthMethod] with: no current user, bearing the [Role] your app.AuthBootstrap declares, with exactly that role's ordinary security {} grants and nothing more.

It exists because a properly-masked credential has exactly one legitimate reader, and until this form a test could not be it:

security {
  deny read PasswordHash when !IsAuthenticator;   // the shape every app is told to write
  deny read ResetToken   when !IsAuthenticator;
}

That mask makes the field unreadable to every principal a test can name — which is correct, and which left a test needing to observe what the auth flow observes with nowhere to stand. The practical result was worse than the gap: an app whose reset flow had to be tested simply left the token unmasked, and an unmasked credential on the [Principal] rides to the browser on the Session.CurrentUser payload.

// what the email would have carried — read as the only thing allowed to see it
string token = "";
runas (AuthBootstrap) { token = User.Single(u => u.Email == "ada@example.com").ResetToken; }

It grants no new authority. It is the arming the platform already performs for Login, Signup and PasswordReset, reachable from a test — so what it can read is what your own auth flow can read, decided entirely by the grants you wrote. It stays test-only like every other runas, and it fails loudly rather than quietly running anonymously if the app declares no app.AuthBootstrap — because a block that reads a masked field while bound to nobody reads null, and an assertion over that would pass for the wrong reason.

Why it is test-only#

runas rebinds the acting user and that user's roles. Inside the block, every rule you wrote evaluates for somebody else. That is exactly what a security test needs and exactly what application code must never be able to do: it would let any code become any user it could merely look up, and on the ordinary shape where signed-in users can read the user directory, that is everybody.

The platform's promise is that you decide security once, on the entity, and it then holds everywhere without you checking — you never have to ask whether a particular read, function or screen honours it. That promise only survives while nothing in the language can step around it. So the compiler refuses runas outside a [Test]:

`runas(...)` is a TEST-ONLY construct and this is not a [Test] function. It rebinds the acting principal and that
principal's roles, which bypasses the security you declared on your entities — so app code may not speak it.

If you need authority before anyone is signed in — a login, a signup, an OAuth callback, which must read or write user rows with no user yet — that is auth bootstrap (login, before anyone is signed in), not this. You declare a role and a policy that leashes it, mark the entry points [[security-auth-method|[AuthMethod]]], and the platform runs them as an ephemeral principal bearing that role. The elevation is declared in one place a reviewer can find, bounded by a predicate, and still not something app code can grant itself.

See also#

Related

Assert

The assertions a test makes. Beyond the usual equality and null checks there are comparisons (Greater, Less, InRange)…

[Test] / [TestFixture]

A test is an ordinary function marked [Test]. It runs against a throwaway clone of the app, so it may create rows and…

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…

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…

auth bootstrap (login, before anyone is signed in)

Under deny-all, login faces a paradox: it must read a user row *before* anyone is authenticated. `app.AuthBootstrap`…