difftreelog
feat functional polkit prompt
in: trunk
7 files changed
Cargo.lockdiffbeforeafterboth--- a/Cargo.lock
+++ b/Cargo.lock
@@ -883,6 +883,7 @@
name = "polkit-shared"
version = "0.1.0"
dependencies = [
+ "nix",
"serde",
"zbus",
]
cmds/polkit-backend/src/main.rsdiffbeforeafterboth--- a/cmds/polkit-backend/src/main.rs
+++ b/cmds/polkit-backend/src/main.rs
@@ -45,7 +45,7 @@
.prompt_text(
echo,
&prompt.to_string_lossy(),
- "Polkit prompt request",
+ "PAM prompt request",
&[],
)
.map_err(|e| {
@@ -81,7 +81,7 @@
let prompt = prompt.to_string_lossy();
let result = self
.0
- .prompt_radio(&prompt, "Polkit prompt request", &[])
+ .prompt_radio(&prompt, "PAM prompt request", &[])
.map_err(|_| ErrorCode::CONV_ERR)?;
Ok(result)
}
cmds/remowt-agent/src/main.rsdiffbeforeafterboth1use std::collections::HashMap;2use std::io::{stdout, Write};3use std::marker::PhantomData;4use std::sync::{Mutex, RwLock};5use std::{future, process};67use clap::Parser;8use polkit_shared::{BackendRequest, Identity};9use tokio::runtime::Handle;10use tokio::task::{AbortHandle, JoinHandle, LocalSet};11use tracing::trace;12use ui_prompt::dbus::DbusPrompterInterface;13use ui_prompt::rofi::RofiPrompter;14use ui_prompt::Prompter;15use zbus::zvariant::{OwnedValue, Str};16use zbus::ObjectServer;17use zbus::{interface, proxy, Connection};18use zbus_polkit::policykit1::Subject;1920struct TemporaryPrompterInterface<P: Prompter + Send + Sync + 'static> {21 connection: Connection,22 path: String,23 _marker: PhantomData<P>,24}25impl<P: Prompter + Send + Sync + 'static> TemporaryPrompterInterface<P> {26 async fn new(connection: Connection, prompter: P) -> Self {27 let path = format!(28 "/remowt/prompters/{}",29 uuid::Uuid::new_v4().to_string().replace("-", "_")30 );31 let _ = connection32 .object_server()33 .at(path.clone(), DbusPrompterInterface(prompter))34 .await;35 Self {36 connection,37 path,38 _marker: PhantomData,39 }40 }41}42impl<P: Prompter + Send + Sync + 'static> Drop for TemporaryPrompterInterface<P> {43 fn drop(&mut self) {44 // FIXME: block_in_place prevents to moving to current_thread runtime45 // There should be a blocking way to remove ObjectServer listener.46 // As far as I can see, it is only async because of async RwLock, shouldn't it be47 // just a sync lock?48 tokio::task::block_in_place(move || {49 Handle::current().block_on(async {50 let _ = self51 .connection52 .object_server()53 .remove::<DbusPrompterInterface<P>, String>(self.path.clone())54 .await;55 });56 });57 }58}5960struct Agent {61 helper: PolkitHelperProxy<'static>,62 tasks: Mutex<HashMap<String, AbortHandle>>,63 connection: Connection,64}65impl Agent {66 async fn new(connection: Connection) -> anyhow::Result<Self> {67 Ok(Self {68 helper: PolkitHelperProxy::new(&connection).await?,69 tasks: Mutex::new(HashMap::new()),70 connection,71 })72 }73}7475#[interface(name = "org.freedesktop.PolicyKit1.AuthenticationAgent")]76impl Agent {77 /// BeginAuthentication method78 #[allow(clippy::too_many_arguments)]79 async fn begin_authentication(80 &mut self,81 action_id: String,82 message: String,83 icon_name: String,84 details: HashMap<String, String>,85 cookie: String,86 identities: Vec<Identity>,87 ) -> zbus::fdo::Result<()> {88 trace!("begin auth");89 let task = {90 let connection = self.connection.clone();91 let helper = self.helper.clone();92 let cookie = cookie.clone();93 tokio::task::spawn(async move {94 trace!("conversation task");95 let prompter = TemporaryPrompterInterface::new(connection, RofiPrompter).await;96 helper97 .init_conversation(98 BackendRequest {99 cookie: cookie.to_owned(),100 environment: HashMap::new(),101 prompter_path: prompter.path.clone(),102 // TODO: Let user choose103 identity: identities.get(0).expect("first always exists").clone(),104 }, // cookie.to_owned(), HashMap::new(), prompter.path.clone()105 )106 .await?;107 println!("ASKED");108 dbg!(action_id, message, icon_name, details, cookie, identities);109110 Ok(())111 })112 };113114 self.tasks115 .lock()116 .unwrap()117 .insert(cookie.clone(), task.abort_handle());118 let result = task.await.expect("join error");119 // The only way to no reach this line, is to either panic in previous line, or if authorization cancelled,120 // while cancellation will remove task by itself.121 // TODO: But still it would be better to have abort guard, which will remove it from HashMap122 self.tasks.lock().unwrap().remove(&cookie);123124 result125 }126127 /// CancelAuthentication method128 async fn cancel_authentication(&self, cookie: &str) -> zbus::fdo::Result<()> {129 trace!("cancel auth");130 if let Some(abort) = self.tasks.lock().unwrap().remove(cookie) {131 abort.abort();132 }133 // debug!("Authentication cancled ! {cookie}");134 Ok(())135 }136}137138const OBJ_PATH: &str = "/0lach/polkitAgent";139140#[proxy(141 interface = "lach.PolkitHelper",142 default_service = "lach.polkit.helper1",143 default_path = "/lach/PolkitHelper"144)]145trait PolkitHelper {146 fn init_conversation(&self, request: BackendRequest) -> zbus::Result<()>;147}148149#[derive(Parser)]150enum Opts {151 Agent,152 AskPass { description: String },153}154155#[tokio::main]156async fn main() -> anyhow::Result<()> {157 tracing_subscriber::fmt::init();158 let opts = Opts::parse();159160 match opts {161 Opts::Agent => {162 trace!("started");163 let conn = Connection::system().await?;164165 let proxy = zbus_polkit::policykit1::AuthorityProxy::new(&conn).await?;166 conn.object_server()167 .at(OBJ_PATH, Agent::new(conn.clone()).await?)168 .await?;169170 let session_id = std::env::var("XDG_SESSION_ID")?;171 let mut details = HashMap::new();172 let val: OwnedValue = {173 let wrapped: Str<'_> = session_id.into();174 wrapped.into()175 };176 details.insert("session-id".to_string(), val);177 proxy178 .register_authentication_agent(179 &Subject {180 subject_kind: "unix-session".to_string(),181 subject_details: details,182 },183 "C",184 OBJ_PATH,185 )186 .await?;187 }188 Opts::AskPass { description } => {189 let password = RofiPrompter190 .prompt_text(false, &description, "SSH password request", &[])191 .await?;192 stdout().lock().write_all(password.as_bytes())?;193 }194 }195196 future::pending().await197}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 // FIXME: block_in_place prevents to moving to current_thread runtime46 // There should be a blocking way to remove ObjectServer listener.47 // As far as I can see, it is only async because of async RwLock, shouldn't it be48 // just a sync lock?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 /// BeginAuthentication method79 #[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 // TODO: Let user choose159 identity: identities[choosen_identity as usize].clone(),160 }, // cookie.to_owned(), HashMap::new(), prompter.path.clone()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 // The only way to no reach this line, is to either panic in previous line, or if authorization cancelled,176 // while cancellation will remove task by itself.177 // TODO: But still it would be better to have abort guard, which will remove it from HashMap178 self.tasks.lock().unwrap().remove(&cookie);179180 result181 }182183 /// CancelAuthentication method184 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 // debug!("Authentication cancled ! {cookie}");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}crates/polkit-shared/Cargo.tomldiffbeforeafterboth--- a/crates/polkit-shared/Cargo.toml
+++ b/crates/polkit-shared/Cargo.toml
@@ -4,5 +4,6 @@
edition = "2021"
[dependencies]
+nix = "0.29.0"
serde = { version = "1.0.204", features = ["derive"] }
zbus = "4.4.0"
crates/polkit-shared/src/lib.rsdiffbeforeafterboth--- a/crates/polkit-shared/src/lib.rs
+++ b/crates/polkit-shared/src/lib.rs
@@ -1,14 +1,83 @@
use std::collections::HashMap;
+use std::{fmt, fs};
+use nix::unistd::{Uid, User};
use serde::{Deserialize, Serialize};
-use zbus::zvariant::{OwnedValue, Type};
+use zbus::zvariant::{OwnedValue, Type, Value};
+
+pub fn emphasize(s: impl AsRef<str>) -> String {
+ format!("<span style=\"italic\"><{}></span>", escape(s),)
+}
+fn command(s: impl AsRef<str>) -> String {
+ format!("<u><tt>{}</tt></u>", s.as_ref())
+}
+fn escape(s: impl AsRef<str>) -> String {
+ s.as_ref()
+ .replace("&", """)
+ .replace("<", "<")
+ .replace(">", ">")
+}
+pub struct PidDisplay(pub u32);
+impl fmt::Display for PidDisplay {
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+ if self.0 == 1 {
+ emphasize("init").fmt(f)
+ } else if let Ok(proc) = fs::read_to_string(format!("/proc/{}/cmdline", self.0)) {
+ write!(
+ f,
+ "<sub>command</sub>{}",
+ command(
+ proc.replace("\0", " ")
+ .strip_suffix(" ")
+ .expect("cmdline should end with NUL")
+ )
+ )
+ } else if let Ok(proc) = fs::read_to_string(format!("/proc/{}/comm", self.0)) {
+ write!(f, "<sub>process</sub>{}", command(proc.replace("\0", " ")))
+ } else {
+ emphasize("unknown process").fmt(f)
+ }
+ }
+}
+
#[derive(Serialize, Deserialize, Type, PartialEq, Debug)]
pub struct Identity {
pub kind: String,
pub details: HashMap<String, OwnedValue>,
}
+impl fmt::Display for Identity {
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+ match self.kind.as_str() {
+ "unix-user" => match self.details.get("uid").map(|v| &**v) {
+ Some(Value::U32(uid)) => match User::from_uid(Uid::from_raw(*uid)) {
+ Ok(Some(u)) => write!(
+ f,
+ "<sub>user</sub>{}<sup>{}</sup>{}",
+ u.name,
+ u.uid,
+ if u.gecos.is_empty() {
+ "".to_owned()
+ } else {
+ format!(": {}", escape(u.gecos.to_string_lossy()))
+ }
+ ),
+ Ok(None) => emphasize("not found").fmt(f),
+ Err(e) => {
+ let user = format!("could not get user: {e}");
+ emphasize(&user).fmt(f)?;
+ Ok(())
+ }
+ },
+
+ _ => emphasize("unknown uid").fmt(f),
+ },
+ _ => emphasize(format!("identity of unknown kind: {}", self.kind)).fmt(f),
+ }
+ }
+}
+
impl Clone for Identity {
fn clone(&self) -> Self {
Self {
crates/ui-prompt/src/lib.rsdiffbeforeafterboth--- a/crates/ui-prompt/src/lib.rs
+++ b/crates/ui-prompt/src/lib.rs
@@ -18,7 +18,7 @@
#[cfg_attr(feature = "dbus", derive(zbus::zvariant::Type))]
#[derive(serde::Serialize, serde::Deserialize, Clone)]
-pub struct Source(Cow<'static, str>);
+pub struct Source(pub Cow<'static, str>);
impl fmt::Display for Source {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "<u>{}</u>", self.0)
@@ -79,8 +79,9 @@
}
pub struct PrependSourcePrompter<P> {
- prompter: P,
- source: Vec<Source>,
+ pub prompter: P,
+ pub source: Vec<Source>,
+ pub description: String,
}
impl<P> PrependSourcePrompter<P> {
fn source(&self, input: &[Source]) -> Vec<Source> {
@@ -88,11 +89,31 @@
out.extend(input.iter().cloned());
out
}
+ fn description(&self, input: &str) -> String {
+ if self.description.is_empty() {
+ input.to_owned()
+ } else if input.is_empty() {
+ self.description.to_owned()
+ } else {
+ format!("{input}\n\n{}", self.description)
+ }
+ }
}
impl<P> Prompter for PrependSourcePrompter<P>
where
P: Prompter + Sync,
{
+ async fn prompt_radio(
+ &self,
+ prompt: &str,
+ description: &str,
+ source: &[Source],
+ ) -> Result<bool> {
+ self.prompter
+ .prompt_radio(prompt, &self.description(description), &self.source(source))
+ .await
+ }
+
async fn prompt_enum(
&self,
prompt: &str,
@@ -101,7 +122,12 @@
source: &[Source],
) -> Result<u32> {
self.prompter
- .prompt_enum(prompt, description, variants, &self.source(source))
+ .prompt_enum(
+ prompt,
+ dbg!(&self.description(description)),
+ variants,
+ &self.source(source),
+ )
.await
}
@@ -113,13 +139,18 @@
source: &[Source],
) -> Result<String> {
self.prompter
- .prompt_text(echo, prompt, description, &self.source(source))
+ .prompt_text(
+ echo,
+ prompt,
+ &self.description(description),
+ &self.source(source),
+ )
.await
}
async fn display_text(&self, error: bool, description: &str, source: &[Source]) -> Result<()> {
self.prompter
- .display_text(error, description, &self.source(source))
+ .display_text(error, &self.description(description), &self.source(source))
.await
}
}
crates/ui-prompt/src/rofi.rsdiffbeforeafterboth--- a/crates/ui-prompt/src/rofi.rs
+++ b/crates/ui-prompt/src/rofi.rs
@@ -27,7 +27,10 @@
description.to_owned()
} else {
let mut out = format!("{description}\n\n<b>Requested on ",);
- for s in source.iter() {
+ for (i, s) in source.iter().enumerate() {
+ if i != 0 {
+ out.push_str(" -> ");
+ }
out.push_str(&s.to_string());
}
out.push_str("</b>");
@@ -43,6 +46,7 @@
fixup_prompt(prompt),
"-format",
"i",
+ "-markup-rows",
]);
cmd.stdin(Stdio::piped());
cmd.stdout(Stdio::piped());
@@ -100,7 +104,10 @@
description.to_owned()
} else {
let mut out = format!("{description}\n\n<b>Requested on ",);
- for s in source.iter() {
+ for (i, s) in source.iter().enumerate() {
+ if i != 0 {
+ out.push_str(" -> ");
+ }
out.push_str(&s.to_string());
}
out.push_str("</b>");