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