
    #jG                    h   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 ddl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Z ej                    j        Z da!de"d<   d)dZ#d*dZ$ e#d          Z%d+dZ&d,dZ' G d d e          Z(d-d!Z)d.d%Z*d/d&Z+d0d(Z,dS )1u
  BasicAuthProvider — username/password dashboard auth (no OAuth IDP).

A self-hosted "just put a password on my dashboard" provider. It plugs
into the same ``DashboardAuthProvider`` framework as the Nous OAuth
provider, but authenticates with a username + password instead of an
OAuth redirect: it sets ``supports_password = True`` and implements
``complete_password_login``. The login page renders a credential form for
it; everything downstream of login (session cookies, verify, refresh,
ws-tickets, logout) is identical to the OAuth path because a password
session is just a :class:`Session` with provider-minted opaque tokens.

This provider has **no external IDP and no database**. Credentials are
configured up front; sessions are stateless HMAC-signed tokens this
provider mints and verifies itself. That keeps it zero-infrastructure —
appropriate for a single-box self-hosted dashboard.

Configuration surfaces (env wins over config.yaml when set non-empty),
mirroring the Nous provider's precedence convention:

  ``config.yaml`` — canonical surface::

      dashboard:
        basic_auth:
          username: admin               # required
          # Provide EITHER a precomputed scrypt hash (preferred — no
          # plaintext at rest) ...
          password_hash: "scrypt$..."   # see hash_password()
          # ... OR a plaintext password (hashed in-memory at load).
          password: "s3cret"
          secret: "<32+ random bytes, base64 or hex>"  # optional; token-signing key
          session_ttl_seconds: 43200    # optional; access-token lifetime (default 12h)

  Environment overrides::

      HERMES_DASHBOARD_BASIC_AUTH_USERNAME
      HERMES_DASHBOARD_BASIC_AUTH_PASSWORD_HASH   # preferred
      HERMES_DASHBOARD_BASIC_AUTH_PASSWORD        # plaintext fallback
      HERMES_DASHBOARD_BASIC_AUTH_SECRET
      HERMES_DASHBOARD_BASIC_AUTH_TTL_SECONDS

If ``secret`` is not configured, a random per-process secret is generated
at startup. That's fine for a single-process dashboard, but means all
sessions are invalidated on restart and sessions don't survive across
multiple worker processes — set an explicit ``secret`` for stable
multi-worker / restart-surviving sessions.

Password hashing uses stdlib :func:`hashlib.scrypt` (memory-hard, no
third-party dependency). ``complete_password_login`` runs a constant-time
comparison and always performs a hash even for an unknown username, so
the endpoint is not a username-enumeration timing oracle.

Skip reasons:
  Like the Nous provider, this exposes a module-level ``LAST_SKIP_REASON``
  the gate's fail-closed branch can surface when the plugin loads but
  declines to register (no username/password configured).
    )annotationsN)AnyOptional)DashboardAuthProviderInvalidCredentialsError
LoginStartRefreshExpiredErrorSessioni  i ' i @                strLAST_SKIP_REASONpasswordreturnc                   t          j        t                    }t          j        |                     d          |t          t          t          t          d          }dt           dt           dt           dt          j        |                                           dt          j        |                                           
S )a@  Return a ``scrypt$n$r$p$<salt_b64>$<dk_b64>`` hash string.

    Use this to precompute ``password_hash`` for config.yaml so plaintext
    never sits at rest. Exposed as a module function so operators can run
    ``python -c "from plugins.dashboard_auth.basic import hash_password;
    print(hash_password('pw'))"``.
    utf-8r   saltnrpdklenmaxmemzscrypt$$)secretstoken_bytes_SCRYPT_SALT_BYTEShashlibscryptencode	_SCRYPT_N	_SCRYPT_R	_SCRYPT_P_SCRYPT_DKLENbase64	b64encodedecode)r   r   dks      J/home/ubuntu/.hermes/hermes-agent/plugins/dashboard_auth/basic/__init__.pyhash_passwordr-   s   s     122D	  



 
 
