
    Fj!                        U d Z ddlmZ ddlZddlmZmZ ddlmZ ddl	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 ddlmZ  ej        e          ZdZded<   ddZddZd dZddZ d!dZ!dS )"a  Auth-gate middleware for the dashboard.

Engaged when ``app.state.auth_required is True``. The gate's job:

  1. Allow a small set of routes through unauthenticated (login page,
     ``/auth/*`` OAuth round trip, ``/api/auth/providers``, static
     assets).
  2. For everything else, demand a valid session cookie and attach the
     verified :class:`Session` to ``request.state.session``.
  3. On HTML routes, redirect missing/invalid cookies to ``/login``.
     On ``/api/*`` routes, return 401 JSON.

The middleware is a no-op when ``auth_required`` is False (loopback
mode); the legacy ``_SESSION_TOKEN`` ``auth_middleware`` handles those
binds.
    )annotationsN)	AwaitableCallable)Request)JSONResponseRedirectResponseResponse)list_providers)
AuditEvent	audit_log)ProviderError)read_session_cookies)PUBLIC_API_PATHS)
z/auth/loginz/auth/callbackz/auth/logout/loginz/api/auth/providersz/assets/z/favicon.icoz/ds-assets/z/fonts/z/fonts-terminal/ztuple[str, ...]_GATE_PUBLIC_PREFIXESpathstrreturnboolc                Z      t           v rdS t           fdt          D                       S )u  True if ``path`` bypasses the OAuth auth gate.

    Two sources of public-ness:

    * :data:`PUBLIC_API_PATHS` — the shared ``/api/*`` allowlist that
      the legacy ``_SESSION_TOKEN`` middleware also honours. Matched
      exactly (no prefix expansion) so adding ``/api/status`` doesn't
      accidentally expose ``/api/status/secret-extension``.
    * :data:`_GATE_PUBLIC_PREFIXES` — auth-bootstrap routes and static
      mounts. Prefix-matched so ``/assets/foo.css`` lights up via
      ``/assets/``.
    Tc              3  N   K   | ]}|k    p                     |          V   d S N
startswith).0prefixr   s     C/usr/local/lib/hermes-agent/hermes_cli/dashboard_auth/middleware.py	<genexpr>z"_path_is_public.<locals>.<genexpr>C   sL         	1$//&11         )r   anyr   )r   s   `r   _path_is_publicr!   4   sN     t    +     r   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-   I   sZ    
/

/
4
4C
 )yy~~a &&(((").87>b8r   reasonr	   c                  ddl m} | j        j        }t	          |           } ||           }|r| d| n| d}|                    d          r |dk    rdnd}t          |d	||d
d          S t          |d          S )u  API routes → 401 JSON with ``login_url``; HTML routes → 302 → /login.

    The JSON envelope carries a ``login_url`` field with a ``next=`` query
    string so the SPA's global 401 handler can drop the user back where
    they were after re-auth. The contract is intentionally simple so any
    fetch-wrapper can implement the redirect without parsing details:

        if response.status === 401 && body.error in ("unauthenticated",
                                                       "session_expired"):
            window.location.assign(body.login_url);

    HTML redirects also carry the ``next=`` query string so direct
    navigation to ``/sessions`` (etc.) without a cookie comes back to
    ``/sessions`` after login.

    Under a reverse proxy with ``X-Forwarded-Prefix: /hermes``, the
    ``login_url`` is prefixed (``/hermes/login?next=...``) so the
    browser's window.location.assign / Location: follow lands on the
    proxied login page rather than the bare ``/login`` (which the
    proxy doesn't route to the dashboard).
    r   prefix_from_requestz/login?next=r   z/api/invalid_or_expired_sessionsession_expiredunauthenticatedUnauthorized)errordetailr.   	login_urli  status_codei.  )urlr:   ) hermes_cli.dashboard_auth.prefixr1   r;   r   _safe_next_targetr   r   r   )r"   r.   r1   r   
next_paramr   r8   
error_codes           r   _unauth_responser@   P   s    , EDDDDD;D"7++J  ))F/9 	6++z+++ 
 w 
 555 " 	
 #( &	  
 
 
 	
 	s;;;;r   c                   | j         j        r*                    d          r                    d          rdS t          fddD                       rdS | j         j        }|r d| n}ddlm}  ||d	          S )
uO  Build the URL-encoded ``next`` query value, or empty string.

    Only same-origin relative paths are accepted; absolute URLs or
    ``//evil.com`` open-redirect attempts are silently dropped. The empty
    string return means the caller produces a bare ``/login`` URL — fine,
    user lands at the dashboard root after re-auth.
    /z//r$   c              3  N   K   | ]}|k    p                     |          V   d S r   r   )r   pr   s     r   r   z$_safe_next_target.<locals>.<genexpr>   sL         		'T__Q''     r   )r   z/auth/z
/api/auth/?r   )quote)safe)r;   r   r   r    queryurllib.parserF   )r"   rH   targetrF   r   s       @r   r=   r=      s     ;D  ts++ tt/D/D r
    3      rKE"'1uTF""""""5b!!!!r   	call_next(Callable[[Request], Awaitable[Response]]c           
     b  K   t          | j        j        dd          s ||            d{V S | j        j        }t          |          r ||            d{V S t          |           \  }}|st          | d          S d}t                      D ]}	 |	                    |          }n# t          $ rx}t                              d|j        |           t          t          j        |j        dt#          |           	           t%          d
d|j        did          cY d}~c S d}~ww xY w| n|^t          t          j        dt#          |                      t          | d          }ddlm}	 ddlm}
  |	| |
|                      |S || j        _         ||            d{V S )zEngaged only when ``app.state.auth_required is True``.

    No-op pass-through in loopback mode so the legacy auth_middleware can
    handle those binds via ``_SESSION_TOKEN``.
    auth_requiredFN	no_cookie)r.   )access_tokenz9dashboard-auth: provider %r unreachable during verify: %sprovider_unreachable)providerr.   ipr7   zAuth provider z unreachablei  r9   no_provider_recognises)r.   rS   r2   r   )clear_session_cookiesr0   )r   )getattrappstater;   r   r!   r   r@   r
   verify_sessionr   _logwarningnamer   r   SESSION_VERIFY_FAILUREr-   r   !hermes_cli.dashboard_auth.cookiesrU   r<   r1   session)r"   rK   r   at_rtr_   rR   eresponserU   r1   s              r   gated_auth_middlewarerd      s}      7;$ou== (Yw''''''''';Dt (Yw'''''''''"7++GB =<<<< G"$$  	--2->>GG 	 	 	LLKq   1!-g&&	     IHMIIIJ          	 E  -+'""	
 	
 	
 	

 $G4PQQQ 	LKKKKKHHHHHHh/B/B7/K/KLLLL#GM7#########s   B**
D,4A+D'D,'D,)r   r   r   r   )r"   r   r   r   )r"   r   r.   r   r   r	   )r"   r   rK   rL   r   r	   )"__doc__
__future__r   loggingtypingr   r   fastapir   fastapi.responsesr   r   r	   hermes_cli.dashboard_authr
   hermes_cli.dashboard_auth.auditr   r   hermes_cli.dashboard_auth.baser   r^   r   &hermes_cli.dashboard_auth.public_pathsr   	getLogger__name__rZ   r   __annotations__r!   r-   r@   r=   rd    r   r   <module>rs      s|      # " " " " "  & & & & & & & &       F F F F F F F F F F 4 4 4 4 4 4 A A A A A A A A 8 8 8 8 8 8 B B B B B B C C C C C Cw""*        *9 9 9 93< 3< 3< 3<l" " " "6A$ A$ A$ A$ A$ A$r   