
    j0@                       d Z ddlmZ ddlZddlZddlmZ ddlmZm	Z	m
Z
 ddlmZmZmZ ddlmZmZ ddlmZmZ dd	lmZmZ dd
lmZmZmZmZmZmZmZ ddl m!Z!  ej"        e#          Z$ e            Z%d4dZ&d4dZ'd4dZ(e%)                    dd          d5d            Z*e%)                    dd          d6d            Z+e%)                    dd          d7d8d!            Z,e%)                    d"d#          	 	 	 	 d9d:d(            Z-d;d*Z.e%/                    d+d,          d<d-            Z0e%)                    d.d/          d<d0            Z1e%/                    d1d2          d<d3            Z2dS )=u  HTTP routes for the dashboard-auth OAuth round trip.

Mounted at root (no prefix) by ``web_server.py``. The router does not
auto-gate; gating is performed by ``gated_auth_middleware``, which
allowlists everything under ``/auth/*`` and ``/api/auth/providers``.

The routes:

  GET  /login              → server-rendered login page
  GET  /auth/login?provider=N → 302 to IDP, sets PKCE cookie
  GET  /auth/callback?code,state → completes login, sets session cookies
  POST /auth/logout        → clears cookies, best-effort revoke
  GET  /api/auth/providers → list registered providers (login bootstrap)
  GET  /api/auth/me        → current Session as JSON (auth-required)
    )annotationsN)Any)	APIRouterHTTPExceptionRequest)HTMLResponseJSONResponseRedirectResponse)get_providerlist_providers)
AuditEvent	audit_log)InvalidCodeErrorProviderError)clear_pkce_cookieclear_session_cookiesdetect_httpsread_pkce_cookieread_session_cookiesset_pkce_cookieset_session_cookies)render_login_htmlrequestr   returnstrc                   ddl m}m} ddlm}m}  |            }|r| dS t          |                     d                    } ||           }|s|S  ||          } ||                    | |j	                             S )u  Reconstruct the absolute callback URL the IDP redirects back to.

    Three resolution tiers:

      1. ``HERMES_DASHBOARD_PUBLIC_URL`` env var or
         ``dashboard.public_url`` in config.yaml — when set, this is
         the complete authority (scheme + host + optional path prefix)
         and we append ``/auth/callback`` verbatim. ``X-Forwarded-Prefix``
         is IGNORED on this code path because the operator has declared
         the public URL — we no longer need to guess from proxy headers,
         and stacking the prefix on top would double-prefix the common
         case where the prefix is already baked into ``public_url``.
         Relief valve for deploys behind reverse proxies whose forwarded
         headers aren't reliable.

      2. ``X-Forwarded-Prefix: /hermes`` (Mission Control deploys) — we
         prepend the prefix to the path FastAPI's ``url_for`` produces
         (it doesn't natively honour this header — it isn't part of the
         Starlette/uvicorn proxy_headers set).

      3. Bare ``request.url_for("auth_callback")`` — under uvicorn's
         ``proxy_headers=True`` this picks up the public https URL from
         ``X-Forwarded-Host`` plus ``X-Forwarded-Proto``. Fly.io's
         default path.
    r   )urlparse
urlunparse)prefix_from_requestresolve_public_url/auth/callbackauth_callback)path)
urllib.parser   r    hermes_cli.dashboard_auth.prefixr   r    r   url_for_replacer#   )	r   r   r   r   r    
public_urlbaseprefixparseds	            ?/usr/local/lib/hermes-agent/hermes_cli/dashboard_auth/routes.py_redirect_urir-   2   s    4 21111111        $#%%J -
 ,,,, w//00D  ))F Xd^^F:foof+Cfk+C+CoDDEEE    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_ipr9   f   sZ    
/

/
4
4C
 )yy~~a &&(((").87>b8r.   c                $    ddl m}  ||           S )aA  Resolve the X-Forwarded-Prefix header for the active request.

    Local indirection so the routes pass a consistent value to the
    cookie helpers (cookie name + Path attribute) and the gate's
    redirect builders (login_url construction). See
    ``hermes_cli.dashboard_auth.prefix`` for the normalisation rules.
    r   )r   )r%   r   )r   r   s     r,   _prefixr;   m   s(     EDDDDDw'''r.   /login
login_page)namer   c                   K   t          | j                            dd                    }t          t	          |          ddi          S )Nnextr0   )	next_pathzCache-Controlz#no-store, no-cache, must-revalidate)r2   )_validate_post_login_targetquery_paramsr3   r   r   )r   rA   s     r,   r=   r=   ~   s]       ,  ,, I I... "GH   r.   z/api/auth/providersauth_providersr   c                 h   K   t                      } | st          ddid          S dd | D             iS )Ndetailzno auth providers registered  )status_code	providersc                ,    g | ]}|j         |j        d S )r>   display_namerK   ).0ps     r,   
<listcomp>z&api_auth_providers.<locals>.<listcomp>   s4     
 
 
 VQ^<<
 
 
r.   )r   r	   )rI   s    r,   api_auth_providersrP      sg        I 
56
 
 
 	

 	 
 

 
 
 r.   z/auth/login
auth_loginr0   providerr@   c           	       K   t          |          }|t          dd|          	 |                    t          |                     }nP# t          $ rC}t          t          j        |dt          |                      t          dd|           d }~ww xY wt          t          j	        |t          |           	           t          |j        d
          }|j                            dd          }d|vr|rd| d| nd| }t          |          }|rddlm}	 | d |	|d           }t#          ||t%          |           t'          |                      |S )Ni  zUnknown provider: rH   rF   )redirect_uriprovider_unreachablerR   reasoniprG   Provider unreachable: )rR   rY   .  urlrH   hermes_session_pkcer0   z	provider=;r   )quotez;next=)safe)payload	use_httpsr*   )r   r   start_loginr-   r   r   r   LOGIN_FAILUREr9   LOGIN_STARTr
   redirect_urlcookie_payloadr3   rB   r$   r`   r   r   r;   )
r   rR   r@   rN   lseresppkce	safe_nextr`   s
             r,   rQ   rQ      s     XAy444
 
 
 	


]]g(>(>]?? 

 

 

$)'""		
 	
 	
 	
 /A//
 
 
 	


 g    SAAAD   !6;;D$04P,8,,d,,,:Ph:P:P ,D11I :&&&&&&99eeIB77799dl7&;&;w    Ks   #A 
B>BBr!   r"   codestateerrorerror_descriptionc           
     \  K   t          |           }|s:t          t          j        dt	          |                      t          dd          t          d |                    d          D                       }|                    dd	          }|                    d
d	          }|                    dd	          }	|                    dd	          }
t          |          }|t          dd|          |rCt          t          j        |d|t	          |                      t          dd| d| d          |r||k    r;t          t          j        |dt	          |                      t          dd          	 |
                    |||	t          |                     }n# t          $ rC}t          t          j        |dt	          |                      t          dd|           d }~wt          $ rC}t          t          j        |dt	          |                      t          dd|           d }~ww xY wt          t          j        ||j        |j        |j        t	          |                      t%          d|j        t)          t+          j                              z
            }t-          |
          pd}t/          |d           }t1          ||j        |j        |t7          |           t9          |           !           t;          |t9          |           "           |S )#Nmissing_pkce_cookie)rX   rY   i  zMissing PKCE state cookierT   c              3  J   K   | ]}d |v |                     d d          V  dS )=   N)r4   )rM   segs     r,   	<genexpr>z auth_callback.<locals>.<genexpr>   s=        !C3JJ		#qJJJJ r.   r_   rR   r0   ro   verifierr@   zUnknown provider in cookie: 	idp_error)rR   rX   rp   rY   zOAuth error from provider: z ()state_mismatchrW   z(OAuth state mismatch (CSRF check failed))rn   ro   code_verifierrU   invalid_codezInvalid code: rV   rG   rZ   )rR   user_idemailorg_idrY   <   /r[   r\   )access_tokenrefresh_tokenaccess_token_expires_inrc   r*   r*   )r   r   r   re   r9   r   dictr4   r3   r   complete_loginr-   r   r   LOGIN_SUCCESSr   r   r   max
expires_atinttimerB   r
   r   r   r   r   r;   r   )r   rn   ro   rp   rq   pkce_rawpartsprovider_nameexpected_statery   next_from_cookierN   sessionrj   
expires_inlandingrk   s                    r,   r"   r"      s       ((H 	
$('""	
 	
 	
 	

 .
 
 
 	
   %-^^C%8%8    E IIj"--MYYw++NyyR((H
 yy,,]##AyC-CC
 
 
 	

  
$"'""	
 	
 	
 	
 NNN:KNNN
 
 
 	

  

E^++$"#'""		
 	
 	
 	
 =
 
 
 	


"""&w//	 # 
 
  J J J$"!'""		
 	
 	
 	
 4HQ4H4HIIII 

 

 

$")'""		
 	
 	
 	
 /A//
 
 
 	


  m~g    R+c$)++.>.>>??J **:;;BsGS999D)+ *w''w    d77#3#34444Ks$    &F' '
H?1>G//H?<>H::H?rawc                    | sdS ddl m}  ||                               d          r                    d          rdS t          fddD                       rdS S )u  Return ``raw`` if it's a safe same-origin path, else empty string.

    The ``next`` query param survives a full OAuth round trip — the gate
    encodes it into the /login redirect, the login page emits it back into
    /auth/login, and the IDP preserves it across /authorize/callback. We
    have to re-validate here because the value came back in via the
    URL (an attacker could craft a /auth/callback URL with their own
    ``next=https://evil.example``).
    r0   r   )unquoter   z//c              3  N   K   | ]}|k    p                     |          V   d S )N)
startswith)rM   rN   decodeds     r,   rx   z._validate_post_login_target.<locals>.<genexpr>k  sN         	1-**1--     r.   )r<   z/auth/z
/api/auth/)r$   r   r   any)r   r   r   s     @r,   rB   rB   Z  s      r$$$$$$gcllGc"" g&8&8&>&> r
    3      rNr.   z/auth/logoutauth_logoutc                   K   t          |           \  }}|r`t                      D ]Q}	 |                    |           # t          $ r+}t                              d|j        |           Y d }~Jd }~ww xY wt          | j        dd           }t          t          j        |r|j        nd|r|j        ndt          |                      t          |           }t!          | dd	          }t#          ||
           t%          ||
           |S )N)r   z'dashboard-auth: revoke on %r failed: %sr   unknownr0   rR   r   rY   r<   r[   r\   r   )r   r   revoke_session	Exception_logwarningr>   getattrro   r   r   LOGOUTrR   r   r9   r;   r
   r   r   )r   _atrtrR   rj   sessr*   rk   s           r,   r   r   s  s[     "7++GC	  '(( 	 	H''b'9999   =M1        7=)T22D#'6$--Y!%-2g	    WF6 1 1 1sCCCD$v....d6****Ks   ?
A4	!A//A4z/api/auth/meauth_mec                   K   t          | j        dd          }|t          dd          |j        |j        |j        |j        |j        |j        dS )zCReturn the verified session as JSON. Auth-required (gate enforces).r   N  UnauthorizedrT   )r   r   rL   r   rR   r   )	r   ro   r   r   r   rL   r   rR   r   )r   r   s     r,   api_auth_mer     sa       7=)T22D|NCCCC<)+Mo  r.   z/api/auth/ws-ticketauth_ws_ticketc                  K   t          | j        dd          }|t          dd          ddlm}m}  ||j        |j                  }t          t          j
        |j        |j        t          |           	           ||d
S )a  Mint a short-lived single-use ticket for the authenticated session.

    Browsers cannot set ``Authorization`` on a WebSocket upgrade, so in
    gated mode the SPA POSTs this endpoint to get a ``?ticket=`` value to
    append to ``/api/pty``, ``/api/ws``, ``/api/pub``, or ``/api/events``.

    The ticket has a 30-second TTL and is single-use. Calling this endpoint
    multiple times in quick succession (e.g. one ticket per WS) is the
    expected pattern.
    r   Nr   r   rT   r   )TTL_SECONDSmint_ticket)r   rR   r   )ticketttl_seconds)r   ro   r   $hermes_cli.dashboard_auth.ws_ticketsr   r   r   rR   r   r   WS_TICKET_MINTEDr9   )r   r   r   r   r   s        r,   api_auth_ws_ticketr     s       7=)T22D|NCCCC NMMMMMMM[FFFF#g	    [999r.   )r   r   r   r   )r   r   r   r   )r   r   )r0   )r   r   rR   r   r@   r   )r0   r0   r0   r0   )
r   r   rn   r   ro   r   rp   r   rq   r   )r   r   r   r   )r   r   )3__doc__
__future__r   loggingr   typingr   fastapir   r   r   fastapi.responsesr   r	   r
   hermes_cli.dashboard_authr   r   hermes_cli.dashboard_auth.auditr   r   hermes_cli.dashboard_auth.baser   r   !hermes_cli.dashboard_auth.cookiesr   r   r   r   r   r   r   $hermes_cli.dashboard_auth.login_pager   	getLogger__name__r   routerr-   r9   r;   r3   r=   rP   rQ   r"   rB   postr   r   r    r.   r,   <module>r      sU    # " " " " "         5 5 5 5 5 5 5 5 5 5 J J J J J J J J J J        B A A A A A A A                         C B B B B Bw""	1F 1F 1F 1Fh9 9 9 9	( 	( 	( 	(" H<((   )(& !(899   :9* M--1 1 1 1 .-1h ?33 y y y y 43yx   2 ^-00   10F N++   ,+( ")9::: : : ;:: : :r.   