docs(security): script authoring and deploy are Administrator OR Designer (lmxopcua#779)
v2-ci / unit-tests (push) Has been cancelled
v2-ci / unit-tests (push) Has been cancelled
The docs said script authoring was Admin-gated (VirtualTags.md "Known
resource limits") and that a Designer "cannot publish" (security.md roles
table). The code admits Designer to both: Scripts, ScriptEdit, EquipmentPage
(virtual tags, scripted alarms), /api/script-analysis/* and Deployments all
carry AdminUiPolicies.ConfigEditor = RequireRole("Administrator",
"Designer"). That is a recorded decision, not drift: AdminUI-001 (98b27fc1)
loosened the analysis backend from FleetAdmin to Administrator,Designer to
match the page gate, and R2-05 converged the same pages on ConfigEditor
"with the same semantics". So the docs follow the code:
- VirtualTags.md: the primary control is ConfigEditor (Administrator or
Designer), which also gates deploy.
- security.md roles table: Designer = the whole ConfigEditor surface incl.
scripts and deploy; Administrator = Designer + the FleetAdmin-only surface
(role grants, API keys, certificate actions, manual failover) — the old
"publish, cluster/node CRUD" were ConfigEditor all along.
- 2026-06-09 Monaco plan: its FleetAdmin gate is marked superseded.
Claude-Session: https://claude.ai/code/session_01GT67AEHd2Acv1YPwGfXEoV
This commit is contained in:
+1
-1
@@ -37,7 +37,7 @@ User scripts are compiled via `Microsoft.CodeAnalysis.CSharp` (regular compiler,
|
||||
|
||||
#### Known resource limits (accepted trade-offs)
|
||||
|
||||
The sandbox cannot pre-empt a script. **Memory:** each run's own-thread allocation is sampled (`GC.GetAllocatedBytesForCurrentThread`) and a run over `Scripting:MaxAllocBytesPerRun` (default 64 MB) quarantines the script — but the sample is taken after the run returns, so a script that allocates fast enough (`new byte[int.MaxValue]` in a loop) can still OOM the process first. **CPU:** a tight loop keeps its pool thread after the timeout fires; `ScriptRunGuard` bounds that to one orphan per script by refusing further runs while it spins (Bad `"previous evaluation still running"`) and quarantines after three consecutive timeouts (a refusal is not a strike) — or immediately on a refusal behind an orphan that has run longer than `Scripting:StuckOrphanQuarantineFactor` (default 10) × the timeout, which is what quarantines a never-returning `while(true)`. Both are mitigations; pre-emptive CPU/memory limits need an out-of-process runner (→ #750). **Stack:** unbounded recursion used to terminate the whole server — a `StackOverflowException` cannot be caught, and lmxopcua#779 reproduced it out of process (local function, mutual recursion, self-referencing `Func<>`, anonymous self-application, recursion through LINQ: the child process died with "Stack overflow." / SIGABRT). `ScriptEvaluator.Compile` now rewrites the script so the body and every lambda, anonymous method and local function starts with `ScriptStackGuard.Check()` (`RuntimeHelpers.TryEnsureSufficientExecutionStack`), so the recursion fails the script with `ScriptStackExhaustedException` — Bad quality and a script-log entry like any throwing script — while there is still stack left. A runtime check rather than an analyzer rule because recursion is not decidable from syntax (anonymous self-application never names itself); bounded recursion keeps working. Limits: the depth reached depends on the evaluating thread's stack size; a recursion that never re-enters script code is not seen (the sandbox exposes no such API); a script can catch the exception, which only unwinds it. Pinned by `ScriptRecursionContainmentTests` (one child process per shape) and `ScriptStackGuardInjectionTests`. Script authoring is gated behind the Admin permission as the primary control. See `docs/ScriptEditor.md` §"Runtime safety".
|
||||
The sandbox cannot pre-empt a script. **Memory:** each run's own-thread allocation is sampled (`GC.GetAllocatedBytesForCurrentThread`) and a run over `Scripting:MaxAllocBytesPerRun` (default 64 MB) quarantines the script — but the sample is taken after the run returns, so a script that allocates fast enough (`new byte[int.MaxValue]` in a loop) can still OOM the process first. **CPU:** a tight loop keeps its pool thread after the timeout fires; `ScriptRunGuard` bounds that to one orphan per script by refusing further runs while it spins (Bad `"previous evaluation still running"`) and quarantines after three consecutive timeouts (a refusal is not a strike) — or immediately on a refusal behind an orphan that has run longer than `Scripting:StuckOrphanQuarantineFactor` (default 10) × the timeout, which is what quarantines a never-returning `while(true)`. Both are mitigations; pre-emptive CPU/memory limits need an out-of-process runner (→ #750). **Stack:** unbounded recursion used to terminate the whole server — a `StackOverflowException` cannot be caught, and lmxopcua#779 reproduced it out of process (local function, mutual recursion, self-referencing `Func<>`, anonymous self-application, recursion through LINQ: the child process died with "Stack overflow." / SIGABRT). `ScriptEvaluator.Compile` now rewrites the script so the body and every lambda, anonymous method and local function starts with `ScriptStackGuard.Check()` (`RuntimeHelpers.TryEnsureSufficientExecutionStack`), so the recursion fails the script with `ScriptStackExhaustedException` — Bad quality and a script-log entry like any throwing script — while there is still stack left. A runtime check rather than an analyzer rule because recursion is not decidable from syntax (anonymous self-application never names itself); bounded recursion keeps working. Limits: the depth reached depends on the evaluating thread's stack size; a recursion that never re-enters script code is not seen (the sandbox exposes no such API); a script can catch the exception, which only unwinds it. Pinned by `ScriptRecursionContainmentTests` (one child process per shape) and `ScriptStackGuardInjectionTests`. Script authoring is gated by the AdminUI `ConfigEditor` policy — **`Administrator` or `Designer`**, not Administrator alone — and that same policy gates deploy, so a Designer can author a script and put it into production; that gate is the primary control (AdminUI-001 deliberately admitted Designers to the script pages and `/api/script-analysis/*`; lmxopcua#779 corrected this sentence, which used to say Admin-only). See `docs/ScriptEditor.md` §"Runtime safety".
|
||||
|
||||
`System.Threading.Tasks` is denied (Core.Scripting-003), which prevents `Task.Run` / `Parallel` fan-out that would spawn background work outliving the timeout.
|
||||
|
||||
|
||||
@@ -30,6 +30,8 @@
|
||||
|
||||
**Authorization:** New endpoints use `.RequireAuthorization("FleetAdmin")` (policy defined in `src/Server/ZB.MOM.WW.OtOpcUa.Security/ServiceCollectionExtensions.cs:113` as `RequireRole("Administrator")` — script design is an admin action, same tier as deploy).
|
||||
|
||||
> **Superseded (AdminUI-001, `98b27fc1`; recorded again in lmxopcua#779):** the shipped gate is the `ConfigEditor` policy — `Administrator` **or `Designer`** — on the script pages, `/api/script-analysis/*` and `/deployments` alike. Designer was being 403'd by the analysis backend while the page admitted it, so the backend was loosened to match the page, not the reverse.
|
||||
|
||||
**Key fidelity notes (read once):**
|
||||
- The analysis return type is `object` (a `Script` is shared across virtual tags of differing `DataType`; per-tag return coercion stays a runtime concern).
|
||||
- The wrapper preamble (usings + `Run` signature + `var ctx = globals.ctx;`) precedes a `#line 1` directive, then the raw user source (column-aligned, not re-indented). Therefore: **diagnostics** — `tree.GetLineSpan(span)` / `d.Location.GetLineSpan()` return *mapped* (user-source) line numbers automatically; **completions/hover/signature** — use the *physical* offset `prefixLength + offsetWithinUserSource`.
|
||||
|
||||
+2
-2
@@ -1016,8 +1016,8 @@ The `AdminRole` enum (`src/Core/ZB.MOM.WW.OtOpcUa.Configuration/Enums/AdminRole.
|
||||
| Role | Capabilities |
|
||||
|---|---|
|
||||
| `Viewer` | Read-only access to drafts, generations, audit log, fleet status. (Was `ConfigViewer`.) |
|
||||
| `Designer` | Viewer plus draft authoring (UNS, equipment, tags, ACLs, driver instances, reservations, CSV imports). Cannot publish. (Was `ConfigEditor`.) |
|
||||
| `Administrator` | Designer plus publish, cluster/node CRUD, credential management, role-grant management. Satisfies both the `FleetAdmin` and `DriverOperator` authorization policies. (Was `FleetAdmin`.) |
|
||||
| `Designer` | Viewer plus the whole `ConfigEditor` surface: authoring (UNS, equipment, tags, ACLs, driver instances, reservations, CSV imports, **scripts** — virtual tags, Calculation tags, scripted-alarm predicates) **and deploy** (`/deployments`). A Designer can therefore put operator-authored C# into production; scripts run in-process on driver nodes (see [VirtualTags.md](VirtualTags.md) §"Known resource limits"). (Was `ConfigEditor`.) |
|
||||
| `Administrator` | Designer plus the `FleetAdmin`-only surface: role-grant management, API keys, certificate trust/untrust/delete and manual redundancy failover. Satisfies both the `FleetAdmin` and `DriverOperator` authorization policies. (Was `FleetAdmin`.) |
|
||||
|
||||
`DriverOperator` is an **authorization policy name** (kept stable), not an `AdminRole` member. It gates **Reconnect** / **Restart** commands against live driver instances from the Admin UI `DriverStatusPanel` and requires the canonical role `Operator` or `Administrator` (`policy.RequireRole("Operator", "Administrator")` in `AddAuthorization`, `src/Server/ZB.MOM.WW.OtOpcUa.Security/ServiceCollectionExtensions.cs`). `Operator` is an appsettings-only string role (not an `AdminRole` member); map an LDAP group to it via `GroupToRole`, e.g. `"ot-driver-operator": "Operator"`. The `FleetAdmin` policy requires the `Administrator` role.
|
||||
|
||||
|
||||
Reference in New Issue
Block a user