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

Base del DM/greeter de carmen. Contrato Authenticator agnóstico:
authenticate(usuario, secreto) -> UserInfo (uid/gid/home/shell).
PamAuthenticator verifica contra PAM (/etc/pam.d/carmen); MockAuthenticator
con credenciales en memoria para tests. AuthError grueso: BadCredentials
vs AccountUnavailable, sin filtrar existencia de cuentas. resolve_user
vía getpwnam. data/carmen como servicio PAM; ejemplo auth-probe.

11 tests; el camino PAM real se ejercita.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
sergio
2026-05-21 17:47:05 +00:00
parent af3be482a9
commit 8a15b812f9
10 changed files with 572 additions and 2 deletions
+79
View File
@@ -0,0 +1,79 @@
//! Resolución de la identidad de un usuario del sistema.
use std::path::PathBuf;
use crate::AuthError;
/// Identidad de un usuario en el sistema: lo que el compositor necesita
/// para arrancar una sesión — fijar uid/gid, `cd` al home, ejecutar el
/// shell o la sesión de escritorio.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct UserInfo {
/// Nombre de login.
pub name: String,
/// User ID.
pub uid: u32,
/// Group ID primario.
pub gid: u32,
/// Directorio personal.
pub home: PathBuf,
/// Shell de login.
pub shell: PathBuf,
}
impl UserInfo {
/// Identidad sintética para tests y para cajas donde el usuario no
/// está en `/etc/passwd`. **No** representa a un usuario real del SO
/// — no usar para fijar privilegios de un proceso real.
pub fn synthetic(name: &str) -> Self {
Self {
name: name.to_string(),
uid: 1000,
gid: 1000,
home: PathBuf::from(format!("/home/{name}")),
shell: PathBuf::from("/bin/sh"),
}
}
}
/// Resuelve un usuario por nombre vía `getpwnam`. `Err` si no existe o
/// si la consulta a `/etc/passwd` (o NSS) falla.
pub fn resolve_user(name: &str) -> Result<UserInfo, AuthError> {
match nix::unistd::User::from_name(name) {
Ok(Some(u)) => Ok(UserInfo {
name: u.name,
uid: u.uid.as_raw(),
gid: u.gid.as_raw(),
home: u.dir,
shell: u.shell,
}),
Ok(None) => Err(AuthError::UnresolvedUser(name.to_string())),
Err(e) => Err(AuthError::Pam(format!("getpwnam({name}): {e}"))),
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn resolves_root() {
// root (uid 0) existe en todo sistema Unix.
let info = resolve_user("root").expect("root debe existir");
assert_eq!(info.uid, 0);
assert_eq!(info.name, "root");
}
#[test]
fn unknown_user_errs() {
let r = resolve_user("usuario-que-no-existe-xyzzy");
assert!(matches!(r, Err(AuthError::UnresolvedUser(_))));
}
#[test]
fn synthetic_has_home_under_slash_home() {
let info = UserInfo::synthetic("prueba");
assert_eq!(info.home, PathBuf::from("/home/prueba"));
assert_eq!(info.uid, 1000);
}
}