walld: implement polkit auth
This commit is contained in:
@@ -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;
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
@@ -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() {
|
||||
|
||||
Reference in New Issue
Block a user