//! Política de capacidades del bus de agente, declarativa. Ver `docs/07-agent-bus.md` §4 y //! `docs/09-trust-model.md`. //! //! El daemon concede caps a cada conexión según su `Peer` (UID/GID ya autenticados vía //! `SO_PEERCRED`). Hasta ahora esa política vivía hardcodeada en código; este módulo la lee de //! `/etc/hammer/agent-caps.toml` para que el operador la ajuste sin recompilar. El peer NUNCA //! elige sus caps: las decide el daemon a partir de esta config. //! //! ```toml //! # Caps para cualquier UID/GID no cubierto por una regla. //! default = ["query"] //! //! # Reglas evaluadas en orden; gana la PRIMERA que casa. Una regla casa si todos los campos //! # que declara (`uid` y/o `gid`) coinciden con el peer. //! [[rule]] //! uid = 0 //! caps = ["query", "compile", "inject", "inject-real", "init"] //! //! [[rule]] //! gid = 994 # p. ej. grupo `hammer` //! caps = ["query", "compile", "inject", "init"] //! ``` use serde::Deserialize; use crate::proto::Cap; /// Config declarativa de la política de capacidades del bus. #[derive(Debug, Clone, Deserialize)] pub struct AgentCapsConfig { /// Caps para peers que no casan ninguna regla. #[serde(default = "default_caps")] pub default: Vec, /// Reglas, evaluadas en orden (la primera que casa gana). #[serde(default)] pub rule: Vec, } /// Una regla: a quién aplica (por `uid` y/o `gid`) y qué caps concede. #[derive(Debug, Clone, Deserialize)] pub struct CapRule { #[serde(default)] pub uid: Option, #[serde(default)] pub gid: Option, pub caps: Vec, } fn default_caps() -> Vec { vec![Cap::Query] } impl Default for AgentCapsConfig { fn default() -> Self { AgentCapsConfig { default: default_caps(), rule: Vec::new(), } } } impl CapRule { /// ¿Casa esta regla con `(uid, gid)`? Casa si todos los campos declarados coinciden. Una /// regla sin `uid` ni `gid` NO casa (sería un catch-all redundante con `default`, y casi /// siempre un error de config); se ignora con cuidado en `caps_for`. fn matches(&self, uid: u32, gid: u32) -> bool { if self.uid.is_none() && self.gid.is_none() { return false; } self.uid.map(|u| u == uid).unwrap_or(true) && self.gid.map(|g| g == gid).unwrap_or(true) } } impl AgentCapsConfig { pub fn from_toml(s: &str) -> crate::Result { toml::from_str(s).map_err(|e| crate::Error::Serde(e.to_string())) } /// Carga la config desde `path`. Devuelve `Ok(None)` si el fichero no existe — el daemon /// decide entonces su política por defecto. pub fn load(path: &std::path::Path) -> crate::Result> { match std::fs::read_to_string(path) { Ok(s) => Ok(Some(Self::from_toml(&s)?)), Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(None), Err(e) => Err(e.into()), } } /// Caps concedidas a un peer `(uid, gid)`: la primera regla que casa, o `default`. pub fn caps_for(&self, uid: u32, gid: u32) -> Vec { for r in &self.rule { if r.matches(uid, gid) { return r.caps.clone(); } } self.default.clone() } } #[cfg(test)] mod tests { use super::*; const CFG: &str = r#" default = ["query"] [[rule]] uid = 0 caps = ["query", "compile", "inject", "inject-real", "init"] [[rule]] gid = 994 caps = ["query", "compile", "inject", "init"] [[rule]] uid = 1000 gid = 1000 caps = ["query", "compile"] "#; #[test] fn root_gets_everything() { let c = AgentCapsConfig::from_toml(CFG).unwrap(); assert_eq!( c.caps_for(0, 0), vec![Cap::Query, Cap::Compile, Cap::Inject, Cap::InjectReal, Cap::Init] ); } #[test] fn first_matching_rule_wins() { let c = AgentCapsConfig::from_toml(CFG).unwrap(); // uid 1000 + gid 994: la regla del gid 994 viene ANTES que la de uid+gid 1000, // así que gana la del grupo. assert_eq!( c.caps_for(1000, 994), vec![Cap::Query, Cap::Compile, Cap::Inject, Cap::Init] ); } #[test] fn both_fields_must_match() { let c = AgentCapsConfig::from_toml(CFG).unwrap(); // uid 1000 + gid 1000: no casa root, no casa gid 994; casa la regla uid=1000,gid=1000. assert_eq!(c.caps_for(1000, 1000), vec![Cap::Query, Cap::Compile]); } #[test] fn unmatched_falls_back_to_default() { let c = AgentCapsConfig::from_toml(CFG).unwrap(); assert_eq!(c.caps_for(31337, 31337), vec![Cap::Query]); } #[test] fn empty_config_defaults_to_query() { let c = AgentCapsConfig::from_toml("").unwrap(); assert_eq!(c.caps_for(0, 0), vec![Cap::Query]); } #[test] fn rule_without_uid_or_gid_is_ignored() { let c = AgentCapsConfig::from_toml( r#" default = ["query"] [[rule]] caps = ["init", "inject-real"] "#, ) .unwrap(); // La regla vacía no casa a nadie ⇒ cae al default. assert_eq!(c.caps_for(0, 0), vec![Cap::Query]); } #[test] fn load_missing_file_is_none() { let got = AgentCapsConfig::load(std::path::Path::new("/no/existe.toml")).unwrap(); assert!(got.is_none()); } }