
    `gjsW                    >   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mZmZ dd
lmZmZmZmZmZ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dZ)	 	 	 	 	 	 ddZ*ddZ+ddd dZ,y)!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_session_providers)
AuditEvent	audit_log)DashboardAuthProviderProviderErrorRefreshExpiredError)clear_sso_attempt_cookieread_session_cookiesread_session_providerread_sso_attempt_cookieset_session_provider_cookieset_sso_attempt_cookie)PUBLIC_API_PATHS)z/auth/loginz/auth/callbackz/auth/password-loginz/auth/logout/loginz/api/auth/providersz/api/mcp/oauth/callback/z/assets/z/favicon.icoz/ds-assets/z/fonts/z/fonts-terminal/ztuple[str, ...]_GATE_PUBLIC_PREFIXESc                F      t         v ry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(  xs j                  |        y wN
startswith).0prefixpaths     W/root/.hermes/venv/lib/python3.12/site-packages/hermes_cli/dashboard_auth/middleware.py	<genexpr>z"_path_is_public.<locals>.<genexpr>P   s.       	1$//&11   "%)r   anyr   )r    s   `r!   _path_is_publicr%   A   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_ipr2   V   sS    
//

/
4C
yy~a &&((")..7>>8b8r&   c                J     t               } r|j                   fd       |S )ag  Prefer the hinted provider without making the hint authoritative.

    The cookie can outlive a provider rename/removal or become stale after a
    deployment change. A stable sort moves a matching provider to the front
    while preserving registration order for every remaining candidate; an
    unknown hint therefore leaves the normal scan unchanged.
    c                "    | j                   k7  S r   )name)providerprovider_hints    r!   <lambda>z,_ordered_session_providers.<locals>.<lambda>i   s    HMM],J r&   )key)r
   sort)r7   	providerss   ` r!   _ordered_session_providersr<   ]   s%     '(IJKr&   c                   ddl m} | j                  j                  }t	        |       } ||       }|r| d| n| d}|j                  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   /api/invalid_or_expired_sessionsession_expiredunauthenticatedUnauthorized)errordetailreason	login_urli  status_code.  urlrJ   ) hermes_cli.dashboard_auth.prefixr?   rM   r    _safe_next_targetr   r   r   )r0   rG   r?   r    
next_paramr   rH   
error_codes           r!   _unauth_responserR   m   s    , E;;D"7+J )F/96(,zl+xv 
 w 55 " 	
 #( &	 
 	
 	s;;r&   c                D   | j                   j                  }|j                  d      ryt        |       r(ddlm} t        | d      }t        | ||              |S t               }t        |      dk7  ryddlm} |d   }t        |d	d
      ry ||       }t        |       }ddlm} | d ||j                  d       }	|r|	 d| }	t        |	d      }ddlm}
 t%        | |
|       |       t'        t(        j*                  |j                  dt-        |              |S )u  Maybe auto-initiate the portal OAuth redirect on an unauth HTML load.

    Returns a 302 → ``/auth/login`` (the existing OAuth-initiation route)
    when ALL of the following hold, else ``None`` (caller falls back to the
    ordinary ``/login`` interstitial):

      * the request is an HTML document navigation, not an ``/api/*`` fetch
        (a fetch() would follow the 302 into the cross-origin OAuth dance
        opaquely — same reason ``_unauth_response`` never redirects APIs);
      * exactly ONE interactive provider is registered — with two or more we
        can't pick for the user, so the ``/login`` chooser must render; with
        zero there's nothing to redirect to;
      * that provider is OAuth-style, not a password form provider. Password
        providers must render ``/login`` so the user can enter credentials;
      * the one-shot loop-guard marker is ABSENT. Its presence means we
        already bounced to the portal once and came back still
        unauthenticated (no portal session) — auto-redirecting again would
        ping-pong, so we fall through to ``/login`` and clear the marker.

    The portal ``/oauth/authorize`` auto-approves any current member of the
    dashboard's org and is a silent 302 when the user already holds a portal
    session, so for the common case (clicked a dashboard link while signed
    in to the portal) this removes the interstitial CLICK entirely. It
    removes a click, not a security check: the redirect lands on
    ``/auth/login`` which runs the unchanged PKCE auth-code flow.
    r@   Nr   r>   	no_cookierG   r      supports_passwordFquotez/auth/login?provider=r(   safez&next=rK   rL   detect_https)	use_httpsr   auto_ssor6   rG   ip)rM   r    r   r   rN   r?   rR   r   r
   lengetattrrO   urllib.parserZ   r5   r   !hermes_cli.dashboard_auth.cookiesr^   r   r   r   LOGIN_STARTr2   )r0   r    r?   respr;   r6   r   rP   rZ   
auth_loginr^   s              r!   _auto_sso_responserj      s   6 ;;Dw w'H< .A'.JK '(I
9~D|Hx,e4 )F"7+J"80x}}21N0OPJ"|6*6

<D
 ?W-f g	 Kr&   c                4   | j                   j                  r"j                  d      rj                  d      ryt        fddD              rydk(  sj                  d      ry| 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(  xs j                  |        y wr   r   )r   pr    s     r!   r"   z$_safe_next_target.<locals>.<genexpr>   s.       		'T__Q''r#   )r   z/auth/z
/api/auth/z/apir@   ?r   rY   r[   )rM   r    r   r$   queryre   rZ   )r0   rp   targetrZ   r    s       @r!   rO   rO      s     ;;D ts+tt/D
 3   v~1KKE"'vQugTF"b!!r&   c           
     H  K   t        | j                  j                  dd      s ||        d{   S t        | j                  dd      r ||        d{   S | j                  j                  }t        |      r ||        d{   S t        |       \  }}t        |       }|s|st        |       }||S t        | d      S d}|r@d}t        |      D ]  }		 |	j                  |      }| n ||t)        dd|did      S |	 t+        | ||      }||\  }}|| j                  _         ||        d{   }ddlm}m} ddlm}  |||j:                  |j<                  t?        |       ||        ||       |       t!        t"        j@                  ||jB                  t'        |              |S t!        t"        j$                  dt'        |              t        | d      }ddlm"} ddlm}  || ||              |S || j                  _         ||        d{   }|s=|jF                  r1ddlm} ddlm} tI        ||jF                   ||        ||              |S 7 =7 7 # t        $ rj}
t        j                  d|	j                  |
       t!        t"        j$                  |	j                  d	t'        |       
       ||	j                  }Y d}
~
d}
~
ww xY w# t        $ r&}
t)        ddt-        |
      did      cY d}
~
S d}
~
ww xY w7 7 w)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token_authenticatedrT   rU   )access_tokenz9dashboard-auth: provider %r unreachable during verify: %sprovider_unreachablera   rF   zAuth provider z unreachablei  rI   )refresh_tokenr7   r   )r^   set_session_cookiesr>   )ru   rw   access_token_expires_inr_   r   r6   )r6   user_idrb   no_provider_recognises)rG   rb   rA   )clear_session_cookiesrV   r]   )r6   r_   r   )%rd   appstaterM   r    r%   r   r   rj   rR   r<   verify_sessionr   _logwarningr5   r   r   SESSION_VERIFY_FAILUREr2   r   _attempt_refreshstrsessionrf   r^   rx   rN   r?   ru   rw   _expires_in_secondsREFRESH_SUCCESSrz   r|   r6   r   )r0   	call_nextr    at_rtr7   autor   unreachable_providerr6   e	refreshednew_sessionrefreshing_providerresponser^   rx   r?   r|   s                      r!   gated_auth_middlewarer     sD     7;;$$ou=w''' w}}3U;w''';;Dtw'''"7+GB)'2Mc "'*K<<  G	  ,02=A 	H"11r1B "%	& ?3?  ^,@+C<PQ 
 	(!+I  /8,K,$/GMM!&w//H M(55)77(;K(H&w/*73, **,#++g&	 O--+'"	

 $G4PQ 	LHh/B7/KL#GMMw''HW--BH#%%"7+&w/		
 OS ( ( (h ! OMM1 55%]]1!'*	 (/+3==(J  	  ^CF:\BC 		 0^ (s   -L"I,&L"I/0L"I2AL"I5,L"/L"K+ #L"=L>C(L"&L 'AL"/L"2L"5	K(>AK#L"#K((L"+	L4LLL"LL" L"c                |    ddl }t        dt        | j                        t        |j                               z
        S )a%  Seconds until the access token's ``exp``, floored at 60.

    Mirrors the auth-route's ``max(60, exp - now)`` so the access-token
    cookie's Max-Age tracks the token lifetime even on a slightly skewed
    clock. ``time`` imported locally to keep the module's import surface
    minimal.
    r   N<   )timemaxint
expires_at)r   r   s     r!   r   r     s/     r3w))*S-==>>r&   )r7   c          
        |syd}t        |      D ]'  }	 |j                  |      }|||j                  fc S  |t        |      y# t        $ r3 t        t        j
                  |j                  dt        |              Y st        $ ri}t        j                  d|j                  |       t        t        j
                  |j                  dt        |              ||j                  }Y d}~d}~ww xY w)a  Try to rotate an expired session via the refresh token.

    The provider hint only changes candidate order. ``RefreshExpiredError``
    rejects the token for that candidate, but cannot prove ownership because
    providers such as Basic raise it for foreign opaque tokens too. Likewise,
    ``ProviderError`` only makes that candidate unavailable. Both are audited
    and the remaining providers are tried. Returns ``None`` only when there is
    no RT or every reachable provider rejects it. If no provider succeeds and
    at least one raised ``ProviderError``, re-raises with that provider's name
    so the caller can return 503 without clearing potentially valid cookies.
    N)rw   refresh_expiredra   z:dashboard-auth: provider %r unreachable during refresh: %srv   )r<   refresh_sessionr   r   r   REFRESH_FAILUREr5   r2   r   r   r   )r0   rw   r7   unavailable_providerr6   r   r   s          r!   r   r     s     '+.}= .	"222OK. "--5.6 '0115 # 	**!(g&	  	LLLq **!-g&	 $+'/}}$	s   A		9C5C5AC00C5)r    r   returnbool)r0   r   r   r   )r7   
str | Noner   zlist[DashboardAuthProvider])r0   r   rG   r   r   r	   )r0   r   r   zResponse | None)r0   r   r   z(Callable[[Request], Awaitable[Response]]r   r	   )r   r   )r0   r   r7   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   rf   r   r   r   r   r   r   &hermes_cli.dashboard_auth.public_pathsr   	getLogger__name__r   r   __annotations__r%   r2   r<   rR   rj   rO   r   r   r    r&   r!   <module>r      s     #  &  F F < A 
  Dw"*   *9  3<lK\""Jss7s sl
? VZ ,r&   