fix(inbound): authorize+secure Database helper, async/deadline-bound DB, wait-timeout-bound WaitForAttribute

Resolves InboundAPI-026/027/028/029 (+ newly-surfaced -030).

- 026: authorize the scoped Database helper in the design doc; SQL-injection
  protection is parameter binding (values never concatenated); allow writes via
  ExecuteAsync; drop the false 'read-only' claim. Named connections only.
- 027: async ADO.NET end-to-end (no .GetAwaiter().GetResult()); honour the method
  deadline token on ExecuteScalarAsync/ExecuteReaderAsync/ExecuteNonQueryAsync +
  a CommandTimeout backstop derived from the method timeout.
- 028: negative-path tests (null-gateway, deadline cancellation, parameterization)
  + e2e Database + WaitForAttribute cases through the real endpoint.
- 029: WaitForAttribute is bounded by its WAIT timeout (per-wait CTS + client-abort
  + explicit token), NOT the method deadline (spec §6) — a long wait may outlive the
  method timeout; WithRequestAborted threads the raw client-abort token separately.
- 030: Central UI compile-surface mirrors (InboundScriptHost / SandboxInboundScriptHost)
  gained the Database member (drifted since the runtime helper was added) so the
  authorized async API type-checks at the design-time gate.
This commit is contained in:
Joseph Doherty
2026-06-23 22:00:17 -04:00
parent d39089f4ed
commit b3c9014379
11 changed files with 540 additions and 68 deletions
+33 -15
View File
@@ -189,7 +189,7 @@ Inbound API scripts **cannot** call shared scripts directly — shared scripts a
- `Route.To("instanceUniqueCode").GetAttributes("attr1", "attr2", ...)` — Read multiple attribute values in a **single call**, returned as a dictionary of name-value pairs.
- `Route.To("instanceUniqueCode").SetAttribute("attributeName", value)` — Write a single attribute value on a specific instance at any site.
- `Route.To("instanceUniqueCode").SetAttributes(dictionary)` — Write multiple attribute values in a **single call**, accepting a dictionary of name-value pairs.
- `Route.To("instanceUniqueCode").WaitForAttribute("attributeName", targetValue, timeout)` — Wait, event-driven, until an attribute on a specific instance at any site reaches `targetValue` (value-equality only across the wire), bounded by `timeout`. Returns `true` if matched within the timeout, `false` if it timed out. The cluster call is bounded by the wait timeout rather than the generic integration timeout.
- `Route.To("instanceUniqueCode").WaitForAttribute("attributeName", targetValue, timeout)` — Wait, event-driven, until an attribute on a specific instance at any site reaches `targetValue` (value-equality only across the wire), bounded by `timeout`. Returns `true` if matched within the timeout, `false` if it timed out. **The wait is bounded by its own `timeout`, not the generic method-level timeout** — this is the one routed call that may legitimately outlive the method timeout (the site enforces `timeout` and returns `false` when it elapses). A client disconnect still cancels the wait. This is the deliberate exception to the rule below that routed calls inherit the method-level timeout (see "Routing Behavior"): a long event-driven wait is the explicit reason `timeout` governs here.
#### Input/Output
- **Input parameters** are available as defined in the method definition.
@@ -199,20 +199,38 @@ Inbound API scripts **cannot** call shared scripts directly — shared scripts a
- `Parameters["key"]` — Raw dictionary access.
- `Parameters.Get<T>("key")` — Typed access (same API as site runtime scripts). See Site Runtime component for full type support.
> **No direct database access.** Inbound API scripts are not given a raw database
> client. Handing a script a raw `SqlConnection` is in direct tension with the
> ScadaBridge script trust model (scripts are forbidden `System.IO`, `Process`,
> `Threading`, `Reflection`, and raw network access). The `ForbiddenApiChecker`
> statically enforces this by delegating to the shared `ScriptAnalysis`
> `ScriptTrustValidator` (component #25), which is the single authoritative
> source of truth for the forbidden-API policy. The unified policy permits
> `System.Diagnostics.Stopwatch`/`Debug` while retaining the `Process`-only ban,
> and adds reflection-gateway member and `dynamic`/`Activator` hardening.
> This is defence-in-depth static enforcement, not a true runtime sandbox. Scripts
> interact with the system only through the curated `Route` and `Parameters`
> surfaces above. If a method needs data from the configuration or machine-data
> databases, that access belongs behind a dedicated, scoped helper — not a
> general-purpose connection — and would be added here as an explicit design change.
#### Database Access
Inbound API scripts may read from and write to the configuration / machine-data
databases through the **curated, scoped `Database` helper**`InboundDatabaseHelper`,
exposed as `InboundScriptContext.Database`. This is the "dedicated, scoped helper added
as an explicit design change" that the script trust model requires: scripts are **never**
handed a raw `SqlConnection`, and never reference `System.Data` (the `ForbiddenApiChecker`,
delegating to the shared `ScriptAnalysis` `ScriptTrustValidator` (#25), still statically
bans `System.IO`, `Process`, `Threading`, `Reflection`, and raw network access — that is
defence-in-depth static enforcement, not a true runtime sandbox).
The helper API (all asynchronous — scripts `await` them; bounded by the method timeout):
- `await Database.QuerySingleAsync<T>("connectionName", sql, parameters)` — first column of the first row as `T` (default if no rows).
- `await Database.QueryAsync("connectionName", sql, parameters)` — all rows as case-insensitive column→value dictionaries.
- `await Database.ExecuteAsync("connectionName", sql, parameters)` — run a write (INSERT/UPDATE/DELETE/DDL); returns rows affected. **Writes are permitted** — the move-in integration records results, not just reads them.
Containment rules (enforced, not advisory):
- **Named connections only.** `connectionName` selects one of the connections configured
on the central database gateway (`IDatabaseGateway`); a script cannot supply an arbitrary
connection string or reach a database the gateway is not configured for.
- **SQL-injection protection.** Statement text is authored by the (design-time) method
script, but every request-derived **value** is passed via `parameters` and bound as a
named `@`-prefixed SQL parameter — never string-concatenated into the command text.
Request input therefore reaches the database only through parameter binding.
- **Deadline-bound.** Calls use the async ADO.NET path end-to-end (no pool-thread blocking)
and honour the method deadline token, with a `CommandTimeout` backstop derived from the
method timeout, so a slow query is bounded by the method timeout.
For everything else, scripts interact with the system through the curated `Route` and
`Parameters` surfaces above.
### Routing Behavior
- The `Route.To()` helper resolves the instance's site assignment from the configuration database and routes the request to the correct site cluster via the Communication Layer.