⎇ 53dbdf0f1deea10d6fc567ea2b43e1e75d6d60cd
crates/core/ente-policy-provider/src/main.rs
Ñawpa willay
Pi imata llamkarqan
ch'uya
chore: monorepo inicial con arje + minga + yahweh absorbidos
1//! ente-policy-provider: Ente que arbitra autorizaciones de Polkit.
2//!
3//! Se anuncia como proveedor de `POLKIT_DECISION_IFACE` en el bus interno.
4//! Cuando `ente-polkit-compat` recibe `CheckAuthorization` D-Bus, forwarda
5//! a este Ente vía Invoke. Aquí decidimos sí/no según política configurada.
6//!
7//! Wire format del blob de entrada: `pid_be_u32 | uid_be_u32 | action_id_utf8`.
8//! Respuesta: `[decision_byte]` — 1 = allow, 0 = deny.
9//!
10//! Política se carga de `/etc/ente/policy.json` (o ruta override por env
11//! `ENTE_POLICY_FILE`). Formato:
12//! ```json
13//! {
14//! "default": "allow",
15//! "rules": [
16//! { "match": "org.freedesktop.hostname1.*", "decision": "allow" },
17//! { "match": "org.freedesktop.login1.power-off", "require_uid": 0 },
18//! { "match": "*.set-*", "decision": "deny", "audit": true }
19//! ]
20//! }
21//! ```
22
23use ente_bus::{BusResponse, BusServer, InvokeHandler, POLKIT_DECISION_IFACE};
24use ente_card::Capability;
25use serde::Deserialize;
26use std::sync::Arc;
27use tokio::signal::unix::{signal, SignalKind};
28use tracing::{info, warn};
29use tracing_subscriber::EnvFilter;
30
31#[derive(Debug, Clone, Deserialize)]
32struct PolicyConfig {
33 #[serde(default = "default_decision")]
34 default: Decision,
35 #[serde(default)]
36 rules: Vec<Rule>,
37}
38
39fn default_decision() -> Decision { Decision::Allow }
40
41#[derive(Debug, Clone, Copy, Deserialize, PartialEq, Eq)]
42#[serde(rename_all = "lowercase")]
43enum Decision { Allow, Deny }
44
45#[derive(Debug, Clone, Deserialize)]
46struct Rule {
47 /// Glob simple: `*` = wildcard. `org.freedesktop.hostname1.*` matchea
48 /// cualquier action_id con ese prefijo.
49 r#match: String,
50 #[serde(default)]
51 decision: Option<Decision>,
52 /// Si presente, sólo este uid pasa. Otros se denegen.
53 #[serde(default)]
54 require_uid: Option<u32>,
55 /// Si presente, sólo este pid pasa.
56 #[serde(default)]
57 require_pid: Option<u32>,
58 #[serde(default)]
59 audit: bool,
60}
61
62impl Default for PolicyConfig {
63 fn default() -> Self {
64 // Default sensato: caps escaladas requieren uid 0; el resto allow.
65 Self {
66 default: Decision::Allow,
67 rules: vec![
68 // Power management: cualquiera puede pedir el reboot,
69 // pero la decisión final está en el holder de Capability::Spawn.
70 Rule {
71 r#match: "org.freedesktop.login1.set-wall-message".into(),
72 decision: Some(Decision::Allow), require_uid: None, require_pid: None, audit: true,
73 },
74 // hostname/timezone/locale: requieren root.
75 Rule {
76 r#match: "org.freedesktop.hostname1.*".into(),
77 decision: None, require_uid: Some(0), require_pid: None, audit: true,
78 },
79 Rule {
80 r#match: "org.freedesktop.timedate1.*".into(),
81 decision: None, require_uid: Some(0), require_pid: None, audit: true,
82 },
83 Rule {
84 r#match: "org.freedesktop.locale1.*".into(),
85 decision: None, require_uid: Some(0), require_pid: None, audit: true,
86 },
87 ],
88 }
89 }
90}
91
92#[tokio::main(flavor = "current_thread")]
93async fn main() -> anyhow::Result<()> {
94 init_tracing();
95 info!("ente-policy-provider: arrancando");
96
97 let policy = load_policy();
98 info!(rules = policy.rules.len(), default = ?policy.default, "policy cargada");
99
100 let handler = PolicyHandler { policy: Arc::new(policy) };
101
102 tokio::spawn(async {
103 let mut term = signal(SignalKind::terminate()).unwrap();
104 let mut int_ = signal(SignalKind::interrupt()).unwrap();
105 tokio::select! {
106 _ = term.recv() => info!("SIGTERM"),
107 _ = int_.recv() => info!("SIGINT"),
108 }
109 std::process::exit(0);
110 });
111
112 // Una única conexión: announce + serve. Bidirectional bajo el hood.
113 let mut server = BusServer::from_env().await?;
114 server.announce(vec![Capability::Endpoint {
115 interface: POLKIT_DECISION_IFACE,
116 version: 1,
117 }]).await?;
118 info!("Announce OK; sirviendo invokes de policy decision");
119 server.serve(handler).await?;
120 Ok(())
121}
122
123struct PolicyHandler {
124 policy: Arc<PolicyConfig>,
125}
126
127impl InvokeHandler for PolicyHandler {
128 fn handle(&mut self, cap: Capability, blob: Vec<u8>) -> BusResponse {
129 // Validar cap (defensa contra forwarding a interface incorrecto).
130 if !matches!(&cap, Capability::Endpoint { interface, .. } if *interface == POLKIT_DECISION_IFACE) {
131 return BusResponse::Error(format!("policy-provider: cap inesperado {cap:?}"));
132 }
133 // Decodificar blob: [pid:4][uid:4][action_id...]
134 if blob.len() < 8 {
135 return BusResponse::Error("blob demasiado corto (esperado pid|uid|action_id)".into());
136 }
137 let pid = u32::from_be_bytes(blob[0..4].try_into().unwrap());
138 let uid = u32::from_be_bytes(blob[4..8].try_into().unwrap());
139 let action_id = match std::str::from_utf8(&blob[8..]) {
140 Ok(s) => s,
141 Err(_) => return BusResponse::Error("action_id no es UTF-8".into()),
142 };
143
144 let decision = decide(&self.policy, action_id, pid, uid);
145 let byte = if decision == Decision::Allow { 1u8 } else { 0u8 };
146 info!(action_id, pid, uid, ?decision, "policy decision");
147 BusResponse::Invoked { result: vec![byte] }
148 }
149}
150
151fn decide(policy: &PolicyConfig, action_id: &str, pid: u32, uid: u32) -> Decision {
152 for rule in &policy.rules {
153 if !glob_match(&rule.r#match, action_id) { continue; }
154 if let Some(req_uid) = rule.require_uid {
155 if uid != req_uid {
156 if rule.audit {
157 info!(action_id, uid, req_uid, "AUDIT: deny por uid mismatch");
158 }
159 return Decision::Deny;
160 }
161 }
162 if let Some(req_pid) = rule.require_pid {
163 if pid != req_pid {
164 if rule.audit {
165 info!(action_id, pid, req_pid, "AUDIT: deny por pid mismatch");
166 }
167 return Decision::Deny;
168 }
169 }
170 if let Some(d) = rule.decision {
171 if rule.audit {
172 info!(action_id, ?d, "AUDIT: rule match con decisión explícita");
173 }
174 return d;
175 }
176 // Rule matched pero sin decisión explícita (sólo require_*) y todos
177 // los requires pasaron — caemos al default.
178 if rule.audit {
179 info!(action_id, ?policy.default, "AUDIT: rule match → default");
180 }
181 return policy.default;
182 }
183 policy.default
184}
185
186/// Glob simple: `*` matchea cualquier cosa. Soporta prefix (`foo.*`),
187/// suffix (`*.bar`) y wildcard exacto (`*`). No es PCRE — intencional.
188fn glob_match(pattern: &str, target: &str) -> bool {
189 if pattern == "*" { return true; }
190 if let Some(prefix) = pattern.strip_suffix(".*") {
191 return target == prefix || target.starts_with(&format!("{prefix}."));
192 }
193 if let Some(suffix) = pattern.strip_prefix("*.") {
194 return target == suffix || target.ends_with(&format!(".{suffix}"));
195 }
196 pattern == target
197}
198
199fn load_policy() -> PolicyConfig {
200 let path = std::env::var("ENTE_POLICY_FILE")
201 .unwrap_or_else(|_| "/etc/ente/policy.json".into());
202 match std::fs::read_to_string(&path) {
203 Ok(content) => match serde_json::from_str(&content) {
204 Ok(p) => { info!(path, "policy file cargado"); p }
205 Err(e) => {
206 warn!(?e, path, "policy file inválido, usando defaults");
207 PolicyConfig::default()
208 }
209 },
210 Err(_) => {
211 info!(path, "policy file ausente — usando defaults conservadores");
212 PolicyConfig::default()
213 }
214 }
215}
216
217fn init_tracing() {
218 let filter = EnvFilter::try_from_default_env()
219 .unwrap_or_else(|_| EnvFilter::new("ente_policy_provider=info"));
220 tracing_subscriber::fmt().with_env_filter(filter).with_target(true).init();
221}