git.delta.rocks / remowt / refs/commits / 960ee4bca68c

difftreelog

feat remowt-askpass, remowt-editor

qprrsuurYaroslav Bolyukin2026-01-25parent: #5f64fb1.patch.diff
in: trunk

5 files changed

modifiedcmds/remowt-agent/Cargo.tomldiffbeforeafterboth
4edition = "2021"4edition = "2021"
55
6[dependencies]6[dependencies]
7anyhow = "1.0.86"7anyhow.workspace = true
8bifrostlink.workspace = true8bifrostlink.workspace = true
9bifrostlink-ports.workspace = true9bifrostlink-ports.workspace = true
10clap = { version = "4.5.13", features = ["derive"] }10clap = { workspace = true, features = ["derive"] }
11futures = "0.3.30"11futures.workspace = true
12futures-util = "0.3.30"12futures-util.workspace = true
13nix = "0.29.0"13nix.workspace = true
14polkit-shared = { version = "0.1.0", path = "../../crates/polkit-shared" }14polkit-shared.workspace = true
15rand = "0.8.5"15rand.workspace = true
16remowt-link-shared = { version = "0.1.0", path = "../../crates/remowt-link-shared" }16remowt-link-shared.workspace = true
17remowt-pty.workspace = true
17serde = { version = "1.0.204", features = ["derive"] }18serde = { workspace = true, features = ["derive"] }
19tempfile.workspace = true
18tokio = { version = "1.39.2", features = ["rt-multi-thread", "fs", "macros"] }20tokio = { workspace = true, features = [
21 "rt",
22 "fs",
23 "macros",
24 "net",
25 "io-util",
26 "time",
27 "process",
28] }
19tokio-util = { version = "0.7.11", features = ["codec"] }29tokio-util = { workspace = true, features = ["codec"] }
20tracing = "0.1.40"30tracing.workspace = true
21tracing-subscriber = "0.3.18"31tracing-subscriber.workspace = true
22ui-prompt = { version = "0.1.0", path = "../../crates/ui-prompt" }32ui-prompt.workspace = true
23uuid = { version = "1.10.0", features = ["v4"] }33uuid = { workspace = true, features = ["v4"] }
24zbus = { version = "4.4.0", features = ["tokio"] }34zbus = { workspace = true, features = ["tokio"] }
25zbus_polkit = { version = "4.0.0", features = ["tokio"] }35zbus_polkit = { workspace = true, features = ["tokio"] }
2636
addedcmds/remowt-agent/src/askpass.rsdiffbeforeafterboth

no changes

addedcmds/remowt-agent/src/bus.rsdiffbeforeafterboth

no changes

addedcmds/remowt-agent/src/editor.rsdiffbeforeafterboth

no changes

