
    `gjG                    l   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+d"dZ,y)#u
  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_REASONc                h   t        j                  t              }t        j                  | j                  d      |t        t        t        t        d      }dt         dt         dt         dt        j                  |      j                          dt        j                  |      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)passwordr   dks      X/root/.hermes/venv/lib/python3.12/site-packages/plugins/dashboard_auth/basic/__init__.pyhash_passwordr,   s   s     12D	 



B )Ai[)AD!((*
+1V-=-=b-A-H-H-J,K	M    c           
        	 |j                  d      \  }}}}}}|dk7  ryt        |      t        |      t        |      }
}	}t        j                  |      }t        j                  |      }	 t        j                  | j                  d      |||	|
t        |      d      }t        j                  ||      S # t        t
        f$ r Y yw xY w# t        t        f$ r Y yw xY w)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)   encodedschemen_sr_sp_ssalt_b64dk_b64r   r   r   r   expectedactuals                 r+   _verify_passwordrA      s    29--2D/S#xXc(CHc#ha1)##F+OOG$h-
 vx00 	"  $ s)   B6 AB6 ,4C 6CCCCz'dummy-password-for-constant-time-verifyc                    t        j                  | d      j                         }t        j                  ||t
        j                        j                         }t        j                  ||z         j                         S )N),:)
separators)jsondumpsr!   r6   newr   sha256digestr&   urlsafe_b64encoder(   )payloadsecretrawsigs       r+   _signrP      sX    
**W
4
;
;
=C
((63
/
6
6
8C##C#I.5577r-   c                t   	 t        j                  | j                               }t        |      t        k  ry |d t          |t         d  }}t        j                  ||t        j                        j                         }t        j                  ||      sy t        j                  |      S # t        $ r Y y w xY wN)r&   urlsafe_b64decoder!   r4   _SIG_LENr6   rH   r   rI   rJ   r7   rF   loads	Exception)tokenrM   blobrN   rO   r?   s         r+   _unsignrY      s    
''7t9 
(#T8)*%5S88FC8??A""31zz# s   5B+ AB+ B+ +	B76B7c                      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y)BasicAuthProviderz?Username/password provider with stateless HMAC-signed sessions.basiczUsername & PasswordT)ttl_secondsc                   |st        d      |st        d      t        |      dk  rt        d      || _        || _        || _        t        dt        |            | _        y )Nzusername must be non-emptyzpassword_hash must be non-emptyr   z secret must be at least 16 bytes<   )r2   r4   	_username_password_hash_secretmaxr0   _ttl)selfusernamepassword_hashrM   r]   s        r+   __init__zBasicAuthProvider.__init__   sc     9::>??v;?@@!+C,-	r-   c                   t        d      )NzzBasicAuthProvider is password-only; there is no OAuth redirect flow. The login page POSTs to /auth/password-login instead.NotImplementedError)re   redirect_uris     r+   start_loginzBasicAuthProvider.start_login   s    !J
 	
r-   c                   t        d      )Nz@BasicAuthProvider is password-only; use complete_password_login.rj   )re   codestatecode_verifierrl   s        r+   complete_loginz BasicAuthProvider.complete_login   s     "N
 	
r-   c                  t        j                  |j                  d      | j                  j                  d            }|r| j                  nt
        }t        ||      }|r|st        d      | j                  | j                        S )Nr   zinvalid username or password)	r6   r7   r!   r`   ra   _DUMMY_HASHrA   r   _mint_session)re   rf   r)   username_oktarget_hashpassword_oks         r+   complete_password_loginz)BasicAuthProvider.complete_password_login   st     ))OOG$dnn&;&;G&D
 .9d))k&x=)*HII!!$..11r-   c                   t        || j                        }|D|j                  d      dk7  s0|j                  dd      t        t	        j                               k  ry | j                  |d|      S )Nkindaccessexpr   r   )rY   rb   getr0   time_session_from_payload)re   access_tokenrL   s      r+   verify_sessionz BasicAuthProvider.verify_session  s^    ,5O{{6"h.{{5!$DIIK(88)),GDDr-   c               R   |st        d      t        || j                        }|D|j                  d      dk7  s0|j                  dd      t	        t        j
                               k  rt        d      | j                  t        |j                  d| j                                    S )Nz#no refresh token present in sessionr{   refreshr}   r   z refresh token expired or invalidsub)	r	   rY   rb   r~   r0   r   ru   r   r`   )re   refresh_tokenrL   s      r+   refresh_sessionz!BasicAuthProvider.refresh_session  s    %&KLL-6O{{6"i/{{5!$DIIK(88%&HII!!#gkk%&H"IJJr-   c                   |}y rR    )re   r   _s      r+   revoke_sessionz BasicAuthProvider.revoke_session  s     r-   c           
     
   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   user_idemaildisplay_nameorg_idprovider
expires_atr   r   )r0   r   rd   rP   rb   _REFRESH_TTL_SECONDSr
   name)re   r   nowr}   r   r   s         r+   ru   zBasicAuthProvider._mint_session%  s    $))+DIIoXc:DLL
 Ys=Q7QRLL
  YY%'	
 		