B	M) 	M 	Mi 	M 	M) 	M 	MD!!((**	M 	M-3-=b-A-A-H-H-J-J	M 	M    encodedboolc           
        	 |                     d          \  }}}}}}|dk    rdS t          |          t          |          t          |          }
}	}t          j        |          }t          j        |          }n# t          t
          f$ r Y dS w xY w	 t          j        |                     d          |||	|
t          |          d          }n# t          t          f$ r Y dS w xY wt          j        ||          S )z@Constant-time scrypt verify. False on any malformed hash string.r   r"   Fr   r   r   )splitintr(   	b64decode
ValueError	TypeErrorr!   r"   r#   lenMemoryErrorhmaccompare_digest)r   r/   schemen_sr_sp_ssalt_b64dk_b64r   r   r   r   expectedactuals                 r,   _verify_passwordrC      s   29--2D2D/S#xX5c((CHHc#hha1))#F++	"   uuOOG$$h--
 
 
 $   uuvx000s)   "A< AA< <BB;C C&%C&z'dummy-password-for-constant-time-verifypayloaddictsecretbytesc                   t          j        | d                                          }t          j        ||t
          j                                                  }t          j	        ||z             
                                S )N),:)
separators)jsondumpsr#   r9   newr!   sha256digestr(   urlsafe_b64encoder*   )rD   rF   rawsigs       r,   _signrT      sd    
*W
4
4
4
;
;
=
=C
(63
/
/
6
6
8
8C#C#I..55777r.   tokenOptional[dict]c                   	 t          j        |                                           }t          |          t          k    rd S |d t                    |t           d          }}t          j        ||t          j                  	                                }t          j
        ||          sd S t          j        |          S # t          $ r Y d S w xY wN)r(   urlsafe_b64decoder#   r7   _SIG_LENr9   rN   r!   rO   rP   r:   rL   loads	Exception)rU   rF   blobrR   rS   rA   s         r,   _unsignr^      s    
'77t99  4
(
#T8)**%5S8FC88??AA"311 	4z#   tts   >B? A'B? +B? ?
CCc                  l    e Zd ZdZdZdZdZ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$Zd%S )/BasicAuthProviderz?Username/password provider with stateless HMAC-signed sessions.basiczUsername & PasswordT)ttl_secondsusernamer   password_hashrF   rG   rb   r3   r   Nonec                   |st          d          |st          d          t          |          dk     rt          d          || _        || _        || _        t          dt          |                    | _        d S )Nzusername must be non-emptyzpassword_hash must be non-emptyr   z secret must be at least 16 bytes<   )r5   r7   	_username_password_hash_secretmaxr3   _ttl)selfrc   rd   rF   rb   s        r,   __init__zBasicAuthProvider.__init__   s      	;9::: 	@>???v;;?@@@!+C,,--			r.   redirect_urir   c                    t          d          )NzzBasicAuthProvider is password-only; there is no OAuth redirect flow. The login page POSTs to /auth/password-login instead.NotImplementedError)rm   ro   s     r,   start_loginzBasicAuthProvider.start_login   s    !J
 
 	
r.   codestatecode_verifierr
   c                    t          d          )Nz@BasicAuthProvider is password-only; use complete_password_login.rq   )rm   rt   ru   rv   ro   s        r,   complete_loginz BasicAuthProvider.complete_login   s     "N
 
 	
