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

Reference / Security

navigation in security predicates (any depth, either side)

allow update where Folder.Workspace.Region.Head == user; // and user.HomeRegion.Company on the principal side

A `where` row filter may follow relations as far as the model goes — `Folder.Workspace.Region.Head == user` is a four-hop rule, and the principal side navigates too (`user.HomeRegion.Company`). There is no depth limit and no shape a shallow rule is allowed that a deep one is not: criteria on intermediate rows, several chains in one predicate, a chain inside a collection hop, negation and `== null` mid-chain all behave the same at any length. Read and write always agree, so a rule that grants you the read grants you the write.

stable1 example compiled by CIsecurityauthorizationquery

Summary#

A where row filter is an ordinary query predicate, so it may follow relations to reach the fact that decides access. The chain can be as long as your model:

entity Note {
  [Required] Folder Folder;
  security {
    // four hops: this note's folder → its workspace → that workspace's region → the region's head.
    allow read, update where Folder.Workspace.Region.Head == user;
  }
}

Nothing about depth changes the rules. A four-hop rule may carry criteria on the rows it passes through, combine several chains, sit inside a collection hop, or be negated — the same as a one-hop rule.

Signature#

allow <verbs> where <chain> <op> <value>;         // <chain> is Rel.Rel….Property, any length
allow <verbs> where user.<chain> == <value>;      // the principal navigates too

Description#

Both sides navigate#

The row under evaluation is reached with a bare property name, and the caller with user. Either may navigate:

// the row's chain compared to the principal's chain
allow read where Folder.Workspace.Region.Company == user.HomeRegion.Company;

A broken chain denies, it does not hide the row#

If any relation along the chain is null, the comparison is not true — so that row satisfies no allow built on the chain. It is not removed from consideration, which matters the moment the chain is one arm of something:

// a workspace with no region is admitted by the LEFT arm; one with a region is decided by the right.
allow read where Folder.Workspace.Region == null || Folder.Workspace.Region.Head == user;

Both arms get their say for every row. A rule cannot accidentally exclude rows by mentioning a relation they lack.

Read and write give the same answer#

Reads are answered by the database and writes are checked in memory at commit, but they evaluate the same predicate and must return the same verdict — at every depth, on both sides of the comparison. You never have to know which engine is deciding, and you never have to shorten a rule to keep them in agreement.

Do not denormalize a relation just to keep a rule short. Copying a parent's key onto a child so the rule can say Organization instead of Application.Organization buys nothing here, and costs a column that can drift out of step with the relation it mirrors.

Does a hop resolve against rows the caller cannot read?#

Following a relation resolves against the data as it is, not against the rows the caller may read. Deciding authorization from what the caller can already see would be circular — you cannot read the membership row that grants you the read. Only the final verdict reaches the caller; no row visited along the way is disclosed.

Examples#

[Principal] entity User {
  string Email;
  security { allow read when IsAuthenticated; }
}

entity Region {
  string Name;
  [Required] User Head;
  security { allow read when IsAuthenticated; }
}

entity Workspace {
  string Name;
  Region Region;                                   // nullable — a workspace need not sit in a region
  [Required] bool Active;
  security { allow read when IsAuthenticated; }
}

enum FolderKind { Normal, Archived }

entity Folder {
  string Name;
  [Required] Workspace Workspace;
  [Required] FolderKind Kind;
  security { allow read when IsAuthenticated; }
}

entity Note {
  string Title;
  [Required] Folder Folder;
  security {
    // four hops to the deciding fact, plus criteria on TWO of the rows along the way.
    allow read, update where Folder.Workspace.Region.Head == user
                          && Folder.Workspace.Active
                          && Folder.Kind != FolderKind.Archived;
  }
}
// a chain inside a collection hop, correlated to a chain on the row
allow read where RegionMember.Any(m => m.Region == Folder.Workspace.Region && m.User == user);
// the shape a denormalized FK used to exist for — now written as it reads
allow update, delete where OrganizationMember.Any(o => o.Organization == Application.Organization
                                                    && o.User == user
                                                    && o.Role != OrgRole.Member);

See also#

Related

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…

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…

capability rows that belong to a user

Some capability tables hold rows that belong to one signed-in user — a chat conversation is yours, not the app's. Those…

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…