You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
With flow `runAs` now enforced (#1888, PR #2302), a SCHEDULE-triggered flow with
the default `runAs:'user'` has no trigger user: `resolveRunDataContext` returns
undefined, so the CRUD nodes pass no ObjectQL `options.context` and the security
middleware — which skips when there is no identity (it delegates auth to the auth
layer) — runs the operation UNSCOPED (effectively elevated). An author who left
`runAs` at the 'user' default expecting a restricted run silently gets an
unscoped one. That is fail-open, and exactly the kind of "security property that
silently does the opposite of what it implies" ADR-0049 prohibits.
This makes the boundary case #1888 deliberately left open an explicit product
decision. Denying outright (fail-closed) would break legitimate scheduled CRUD —
empirically 2 of 3 example scheduled CRUD flows relied on the default — and
silently elevating would hide the author's intent. So prevention happens where
the platform can tell intent apart (author/build time), and the runtime stays
non-breaking but is no longer silent:
- Author-time lint (@objectstack/cli, lintFlowPatterns): a new advisory rule
`flow-schedule-runas-unscoped` flags a schedule-triggered flow whose effective
`runAs` is user (explicit or unset) and which performs a data op, pointing the
author at `runAs:'system'`. Catches it at compile time before deploy (most
flows are AI-authored).
- Runtime warning (@objectstack/service-automation): the engine emits one clear
warning per run when a user-mode run resolves no trigger identity and the flow
touches data — the fail-open is audible, not silent. Behavior is otherwise
unchanged (scheduled CRUD is not broken). New helpers `runIsUnscopedUserMode`,
`flowTouchesData`, `DATA_NODE_TYPES` exported beside `resolveRunDataContext`;
the two runContext construction sites collapse into one `resolveRunContext`.
- Spec describe (@objectstack/spec): FlowSchema.runAs now states a scheduled run
has no user, so under `user` it runs unscoped — declare `system`.
First-party example flows that tripped the new lint are fixed to declare
`runAs:'system'` (stale_opportunity_sweep, app-todo task_reminder /
overdue_escalation) — they read/write across owners and were running unscoped.
Longer term, attributing scheduled runs to a dedicated service principal
(scopable + audit-attributable) is the right enforcement — M2 follow-up.
Proven by a service-automation unit test (warns once for a user-less user-mode
data run; silent for system / an identified user / a data-less flow) and a
dogfood gate (flow-runas-schedule.dogfood.test.ts) that drives user-less runs
through the real automation + security + data stack: a `runAs:'user'` run reads +
writes an owner-scoped note a member cannot — audibly — while `runAs:'system'` is
the explicit, warning-free equivalent.
Refs #1888, ADR-0049.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
0 commit comments