
    (Gj4                    &   U d Z ddlmZ ddlZddlZddlZddlZddlZddlZddl	Z	ddl
ZddlmZmZmZ ddlZddlmZmZmZmZmZmZ  ej        e          ZdZdZdZdZd	Zd
Z da!de"d<   d!dZ#d"dZ$ G d de          Z%d#dZ&d$dZ'd%dZ(d&d Z)dS )'un  SelfHostedOIDCProvider — generic self-hosted OpenID Connect dashboard auth.

A standards-compliant OpenID Connect Relying Party for the ``hermes dashboard``
OAuth gate. Unlike the bundled ``nous`` provider (which encodes Nous Portal's
bespoke contract — ``agent:{instance_id}`` client ids, a custom access-token
JWT, the ``x-nous-refresh-token`` header, an ``oauth_contract_version`` claim),
this provider speaks **plain OIDC** so it works against any conformant
self-hosted identity provider:

    Authentik · Keycloak · Zitadel · Authelia · Auth0 · Okta · Google · …

It is a pure drop-in plugin: it implements the five
:class:`~hermes_cli.dashboard_auth.DashboardAuthProvider` methods and touches
nothing in core auth/runtime/login. The HTTP round trip, cookies, CSRF
``state`` check and ``redirect_uri`` reconstruction are all owned by
``hermes_cli/dashboard_auth/routes.py``; this provider only:

  1. discovers the IDP's endpoints from ``{issuer}/.well-known/openid-configuration``,
  2. builds the ``/authorize`` URL with PKCE (S256),
  3. exchanges the authorization code for tokens at the discovered
     ``token_endpoint``,
  4. verifies the **ID token** (RS256/ES256) against the discovered
     ``jwks_uri`` with ``iss`` / ``aud`` pinned to the configured issuer /
     client id, and maps standard OIDC claims (``sub``, ``email``, ``name``)
     onto a :class:`~hermes_cli.dashboard_auth.Session`.

Why the ID token (not the access token)? OIDC guarantees the ID token is a
signed JWT carrying identity claims — that is its entire purpose. The access
token's format is opaque to the client per the spec; many IDPs issue random
opaque strings the client cannot verify locally. Verifying the ID token is the
only choice that is universally correct across self-hosted IDPs. (The ``nous``
provider verifies its *access* token because Nous Portal mints a custom JWT
access token with the dashboard claims baked in — a non-OIDC shortcut.)

Both **public** (PKCE-only) and **confidential** (PKCE + ``client_secret``)
clients are supported. A self-hoster who registers a public client configures
no secret and the token-endpoint calls authenticate with PKCE alone (the
default). A self-hoster whose IDP defaults the client to *confidential*
(Authentik and Keycloak commonly do) sets ``client_secret`` and the provider
additionally authenticates the client at the token endpoint, choosing
``client_secret_basic`` (HTTP Basic header) or ``client_secret_post`` (secret
in the form body) from the IDP's advertised
``token_endpoint_auth_methods_supported``. PKCE is sent in **both** modes —
the secret is client authentication layered on top, never a replacement for
PKCE (OAuth 2.1 / RFC 9700 keep PKCE mandatory regardless).

Configuration surfaces (env wins over config.yaml when set non-empty, so a
provisioned-but-not-populated secret can't shadow a valid config.yaml entry —
same precedence convention as the ``nous`` plugin)::

    # config.yaml — canonical surface
    dashboard:
      oauth:
        provider: self-hosted
        self_hosted:
          issuer: https://auth.example.com/application/o/hermes/   # required
          client_id: hermes-dashboard                              # required
          scopes: "openid profile email"                           # optional
          # client_secret: set ONLY for a confidential client. It is a
          # credential — prefer the env var / ~/.hermes/.env over config.yaml.

    # Environment overrides (Docker/Fly secret injection)
    HERMES_DASHBOARD_OIDC_ISSUER
    HERMES_DASHBOARD_OIDC_CLIENT_ID
    HERMES_DASHBOARD_OIDC_SCOPES        # optional; defaults to "openid profile email"
    HERMES_DASHBOARD_OIDC_CLIENT_SECRET # optional; set for a confidential client
                                        # (the .env file is the canonical home —
                                        # it's a secret, not a behavioural setting)

Skip reasons: when the plugin loads but can't register (missing issuer /
client_id), it writes a human-readable reason to the module-level
:data:`LAST_SKIP_REASON` so the gate's fail-closed branch can surface a useful
operator error instead of the bare "no providers registered".
    )annotationsN)AnyDictOptional)DashboardAuthProviderInvalidCodeError