modifiedcmds/remowt-agent/src/main.rsdiffbeforeafterboth
1use std::borrow::Cow;1use std::borrow::Cow;
2use std::collections::{BTreeMap, HashMap};2use std::collections::{BTreeMap, HashMap};
3use std::fs::Permissions;
3use std::future;4use std::future::pending;
4use std::io::{stdout, Write};5use std::os::unix::fs::PermissionsExt as _;
5use std::path::PathBuf;6use std::path::PathBuf;
6use std::sync::{Arc, Mutex, OnceLock};7use std::sync::{Arc, Mutex, OnceLock};
78
9use bifrostlink::declarative::RemoteEndpoints;
8use bifrostlink::{AddressT, Rpc};10use bifrostlink::Rpc;
11use bifrostlink_ports::stdio::from_stdio;
9use bifrostlink_ports::unix_socket::from_socket;12use bifrostlink_ports::unix_socket::from_socket;
10use clap::Parser;13use clap::Parser;
11use polkit_shared::{emphasize, BackendRequest, Identity, PidDisplay};14use polkit_shared::{emphasize, BackendRequest, Identity, PidDisplay};
15use remowt_link_shared::editor::EditorEndpointsClient;
12use remowt_link_shared::Address;16use remowt_link_shared::{Address, BifConfig, Fs, Pty, Systemd};
13use tokio::io::{AsyncReadExt, AsyncWriteExt};17use tokio::fs;
14use tokio::net::UnixStream;18use tokio::net::UnixStream;
15use tokio::runtime::Runtime;19use tokio::runtime::Builder;
16use tokio::task::AbortHandle;20use tokio::task::AbortHandle;
17use tracing::{info, trace};21use tracing::{info, trace};
18use ui_prompt::rofi::RofiPrompter;22use ui_prompt::bifrost::PromptEndpointsClient;
19use ui_prompt::{PrependSourcePrompter, Prompter, Source};23use ui_prompt::{PrependSourcePrompter, Prompter, Source};
20use zbus::fdo;24use zbus::fdo;
21use zbus::zvariant::{OwnedValue, Str};25use zbus::zvariant::{OwnedValue, Str};
22use zbus::{interface, proxy, Connection};26use zbus::{interface, proxy, Connection};
23use zbus_polkit::policykit1::Subject;27use zbus_polkit::policykit1::Subject;
2428
25use self::helper::{Helper, SuidHelper};29use self::helper::{Helper, SocketHelper, SuidHelper};
2630
31pub mod askpass;
32pub mod bus;
33pub mod editor;
27pub mod helper;34pub mod helper;
2835
29struct CancelTaskOnDrop {36struct CancelTaskOnDrop {
44 }51 }
45}52}
4653
47struct Agent<H> {54struct Agent<H, P> {
48 tasks: Arc<Mutex<HashMap<String, AbortHandle>>>,55 tasks: Arc<Mutex<HashMap<String, AbortHandle>>>,
49 helper: H,56 helper: H,
57 prompter: P,
50}58}
51impl<H> Agent<H> {59impl<H, P> Agent<H, P> {
52 fn new(helper: H) -> Self {60 fn new(helper: H, prompter: P) -> Self {
53 Agent {61 Agent {
54 tasks: Arc::new(Mutex::new(HashMap::new())),62 tasks: Arc::new(Mutex::new(HashMap::new())),
55 helper,63 helper,
64 prompter,
56 }65 }
57 }66 }
58}67}
5968
60#[interface(name = "org.freedesktop.PolicyKit1.AuthenticationAgent")]69#[interface(name = "org.freedesktop.PolicyKit1.AuthenticationAgent")]
61impl<H> Agent<H>70impl<H, P> Agent<H, P>
62where71where
63 H: Helper + Clone + Send + Sync + 'static,72 H: Helper + Clone + Send + Sync + 'static,
73 P: Prompter + Clone + Send + Sync + 'static,
64{74{
65 /// BeginAuthentication method75 /// BeginAuthentication method
66 #[allow(clippy::too_many_arguments)]76 #[allow(clippy::too_many_arguments)]
67 async fn begin_authentication(77 async fn begin_authentication(
68 &self,78 &self,
69 action_id: String,79 action_id: String,
70 message: String,80 message: String,
71 icon_name: String,81 _icon_name: String,
72 mut details: BTreeMap<String, String>,82 mut details: BTreeMap<String, String>,
73 cookie: String,83 cookie: String,
74 identities: Vec<Identity>,84 identities: Vec<Identity>,
78 let _cancel_guard = Arc::new(OnceLock::new());88 let _cancel_guard = Arc::new(OnceLock::new());
79 let task = {89 let task = {
80 let helper = self.helper.clone();90 let helper = self.helper.clone();
91 let prompter = self.prompter.clone();
81 let cookie = cookie.clone();92 let cookie = cookie.clone();
82 let _cancel_guard = _cancel_guard.clone();93 let _cancel_guard = _cancel_guard.clone();
83 tokio::task::spawn(async move {94 tokio::task::spawn(async move {
103 let mut prompter = PrependSourcePrompter {114 let mut prompter = PrependSourcePrompter {
104 source: vec![Source(Cow::Borrowed("polkit agent"))],115 source: vec![Source(Cow::Borrowed("polkit agent"))],
105 description: description.clone(),116 description: description.clone(),
106 prompter: RofiPrompter,117 prompter,
107 };118 };
108119
109 let identity_displays: Vec<String> =120 let identity_displays: Vec<String> =
194205
195#[derive(Parser)]206#[derive(Parser)]
196enum Opts {207enum Opts {
197 Agent,
198 AskPass {208 AskPass {
209 prompt: String,
199 description: String,210 description: String,
200 },211 },
212 Editor {
213 /// Argument to nvim
214 path: String,
215 },
201 RealAgent {216 RealAgent {
202 #[arg(long)]217 #[arg(long)]
203 path: PathBuf,218 path: Option<PathBuf>,
219 /// Expect own address to be AgentPrivileged, skip installing polkit agent
220 #[arg(long)]
221 privileged: bool,
204 },222 },
205}223}
206224
207fn main() -> anyhow::Result<()> {225fn main() -> anyhow::Result<()> {
226 // Log to stderr: `privileged-agent` uses stdout as the bifrost transport,
227 // so anything written there would corrupt the stream.
208 tracing_subscriber::fmt::init();228 tracing_subscriber::fmt()
229 .with_writer(std::io::stderr)
230 .init();
209 let opts = Opts::parse();231 let opts = Opts::parse();
210232
211 let runtime = Runtime::new()?;233 let runtime = Builder::new_current_thread().enable_all().build()?;
212234
213 match opts {235 match opts {
214 Opts::Agent => {236 Opts::AskPass {
215 // TODO: Setup env, directories with various things...237 prompt,
238 description,
216 runtime.block_on(main_agent())239 } => runtime.block_on(askpass::ask(&prompt, description)),
217 }
218 Opts::AskPass { description } => runtime.block_on(main_askpass(description)),240 Opts::Editor { path } => runtime.block_on(editor::edit(path)),
219 Opts::RealAgent { path } => runtime.block_on(main_real_agent(path)),241 Opts::RealAgent { path, privileged } => runtime.block_on(main_real_agent(path, privileged)),
220 }242 }
221}243}
222async fn main_real_agent(path: PathBuf) -> anyhow::Result<()> {244async fn main_real_agent(path: Option<PathBuf>, privileged: bool) -> anyhow::Result<()> {
223 let mut stream = UnixStream::connect(path).await?;245 let address = if privileged {
246 Address::AgentPrivileged
247 } else {
248 Address::Agent
249 };
250 let mut rpc = Rpc::<BifConfig>::new(address);
251
252 Fs::new().register_endpoints(&mut rpc);
253 Systemd.register_endpoints(&mut rpc);
254 Pty::new().register_endpoints(&mut rpc);
255
256 let user_prompter = PromptEndpointsClient::wrap(rpc.remote(Address::User));
257 let editor_client = EditorEndpointsClient::wrap(rpc.remote(Address::User));
258
259 let bus = bus::spawn().await?;
224 stream.write_all(b"REMOWT_HELLO\0").await?;260 askpass::serve(&bus.conn, user_prompter.clone()).await?;
261 editor::serve(&bus.conn, editor_client).await?;
262
225 let mut buf = [0u8; 12];263 let helpers = tempfile::Builder::new().prefix("remowt-path.").tempdir()?;
264 let exe = std::env::current_exe()?;
265 let askpass_helper = helpers.path().join("remowt-askpass");
266 let editor_helper = helpers.path().join("remowt-editor");
267 {
268 let script = format!(
269 "#!/bin/sh\nexec {} ask-pass \"password\" \"$1\"\n",
270 sh_quote(&exe.to_string_lossy())
271 );
226 stream.read_exact(&mut buf).await?;272 fs::write(&askpass_helper, script).await?;
227 assert_eq!(&buf, b"REMOWT_EHLO\0");273 fs::set_permissions(&askpass_helper, Permissions::from_mode(0o755)).await?;
274 }
275 {
228 let port = from_socket(stream);276 let script = format!(
277 "#!/bin/sh\nexec {} editor \"$1\"\n",
278 sh_quote(&exe.to_string_lossy())
279 );
280 fs::write(&editor_helper, script).await?;
281 fs::set_permissions(&editor_helper, Permissions::from_mode(0o755)).await?;
282 }
283
284 // Safety: Hoping tokio own threads won't read any of those...
285 unsafe {
286 prepend_path(helpers.path());
287 std::env::set_var("SUDO_ASKPASS", &askpass_helper);
288 std::env::set_var("SSH_ASKPASS", &askpass_helper);
289 std::env::set_var("SSH_ASKPASS_REQUIRE", "force");
290 std::env::set_var("EDITOR", &editor_helper);
291 std::env::set_var("VISUAL", &editor_helper);
292 std::env::set_var("DBUS_SESSION_BUS_ADDRESS", &bus.address);
293 }
294
229 let rpc = Rpc::<Address, remowt_link_shared::Error>::new(Address::Agent);295 let port = match path {
296 Some(path) => from_socket(UnixStream::connect(path).await?),
297 None => from_stdio(),
298 };
230 rpc.add_direct(Address::User, port, bifrostlink::Rtt(0));299 rpc.add_direct(Address::User, port, bifrostlink::Rtt(0));
300
301 let polkit_conn = if !privileged {
302 // The unprivileged agent doubles as a polkit authentication agent so
303 // `run0` (e.g. our own elevation) routes its prompt to the User over
304 // bifrost instead of failing on a tty-less session.
305 let conn = Connection::system().await?;
306 let helper = SocketHelper {
307 fallback: SuidHelper,
308 };
231 Ok(())309 register_auth_agent(&conn, Agent::new(helper, user_prompter)).await?;
310 Some(conn)
311 } else {
312 None
313 };
314
315 let _keep_alive = (bus, helpers, polkit_conn);
316 pending().await
232}317}
233318
234async fn main_agent() -> anyhow::Result<()> {319async fn register_auth_agent<H, P>(conn: &Connection, agent: Agent<H, P>) -> anyhow::Result<()>
235 trace!("started");320where
236 let conn = Connection::system().await?;321 H: Helper + Clone + Send + Sync + 'static,
237322 P: Prompter + Clone + Send + Sync + 'static,
323{
324 let proxy = zbus_polkit::policykit1::AuthorityProxy::new(conn).await?;
325 conn.object_server().at(OBJ_PATH, agent).await?;
326
327 let subject = auth_agent_subject()?;
328 proxy
329 .register_authentication_agent(&subject, "C", OBJ_PATH)
330 .await?;
331 info!(kind = subject.subject_kind, "registered polkit agent");
332 Ok(())
333}
334
335fn auth_agent_subject() -> anyhow::Result<Subject> {
238 let proxy = zbus_polkit::policykit1::AuthorityProxy::new(&conn).await?;336 let mut details = HashMap::new();
239 conn.object_server()
240 .at(OBJ_PATH, Agent::new(SuidHelper))
241 .await?;
242
243 let session_id = std::env::var("XDG_SESSION_ID")?;337 if let Ok(session_id) = std::env::var("XDG_SESSION_ID") {
244 let mut details = HashMap::new();
245 let val: OwnedValue = {338 let val: OwnedValue = Str::from(session_id).into();
246 let wrapped: Str<'_> = session_id.into();
247 wrapped.into()
248 };
249 details.insert("session-id".to_string(), val);339 details.insert("session-id".to_string(), val);
250 proxy
251 .register_authentication_agent(340 return Ok(Subject {
252 &Subject {
253 subject_kind: "unix-session".to_string(),341 subject_kind: "unix-session".to_string(),
254 subject_details: details,342 subject_details: details,
255 },343 });
256 "C",344 }
257 OBJ_PATH,345
258 )
259 .await?;
260 future::pending().await346 details.insert("pid".to_string(), OwnedValue::from(std::process::id()));
261}347 Ok(Subject {
348 subject_kind: "unix-process".to_string(),
349 subject_details: details,
350 })
351}
262352
263async fn main_askpass(description: String) -> anyhow::Result<()> {353fn sh_quote(s: &str) -> String {
264 let password = RofiPrompter354 format!("'{}'", s.replace('\'', "'\\''"))
265 .prompt_text(false, &description, "SSH password request", &[])355}
266 .await?;356
357/// Prepend `dir` to the process `PATH`.
358///
359/// # SAFETY
360///
361/// Same as `set_var`
362unsafe fn prepend_path(dir: &std::path::Path) {
363 let value = match std::env::var_os("PATH") {
364 Some(existing) => {
267 stdout().lock().write_all(password.as_bytes())?;365 let mut v = dir.as_os_str().to_owned();
366 v.push(":");
367 v.push(existing);
368 v
369 }
370 None => dir.as_os_str().to_owned(),
371 };
372 unsafe {
268 future::pending().await373 std::env::set_var("PATH", value);
269}374 }
375}
270376