1use std::collections::{HashMap, HashSet};2use std::ffi::{CStr, CString};3use std::future::pending;4use std::sync::LazyLock;56use anyhow::Context as _;7use clap::Parser;8use nix::unistd::{setuid, Uid, User};9use pam_client::{Context, ConversationHandler, ErrorCode, Flag};10use polkit_shared::BackendRequest;11use tokio::runtime::Handle;12use tokio::task::{block_in_place, spawn_blocking};13use tracing::trace;14use ui_prompt::dbus::DbusPrompterProxyBlocking;15use ui_prompt::{BlockingPrompter, Prompter};16use zbus::fdo;17use zbus::message::Header;18use zbus::zvariant::OwnedValue;19use zbus::{blocking, interface, proxy, Connection};2021struct Helper {22 connection: Connection,23 blocking_connection: blocking::Connection,24}2526static ALLOWED_ENVIRONMENT: LazyLock<HashSet<&str>> = LazyLock::new(|| {27 [28 29 "SSH_AUTH_SOCK",30 31 "SSH_AUTH_INFO_0",32 33 "SUDO_USER",34 ]35 .into_iter()36 .collect()37});3839struct Conversation<P>(P);40impl<P: BlockingPrompter> Conversation<P> {41 fn prompt_inner(&self, echo: bool, prompt: &CStr) -> Result<CString, ErrorCode> {42 trace!("do prompt");43 let out = self44 .045 .prompt_text(46 echo,47 &prompt.to_string_lossy(),48 "Polkit prompt request",49 &[],50 )51 .map_err(|e| {52 trace!("prompt error: {e}");53 ErrorCode::CONV_ERR54 })?;55 CString::new(out).map_err(|_| ErrorCode::CONV_AGAIN)56 }57 fn text_inner(&self, error: bool, msg: &CStr) {58 trace!("do text");59 let msg = msg.to_string_lossy();60 let _ = self.0.display_text(error, &msg, &[]);61 }62}63impl<P: BlockingPrompter> ConversationHandler for Conversation<P> {64 fn prompt_echo_on(&mut self, prompt: &CStr) -> Result<CString, ErrorCode> {65 self.prompt_inner(true, prompt)66 }6768 fn prompt_echo_off(&mut self, prompt: &CStr) -> Result<CString, ErrorCode> {69 self.prompt_inner(false, prompt)70 }7172 fn text_info(&mut self, msg: &CStr) {73 self.text_inner(false, msg)74 }7576 fn error_msg(&mut self, msg: &CStr) {77 self.text_inner(true, msg)78 }7980 fn radio_prompt(&mut self, prompt: &CStr) -> Result<bool, ErrorCode> {81 let prompt = prompt.to_string_lossy();82 let result = self83 .084 .prompt_radio(&prompt, "Polkit prompt request", &[])85 .map_err(|_| ErrorCode::CONV_ERR)?;86 Ok(result)87 }88}8990#[proxy(91 default_service = "org.freedesktop.DBus",92 default_path = "/org/freedesktop/DBus"93)]94trait DBus {95 fn get_connection_credentials(&self, body: &str) -> zbus::Result<HashMap<String, OwnedValue>>;96}9798#[interface(name = "lach.PolkitHelper")]99impl Helper {100 async fn init_conversation(101 &self,102 request: BackendRequest,103 #[zbus(header)] hdr: Header<'_>,104 ) -> fdo::Result<()> {105 let Some(sender) = hdr.sender().map(|v| v.to_owned()) else {106 trace!("missing sender");107 return Err(fdo::Error::AuthFailed("missing sender".to_owned()));108 };109110 let dbus = DBusProxy::new(&self.connection).await?;111112 113 114 let reply = dbus.get_connection_credentials(&sender).await?;115 let uid: u32 = (&reply["UnixUserID"]).try_into().unwrap();116117 let blocking_connection = self.blocking_connection.clone();118 let thread_result: fdo::Result<()> = block_in_place(move || {119 trace!("find user");120 let user = User::from_uid(Uid::from_raw(uid))121 .map_err(|_| fdo::Error::AuthFailed("error querying user".to_owned()))?122 .ok_or_else(|| fdo::Error::AuthFailed("uid not found".to_owned()))?;123124 let responder = DbusPrompterProxyBlocking::new(125 &blocking_connection,126 sender,127 request.prompter_path,128 )?;129 let conversation = Conversation(responder);130 trace!("run context for {}", &user.name);131 let mut ctx = Context::new(132 133 "login",134 Some(&user.name),135 conversation,136 )137 .map_err(|_| fdo::Error::Failed("pam context init failed".to_owned()))?;138139 trace!("fill env");140 for (k, v) in request.environment {141 if k.contains('=') || !ALLOWED_ENVIRONMENT.contains(k.as_str()) {142 continue;143 }144 let _ = ctx.putenv(format!("{k}={v}"));145 }146147 trace!("authenticate");148 ctx.authenticate(Flag::NONE)149 .map_err(|_| fdo::Error::AuthFailed("pam authentication failed".to_owned()))?;150151 trace!("acct mgmt");152 ctx.acct_mgmt(Flag::NONE)153 .map_err(|_| fdo::Error::AuthFailed("pam acct mgmt failed".to_owned()))?;154155 Ok(())156 });157158 thread_result?;159160 trace!("respond");161 let proxy = zbus_polkit::policykit1::AuthorityProxy::new(&self.connection).await?;162163 let identity_details = request164 .identity165 .details166 .iter()167 .map(|(k, v)| (k.as_str(), (**v).try_clone().expect("success")))168 .collect::<HashMap<_, _>>();169 proxy170 .authentication_agent_response2(171 uid,172 &request.cookie,173 &zbus_polkit::policykit1::Identity {174 identity_kind: &request.identity.kind,175 identity_details: &identity_details,176 },177 )178 .await?;179 Ok(())180 }181}182183const OBJ_PATH: &str = "/lach/PolkitHelper";184185#[derive(Parser)]186struct Opts {187 188 189 #[arg(long)]190 session: bool,191}192193#[tokio::main]194async fn main() -> anyhow::Result<()> {195 tracing_subscriber::fmt::init();196 let opts = Opts::parse();197 let connection = if opts.session {198 Connection::session().await199 } else {200 Connection::system().await201 }202 .context("failed to open connection")?;203204 let session = opts.session;205 let blocking_connection: anyhow::Result<blocking::Connection> = spawn_blocking(move || {206 Ok(if session {207 blocking::Connection::session()?208 } else {209 blocking::Connection::system()?210 })211 })212 .await?;213 let blocking_connection = blocking_connection.context("failed to open blocking connection")?;214215 if opts.session {216 setuid(Uid::from_raw(0))217 .context("polkit-backend needs to be suid if run in session mode")?;218 }219220 connection221 .object_server()222 .at(223 OBJ_PATH,224 Helper {225 connection: connection.clone(),226 blocking_connection,227 },228 )229 .await230 .context("failed listen path")?;231232 connection233 .request_name("lach.polkit.helper1")234 .await235 .context("failed to request name")?;236237 pending().await238}