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:
@@ -19,6 +19,7 @@ public class RouteHelper
|
||||
private readonly IInstanceLocator _instanceLocator;
|
||||
private readonly IInstanceRouter _instanceRouter;
|
||||
private readonly CancellationToken _deadlineToken;
|
||||
private readonly CancellationToken _requestAbortedToken;
|
||||
private readonly Guid? _parentExecutionId;
|
||||
|
||||
/// <summary>
|
||||
@@ -29,7 +30,7 @@ public class RouteHelper
|
||||
public RouteHelper(
|
||||
IInstanceLocator instanceLocator,
|
||||
IInstanceRouter instanceRouter)
|
||||
: this(instanceLocator, instanceRouter, CancellationToken.None, parentExecutionId: null)
|
||||
: this(instanceLocator, instanceRouter, CancellationToken.None, CancellationToken.None, parentExecutionId: null)
|
||||
{
|
||||
}
|
||||
|
||||
@@ -37,11 +38,13 @@ public class RouteHelper
|
||||
IInstanceLocator instanceLocator,
|
||||
IInstanceRouter instanceRouter,
|
||||
CancellationToken deadlineToken,
|
||||
CancellationToken requestAbortedToken,
|
||||
Guid? parentExecutionId)
|
||||
{
|
||||
_instanceLocator = instanceLocator;
|
||||
_instanceRouter = instanceRouter;
|
||||
_deadlineToken = deadlineToken;
|
||||
_requestAbortedToken = requestAbortedToken;
|
||||
_parentExecutionId = parentExecutionId;
|
||||
}
|
||||
|
||||
@@ -55,7 +58,20 @@ public class RouteHelper
|
||||
/// <param name="deadlineToken">The executing method's timeout cancellation token to inherit for routed calls.</param>
|
||||
/// <returns>A new <see cref="RouteHelper"/> inheriting the given deadline token.</returns>
|
||||
public RouteHelper WithDeadline(CancellationToken deadlineToken) =>
|
||||
new(_instanceLocator, _instanceRouter, deadlineToken, _parentExecutionId);
|
||||
new(_instanceLocator, _instanceRouter, deadlineToken, _requestAbortedToken, _parentExecutionId);
|
||||
|
||||
/// <summary>
|
||||
/// InboundAPI-029: returns a <see cref="RouteHelper"/> carrying the raw request-abort
|
||||
/// token (a client disconnect) <em>separately</em> from the method deadline. Most
|
||||
/// routed calls remain bounded by the method deadline (which already incorporates the
|
||||
/// abort), but <see cref="RouteTarget.WaitForAttribute"/> uses this token so its wait
|
||||
/// is bounded by the WAIT timeout — not the generic method deadline (spec §6) — while
|
||||
/// still being cancelled by a client disconnect.
|
||||
/// </summary>
|
||||
/// <param name="requestAbortedToken">The raw client-disconnect token, independent of the method timeout.</param>
|
||||
/// <returns>A new <see cref="RouteHelper"/> carrying the given request-abort token.</returns>
|
||||
public RouteHelper WithRequestAborted(CancellationToken requestAbortedToken) =>
|
||||
new(_instanceLocator, _instanceRouter, _deadlineToken, requestAbortedToken, _parentExecutionId);
|
||||
|
||||
/// <summary>
|
||||
/// Audit Log #23 (ParentExecutionId): returns a <see cref="RouteHelper"/> whose
|
||||
@@ -69,7 +85,7 @@ public class RouteHelper
|
||||
/// <param name="parentExecutionId">The inbound request's execution id to stamp as the spawning parent on routed calls, or null for non-routed runs.</param>
|
||||
/// <returns>A new <see cref="RouteHelper"/> carrying the given parent execution id.</returns>
|
||||
public RouteHelper WithParentExecutionId(Guid? parentExecutionId) =>
|
||||
new(_instanceLocator, _instanceRouter, _deadlineToken, parentExecutionId);
|
||||
new(_instanceLocator, _instanceRouter, _deadlineToken, _requestAbortedToken, parentExecutionId);
|
||||
|
||||
/// <summary>
|
||||
/// Creates a route target for the specified instance.
|
||||
@@ -79,7 +95,7 @@ public class RouteHelper
|
||||
public RouteTarget To(string instanceCode)
|
||||
{
|
||||
return new RouteTarget(
|
||||
instanceCode, _instanceLocator, _instanceRouter, _deadlineToken, _parentExecutionId);
|
||||
instanceCode, _instanceLocator, _instanceRouter, _deadlineToken, _requestAbortedToken, _parentExecutionId);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -88,10 +104,18 @@ public class RouteHelper
|
||||
/// </summary>
|
||||
public class RouteTarget
|
||||
{
|
||||
// InboundAPI-029: a small grace past the wait timeout. The SITE enforces the wait
|
||||
// timeout and returns Matched=false when it elapses; the local backstop fires only
|
||||
// if the site fails to respond, so it must sit slightly LATER than the wait timeout
|
||||
// (it must not pre-empt the site's own timed-out response and turn a clean `false`
|
||||
// into a cancellation).
|
||||
private static readonly TimeSpan WaitResponseGrace = TimeSpan.FromSeconds(5);
|
||||
|
||||
private readonly string _instanceCode;
|
||||
private readonly IInstanceLocator _instanceLocator;
|
||||
private readonly IInstanceRouter _instanceRouter;
|
||||
private readonly CancellationToken _deadlineToken;
|
||||
private readonly CancellationToken _requestAbortedToken;
|
||||
private readonly Guid? _parentExecutionId;
|
||||
|
||||
/// <summary>
|
||||
@@ -101,18 +125,21 @@ public class RouteTarget
|
||||
/// <param name="instanceLocator">Service to resolve the site id for the instance.</param>
|
||||
/// <param name="instanceRouter">Service to route cross-site calls.</param>
|
||||
/// <param name="deadlineToken">Cancellation token representing the method-level deadline.</param>
|
||||
/// <param name="requestAbortedToken">Raw client-disconnect token, independent of the method timeout (InboundAPI-029).</param>
|
||||
/// <param name="parentExecutionId">Optional parent execution id for audit correlation on routed calls.</param>
|
||||
internal RouteTarget(
|
||||
string instanceCode,
|
||||
IInstanceLocator instanceLocator,
|
||||
IInstanceRouter instanceRouter,
|
||||
CancellationToken deadlineToken,
|
||||
CancellationToken requestAbortedToken,
|
||||
Guid? parentExecutionId)
|
||||
{
|
||||
_instanceCode = instanceCode;
|
||||
_instanceLocator = instanceLocator;
|
||||
_instanceRouter = instanceRouter;
|
||||
_deadlineToken = deadlineToken;
|
||||
_requestAbortedToken = requestAbortedToken;
|
||||
_parentExecutionId = parentExecutionId;
|
||||
}
|
||||
|
||||
@@ -211,11 +238,22 @@ public class RouteTarget
|
||||
/// wire: the target is canonically encoded via <see cref="AttributeValueCodec"/> and
|
||||
/// the site evaluates equality — there is no predicate and no quality flag in the
|
||||
/// comparison.
|
||||
///
|
||||
/// <para>
|
||||
/// InboundAPI-029: unlike the other routed calls (which inherit the method deadline
|
||||
/// per InboundAPI-016), the wait is bounded by <paramref name="timeout"/> — the WAIT
|
||||
/// timeout — NOT the generic method deadline. The SITE enforces <paramref name="timeout"/>
|
||||
/// and returns <c>Matched=false</c> when it elapses; the local token is built from a
|
||||
/// per-wait CTS (the wait timeout plus a small grace), an explicit caller token, and
|
||||
/// the client-disconnect token, but deliberately EXCLUDES the method deadline. So a
|
||||
/// wait longer than the method timeout runs to its full wait timeout, as spec §6
|
||||
/// requires, while a client disconnect still cancels it.
|
||||
/// </para>
|
||||
/// </summary>
|
||||
/// <param name="attributeName">Name of the attribute to wait on.</param>
|
||||
/// <param name="targetValue">Target value the attribute must equal for the wait to match.</param>
|
||||
/// <param name="timeout">Maximum time to wait for the attribute to reach the target value.</param>
|
||||
/// <param name="cancellationToken">Optional cancellation token; defaults to the method deadline.</param>
|
||||
/// <param name="timeout">Maximum time to wait for the attribute to reach the target value; this — not the method deadline — bounds the wait.</param>
|
||||
/// <param name="cancellationToken">Optional explicit cancellation token (a tighter caller-supplied bound); the wait is otherwise bounded by <paramref name="timeout"/>.</param>
|
||||
/// <returns>A task that resolves to <c>true</c> if the attribute reached the target value, <c>false</c> if the wait timed out.</returns>
|
||||
public async Task<bool> WaitForAttribute(
|
||||
string attributeName,
|
||||
@@ -223,7 +261,12 @@ public class RouteTarget
|
||||
TimeSpan timeout,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var token = Effective(cancellationToken);
|
||||
// InboundAPI-029: bound the wait by the WAIT timeout (+ grace backstop), the
|
||||
// client-disconnect token, and an explicit caller token — NOT the method deadline.
|
||||
using var waitCts = new CancellationTokenSource(timeout + WaitResponseGrace);
|
||||
using var linked = CancellationTokenSource.CreateLinkedTokenSource(
|
||||
waitCts.Token, _requestAbortedToken, cancellationToken);
|
||||
var token = linked.Token;
|
||||
var siteId = await ResolveSiteAsync(token);
|
||||
|
||||
// Audit Log #23 (ParentExecutionId): mirrors the Call path — stamp the
|
||||
|
||||
Reference in New Issue
Block a user