
    (GjR                       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%dS )$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_routespathstrreturnNonec                z    t           5  t                              |            ddd           dS # 1 swxY w Y   dS )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r   s    J/home/rurouni/.hermes/hermes-agent/hermes_cli/dashboard_auth/token_auth.pyregister_token_router   <   s     
    $                                   s   044boolc                V    t           5  | t          v cddd           S # 1 swxY w Y   dS )z@True if ``path`` was registered as token-authable (exact match).N)r   r   r   s    r   is_token_router   G   st    	 % %}$% % % % % % % % % % % % % % % % % %s   	""c                 x    t           5  t                                           ddd           dS # 1 swxY w Y   dS )z,Test-only: drop all registered token routes.N)r   r   clear     r   clear_token_routesr"   M   s~    	                   s   /33requestr   c                    | j                             dd          }|r-|                    d          d                                         S | j        r| j        j        ndS )Nzx-forwarded-for ,r   )headersgetsplitstripclienthost)r#   fwds     r   
_client_ipr.   S   sZ    
/

/
4
4C
 )yy~~a &&(((").87>b8r!   c                "   | j                             dd          }|                    dd          }t          |          dk    rJ|d                                                                         dk    r|d                                         S dS )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_tokenr9   Z   sz     ?33DJJsAE
5zzQ58>>++1133x??Qx~~2r!   .Tuple[Optional[TokenPrincipal], Optional[str]]c                ~   t          |           }|sdS d}t                      D ]}	 |                    |          }nt# t          $ r4}t                              d|j        |           ||j        }Y d}~Sd}~wt          $ r+}t                              d|j        |           Y d}~d}~ww xY w||dfc S d|fS )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)r9   r   verify_tokenr   _logwarningname	Exception)r#   r<   unreachableprovider	principales         r   authenticate_tokenrF   h   s    !))E z!%K(** # #	 --E-::II 	 	 	LLQq   "&mHHHH 	 	 	LLLq   HHHH	  d?""" !s!   >
B/*A77B/!B**B/	call_next(Callable[[Request], Awaitable[Response]]r
   c           	       K   | j         j        }t          |          s ||            d{V S t          |           \  }}|)|| j        _        d| j        _         ||            d{V S |rBt          t          j	        |d|t          |                      t          dd|did	          S t          t          j	        d
|t          |                      t          dddd	          S )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)rC   reasonr   ipdetailzAuth provider z unreachablei  )status_codeno_provider_recognises_token)rK   r   rL   unauthenticatedUnauthorized)errorrM   i  )urlr   r   rF   statetoken_principaltoken_authenticatedr   r   TOKEN_AUTH_FAILUREr.   r	   )r#   rG   r   rD   rB   s        r   token_auth_middlewarerX      sR     $ ;D$ (Yw'''''''''/88I{(1%,0)Yw''''''''' 
) )'""	
 	
 	
 	
 CCCCD
 
 
 	

 %-g	    #~>>   r!   )r   r   r   r   )r   r   r   r   )r   r   )r#   r   r   r   )r#   r   r   r:   )r#   r   rG   rH   r   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__r>   setr   __annotations__Lockr   r   r   r"   r.   r9   rF   rX   r    r!   r   <module>rh      s  % % %L # " " " " "      7 7 7 7 7 7 7 7 7 7 7 7       4 4 4 4 4 4 4 4 : : : : : : A A A A A A A A H H H H H H H Hw"" #%%    	       % % % %   9 9 9 9   % % % %P2 2 2 2 2 2r!   