LoginStartProviderErrorRefreshExpiredErrorSessionzopenid profile email)RS256ES256RS384RS512ES384ES512g      $@i  i,   strLAST_SKIP_REASONrawbytesreturnc                t    t          j        |                               d                                          S )u6   Base64url-encode without ``=`` padding (RFC 7636 §4).   =)base64urlsafe_b64encoderstripdecode)r   s    Q/home/rurouni/.hermes/hermes-agent/plugins/dashboard_auth/self_hosted/__init__.py_b64url_no_padr       s-    #C((//55<<>>>    urlfieldc                   t           j                            |           }|j        dk    r| S |j        dk    r|j        pddv r| S t          d| d|           )a{  Reject an endpoint URL that isn't HTTPS (loopback http is allowed).

    OAuth credentials (codes, tokens) flow over these URLs. We require HTTPS
    for everything except an explicit loopback host so a misconfigured issuer
    can't ship the authorization code / refresh token in cleartext. Returns
    the URL unchanged on success; raises :class:`ProviderError` otherwise.
    httpshttpr   )	localhostz	127.0.0.1z::1zOIDC z. must be https:// (or http on localhost), got )urllibparseurlparseschemehostnamer
   )r"   r#   parseds      r   _require_https_or_loopbackr.      s|     \""3''F}
}FO$9r ? $ $
 

LLLSLL  r!   c                      e Zd ZdZdZdZeddd8dZd9dZd:dZ	d;dZ
d<dZd=dZd>dZdd d!d?d*Zd@d+ZdAd,Zd@d-ZdBd/ZdCd1ZdDd3ZdEd4ZdFd7Zd S )GSelfHostedOIDCProviderzHGeneric self-hosted OpenID Connect provider (authorization-code + PKCE).zself-hostedzSelf-Hosted OIDCr   )scopesclient_secretissuerr   	client_idr1   r2   r   Nonec                  |st          d          |st          d          |                    d          | _        t          | j        d           || _        |                                pt          | _        |pd                                | _        d | _	        d| _
        t          j                    | _        d | _        d S )Nzissuer is requiredzclient_id is required/r3   r#   r   g        )
ValueErrorr   _issuerr.   
_client_idstrip_DEFAULT_SCOPES_scopes_client_secret
_discovery_discovery_fetched_at	threadingLock_discovery_lock_jwks_client)selfr3   r4   r1   r2   s        r   __init__zSelfHostedOIDCProvider.__init__   s      	31222 	64555 }}S))"4<x@@@@#||~~8  -299;;
 26,/"(~//!%r!   redirect_urir	   c                  |                      |           |                                 }t          t          j        d                    }t          t          j        |                    d                                                              }t          t          j        d                    }d| j	        || j
        ||dd}|d          dt          j                            |           }d	d
| d| i}t          ||          S )N@   ascii    codeS256)response_typer4   rH   scopestatecode_challengecode_challenge_methodauthorization_endpoint?hermes_session_pkcezstate=z
;verifier=)redirect_urlcookie_payload)_validate_redirect_uri_get_discoveryr    secretstoken_byteshashlibsha256encodedigestr;   r>   r(   r)   	urlencoder	   )	rF   rH   discocode_verifierrR   rQ   paramsrW   rX   s	            r   start_loginz"SelfHostedOIDCProvider.start_login   s   ##L111##%%&w':2'>'>??'N=//8899@@BB
 
 w226677 $(\,%+
 
 -.QQ1G1G1O1OQQ 	 "#LE#L#L]#L#L
 |NSSSSr!   rM   rQ   rc   r   c                   |}|                                  }d||| j        |d}|                     |          \  }}	|                    |           |                     |d         |t
          |	          S )Nauthorization_code)
