gioser

tawasuyu/brahman

Rust 326TOML 123JSON 14Markdown 114554 Sutichaykuna

⎇ 8a15b812f9507defc8726286bd4a541f1f789243 crates/protocol/brahman-auth/src/pam_backend.rs Rust · 120 siq'ikuna · 4.1 KB Ñawpa willay Pi imata llamkarqan ch'uya

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

1//! Backend PAM del contrato [`Authenticator`](crate::Authenticator).
2//!
3//! El módulo se llama `pam_backend` (no `pam`) para no chocar con el
4//! crate externo `pam`, del que depende.
5
6use pam::{Client, PamError, PamReturnCode};
7
8use crate::{resolve_user, AuthError, Authenticator, UserInfo};
9
10/// Servicio PAM por defecto del escritorio carmen. Resuelve a
11/// `/etc/pam.d/carmen` — ver el archivo `data/carmen` de este crate.
12pub const DEFAULT_SERVICE: &str = "carmen";
13
14/// Autentica contra PAM: el mismo subsistema de `login`/`sudo`. Honra
15/// `/etc/pam.d/<service>` — módulos, 2FA, llaves FIDO2, `pam_faillock`,
16/// lo que el administrador configure ahí, sin que `brahman-auth` lo sepa.
17#[derive(Debug, Clone)]
18pub struct PamAuthenticator {
19    service: String,
20}
21
22impl PamAuthenticator {
23    /// Autenticador para un servicio PAM concreto (`/etc/pam.d/<service>`).
24    pub fn new(service: impl Into<String>) -> Self {
25        Self {
26            service: service.into(),
27        }
28    }
29
30    /// Autenticador para el servicio por defecto del escritorio,
31    /// [`DEFAULT_SERVICE`].
32    pub fn carmen() -> Self {
33        Self::new(DEFAULT_SERVICE)
34    }
35
36    /// Nombre del servicio PAM que usa este autenticador.
37    pub fn service(&self) -> &str {
38        &self.service
39    }
40}
41
42impl Default for PamAuthenticator {
43    fn default() -> Self {
44        Self::carmen()
45    }
46}
47
48impl Authenticator for PamAuthenticator {
49    fn authenticate(&self, username: &str, secret: &str) -> Result<UserInfo, AuthError> {
50        // Un handle PAM nuevo por intento: PAM es stateful por
51        // transacción y reusar el handle entre intentos es un bug. El
52        // `Client` cierra la transacción (`pam_end`) en su `Drop`.
53        let mut client = Client::with_password(&self.service)
54            .map_err(|e| AuthError::Pam(format!("pam_start({}): {e}", self.service)))?;
55        client.conversation_mut().set_credentials(username, secret);
56
57        // `authenticate()` del crate hace pam_authenticate + pam_acct_mgmt:
58        // cubre credenciales Y estado de la cuenta en un solo paso.
59        client.authenticate().map_err(map_pam_error)?;
60
61        // Credenciales válidas: resolvemos la identidad del sistema.
62        resolve_user(username)
63    }
64}
65
66/// Traduce un error de PAM a la taxonomía gruesa de [`AuthError`].
67fn map_pam_error(err: PamError) -> AuthError {
68    match err.0 {
69        // Credenciales: el greeter debe dejar reintentar.
70        PamReturnCode::Auth_Err
71        | PamReturnCode::User_Unknown
72        | PamReturnCode::Cred_Insufficient
73        | PamReturnCode::MaxTries => AuthError::BadCredentials,
74
75        // Cuenta válida pero vetada o que requiere una acción.
76        PamReturnCode::Acct_Expired => AuthError::AccountUnavailable("la cuenta expiró".into()),
77        PamReturnCode::Cred_Expired => {
78            AuthError::AccountUnavailable("las credenciales expiraron".into())
79        }
80        PamReturnCode::AuthTok_Expired => {
81            AuthError::AccountUnavailable("la contraseña expiró".into())
82        }
83        PamReturnCode::New_Authtok_Reqd => {
84            AuthError::AccountUnavailable("requiere cambiar la contraseña".into())
85        }
86        PamReturnCode::Perm_Denied => {
87            AuthError::AccountUnavailable("acceso denegado por política".into())
88        }
89
90        // Todo lo demás: fallo de infraestructura PAM.
91        other => AuthError::Pam(format!("{other:?}")),
92    }
93}
94
95#[cfg(test)]
96mod tests {
97    use super::*;
98
99    #[test]
100    fn carmen_uses_default_service() {
101        assert_eq!(PamAuthenticator::carmen().service(), DEFAULT_SERVICE);
102        assert_eq!(PamAuthenticator::default().service(), "carmen");
103    }
104
105    #[test]
106    fn custom_service_name() {
107        assert_eq!(PamAuthenticator::new("login").service(), "login");
108    }
109
110    #[test]
111    fn unknown_service_fails_gracefully() {
112        // Sin `/etc/pam.d/<servicio>` PAM cae a `other` (deny). Debe
113        // devolver un `AuthError`, nunca paniquear.
114        let auth = PamAuthenticator::new("brahman-auth-servicio-inexistente-xyz");
115        assert!(
116            auth.authenticate("root", "contraseña-cualquiera").is_err(),
117            "un servicio inexistente debe fallar limpio"
118        );
119    }
120}