docs(comments): strip internal task/milestone/bundle bookkeeping from code comments
Remove project bookkeeping citations from shipped code comments across the solution: hyphenated task IDs (WP-14, StoreAndForward-025), milestone/task/ issue refs (M3, Task 4, Audit Log #23, #21), Bundle X task-bundle labels, and C/D/K/S/T phase labels. Comment text only — no code logic, string/log literals, or XML-doc structure changed. Genuine descriptions are preserved (only the citation is stripped), and technical lookalikes are retained (UTF-8, SHA-256, T00:00:00, M365, UTC-5, pre-C4/pre-C5 schema versions). Flagged by the new CommentChecker TaskReferenceInComment / TrackingReferenceInComment checks plus targeted grep passes; full solution builds clean, append-only guard tests pass.
This commit is contained in:
@@ -26,15 +26,15 @@ public sealed class InboundApiEndpoint
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// WP-1: POST /api/{methodName} endpoint registration.
|
||||
/// WP-2: Method routing and parameter validation.
|
||||
/// WP-3: Script execution on central.
|
||||
/// WP-5: Error handling — 401, 403, 400, 500.
|
||||
/// POST /api/{methodName} endpoint registration.
|
||||
/// Method routing and parameter validation.
|
||||
/// Script execution on central.
|
||||
/// Error handling — 401, 403, 400, 500.
|
||||
/// </summary>
|
||||
public static class EndpointExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Auth re-arch (A+B), InboundAPI-011 successor: the single message used for
|
||||
/// Auth re-arch (A+B), successor: the single message used for
|
||||
/// BOTH "method not found" and "key not in scope for this method" so the two
|
||||
/// outcomes are indistinguishable to the caller. A caller holding any valid key
|
||||
/// must not be able to enumerate which method names exist by observing a
|
||||
@@ -57,7 +57,7 @@ public static class EndpointExtensions
|
||||
public static IEndpointRouteBuilder MapInboundAPI(this IEndpointRouteBuilder endpoints)
|
||||
{
|
||||
endpoints.MapPost("/api/{methodName}", HandleInboundApiRequest)
|
||||
// InboundAPI-006 / InboundAPI-008: active-node gating + request body
|
||||
// Active-node gating + request body
|
||||
// size cap are enforced by the endpoint filter before the handler runs.
|
||||
.AddEndpointFilter<InboundApiEndpointFilter>();
|
||||
return endpoints;
|
||||
@@ -96,14 +96,14 @@ public static class EndpointExtensions
|
||||
|
||||
if (!verification.Succeeded)
|
||||
{
|
||||
// WP-5: 401 for any verifier failure. The failure reason is
|
||||
// 401 for any verifier failure. The failure reason is
|
||||
// discriminated for our own logs/telemetry but NEVER surfaced to the
|
||||
// caller — every reason maps to the one generic message so the auth
|
||||
// stage (missing vs unknown-key vs revoked vs secret-mismatch) is not
|
||||
// leaked.
|
||||
ScadaBridgeTelemetry.RecordInboundApiRequest("<unauthorized>");
|
||||
|
||||
// WP-5: Failures-only logging.
|
||||
// Failures-only logging.
|
||||
logger.LogWarning(
|
||||
"Inbound API auth failure for method {Method}: {Failure} (status 401)",
|
||||
methodName, verification.Failure);
|
||||
@@ -122,7 +122,7 @@ public static class EndpointExtensions
|
||||
// byte-identical (status + message), so a caller holding a valid key
|
||||
// cannot probe which method names exist.
|
||||
//
|
||||
// Review #3 (scope-check-before-DB-lookup): the in-memory scope check runs
|
||||
// The in-memory scope check runs
|
||||
// FIRST and the DB GetMethodByNameAsync only runs when the caller is in
|
||||
// scope. This removes a timing oracle — a not-in-scope caller no longer
|
||||
// pays a DB round-trip whose latency could distinguish "valid scope but no
|
||||
@@ -130,7 +130,7 @@ public static class EndpointExtensions
|
||||
// the reject path. The combined `method == null || !inScope` guard still
|
||||
// emits the single identical 403 body, preserving enumeration-safety.
|
||||
//
|
||||
// Review #2 (scope case-policy): scope strings ARE the method names, and
|
||||
// Scope strings ARE the method names, and
|
||||
// identity.Scopes.Contains(methodName) is an ordinal, case-SENSITIVE
|
||||
// comparison (HashSet<string> with the default StringComparer.Ordinal). A
|
||||
// scope must therefore match the registered method name's casing EXACTLY —
|
||||
@@ -161,18 +161,18 @@ public static class EndpointExtensions
|
||||
// set of configured API methods — never the raw caller-supplied route value.
|
||||
ScadaBridgeTelemetry.RecordInboundApiRequest(method.Name);
|
||||
|
||||
// Audit Log (#23 M4 Bundle D): publish the verified key's display name so
|
||||
// Publish the verified key's display name so
|
||||
// AuditWriteMiddleware can populate AuditEvent.Actor in its finally
|
||||
// block. Done AFTER auth+authz succeeded — auth failures leave the
|
||||
// slot empty and the middleware records the row with Actor=null.
|
||||
httpContext.Items[AuditWriteMiddleware.AuditActorItemKey] =
|
||||
identity.DisplayName;
|
||||
|
||||
// WP-2: Deserialize and validate parameters
|
||||
// Deserialize and validate parameters
|
||||
JsonElement? body = null;
|
||||
try
|
||||
{
|
||||
// InboundAPI-020: the content-type sniff must be case-insensitive — a
|
||||
// The content-type sniff must be case-insensitive — a
|
||||
// request with `application/JSON` or `Application/Json` is still JSON
|
||||
// and must enter the body-parsing path. The previous case-sensitive
|
||||
// `Contains("json")` silently skipped JSON deserialization for any
|
||||
@@ -193,7 +193,7 @@ public static class EndpointExtensions
|
||||
statusCode: 400);
|
||||
}
|
||||
|
||||
// M9-T32b: thread the JSON-Schema $ref resolution seam into parameter
|
||||
// Thread the JSON-Schema $ref resolution seam into parameter
|
||||
// validation so a method whose ParameterDefinitions use a {"$ref":"lib:Name"}
|
||||
// resolves the reference at RUNTIME (not just at deploy time). The shared-schema
|
||||
// library is pre-loaded ONCE per request into an in-memory map (the seam the
|
||||
@@ -214,12 +214,12 @@ public static class EndpointExtensions
|
||||
statusCode: 400);
|
||||
}
|
||||
|
||||
// WP-3: Execute the method's script
|
||||
// Execute the method's script
|
||||
var timeout = method.TimeoutSeconds > 0
|
||||
? TimeSpan.FromSeconds(method.TimeoutSeconds)
|
||||
: options.DefaultMethodTimeout;
|
||||
|
||||
// Audit Log #23 (ParentExecutionId): the inbound request's per-request
|
||||
// The inbound request's per-request
|
||||
// ExecutionId was minted early by AuditWriteMiddleware and stashed on
|
||||
// HttpContext.Items. Thread it into the executor so a routed
|
||||
// Route.To(...).Call(...) carries it as RouteToCallRequest.ParentExecutionId
|
||||
@@ -237,7 +237,7 @@ public static class EndpointExtensions
|
||||
|
||||
if (!scriptResult.Success)
|
||||
{
|
||||
// InboundAPI-004: a client-aborted request is not a script failure.
|
||||
// A client-aborted request is not a script failure.
|
||||
// Do not pollute the failure log (reserved for genuine script errors)
|
||||
// and do not attempt to write a 500 body to an already-gone connection.
|
||||
if (httpContext.RequestAborted.IsCancellationRequested)
|
||||
@@ -245,7 +245,7 @@ public static class EndpointExtensions
|
||||
return Results.Empty;
|
||||
}
|
||||
|
||||
// WP-5: 500 for script failures, safe error message
|
||||
// 500 for script failures, safe error message
|
||||
logger.LogWarning(
|
||||
"Inbound API script failure for method {Method}: {Error}",
|
||||
methodName, scriptResult.ErrorMessage);
|
||||
|
||||
@@ -3,18 +3,18 @@ using ZB.MOM.WW.ScadaBridge.ScriptAnalysis;
|
||||
namespace ZB.MOM.WW.ScadaBridge.InboundAPI;
|
||||
|
||||
/// <summary>
|
||||
/// InboundAPI-005: Enforces the ScadaBridge script trust model on inbound API method
|
||||
/// Enforces the ScadaBridge script trust model on inbound API method
|
||||
/// scripts before they are compiled into executable handlers.
|
||||
///
|
||||
/// This class is now a thin shim that delegates to the shared, authoritative
|
||||
/// <see cref="ScriptTrustValidator.FindViolations"/> implemented in
|
||||
/// <c>ZB.MOM.WW.ScadaBridge.ScriptAnalysis</c> (M3.4). The unified validator runs
|
||||
/// <c>ZB.MOM.WW.ScadaBridge.ScriptAnalysis</c>. The unified validator runs
|
||||
/// both a semantic symbol pass (catching alias / <c>global::</c> / <c>using static</c>
|
||||
/// escapes) and the reflection-gateway + <c>dynamic</c> / <c>Activator</c> syntactic
|
||||
/// hardening that previously lived exclusively in this file.
|
||||
///
|
||||
/// <para>
|
||||
/// InboundAPI-015: a purely namespace-textual deny-list is bypassable because
|
||||
/// A purely namespace-textual deny-list is bypassable because
|
||||
/// reflection is reachable through members of <em>permitted</em> types that never
|
||||
/// spell a forbidden namespace, e.g.
|
||||
/// <c>typeof(string).Assembly.GetType("System.IO.File")</c>. The shared validator
|
||||
@@ -22,8 +22,8 @@ namespace ZB.MOM.WW.ScadaBridge.InboundAPI;
|
||||
/// hardening — <c>GetType</c>, <c>Assembly</c>, <c>GetMethod</c>, <c>InvokeMember</c>,
|
||||
/// <c>CreateInstance</c>, and the <c>dynamic</c> keyword are all rejected. This
|
||||
/// remains hardening of a best-effort static check, <strong>not</strong> a true sandbox
|
||||
/// (see the security notes in <c>code-reviews/InboundAPI/findings.md</c>,
|
||||
/// InboundAPI-015). The check is defence-in-depth; genuine containment needs a
|
||||
/// (see the security notes in <c>code-reviews/InboundAPI/findings.md</c>).
|
||||
/// The check is defence-in-depth; genuine containment needs a
|
||||
/// runtime boundary (restricted <c>AssemblyLoadContext</c> / curated reference set /
|
||||
/// out-of-process sandbox).
|
||||
/// </para>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
namespace ZB.MOM.WW.ScadaBridge.InboundAPI;
|
||||
|
||||
/// <summary>
|
||||
/// InboundAPI-008: abstraction the inbound API endpoint uses to determine whether
|
||||
/// Abstraction the inbound API endpoint uses to determine whether
|
||||
/// this node is the active (cluster-leader) central node.
|
||||
///
|
||||
/// The design states the inbound API is "Central cluster only (active node)" and
|
||||
|
||||
@@ -7,7 +7,7 @@ namespace ZB.MOM.WW.ScadaBridge.InboundAPI;
|
||||
/// The production implementation (<see cref="CommunicationServiceInstanceRouter"/>)
|
||||
/// delegates to <c>ZB.MOM.WW.ScadaBridge.Communication.CommunicationService</c>; the interface
|
||||
/// exists so <see cref="RouteHelper"/>/<see cref="RouteTarget"/> can be unit tested
|
||||
/// without a live actor system (InboundAPI-017).
|
||||
/// without a live actor system.
|
||||
/// </summary>
|
||||
public interface IInstanceRouter
|
||||
{
|
||||
|
||||
@@ -12,11 +12,11 @@ namespace ZB.MOM.WW.ScadaBridge.InboundAPI;
|
||||
///
|
||||
/// <list type="bullet">
|
||||
/// <item><description>
|
||||
/// InboundAPI-008 — active-node gating. The inbound API is central-active-node-only;
|
||||
/// Active-node gating. The inbound API is central-active-node-only;
|
||||
/// a standby node returns HTTP 503 so it never executes method scripts.
|
||||
/// </description></item>
|
||||
/// <item><description>
|
||||
/// InboundAPI-006 — request body size cap. Oversized bodies are rejected with HTTP
|
||||
/// Request body size cap. Oversized bodies are rejected with HTTP
|
||||
/// 413 before being buffered into a <c>JsonDocument</c>.
|
||||
/// </description></item>
|
||||
/// </list>
|
||||
@@ -47,7 +47,7 @@ public sealed class InboundApiEndpointFilter : IEndpointFilter
|
||||
{
|
||||
var httpContext = context.HttpContext;
|
||||
|
||||
// InboundAPI-008: refuse to serve the inbound API on a standby central node.
|
||||
// Refuse to serve the inbound API on a standby central node.
|
||||
// The gate is optional — when no IActiveNodeGate is registered (non-clustered
|
||||
// host / tests) the API is served, preserving prior behaviour.
|
||||
var gate = httpContext.RequestServices.GetService<IActiveNodeGate>();
|
||||
@@ -60,7 +60,7 @@ public sealed class InboundApiEndpointFilter : IEndpointFilter
|
||||
statusCode: StatusCodes.Status503ServiceUnavailable);
|
||||
}
|
||||
|
||||
// InboundAPI-006: cap the request body size. Reject an over-limit body up
|
||||
// Cap the request body size. Reject an over-limit body up
|
||||
// front via Content-Length; also lower the per-request max body size so a
|
||||
// chunked/unknown-length stream is cut off by Kestrel as it is read.
|
||||
var maxBytes = _options.MaxRequestBodyBytes;
|
||||
|
||||
@@ -3,7 +3,7 @@ namespace ZB.MOM.WW.ScadaBridge.InboundAPI;
|
||||
public class InboundApiOptions
|
||||
{
|
||||
/// <summary>
|
||||
/// Default cap on the inbound API request body, in bytes (InboundAPI-006).
|
||||
/// Default cap on the inbound API request body, in bytes.
|
||||
/// </summary>
|
||||
public const long DefaultMaxRequestBodyBytes = 1L * 1024 * 1024; // 1 MiB
|
||||
|
||||
@@ -11,7 +11,7 @@ public class InboundApiOptions
|
||||
public TimeSpan DefaultMethodTimeout { get; set; } = TimeSpan.FromSeconds(30);
|
||||
|
||||
/// <summary>
|
||||
/// InboundAPI-006: maximum accepted request body size for <c>POST /api/{methodName}</c>.
|
||||
/// Maximum accepted request body size for <c>POST /api/{methodName}</c>.
|
||||
/// Requests whose body exceeds this are rejected with HTTP 413 before being
|
||||
/// buffered into a <see cref="System.Text.Json.JsonDocument"/>. The inbound API
|
||||
/// has no rate limiting (a deliberate design choice), so an explicit, modest cap
|
||||
@@ -23,7 +23,7 @@ public class InboundApiOptions
|
||||
/// Server-side HMAC pepper for inbound-API bearer credentials, bound from
|
||||
/// <c>ScadaBridge:InboundApi:ApiKeyPepper</c>.
|
||||
/// <para>
|
||||
/// Auth re-arch (C5): the legacy SQL Server hashing path that consumed this
|
||||
/// Auth re-arch: the legacy SQL Server hashing path that consumed this
|
||||
/// property was retired. The pepper itself is still required — the shared
|
||||
/// ZB.MOM.WW.Auth.ApiKeys verifier reads the SAME configuration key
|
||||
/// (<c>PepperSecretName</c> in the Host composition root points at it) to pepper
|
||||
|
||||
@@ -13,7 +13,7 @@ namespace ZB.MOM.WW.ScadaBridge.InboundAPI;
|
||||
/// by name and never reference <c>System.Data</c>.
|
||||
///
|
||||
/// <para>
|
||||
/// <b>SQL-injection protection (InboundAPI-026).</b> Statement text is authored by the
|
||||
/// <b>SQL-injection protection.</b> Statement text is authored by the
|
||||
/// (design-time) method script, but every <em>value</em> is bound as a named SQL
|
||||
/// parameter (anonymous-object properties become <c>@</c>-prefixed parameters via
|
||||
/// <see cref="AddParameters"/>) and is NEVER string-concatenated into the command text.
|
||||
@@ -24,13 +24,13 @@ namespace ZB.MOM.WW.ScadaBridge.InboundAPI;
|
||||
/// </para>
|
||||
///
|
||||
/// <para>
|
||||
/// <b>Reads and writes are both permitted</b> (InboundAPI-026 design decision): the move-in
|
||||
/// <b>Reads and writes are both permitted</b>: the move-in
|
||||
/// integration needs to record results, not just read them. Use <see cref="QueryAsync"/>
|
||||
/// / <see cref="QuerySingleAsync{T}"/> for reads and <see cref="ExecuteAsync"/> for writes.
|
||||
/// </para>
|
||||
///
|
||||
/// <para>
|
||||
/// <b>Async + deadline-bound (InboundAPI-027).</b> Every call uses the async ADO.NET path
|
||||
/// <b>Async + deadline-bound.</b> Every call uses the async ADO.NET path
|
||||
/// end-to-end (no <c>.GetAwaiter().GetResult()</c> blocking a pool thread) and honours the
|
||||
/// executing method's deadline token on the command itself, with a <see cref="DbCommand.CommandTimeout"/>
|
||||
/// backstop derived from the method timeout — so a slow query is bounded by the method
|
||||
@@ -103,7 +103,7 @@ public sealed class InboundDatabaseHelper
|
||||
|
||||
/// <summary>
|
||||
/// Executes a write statement (INSERT/UPDATE/DELETE/DDL) and returns the number of
|
||||
/// rows affected. Writes are authorized for inbound API scripts (InboundAPI-026);
|
||||
/// rows affected. Writes are authorized for inbound API scripts;
|
||||
/// values are still bound as parameters, never concatenated.
|
||||
/// </summary>
|
||||
/// <param name="connectionName">Name of a connection configured on the central database gateway.</param>
|
||||
@@ -122,7 +122,7 @@ public sealed class InboundDatabaseHelper
|
||||
{
|
||||
var cmd = conn.CreateCommand();
|
||||
cmd.CommandText = sql;
|
||||
// InboundAPI-027: a CommandTimeout backstop derived from the method timeout so a
|
||||
// A CommandTimeout backstop derived from the method timeout so a
|
||||
// slow query cannot outrun the method deadline even if the provider does not
|
||||
// honour token cancellation mid-statement.
|
||||
if (_commandTimeoutSeconds > 0) cmd.CommandTimeout = _commandTimeoutSeconds;
|
||||
|
||||
@@ -13,25 +13,25 @@ using ZB.MOM.WW.ScadaBridge.Commons.Types;
|
||||
namespace ZB.MOM.WW.ScadaBridge.InboundAPI;
|
||||
|
||||
/// <summary>
|
||||
/// WP-3: Executes the C# script associated with an inbound API method.
|
||||
/// Executes the C# script associated with an inbound API method.
|
||||
/// Compiles method scripts via Roslyn and caches compiled delegates.
|
||||
/// </summary>
|
||||
public class InboundScriptExecutor
|
||||
{
|
||||
private readonly ILogger<InboundScriptExecutor> _logger;
|
||||
|
||||
// InboundAPI-001: this executor is registered as a singleton and its handler cache
|
||||
// This executor is registered as a singleton and its handler cache
|
||||
// is read and written from concurrent ASP.NET request threads. A plain Dictionary is
|
||||
// not safe for concurrent read/write, so a ConcurrentDictionary is used throughout.
|
||||
private readonly ConcurrentDictionary<string, Func<InboundScriptContext, Task<object?>>> _scriptHandlers = new();
|
||||
|
||||
// InboundAPI-009: a script that fails to compile (or violates the trust model)
|
||||
// A script that fails to compile (or violates the trust model)
|
||||
// is recorded here so it is compiled at most once. Without this, every subsequent
|
||||
// request for a broken method re-runs the expensive Roslyn compilation — a CPU
|
||||
// amplification vector since the inbound API has no rate limiting. The entry is
|
||||
// cleared whenever the method is (re)compiled via CompileAndRegister.
|
||||
//
|
||||
// InboundAPI-024: bound the cache so a spam attack of unique method names cannot
|
||||
// Bound the cache so a spam attack of unique method names cannot
|
||||
// grow it without bound. Once the cap is reached new bad-method records are
|
||||
// dropped — the cache is just a fast-fail optimisation; the per-request DB
|
||||
// lookup remains the correctness path.
|
||||
@@ -39,14 +39,14 @@ public class InboundScriptExecutor
|
||||
private readonly ConcurrentDictionary<string, byte> _knownBadMethods = new();
|
||||
|
||||
/// <summary>
|
||||
/// InboundAPI-024 diagnostic helper — returns the current size of the
|
||||
/// Diagnostic helper — returns the current size of the
|
||||
/// known-bad-methods cache so tests can assert the cap is honoured. Internal
|
||||
/// so the cache itself stays an implementation detail.
|
||||
/// </summary>
|
||||
internal int KnownBadMethodCount => _knownBadMethods.Count;
|
||||
|
||||
/// <summary>
|
||||
/// InboundAPI-024: records <paramref name="methodName"/> in the known-bad-methods
|
||||
/// Records <paramref name="methodName"/> in the known-bad-methods
|
||||
/// cache only if the cache has not reached <see cref="KnownBadMethodsCap"/>.
|
||||
/// Once full, new records are dropped (paying the cheap recompile next time
|
||||
/// rather than leaking memory under a unique-name flood). Existing entries are
|
||||
@@ -120,9 +120,9 @@ public class InboundScriptExecutor
|
||||
var (handler, compileErrors) = Compile(method);
|
||||
if (handler == null)
|
||||
{
|
||||
// InboundAPI-009: record the failure so the lazy-compile path does not
|
||||
// keep recompiling a broken script on every request. InboundAPI-024:
|
||||
// routed through the capped TryRecordBadMethod helper so the cache
|
||||
// Record the failure so the lazy-compile path does not
|
||||
// keep recompiling a broken script on every request. Routed
|
||||
// through the capped TryRecordBadMethod helper so the cache
|
||||
// cannot grow without bound under a flood of unique method names.
|
||||
errors = compileErrors;
|
||||
TryRecordBadMethod(method.Name);
|
||||
@@ -145,7 +145,7 @@ public class InboundScriptExecutor
|
||||
/// <summary>
|
||||
/// Compiles a single API method script into an executable handler. Returns
|
||||
/// <c>null</c> when the script is missing, fails to compile, or violates the
|
||||
/// script trust model (InboundAPI-005). Does not mutate the handler cache.
|
||||
/// script trust model. Does not mutate the handler cache.
|
||||
/// </summary>
|
||||
private (Func<InboundScriptContext, Task<object?>>? Handler, IReadOnlyList<string> Errors) Compile(ApiMethod method)
|
||||
{
|
||||
@@ -155,7 +155,7 @@ public class InboundScriptExecutor
|
||||
return (null, new[] { "Method has no script code." });
|
||||
}
|
||||
|
||||
// InboundAPI-005: enforce the script trust model before compiling. Roslyn
|
||||
// Enforce the script trust model before compiling. Roslyn
|
||||
// scripting performs no API allow/deny-listing, so forbidden namespaces must
|
||||
// be rejected statically or the script could reach the host process.
|
||||
var violations = ForbiddenApiChecker.FindViolations(method.Script);
|
||||
@@ -225,7 +225,7 @@ public class InboundScriptExecutor
|
||||
/// <param name="timeout">Timeout duration for script execution.</param>
|
||||
/// <param name="cancellationToken">Cancellation token.</param>
|
||||
/// <param name="parentExecutionId">
|
||||
/// Audit Log #23 (ParentExecutionId): the inbound API request's per-request
|
||||
/// ParentExecutionId: the inbound API request's per-request
|
||||
/// <c>ExecutionId</c> (minted early by <c>AuditWriteMiddleware</c> and stashed
|
||||
/// on <c>HttpContext.Items</c>). When supplied, a routed
|
||||
/// <c>Route.To(...).Call(...)</c> inside the script carries it as
|
||||
@@ -245,7 +245,7 @@ public class InboundScriptExecutor
|
||||
// Every call site passes it by named argument (parentExecutionId:).
|
||||
Guid? parentExecutionId = null)
|
||||
{
|
||||
// InboundAPI-004: keep the timeout source and the request-abort source
|
||||
// Keep the timeout source and the request-abort source
|
||||
// separable. A single linked CTS makes a genuine client disconnect
|
||||
// indistinguishable from a method timeout, so a normal disconnect would be
|
||||
// logged and reported as "Script execution timed out". Use a dedicated
|
||||
@@ -255,7 +255,7 @@ public class InboundScriptExecutor
|
||||
cancellationToken, timeoutCts.Token);
|
||||
|
||||
// IpsenMES MoveIn: expose the scoped, parameterized Database helper to the script
|
||||
// (reads + writes — InboundAPI-026). Resolve the gateway from a fresh DI scope
|
||||
// (reads + writes). Resolve the gateway from a fresh DI scope
|
||||
// (declared outside the try so it lives until after the script handler runs and is
|
||||
// disposed here) so a scoped IDatabaseGateway is honoured. GetService (not
|
||||
// GetRequiredService) so a method that never touches Database still runs even when
|
||||
@@ -283,22 +283,22 @@ public class InboundScriptExecutor
|
||||
try
|
||||
{
|
||||
var gateway = scope?.ServiceProvider.GetService<IDatabaseGateway>();
|
||||
// InboundAPI-027: pass the method timeout so the helper derives a
|
||||
// Pass the method timeout so the helper derives a
|
||||
// CommandTimeout backstop and forwards cts.Token to every async DB call —
|
||||
// a slow query is then bounded by the method deadline.
|
||||
var dbHelper = new InboundDatabaseHelper(gateway, cts.Token, timeout);
|
||||
|
||||
// InboundAPI-016: bind the route helper to the method deadline so a
|
||||
// Bind the route helper to the method deadline so a
|
||||
// routed Route.To(...).Call(...) inherits the method-level timeout
|
||||
// without the script having to thread the context token by hand.
|
||||
//
|
||||
// InboundAPI-029: also pass the raw request-abort token separately so
|
||||
// Also pass the raw request-abort token separately so
|
||||
// Route.To(...).WaitForAttribute(...) can be bounded by its WAIT timeout
|
||||
// (not the generic method deadline) while still being cancelled by a
|
||||
// client disconnect — see RouteTarget.WaitForAttribute.
|
||||
//
|
||||
// Audit Log #23 (ParentExecutionId): also bind the inbound request's
|
||||
// ExecutionId so a routed call carries it as ParentExecutionId — the
|
||||
// Also bind the inbound request's ExecutionId (ParentExecutionId) so a
|
||||
// routed call carries it as ParentExecutionId — the
|
||||
// spawned site script execution points back at this inbound request.
|
||||
var context = new InboundScriptContext(
|
||||
parameters,
|
||||
@@ -310,7 +310,7 @@ public class InboundScriptExecutor
|
||||
|
||||
if (!_scriptHandlers.TryGetValue(method.Name, out var handler))
|
||||
{
|
||||
// InboundAPI-009: a method already known to fail compilation must not
|
||||
// A method already known to fail compilation must not
|
||||
// be recompiled on every request — short-circuit before Roslyn runs.
|
||||
if (_knownBadMethods.ContainsKey(method.Name))
|
||||
return new InboundScriptResult(false, null, "Script compilation failed for this method");
|
||||
@@ -322,7 +322,7 @@ public class InboundScriptExecutor
|
||||
if (compiled == null)
|
||||
{
|
||||
// Cache the failure so the next request short-circuits above.
|
||||
// InboundAPI-024: routed through TryRecordBadMethod so the
|
||||
// Routed through TryRecordBadMethod so the
|
||||
// cache is bounded under a flood of unique method names.
|
||||
TryRecordBadMethod(method.Name);
|
||||
return new InboundScriptResult(false, null, "Script compilation failed for this method");
|
||||
@@ -336,7 +336,7 @@ public class InboundScriptExecutor
|
||||
? JsonSerializer.Serialize(result)
|
||||
: null;
|
||||
|
||||
// M9-T32b: thread the JSON-Schema $ref resolution seam into return
|
||||
// Thread the JSON-Schema $ref resolution seam into return
|
||||
// validation so a method whose ReturnDefinition uses a {"$ref":"lib:Name"}
|
||||
// resolves the reference at RUNTIME (not just at deploy time). The
|
||||
// shared-schema library is pre-loaded ONCE from the per-execution DI scope
|
||||
@@ -349,7 +349,7 @@ public class InboundScriptExecutor
|
||||
var resolveRef = await SchemaRefResolver.BuildAsync(
|
||||
sharedSchemaRepo, [method.ReturnDefinition], cts.Token);
|
||||
|
||||
// InboundAPI-014: validate the script's return value against the
|
||||
// Validate the script's return value against the
|
||||
// method's declared ReturnDefinition. A method whose script returns a
|
||||
// shape inconsistent with its definition must not silently emit a
|
||||
// malformed 200 — surface it as a script failure (500) and log.
|
||||
@@ -366,7 +366,7 @@ public class InboundScriptExecutor
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
// InboundAPI-004: distinguish a genuine method timeout from a client
|
||||
// Distinguish a genuine method timeout from a client
|
||||
// abort. Only the timeout CTS firing is a real timeout; if the caller's
|
||||
// request token fired, the client disconnected — do not pollute the
|
||||
// timeout log (reserved for genuine script-execution timeouts).
|
||||
@@ -382,7 +382,7 @@ public class InboundScriptExecutor
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Script execution failed for method {Method}", method.Name);
|
||||
// WP-5: Safe error message, no internal details
|
||||
// Safe error message, no internal details
|
||||
return new InboundScriptResult(false, null, "Internal script error");
|
||||
}
|
||||
finally
|
||||
|
||||
@@ -15,7 +15,7 @@ using ZB.MOM.WW.ScadaBridge.Commons.Types.Enums;
|
||||
namespace ZB.MOM.WW.ScadaBridge.InboundAPI.Middleware;
|
||||
|
||||
/// <summary>
|
||||
/// Audit Log #23 (M4 Bundle D, T7) — emits one <see cref="AuditChannel.ApiInbound"/>
|
||||
/// Emits one <see cref="AuditChannel.ApiInbound"/>
|
||||
/// row per inbound API request via <see cref="ICentralAuditWriter"/> covering the
|
||||
/// full set of response shapes:
|
||||
///
|
||||
@@ -39,7 +39,7 @@ namespace ZB.MOM.WW.ScadaBridge.InboundAPI.Middleware;
|
||||
/// for Bearer API-key callers), so the handler stashes the resolved API key name on
|
||||
/// <see cref="HttpContext.Items"/> under <see cref="AuditActorItemKey"/> after
|
||||
/// <c>IApiKeyVerifier.VerifyAsync</c> succeeds. The middleware reads it in
|
||||
/// its <c>finally</c> block. Phase 3: when no API-key name is stashed, the actor is
|
||||
/// its <c>finally</c> block. When no API-key name is stashed, the actor is
|
||||
/// sourced from the authenticated <em>interactive</em> principal via
|
||||
/// <see cref="IAuditActorAccessor"/> (a cookie/LDAP-authenticated inbound user,
|
||||
/// keyed off the canonical username claim). On auth failures (401/403) the actor is
|
||||
@@ -80,7 +80,7 @@ public sealed class AuditWriteMiddleware
|
||||
public const string AuditActorItemKey = "ZB.MOM.WW.ScadaBridge.InboundAPI.AuditActor";
|
||||
|
||||
/// <summary>
|
||||
/// Audit Log #23 (ParentExecutionId): <see cref="HttpContext.Items"/> key under
|
||||
/// ParentExecutionId: <see cref="HttpContext.Items"/> key under
|
||||
/// which this middleware stashes the inbound request's per-request
|
||||
/// <c>ExecutionId</c> (a <see cref="Guid"/>) at the very start of the request.
|
||||
/// The id is minted ONCE and shared: the endpoint handler reads it to thread it
|
||||
@@ -106,14 +106,14 @@ public sealed class AuditWriteMiddleware
|
||||
/// <param name="logger">Logger for this middleware.</param>
|
||||
/// <param name="options">Live-reloadable audit log options, read per-request.</param>
|
||||
/// <param name="actorAccessor">
|
||||
/// Phase 3 (optional): resolves the audit <see cref="AuditEvent.Actor"/> from the
|
||||
/// Resolves the audit <see cref="AuditEvent.Actor"/> from the
|
||||
/// authenticated principal on a cookie/LDAP-authenticated inbound request. Optional
|
||||
/// so existing tests (and any composition without the accessor registered) still
|
||||
/// construct the middleware; when absent, actor resolution falls back to the
|
||||
/// stashed API-key name only.
|
||||
/// </param>
|
||||
/// <param name="ceilingHitsCounter">
|
||||
/// M5.3 (T7, optional): incremented whenever an inbound request or response
|
||||
/// Incremented whenever an inbound request or response
|
||||
/// body is truncated at <see cref="AuditLogOptions.InboundMaxBytes"/>. Optional
|
||||
/// so existing tests and composition roots without the central health snapshot
|
||||
/// wired still construct without the counter; a NoOp is used when absent.
|
||||
@@ -149,7 +149,7 @@ public sealed class AuditWriteMiddleware
|
||||
var opts = _options.CurrentValue;
|
||||
var cap = opts.InboundMaxBytes;
|
||||
|
||||
// Audit Log #23 (ParentExecutionId): mint the inbound request's per-request
|
||||
// ParentExecutionId: mint the inbound request's per-request
|
||||
// ExecutionId ONCE, here at the start of the request, and stash it on
|
||||
// HttpContext.Items. Two consumers share this single id:
|
||||
// (a) the endpoint handler reads it to thread onto a routed
|
||||
@@ -166,7 +166,7 @@ public sealed class AuditWriteMiddleware
|
||||
// of the pipeline for us — but we also rewind to position 0 after our
|
||||
// own read so the very next reader starts from the top.
|
||||
//
|
||||
// InboundAPI-019: skip EnableBuffering for bodyless requests (a known
|
||||
// Skip EnableBuffering for bodyless requests (a known
|
||||
// empty Content-Length or a method that conventionally carries no body —
|
||||
// GET / HEAD / DELETE / TRACE / OPTIONS). The FileBufferingReadStream
|
||||
// wrapper EnableBuffering installs allocates an internal buffer regardless
|
||||
@@ -176,7 +176,7 @@ public sealed class AuditWriteMiddleware
|
||||
// returns (null, false) for the bodyless case anyway, so the audit row
|
||||
// is unchanged.
|
||||
//
|
||||
// M5.3 (T7): check if the matched method/target has SkipBodyCapture set.
|
||||
// Check if the matched method/target has SkipBodyCapture set.
|
||||
// The route value is resolved BEFORE the pipeline runs (route matching
|
||||
// has already bound {methodName} at this point), so we can skip the
|
||||
// EnableBuffering allocation and body read up front.
|
||||
@@ -224,7 +224,7 @@ public sealed class AuditWriteMiddleware
|
||||
// original sink; this just pulls back the bounded UTF-8 string.
|
||||
ctx.Response.Body = originalResponseBody;
|
||||
var (capturedResponseBody, capturedResponseTruncated) = captureStream.GetCapturedBody();
|
||||
// M5.3 (T7): if SkipBodyCapture is set, discard the captured response
|
||||
// If SkipBodyCapture is set, discard the captured response
|
||||
// body (the request body was never captured above). The row + headers
|
||||
// still emit with null RequestSummary / ResponseSummary.
|
||||
// Truncation flags are also cleared so ceiling-hit counter is not
|
||||
@@ -279,14 +279,14 @@ public sealed class AuditWriteMiddleware
|
||||
var actor = isAuthFailure ? null : ResolveActor(ctx);
|
||||
var methodName = ResolveMethodName(ctx);
|
||||
|
||||
// M5.3 (T7): increment the ceiling-hits counter once per request
|
||||
// Increment the ceiling-hits counter once per request
|
||||
// that hit the cap on EITHER the request or response body.
|
||||
if (requestTruncated || responseTruncated)
|
||||
{
|
||||
try { _ceilingHitsCounter.Increment(); } catch { /* swallow per §7 */ }
|
||||
}
|
||||
|
||||
// M5.3 (T7): capture request headers into Extra JSON alongside the
|
||||
// Capture request headers into Extra JSON alongside the
|
||||
// existing remoteIp / userAgent provenance fields. The header
|
||||
// collection is run through the SAME header-redaction list
|
||||
// (AuditLogOptions.HeaderRedactList) that the ScadaBridgeAuditRedactor
|
||||
@@ -325,7 +325,7 @@ public sealed class AuditWriteMiddleware
|
||||
occurredAtUtc: DateTime.UtcNow,
|
||||
actor: actor,
|
||||
target: methodName,
|
||||
// Audit Log #23: the per-request execution id minted ONCE at the
|
||||
// The per-request execution id minted ONCE at the
|
||||
// start of the request (InvokeAsync) and stashed on
|
||||
// HttpContext.Items. The same id is threaded onto a routed
|
||||
// RouteToCallRequest.ParentExecutionId by the endpoint handler,
|
||||
@@ -347,7 +347,7 @@ public sealed class AuditWriteMiddleware
|
||||
// Central direct-write — no site-local forwarding state (not a
|
||||
// canonical field).
|
||||
|
||||
// InboundAPI-018: fire-and-forget the writer so the user-facing
|
||||
// Fire-and-forget the writer so the user-facing
|
||||
// response stays non-blocking (alog.md §13 — audit emission must
|
||||
// NEVER abort or delay the user request), but observe the returned
|
||||
// Task so an asynchronous fault is logged instead of vanishing into
|
||||
@@ -368,7 +368,7 @@ public sealed class AuditWriteMiddleware
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// InboundAPI-018: observe the audit writer's returned <see cref="Task"/>
|
||||
/// Observe the audit writer's returned <see cref="Task"/>
|
||||
/// so a fault that surfaces ASYNCHRONOUSLY (e.g. a DB timeout deep in the
|
||||
/// central audit pipeline) is logged at Warning rather than dropped into
|
||||
/// <see cref="TaskScheduler.UnobservedTaskException"/>. Stays
|
||||
@@ -398,7 +398,7 @@ public sealed class AuditWriteMiddleware
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// InboundAPI-019: decides whether the request is likely to carry a body, so the
|
||||
/// Decides whether the request is likely to carry a body, so the
|
||||
/// caller can skip <see cref="HttpRequestRewindExtensions.EnableBuffering(HttpRequest)"/>
|
||||
/// (and the associated <c>FileBufferingReadStream</c> allocation) on requests that
|
||||
/// definitely won't have one. Returns <c>true</c> when <see cref="HttpRequest.ContentLength"/>
|
||||
@@ -533,7 +533,7 @@ public sealed class AuditWriteMiddleware
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Audit Log #23 (ParentExecutionId): reads the inbound request's per-request
|
||||
/// ParentExecutionId: reads the inbound request's per-request
|
||||
/// <c>ExecutionId</c> that <see cref="InvokeAsync"/> minted and stashed on
|
||||
/// <see cref="HttpContext.Items"/> under <see cref="InboundExecutionIdItemKey"/>.
|
||||
/// Throws <see cref="InvalidOperationException"/> if the slot is absent — for a
|
||||
@@ -563,7 +563,7 @@ public sealed class AuditWriteMiddleware
|
||||
/// <see cref="HttpContext.Items"/> after successful key auth (the
|
||||
/// key-authenticated path — the canonical identity of an API-key caller);</description></item>
|
||||
/// <item><description>otherwise the authenticated <em>interactive</em> principal
|
||||
/// resolved through <see cref="IAuditActorAccessor"/> (Phase 3 — a
|
||||
/// resolved through <see cref="IAuditActorAccessor"/> (a
|
||||
/// cookie/LDAP-authenticated inbound user, sourced from the canonical username
|
||||
/// claim). The accessor reads the ambient <see cref="HttpContext.User"/>, so the
|
||||
/// fall-through here only fires when no API-key name was stashed;</description></item>
|
||||
@@ -571,7 +571,7 @@ public sealed class AuditWriteMiddleware
|
||||
/// principal back as an actor.</description></item>
|
||||
/// </list>
|
||||
/// The accessor is optional (constructor default <c>null</c>); when absent only
|
||||
/// the stashed API-key name is consulted, preserving the pre-Phase-3 behaviour.
|
||||
/// the stashed API-key name is consulted, preserving the previous behaviour.
|
||||
/// </summary>
|
||||
private string? ResolveActor(HttpContext ctx)
|
||||
{
|
||||
@@ -582,7 +582,7 @@ public sealed class AuditWriteMiddleware
|
||||
return name;
|
||||
}
|
||||
|
||||
// Phase 3: an interactive cookie/LDAP-authenticated inbound user records
|
||||
// An interactive cookie/LDAP-authenticated inbound user records
|
||||
// their real identity as Actor. Returns null for the key-authenticated
|
||||
// and auth-failure paths (no authenticated interactive principal), so the
|
||||
// existing API-key/auth-failure behaviour is preserved.
|
||||
|
||||
@@ -4,12 +4,12 @@ using ZB.MOM.WW.ScadaBridge.Commons.Types.InboundApi;
|
||||
namespace ZB.MOM.WW.ScadaBridge.InboundAPI;
|
||||
|
||||
/// <summary>
|
||||
/// WP-2: Validates and deserializes a JSON request body against a method's
|
||||
/// Validates and deserializes a JSON request body against a method's
|
||||
/// parameter definitions. Extended type system: Boolean, Integer, Float,
|
||||
/// String, Object, List.
|
||||
///
|
||||
/// <para>
|
||||
/// InboundAPI-M2.6: validation is now RECURSIVE and type-aware for the
|
||||
/// Validation is now RECURSIVE and type-aware for the
|
||||
/// extended <c>Object</c> / <c>List</c> types. Declared object fields are
|
||||
/// validated against their declared (nested) types, list elements against the
|
||||
/// declared element type, and scalars at any depth against the extended type —
|
||||
@@ -31,7 +31,7 @@ public static class ParameterValidator
|
||||
/// <param name="body">The parsed JSON request body; null or undefined if no body was supplied.</param>
|
||||
/// <param name="parameterDefinitions">JSON Schema describing the method's parameters (an object schema), or null/empty when no parameters are defined. The legacy flat-array form is also accepted.</param>
|
||||
/// <param name="resolveRef">
|
||||
/// M9-T32b: optional JSON-Schema <c>$ref</c> resolution seam mapping a
|
||||
/// Optional JSON-Schema <c>$ref</c> resolution seam mapping a
|
||||
/// <c>{"$ref":"lib:Name"}</c> reference target's name to the referenced schema JSON
|
||||
/// (or <c>null</c> when the library entry does not exist). The endpoint pre-loads the
|
||||
/// shared-schema library (backed by <c>ISharedSchemaRepository</c>) and supplies it
|
||||
@@ -46,7 +46,7 @@ public static class ParameterValidator
|
||||
string? parameterDefinitions,
|
||||
Func<string, string?>? resolveRef = null)
|
||||
{
|
||||
// M9-T32b: parse through the ref-COLLECTING path. A {"$ref":"lib:Name"} that the
|
||||
// Parse through the ref-COLLECTING path. A {"$ref":"lib:Name"} that the
|
||||
// resolver can satisfy is resolved inline; a dangling/cyclic/over-depth ref is
|
||||
// collected (not thrown) so the runtime returns a descriptive "could not be
|
||||
// resolved" message instead of an opaque "Invalid parameter definitions" — the
|
||||
@@ -119,7 +119,7 @@ public static class ParameterValidator
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// M9-T32b: renders the unresolved <c>{"$ref":"lib:Name"}</c> references for a clear,
|
||||
/// Renders the unresolved <c>{"$ref":"lib:Name"}</c> references for a clear,
|
||||
/// descriptive runtime error — the bare <c>lib:</c>-qualified pointer name stays
|
||||
/// separate from any parenthesised reason (cyclic/over-depth), so the message reads
|
||||
/// e.g. <c>schema(s) 'lib:Foo' (cyclic reference)</c> rather than embedding the
|
||||
@@ -137,7 +137,7 @@ public static class ParameterValidator
|
||||
/// arrays to <see cref="List{T}"/>.
|
||||
///
|
||||
/// <para>
|
||||
/// InboundAPI-#55: coercion is RECURSIVE. Nested object fields and list
|
||||
/// Coercion is RECURSIVE. Nested object fields and list
|
||||
/// elements are coerced to typed CLR values too (string/long/double/bool,
|
||||
/// or nested Dictionary/List), so NO raw <see cref="JsonElement"/> survives
|
||||
/// anywhere in the returned graph. Previously the <c>object</c> case used
|
||||
@@ -254,7 +254,7 @@ public static class ParameterValidator
|
||||
{
|
||||
// Declared object/array element: recurse via Materialize so each
|
||||
// element follows its declared (nested) schema — nested scalars
|
||||
// become typed CLR values, never raw JsonElement (#55).
|
||||
// become typed CLR values, never raw JsonElement.
|
||||
var list = new List<object?>(element.GetArrayLength());
|
||||
foreach (var e in element.EnumerateArray()) list.Add(Materialize(e, items));
|
||||
return list;
|
||||
|
||||
@@ -4,7 +4,7 @@ using ZB.MOM.WW.ScadaBridge.Commons.Types.InboundApi;
|
||||
namespace ZB.MOM.WW.ScadaBridge.InboundAPI;
|
||||
|
||||
/// <summary>
|
||||
/// InboundAPI-014: validates a method script's return value against the method's
|
||||
/// Validates a method script's return value against the method's
|
||||
/// declared <c>ReturnDefinition</c>. <c>Component-InboundAPI.md</c> ("Return Value
|
||||
/// Definition" / "Response Format") states the success body has "fields matching
|
||||
/// the return value definition"; this is the response-side mirror of
|
||||
@@ -18,7 +18,7 @@ namespace ZB.MOM.WW.ScadaBridge.InboundAPI;
|
||||
/// </para>
|
||||
///
|
||||
/// <para>
|
||||
/// InboundAPI-M2.6: validation is RECURSIVE and type-aware — declared object
|
||||
/// Validation is RECURSIVE and type-aware — declared object
|
||||
/// fields are validated against their declared (nested) types, list elements
|
||||
/// against the declared element type, and scalars at any depth — with
|
||||
/// path-qualified errors. The recursion is shared with
|
||||
@@ -37,7 +37,7 @@ public static class ReturnValueValidator
|
||||
/// <param name="resultJson">The JSON-serialized script return value to validate.</param>
|
||||
/// <param name="returnDefinition">JSON Schema describing the method's return value, or null/empty to skip validation. The legacy flat-array form is also accepted.</param>
|
||||
/// <param name="resolveRef">
|
||||
/// M9-T32b: optional JSON-Schema <c>$ref</c> resolution seam mapping a
|
||||
/// Optional JSON-Schema <c>$ref</c> resolution seam mapping a
|
||||
/// <c>{"$ref":"lib:Name"}</c> reference target's name to the referenced schema JSON
|
||||
/// (or <c>null</c> when the library entry does not exist). The executor pre-loads the
|
||||
/// shared-schema library (backed by <c>ISharedSchemaRepository</c>) and supplies it
|
||||
@@ -58,7 +58,7 @@ public static class ReturnValueValidator
|
||||
return ReturnValidationResult.Valid();
|
||||
}
|
||||
|
||||
// M9-T32b: parse through the ref-COLLECTING path so a {"$ref":"lib:Name"} the
|
||||
// Parse through the ref-COLLECTING path so a {"$ref":"lib:Name"} the
|
||||
// resolver can satisfy is resolved inline, and a dangling/cyclic/over-depth ref is
|
||||
// surfaced as a descriptive "could not be resolved" message rather than an opaque
|
||||
// "Invalid return definition" from a swallowed JsonException.
|
||||
|
||||
@@ -5,11 +5,11 @@ using ZB.MOM.WW.ScadaBridge.Commons.Types;
|
||||
namespace ZB.MOM.WW.ScadaBridge.InboundAPI;
|
||||
|
||||
/// <summary>
|
||||
/// WP-4: Route.To() helper for cross-site calls from inbound API scripts.
|
||||
/// Route.To() helper for cross-site calls from inbound API scripts.
|
||||
/// Resolves instance to site, routes via <see cref="IInstanceRouter"/>, blocks until
|
||||
/// response or timeout. Site unreachable returns error (no store-and-forward).
|
||||
///
|
||||
/// InboundAPI-016: the helper carries the executing method's <see cref="CancellationToken"/>
|
||||
/// The helper carries the executing method's <see cref="CancellationToken"/>
|
||||
/// (the method-level timeout). Routed calls inherit that deadline by default, so a
|
||||
/// natural script — <c>Route.To("inst").Call("doWork", p)</c> — is timeout-bounded
|
||||
/// without the script having to thread a token explicitly.
|
||||
@@ -49,7 +49,7 @@ public class RouteHelper
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// InboundAPI-016: returns a <see cref="RouteHelper"/> whose routed calls inherit
|
||||
/// Returns a <see cref="RouteHelper"/> whose routed calls inherit
|
||||
/// <paramref name="deadlineToken"/> (the executing method's timeout) by default.
|
||||
/// <see cref="InboundScriptExecutor"/> calls this when it builds the script
|
||||
/// context so the method timeout actually covers routed calls, as the design doc
|
||||
@@ -61,7 +61,7 @@ public class RouteHelper
|
||||
new(_instanceLocator, _instanceRouter, deadlineToken, _requestAbortedToken, _parentExecutionId);
|
||||
|
||||
/// <summary>
|
||||
/// InboundAPI-029: returns a <see cref="RouteHelper"/> carrying the raw request-abort
|
||||
/// 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
|
||||
@@ -74,7 +74,7 @@ public class RouteHelper
|
||||
new(_instanceLocator, _instanceRouter, _deadlineToken, requestAbortedToken, _parentExecutionId);
|
||||
|
||||
/// <summary>
|
||||
/// Audit Log #23 (ParentExecutionId): returns a <see cref="RouteHelper"/> whose
|
||||
/// Audit Log (ParentExecutionId): returns a <see cref="RouteHelper"/> whose
|
||||
/// routed <see cref="RouteTarget.Call"/> requests carry
|
||||
/// <paramref name="parentExecutionId"/> as <see cref="RouteToCallRequest.ParentExecutionId"/>.
|
||||
/// For an inbound API request this is the inbound request's own per-request
|
||||
@@ -100,7 +100,7 @@ public class RouteHelper
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// WP-4: Represents a route target (an instance) for cross-site calls.
|
||||
/// Represents a route target (an instance) for cross-site calls.
|
||||
/// </summary>
|
||||
public class RouteTarget
|
||||
{
|
||||
@@ -118,7 +118,7 @@ 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="requestAbortedToken">Raw client-disconnect token, independent of the method timeout.</param>
|
||||
/// <param name="parentExecutionId">Optional parent execution id for audit correlation on routed calls.</param>
|
||||
internal RouteTarget(
|
||||
string instanceCode,
|
||||
@@ -141,7 +141,7 @@ public class RouteTarget
|
||||
/// perspective. <paramref name="parameters"/> may be a dictionary or an
|
||||
/// anonymous object (<c>new { name = "Bob" }</c>) — see <see cref="ScriptArgs"/>.
|
||||
///
|
||||
/// InboundAPI-016: when <paramref name="cancellationToken"/> is not supplied the
|
||||
/// When <paramref name="cancellationToken"/> is not supplied the
|
||||
/// routed call inherits the executing method's timeout, so the call is bounded by
|
||||
/// the method-level deadline with no token argument.
|
||||
/// </summary>
|
||||
@@ -158,7 +158,7 @@ public class RouteTarget
|
||||
var siteId = await ResolveSiteAsync(token);
|
||||
var correlationId = Guid.NewGuid().ToString();
|
||||
|
||||
// Audit Log #23 (ParentExecutionId): stamp the spawning execution's id
|
||||
// Audit Log (ParentExecutionId): stamp the spawning execution's id
|
||||
// (the inbound API request's ExecutionId) so the routed site script
|
||||
// records this call's parent. CorrelationId above is a separate concern
|
||||
// — the per-operation lifecycle id, freshly minted per routed call.
|
||||
@@ -205,7 +205,7 @@ public class RouteTarget
|
||||
var siteId = await ResolveSiteAsync(token);
|
||||
var correlationId = Guid.NewGuid().ToString();
|
||||
|
||||
// Audit Log #23 (ParentExecutionId): mirrors the Call path — stamp the
|
||||
// Audit Log (ParentExecutionId): mirrors the Call path — stamp the
|
||||
// spawning inbound request's ExecutionId so future site-side audit
|
||||
// emission for routed reads can record this read's parent. Symmetric
|
||||
// with RouteToCallRequest so script authors get the same correlation
|
||||
@@ -233,8 +233,8 @@ public class RouteTarget
|
||||
/// 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
|
||||
/// Unlike the other routed calls (which inherit the method deadline
|
||||
/// by default), 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
|
||||
@@ -254,12 +254,12 @@ public class RouteTarget
|
||||
TimeSpan timeout,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
// InboundAPI-031: do NOT impose a local wait-timeout backstop here. The site
|
||||
// Do NOT impose a local wait-timeout backstop here. The site
|
||||
// enforces the wait `timeout` and returns Matched=false when it elapses, and the
|
||||
// cluster Ask in CommunicationService.RouteToWaitForAttributeAsync already bounds
|
||||
// the round trip by `timeout + IntegrationTimeout` — the authoritative backstop
|
||||
// for a missing site response. A local CTS of `timeout + small grace` (the prior
|
||||
// InboundAPI-029 approach) was TIGHTER than that round-trip budget, so a
|
||||
// approach) was TIGHTER than that round-trip budget, so a
|
||||
// slow-but-valid timed-out response could be cancelled into an exception instead
|
||||
// of the spec-mandated `false`. Link ONLY the client-disconnect token and an
|
||||
// explicit caller token — NOT the method deadline — so a client abort still
|
||||
@@ -269,7 +269,7 @@ public class RouteTarget
|
||||
var token = linked.Token;
|
||||
var siteId = await ResolveSiteAsync(token);
|
||||
|
||||
// Audit Log #23 (ParentExecutionId): mirrors the Call path — stamp the
|
||||
// Audit Log (ParentExecutionId): mirrors the Call path — stamp the
|
||||
// spawning inbound request's ExecutionId so future site-side audit
|
||||
// emission for routed waits can record this wait's parent. CorrelationId
|
||||
// is the per-operation lifecycle id, freshly minted per routed wait.
|
||||
@@ -320,7 +320,7 @@ public class RouteTarget
|
||||
var siteId = await ResolveSiteAsync(token);
|
||||
var correlationId = Guid.NewGuid().ToString();
|
||||
|
||||
// Audit Log #23 (ParentExecutionId): mirrors the Call path — stamp the
|
||||
// Audit Log (ParentExecutionId): mirrors the Call path — stamp the
|
||||
// spawning inbound request's ExecutionId so future site-side audit
|
||||
// emission for routed writes can record this write's parent. Symmetric
|
||||
// with RouteToCallRequest so script authors get the same correlation
|
||||
@@ -339,7 +339,7 @@ public class RouteTarget
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// InboundAPI-016: a routed call with no explicit token inherits the executing
|
||||
/// A routed call with no explicit token inherits the executing
|
||||
/// method's deadline. An explicitly supplied token (for a tighter bound) wins.
|
||||
/// </summary>
|
||||
private CancellationToken Effective(CancellationToken explicitToken) =>
|
||||
|
||||
@@ -4,7 +4,7 @@ using ZB.MOM.WW.ScadaBridge.Commons.Types.InboundApi;
|
||||
namespace ZB.MOM.WW.ScadaBridge.InboundAPI;
|
||||
|
||||
/// <summary>
|
||||
/// M9-T32b: builds the synchronous JSON-Schema <c>$ref</c> resolution seam the
|
||||
/// Builds the synchronous JSON-Schema <c>$ref</c> resolution seam the
|
||||
/// InboundAPI RUNTIME validators (<see cref="ParameterValidator"/> /
|
||||
/// <see cref="ReturnValueValidator"/>) consume, backed by the central shared-schema
|
||||
/// library (<see cref="ISharedSchemaRepository"/>).
|
||||
|
||||
@@ -11,7 +11,7 @@ public static class ServiceCollectionExtensions
|
||||
/// <returns>The same <see cref="IServiceCollection"/> to allow chaining.</returns>
|
||||
public static IServiceCollection AddInboundAPI(this IServiceCollection services)
|
||||
{
|
||||
// Auth re-arch (C5): inbound authentication is handled by the shared
|
||||
// Inbound authentication is handled by the shared
|
||||
// ZB.MOM.WW.Auth.ApiKeys verifier (Bearer sbk_<keyId>_<secret>), registered by
|
||||
// AddZbApiKeyAuth in the Host composition root. The legacy ApiKeyValidator +
|
||||
// peppered IApiKeyHasher and the SQL Server ApiKey store were retired, so this
|
||||
@@ -19,11 +19,11 @@ public static class ServiceCollectionExtensions
|
||||
services.AddSingleton<InboundScriptExecutor>();
|
||||
services.AddScoped<RouteHelper>();
|
||||
|
||||
// InboundAPI-017: routed calls go through the IInstanceRouter seam; the
|
||||
// Routed calls go through the IInstanceRouter seam; the
|
||||
// production implementation delegates to CommunicationService.
|
||||
services.AddScoped<IInstanceRouter, CommunicationServiceInstanceRouter>();
|
||||
|
||||
// InboundAPI-006 / InboundAPI-008: endpoint filter enforcing the request
|
||||
// Endpoint filter enforcing the request
|
||||
// body size cap and active-node gating for POST /api/{methodName}.
|
||||
services.AddSingleton<InboundApiEndpointFilter>();
|
||||
|
||||
|
||||
Reference in New Issue
Block a user