walld: implement polkit auth

This commit is contained in:
2026-07-30 19:08:15 +09:00
parent 73dbfc934f
commit bdc5121fab
6 changed files with 453 additions and 18 deletions
Generated
+227 -8
View File
@@ -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",
]
+1
View File
@@ -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"
+1
View File
@@ -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;
+128
View File
@@ -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<String, String>)>;
}
#[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<zbus::zvariant::OwnedObjectPath>;
}
#[proxy(interface = "org.freedesktop.login1.Session")]
trait LoginSession {
#[zbus(property)]
fn id(&self) -> zbus::Result<String>;
}
#[derive(Clone)]
pub struct PolkitAuthority {
connection: Connection,
}
impl PolkitAuthority {
pub async fn connect() -> anyhow::Result<Self> {
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<String> {
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<bool> {
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/<pid>/stat`
fn process_start_time(pid: u32) -> anyhow::Result<u64> {
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::<u64>()
.context("starttime field is not a valid number")
}
+24 -2
View File
@@ -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<IgnoreList>,
polkit: Option<PolkitAuthority>,
}
impl VarlinkRoowhoo2ClientServer {
@@ -60,12 +61,14 @@ impl VarlinkRoowhoo2ClientServer {
rwhod_enabled: bool,
fingerd_enabled: bool,
finger_ignore_list: Option<IgnoreList>,
polkit: Option<PolkitAuthority>,
) -> Self {
Self {
whod_status_store,
rwhod_enabled,
fingerd_enabled,
finger_ignore_list,
polkit,
}
}
}
@@ -283,6 +286,7 @@ impl zlink::Service<UnixStream> 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<UnixStream> 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<IgnoreList>,
) -> 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);
+72 -8
View File
@@ -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<bool> {
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<bool> {
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<Output = anyhow::Result<bool>>,
{
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<String>,
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<String>,
user: String,
tty: Option<String>,
message: String,
) -> Result<VarlinkWriteResponse, VarlinkWalldClientError> {
// 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() {