gioser

tawasuyu/brahman

Rust 326TOML 123JSON 14Markdown 114554 Definitions

⎇ 8a15b812f9507defc8726286bd4a541f1f789243 crates/protocol/brahman-auth/src/lib.rs Rust · 139 lines · 5.0 KB History Who touched what raw

feat(auth): brahman-auth — autenticación del escritorio (PAM + mock) 8a15b812f · sergio ·

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}