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:
@@ -20,11 +20,52 @@ public class InboundScriptHost
|
||||
/// </summary>
|
||||
public RouteHelper Route { get; } = new();
|
||||
|
||||
/// <summary>
|
||||
/// Scoped, parameterized database access. Editor mirror of
|
||||
/// ZB.MOM.WW.ScadaBridge.InboundAPI.InboundDatabaseHelper (InboundAPI-026/030) — its
|
||||
/// signatures must match the runtime helper so a script using <c>Database.*</c>
|
||||
/// type-checks during analysis.
|
||||
/// </summary>
|
||||
public DatabaseAccessor Database { get; } = new();
|
||||
|
||||
/// <summary>
|
||||
/// The cancellation token for the operation.
|
||||
/// </summary>
|
||||
public System.Threading.CancellationToken CancellationToken { get; }
|
||||
|
||||
/// <summary>Editor mirror of ZB.MOM.WW.ScadaBridge.InboundAPI.InboundDatabaseHelper.</summary>
|
||||
public class DatabaseAccessor
|
||||
{
|
||||
/// <summary>First column of the first row as <typeparamref name="T"/> (default if no rows).</summary>
|
||||
/// <typeparam name="T">The scalar result type.</typeparam>
|
||||
/// <param name="connectionName">A configured database connection name.</param>
|
||||
/// <param name="sql">The SQL statement; values supplied via <paramref name="parameters"/>.</param>
|
||||
/// <param name="parameters">Optional anonymous object of bound parameters.</param>
|
||||
/// <returns>A task that resolves to the scalar, or default.</returns>
|
||||
public System.Threading.Tasks.Task<T?> QuerySingleAsync<T>(
|
||||
string connectionName, string sql, object? parameters = null) =>
|
||||
System.Threading.Tasks.Task.FromResult<T?>(default);
|
||||
|
||||
/// <summary>All rows as case-insensitive column→value dictionaries.</summary>
|
||||
/// <param name="connectionName">A configured database connection name.</param>
|
||||
/// <param name="sql">The SQL statement; values supplied via <paramref name="parameters"/>.</param>
|
||||
/// <param name="parameters">Optional anonymous object of bound parameters.</param>
|
||||
/// <returns>A task that resolves to the result rows.</returns>
|
||||
public System.Threading.Tasks.Task<IReadOnlyList<IReadOnlyDictionary<string, object?>>> QueryAsync(
|
||||
string connectionName, string sql, object? parameters = null) =>
|
||||
System.Threading.Tasks.Task.FromResult<IReadOnlyList<IReadOnlyDictionary<string, object?>>>(
|
||||
new List<IReadOnlyDictionary<string, object?>>());
|
||||
|
||||
/// <summary>Runs a write statement; returns rows affected.</summary>
|
||||
/// <param name="connectionName">A configured database connection name.</param>
|
||||
/// <param name="sql">The SQL statement; values supplied via <paramref name="parameters"/>.</param>
|
||||
/// <param name="parameters">Optional anonymous object of bound parameters.</param>
|
||||
/// <returns>A task that resolves to the number of rows affected.</returns>
|
||||
public System.Threading.Tasks.Task<int> ExecuteAsync(
|
||||
string connectionName, string sql, object? parameters = null) =>
|
||||
System.Threading.Tasks.Task.FromResult(0);
|
||||
}
|
||||
|
||||
/// <summary>Editor mirror of ZB.MOM.WW.ScadaBridge.InboundAPI.RouteHelper.</summary>
|
||||
public class RouteHelper
|
||||
{
|
||||
|
||||
@@ -23,6 +23,48 @@ public class SandboxInboundScriptHost
|
||||
/// <summary>Gets the route accessor; every call throws <see cref="ScriptSandboxException"/> in a test run.</summary>
|
||||
public RouteAccessor Route { get; } = new();
|
||||
|
||||
/// <summary>Gets the database accessor; every call throws <see cref="ScriptSandboxException"/> in a test run.</summary>
|
||||
public DatabaseAccessor Database { get; } = new();
|
||||
|
||||
/// <summary>
|
||||
/// Mirror of ZB.MOM.WW.ScadaBridge.InboundAPI.InboundDatabaseHelper — signatures match
|
||||
/// the runtime helper so the same user code compiles, but every call throws because a
|
||||
/// central Test Run has no configured database connection to reach (mirrors how
|
||||
/// <see cref="RouteAccessor"/> throws on cross-site routing).
|
||||
/// </summary>
|
||||
public class DatabaseAccessor
|
||||
{
|
||||
/// <summary>Always throws <see cref="ScriptSandboxException"/>; database access is unavailable in a Test Run.</summary>
|
||||
/// <typeparam name="T">The scalar result type.</typeparam>
|
||||
/// <param name="connectionName">Connection name (included in the exception message).</param>
|
||||
/// <param name="sql">Unused SQL.</param>
|
||||
/// <param name="parameters">Unused parameters.</param>
|
||||
/// <returns>Never returns; always throws <see cref="ScriptSandboxException"/>.</returns>
|
||||
public Task<T?> QuerySingleAsync<T>(string connectionName, string sql, object? parameters = null) =>
|
||||
throw Unavailable($"Database.QuerySingleAsync(\"{connectionName}\")");
|
||||
|
||||
/// <summary>Always throws <see cref="ScriptSandboxException"/>; database access is unavailable in a Test Run.</summary>
|
||||
/// <param name="connectionName">Connection name (included in the exception message).</param>
|
||||
/// <param name="sql">Unused SQL.</param>
|
||||
/// <param name="parameters">Unused parameters.</param>
|
||||
/// <returns>Never returns; always throws <see cref="ScriptSandboxException"/>.</returns>
|
||||
public Task<IReadOnlyList<IReadOnlyDictionary<string, object?>>> QueryAsync(
|
||||
string connectionName, string sql, object? parameters = null) =>
|
||||
throw Unavailable($"Database.QueryAsync(\"{connectionName}\")");
|
||||
|
||||
/// <summary>Always throws <see cref="ScriptSandboxException"/>; database access is unavailable in a Test Run.</summary>
|
||||
/// <param name="connectionName">Connection name (included in the exception message).</param>
|
||||
/// <param name="sql">Unused SQL.</param>
|
||||
/// <param name="parameters">Unused parameters.</param>
|
||||
/// <returns>Never returns; always throws <see cref="ScriptSandboxException"/>.</returns>
|
||||
public Task<int> ExecuteAsync(string connectionName, string sql, object? parameters = null) =>
|
||||
throw Unavailable($"Database.ExecuteAsync(\"{connectionName}\")");
|
||||
|
||||
private static ScriptSandboxException Unavailable(string operation) =>
|
||||
new($"{operation} is not available in Test Run — database access needs the " +
|
||||
"central configuration / machine-data databases.");
|
||||
}
|
||||
|
||||
/// <summary>Mirror of ZB.MOM.WW.ScadaBridge.InboundAPI.RouteHelper.</summary>
|
||||
public class RouteAccessor
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user