tawasuyu
⎇ 48e41331a104ba136b34e7695dccb08ef9188703 crates/ente-polkit-compat/src/main.rs Rust · 262 lines · 9.1 KB History Who touched what raw

journal-query CLI, machined, polkit con grants reales, GNOME boot test 48e41331a · Sergio ·

1//! ente-polkit-compat: shim de `org.freedesktop.PolicyKit1.Authority`.
2//!
3//! Polkit autoriza llamadas privilegiadas (e.g. SetHostname, PowerOff).
4//! En el fractal no usamos polkit como gatekeeper — la auth se hace en
5//! el bus interno via SO_PEERCRED y capability grants. Pero apps que
6//! usan polkit (gnome-control-center, etc) bloquean en `CheckAuthorization`
7//! si no responde nadie.
8//!
9//! Este shim responde "is_authorized=true" siempre — el fractal queda
10//! como sistema confiado. El logging deja audit trail de qué acciones se
11//! han pedido para futuro análisis.
12//!
13//! Producción real: integrar con el grant system del bus interno —
14//! CheckAuthorization solicita un token al graph y devuelve true/false
15//! según el resultado.
16
17use ente_bus::{BusClient, BusRequest, BusResponse, POLKIT_DECISION_IFACE, POLKIT_SERVICE_IFACE};
18use ente_card::Capability;
19use std::collections::HashMap;
20use tokio::signal::unix::{signal, SignalKind};
21use tracing::{debug, info, warn};
22use tracing_subscriber::EnvFilter;
23use zbus::{fdo, interface, zvariant::OwnedValue};
24
25const BUS_NAME: &str = "org.freedesktop.PolicyKit1";
26const OBJ_PATH: &str = "/org/freedesktop/PolicyKit1/Authority";
27
28#[tokio::main(flavor = "current_thread")]
29async fn main() -> anyhow::Result<()> {
30    init_tracing();
31    info!("ente-polkit-compat: arrancando");
32    announce_to_fractal().await;
33
34    let manager = PolkitAuthority;
35    let conn_result = zbus::connection::Builder::system()
36        .and_then(|b| b.name(BUS_NAME))
37        .and_then(|b| b.serve_at(OBJ_PATH, manager));
38    match conn_result {
39        Ok(builder) => match builder.build().await {
40            Ok(_conn) => {
41                info!(name = BUS_NAME, "name acquired, sirviendo");
42                wait_for_term().await
43            }
44            Err(e) => { warn!(?e, "build conn falló — modo idle"); wait_for_term().await }
45        },
46        Err(e) => { warn!(?e, "builder D-Bus falló — modo idle"); wait_for_term().await }
47    }
48}
49
50struct PolkitAuthority;
51
52/// Wire format de Polkit: `Subject = (s, a{sv})` — kind ("unix-session",
53/// "unix-process", "system-bus-name") + detalles. El detail típico:
54///   {"pid": u32, "start-time": u64, "uid": u32}
55type Subject = (String, HashMap<String, OwnedValue>);
56
57/// Resultado de `CheckAuthorization`: `(b, b, a{ss})` —
58/// is_authorized, is_challenge, details.
59type AuthResult = (bool, bool, HashMap<String, String>);
60
61#[interface(name = "org.freedesktop.PolicyKit1.Authority")]
62impl PolkitAuthority {
63    async fn check_authorization(
64        &self,
65        subject: Subject,
66        action_id: String,
67        _details: HashMap<String, String>,
68        _flags: u32,
69        _cancellation_id: String,
70    ) -> fdo::Result<AuthResult> {
71        let (subj_kind, subj_details) = subject;
72        let pid = subj_details.get("pid")
73            .and_then(|v| u32::try_from(v).ok());
74        let uid = subj_details.get("uid")
75            .and_then(|v| u32::try_from(v).ok());
76
77        // Pregunta al bus interno del fractal si hay un policy provider.
78        // Si lo hay, su decisión gobierna. Si no (NoProvider), default = allow.
79        let decision = query_policy(&action_id, pid, uid).await;
80        info!(%action_id, %subj_kind, ?pid, ?uid, ?decision, "CheckAuthorization");
81        Ok((decision.allow, false, HashMap::new()))
82    }
83
84    async fn check_authorization_by_async(
85        &self,
86        subject: Subject,
87        action_id: String,
88        details: HashMap<String, String>,
89        flags: u32,
90        cancellation_id: String,
91    ) -> fdo::Result<AuthResult> {
92        // Mismo comportamiento; algunos clientes llaman la versión async.
93        self.check_authorization(subject, action_id, details, flags, cancellation_id).await
94    }
95
96    async fn cancel_check_authorization(&self, _cancellation_id: String) -> fdo::Result<()> {
97        Ok(())
98    }
99
100    async fn enumerate_actions(&self, _locale: String) -> fdo::Result<Vec<EnumeratedAction>> {
101        // Devolvemos lista vacía — no enumeramos acciones registradas.
102        // El llamador (típicamente gnome-control-center settings panel)
103        // debería degradar grácilmente.
104        Ok(vec![])
105    }
106
107    async fn register_authentication_agent(
108        &self,
109        _subject: Subject,
110        _locale: String,
111        _object_path: String,
112    ) -> fdo::Result<()> {
113        info!("RegisterAuthenticationAgent (no-op)");
114        Ok(())
115    }
116
117    async fn register_authentication_agent_with_options(
118        &self,
119        _subject: Subject,
120        _locale: String,
121        _object_path: String,
122        _options: HashMap<String, OwnedValue>,
123    ) -> fdo::Result<()> {
124        Ok(())
125    }
126
127    async fn unregister_authentication_agent(
128        &self,
129        _subject: Subject,
130        _object_path: String,
131    ) -> fdo::Result<()> {
132        Ok(())
133    }
134
135    async fn authentication_agent_response(
136        &self,
137        _cookie: String,
138        _identity: (String, HashMap<String, OwnedValue>),
139    ) -> fdo::Result<()> {
140        Ok(())
141    }
142
143    async fn enumerate_temporary_authorizations(
144        &self,
145        _subject: Subject,
146    ) -> fdo::Result<Vec<TemporaryAuth>> {
147        Ok(vec![])
148    }
149
150    async fn revoke_temporary_authorizations(&self, _subject: Subject) -> fdo::Result<()> {
151        Ok(())
152    }
153
154    async fn revoke_temporary_authorization_by_id(&self, _id: String) -> fdo::Result<()> {
155        Ok(())
156    }
157
158    #[zbus(property)]
159    async fn backend_name(&self) -> String { "ente-polkit-compat".into() }
160
161    #[zbus(property)]
162    async fn backend_version(&self) -> String { env!("CARGO_PKG_VERSION").into() }
163
164    #[zbus(property)]
165    async fn backend_features(&self) -> u32 { 0 }
166}
167
168/// Wire signature de EnumerateActions item:
169/// `(ssssssuusa{ss})` — action_id, descripción, message, vendor, vendor_url,
170/// icon_name, implicit_any, implicit_inactive, implicit_active, annotations.
171type EnumeratedAction = (
172    String, String, String, String, String, String,
173    u32, u32, String, HashMap<String, String>,
174);
175
176/// Wire signature de TemporaryAuthorization:
177/// `(sssss)` — id, action_id, subject_kind, subject_detail, time_obtained, time_expires.
178/// Aquí `(string)` * 5 + 2 timestamps. Simplificamos al subset relevante.
179type TemporaryAuth = (String, String, (String, HashMap<String, OwnedValue>), u64, u64);
180
181/// Resultado de una consulta de policy al fractal.
182#[derive(Debug)]
183struct PolicyDecision {
184    allow: bool,
185    /// Origen: "fractal" si vino del bus, "default-allow" si no había proveedor.
186    source: &'static str,
187}
188
189/// Pregunta al bus interno: ¿hay alguien que decida sobre `action_id`?
190/// Wire format del blob: `pid_u32_be | uid_u32_be | action_id_utf8`.
191/// El proveedor responde con `Invoked { result: [0|1] }` — 1 = allow.
192async fn query_policy(action_id: &str, pid: Option<u32>, uid: Option<u32>) -> PolicyDecision {
193    let mut blob = Vec::with_capacity(8 + action_id.len());
194    blob.extend_from_slice(&pid.unwrap_or(0).to_be_bytes());
195    blob.extend_from_slice(&uid.unwrap_or(0).to_be_bytes());
196    blob.extend_from_slice(action_id.as_bytes());
197
198    let mut client = match BusClient::from_env().await {
199        Ok(c) => c,
200        Err(e) => {
201            debug!(?e, "no bus client — default allow");
202            return PolicyDecision { allow: true, source: "no-bus" };
203        }
204    };
205    let req = BusRequest::Invoke {
206        cap: Capability::Endpoint {
207            interface: POLKIT_DECISION_IFACE,
208            version: 1,
209        },
210        blob,
211    };
212    match client.call(req).await {
213        Ok(BusResponse::Invoked { result }) => {
214            let allow = result.first().copied().unwrap_or(1) != 0;
215            PolicyDecision { allow, source: "fractal" }
216        }
217        Ok(BusResponse::Error(msg)) if msg.contains("sin proveedor") => {
218            // No hay policy provider — default allow.
219            PolicyDecision { allow: true, source: "default-allow" }
220        }
221        Ok(other) => {
222            warn!(?other, "policy: respuesta inesperada — default allow");
223            PolicyDecision { allow: true, source: "default-allow" }
224        }
225        Err(e) => {
226            warn!(?e, "policy: bus call falló — default allow");
227            PolicyDecision { allow: true, source: "default-allow" }
228        }
229    }
230}
231
232async fn announce_to_fractal() {
233    if let Ok(mut client) = BusClient::from_env().await {
234        let req = BusRequest::Announce {
235            capabilities: vec![Capability::Endpoint {
236                interface: POLKIT_SERVICE_IFACE,
237                version: 1,
238            }],
239        };
240        match client.call(req).await {
241            Ok(BusResponse::Ok) => info!("Announce → bus interno OK"),
242            Ok(other) => warn!(?other, "Announce respuesta inesperada"),
243            Err(e) => warn!(?e, "Announce falló"),
244        }
245    }
246}
247
248async fn wait_for_term() -> anyhow::Result<()> {
249    let mut term = signal(SignalKind::terminate())?;
250    let mut int_ = signal(SignalKind::interrupt())?;
251    tokio::select! {
252        _ = term.recv() => info!("SIGTERM"),
253        _ = int_.recv() => info!("SIGINT"),
254    }
255    Ok(())
256}
257
258fn init_tracing() {
259    let filter = EnvFilter::try_from_default_env()
260        .unwrap_or_else(|_| EnvFilter::new("ente_polkit_compat=info"));
261    tracing_subscriber::fmt().with_env_filter(filter).with_target(true).init();
262}