Skip to content

symfonic.agent.fastapi.identity

identity

Who is calling, whichever auth path answered.

Two auth paths reach a handler and they publish identity differently. The legacy verifier owns the Request and stashes user_id / is_admin on request.state; the injected resolver never sees a request and publishes an :class:~symfonic.platform.values.AuthenticatedPrincipal instead. Eight call sites across five modules read the legacy attributes directly, so before this module an app that migrated to the resolver lost its identity in every one of them — with no error, because every read was a getattr with a default.

Kept out of dependencies deliberately: that module is the transport seam (headers in, scope out) and already carries the resolver installer, the production gate and the legacy global. "Who is the caller" is a different question with different consumers, and it was the growth of dependencies past its budget that made the split obvious.

request_is_admin

request_is_admin(request: Any) -> bool

Whether the caller is a platform admin.

hasattr rather than a truthy check, because is_admin = False is a decision the legacy verifier made and absence is not. Reading it with a falsy default would let an unauthenticated request and a request explicitly refused admin look identical, and then the principal would silently override a denial the verifier had already issued.

Source code in src/symfonic/agent/fastapi/identity.py
def request_is_admin(request: Any) -> bool:
    """Whether the caller is a platform admin.

    ``hasattr`` rather than a truthy check, because ``is_admin = False`` is a
    *decision* the legacy verifier made and absence is not. Reading it with a
    falsy default would let an unauthenticated request and a request explicitly
    refused admin look identical, and then the principal would silently
    override a denial the verifier had already issued.
    """
    state = getattr(request, "state", None)
    if state is not None and hasattr(state, "is_admin"):
        return bool(state.is_admin)
    principal = request_principal(request)
    if principal is None:
        return False
    return bool(getattr(principal, "is_admin", False))

request_principal

request_principal(request: Any) -> Any | None

The principal the injected resolver derived for this request, if any.

Source code in src/symfonic/agent/fastapi/identity.py
def request_principal(request: Any) -> Any | None:
    """The principal the injected resolver derived for this request, if any."""
    state = getattr(request, "state", None)
    if state is None:
        return None
    return getattr(state, PRINCIPAL_STATE_ATTR, None)

request_user_id

request_user_id(request: Any) -> str | None

The authenticated caller's id, or None when nothing authenticated.

The legacy stash wins when present, and the precedence is deliberate rather than incidental: a verifier that saw the request can enrich what it stashes — the scaffold promotes an org owner to admin after its membership query — so between two answers the more informed one is the one that had the request in hand.

None rather than a raise. This is read in audit emission and the budget breaker, and a deployment with no verifier is explicitly supported for dev; turning that into a 500 would punish the supported case.

Source code in src/symfonic/agent/fastapi/identity.py
def request_user_id(request: Any) -> str | None:
    """The authenticated caller's id, or ``None`` when nothing authenticated.

    The legacy stash wins when present, and the precedence is deliberate rather
    than incidental: a verifier that saw the request can enrich what it stashes
    — the scaffold promotes an org owner to admin after its membership query —
    so between two answers the more informed one is the one that had the
    request in hand.

    ``None`` rather than a raise. This is read in audit emission and the budget
    breaker, and a deployment with no verifier is explicitly supported for dev;
    turning that into a 500 would punish the supported case.
    """
    state = getattr(request, "state", None)
    if state is not None:
        legacy = getattr(state, "user_id", None)
        if legacy is not None:
            return str(legacy)
    principal = request_principal(request)
    if principal is None:
        return None
    value = getattr(principal, "principal_id", None)
    return str(value) if value else None