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

Class diagram orchestrata_ultimate.gateway.handlers._scope_guard

Dependency diagram

Dependency diagram orchestrata_ultimate.gateway.handlers._scope_guard

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

Permalink: https://gitlab.com/MSamy/orchestrata-ultimate-architecture/-/blob/a4cbc2bdf/src/orchestrata_ultimate/gateway/handlers/_scope_guard.py

"""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"