tawasuyu

tawasuyu/brahman

Rust 572TOML 217Markdown 68JSON 198326 Definitions

⎇ bb21c28eb158d264c7b03f48fba731a75c1648f0 crates/protocol/brahman-auth/src/lib.rs Rust · 141 lines · 5.1 KB History Who touched what raw

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