grant_typerM   rH   r4   rc   token_endpoint)bad_request_excextra_headers)rZ   r;   _token_endpoint_authupdate	_exchanger   )
rF   rM   rQ   rc   rH   _rb   data
extra_datark   s
             r   complete_loginz%SelfHostedOIDCProvider.complete_login   s     ##%% /(*
 
 %)$=$=e$D$D!
MJ~~"#,'	  
 
 	
r!   refresh_tokenc                  |st          d          |                                 }d| j        || j        d}|                     |          \  }}|                    |           |                     |d         |t           ||          S )Nz#no refresh token present in sessionrs   )rh   r4   rs   rP   ri   )rj   previous_refresh_tokenrk   )r   rZ   r;   r>   rl   rm   rn   )rF   rs   rb   rp   rq   rk   s         r   refresh_sessionz&SelfHostedOIDCProvider.refresh_session  s     	M%&KLLL##%% ** \
 
 %)$=$=e$D$D!
MJ~~"#/#0'  
 
 	
r!   access_tokenOptional[Session]c                   	 |                      |          }n# t          $ r Y d S t          $ r  w xY w|                     |d|          S )Nr   id_tokenrs   claims)_verify_id_tokenr   r
   _session_from_tokens)rF   rw   r|   s      r   verify_sessionz%SelfHostedOIDCProvider.verify_session.  sy    	**<88FF 	 	 	44 	 	 		 ((!F ) 
 
 	
s    
00c                  |sd S 	 |                                  }n# t          $ r Y d S w xY wt          |                    d          pd                                          }|sd S |d| j        d}ddi}|                     |          \  }}|                    |           |                    |           	 t          j	        |||t                     n2# t          $ r%}t                              d|           Y d }~nd }~ww xY wd S )	Nrevocation_endpointr   rs   )tokentoken_type_hintr4   Acceptapplication/jsonrp   headerstimeoutz-self-hosted OIDC: revoke failed (ignored): %s)rZ   r
   r   getr<   r;   rl   rm   httpxpost_TOKEN_ENDPOINT_TIMEOUT_SEC	Exceptionloggerdebug)	rF   rs   rb   endpointrp   r   rq   rk   excs	            r   revoke_sessionz%SelfHostedOIDCProvider.revoke_sessionA  sc     	4	''))EE 	 	 	44	uyy!677=2>>DDFF 	4".
 

 /0 %)$=$=e$D$D!
MJ}%%%	OJ3	      	O 	O 	OLLH#NNNNNNNN	Ots$    
))8C 
D D  Drb   Dict[str, Any]%tuple[Dict[str, str], Dict[str, str]]c                   | j         si i fS |                    d          pg }d|v od|v}|rd| j         ii fS t          j                            | j        d           dt          j                            | j         d           }t          j        |                    d                    	                    d	          }i d
d| ifS )ua  Return ``(extra_data, extra_headers)`` for token-endpoint client auth.

        Public client (no ``client_secret`` configured): returns ``({}, {})`` —
        the exchange authenticates with PKCE alone, exactly as before this
        method existed.

        Confidential client (``client_secret`` set): authenticates the client
        per RFC 6749 §2.3.1, choosing the method from the IDP's advertised
        ``token_endpoint_auth_methods_supported``:

          * ``client_secret_post`` advertised (and ``client_secret_basic`` not)
            → secret in the form body.
          * otherwise → HTTP Basic ``Authorization`` header (the OIDC default;
            also the fallback when the IDP advertises nothing).

        PKCE's ``code_verifier`` is sent regardless by the callers — the secret
        is layered on top, never a replacement.
        %token_endpoint_auth_methods_supportedclient_secret_postclient_secret_basicr2   r   )safe:zutf-8rK   AuthorizationzBasic )
