git.delta.rocks / remowt / refs/commits / d819a0103251

difftreelog

feat functional polkit prompt

pvnwrxrnYaroslav Bolyukin2024-08-06parent: #2499daa.patch.diff
in: trunk

7 files changed

modifiedCargo.lockdiffbeforeafterboth
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -883,6 +883,7 @@
 name = "polkit-shared"
 version = "0.1.0"
 dependencies = [
+ "nix",
  "serde",
  "zbus",
 ]
modifiedcmds/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)
     }
modifiedcmds/remowt-agent/src/main.rsdiffbeforeafterboth
before · cmds/remowt-agent/src/main.rs
1use 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}
after · cmds/remowt-agent/src/main.rs
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}
modifiedcrates/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"
modifiedcrates/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\">&lt;{}&gt;</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("&", "&quot;")
+        .replace("<", "&lt;")
+        .replace(">", "&gt;")
+}
 
+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 {
modifiedcrates/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
     }
 }
modifiedcrates/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>");