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…
⎇ 42288230f9560814e356586d8b9eeb36747b95fc
crates/hammer-core/src/caps.rs
History
Who touched what
raw
Fase 5: política de caps del bus desde agent-caps.toml
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}