r?   r   r(   r)   quoter;   r   	b64encoder_   r   )rF   rb   methodsprefer_postuserpassencodeds         r   rl   z+SelfHostedOIDCProvider._token_endpoint_authf  s    * " 	r6M))CDDJ G+ 5%W4 	  	>#T%892== |!!$/!;; B B|!!$"5B!??B B 	 "8??7#;#;<<CCGLLO%7g%7%7888r!   N)ru   rk   ri   rp   Dict[str, str]rj   type[Exception]ru   rk   Optional[Dict[str, str]]c                  ddi}|r|                     |           	 t          j        |||t                    }n*# t          j        $ r}t          d|           |d}~ww xY w|j        dk    r9|                     |          }	|	                    dd          }
 |d	|
           |j        d
k    r't          d|j         d|j	        dd
                   |                     |          }|                    d          }|rt          |t                    st          d          t          |                    dd                                                    }|r|dk    rt          d|          |                     |          }|                    d          }t          |t                    r|s|pd}|                     |||          S )u  POST the token endpoint and turn the response into a Session.

        Shared by ``complete_login`` (auth-code grant) and ``refresh_session``
        (refresh grant). ``bad_request_exc`` is raised on a 400 —
        ``InvalidCodeError`` for the auth-code path, ``RefreshExpiredError``
        for the refresh path — preserving the middleware's distinct handling.

        ``extra_headers`` carries the confidential-client ``Authorization``
        header when one applies (see ``_token_endpoint_auth``); it is empty for
        a public client, so the request is byte-identical to the pre-
        confidential-client behaviour in that case.
        r   r   r   z!OIDC token endpoint unreachable: Ni  errorinvalid_requestzIDP rejected token request:    zOIDC token endpoint returned z: r{   u   OIDC token response missing id_token — ensure the 'openid' scope is configured and the client is allowed to receive an ID token.
token_typer   bearerzunexpected token_type=rs   rz   )rm   r   r   r   RequestErrorr
   status_code_parse_json_bodyr   text
isinstancer   lowerr}   r~   )rF   ri   rp   rj   ru   rk   r   responser   body
error_codepayloadr{   r   r|   rs   s                   r   rn   z SelfHostedOIDCProvider._exchange  sP   * /0 	*NN=)))
	z3	  HH ! 	 	 	9C99 	
 3&&((22D'+<==J!/;z;;   3&&+0D + +=#&+ +  
 ''11;;z** 	z(C88 	   \26677==??
 	I*00 G G GHHH&&x00
  O44--- 	9] 	928bM((]6 ) 
 
 	
s   ; A"
AA"c                   t          j                     }| j        || j        z
  t          k     r| j        S | j        5  t          j                     }| j        &|| j        z
  t          k     r| j        cddd           S |                                 }|| _        || _        d| _        |cddd           S # 1 swxY w Y   dS )z=Return the cached OIDC discovery document, fetching if stale.N)timer@   rA   _DISCOVERY_CACHE_TTL_SECrD   _fetch_discoveryrE   )rF   nowrb   s      r   rZ   z%SelfHostedOIDCProvider._get_discovery  s!   ikkO't115MMM?"! 	 	)++C+4559QQQ	 	 	 	 	 	 	 	 ))++E#DO),D& !%D	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	s   4B4=*B44B8;B8c                    | j          dS )Nz!/.well-known/openid-configuration)r:   )rF   s    r   _discovery_urlz%SelfHostedOIDCProvider._discovery_url  s    ,AAAAr!   c                   |                                  }	 t          j        |ddit          d          }n*# t          j        $ r}t          d|           |d }~ww xY w|j        dk    rt          d|j         d|          |                     |          }|st          d	          t          |                    d
d          pd          	                                }t          |                    dd          pd          	                                }t          |                    dd          pd          	                                }|r|r|st          d          t          |                    dd          pd          	                                }|r8|
                    d          | j        k    rt          d|d| j                  t          |d
           t          |d           t          |d           t          |                    dd          pd          	                                }	|                    d          }
