1use std::borrow::Cow;2use std::collections::{BTreeMap, HashMap};3use std::io::{stdout, Write};4use std::marker::PhantomData;5use std::sync::{Mutex, RwLock};6use std::{future, process};78use clap::Parser;9use polkit_shared::{emphasize, BackendRequest, Identity, PidDisplay};10use tokio::runtime::Handle;11use tokio::task::{AbortHandle, JoinHandle, LocalSet};12use tracing::trace;13use ui_prompt::dbus::DbusPrompterInterface;14use ui_prompt::rofi::RofiPrompter;15use ui_prompt::{PrependSourcePrompter, Prompter, Source};16use zbus::zvariant::{OwnedValue, Str};17use zbus::{fdo, ObjectServer};18use zbus::{interface, proxy, Connection};19use zbus_polkit::policykit1::Subject;2021struct TemporaryPrompterInterface<P: Prompter + Send + Sync + 'static> {22 connection: Connection,23 path: String,24 _marker: PhantomData<P>,25}26impl<P: Prompter + Send + Sync + 'static> TemporaryPrompterInterface<P> {27 async fn new(connection: Connection, prompter: P) -> Self {28 let path = format!(29 "/remowt/prompters/{}",30 uuid::Uuid::new_v4().to_string().replace("-", "_")31 );32 let _ = connection33 .object_server()34 .at(path.clone(), DbusPrompterInterface(prompter))35 .await;36 Self {37 connection,38 path,39 _marker: PhantomData,40 }41 }42}43impl<P: Prompter + Send + Sync + 'static> Drop for TemporaryPrompterInterface<P> {44 fn drop(&mut self) {45 46 47 48 49 tokio::task::block_in_place(move || {50 Handle::current().block_on(async {51 let _ = self52 .connection53 .object_server()54 .remove::<DbusPrompterInterface<P>, String>(self.path.clone())55 .await;56 });57 });58 }59}6061struct Agent {62 helper: PolkitHelperProxy<'static>,63 tasks: Mutex<HashMap<String, AbortHandle>>,64 connection: Connection,65}66impl Agent {67 async fn new(connection: Connection) -> anyhow::Result<Self> {68 Ok(Self {69 helper: PolkitHelperProxy::new(&connection).await?,70 tasks: Mutex::new(HashMap::new()),71 connection,72 })73 }74}7576#[interface(name = "org.freedesktop.PolicyKit1.AuthenticationAgent")]77impl Agent {78 79 #[allow(clippy::too_many_arguments)]80 async fn begin_authentication(81 &mut self,82 action_id: String,83 message: String,84 icon_name: String,85 mut details: BTreeMap<String, String>,86 cookie: String,87 identities: Vec<Identity>,88 ) -> zbus::fdo::Result<()> {89 use std::fmt::Write;90 trace!("begin auth");91 let task = {92 let connection = self.connection.clone();93 let helper = self.helper.clone();94 let cookie = cookie.clone();95 tokio::task::spawn(async move {96 trace!("conversation task");97 let mut description = format!("{message}\n\n<b>Action id:</b> {action_id}",);98 if let Some(subject) = details.remove("polkit.caller-pid") {99 let _ = write!(description, "\n<b>Caller:</b> ");100 if let Ok(pid) = subject.parse::<u32>() {101 let _ = write!(description, "{}", PidDisplay(pid));102 } else {103 let _ = write!(description, "{}", emphasize("invalid pid"));104 }105 }106 if let Some(subject) = details.remove("polkit.subject-pid") {107 let _ = write!(description, "\n<b>Subject:</b> ");108 if let Ok(pid) = subject.parse::<u32>() {109 let _ = write!(description, "{}", PidDisplay(pid));110 } else {111 let _ = write!(description, "{}", emphasize("invalid pid"));112 }113 }114 let mut prompter = PrependSourcePrompter {115 source: vec![Source(Cow::Borrowed("polkit agent"))],116 description: description.clone(),117 prompter: RofiPrompter,118 };119120 let identity_displays: Vec<String> =121 identities.iter().map(|v| v.to_string()).collect();122 let identity_displays: Vec<&str> =123 identity_displays.iter().map(|v| v.as_str()).collect();124 let choosen_identity = match identity_displays.len() {125 0 => {126 return Err(fdo::Error::AuthFailed(127 "no identity to authenticate as".to_owned(),128 ))129 }130 1 => 0,131 _ => {132 prompter133 .prompt_enum(134 "Identity",135 "Select identity to use for polkit authorization",136 &identity_displays,137 &[],138 )139 .await?140 }141 };142143 let _ = write!(144 description,145 "\n<b>Identity:</b> {}",146 identities[choosen_identity as usize]147 );148 prompter.description = description;149150 prompter.source.push(Source(Cow::Borrowed("polkit daemon")));151 let prompter = TemporaryPrompterInterface::new(connection, prompter).await;152 helper153 .init_conversation(154 BackendRequest {155 cookie: cookie.to_owned(),156 environment: HashMap::new(),157 prompter_path: prompter.path.clone(),158 159 identity: identities[choosen_identity as usize].clone(),160 }, 161 )162 .await?;163 println!("ASKED");164 dbg!(action_id, message, icon_name, details, cookie, identities);165166 Ok(())167 })168 };169170 self.tasks171 .lock()172 .unwrap()173 .insert(cookie.clone(), task.abort_handle());174 let result = task.await.expect("join error");175 176 177 178 self.tasks.lock().unwrap().remove(&cookie);179180 result181 }182183 184 async fn cancel_authentication(&self, cookie: &str) -> zbus::fdo::Result<()> {185 trace!("cancel auth");186 if let Some(abort) = self.tasks.lock().unwrap().remove(cookie) {187 abort.abort();188 }189 190 Ok(())191 }192}193194const OBJ_PATH: &str = "/org/freedesktop/PolicyKit1/AuthenticationAgent";195196#[proxy(197 interface = "lach.PolkitHelper",198 default_service = "lach.polkit.helper1",199 default_path = "/lach/PolkitHelper"200)]201trait PolkitHelper {202 fn init_conversation(&self, request: BackendRequest) -> zbus::Result<()>;203}204205#[derive(Parser)]206enum Opts {207 Agent,208 AskPass { description: String },209}210211#[tokio::main]212async fn main() -> anyhow::Result<()> {213 tracing_subscriber::fmt::init();214 let opts = Opts::parse();215216 match opts {217 Opts::Agent => {218 trace!("started");219 let conn = Connection::system().await?;220221 let proxy = zbus_polkit::policykit1::AuthorityProxy::new(&conn).await?;222 conn.object_server()223 .at(OBJ_PATH, Agent::new(conn.clone()).await?)224 .await?;225226 let session_id = std::env::var("XDG_SESSION_ID")?;227 let mut details = HashMap::new();228 let val: OwnedValue = {229 let wrapped: Str<'_> = session_id.into();230 wrapped.into()231 };232 details.insert("session-id".to_string(), val);233 proxy234 .register_authentication_agent(235 &Subject {236 subject_kind: "unix-session".to_string(),237 subject_details: details,238 },239 "C",240 OBJ_PATH,241 )242 .await?;243 }244 Opts::AskPass { description } => {245 let password = RofiPrompter246 .prompt_text(false, &description, "SSH password request", &[])247 .await?;248 stdout().lock().write_all(password.as_bytes())?;249 }250 }251252 future::pending().await253}