r.   r   c                  t          j        |                    d          | j                            d                    }|r| j        nt
          }t          ||          }|r|st          d          |                     | j                  S )Nr   zinvalid username or password)	r9   r:   r#   rh   ri   _DUMMY_HASHrC   r   _mint_session)rm   rc   r   username_oktarget_hashpassword_oks         r,   complete_password_loginz)BasicAuthProvider.complete_password_login   s     )OOG$$dn&;&;G&D&D
 
 .9Id))k&x== 	J 	J)*HIII!!$.111r.   access_tokenOptional[Session]c                  t          || j                  }|Q|                    d          dk    s8|                    dd          t          t	          j                              k    rd S |                     |d|          S )Nkindaccessexpr   r   )r^   rj   getr3   time_session_from_payload)rm   r   rD   s      r,   verify_sessionz BasicAuthProvider.verify_session  ss    ,55O{{6""h..{{5!$$DIKK(8(8884)),GDDDr.   refresh_tokenc                  |st          d          t          || j                  }|Q|                    d          dk    s8|                    dd          t	          t          j                              k    rt          d          |                     t          |                    d| j                                      S )Nz#no refresh token present in sessionr   refreshr   r   z refresh token expired or invalidsub)	r	   r^   rj   r   r3   r   r{   r   rh   )rm   r   rD   s      r,   refresh_sessionz!BasicAuthProvider.refresh_session  s     	M%&KLLL-66O{{6""i//{{5!$$DIKK(8(888%&HIII!!#gkk%&H&H"I"IJJJr.   c               
    |}d S rX    )rm   r   _s      r,   revoke_sessionz BasicAuthProvider.revoke_session  s     tr.   user_idc           
        t          t          j                              }|| j        z   }t          |d|d| j                  }t          |d|t
          z   d| j                  }t          |d|d| j        |||          S )Nr   )r   r   r   r   r   r   emaildisplay_nameorg_idprovider
expires_atr   r   )r3   r   rl   rT   rj   _REFRESH_TTL_SECONDSr
   name)rm   r   nowr   r   r   s         r,   r{   zBasicAuthProvider._mint_session%  s    $)++DIoXc::DL
 
 Ys=Q7QRRL
 
  Y%'	
 	
 	
 		
r.   rD   rE   c                    t          |                    dd                    }t          |d|d| j        t	          |d                   ||          S )Nr   r   r   r   )r   r   r
   r   r3   )rm   r   r   rD   r   s        r,   r   z'BasicAuthProvider._session_from_payload:  s\     gkk%,,-- Y75>**%'	
 	
 	
 		
r.   N)
rc   r   rd   r   rF   rG   rb   r3   r   re   )ro   r   r   r   )
rt   r   ru   r   rv   r   ro   r   r   r
   )rc   r   r   r   r   r
   )r   r   r   r   )r   r   r   r
   )r   r   r   re   )r   r   r   r
   )r   r   r   r   rD   rE   r   r
   )__name__
__module____qualname____doc__r   r   supports_password_DEFAULT_TTL_SECONDSrn   rs   rx   r   r   r   r   r{   r   r   r.   r,   r`   r`      s        IID(L 0. . . . . .*
 
 
 

 
 
 
2 2 2 2&E E E E
K 
K 
K 
K   
 
 
 
*
 
 
 
 
 
r.   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 ``dashboard.basic_auth`` from config.yaml, or ``{}``.

    Robust to load_config() raising, the keys being absent, or the value
    not being a dict — every shape falls through to ``{}``.
    r   )cfg_getload_configzUdashboard-auth-basic: load_config() raised %s; falling back to env-only configurationN	dashboard
basic_auth)default)hermes_cli.configr   r   r\   loggerdebug
isinstancerE   )r   r   cfgexcsections        r,   _load_config_basic_auth_sectionr   O  s    
::::::::kmm   5	
 	
 	

 						 gc;dCCCG $//777R7s    
AAAAenv_namecfg_sectioncfg_keyc                    t           j                            | d                                          }|r|S t	          |                    |d          pd                                          S )z<Env-wins-over-config resolution; empty env treated as unset.r   )osenvironr   stripr   )r   r   r   envs       r,   _resolver   d  s]    
*..2
&
&
,
,
.
.C
 
{w++1r2288:::r.   c                X   t          d| d          }|s.t                              d           t          j        d          S t
          j        t          j        fD ]<}	  ||          }t          |          dk    r|c S &# t          t          f$ r Y 9w xY w|                    d          S )u   Resolve the token-signing secret.

    Accepts base64 or hex or raw text from config/env. When unset,
    generates a random per-process secret (sessions then don't survive a
    restart or span multiple workers — logged at INFO).
    "HERMES_DASHBOARD_BASIC_AUTH_SECRETrF   zdashboard-auth-basic: no 'secret' configured; generating a random per-process signing key. Sessions will not survive a restart or span multiple workers. Set dashboard.basic_auth.secret (or HERMES_DASHBOARD_BASIC_AUTH_SECRET) for stable sessions.r   r   r   )r   r   infor   r   r(   r4   rG   fromhexr7   r5   r6   r#   )r   rR   decoderdecodeds       r,   _resolve_secretr   l  s     ,k8 C  '	
 	
 	
 "2&&&$em4  	gcllG7||r!! "I& 	 	 	D	::gs   B  BBre   c                   da t                      }t          d|d          }t          d|d          }t          d|d          }t          d|d	          }|s$d