t          |
t                    rd |
D             ng }|p| j        ||||	|dS )Nr   r   T)r   r   follow_redirectszOIDC discovery unreachable: r   zOIDC discovery returned z for z'OIDC discovery returned a non-JSON bodyrT   r   ri   jwks_urizPOIDC discovery missing one of authorization_endpoint / token_endpoint / jwks_urir3   r7   z4OIDC discovery issuer mismatch: document advertises z but configured issuer is r8   r   r   c                ,    g | ]}t          |          S  r   ).0ms     r   
<listcomp>z;SelfHostedOIDCProvider._fetch_discovery.<locals>.<listcomp>C  s    ...SVV...r!   )r3   rT   ri   r   r   r   )r   r   r   _DISCOVERY_TIMEOUT_SECr   r
   r   r   r   r<   r   r:   r.   r   list)rF   r"   r   r   r   rT   ri   r   advertised_issuerr   auth_methods_rawtoken_endpoint_auth_methodss               r   r   z'SelfHostedOIDCProvider._fetch_discovery  s   !!##	O  y!#56.!%	  HH ! 	O 	O 	O Ds D DEE3N	O3&&M8+?MMcMM   ''11 	K IJJJ!$KK0"55;"
 "

%'' 	 W[[)92>>D"EEKKMMw{{:r228b99??AA% 	^ 	8 	,    Hb 9 9 ?R@@FFHH 	!2!9!9#!>!>$,!N!N$$$ $<$ $   	#"*B	
 	
 	
 	
 	#>9IJJJJ"8:>>>>!KK-r228b
 

%'' 	 #;;'NOO *D11..-.... 	$ (74<&<, #65P
 
 	
s   6 AAAr   c                    | j         8ddlm} |                                 } ||d         dt                    | _         | j         S )Nr   )PyJWKClientr   T)
cache_keyslifespan)rE   jwtr   rZ   _JWKS_CACHE_SECONDS)rF   r   rb   s      r   _get_jwks_clientz'SelfHostedOIDCProvider._get_jwks_clientS  s`    $''''''''))E +j!,! ! !D
   r!   r{   c           
        dd l }|                                 }	 |                                                     |          }nE# |j        $ r}t          d|           |d }~wt          $ r}t          d|          |d }~ww xY w	 |                    ||j        t          t                    | j        |d         dg di          }n# |j        $ r}t          d|           |d }~w|j        $ r}d}	 |                    |d	d	d
          }d|                    d          d|                    d          d|d         d| j        d	}n# t          $ r Y nw xY wt          d| |           |d }~ww xY w|S )Nr   zJWKS lookup failed: r3   require)expiataudisssub)
algorithmsaudiencer3   optionszID token expired: r   F)verify_signature
verify_exp)r   z [token iss=r   z aud=r   z; expected iss=]zID token verification failed: )r   rZ   r   get_signing_key_from_jwtPyJWKClientErrorr
   r   r   keyr   _ALLOWED_ID_TOKEN_ALGSr;   ExpiredSignatureErrorr   InvalidTokenErrorr   )	rF   r{   r   rb   signing_keyr   r|   details
unverifieds	            r   r}   z'SelfHostedOIDCProvider._verify_id_token_  sF   


##%%	I//11JJ KK # 	G 	G 	G <s < <==3F 	I 	I 	I >s > >??SH	I!	ZZ 677X"$G$G$GH    FF ( 	H 	H 	H"#=#=#=>>CG$ 	 	 	
 G ZZ16eLL (  

0:>>%#8#8 0 0%>>%000 0$)(O0 0  ?0 0 0     ??g?? '	. sm   'A 
BAB,A??BAC
 

E<C''E<4E77AEE7
EE7EE77E<r|   c                  t          |                    dd                    }|st          d          t          |                    dd          pd          }t          |                    d          p-|                    d          p|                    d          p|pd          }|                    d          p|                    d	          pd}|sK|                    d
          }t          |t                    r!|rd                    d |D                       }t          |pd          }t          ||||| j        t          |d                   ||          S )u\  Map verified OIDC claims onto a Session.

        The verified ID token is stored in ``Session.access_token`` so the
        per-request ``verify_session`` re-verifies a real JWT. The opaque
        OAuth access token is intentionally NOT stored — Hermes does not call
        any resource API with it; the dashboard only needs identity.
        r   r   z&ID token missing 'sub' (user_id) claimemailnamepreferred_usernamenicknameorg_idorganizationgroups,c              3  4   K   | ]}t          |          V  d S )Nr   )r   gs     r   	<genexpr>z>SelfHostedOIDCProvider._session_from_tokens.<locals>.<genexpr>  s(      !9!9Q#a&&!9!9!9!9!9!9r!   r   )user_idr   display_namer   provider
expires_atrw   rs   )	r   r   r
   r   r   joinr   r   int)	rF   r{   rs   r|   r   r   r   r   r   s	            r   r~   z+SelfHostedOIDCProvider._session_from_tokens  s{    fjj++,, 	J HIIIFJJw++1r22JJv zz.//zz*%%  
 
 H%%IN)C)CIr 	:ZZ))F&$'' :F :!9!9&!9!9!999V\r""%Y6%=))!'	
 	
 	
 		
