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

Reference / Security

security { }

security { allow read, create, update, delete when <policy>; allow read where <row predicate>; deny read <Field> when <policy>; // a field mask }

The rules that decide who may read and write an entity's rows. A where clause filters by the row (the owner sees their own); a when clause gates by the principal (staff see everything). Rules are compiled into every query.

stable8 examples compiled by CIsecurityentity

Summary#

A security { } block on an entity decides who may read and write its rows. It is not a check you call — it is compiled into every query and every write, so there is no code path that can go around it. A row you may not see is not fetched and hidden; it is never in the result at all.

Two kinds of rule, and the distinction is the whole model:

  • where filters by the rowthe owner sees their own rows.
  • when gates by the principalstaff see everything.

Signature#

entity <E> {
  // Everything is denied already. You do NOT write `default deny` — you only write what is ALLOWED.
  security {
    allow <verbs> when  <policy>;        // …to principals satisfying this policy
    allow <verbs> where <row predicate>; // …only the rows matching this
    deny  read <Field> when <policy>;    // a FIELD mask — the row comes back, the field does not
  }
}

// <verbs> — one, or several, comma-separated:
//   read · create · update · delete

policy <Name> => <predicate over `user`>;   // a named, reusable principal test

Description#

You only ever write what is ALLOWED#

Everything is denied before you say a word, so a security { } block is a list of grants. There is nothing to turn off first, and you never write default deny — that is the state you are already in.

The clearest demonstration is the locked entity: to make one that nobody may read or create, say nothing at all.

entity Secret {
  [MaxLength(100)] string Code;
  // No security block. Nobody reads it, nobody creates it. Locked, by saying nothing.
}

entity Article {
  [MaxLength(200)] string Title;
  // Public — and note you must say WHO: a bare `allow read;` with no `when`/`where` is a compile error.
  security { allow read when IsAuthenticated || IsAnonymous; }
}

That is the whole posture in one screen: silence denies, and only a grant opens. The failure mode of forgetting a rule is "nobody can do it" — reported within the minute — rather than "everybody can", which nobody reports until it is somebody else's headline.

where — the owner sees their own#

A where clause is a predicate over the row, and user is the acting principal. It becomes part of the query:

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

entity Doc {
  User Owner;
  [MaxLength(200)] string Title;
  security {
    allow read where Owner == user;      // I see mine; you see yours
  }
}

Under this rule Doc.Count() returns a different number for different users, and both are correct. That is the point: the filter is part of the query, not a mask applied afterwards.

Read and write are separate#

They usually differ — a team can all read a memo, but only its author may change it:

entity Memo {
  User Owner;
  [MaxLength(200)] string Note;
  security {
    allow read when IsAuthenticated;     // any signed-in colleague can see it
    allow update where Owner == user;    // only the author can change it
  }
}

Which verbs can I grant?#

A grant names what may be done: read · create · update · delete. Several may share one rule, comma-separated — and they routinely differ, which is the point of naming them separately:

entity Organization {
  [Required, MaxLength(200)] string Name;
  [Required, MaxLength(80)] string Slug;
  security {
    allow read when IsStaff;                     // staff can see every org
    allow create, update, delete when IsAdmin;   // only an admin may change the SET of them
  }
}

A rule with no verb list is not a thing you can write: you always say what is being allowed. "Access" is not a permission — reading is, and deleting is, and they are not the same decision.

A where on create — checking the row you are writing#

A where filters existing rows for read/update/delete. On create there is no existing row — so the same where is checked against the row you are writing: a create is refused unless the new row satisfies the predicate. (This is a WITH-CHECK, the INSERT half of row-level security.) One predicate governs all four verbs — you may only bring into existence a row you would be allowed to own:

entity Ledger {
  User Owner;
  [MaxLength(200)] string Note;
  security {
    // The SAME `where` governs create: on the commit path it validates the ROW being written.
    allow read, create, update, delete where Owner == user;
  }
}

Here a caller may create a Ledger owned by themselves (the row satisfies Owner == user), but a create whose Owner is someone else is denied at commit and rolled back — not saved-then-hidden.

This is the only way to scope a create by a column the app sets itself (an Owner ref, an Organization an app supplies). It is different from the auto-stamped ownership idiom — allow create when IsAuthenticated + allow read, update, delete where CreatedBy == user.Id — which is safe only because CreatedBy is stamped by the platform and cannot be forged. When the scoping column is one the caller writes, a role-only when (which never sees the row) would let them write it for any tenant; the create where is what refuses that.

deny read <Field> — the field mask#

Sometimes the ROW is fine and one FIELD is not. A password hash is the canonical case: the login flow must read it to verify a credential, and nobody else ever should — not an admin, not the user themselves, not a support tool, not an export.

deny read <Field> when <policy> masks a single field. The row still comes back; the field does not:

// `IsAuthenticator` is the ephemeral principal the login flow runs as (see [auth bootstrap (login, before anyone is signed in)](/reference/security/auth-bootstrap/)).
policy IsAuthenticator => RoleGrant.Any(g => g.Grantee == user && g.Level == AppRole.Authenticator);

entity Credential {
  [Required] User Owner;
  /// Salted hash of the password — never the plaintext.
  [MaxLength(200)] string PasswordHash;
  security {
    allow read when IsStaff;                        // staff read the directory …
    deny read PasswordHash when !IsAuthenticator;   // … but the hash, only the auth flow, ever
    allow update where Owner == user;               // a user maintains their own credential
  }
}

This is worth reaching for more often than people do. A field mask is a narrow, auditable statement — this column, to these people, never — and it survives every future query, endpoint, export and tool automatically, because it is enforced where the data is read rather than wherever someone remembered to be careful.

when — gate by who is asking#

A where asks "is this row yours?". A when asks "are you the kind of person who may do this at all?". Name that test with a policy and reuse it:

enum AppRole { Staff, Admin, Authenticator }

entity RoleGrant {
  User Grantee;
  [Required] AppRole Level;
}

policy IsStaff => RoleGrant.Any(g => g.Grantee == user && g.Level == AppRole.Staff);
policy IsAdmin => RoleGrant.Any(g => g.Grantee == user && g.Level == AppRole.Admin);

entity Report {
  [MaxLength(200)] string Title;
  security {
    allow read when IsStaff;             // staff read every report; nobody else reads any
  }
}

A policy is written once and referenced everywhere, so the definition of "staff" lives in one place. When it changes, it changes everywhere — which is the only way it stays true.

Test it, or you have not written it#

A security rule you have not tested is a rule you believe you wrote. Prove it, with runas and Assert.Denied:

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

[Test(Seed)]
void Bob_cannot_see_Alices_doc() {
  var bob = User.Single(u => u.Name == "Bob");
  runas(bob) {
    Assert.Equal(0, Doc.Count());       // not "hidden" — for Bob, the row does not exist
  }
}

Write one of these for every rule that matters. It is what stops a refactor six months from now from quietly opening a door nobody notices is open.

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…

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…

Assert

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

entity

Declares a persisted type — a table of rows the app stores, queries and secures. Every entity gets an Id and audit…