⎇ 3dc85ebdcdd5fda0a1be0db5f8fbce5859266f2e
shared/auth/auth-core/src/lib.rs
Ñawpa willay
Pi imata llamkarqan
ch'uya
feat: mirada standalone — compositor Wayland + WM sobre Llimphi (build magro)
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}