Osy#betaRun it on Osyrin: osyrin.com

Reference / Security

runas (actor, onBehalfOf: person) — an actor acting for a person

runas (<Actor>, onBehalfOf: <person>, scope: <row>) { … }

`runas (Actor, onBehalfOf: person) { … }` runs a block as a declared actor FOR a person: inside it nothing is read or written that the actor or the person could not reach alone. A count is over the rows both may read, a column either may not read is hidden, a write needs both to be allowed. `scope: row` names the piece of work the block is done for, and every change the block makes is recorded with the actor, the person and that work.

preview1 example compiled by CIsecurityactordelegationaudit

Summary#

An actor that works FOR somebody must not reach further than either of them alone. A sorting machine holding a dispatcher's role, started by a courier, would otherwise read every round in the depot for her.

onBehalfOf: says who the actor is working for, and the block is then held to both:

runas (SortingMachine, onBehalfOf: courier) {
  var mine = Parcel.Count();           // the parcels BOTH may read: hers
}

Signature#

runas (<Actor>, onBehalfOf: <person>) { … }                 // person: a row of your [Principal] type
runas (<Actor>, onBehalfOf: <person>, scope: <row>) { … }   // scope: the piece of work — a task, a ticket, an invoice
runas (<Actor>, scope: <row>) { … }                         // an actor acting for nobody, naming its work

<Actor> is a declared actor — a class marked [Actor], or Actor.From(row) ([Actor] — a principal that code may run as). The two named arguments may come in either order.

Description#

What may the block read?#

