Skip to content

symfonic.agent.fastapi.dependencies

dependencies

FastAPI dependencies for tenant scope extraction.

Provides get_tenant_scope as a FastAPI Depends function that extracts multi-tenant context from HTTP headers.

Security model

X-Tenant-ID on its own is untrusted input. The core library cannot assume a specific auth model (JWT, API key, mTLS, ...), so it exposes a pluggable async verifier that downstream apps register at startup to enforce "this caller is allowed to access this tenant".

Typical scaffolded wiring (see symfonic.cli.templates/app/main.py.j2):

.. code-block:: python

from symfonic.agent.fastapi.dependencies import set_tenant_auth_verifier

async def verify_tenant_access(request, tenant_id):
    # decode JWT, look up user, confirm membership in tenant_id
    ...

set_tenant_auth_verifier(verify_tenant_access)

When no verifier is registered the dependency logs a WARNING once and accepts X-Tenant-ID on trust -- suitable for tests / dev only, never production.

TenantAuthVerifier module-attribute

TenantAuthVerifier = Callable[
    [Request, str], Awaitable[None]
]

Signature: async def verifier(request, tenant_id) -> None.

Implementations should raise fastapi.HTTPException (401 / 403) to deny the request. Returning None is an explicit allow.

get_tenant_auth_verifier

get_tenant_auth_verifier() -> TenantAuthVerifier | None

Return the currently registered verifier, if any.

Exposed primarily for diagnostics / tests.

Source code in src/symfonic/agent/fastapi/dependencies.py
def get_tenant_auth_verifier() -> TenantAuthVerifier | None:
    """Return the currently registered verifier, if any.

    Exposed primarily for diagnostics / tests.
    """
    return _tenant_auth_verifier

get_tenant_scope async

get_tenant_scope(request: Request) -> FrameworkTenantScope

Extract a FrameworkTenantScope from request headers.

Required headers

X-Tenant-ID: Unique tenant identifier (required).

Optional headers

X-Sub-Tenant-ID: Sub-tenant identifier. X-Namespace: Memory namespace.

Security

If a tenant-auth verifier was registered via set_tenant_auth_verifier it is invoked with the request and the parsed tenant id. The verifier is expected to confirm the caller is authenticated AND authorised for the requested tenant, raising HTTPException(401/403) on denial.

When no verifier is registered a one-shot WARNING is logged and the tenant id is accepted on trust -- acceptable for unit tests and local dev, never for production.

Raises:

Type Description
HTTPException 401

Missing X-Tenant-ID header, missing auth, or invalid credentials (propagated from the verifier).

HTTPException 403

Caller is authenticated but not a member of the requested tenant (propagated from the verifier).

HTTPException 400

Invalid scope (e.g. divergent sub_tenant_id/namespace).

Source code in src/symfonic/agent/fastapi/dependencies.py
async def get_tenant_scope(request: Request) -> FrameworkTenantScope:
    """Extract a FrameworkTenantScope from request headers.

    Required headers:
        X-Tenant-ID: Unique tenant identifier (required).

    Optional headers:
        X-Sub-Tenant-ID: Sub-tenant identifier.
        X-Namespace: Memory namespace.

    Security:
        If a tenant-auth verifier was registered via
        ``set_tenant_auth_verifier`` it is invoked with the request and
        the parsed tenant id.  The verifier is expected to confirm the
        caller is authenticated AND authorised for the requested tenant,
        raising ``HTTPException(401/403)`` on denial.

        When no verifier is registered a one-shot WARNING is logged and
        the tenant id is accepted on trust -- acceptable for unit tests
        and local dev, never for production.

    Raises:
        HTTPException 401: Missing X-Tenant-ID header, missing auth, or
            invalid credentials (propagated from the verifier).
        HTTPException 403: Caller is authenticated but not a member of
            the requested tenant (propagated from the verifier).
        HTTPException 400: Invalid scope (e.g. divergent
            sub_tenant_id/namespace).
    """
    tenant_id = request.headers.get("X-Tenant-ID")
    if not tenant_id:
        raise HTTPException(
            status_code=401,
            detail="Missing required X-Tenant-ID header",
        )

    # Enforce the pluggable auth hook *before* building the scope so
    # unauthorised callers never reach the memory orchestrator.
    global _missing_verifier_warning_emitted
    if _tenant_auth_verifier is not None:
        await _tenant_auth_verifier(request, tenant_id)
    elif not _missing_verifier_warning_emitted:
        logger.warning(
            "No tenant auth verifier registered — X-Tenant-ID is being "
            "trusted unconditionally. Call "
            "symfonic.agent.fastapi.dependencies.set_tenant_auth_verifier() "
            "at app startup to enforce authentication + tenant membership.",
        )
        _missing_verifier_warning_emitted = True

    sub_tenant_id = request.headers.get("X-Sub-Tenant-ID") or None
    namespace = request.headers.get("X-Namespace") or None

    try:
        return FrameworkTenantScope(
            tenant_id=tenant_id,
            sub_tenant_id=sub_tenant_id,
            namespace=namespace,
        )
    except (ScopeValidationError, ValueError) as exc:
        raise HTTPException(status_code=400, detail=str(exc)) from exc

set_tenant_auth_verifier

set_tenant_auth_verifier(
    verifier: TenantAuthVerifier | None,
) -> None

Register (or clear) the tenant-access verifier.

Call this once at app startup, before mounting the agent router. Passing None unregisters the current verifier (useful for tests).

The verifier receives the raw Request plus the parsed tenant_id from the X-Tenant-ID header and must raise an HTTPException to deny access. Any other exception is treated as a 500 by FastAPI.

Source code in src/symfonic/agent/fastapi/dependencies.py
def set_tenant_auth_verifier(verifier: TenantAuthVerifier | None) -> None:
    """Register (or clear) the tenant-access verifier.

    Call this once at app startup, *before* mounting the agent router.
    Passing ``None`` unregisters the current verifier (useful for tests).

    The verifier receives the raw ``Request`` plus the parsed ``tenant_id``
    from the ``X-Tenant-ID`` header and must raise an ``HTTPException`` to
    deny access.  Any other exception is treated as a 500 by FastAPI.
    """
    global _tenant_auth_verifier, _missing_verifier_warning_emitted
    _tenant_auth_verifier = verifier
    # Reset the one-shot warning flag so re-registering after a clear
    # doesn't silently swallow the dev-mode warning again.
    _missing_verifier_warning_emitted = False