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:
Generated
+82
-2
@@ -1522,6 +1522,26 @@ dependencies = [
|
|||||||
"serde",
|
"serde",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "bindgen"
|
||||||
|
version = "0.69.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "271383c67ccabffb7381723dea0672a673f292304fcb45c01cc648c7a8d58088"
|
||||||
|
dependencies = [
|
||||||
|
"bitflags 2.11.1",
|
||||||
|
"cexpr",
|
||||||
|
"clang-sys",
|
||||||
|
"itertools 0.11.0",
|
||||||
|
"lazy_static",
|
||||||
|
"lazycell",
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"regex",
|
||||||
|
"rustc-hash 1.1.0",
|
||||||
|
"shlex",
|
||||||
|
"syn 2.0.117",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "bindgen"
|
name = "bindgen"
|
||||||
version = "0.71.1"
|
version = "0.71.1"
|
||||||
@@ -1806,6 +1826,16 @@ dependencies = [
|
|||||||
"ulid",
|
"ulid",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "brahman-auth"
|
||||||
|
version = "0.1.0"
|
||||||
|
dependencies = [
|
||||||
|
"nix 0.29.0",
|
||||||
|
"pam",
|
||||||
|
"rpassword",
|
||||||
|
"thiserror 2.0.18",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "brahman-broker"
|
name = "brahman-broker"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
@@ -5086,7 +5116,7 @@ dependencies = [
|
|||||||
"ashpd 0.11.1",
|
"ashpd 0.11.1",
|
||||||
"async-task",
|
"async-task",
|
||||||
"backtrace",
|
"backtrace",
|
||||||
"bindgen",
|
"bindgen 0.71.1",
|
||||||
"blade-graphics",
|
"blade-graphics",
|
||||||
"blade-macros",
|
"blade-macros",
|
||||||
"blade-util",
|
"blade-util",
|
||||||
@@ -5238,7 +5268,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "05cb8912ae17371725132d2b7eec6797a255accc95d58ee5c1134b529810f14b"
|
checksum = "05cb8912ae17371725132d2b7eec6797a255accc95d58ee5c1134b529810f14b"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"bindgen",
|
"bindgen 0.71.1",
|
||||||
"core-foundation 0.10.0",
|
"core-foundation 0.10.0",
|
||||||
"core-video",
|
"core-video",
|
||||||
"ctor",
|
"ctor",
|
||||||
@@ -6557,6 +6587,12 @@ dependencies = [
|
|||||||
"spin",
|
"spin",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "lazycell"
|
||||||
|
version = "1.3.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "leak"
|
name = "leak"
|
||||||
version = "0.1.2"
|
version = "0.1.2"
|
||||||
@@ -9397,6 +9433,40 @@ dependencies = [
|
|||||||
"windows 0.59.0",
|
"windows 0.59.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pam"
|
||||||
|
version = "0.8.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "8ab553c52103edb295d8f7d6a3b593dc22a30b1fb99643c777a8f36915e285ba"
|
||||||
|
dependencies = [
|
||||||
|
"libc",
|
||||||
|
"memchr",
|
||||||
|
"pam-macros",
|
||||||
|
"pam-sys",
|
||||||
|
"users",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pam-macros"
|
||||||
|
version = "0.0.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "c94f3b9b97df3c6d4e51a14916639b24e02c7d15d1dba686ce9b1118277cb811"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn 1.0.109",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pam-sys"
|
||||||
|
version = "1.0.0-alpha5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "ce9484729b3e52c0bacdc5191cb6a6a5f31ef4c09c5e4ab1209d3340ad9e997b"
|
||||||
|
dependencies = [
|
||||||
|
"bindgen 0.69.5",
|
||||||
|
"libc",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "parking"
|
name = "parking"
|
||||||
version = "2.2.1"
|
version = "2.2.1"
|
||||||
@@ -14139,6 +14209,16 @@ version = "2.1.3"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "daf8dba3b7eb870caf1ddeed7bc9d2a049f3cfdfae7cb521b087cc33ae4c49da"
|
checksum = "daf8dba3b7eb870caf1ddeed7bc9d2a049f3cfdfae7cb521b087cc33ae4c49da"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "users"
|
||||||
|
version = "0.10.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "aa4227e95324a443c9fcb06e03d4d85e91aabe9a5a02aa818688b6918b6af486"
|
||||||
|
dependencies = [
|
||||||
|
"libc",
|
||||||
|
"log",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "usvg"
|
name = "usvg"
|
||||||
version = "0.45.1"
|
version = "0.45.1"
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ members = [
|
|||||||
"crates/protocol/brahman-dht",
|
"crates/protocol/brahman-dht",
|
||||||
"crates/protocol/brahman-card-discovery",
|
"crates/protocol/brahman-card-discovery",
|
||||||
"crates/protocol/brahman-ssh-multiplex",
|
"crates/protocol/brahman-ssh-multiplex",
|
||||||
|
"crates/protocol/brahman-auth",
|
||||||
"crates/protocol/arje-card",
|
"crates/protocol/arje-card",
|
||||||
|
|
||||||
# ============================================================
|
# ============================================================
|
||||||
@@ -374,6 +375,9 @@ notify = "6.1"
|
|||||||
clap = { version = "4", features = ["derive"] }
|
clap = { version = "4", features = ["derive"] }
|
||||||
rpassword = "7"
|
rpassword = "7"
|
||||||
|
|
||||||
|
# === PAM (brahman-auth) ===
|
||||||
|
pam = "0.8"
|
||||||
|
|
||||||
# === D-Bus (arje compat) ===
|
# === D-Bus (arje compat) ===
|
||||||
zbus = { version = "4", default-features = false, features = ["tokio"] }
|
zbus = { version = "4", default-features = false, features = ["tokio"] }
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,17 @@
|
|||||||
|
[package]
|
||||||
|
name = "brahman-auth"
|
||||||
|
version.workspace = true
|
||||||
|
edition.workspace = true
|
||||||
|
rust-version.workspace = true
|
||||||
|
license.workspace = true
|
||||||
|
authors.workspace = true
|
||||||
|
publish.workspace = true
|
||||||
|
description = "brahman-auth — autenticación del escritorio: contrato Authenticator agnóstico + backend PAM + mock. Lo consume el greeter de carmen (mirada)."
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
nix = { workspace = true }
|
||||||
|
thiserror = { workspace = true }
|
||||||
|
pam = { workspace = true }
|
||||||
|
|
||||||
|
[dev-dependencies]
|
||||||
|
rpassword = { workspace = true }
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
# brahman-auth
|
||||||
|
|
||||||
|
Autenticación del escritorio. Contrato `Authenticator` agnóstico del
|
||||||
|
backend, con dos implementaciones.
|
||||||
|
|
||||||
|
## Para qué
|
||||||
|
|
||||||
|
El greeter de carmen (mirada) necesita verificar la contraseña del
|
||||||
|
usuario y, en éxito, saber su `uid/gid/home/shell` para arrancar la
|
||||||
|
sesión. Eso es exactamente lo que entrega `Authenticator::authenticate`:
|
||||||
|
|
||||||
|
```rust
|
||||||
|
use brahman_auth::{Authenticator, PamAuthenticator};
|
||||||
|
|
||||||
|
let auth = PamAuthenticator::carmen();
|
||||||
|
match auth.authenticate("sergio", &password) {
|
||||||
|
Ok(info) => arrancar_sesion(info), // info: UserInfo
|
||||||
|
Err(e) => mostrar_error_en_greeter(e),
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Backends
|
||||||
|
|
||||||
|
- **`PamAuthenticator`** — verifica contra PAM (`/etc/pam.d/<servicio>`),
|
||||||
|
el mismo subsistema de `login` y `sudo`. Hereda lo que el
|
||||||
|
administrador configure ahí (2FA, FIDO2, `pam_faillock`…) sin que el
|
||||||
|
crate lo sepa.
|
||||||
|
- **`MockAuthenticator`** — credenciales fijas en memoria. Para tests y
|
||||||
|
para iterar el greeter en cajas sin PAM configurado.
|
||||||
|
|
||||||
|
`AuthError` es deliberadamente grueso: el greeter sólo distingue
|
||||||
|
"reintentá" (`BadCredentials`) de "cuenta vetada" (`AccountUnavailable`),
|
||||||
|
y nunca puede saber si un usuario existe.
|
||||||
|
|
||||||
|
## Servicio PAM
|
||||||
|
|
||||||
|
`data/carmen` es el archivo de servicio. Instalarlo:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
install -Dm644 data/carmen /etc/pam.d/carmen
|
||||||
|
```
|
||||||
|
|
||||||
|
Ajustar el `include` a la pila de login de la distribución (ver los
|
||||||
|
comentarios del archivo).
|
||||||
|
|
||||||
|
## Probar contra PAM en una máquina real
|
||||||
|
|
||||||
|
```sh
|
||||||
|
cargo run -p brahman-auth --example auth-probe -- "$USER" login
|
||||||
|
```
|
||||||
|
|
||||||
|
Pide la contraseña sin eco e informa el `UserInfo` resuelto.
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
#%PAM-1.0
|
||||||
|
#
|
||||||
|
# Servicio PAM del greeter de carmen (mirada). Instalar como
|
||||||
|
# /etc/pam.d/carmen.
|
||||||
|
#
|
||||||
|
# El `include` apunta a la pila de login de la distribución; ajustar
|
||||||
|
# según corresponda:
|
||||||
|
# Arch system-login
|
||||||
|
# Debian/Ubuntu common-auth / common-account / ... (una por línea)
|
||||||
|
# Fedora/RHEL system-auth + postlogin
|
||||||
|
#
|
||||||
|
auth include system-login
|
||||||
|
account include system-login
|
||||||
|
password include system-login
|
||||||
|
session include system-login
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
//! Prueba interactiva de `brahman-auth` contra PAM. Sirve para verificar
|
||||||
|
//! la configuración de `/etc/pam.d/<servicio>` en una máquina real.
|
||||||
|
//!
|
||||||
|
//! `cargo run -p brahman-auth --example auth-probe -- [usuario] [servicio]`
|
||||||
|
//!
|
||||||
|
//! Pide la contraseña sin eco. El servicio por defecto es `carmen`; si
|
||||||
|
//! `/etc/pam.d/carmen` aún no está instalado, probar con `login`.
|
||||||
|
|
||||||
|
use brahman_auth::{Authenticator, PamAuthenticator};
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
let mut args = std::env::args().skip(1);
|
||||||
|
let user = args
|
||||||
|
.next()
|
||||||
|
.or_else(|| std::env::var("USER").ok())
|
||||||
|
.unwrap_or_else(|| "root".into());
|
||||||
|
let service = args.next().unwrap_or_else(|| "carmen".into());
|
||||||
|
|
||||||
|
let password = match rpassword::prompt_password(format!("Contraseña de {user}: ")) {
|
||||||
|
Ok(p) => p,
|
||||||
|
Err(e) => {
|
||||||
|
eprintln!("no se pudo leer la contraseña: {e}");
|
||||||
|
std::process::exit(2);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let auth = PamAuthenticator::new(&service);
|
||||||
|
println!("autenticando «{user}» contra el servicio PAM «{service}»…");
|
||||||
|
match auth.authenticate(&user, &password) {
|
||||||
|
Ok(info) => {
|
||||||
|
println!("✓ autenticado");
|
||||||
|
println!(" uid={} gid={}", info.uid, info.gid);
|
||||||
|
println!(" home={}", info.home.display());
|
||||||
|
println!(" shell={}", info.shell.display());
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
eprintln!("✗ {e}");
|
||||||
|
std::process::exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,139 @@
|
|||||||
|
//! `brahman-auth` — autenticación del escritorio.
|
||||||
|
//!
|
||||||
|
//! Contrato [`Authenticator`] agnóstico del backend, con dos
|
||||||
|
//! implementaciones:
|
||||||
|
//!
|
||||||
|
//! - [`PamAuthenticator`] — el camino real: verifica contra PAM
|
||||||
|
//! (`/etc/pam.d/<servicio>`), el mismo subsistema que usan `login`,
|
||||||
|
//! `sudo` y los gestores de login clásicos. Hereda lo que el
|
||||||
|
//! administrador configure ahí (2FA, llaves FIDO2, `pam_faillock`…)
|
||||||
|
//! sin que `brahman-auth` tenga que saberlo.
|
||||||
|
//! - [`MockAuthenticator`] — credenciales fijas en memoria, para tests
|
||||||
|
//! y para iterar el greeter en cajas sin PAM configurado.
|
||||||
|
//!
|
||||||
|
//! Lo consume el greeter de carmen (mirada): el usuario teclea su
|
||||||
|
//! contraseña, el greeter llama a [`Authenticator::authenticate`], y en
|
||||||
|
//! éxito recibe un [`UserInfo`] con uid/gid/home/shell — lo que el
|
||||||
|
//! compositor necesita para arrancar la sesión.
|
||||||
|
|
||||||
|
mod pam_backend;
|
||||||
|
mod user;
|
||||||
|
|
||||||
|
pub use pam_backend::{PamAuthenticator, DEFAULT_SERVICE};
|
||||||
|
pub use user::{resolve_user, UserInfo};
|
||||||
|
|
||||||
|
use std::collections::HashMap;
|
||||||
|
|
||||||
|
/// Por qué falló una autenticación. Variantes deliberadamente gruesas:
|
||||||
|
/// el greeter sólo necesita saber si conviene reintentar (problema de
|
||||||
|
/// credenciales) o si la cuenta está vetada — y nunca debe poder
|
||||||
|
/// distinguir "usuario inexistente" de "contraseña errada".
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
|
||||||
|
pub enum AuthError {
|
||||||
|
/// Usuario o contraseña incorrectos. El greeter deja reintentar sin
|
||||||
|
/// revelar cuál de los dos falló.
|
||||||
|
#[error("usuario o contraseña incorrectos")]
|
||||||
|
BadCredentials,
|
||||||
|
|
||||||
|
/// Las credenciales son válidas pero la cuenta está deshabilitada,
|
||||||
|
/// expirada o requiere una acción (cambio de contraseña).
|
||||||
|
#[error("la cuenta no está disponible: {0}")]
|
||||||
|
AccountUnavailable(String),
|
||||||
|
|
||||||
|
/// Fallo del subsistema PAM no atribuible a las credenciales
|
||||||
|
/// (servicio mal configurado, módulo roto, etc.).
|
||||||
|
#[error("fallo de PAM: {0}")]
|
||||||
|
Pam(String),
|
||||||
|
|
||||||
|
/// No se pudo resolver la identidad del usuario en el sistema tras
|
||||||
|
/// una autenticación válida (caso raro: `/etc/passwd` inconsistente).
|
||||||
|
#[error("no se pudo resolver el usuario «{0}» en el sistema")]
|
||||||
|
UnresolvedUser(String),
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Verifica credenciales y, en éxito, entrega la identidad del sistema.
|
||||||
|
///
|
||||||
|
/// `&self`: cada llamada es un intento de login independiente. Las
|
||||||
|
/// implementaciones crean su propio estado por intento — PAM exige un
|
||||||
|
/// handle nuevo por transacción, reusarlo entre intentos es un bug.
|
||||||
|
pub trait Authenticator {
|
||||||
|
fn authenticate(&self, username: &str, secret: &str) -> Result<UserInfo, AuthError>;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Autenticador de credenciales fijas en memoria. No toca PAM: sirve
|
||||||
|
/// para tests y para iterar el greeter en cajas headless sin
|
||||||
|
/// `/etc/pam.d` configurado.
|
||||||
|
#[derive(Debug, Default, Clone)]
|
||||||
|
pub struct MockAuthenticator {
|
||||||
|
creds: HashMap<String, String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl MockAuthenticator {
|
||||||
|
/// Crea un autenticador sin usuarios: todo intento falla con
|
||||||
|
/// [`AuthError::BadCredentials`] hasta registrar alguno.
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self::default()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Registra un par usuario/secreto aceptado. Encadenable.
|
||||||
|
pub fn with_user(mut self, username: &str, secret: &str) -> Self {
|
||||||
|
self.creds.insert(username.to_string(), secret.to_string());
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Authenticator for MockAuthenticator {
|
||||||
|
fn authenticate(&self, username: &str, secret: &str) -> Result<UserInfo, AuthError> {
|
||||||
|
// Mismo error para usuario inexistente y para contraseña errada:
|
||||||
|
// no filtra la existencia de cuentas.
|
||||||
|
match self.creds.get(username) {
|
||||||
|
Some(expected) if expected == secret => {
|
||||||
|
// Si el usuario existe en el SO, info real; sino,
|
||||||
|
// sintética (suficiente para tests y dev headless).
|
||||||
|
Ok(resolve_user(username).unwrap_or_else(|_| UserInfo::synthetic(username)))
|
||||||
|
}
|
||||||
|
_ => Err(AuthError::BadCredentials),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn mock_accepts_registered_user() {
|
||||||
|
let auth = MockAuthenticator::new().with_user("sergio", "clave");
|
||||||
|
let info = auth.authenticate("sergio", "clave").expect("debe pasar");
|
||||||
|
assert_eq!(info.name, "sergio");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn mock_rejects_wrong_password() {
|
||||||
|
let auth = MockAuthenticator::new().with_user("sergio", "clave");
|
||||||
|
assert_eq!(
|
||||||
|
auth.authenticate("sergio", "mala"),
|
||||||
|
Err(AuthError::BadCredentials)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn mock_unknown_user_indistinguishable_from_wrong_password() {
|
||||||
|
let auth = MockAuthenticator::new().with_user("sergio", "clave");
|
||||||
|
assert_eq!(
|
||||||
|
auth.authenticate("nadie", "x"),
|
||||||
|
Err(AuthError::BadCredentials)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn empty_mock_rejects_everything() {
|
||||||
|
assert!(MockAuthenticator::new().authenticate("root", "").is_err());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn auth_error_is_displayable() {
|
||||||
|
assert!(!AuthError::BadCredentials.to_string().is_empty());
|
||||||
|
assert!(AuthError::Pam("x".into()).to_string().contains("PAM"));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,120 @@
|
|||||||
|
//! Backend PAM del contrato [`Authenticator`](crate::Authenticator).
|
||||||
|
//!
|
||||||
|
//! El módulo se llama `pam_backend` (no `pam`) para no chocar con el
|
||||||
|
//! crate externo `pam`, del que depende.
|
||||||
|
|
||||||
|
use pam::{Client, PamError, PamReturnCode};
|
||||||
|
|
||||||
|
use crate::{resolve_user, AuthError, Authenticator, UserInfo};
|
||||||
|
|
||||||
|
/// Servicio PAM por defecto del escritorio carmen. Resuelve a
|
||||||
|
/// `/etc/pam.d/carmen` — ver el archivo `data/carmen` de este crate.
|
||||||
|
pub const DEFAULT_SERVICE: &str = "carmen";
|
||||||
|
|
||||||
|
/// Autentica contra PAM: el mismo subsistema de `login`/`sudo`. Honra
|
||||||
|
/// `/etc/pam.d/<service>` — módulos, 2FA, llaves FIDO2, `pam_faillock`,
|
||||||
|
/// lo que el administrador configure ahí, sin que `brahman-auth` lo sepa.
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct PamAuthenticator {
|
||||||
|
service: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PamAuthenticator {
|
||||||
|
/// Autenticador para un servicio PAM concreto (`/etc/pam.d/<service>`).
|
||||||
|
pub fn new(service: impl Into<String>) -> Self {
|
||||||
|
Self {
|
||||||
|
service: service.into(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Autenticador para el servicio por defecto del escritorio,
|
||||||
|
/// [`DEFAULT_SERVICE`].
|
||||||
|
pub fn carmen() -> Self {
|
||||||
|
Self::new(DEFAULT_SERVICE)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Nombre del servicio PAM que usa este autenticador.
|
||||||
|
pub fn service(&self) -> &str {
|
||||||
|
&self.service
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for PamAuthenticator {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self::carmen()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Authenticator for PamAuthenticator {
|
||||||
|
fn authenticate(&self, username: &str, secret: &str) -> Result<UserInfo, AuthError> {
|
||||||
|
// Un handle PAM nuevo por intento: PAM es stateful por
|
||||||
|
// transacción y reusar el handle entre intentos es un bug. El
|
||||||
|
// `Client` cierra la transacción (`pam_end`) en su `Drop`.
|
||||||
|
let mut client = Client::with_password(&self.service)
|
||||||
|
.map_err(|e| AuthError::Pam(format!("pam_start({}): {e}", self.service)))?;
|
||||||
|
client.conversation_mut().set_credentials(username, secret);
|
||||||
|
|
||||||
|
// `authenticate()` del crate hace pam_authenticate + pam_acct_mgmt:
|
||||||
|
// cubre credenciales Y estado de la cuenta en un solo paso.
|
||||||
|
client.authenticate().map_err(map_pam_error)?;
|
||||||
|
|
||||||
|
// Credenciales válidas: resolvemos la identidad del sistema.
|
||||||
|
resolve_user(username)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Traduce un error de PAM a la taxonomía gruesa de [`AuthError`].
|
||||||
|
fn map_pam_error(err: PamError) -> AuthError {
|
||||||
|
match err.0 {
|
||||||
|
// Credenciales: el greeter debe dejar reintentar.
|
||||||
|
PamReturnCode::Auth_Err
|
||||||
|
| PamReturnCode::User_Unknown
|
||||||
|
| PamReturnCode::Cred_Insufficient
|
||||||
|
| PamReturnCode::MaxTries => AuthError::BadCredentials,
|
||||||
|
|
||||||
|
// Cuenta válida pero vetada o que requiere una acción.
|
||||||
|
PamReturnCode::Acct_Expired => AuthError::AccountUnavailable("la cuenta expiró".into()),
|
||||||
|
PamReturnCode::Cred_Expired => {
|
||||||
|
AuthError::AccountUnavailable("las credenciales expiraron".into())
|
||||||
|
}
|
||||||
|
PamReturnCode::AuthTok_Expired => {
|
||||||
|
AuthError::AccountUnavailable("la contraseña expiró".into())
|
||||||
|
}
|
||||||
|
PamReturnCode::New_Authtok_Reqd => {
|
||||||
|
AuthError::AccountUnavailable("requiere cambiar la contraseña".into())
|
||||||
|
}
|
||||||
|
PamReturnCode::Perm_Denied => {
|
||||||
|
AuthError::AccountUnavailable("acceso denegado por política".into())
|
||||||
|
}
|
||||||
|
|
||||||
|
// Todo lo demás: fallo de infraestructura PAM.
|
||||||
|
other => AuthError::Pam(format!("{other:?}")),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn carmen_uses_default_service() {
|
||||||
|
assert_eq!(PamAuthenticator::carmen().service(), DEFAULT_SERVICE);
|
||||||
|
assert_eq!(PamAuthenticator::default().service(), "carmen");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn custom_service_name() {
|
||||||
|
assert_eq!(PamAuthenticator::new("login").service(), "login");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn unknown_service_fails_gracefully() {
|
||||||
|
// Sin `/etc/pam.d/<servicio>` PAM cae a `other` (deny). Debe
|
||||||
|
// devolver un `AuthError`, nunca paniquear.
|
||||||
|
let auth = PamAuthenticator::new("brahman-auth-servicio-inexistente-xyz");
|
||||||
|
assert!(
|
||||||
|
auth.authenticate("root", "contraseña-cualquiera").is_err(),
|
||||||
|
"un servicio inexistente debe fallar limpio"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,6 +2,29 @@
|
|||||||
|
|
||||||
Contratos canónicos + routing entre módulos. Antes: `core/brahman-*` + `shared/brahman-*`.
|
Contratos canónicos + routing entre módulos. Antes: `core/brahman-*` + `shared/brahman-*`.
|
||||||
|
|
||||||
|
### feat(brahman-auth): autenticación del escritorio — contrato + PAM + mock
|
||||||
|
|
||||||
|
Crate nuevo `crates/protocol/brahman-auth`: la base del DM/greeter de
|
||||||
|
carmen (mirada). Contrato `Authenticator` agnóstico del backend:
|
||||||
|
`authenticate(usuario, secreto) -> UserInfo`, donde `UserInfo` lleva
|
||||||
|
`uid/gid/home/shell` — lo que el compositor necesita para arrancar la
|
||||||
|
sesión.
|
||||||
|
|
||||||
|
- **`PamAuthenticator`** — verifica contra PAM (`/etc/pam.d/<servicio>`,
|
||||||
|
por defecto `carmen`), el mismo subsistema de `login`/`sudo`. Un handle
|
||||||
|
PAM nuevo por intento; `authenticate()` cubre credenciales + estado de
|
||||||
|
la cuenta. `map_pam_error` traduce los `PamReturnCode` a la taxonomía
|
||||||
|
gruesa de `AuthError`.
|
||||||
|
- **`MockAuthenticator`** — credenciales fijas en memoria, para tests y
|
||||||
|
para iterar el greeter en cajas sin PAM.
|
||||||
|
- `AuthError` deliberadamente grueso: `BadCredentials` (reintentar) vs
|
||||||
|
`AccountUnavailable` (cuenta vetada); usuario inexistente y contraseña
|
||||||
|
errada dan el **mismo** error (no filtra existencia de cuentas).
|
||||||
|
- `resolve_user` vía `getpwnam` (nix). `UserInfo::synthetic` para dev.
|
||||||
|
- `data/carmen` — archivo de servicio PAM. Ejemplo `auth-probe` para
|
||||||
|
verificar PAM en una máquina real.
|
||||||
|
- 11 tests; el camino PAM real se ejercita (falla limpio sin servicio).
|
||||||
|
|
||||||
### feat(brahman-demo): bootstrap script reproducible — broker + producer + consumer + 4 explorers
|
### feat(brahman-demo): bootstrap script reproducible — broker + producer + consumer + 4 explorers
|
||||||
Iter 22. Cierra el set de iteraciones de hoy: cualquier persona (o
|
Iter 22. Cierra el set de iteraciones de hoy: cualquier persona (o
|
||||||
future-me retomando el repo) puede levantar el escenario completo
|
future-me retomando el repo) puede levantar el escenario completo
|
||||||
|
|||||||
Reference in New Issue
Block a user