a t                              dt                      dS |s&|s$da t                              dt                      dS t          j                            dd          	                                }|r*t          |          }t                              d           n+|s)t          |          }t                              d           t          |          }	 |rt          |          nt          }n# t          $ r
 t          }Y nw xY w	 t!          ||||          }	n=# t          $ r0}
d|
 a t                              dt                      Y d}
~
dS d}
~
ww xY w|                     |	           t                              d|           dS )u  Plugin entry — registers BasicAuthProvider when credentials exist.

    Loopback / ``--insecure`` operators and anyone using the OAuth
    provider leave ``dashboard.basic_auth`` unset, so this plugin is a
    no-op for them. When username + (password or password_hash) are
    configured, it registers a password provider that the login page
    renders as a credential form.
    r   $HERMES_DASHBOARD_BASIC_AUTH_USERNAMErc   )HERMES_DASHBOARD_BASIC_AUTH_PASSWORD_HASHrd   $HERMES_DASHBOARD_BASIC_AUTH_PASSWORDr   'HERMES_DASHBOARD_BASIC_AUTH_TTL_SECONDSsession_ttl_secondsa*  dashboard.basic_auth.username is not set (and HERMES_DASHBOARD_BASIC_AUTH_USERNAME is empty). Set a username and a password (or password_hash) under dashboard.basic_auth in config.yaml to enable username/password dashboard login, or use the OAuth provider, or pass --insecure to skip the auth gate.zdashboard-auth-basic: %sNu   dashboard.basic_auth.username is set but neither password_hash nor password is configured. Provide one of them (password_hash is preferred — compute it with plugins.dashboard_auth.basic.hash_password).zbdashboard-auth-basic: hashed env-supplied password in-memory (overrides any config password_hash).zdashboard-auth-basic: hashed plaintext password in-memory. For production, precompute dashboard.basic_auth.password_hash and remove the plaintext password from config.)rc   rd   rF   rb   z'BasicAuthProvider construction failed: z@dashboard-auth-basic: registered password provider (username=%s))r   r   r   r   r   warningr   r   r   r   r-   r   r   r3   r   r5   r`    register_dashboard_auth_provider)ctxr   rc   rd   	plaintextttl_rawplaintext_from_envrF   ttlr   r   s              r,   registerr     sr    -//G. H 3Wo M . I 17<Q G  	L 	 	/1ABBB  ; 	 	13CDDD . egg   
%&8994	
 	
 	
 	
  
%i00=	
 	
 	
 W%%F#%?c'lll+? # # #"#
$'	
 
 
    JSJJ13CDDD
 ((222
KKJ    s*   <E E)(E)-F 
F;%F66F;)r   r   r   r   )r   r   r/   r   r   r0   )rD   rE   rF   rG   r   r   )rU   r   rF   rG   r   rV   )r   rE   )r   r   r   rE   r   r   r   r   )r   rE   r   rG   )r   re   )-r   
__future__r   r(   r!   r9   rL   loggingr   r   r   typingr   r   hermes_cli.dashboard_authr   r   r   r	   r
   	getLoggerr   r   r   r   r$   r%   r&   r'   r    rO   digest_sizerZ   r   __annotations__r-   rC   rz   rT   r^   r`   r   r   r   r   r   r.   r,   <module>r      s+  7 7 7r # " " " " "      				                                
	8	$	$ $ ( 
 			 
 7>'         01 1 1 1: mEFF8 8 8 8   &~
 ~
 ~
 ~
 ~
- ~
 ~
 ~
L8 8 8 8*; ; ; ;   <a a a a a ar.   