Summary#
A workflow body writes as the system, not as the caller. The entity's security {} block gates what a PERSON
may do directly; it does not gate what a transition does once an event has been legitimately raised.
Signature#
state Open {
on Close {
this.Item.Note = "closed by the workflow"; // lands even if NO rule grants `update` to anyone
goto Closed;
}
}Description#
Where the authorization actually happens#
Two different questions, and only the first one is about the person:
- May they raise this event?
[Authorize(principal => …)]on the event, and a slot'sCandidatesfor who may hold the work. See [Authorize] (event). - May the body write this row? Not asked. The engine opens a system data context for the run, and a system context is defined as one that opts out of data security.
So a transition is trusted code, in the same sense a server function's own bookkeeping is. Put the gate on the EVENT, where the person is.
Why it is built that way#
A run outlives the request that started it. A milestone that expires at 2am, a deadline, a retry after a service
came back — these resume with no acting principal at all, and a rule written allow update where Owner == user
has no user to compare against. If a body were gated by the caller's rules, the same transition would succeed or
fail depending on who happened to trigger it, and a timer-driven one could never succeed.
What this means for your model#
- Do not rely on
security {}to stop a workflow. If a field must never change once a run owns it, that is a guard in the body or an[Immutable]on the member — not a missingallow update. - A read from a page is still gated. This is about the BODY. What the person then sees on screen goes through the ordinary read rules, unchanged.
[Authorize]is the real perimeter. An event anybody may raise is an event anybody may cause every write in its transition to happen.
Examples#
A whole app, compiled: Ticket grants read and create and no update to anyone, and the transition writes
Note anyway.
enum TicketState { Open, Closed }
entity Ticket {
[MaxLength(80)] string Title;
TicketState Status = TicketState.Open;
[MaxLength(200)] string Note = "";
security { allow read, create when IsAnonymous || IsAuthenticated; } // no `update`, to anyone
}
workflow TicketFlow {
Tracks = Ticket.Status;
Initial = Open;
event Close();
state Open {
on Close {
this.Item.Note = "closed by the workflow"; // lands: a body is not gated by the block above
goto Closed;
}
}
state Closed { }
}
void CloseIt(Ticket t) { TicketFlow.RaiseClose(t); }Gate the EVENT, which is where the person is:
[Authorize(principal => principal == this.Item.Owner)]
event Close();See also#
- [Authorize] (event) — who may raise an event
- subscribe — who may hold a work slot
- security { } — what the
security {}block gates