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

Reference / Testing

[runas(Name)] test attribute and principal selectors

principal Alice => User.Single(u => u.Name == "Alice"); [runas(Alice)]

A test runs deny-all as an anonymous principal, so to read or write real data it must act AS a seeded `[Principal]`. Declare a named selector with `principal Name => <query>;`, then put `[runas(Name)]` on a `[Test]` to run its whole body as that principal. The `runas(){}` block form stays for the rarer case of switching principals mid-test. `[runas]` binds to a fixture-seeded row and never creates one.

preview2 examples compiled by CItestingsecurityauthorization

Summary#

An app is deny-all by default, and a [Test] runs in a secured context as an initially-anonymous principal. So any test that reads or writes real data must act as some seeded [Principal] row. Two pieces make that declarative:

  • principal Name => <selector>; — a top-level declaration that names a selector resolving, at runtime, to a single [Principal] row (e.g. principal Alice => User.Single(u => u.Name == "Alice");). It mirrors a policy Name => …;, but the body is a single-entity query, not a boolean.
  • [runas(Name)] — an attribute on a [Test] that runs the whole body as that principal.

The runas(<expr>) { … } block form is still there for the case it is uniquely good at: two principals in one test (Alice creates a row; then as Bob, assert he cannot see it). Attribute = whole test; block = a sub-region; the inner block wins for its span.

Signature#

principal <Name> => <single-entity selector>;   // names a seeded [Principal] row

[Test(<Fixture>)]
[runas(<Name>)]                                  // the whole body runs as <Name>
void <TestName>() { … }

Description#

[runas(Name)] binds, never creates. The named selector must resolve to a row the fixture already seeded; a selector that matches nothing fails the test loudly (it is User.Single(…), not a silent fallback). Naming a principal that was never declared is a compile error, as is putting [runas] on a function that is not a [Test]. This is deliberate — if [runas] could conjure a principal, a denial test would be fake, quietly passing against a principal that production would never grant.

Running as a principal rebinds more than the row: the effective role list is resolved for that principal (so a role-gated allow read when … activates only for a principal actually granted the role), and role-dependent row filters re-evaluate against the acting user. Data staged during the test settles as that principal at the end of the run, so an owner-scoped write commits under the owner with no extra ceremony.

A declared principal name is also usable as a value: writing Alice where a value is expected resolves to that same seeded row (its selector), so new Doc { Owner = Alice } reads naturally — no var alice = User.Single(…) re-query. An ordinary local, parameter, or entity of the same name always shadows it (the principal name is a last-resort resolution, never an override).

Use the attribute for the common case — "this whole test runs as one principal" — and reach for the runas(){} block only when a single test genuinely needs to change principals partway through, or must assert outside the run-as region (for example, to observe what committed after the block settled).

A principal declaration is authored in a test context alongside the [TestFixture] that seeds its row; the fixture seeds unsecured (so it can create freely), and the selector gives that row a compile-time name the attribute carries.

Examples#

A single-principal read test — the whole body runs as Alice, who sees only her own owner-scoped rows:

[Principal]
entity User {
  [Required, MaxLength(60)] string Name;
  security { allow read where Id == user.Id; }   // you can read yourself. Nobody enumerates the user table.
}

entity Doc {
  [Required] User Owner;
  [MaxLength(200)] string Title;
  security {
    allow create when IsAuthenticated;
    allow read where Owner == user;
  }
}
principal Alice => User.Single(u => u.Name == "Alice");
principal Bob   => User.Single(u => u.Name == "Bob");

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

[Test(Seed)]
[runas(Alice)]
void Alice_sees_only_her_own_rows() {
  Assert.NotNull(Doc.FirstOrDefault(d => d.Title == "alice-doc"));
}

// Two principals in one test — the case the attribute alone cannot express. `[runas(Alice)]` is the body's
// default; the inner `runas(Bob) { }` block wins for its span.
[Test(Seed)]
[runas(Alice)]
void Alice_creates_Bob_cannot_see() {
  var secret = new Doc { Owner = Alice, Title = "secret" };   // `Alice` as a value — the seeded row
  runas(Bob) {
    Assert.Null(Doc.FirstOrDefault(d => d.Title == "secret"));
  }
}

Note that the User table above is locked — nothing may enumerate it, which is how you would really write it. The selector still finds Alice, because binding who a test acts as is scaffolding, not an app data read: it resolves the same way the fixture seeds, past the app's own rules. You never have to loosen the user table to test your security.

The second test is a real proof rather than an illustration: the row is created by one user and genuinely not selected for the other. The rule is inside the query, so there is nothing for Bob to be "hidden" from.

See also#

Related

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…

Running tests

Runs your app's tests against a throwaway copy of its database, reporting each test as it finishes. Your local source…