r!   c                    t           j                            |          }|j        dvrt	          d|          |j        r|j                            d          st	          d|          dS )u  Fast-fail obviously-broken redirect_uris before bouncing to the IDP.

        The IDP's own allowlist is authoritative; this just catches the common
        operator-error case with a clear message. We allow any ``http://`` host
        (not just localhost) so self-hosted dashboards reached over plain HTTP —
        LAN IPs, internal hostnames, reverse proxies that terminate TLS upstream
        — are not rejected here; the IDP makes the final call on which
        redirect_uris are permitted. Mirrors the nous provider.
        )r%   r&   z"redirect_uri must be http(s), got z/auth/callbackz6redirect_uri path must end with '/auth/callback', got N)r(   r)   r*   r+   r
   pathendswith)rF   rH   r-   s      r   rY   z-SelfHostedOIDCProvider._validate_redirect_uri  s     &&|44= 111E\EE   { 	&+"6"67G"H"H 	(#( (  	 	r!   r   httpx.Responsec                    |j                             dd          }|                    d          si S 	 |                                }n# t          $ r i cY S w xY wt          |t                    r|ni S )Nzcontent-typer   r   )r   r   
startswithjsonr9   r   dict)rF   r   ctyper   s       r   r   z'SelfHostedOIDCProvider._parse_json_body  s     $$^R88 233 	I	==??DD 	 	 	III	!$--5tt25s   A	 	AA)
r3   r   r4   r   r1   r   r2   r   r   r5   )rH   r   r   r	   )
rM   r   rQ   r   rc   r   rH   r   r   r   )rs   r   r   r   )rw   r   r   rx   )rs   r   r   r5   )rb   r   r   r   )ri   r   rp   r   rj   r   ru   r   rk   r   r   r   )r   r   )r   r   )r   r   )r{   r   r   r   )r{   r   rs   r   r|   r   r   r   )rH   r   r   r5   )r   r   r   r   )__name__
__module____qualname____doc__r   r   r=   rG   re   rr   rv   r   r   rl   rn   rZ   r   r   r   r}   r~   rY   r   r   r!   r   r0   r0      s       RRD%L & &  &  &  &  &  &HT T T T:
 
 
 
>
 
 
 
4
 
 
 
&! ! ! !J*9 *9 *9 *9d ')26I
 I
 I
 I
 I
 I
Z   .B B B BU
 U
 U
 U
r
! 
! 
! 
!1 1 1 1j.
 .
 .
 .
