
    `gjR                    0   U d Z ddlmZ ddlZddlZddlmZmZmZm	Z	 ddl
mZ ddlmZmZ ddlmZ ddlmZmZ dd	lmZmZ  ej.                  e      Z e       Zd
ed<    ej:                         ZddZddZ ddZ!ddZ"ddZ#	 	 	 	 ddZ$	 	 	 	 	 	 ddZ%y)u  Route-agnostic non-interactive (bearer-token) auth seam for the dashboard.

This is the generic API-token capability (decisions.md Q-C): a reusable seam
that ANY service-to-service / machine-credential provider plugs into, NOT a
drain-specific hook. The drain bearer-secret plugin is merely the first
consumer.

How it fits the existing auth framework:

  * The interactive gate (``gated_auth_middleware``) authenticates a human
    via a session cookie on every non-public route. A service caller has no
    cookie — it presents a bearer token in the ``Authorization`` header on a
    single request. That is what this seam verifies.

  * A route opts in by registering its exact path via
    :func:`register_token_route`. Only registered paths are token-authable;
    everything else is untouched, so this can never accidentally widen the
    auth surface of an existing route.

  * :func:`token_auth_middleware` runs OUTERMOST (installed last in
    ``web_server.py``). For a token route it fully owns the auth decision:
    authenticate via the stacked token providers, attach the verified
    :class:`~hermes_cli.dashboard_auth.base.TokenPrincipal` to
    ``request.state.token_principal`` + set ``request.state.token_authenticated``,
    and pass through; otherwise reject (401 unauthenticated, or 503 when a
    provider's backing store was unreachable). The downstream cookie/session
    gates honour ``token_authenticated`` and skip enforcement, so a
    token-authed service request is never bounced to ``/login``.

  * Fails closed: a token route with no registered token provider, no token,
    or an unrecognised token gets 401 — never an open pass-through.

Provider stacking mirrors ``verify_session``: each ``supports_token`` provider
is consulted in registration order until one returns a principal. A provider
that doesn't recognise the token returns ``None`` and the seam moves on; a
provider whose backing store is unreachable raises ``ProviderError``, which the
seam remembers and surfaces as 503 only if NO provider accepts the token.
    )annotationsN)	AwaitableCallableOptionalTuple)Request)JSONResponseResponse)list_token_providers)
AuditEvent	audit_log)ProviderErrorTokenPrincipalzset[str]_token_routesc                d    t         5  t        j                  |        ddd       y# 1 sw Y   yxY w)u  Mark ``path`` (exact match) as token-authable.

    Idempotent. Call at module import / app setup so the seam knows which
    routes to guard. Registering a route does NOT make it public — it makes
    it authenticate by token instead of by session cookie.
    N)_lockr   addpaths    W/root/.hermes/venv/lib/python3.12/site-packages/hermes_cli/dashboard_auth/token_auth.pyregister_token_router   <   s*     
  $     s   &/c                J    t         5  | t        v cddd       S # 1 sw Y   yxY w)z@True if ``path`` was registered as token-authable (exact match).N)r   r   r   s    r   is_token_router   G   s"    	 %}$% % %s   "c                 b    t         5  t        j                          ddd       y# 1 sw Y   yxY w)z,Test-only: drop all registered token routes.N)r   r   clear     r   clear_token_routesr   M   s&    	   s   %.c                    | j                   j                  dd      }|r"|j                  d      d   j                         S | j                  r| j                  j
                  S dS )Nzx-forwarded-for ,r   )headersgetsplitstripclienthost)requestfwds     r   
_client_ipr*   S   sS    
//

/
4C
yy~a &&((")..7>>8b8r   c                    | j                   j                  dd      }|j                  dd      }t        |      dk(  r7|d   j	                         j                         dk(  r|d   j	                         S y)u  Return the bearer token from the ``Authorization`` header, or "".

    Accepts ``<scheme> <token>`` where scheme is "bearer" (case-insensitive).
    Returns an empty string for a missing/malformed header or a non-bearer
    scheme — the caller treats "" as "no token presented".
    authorizationr           r   bearer)r"   r#   r$   lenr%   lower)r(   authpartss      r   extract_bearer_tokenr5   Z   sd     ??3DJJsAE
5zQ58>>+113x?Qx~~r   c                l   t        |       }|syd}t               D ]  }	 |j                  |      }||dfc S  d|fS # t        $ r9}t        j                  d|j                  |       ||j                  }Y d}~ad}~wt        $ r+}t        j                  d|j                  |       Y d}~d}~ww xY w)uG  Try every token provider against the request's bearer token.

    Returns ``(principal, unreachable_provider_name)``:
      * ``(TokenPrincipal, None)`` — a provider recognised and accepted the token.
      * ``(None, None)`` — no token, or no provider recognised it (reject 401).
      * ``(None, name)`` — no provider accepted it AND at least one provider's
        backing store was unreachable (the caller surfaces 503, not 401, so a
        transient outage doesn't read as "bad credentials").

    Never raises: a provider ``ProviderError`` is caught and remembered.
    )NNN)tokenz?dashboard-auth: token provider %r unreachable during verify: %sz:dashboard-auth: token provider %r raised during verify: %s)r5   r   verify_tokenr   _logwarningname	Exception)r(   r7   unreachableprovider	principales         r   authenticate_tokenrA   h   s     !)E!%K(* #	 --E-:I  d?"%#& !  	LLQq "&mm 	LLLq 	s!   ?	B3/A<<B3!B..B3c           	       K   | j                   j                  }t        |      s ||        d{   S t        |       \  }}|2|| j                  _        d| j                  _         ||        d{   S |r:t        t        j                  |d|t        |              t        dd|did	      S t        t        j                  d
|t        |              t        dddd	      S 7 7 xw)u  Outermost auth seam for token-authable routes.

    No-op pass-through for any path not registered via
    :func:`register_token_route`. For a registered path, token auth is the
    only accepted scheme:

      * valid token  → attach principal + ``token_authenticated`` flag, pass through.
      * unreachable  → 503 (provider backing store down; not "bad credentials").
      * otherwise    → 401 unauthenticated.

    Runs before the cookie/session gates (installed last in ``web_server.py``).
    The cookie gates honour ``request.state.token_authenticated`` and skip
    enforcement, so a token-authed request is never redirected to ``/login``.
    NTprovider_unreachable)r>   reasonr   ipdetailzAuth provider z unreachablei  )status_codeno_provider_recognises_token)rD   r   rE   unauthenticatedUnauthorized)errorrF   i  )urlr   r   rA   statetoken_principaltoken_authenticatedr   r   TOKEN_AUTH_FAILUREr*   r	   )r(   	call_nextr   r?   r=   s        r   token_auth_middlewarerR      s     $ ;;D$w'''/8I{(1%,0)w''')) )'"	
 lCD
 	

 %%-g	 #~> 7 ( (s#   -C,C(AC,1C*2A7C,*C,)r   strreturnNone)r   rS   rT   bool)rT   rU   )r(   r   rT   rS   )r(   r   rT   z.Tuple[Optional[TokenPrincipal], Optional[str]])r(   r   rQ   z(Callable[[Request], Awaitable[Response]]rT   r
   )&__doc__
__future__r   logging	threadingtypingr   r   r   r   fastapir   fastapi.responsesr	   r
   hermes_cli.dashboard_authr   hermes_cli.dashboard_auth.auditr   r   hermes_cli.dashboard_auth.baser   r   	getLogger__name__r9   setr   __annotations__Lockr   r   r   r   r*   r5   rA   rR   r   r   r   <module>rf      s   %L #   7 7  4 : A Hw" %x 	 %9%%3%P2272 2r   