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
+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() {