`   *6 6 6 6 6 6r!   r0   r   c                     	 ddl m} m}  |            }n4# t          $ r'}t                              d|           i cY d}~S d}~ww xY w | |ddd          }t          |t                    r|ni S )u  Return the ``dashboard.oauth`` block from config.yaml, or ``{}``.

    Robust to load_config() raising, the ``dashboard`` key being absent or
    non-dict, and ``oauth`` being present but not a dict — each falls through
    to ``{}`` so callers can rely on ``.get(...)``.
    r   )cfg_getload_configz[dashboard-auth-self-hosted: load_config() raised %s; falling back to env-only configurationN	dashboardoauth)default)hermes_cli.configr  r  r   r   r   r   r   )r  r  cfgr   sections        r   _load_config_oauth_sectionr    s    
::::::::kmm   5	
 	
 	

 						 gc;>>>G $//777R7s    
AAAAoauth_sectionc                ^    |                      d          }t          |t                    r|ni S )z@Return the ``dashboard.oauth.self_hosted`` sub-block, or ``{}``.self_hosted)r   r   r   )r  r   s     r   _oidc_subsectionr    s.    


M
*
*CS$''/33R/r!   env_var	cfg_valuer   c                    t           j                            | d                                          }|r|S t	          |pd                                          S )zenv-wins-config with empty-is-unset precedence.

    1. ``env_var`` when non-empty after strip (an empty provisioned secret
       must not shadow a valid config.yaml entry).
    2. ``cfg_value`` from config.yaml.
    3. Empty string.
    r   )osenvironr   r<   r   )r  r  envs      r   _resolve_settingr    sP     *.."
%
%
+
+
-
-C
 
yB%%'''r!   r5   c           	     .   da t                      }t          |          }t          d|                    d                    }t          d|                    d                    }t          d|                    d                    pt
          }t          d|                    d	                    }|r|sEd
t          |          dt          |          da t                              dt                      dS 	 t          ||||          }nD# t          t          f$ r0}d| a t                              dt                      Y d}~dS d}~ww xY w|                     |           t                              d|||t          |                     dS )u  Plugin entry — called by the plugin loader at startup.

    Registers :class:`SelfHostedOIDCProvider` only when both an issuer and a
    client_id are configured (via ``HERMES_DASHBOARD_OIDC_*`` env vars or the
    ``dashboard.oauth.self_hosted`` block in config.yaml). Operator-owned
    loopback / ``--insecure`` dashboards leave these unset, so the plugin is a
    no-op for them.

    On skip, writes a reason to :data:`LAST_SKIP_REASON` that names BOTH
    configuration surfaces so operators don't guess wrong about which to set.
    r   HERMES_DASHBOARD_OIDC_ISSUERr3   HERMES_DASHBOARD_OIDC_CLIENT_IDr4   HERMES_DASHBOARD_OIDC_SCOPESr1   #HERMES_DASHBOARD_OIDC_CLIENT_SECRETr2   u:  Self-hosted OIDC dashboard auth is not configured. Set both an issuer and a client_id — either as env vars (HERMES_DASHBOARD_OIDC_ISSUER + HERMES_DASHBOARD_OIDC_CLIENT_ID) or under dashboard.oauth.self_hosted.{issuer,client_id} in config.yaml — or pass --insecure to skip the OAuth gate entirely. (issuer set: z; client_id set: )zdashboard-auth-self-hosted: %sN)r3   r4   r1   r2   z,SelfHostedOIDCProvider construction failed: zedashboard-auth-self-hosted: registered provider (issuer=%s, client_id=%s, scopes=%r, confidential=%s))r   r  r  r  r   r=   boolr   r   r0   r9   r
   warning register_dashboard_auth_providerinfo)	ctxr  oidc_cfgr3   r4   r1   r2   r   r   s	            r   registerr'    s    .00M..H&X(>(> F !)8<<+D+D I 	7h9O9OPP 	  %-x||O/L/L M    F||||T)____. 	 	57GHHH)'	
 
 
 &   @3@@ 	 	79IJJJ ((222
KK	@]    s   =D E"%EE)r   r   r   r   )r"   r   r#   r   r   r   )r   r   )r  r   r   r   )r  r   r  r   r   r   )r   r5   )*r  
__future__r   r   r]   loggingr  r[   rB   r   urllib.parser(   typingr   r   r   r   hermes_cli.dashboard_authr   r   r	   r
   r   r   	getLoggerr  r   r=   r   r   r   r   r   r   __annotations__r    r.   r0   r  r  r  r'  r   r!   r   <module>r/     s  I I IV # " " " " "    				           & & & & & & & & & &                 
	8	$	$ ) P   " 
           ? ? ? ?
   6s6 s6 s6 s6 s62 s6 s6 s6v8 8 8 8,0 0 0 0( ( ( (G G G G G Gr!   