orchestrata_ultimate.gateway.handlers._scope_guard
Reference › orchestrata_ultimate › gateway › handlers › _scope_guard
ScopeGuard centralises the read_only versus read_write check that the workflow and config RPCs attach to their handlers (SPEC-014 US-014-005). It is built from the connection manager in session and used by the config and workflow handlers; rpc creates it. require compares the scope returned by granted_scope with the required one using a rank table and raises RPCError with code -32604 if the granted scope ranks lower. Both methods run inside a tracing span. Per-token scopes are listed in the code as a named follow-up. See gateway.
At a glance
| Source | src/orchestrata_ultimate/gateway/handlers/_scope_guard.py |
| Lines of code | 88 |
| Classes | 1 |
| Functions | 0 |
| Imports (internal) | orchestrata_ultimate.core.base.traces, orchestrata_ultimate.core.tracing.tracer, orchestrata_ultimate.gateway.exceptions, orchestrata_ultimate.gateway.session |
| Imported by | orchestrata_ultimate.gateway.handlers.config, orchestrata_ultimate.gateway.handlers.workflow, orchestrata_ultimate.gateway.rpc |
| Tests that import it | tests/gateway/handlers/test_scope_guard.py, tests/generated_gateway/test_handlers__scope_guard_generated.py |
Class diagram
Dependency diagram
API
Constants
| Name | Type | Value |
|---|---|---|
_SCOPE_RANK |
dict[str, int] |
{'read_only': 0, 'read_write': 1} |
Classes
ScopeGuard
Line 22
Centralizes the read_only/read_write scope check every workflow/config RPC attaches to its handler.
| Method | Signature | Summary |
|---|---|---|
__init__ |
__init__(connections: ConnectionManager, tracer: Tracer | None = None) -> None |
Wire the guard to the gateway's connection registry. |
require |
async require(connection_id: str, scope: str) -> None |
Raise unless connection_id's granted scope covers scope. |
granted_scope |
async granted_scope(connection_id: str) -> str |
The scope granted to connection_id. |
Source
"""Per-connection scope enforcement for workflow/config RPCs (SPEC-014
REQ-014-076/080/081).
The resurrected REQs attach a scope to every method (``read_write`` for
workflow.*, ``read_only`` for config.get/watch) but no gateway handler
enforces scope today: ``AuthHandler`` grants a flat ``read_write`` to every
authenticated connection (handlers/auth.py) and ``ConnectionInfo`` carries
no scope field. ``ScopeGuard`` is the single seam where real per-token
scopes land later (D8) -- until then it mirrors that same flat grant.
"""
from orchestrata_ultimate.core.base.traces import traces
from orchestrata_ultimate.core.tracing.tracer import Tracer
from orchestrata_ultimate.gateway.exceptions import RPCError
from orchestrata_ultimate.gateway.session import ConnectionManager
# read_write implies read_only; used by require() to check the granted
# scope covers the requested one.
_SCOPE_RANK: dict[str, int] = {"read_only": 0, "read_write": 1}
class ScopeGuard:
"""Centralizes the read_only/read_write scope check every workflow/config
RPC attaches to its handler."""
def __init__(
self,
connections: ConnectionManager,
tracer: Tracer | None = None,
) -> None:
"""Wire the guard to the gateway's connection registry.
Args:
connections: Connection lifecycle manager.
tracer: Injected debug tracer; defaults to the process Singleton.
"""
self._connections = connections
self._tracer = tracer or Tracer.default()
@traces(
spec="014",
story="US-014-005",
reqs=["REQ-014-076", "REQ-014-080", "REQ-014-081"],
)
async def require(self, connection_id: str, scope: str) -> None:
"""Raise unless *connection_id*'s granted scope covers *scope*.
Args:
connection_id: Caller connection ID.
scope: Required scope (``"read_only"`` or ``"read_write"``).
Raises:
RPCError: Insufficient scope (-32604).
"""
with self._tracer.span("ScopeGuard.require"):
granted = await self.granted_scope(connection_id)
if _SCOPE_RANK.get(granted, 0) < _SCOPE_RANK.get(scope, 0):
raise RPCError(
f"insufficient scope for {connection_id}: "
f"{scope} required, {granted} granted",
rpc_code=-32604,
)
@traces(
spec="014",
story="US-014-005",
reqs=["REQ-014-076", "REQ-014-080", "REQ-014-081"],
)
async def granted_scope(self, connection_id: str) -> str:
"""The scope granted to *connection_id*.
``AuthHandler`` flatly grants ``read_write`` to every authenticated
caller today -- no per-connection scope storage exists yet -- so
this always returns ``read_write`` regardless of whether the
connection is even registered, matching the RPC dispatcher's
existing posture of accepting ad-hoc connection ids without prior
``ConnectionManager.register`` (see
tests/gateway/test_rpc_behavioral.py). Real per-token scopes are a
named follow-up (D8), not smuggled in here.
Args:
connection_id: Caller connection ID.
Returns:
The granted scope string.
"""
await self._connections.get(connection_id)
return "read_write"