r-   c                    t        |j                  dd            }t        |d|d| j                  t	        |d         ||      S )Nr   r   r}   r   )r   r~   r
   r   r0   )re   r   r   rL   r   s        r+   r   z'BasicAuthProvider._session_from_payload:  sK     gkk%,- YY75>*%'	
 		
r-   N)
rf   r   rg   r   rM   bytesr]   r0   returnNone)rl   r   r   r   )
ro   r   rp   r   rq   r   rl   r   r   r
   )rf   r   r)   r   r   r
   )r   r   r   zOptional[Session])r   r   r   r
   )r   r   r   r   )r   r   r   r
   )r   r   r   r   rL   dictr   r
   )__name__
__module____qualname____doc__r   r   supports_password_DEFAULT_TTL_SECONDSrh   rm   rr   ry   r   r   r   ru   r   r   r-   r+   r[   r[      s    ID(L 0. . 	.
 . . 
.*


#&
7:
JM
	
22*-2	2&E
K
*

03
>B
	
r-   r[   c                     	 ddl m} m}  |       } | |ddd      }t        |t              r|S i S # t        $ r"}t        j                  d|       i cY d}~S d}~ww xY w)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   rV   loggerdebug
isinstancer   )r   r   cfgexcsections        r+   _load_config_basic_auth_sectionr   O  sd    
:m c;dCG $/77R7  5	

 	s   1 	AAAAc                    t         j                  j                  | d      j                         }|r|S t	        |j                  |d      xs d      j                         S )z<Env-wins-over-config resolution; empty env treated as unset.r   )osenvironr~   stripr   )env_namecfg_sectioncfg_keyenvs       r+   _resolver   d  sK    
**..2
&
,
,
.C

{w+1r288::r-   c                @   t        d| d      }|s*t        j                  d       t        j                  d      S t
        j                  t        j                  fD ]  }	  ||      }t        |      dk\  r|c S  |j                  d      S # t        t        f$ r Y Bw xY w)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_SECRETrM   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&   r1   r   fromhexr4   r2   r3   r!   )r   rN   decoderdecodeds       r+   _resolve_secretr   l  s     ,k8C 	
 ""2&&$$emm4 	clG7|r! " ::g I& 		s   BBBc                *   da t               }t        d|d      }t        d|d      }t        d|d      }t        d|d	      }|sd
a t        j	                  dt                y|s|sda t        j                  dt                yt        j                  j                  dd      j                         }|r!t        |      }t        j                  d       n"|s t        |      }t        j                  d       t        |      }	 |rt        |      nt        }	 t!        ||||      }	| j#                  |	       t        j                  d|       y# t        $ r	 t        }Y Iw xY w# t        $ r)}
d|
 a t        j                  dt                Y d}
~
yd}
~
ww xY w)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_USERNAMErf   )HERMES_DASHBOARD_BASIC_AUTH_PASSWORD_HASHrg   $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.)rf   rg   rM   r]   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   r0   r   r2   r[    register_dashboard_auth_provider)ctxr   rf   rg   	plaintextttl_rawplaintext_from_envrM   ttlr   r   s              r+   registerr     s    -/G.H 3WoM .I 17<QG L 	 	/1AB; 	 	13CD .eg  %&894	
 %i0=	
 W%F#%c'l+?
$'	
 ((2
KKJ!  #"#  DSEJ13CDs*    E E  EE 	F)FF)r)   r   r   r   )r)   r   r8   r   r   bool)rL   r   rM   r   r   r   )rW   r   rM   r   r   zOptional[dict])r   r   )r   r   r   r   r   r   r   r   )r   r   r   r   )r   r   )-r   
__future__r   r&   r   r6   rF   loggingr   r   r   typingr   r   hermes_cli.dashboard_authr   r   r   r	   r
   	getLoggerr   r   r   r   r"   r#   r$   r%   r   rI   digest_sizerT   r   __annotations__r,   rA   rt   rP   rY   r[   r   r   r   r   r   r-   r+   <module>r      s   7r #      	      
		8	$ $ ( 
 			 
 7>>''  # 01: EF8&~
- ~
L8*;<ar-   