From bdc5121fabed63e3e1587136776b80502e46d82c Mon Sep 17 00:00:00 2001 From: h7x4 Date: Thu, 30 Jul 2026 19:08:15 +0900 Subject: [PATCH] walld: implement polkit auth --- Cargo.lock | 235 ++++++++++++++++++++++++++++++-- Cargo.toml | 1 + src/server.rs | 1 + src/server/polkit.rs | 128 +++++++++++++++++ src/server/varlink_api.rs | 26 +++- src/server/varlink_api/walld.rs | 80 +++++++++-- 6 files changed, 453 insertions(+), 18 deletions(-) create mode 100644 src/server/polkit.rs diff --git a/Cargo.lock b/Cargo.lock index 1203861..0aefc6e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -56,7 +56,7 @@ version = "1.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" dependencies = [ - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] @@ -67,7 +67,7 @@ checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" dependencies = [ "anstyle", "once_cell_polyfill", - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] @@ -118,6 +118,28 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "async-recursion" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b43422f69d8ff38f95f1b2bb76517c91589a924d1559a0e935d7c8ce0274c11" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "async-trait" +version = "0.1.91" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae36dc4177970ef04fde5178d3e2429882def40e57a451f919c098f72baa6cec" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.2", +] + [[package]] name = "autocfg" version = "1.5.1" @@ -346,6 +368,33 @@ version = "1.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "91622ff5e7162018101f2fea40d6ebf4a78bbe5a49736a2020649edf9693679e" +[[package]] +name = "endi" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "66b7e2430c6dff6a955451e2cfc438f09cea1965a9d6f87f7e3b90decc014099" + +[[package]] +name = "enumflags2" +version = "0.7.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1027f7680c853e056ebcec683615fb6fbbc07dbaa13b4d5d9442b146ded4ecef" +dependencies = [ + "enumflags2_derive", + "serde", +] + +[[package]] +name = "enumflags2_derive" +version = "0.7.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67c78a4d8fdf9953a5c9d458f9efe940fd97a0cab0941c075a813ac594733827" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + [[package]] name = "equivalent" version = "1.0.2" @@ -359,7 +408,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ "libc", - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] @@ -576,6 +625,12 @@ version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c" +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + [[package]] name = "iana-time-zone" version = "0.1.65" @@ -849,7 +904,7 @@ version = "0.50.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" dependencies = [ - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] @@ -888,6 +943,16 @@ version = "1.70.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" +[[package]] +name = "ordered-stream" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9aa2b01e1d916879f73a53d01d1d6cee68adbb31d6d9177a8cfce093cced1d50" +dependencies = [ + "futures-core", + "pin-project-lite", +] + [[package]] name = "os_display" version = "0.1.4" @@ -968,6 +1033,15 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" +[[package]] +name = "proc-macro-crate" +version = "3.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e67ba7e9b2b56446f1d419b1d807906278ffa1a658a8a5d8a39dcb1f5a78614f" +dependencies = [ + "toml_edit", +] + [[package]] name = "proc-macro2" version = "1.0.107" @@ -1040,6 +1114,7 @@ dependencies = [ "tracing-subscriber", "users", "uucore", + "zbus", "zlink", ] @@ -1059,7 +1134,7 @@ dependencies = [ "errno", "libc", "linux-raw-sys", - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] @@ -1132,6 +1207,17 @@ dependencies = [ "zmij", ] +[[package]] +name = "serde_repr" +version = "0.1.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d3b1629de253c70a0508c3899572da79ca359fdab27c7920ff00406df418906" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.2", +] + [[package]] name = "serde_spanned" version = "1.1.1" @@ -1191,7 +1277,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c3d1e2c7f27f8d4cb10542a02c49005dbd6e93095799d6f3be745fae9f8fedd4" dependencies = [ "libc", - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] @@ -1232,7 +1318,7 @@ dependencies = [ "getrandom", "once_cell", "rustix", - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] @@ -1242,7 +1328,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "230a1b821ccbd75b185820a1f1ff7b14d21da1e442e22c0863ea5f08771a8874" dependencies = [ "rustix", - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] @@ -1394,6 +1480,18 @@ dependencies = [ "serde_core", ] +[[package]] +name = "toml_edit" +version = "0.25.13+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6975367e4d2ef766d86af01ffad14b622fecc8d4357a998fbc4deb6e9bacaf9b" +dependencies = [ + "indexmap", + "toml_datetime", + "toml_parser", + "winnow 1.0.4", +] + [[package]] name = "toml_parser" version = "1.1.2+spec-1.1.0" @@ -1490,6 +1588,17 @@ dependencies = [ "rustc-hash", ] +[[package]] +name = "uds_windows" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2f6fb2847f6742cd76af783a2a2c49e9375d0a111c7bef6f71cd9e738c72d6e" +dependencies = [ + "memoffset", + "tempfile", + "windows-sys 0.61.2", +] + [[package]] name = "unic-langid" version = "0.9.6" @@ -1568,6 +1677,17 @@ dependencies = [ "quote", ] +[[package]] +name = "uuid" +version = "1.24.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf3923a6f5c4c6382e0b653c4117f48d631ea17f38ed86e2a828e6f7412f5239" +dependencies = [ + "js-sys", + "serde_core", + "wasm-bindgen", +] + [[package]] name = "valuable" version = "0.1.1" @@ -1787,6 +1907,65 @@ name = "winnow" version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "23b97319f7b8343df12cc98938e5c3eb436064524c8d2b4e30a1d3a36eecdf81" +dependencies = [ + "memchr", +] + +[[package]] +name = "zbus" +version = "5.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe18fb60dc696039e738717b76eaea21e7a4489bbb1885020b43c94236d7e98a" +dependencies = [ + "async-broadcast", + "async-recursion", + "async-trait", + "enumflags2", + "event-listener", + "futures-core", + "futures-lite", + "hex", + "libc", + "ordered-stream", + "rustix", + "serde", + "serde_repr", + "tokio", + "tracing", + "uds_windows", + "uuid", + "windows-sys 0.61.2", + "winnow 1.0.4", + "zbus_macros", + "zbus_names", + "zvariant", +] + +[[package]] +name = "zbus_macros" +version = "5.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe96480bed92df2b442a1a30df364e12d08eed03aeb061f2b8dc6afb2be91119" +dependencies = [ + "proc-macro-crate", + "proc-macro2", + "quote", + "syn 2.0.119", + "zbus_names", + "zvariant", + "zvariant_utils", +] + +[[package]] +name = "zbus_names" +version = "4.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8bf88b4a3ff53e883001e0e0115b297a9d53c31b9c1edd2bfdd853e3428624e" +dependencies = [ + "serde", + "winnow 1.0.4", + "zvariant", +] [[package]] name = "zerofrom" @@ -1888,3 +2067,43 @@ name = "zmij" version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b" + +[[package]] +name = "zvariant" +version = "5.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bee2a0bcd2a907786a456fff45aaaaf54c9ba5f50b71ae9ec1a4edd200c94911" +dependencies = [ + "endi", + "enumflags2", + "serde", + "winnow 1.0.4", + "zvariant_derive", + "zvariant_utils", +] + +[[package]] +name = "zvariant_derive" +version = "5.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38a708216a18780796770bfe3f4739c7c83a3e8f789b755534bbbc06e4e23e12" +dependencies = [ + "proc-macro-crate", + "proc-macro2", + "quote", + "syn 2.0.119", + "zvariant_utils", +] + +[[package]] +name = "zvariant_utils" +version = "3.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90cb9383f9b45290407a1258b202d3f8f01db719eb60b4e4055c6375af4fc7c7" +dependencies = [ + "proc-macro2", + "quote", + "serde", + "syn 2.0.119", + "winnow 1.0.4", +] diff --git a/Cargo.toml b/Cargo.toml index d0b0a35..81d7dbe 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -43,6 +43,7 @@ netlink-proto = "0.12.0" netlink-sys = { version = "0.8.8", features = ["tokio_socket"] } netlink-packet-audit = "0.6.1" thiserror = "2.0.19" +zbus = { version = "5.18.0", default-features = false, features = ["tokio"] } [dev-dependencies] indoc = "2.0.7" diff --git a/src/server.rs b/src/server.rs index 65a71c6..3d58ed6 100644 --- a/src/server.rs +++ b/src/server.rs @@ -1,6 +1,7 @@ pub mod config; pub mod fingerd; pub mod ignore_list; +pub mod polkit; pub mod rwhod; pub mod varlink_api; pub mod walld; diff --git a/src/server/polkit.rs b/src/server/polkit.rs new file mode 100644 index 0000000..e18a524 --- /dev/null +++ b/src/server/polkit.rs @@ -0,0 +1,128 @@ +//! polkit authorization checks against `org.freedesktop.PolicyKit1.Authority`. + +use std::collections::HashMap; + +use anyhow::Context; +use zbus::{Connection, proxy, zvariant::Value}; + +#[proxy( + default_service = "org.freedesktop.PolicyKit1", + default_path = "/org/freedesktop/PolicyKit1/Authority", + interface = "org.freedesktop.PolicyKit1.Authority" +)] +trait Authority { + #[allow(clippy::type_complexity)] + fn check_authorization( + &self, + subject: &(&str, HashMap<&str, Value<'_>>), + action_id: &str, + details: &HashMap<&str, &str>, + flags: u32, + cancellation_id: &str, + ) -> zbus::Result<(bool, bool, HashMap)>; +} + +#[proxy( + default_service = "org.freedesktop.login1", + default_path = "/org/freedesktop/login1", + interface = "org.freedesktop.login1.Manager" +)] +trait LoginManager { + #[zbus(name = "GetSessionByPID")] + fn get_session_by_pid(&self, pid: u32) -> zbus::Result; +} + +#[proxy(interface = "org.freedesktop.login1.Session")] +trait LoginSession { + #[zbus(property)] + fn id(&self) -> zbus::Result; +} + +#[derive(Clone)] +pub struct PolkitAuthority { + connection: Connection, +} + +impl PolkitAuthority { + pub async fn connect() -> anyhow::Result { + let connection = Connection::system() + .await + .context("failed to connect to the D-Bus system bus")?; + Ok(Self { connection }) + } + + pub fn connection(&self) -> &Connection { + &self.connection + } + + /// Resolve `pid`'s logind session id, if it has one. + async fn session_id_for_pid(&self, pid: u32) -> Option { + let manager = LoginManagerProxy::new(&self.connection).await.ok()?; + let path = manager.get_session_by_pid(pid).await.ok()?; + let session = LoginSessionProxy::builder(&self.connection) + .path(path) + .ok()? + .build() + .await + .ok()?; + session.id().await.ok() + } + + /// Build a polkit subject for `pid`, using details from `logind` if possible. + async fn subject( + &self, + pid: u32, + ) -> anyhow::Result<(&'static str, HashMap<&'static str, Value<'_>>)> { + if let Some(session_id) = self.session_id_for_pid(pid).await { + let mut details = HashMap::new(); + details.insert("session-id", Value::from(session_id)); + return Ok(("unix-session", details)); + } + + let start_time = process_start_time(pid)?; + let mut details = HashMap::new(); + details.insert("pid", Value::from(pid)); + details.insert("start-time", Value::from(start_time)); + Ok(("unix-process", details)) + } + + /// Ask polkit whether `pid` is authorized to perform `action_id` + pub(crate) async fn check( + &self, + pid: u32, + action_id: &str, + details: HashMap<&str, &str>, + ) -> anyhow::Result { + let proxy = AuthorityProxy::new(&self.connection) + .await + .context("failed to build polkit Authority proxy")?; + + let subject = self.subject(pid).await?; + + let (authorized, _interactive, _details) = proxy + .check_authorization(&subject, action_id, &details, 0, "") + .await + .context("CheckAuthorization call failed")?; + + Ok(authorized) + } +} + +/// Read a process's start time from `/proc//stat` +fn process_start_time(pid: u32) -> anyhow::Result { + let stat = std::fs::read_to_string(format!("/proc/{pid}/stat")) + .with_context(|| format!("failed to read /proc/{pid}/stat"))?; + + let after_comm = stat + .rsplit_once(')') + .with_context(|| format!("malformed /proc/{pid}/stat"))? + .1; + + // starttime is field 22; index 19 once `state` (field 3) is index 0. + after_comm + .split_whitespace() + .nth(19) + .with_context(|| format!("missing starttime field in /proc/{pid}/stat"))? + .parse::() + .context("starttime field is not a valid number") +} diff --git a/src/server/varlink_api.rs b/src/server/varlink_api.rs index 666fe31..ca303ee 100644 --- a/src/server/varlink_api.rs +++ b/src/server/varlink_api.rs @@ -13,7 +13,7 @@ use zlink::{ tokio::unix::{Listener as UnixListener, Stream as UnixStream}, }; -use crate::server::{ignore_list::IgnoreList, rwhod::RwhodStatusStore}; +use crate::server::{ignore_list::IgnoreList, polkit::PolkitAuthority, rwhod::RwhodStatusStore}; pub use crate::server::varlink_api::{fingerd::*, rwhod::*, walld::*}; @@ -46,12 +46,13 @@ pub enum VarlinkReplyError { Walld(VarlinkWalldClientError), } -#[derive(Debug, Clone)] +#[derive(Clone)] pub struct VarlinkRoowhoo2ClientServer { whod_status_store: RwhodStatusStore, rwhod_enabled: bool, fingerd_enabled: bool, finger_ignore_list: Option, + polkit: Option, } impl VarlinkRoowhoo2ClientServer { @@ -60,12 +61,14 @@ impl VarlinkRoowhoo2ClientServer { rwhod_enabled: bool, fingerd_enabled: bool, finger_ignore_list: Option, + polkit: Option, ) -> Self { Self { whod_status_store, rwhod_enabled, fingerd_enabled, finger_ignore_list, + polkit, } } } @@ -283,6 +286,7 @@ impl zlink::Service for VarlinkRoowhoo2ClientServer { let result = match timeout( Duration::from_secs(2), handle_wall_request( + self.polkit.as_ref(), peer_pid, peer_uid, source_tty.clone(), @@ -329,6 +333,7 @@ impl zlink::Service for VarlinkRoowhoo2ClientServer { let result = match timeout( Duration::from_secs(2), handle_write_request( + self.polkit.as_ref(), peer_pid, peer_uid, source_tty.clone(), @@ -375,11 +380,28 @@ pub async fn varlink_client_server_task( fingerd_enabled: bool, finger_ignore_list: Option, ) -> anyhow::Result<()> { + let polkit = match timeout(Duration::from_secs(5), PolkitAuthority::connect()).await { + Ok(Ok(polkit)) => Some(polkit), + Ok(Err(e)) => { + tracing::warn!( + "Failed to connect to polkit; Wall/Write requests will be denied: {e:#}" + ); + None + } + Err(_) => { + tracing::warn!( + "Timed out connecting to polkit after 5 seconds; Wall/Write requests will be denied" + ); + None + } + }; + let service = VarlinkRoowhoo2ClientServer::new( whod_status_store, rwhod_enabled, fingerd_enabled, finger_ignore_list, + polkit, ); let server = zlink::Server::new(socket, service); diff --git a/src/server/varlink_api/walld.rs b/src/server/varlink_api/walld.rs index 3bf2121..31c5a77 100644 --- a/src/server/varlink_api/walld.rs +++ b/src/server/varlink_api/walld.rs @@ -1,10 +1,13 @@ -use std::time::Duration; +use std::{collections::HashMap, time::Duration}; use nix::unistd::gethostname; use serde::{Deserialize, Serialize}; use zlink::ReplyError; -use crate::server::walld::tty_utils::{self, TtyError}; +use crate::server::{ + polkit::PolkitAuthority, + walld::tty_utils::{self, TtyError}, +}; #[zlink::proxy("no.ntnu.pvv.roowho2.walld")] pub trait VarlinkWalldClientProxy { @@ -162,11 +165,65 @@ fn tty_error_to_walld_error(user: &str, tty: &str, err: TtyError) -> VarlinkWall } } -// TODO: have the client send the name of the TTY -// verify that the TTY is owned by the user from peercred. +const POLKIT_ACTION_WALL: &str = "no.ntnu.pvv.roowho2.wall"; +const POLKIT_ACTION_WRITE: &str = "no.ntnu.pvv.roowho2.write"; +/// Check whether `pid` is allowed to broadcast a `wall` message, optionally restricted to `group`. +async fn check_wall( + polkit: &PolkitAuthority, + pid: u32, + group: Option<&str>, +) -> anyhow::Result { + let mut details = HashMap::new(); + if let Some(group) = group { + details.insert("group", group); + } + polkit.check(pid, POLKIT_ACTION_WALL, details).await +} + +/// Check whether `pid` is allowed to `write` to `user`, optionally on a specific `tty`. +async fn check_write( + polkit: &PolkitAuthority, + pid: u32, + user: &str, + tty: Option<&str>, +) -> anyhow::Result { + let mut details = HashMap::new(); + details.insert("user", user); + if let Some(tty) = tty { + details.insert("tty", tty); + } + polkit.check(pid, POLKIT_ACTION_WRITE, details).await +} + +/// Wrapper doing some basic error handling against a polkit check. +async fn authorize<'a, F, Fut>( + polkit: Option<&'a PolkitAuthority>, + check: F, +) -> Result<(), VarlinkWalldClientError> +where + F: FnOnce(&'a PolkitAuthority) -> Fut, + Fut: std::future::Future>, +{ + let Some(polkit) = polkit else { + tracing::error!("polkit authority is unavailable; denying request"); + return Err(VarlinkWalldClientError::NotAuthorized); + }; + + match check(polkit).await { + Ok(true) => Ok(()), + Ok(false) => Err(VarlinkWalldClientError::NotAuthorized), + Err(err) => { + tracing::error!("polkit authorization check failed: {err:#}"); + Err(VarlinkWalldClientError::NotAuthorized) + } + } +} + +#[allow(clippy::too_many_arguments)] pub async fn handle_wall_request( - _source_pid: u32, + polkit: Option<&PolkitAuthority>, + source_pid: u32, source_uid: u32, source_tty: Option, message: String, @@ -182,7 +239,10 @@ pub async fn handle_wall_request( }); } - // TODO: authorize against polkit + authorize(polkit, |authority| { + check_wall(authority, source_pid, group.as_deref()) + }) + .await?; let body = tty_utils::format_wall_message( &username_for_uid(source_uid), @@ -216,14 +276,18 @@ pub async fn handle_wall_request( const DEFAULT_WRITE_TIMEOUT: Duration = Duration::from_secs(30); pub async fn handle_write_request( - _source_pid: u32, + polkit: Option<&PolkitAuthority>, + source_pid: u32, source_uid: u32, source_tty: Option, user: String, tty: Option, message: String, ) -> Result { - // TODO: authorize against polkit + authorize(polkit, |authority| { + check_write(authority, source_pid, &user, tty.as_deref()) + }) + .await?; let sessions = tty_utils::sessions_for_user(&user); if sessions.is_empty() {