gioser

sergio/takana

> Una distribución Linux construida con un laboratorio funcional y hermético en el > sótano, y una terminal mutable, clásica y anárquica en el piso de arriba — diseñada > desde el suelo para que una IA programadora entienda, modifique y co…

TOML 815Rust 56Shell 37Markdown 291229 Sutichaykuna

⎇ 42288230f9560814e356586d8b9eeb36747b95fc crates/hammer-core/src/caps.rs Rust · 179 siq'ikuna · 5.3 KB Ñawpa willay Pi imata llamkarqan ch'uya

Fase 5: política de caps del bus desde agent-caps.toml 42288230f · Sergio ·

1//! Política de capacidades del bus de agente, declarativa. Ver `docs/07-agent-bus.md` §4 y
2//! `docs/09-trust-model.md`.
3//!
4//! El daemon concede caps a cada conexión según su `Peer` (UID/GID ya autenticados vía
5//! `SO_PEERCRED`). Hasta ahora esa política vivía hardcodeada en código; este módulo la lee de
6//! `/etc/hammer/agent-caps.toml` para que el operador la ajuste sin recompilar. El peer NUNCA
7//! elige sus caps: las decide el daemon a partir de esta config.
8//!
9//! ```toml
10//! # Caps para cualquier UID/GID no cubierto por una regla.
11//! default = ["query"]
12//!
13//! # Reglas evaluadas en orden; gana la PRIMERA que casa. Una regla casa si todos los campos
14//! # que declara (`uid` y/o `gid`) coinciden con el peer.
15//! [[rule]]
16//! uid = 0
17//! caps = ["query", "compile", "inject", "inject-real", "init"]
18//!
19//! [[rule]]
20//! gid = 994            # p. ej. grupo `hammer`
21//! caps = ["query", "compile", "inject", "init"]
22//! ```
23
24use serde::Deserialize;
25
26use crate::proto::Cap;
27
28/// Config declarativa de la política de capacidades del bus.
29#[derive(Debug, Clone, Deserialize)]
30pub struct AgentCapsConfig {
31    /// Caps para peers que no casan ninguna regla.
32    #[serde(default = "default_caps")]
33    pub default: Vec<Cap>,
34    /// Reglas, evaluadas en orden (la primera que casa gana).
35    #[serde(default)]
36    pub rule: Vec<CapRule>,
37}
38
39/// Una regla: a quién aplica (por `uid` y/o `gid`) y qué caps concede.
40#[derive(Debug, Clone, Deserialize)]
41pub struct CapRule {
42    #[serde(default)]
43    pub uid: Option<u32>,
44    #[serde(default)]
45    pub gid: Option<u32>,
46    pub caps: Vec<Cap>,
47}
48
49fn default_caps() -> Vec<Cap> {
50    vec![Cap::Query]
51}
52
53impl Default for AgentCapsConfig {
54    fn default() -> Self {
55        AgentCapsConfig {
56            default: default_caps(),
57            rule: Vec::new(),
58        }
59    }
60}
61
62impl CapRule {
63    /// ¿Casa esta regla con `(uid, gid)`? Casa si todos los campos declarados coinciden. Una
64    /// regla sin `uid` ni `gid` NO casa (sería un catch-all redundante con `default`, y casi
65    /// siempre un error de config); se ignora con cuidado en `caps_for`.
66    fn matches(&self, uid: u32, gid: u32) -> bool {
67        if self.uid.is_none() && self.gid.is_none() {
68            return false;
69        }
70        self.uid.map(|u| u == uid).unwrap_or(true) && self.gid.map(|g| g == gid).unwrap_or(true)
71    }
72}
73
74impl AgentCapsConfig {
75    pub fn from_toml(s: &str) -> crate::Result<AgentCapsConfig> {
76        toml::from_str(s).map_err(|e| crate::Error::Serde(e.to_string()))
77    }
78
79    /// Carga la config desde `path`. Devuelve `Ok(None)` si el fichero no existe — el daemon
80    /// decide entonces su política por defecto.
81    pub fn load(path: &std::path::Path) -> crate::Result<Option<AgentCapsConfig>> {
82        match std::fs::read_to_string(path) {
83            Ok(s) => Ok(Some(Self::from_toml(&s)?)),
84            Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(None),
85            Err(e) => Err(e.into()),
86        }
87    }
88
89    /// Caps concedidas a un peer `(uid, gid)`: la primera regla que casa, o `default`.
90    pub fn caps_for(&self, uid: u32, gid: u32) -> Vec<Cap> {
91        for r in &self.rule {
92            if r.matches(uid, gid) {
93                return r.caps.clone();
94            }
95        }
96        self.default.clone()
97    }
98}
99
100#[cfg(test)]
101mod tests {
102    use super::*;
103
104    const CFG: &str = r#"
105default = ["query"]
106
107[[rule]]
108uid = 0
109caps = ["query", "compile", "inject", "inject-real", "init"]
110
111[[rule]]
112gid = 994
113caps = ["query", "compile", "inject", "init"]
114
115[[rule]]
116uid = 1000
117gid = 1000
118caps = ["query", "compile"]
119"#;
120
121    #[test]
122    fn root_gets_everything() {
123        let c = AgentCapsConfig::from_toml(CFG).unwrap();
124        assert_eq!(
125            c.caps_for(0, 0),
126            vec![Cap::Query, Cap::Compile, Cap::Inject, Cap::InjectReal, Cap::Init]
127        );
128    }
129
130    #[test]
131    fn first_matching_rule_wins() {
132        let c = AgentCapsConfig::from_toml(CFG).unwrap();
133        // uid 1000 + gid 994: la regla del gid 994 viene ANTES que la de uid+gid 1000,
134        // así que gana la del grupo.
135        assert_eq!(
136            c.caps_for(1000, 994),
137            vec![Cap::Query, Cap::Compile, Cap::Inject, Cap::Init]
138        );
139    }
140
141    #[test]
142    fn both_fields_must_match() {
143        let c = AgentCapsConfig::from_toml(CFG).unwrap();
144        // uid 1000 + gid 1000: no casa root, no casa gid 994; casa la regla uid=1000,gid=1000.
145        assert_eq!(c.caps_for(1000, 1000), vec![Cap::Query, Cap::Compile]);
146    }
147
148    #[test]
149    fn unmatched_falls_back_to_default() {
150        let c = AgentCapsConfig::from_toml(CFG).unwrap();
151        assert_eq!(c.caps_for(31337, 31337), vec![Cap::Query]);
152    }
153
154    #[test]
155    fn empty_config_defaults_to_query() {
156        let c = AgentCapsConfig::from_toml("").unwrap();
157        assert_eq!(c.caps_for(0, 0), vec![Cap::Query]);
158    }
159
160    #[test]
161    fn rule_without_uid_or_gid_is_ignored() {
162        let c = AgentCapsConfig::from_toml(
163            r#"
164default = ["query"]
165[[rule]]
166caps = ["init", "inject-real"]
167"#,
168        )
169        .unwrap();
170        // La regla vacía no casa a nadie ⇒ cae al default.
171        assert_eq!(c.caps_for(0, 0), vec![Cap::Query]);
172    }
173
174    #[test]
175    fn load_missing_file_is_none() {
176        let got = AgentCapsConfig::load(std::path::Path::new("/no/existe.toml")).unwrap();
177        assert!(got.is_none());
178    }
179}