Only what both may read. The actor's read rules and the person's are both applied to every query, in the database: a list holds the rows both may read, and so does every count, sum and page of it. A column either of them may not read is hidden ([[security-entity-security#field-mask]]).

the questionthe actor alonefor the person
Parcel.Count()every parcel its roles admitonly those that are also the person's to read
a column the person may not readits valuehidden
a row only the PERSON may readnot readablestill not readable — the actor gains nothing from the person

A report compiled by Explore is read the same way: it counts and groups only what both may read. Two reads cannot carry the person's rule beside the actor's yet, and are refused inside the block rather than answered as the actor alone: a semantic search (Memory.Search), and a read of an entity that lives in an external data source (data held somewhere else (app.DataSources)).

What may the block write?#

Only what both may write. A create, an update of each column and a delete are refused unless the actor's rules and the person's both allow them — row by row, and for a statement over many rows (Update) only the rows both may change are changed. A refusal is the same denial either of them would meet alone.

An action run by name inside the block ([Action] — what an app offers to be done, by name) is held to the same two.

What does "nobody" mean?#

onBehalfOf: with no person — a null, or an account that no longer exists — is nobody's rights: what a signed-out visitor may do. It never means "no limit". To run as the actor alone, leave onBehalfOf: out.

Can code inside the block act for somebody else?#

No. A block entered for a person keeps that person for everything it calls: a function inside it that runs as an actor without naming anybody is still held to them, and one that names a different person is refused.

What does scope: record?#

Which piece of work the block is done for. Every change an actor makes is written to the app's change trail (audit read access (app.Audit)) whatever else the app audits, and each entry says:

who actedthe actor's account, and its declared name
for whomthe person named by onBehalfOf: — empty when it acted for nobody
for whatthe row named by scope:, with its type

A block that names no scope: still gets an identity of its own, so every change of one block can be found together. That is what lets "why does this row say that?" be answered from the row: its last change leads to the actor, the person and the piece of work.

Examples#

using Osysharp.Observability;

[Role] enum DepotRole { Dispatcher, Courier }

[Principal] entity Account {
  [Unique, MaxLength(200)] string Email;
  security { allow read when IsAuthenticated; }
}

[RoleGrant] entity RoleGrant {
  [Required] Account Holder;
  [Required] DepotRole Level;
  security { allow read when IsAuthenticated; }
}

policy IsDispatcher => RoleGrant.Any(g => g.Holder == user && g.Level == DepotRole.Dispatcher);
policy IsCourier    => RoleGrant.Any(g => g.Holder == user && g.Level == DepotRole.Courier);

[Actor(Roles = [DepotRole.Dispatcher])]
public class SortingMachine {
  public static Account Principal => new Account { Email = "[email protected]" };
}

entity Parcel {
  [Required, MaxLength(40)] string Code;
  [Required] Account Courier;
  [MaxLength(40)] string Bay = "";
  [MaxLength(200)] string Note = "";
  security {
    allow read, update when IsDispatcher;                        // a dispatcher sees and moves every parcel
    allow read when IsCourier where Courier == user;             // a courier sees her own
    allow update Note when IsCourier where Courier == user;      // …and may leave a note on it — not move it
  }
}

int MachineCounts()               { runas (SortingMachine) { return Parcel.Count(); } }
int CountFor(Account courier)     { runas (SortingMachine, onBehalfOf: courier) { return Parcel.Count(); } }
int CountForNobody()              { runas (SortingMachine, onBehalfOf: null) { return Parcel.Count(); } }

void NoteFor(Account courier, string code, string note) {
  runas (SortingMachine, onBehalfOf: courier) {
    var parcel = Parcel.Single(p => p.Code == code);
    parcel.Note = note;
  }
}

void MoveFor(Account courier, string code, string bay) {
  runas (SortingMachine, onBehalfOf: courier) {
    var parcel = Parcel.Single(p => p.Code == code);
    parcel.Bay = bay;
  }
}

[TestFixture]
void Depot() {
  var dana = new Account { Email = "[email protected]" };
  var cleo = new Account { Email = "[email protected]" };
  var omar = new Account { Email = "[email protected]" };
  new RoleGrant { Holder = dana, Level = DepotRole.Dispatcher };
  new RoleGrant { Holder = cleo, Level = DepotRole.Courier };
  new RoleGrant { Holder = omar, Level = DepotRole.Courier };
  new Parcel { Code = "C-1", Courier = cleo };
  new Parcel { Code = "C-2", Courier = cleo };
  new Parcel { Code = "O-1", Courier = omar };
}

principal Dana => Account.Single(a => a.Email == "[email protected]");

[Test(Depot)]
[runas(Dana)]
void for_a_courier_the_machine_reaches_only_what_she_could() {
  var cleo = Account.Single(a => a.Email == "[email protected]");

  Assert.Equal(3, MachineCounts());        // the machine alone: every parcel
  Assert.Equal(2, CountFor(cleo));         // for Cleo: her two
  Assert.Equal(0, CountForNobody());       // for nobody: nothing

  NoteFor(cleo, "C-1", "leave at the side door");                       // both may write the note
  Assert.Denied(() => MoveFor(cleo, "C-1", "B4"));                     // the machine may move it; Cleo may not

  var parcel = Parcel.Single(p => p.Code == "C-1");
  Assert.Equal("leave at the side door", parcel.Note);
  Assert.Equal("", parcel.Bay);
}

See also#

Related

[Actor] — a principal that code may run as

A class marked `[Actor]` declares a principal that code may run as: a robot, a nightly job, an integration, a digital…

acting for another principal

A shared terminal, a kiosk, a scanner or a back-office integration is a **user of your app**, not a mode of it. It…

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…

when is who, where is which rows

A rule's `when` says WHO it is for and its `where` says WHICH ROWS. A rule whose `when` does not hold is never asked…

Security.MayRead — may THIS person read this row, under their own rules

`Security.MayRead(person, row)` answers whether a given person may read a given row under THEIR OWN security — the…

[Action] — what an app offers to be done, by name

Mark a function `[Action]` and the app offers it to be done BY NAME — by a rule, a macro, a tool — with everything a…

audit read access (app.Audit)

`app.Audit` configures the app's audit trails — WHO may read each one, and whether it is recorded at all. An app that…

Changes

The lightweight, ungated answer to "who changed this row, and when": field NAMES only, never values, newest first…

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…