1
//! polkit authorization checks against `org.freedesktop.PolicyKit1.Authority`.
2

            
3
use std::collections::HashMap;
4

            
5
use anyhow::Context;
6
use 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
)]
13
trait 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
)]
30
trait 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")]
36
trait LoginSession {
37
    #[zbus(property)]
38
    fn id(&self) -> zbus::Result<String>;
39
}
40

            
41
#[derive(Clone)]
42
pub struct PolkitAuthority {
43
    connection: Connection,
44
}
45

            
46
impl 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`
112
fn 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
}