fix(inbound-api): Flow Debugger method-entry Live run enforces the operator's site scope at route time (arch-review F2)
A method-entry Live run had no site-scope check at all. RunValueChangeAsync calls EnforceSiteScope on its named instance, but a method run has no single target site up front — the operator picks a METHOD, and where its script routes is only known once it runs — so a site-scoped Deployer could reach any site the method happened to route to. The scope is therefore pushed into RouteHelper and enforced at route time: - RouteHelper.WithSiteScope(IReadOnlySet<string>?) carries a set of site IDENTIFIERS (null = unscoped, the state of every ordinary inbound HTTP request; an EMPTY set permits nothing, the fail-closed outcome for a scoped user whose sites no longer exist). Threaded through every With* copy and To(). - The gate lives in RouteTarget.ResolveSiteAsync — the single choke point that Call, GetAttributes, WaitForAttribute and SetAttributes all pass through — so it fires BEFORE any site traffic and throws the new RouteSiteScopeException. - InboundScriptExecutor maps it to error code SITE_SCOPE_DENIED. The type is InboundAPI's own (that assembly cannot reference ManagementService), and it derives from InvalidOperationException so base-type callers are unaffected. - FlowDebugRunner.RunMethodAsync resolves the user's PermittedSiteIds (numeric) to identifiers via ISiteRepository and converts SITE_SCOPE_DENIED back to SiteScopeViolationException (-> ManagementUnauthorized), AFTER writing the inbound audit row so a denied attempt is still recorded against the operator. Administrators and unscoped users get a null scope and are unaffected, exactly as EnforceSiteScope treats them. Every contract change is a trailing optional parameter. Mutation-checked: disabling the gate fails 3 of the 5 new RouteHelper tests and 1 of the runner tests, while the in-scope/unscoped positives still pass. Claude-Session: https://claude.ai/code/session_01NcMG6xXdsJbmD2wYAgkK29
This commit is contained in:
@@ -506,6 +506,23 @@ public class InboundScriptExecutor
|
||||
_logger.LogDebug("Inbound API request for method {Method} cancelled by client", method.Name);
|
||||
return new InboundScriptResult(false, null, "Request cancelled by client");
|
||||
}
|
||||
catch (RouteSiteScopeException ex)
|
||||
{
|
||||
// Arch-review F2: the script routed to a site outside the caller's site
|
||||
// scope. RouteTarget.ResolveSiteAsync refused BEFORE any site traffic, so
|
||||
// the script never saw a routed reply — there is nothing to roll back.
|
||||
// The message is the operator-facing denial, safe to surface verbatim
|
||||
// (it names only the instance and its site), unlike the generic
|
||||
// SCRIPT_ERROR path which deliberately hides internals.
|
||||
//
|
||||
// Ordered before the SiteUnreachableException catch purely for
|
||||
// readability: the two types are siblings under InvalidOperationException,
|
||||
// so neither shadows the other.
|
||||
_logger.LogWarning(
|
||||
"API method {Method} refused a routed call outside the caller's site scope: {Message}",
|
||||
method.Name, ex.Message);
|
||||
return new InboundScriptResult(false, null, ex.Message, "SITE_SCOPE_DENIED");
|
||||
}
|
||||
catch (SiteUnreachableException)
|
||||
{
|
||||
// C3: a routed Route.To(...).Call(...) reached a site that could not be
|
||||
|
||||
Reference in New Issue
Block a user