Files
feresaria/shell/ferese-polkit-agent/src/main.rs
T

183 lines
5.5 KiB
Rust

mod prompt;
mod session;
use serde::{Deserialize, Serialize};
use std::{
collections::HashMap,
sync::{
Arc, Mutex,
atomic::{AtomicBool, Ordering},
},
};
use zbus::zvariant::{OwnedValue, Value};
const AGENT_PATH: &str = "/dev/ferese/PolkitAgent";
const AUTHORITY: &str = "org.freedesktop.PolicyKit1";
const AUTHORITY_PATH: &str = "/org/freedesktop/PolicyKit1/Authority";
const AUTHORITY_INTERFACE: &str = "org.freedesktop.PolicyKit1.Authority";
#[derive(Debug, Serialize, Deserialize)]
#[serde(tag = "type", rename_all = "snake_case")]
enum PromptEvent {
Start { message: String, user: String },
Request { prompt: String, echo: bool },
Info { text: String },
Error { text: String },
Response { value: String },
Cancel,
}
type Identity = (String, HashMap<String, OwnedValue>);
#[derive(Default)]
struct Agent {
active: Mutex<HashMap<String, Arc<AtomicBool>>>,
}
#[zbus::interface(name = "org.freedesktop.PolicyKit1.AuthenticationAgent")]
impl Agent {
async fn begin_authentication(
&self,
_action_id: String,
message: String,
_icon_name: String,
_details: HashMap<String, String>,
cookie: String,
identities: Vec<Identity>,
) -> zbus::fdo::Result<()> {
let uid = choose_identity(&identities).ok_or_else(|| {
zbus::fdo::Error::Failed("No supported authentication identity".into())
})?;
let cancelled = Arc::new(AtomicBool::new(false));
{
let mut active = self.active.lock().unwrap();
if active.contains_key(&cookie) {
return Err(zbus::fdo::Error::Failed("Duplicate request".into()));
}
active.insert(cookie.clone(), cancelled.clone());
}
let result = tokio::task::spawn_blocking({
let cookie = cookie.clone();
move || {
session::authenticate(uid, message.chars().take(240).collect(), cookie, cancelled)
}
})
.await
.map_err(|error| zbus::fdo::Error::Failed(error.to_string()))?;
self.active.lock().unwrap().remove(&cookie);
match result {
Ok(true) => Ok(()),
Ok(false) => Err(zbus::fdo::Error::Failed("Authentication cancelled".into())),
Err(error) => Err(zbus::fdo::Error::Failed(error)),
}
}
fn cancel_authentication(&self, cookie: String) {
if let Some(cancelled) = self.active.lock().unwrap().get(&cookie) {
cancelled.store(true, Ordering::Release);
}
}
}
fn choose_identity(identities: &[Identity]) -> Option<u32> {
let available: Vec<_> = identities
.iter()
.filter(|(kind, _)| kind == "unix-user")
.filter_map(|(_, details)| {
details
.get("uid")
.and_then(|uid| u32::try_from(uid.clone()).ok())
.filter(|uid| *uid <= i32::MAX as u32)
})
.collect();
let current = unsafe { libc::geteuid() };
available
.iter()
.copied()
.find(|uid| *uid == current)
.or_else(|| available.first().copied())
}
fn session_subject() -> Result<(String, HashMap<String, Value<'static>>), String> {
let stat = std::fs::read_to_string("/proc/self/stat").map_err(|error| error.to_string())?;
let suffix = stat
.rsplit_once(") ")
.map(|(_, suffix)| suffix)
.ok_or("Invalid /proc/self/stat")?;
let started = suffix
.split_whitespace()
.nth(19)
.ok_or("Missing process start time")?
.parse::<u64>()
.map_err(|error| error.to_string())?;
let mut details = HashMap::new();
details.insert("pid".to_owned(), Value::from(std::process::id()));
details.insert(
"uid".to_owned(),
Value::from(unsafe { libc::geteuid() } as i32),
);
details.insert("start-time".to_owned(), Value::from(started));
Ok(("unix-process".into(), details))
}
async fn run_agent() -> Result<(), Box<dyn std::error::Error>> {
let subject = session_subject()?;
let connection = zbus::connection::Builder::system()?
.serve_at(AGENT_PATH, Agent::default())?
.build()
.await?;
let authority =
zbus::Proxy::new(&connection, AUTHORITY, AUTHORITY_PATH, AUTHORITY_INTERFACE).await?;
let locale = std::env::var("LANG").unwrap_or_default();
authority
.call::<_, _, ()>(
"RegisterAuthenticationAgent",
&(subject, locale, AGENT_PATH),
)
.await?;
std::future::pending::<()>().await;
Ok(())
}
fn main() -> Result<(), Box<dyn std::error::Error>> {
unsafe {
libc::prctl(libc::PR_SET_DUMPABLE, 0);
}
match std::env::args().nth(1).as_deref() {
None => tokio::runtime::Builder::new_multi_thread()
.enable_all()
.build()?
.block_on(run_agent()),
Some("--prompt") => prompt::run(),
Some("--check") => session::check().map_err(Into::into),
_ => Err("Usage: ferese-polkit-agent [--check]".into()),
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn chooses_current_identity_when_available() {
let current = unsafe { libc::geteuid() };
let identity = |uid: u32| {
(
"unix-user".to_owned(),
HashMap::from([("uid".to_owned(), OwnedValue::from(uid))]),
)
};
assert_eq!(
choose_identity(&[identity(0), identity(current)]),
Some(current)
);
}
}