Skip to main content

roowho2_lib/server/
polkit.rs

1//! polkit authorization checks against `org.freedesktop.PolicyKit1.Authority`.
2
3use std::collections::HashMap;
4
5use anyhow::Context;
6use zbus::{Connection, proxy, zvariant::Value};
7
8#[proxy(
9    default_service = "org.freedesktop.PolicyKit1",
10    default_path = "/org/freedesktop/PolicyKit1/Authority",
11    interface = "org.freedesktop.PolicyKit1.Authority"
12)]
13trait Authority {
14    #[allow(clippy::type_complexity)]
15    fn check_authorization(
16        &self,
17        subject: &(&str, HashMap<&str, Value<'_>>),
18        action_id: &str,
19        details: &HashMap<&str, &str>,
20        flags: u32,
21        cancellation_id: &str,
22    ) -> zbus::Result<(bool, bool, HashMap<String, String>)>;
23}
24
25#[proxy(
26    default_service = "org.freedesktop.login1",
27    default_path = "/org/freedesktop/login1",
28    interface = "org.freedesktop.login1.Manager"
29)]
30trait LoginManager {
31    #[zbus(name = "GetSessionByPID")]
32    fn get_session_by_pid(&self, pid: u32) -> zbus::Result<zbus::zvariant::OwnedObjectPath>;
33}
34
35#[proxy(interface = "org.freedesktop.login1.Session")]
36trait LoginSession {
37    #[zbus(property)]
38    fn id(&self) -> zbus::Result<String>;
39}
40
41#[derive(Clone)]
42pub struct PolkitAuthority {
43    connection: Connection,
44}
45
46impl PolkitAuthority {
47    pub async fn connect() -> anyhow::Result<Self> {
48        let connection = Connection::system()
49            .await
50            .context("failed to connect to the D-Bus system bus")?;
51        Ok(Self { connection })
52    }
53
54    pub fn connection(&self) -> &Connection {
55        &self.connection
56    }
57
58    /// Resolve `pid`'s logind session id, if it has one.
59    async fn session_id_for_pid(&self, pid: u32) -> Option<String> {
60        let manager = LoginManagerProxy::new(&self.connection).await.ok()?;
61        let path = manager.get_session_by_pid(pid).await.ok()?;
62        let session = LoginSessionProxy::builder(&self.connection)
63            .path(path)
64            .ok()?
65            .build()
66            .await
67            .ok()?;
68        session.id().await.ok()
69    }
70
71    /// Build a polkit subject for `pid`, using details from `logind` if possible.
72    async fn subject(
73        &self,
74        pid: u32,
75    ) -> anyhow::Result<(&'static str, HashMap<&'static str, Value<'_>>)> {
76        if let Some(session_id) = self.session_id_for_pid(pid).await {
77            let mut details = HashMap::new();
78            details.insert("session-id", Value::from(session_id));
79            return Ok(("unix-session", details));
80        }
81
82        let start_time = process_start_time(pid)?;
83        let mut details = HashMap::new();
84        details.insert("pid", Value::from(pid));
85        details.insert("start-time", Value::from(start_time));
86        Ok(("unix-process", details))
87    }
88
89    /// Ask polkit whether `pid` is authorized to perform `action_id`
90    pub(crate) async fn check(
91        &self,
92        pid: u32,
93        action_id: &str,
94        details: HashMap<&str, &str>,
95    ) -> anyhow::Result<bool> {
96        let proxy = AuthorityProxy::new(&self.connection)
97            .await
98            .context("failed to build polkit Authority proxy")?;
99
100        let subject = self.subject(pid).await?;
101
102        let (authorized, _interactive, _details) = proxy
103            .check_authorization(&subject, action_id, &details, 0, "")
104            .await
105            .context("CheckAuthorization call failed")?;
106
107        Ok(authorized)
108    }
109}
110
111/// Read a process's start time from `/proc/<pid>/stat`
112fn process_start_time(pid: u32) -> anyhow::Result<u64> {
113    let stat = std::fs::read_to_string(format!("/proc/{pid}/stat"))
114        .with_context(|| format!("failed to read /proc/{pid}/stat"))?;
115
116    let after_comm = stat
117        .rsplit_once(')')
118        .with_context(|| format!("malformed /proc/{pid}/stat"))?
119        .1;
120
121    // starttime is field 22; index 19 once `state` (field 3) is index 0.
122    after_comm
123        .split_whitespace()
124        .nth(19)
125        .with_context(|| format!("missing starttime field in /proc/{pid}/stat"))?
126        .parse::<u64>()
127        .context("starttime field is not a valid number")
128}