⎇ 8a15b812f9507defc8726286bd4a541f1f789243
crates/protocol/brahman-auth/src/lib.rs
History
Who touched what
raw
feat(auth): brahman-auth — autenticación del escritorio (PAM + mock)
1//! `brahman-auth` — autenticación del escritorio.
2//!
3//! Contrato [`Authenticator`] agnóstico del backend, con dos
4//! implementaciones:
5//!
6//! - [`PamAuthenticator`] — el camino real: verifica contra PAM
7//! (`/etc/pam.d/<servicio>`), el mismo subsistema que usan `login`,
8//! `sudo` y los gestores de login clásicos. Hereda lo que el
9//! administrador configure ahí (2FA, llaves FIDO2, `pam_faillock`…)
10//! sin que `brahman-auth` tenga que saberlo.
11//! - [`MockAuthenticator`] — credenciales fijas en memoria, para tests
12//! y para iterar el greeter en cajas sin PAM configurado.
13//!
14//! Lo consume el greeter de carmen (mirada): el usuario teclea su
15//! contraseña, el greeter llama a [`Authenticator::authenticate`], y en
16//! éxito recibe un [`UserInfo`] con uid/gid/home/shell — lo que el
17//! compositor necesita para arrancar la sesión.
18
19mod pam_backend;
20mod user;
21
22pub use pam_backend::{PamAuthenticator, DEFAULT_SERVICE};
23pub use user::{resolve_user, UserInfo};
24
25use std::collections::HashMap;
26
27/// Por qué falló una autenticación. Variantes deliberadamente gruesas:
28/// el greeter sólo necesita saber si conviene reintentar (problema de
29/// credenciales) o si la cuenta está vetada — y nunca debe poder
30/// distinguir "usuario inexistente" de "contraseña errada".
31#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
32pub enum AuthError {
33 /// Usuario o contraseña incorrectos. El greeter deja reintentar sin
34 /// revelar cuál de los dos falló.
35 #[error("usuario o contraseña incorrectos")]
36 BadCredentials,
37
38 /// Las credenciales son válidas pero la cuenta está deshabilitada,
39 /// expirada o requiere una acción (cambio de contraseña).
40 #[error("la cuenta no está disponible: {0}")]
41 AccountUnavailable(String),
42
43 /// Fallo del subsistema PAM no atribuible a las credenciales
44 /// (servicio mal configurado, módulo roto, etc.).
45 #[error("fallo de PAM: {0}")]
46 Pam(String),
47
48 /// No se pudo resolver la identidad del usuario en el sistema tras
49 /// una autenticación válida (caso raro: `/etc/passwd` inconsistente).
50 #[error("no se pudo resolver el usuario «{0}» en el sistema")]
51 UnresolvedUser(String),
52}
53
54/// Verifica credenciales y, en éxito, entrega la identidad del sistema.
55///
56/// `&self`: cada llamada es un intento de login independiente. Las
57/// implementaciones crean su propio estado por intento — PAM exige un
58/// handle nuevo por transacción, reusarlo entre intentos es un bug.
59pub trait Authenticator {
60 fn authenticate(&self, username: &str, secret: &str) -> Result<UserInfo, AuthError>;
61}
62
63/// Autenticador de credenciales fijas en memoria. No toca PAM: sirve
64/// para tests y para iterar el greeter en cajas headless sin
65/// `/etc/pam.d` configurado.
66#[derive(Debug, Default, Clone)]
67pub struct MockAuthenticator {
68 creds: HashMap<String, String>,
69}
70
71impl MockAuthenticator {
72 /// Crea un autenticador sin usuarios: todo intento falla con
73 /// [`AuthError::BadCredentials`] hasta registrar alguno.
74 pub fn new() -> Self {
75 Self::default()
76 }
77
78 /// Registra un par usuario/secreto aceptado. Encadenable.
79 pub fn with_user(mut self, username: &str, secret: &str) -> Self {
80 self.creds.insert(username.to_string(), secret.to_string());
81 self
82 }
83}
84
85impl Authenticator for MockAuthenticator {
86 fn authenticate(&self, username: &str, secret: &str) -> Result<UserInfo, AuthError> {
87 // Mismo error para usuario inexistente y para contraseña errada:
88 // no filtra la existencia de cuentas.
89 match self.creds.get(username) {
90 Some(expected) if expected == secret => {
91 // Si el usuario existe en el SO, info real; sino,
92 // sintética (suficiente para tests y dev headless).
93 Ok(resolve_user(username).unwrap_or_else(|_| UserInfo::synthetic(username)))
94 }
95 _ => Err(AuthError::BadCredentials),
96 }
97 }
98}
99
100#[cfg(test)]
101mod tests {
102 use super::*;
103
104 #[test]
105 fn mock_accepts_registered_user() {
106 let auth = MockAuthenticator::new().with_user("sergio", "clave");
107 let info = auth.authenticate("sergio", "clave").expect("debe pasar");
108 assert_eq!(info.name, "sergio");
109 }
110
111 #[test]
112 fn mock_rejects_wrong_password() {
113 let auth = MockAuthenticator::new().with_user("sergio", "clave");
114 assert_eq!(
115 auth.authenticate("sergio", "mala"),
116 Err(AuthError::BadCredentials)
117 );
118 }
119
120 #[test]
121 fn mock_unknown_user_indistinguishable_from_wrong_password() {
122 let auth = MockAuthenticator::new().with_user("sergio", "clave");
123 assert_eq!(
124 auth.authenticate("nadie", "x"),
125 Err(AuthError::BadCredentials)
126 );
127 }
128
129 #[test]
130 fn empty_mock_rejects_everything() {
131 assert!(MockAuthenticator::new().authenticate("root", "").is_err());
132 }
133
134 #[test]
135 fn auth_error_is_displayable() {
136 assert!(!AuthError::BadCredentials.to_string().is_empty());
137 assert!(AuthError::Pam("x".into()).to_string().contains("PAM"));
138 }
139}