From 16f8cfd97209b847644d00ef14d62d4a15d19e7e Mon Sep 17 00:00:00 2001 From: cybermaggedon Date: Thu, 25 Jun 2026 13:44:57 +0100 Subject: [PATCH] fix: use envelope workspace for mux authorisation, not inner request body (#1000) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The mux was extracting the authorisation resource workspace from the inner request body via registry extractors. But workspace-scoped services (config, flow, librarian, etc.) receive workspace from the queue identity, not the message body — the inner workspace field is a dead field that no service handler reads. This caused access-denied errors when the inner body's workspace (e.g. CLI default "default") disagreed with the caller's assigned workspace, even though the envelope workspace was correct. Fix: resolve workspace from the envelope only. Split the non-flow authorisation path by resource level — WORKSPACE ops use the envelope workspace directly; SYSTEM ops (IAM) still use registry extractors since they legitimately read operation-specific body fields. --- .../trustgraph/gateway/dispatch/mux.py | 26 +++++++++++++------ 1 file changed, 18 insertions(+), 8 deletions(-) diff --git a/trustgraph-flow/trustgraph/gateway/dispatch/mux.py b/trustgraph-flow/trustgraph/gateway/dispatch/mux.py index 9b119f8e..0093c563 100644 --- a/trustgraph-flow/trustgraph/gateway/dispatch/mux.py +++ b/trustgraph-flow/trustgraph/gateway/dispatch/mux.py @@ -5,6 +5,7 @@ import uuid import logging from ..capabilities import PUBLIC, AUTHENTICATED +from ..registry import ResourceLevel # Module logger logger = logging.getLogger(__name__) @@ -159,12 +160,14 @@ class Mux: return # Resolve workspace (default-fill from the caller's - # bound workspace). Workspace resolution applies to all - # operations regardless of capability level. + # bound workspace). The envelope workspace is the + # single canonical workspace for routing AND + # authorisation. The inner request body's workspace + # field is not consulted — workspace-scoped services + # receive workspace from the queue identity, not the + # message body. try: await enforce_workspace(data, self.identity, self.auth) - if isinstance(inner, dict): - await enforce_workspace(inner, self.identity, self.auth) # Authorisation: capability sentinels short-circuit # the regime call; capability strings go through @@ -176,11 +179,18 @@ class Mux: "flow": data.get("flow", ""), } parameters = {} + elif op.resource_level == ResourceLevel.WORKSPACE: + # Workspace-scoped services (config, flow, + # librarian, etc.) — workspace comes from the + # envelope, same as flow-level services. + resource = { + "workspace": data.get("workspace", ""), + } + parameters = {} else: - # Build a minimal RequestContext so the matched - # operation's own extractors decide resource - # and parameters — same path the HTTP - # endpoints take. + # System-level services (IAM) — resource is + # {} and parameters come from the inner body + # (e.g. user.workspace, workspace_record.id). from ..registry import RequestContext ctx = RequestContext( body=inner if isinstance(inner, dict) else {},