process auth queue via polkit helper
Authored by
mfwolffe <wolffemf@dukes.jmu.edu>
- SHA
517f24c7914bfe6e8291c15306a73cd51a58a85a- Parents
-
81f4555 - Tree
559dc1d
517f24c
517f24c7914bfe6e8291c15306a73cd51a58a85a81f4555
559dc1d| Status | File | + | - |
|---|---|---|---|
| M |
README.md
|
5 | 0 |
| M |
garcard/src/agent.rs
|
245 | 6 |
| M |
garcard/src/daemon.rs
|
10 | 6 |
| M |
garcard/src/main.rs
|
1 | 0 |
| M |
garcard/src/polkit_helper.rs
|
8 | 2 |
| A |
garcard/src/prompt.rs
|
110 | 0 |
| M |
garcard/src/state.rs
|
20 | 30 |
README.mdmodified@@ -21,5 +21,10 @@ Environment overrides: | |||
| 21 | 4. `GARCARD_AGENT_BACKEND` | 21 | 4. `GARCARD_AGENT_BACKEND` |
| 22 | 5. `GARCARD_POLKIT_OBJECT_PATH` | 22 | 5. `GARCARD_POLKIT_OBJECT_PATH` |
| 23 | 6. `GARCARD_LOCALE` | 23 | 6. `GARCARD_LOCALE` |
| 24 | +7. `GARCARD_POLKIT_HELPER_SOCKET` | ||
| 25 | +8. `GARCARD_PROMPT_COMMAND` | ||
| 24 | 26 | ||
| 25 | See `examples/config.toml` for a starter file. | 27 | See `examples/config.toml` for a starter file. |
| 28 | + | ||
| 29 | +`GARCARD_PROMPT_COMMAND` is optional. If unset, `garcard` falls back to | ||
| 30 | +`systemd-ask-password` for prompt interaction. | ||
garcard/src/agent.rsmodified@@ -1,7 +1,12 @@ | |||
| 1 | +use crate::polkit_helper::{DEFAULT_HELPER_SOCKET, HelperOutcome, HelperSocketClient}; | ||
| 2 | +use crate::prompt::CommandPrompt; | ||
| 1 | use crate::state::{AuthPhase, AuthQueue, AuthState, QueueInsert}; | 3 | use crate::state::{AuthPhase, AuthQueue, AuthState, QueueInsert}; |
| 2 | use anyhow::{Context, Result}; | 4 | use anyhow::{Context, Result}; |
| 3 | use std::collections::HashMap; | 5 | use std::collections::HashMap; |
| 6 | +use std::path::PathBuf; | ||
| 7 | +use std::sync::atomic::{AtomicBool, Ordering}; | ||
| 4 | use std::sync::{Arc, Mutex}; | 8 | use std::sync::{Arc, Mutex}; |
| 9 | +use std::thread; | ||
| 5 | use zbus::blocking::{Connection, Proxy}; | 10 | use zbus::blocking::{Connection, Proxy}; |
| 6 | use zbus::fdo; | 11 | use zbus::fdo; |
| 7 | use zbus::zvariant::{OwnedObjectPath, OwnedValue}; | 12 | use zbus::zvariant::{OwnedObjectPath, OwnedValue}; |
@@ -53,21 +58,49 @@ struct AuthRequest { | |||
| 53 | identities: Vec<Subject>, | 58 | identities: Vec<Subject>, |
| 54 | } | 59 | } |
| 55 | 60 | ||
| 61 | +#[derive(Debug, Clone)] | ||
| 62 | +struct ActiveRequest { | ||
| 63 | + action_id: String, | ||
| 64 | + message: String, | ||
| 65 | + icon_name: String, | ||
| 66 | + detail_count: usize, | ||
| 67 | + cookie: String, | ||
| 68 | + username: String, | ||
| 69 | +} | ||
| 70 | + | ||
| 56 | #[derive(Debug)] | 71 | #[derive(Debug)] |
| 57 | struct PolkitRuntime { | 72 | struct PolkitRuntime { |
| 58 | auth_state: Arc<AuthState>, | 73 | auth_state: Arc<AuthState>, |
| 59 | queue: Mutex<AuthQueue<AuthRequest>>, | 74 | queue: Mutex<AuthQueue<AuthRequest>>, |
| 75 | + helper_client: HelperSocketClient, | ||
| 76 | + processing: AtomicBool, | ||
| 77 | + worker_enabled: bool, | ||
| 60 | } | 78 | } |
| 61 | 79 | ||
| 62 | impl PolkitRuntime { | 80 | impl PolkitRuntime { |
| 63 | fn new(auth_state: Arc<AuthState>) -> Self { | 81 | fn new(auth_state: Arc<AuthState>) -> Self { |
| 82 | + Self::new_with_worker(auth_state, true) | ||
| 83 | + } | ||
| 84 | + | ||
| 85 | + #[cfg(test)] | ||
| 86 | + fn new_without_worker(auth_state: Arc<AuthState>) -> Self { | ||
| 87 | + Self::new_with_worker(auth_state, false) | ||
| 88 | + } | ||
| 89 | + | ||
| 90 | + fn new_with_worker(auth_state: Arc<AuthState>, worker_enabled: bool) -> Self { | ||
| 91 | + let helper_socket = std::env::var_os("GARCARD_POLKIT_HELPER_SOCKET") | ||
| 92 | + .map(PathBuf::from) | ||
| 93 | + .unwrap_or_else(|| PathBuf::from(DEFAULT_HELPER_SOCKET)); | ||
| 64 | Self { | 94 | Self { |
| 65 | auth_state, | 95 | auth_state, |
| 66 | queue: Mutex::new(AuthQueue::default()), | 96 | queue: Mutex::new(AuthQueue::default()), |
| 97 | + helper_client: HelperSocketClient::new(helper_socket), | ||
| 98 | + processing: AtomicBool::new(false), | ||
| 99 | + worker_enabled, | ||
| 67 | } | 100 | } |
| 68 | } | 101 | } |
| 69 | 102 | ||
| 70 | - fn begin_authentication(&self, request: AuthRequest) -> Result<QueueInsert> { | 103 | + fn begin_authentication(self: &Arc<Self>, request: AuthRequest) -> Result<QueueInsert> { |
| 71 | let (insert, active, queued) = { | 104 | let (insert, active, queued) = { |
| 72 | let mut queue = self | 105 | let mut queue = self |
| 73 | .queue | 106 | .queue |
@@ -90,10 +123,14 @@ impl PolkitRuntime { | |||
| 90 | self.auth_state.set_phase(AuthPhase::PendingPrompt); | 123 | self.auth_state.set_phase(AuthPhase::PendingPrompt); |
| 91 | } | 124 | } |
| 92 | 125 | ||
| 126 | + if self.worker_enabled && active > 0 { | ||
| 127 | + self.ensure_worker(); | ||
| 128 | + } | ||
| 129 | + | ||
| 93 | Ok(insert) | 130 | Ok(insert) |
| 94 | } | 131 | } |
| 95 | 132 | ||
| 96 | - fn cancel_authentication(&self, cookie: &str) -> Result<bool> { | 133 | + fn cancel_authentication(self: &Arc<Self>, cookie: &str) -> Result<bool> { |
| 97 | let (canceled_active, removed_queued, active, queued) = { | 134 | let (canceled_active, removed_queued, active, queued) = { |
| 98 | let mut queue = self | 135 | let mut queue = self |
| 99 | .queue | 136 | .queue |
@@ -118,6 +155,9 @@ impl PolkitRuntime { | |||
| 118 | } else { | 155 | } else { |
| 119 | self.auth_state.set_phase(AuthPhase::Canceled); | 156 | self.auth_state.set_phase(AuthPhase::Canceled); |
| 120 | } | 157 | } |
| 158 | + if self.worker_enabled && active > 0 { | ||
| 159 | + self.ensure_worker(); | ||
| 160 | + } | ||
| 121 | return Ok(true); | 161 | return Ok(true); |
| 122 | } | 162 | } |
| 123 | 163 | ||
@@ -131,15 +171,190 @@ impl PolkitRuntime { | |||
| 131 | Ok(false) | 171 | Ok(false) |
| 132 | } | 172 | } |
| 133 | 173 | ||
| 174 | + fn ensure_worker(self: &Arc<Self>) { | ||
| 175 | + if self.processing.swap(true, Ordering::AcqRel) { | ||
| 176 | + return; | ||
| 177 | + } | ||
| 178 | + | ||
| 179 | + let runtime = Arc::clone(self); | ||
| 180 | + let spawn_result = thread::Builder::new() | ||
| 181 | + .name("garcard-auth-worker".to_string()) | ||
| 182 | + .spawn(move || runtime.process_loop()); | ||
| 183 | + if let Err(err) = spawn_result { | ||
| 184 | + self.processing.store(false, Ordering::Release); | ||
| 185 | + tracing::error!(error = %err, "Failed to spawn garcard auth worker"); | ||
| 186 | + } | ||
| 187 | + } | ||
| 188 | + | ||
| 189 | + fn process_loop(self: Arc<Self>) { | ||
| 190 | + loop { | ||
| 191 | + let Some(active) = self.active_request_snapshot() else { | ||
| 192 | + break; | ||
| 193 | + }; | ||
| 194 | + | ||
| 195 | + self.auth_state.set_phase(AuthPhase::Verifying); | ||
| 196 | + tracing::info!( | ||
| 197 | + action_id = %active.action_id, | ||
| 198 | + icon_name = %active.icon_name, | ||
| 199 | + detail_count = active.detail_count, | ||
| 200 | + "Processing polkit auth request" | ||
| 201 | + ); | ||
| 202 | + | ||
| 203 | + let outcome = self.authenticate_active_request(&active); | ||
| 204 | + self.complete_request(&active.cookie, outcome); | ||
| 205 | + } | ||
| 206 | + | ||
| 207 | + self.processing.store(false, Ordering::Release); | ||
| 208 | + if self.has_active_request() { | ||
| 209 | + self.ensure_worker(); | ||
| 210 | + } | ||
| 211 | + } | ||
| 212 | + | ||
| 213 | + fn has_active_request(&self) -> bool { | ||
| 214 | + self.queue | ||
| 215 | + .lock() | ||
| 216 | + .map(|queue| queue.active().is_some()) | ||
| 217 | + .unwrap_or(false) | ||
| 218 | + } | ||
| 219 | + | ||
| 220 | + fn active_request_snapshot(&self) -> Option<ActiveRequest> { | ||
| 221 | + let queue = self.queue.lock().ok()?; | ||
| 222 | + let request = queue.active()?; | ||
| 223 | + | ||
| 224 | + let username = resolve_identity_username(&request.identities) | ||
| 225 | + .or_else(current_username) | ||
| 226 | + .or_else(|| std::env::var("USER").ok()) | ||
| 227 | + .unwrap_or_else(|| "unknown".to_string()); | ||
| 228 | + | ||
| 229 | + Some(ActiveRequest { | ||
| 230 | + action_id: request.action_id.clone(), | ||
| 231 | + message: request.message.clone(), | ||
| 232 | + icon_name: request.icon_name.clone(), | ||
| 233 | + detail_count: request.details.len(), | ||
| 234 | + cookie: request.cookie.clone(), | ||
| 235 | + username, | ||
| 236 | + }) | ||
| 237 | + } | ||
| 238 | + | ||
| 239 | + fn authenticate_active_request(&self, request: &ActiveRequest) -> HelperOutcome { | ||
| 240 | + let mut prompts = CommandPrompt::default(); | ||
| 241 | + let prompt_context = if request.message.is_empty() { | ||
| 242 | + request.action_id.as_str() | ||
| 243 | + } else { | ||
| 244 | + request.message.as_str() | ||
| 245 | + }; | ||
| 246 | + tracing::info!(context = %prompt_context, "Starting helper authentication dialog"); | ||
| 247 | + | ||
| 248 | + match self | ||
| 249 | + .helper_client | ||
| 250 | + .authenticate(&request.username, &request.cookie, &mut prompts) | ||
| 251 | + { | ||
| 252 | + Ok(outcome) => outcome, | ||
| 253 | + Err(err) => { | ||
| 254 | + tracing::warn!( | ||
| 255 | + action_id = %request.action_id, | ||
| 256 | + error = %err, | ||
| 257 | + "Polkit helper authentication failed" | ||
| 258 | + ); | ||
| 259 | + HelperOutcome::Denied | ||
| 260 | + } | ||
| 261 | + } | ||
| 262 | + } | ||
| 263 | + | ||
| 264 | + fn complete_request(&self, cookie: &str, outcome: HelperOutcome) { | ||
| 265 | + let (removed, active, queued) = { | ||
| 266 | + let mut queue = match self.queue.lock() { | ||
| 267 | + Ok(queue) => queue, | ||
| 268 | + Err(_) => { | ||
| 269 | + tracing::error!("auth request queue lock poisoned"); | ||
| 270 | + return; | ||
| 271 | + } | ||
| 272 | + }; | ||
| 273 | + let removed = queue | ||
| 274 | + .complete_active_if(|request| request.cookie == cookie) | ||
| 275 | + .is_some(); | ||
| 276 | + let (active, queued) = queue.counts(); | ||
| 277 | + (removed, active, queued) | ||
| 278 | + }; | ||
| 279 | + | ||
| 280 | + self.auth_state.sync_queue_counts(active, queued); | ||
| 281 | + if !removed { | ||
| 282 | + if active == 0 && queued == 0 && self.auth_state.phase() == AuthPhase::Verifying { | ||
| 283 | + self.auth_state.set_phase(AuthPhase::Idle); | ||
| 284 | + } | ||
| 285 | + return; | ||
| 286 | + } | ||
| 287 | + | ||
| 288 | + if active > 0 { | ||
| 289 | + self.auth_state.set_phase(AuthPhase::PendingPrompt); | ||
| 290 | + return; | ||
| 291 | + } | ||
| 292 | + | ||
| 293 | + let phase = match outcome { | ||
| 294 | + HelperOutcome::Authorized => AuthPhase::Success, | ||
| 295 | + HelperOutcome::Denied => AuthPhase::Failure, | ||
| 296 | + HelperOutcome::Canceled => AuthPhase::Canceled, | ||
| 297 | + }; | ||
| 298 | + self.auth_state.set_phase(phase); | ||
| 299 | + } | ||
| 300 | + | ||
| 134 | fn reset(&self) { | 301 | fn reset(&self) { |
| 135 | if let Ok(mut queue) = self.queue.lock() { | 302 | if let Ok(mut queue) = self.queue.lock() { |
| 136 | queue.clear(); | 303 | queue.clear(); |
| 137 | } | 304 | } |
| 305 | + self.processing.store(false, Ordering::Release); | ||
| 138 | self.auth_state.set_phase(AuthPhase::Idle); | 306 | self.auth_state.set_phase(AuthPhase::Idle); |
| 139 | self.auth_state.sync_queue_counts(0, 0); | 307 | self.auth_state.sync_queue_counts(0, 0); |
| 140 | } | 308 | } |
| 141 | } | 309 | } |
| 142 | 310 | ||
| 311 | +fn resolve_identity_username(identities: &[Subject]) -> Option<String> { | ||
| 312 | + for (kind, details) in identities { | ||
| 313 | + if kind != "unix-user" { | ||
| 314 | + continue; | ||
| 315 | + } | ||
| 316 | + | ||
| 317 | + if let Some(value) = details.get("name") { | ||
| 318 | + if let Ok(name) = <&str>::try_from(value) { | ||
| 319 | + return Some(name.to_string()); | ||
| 320 | + } | ||
| 321 | + } | ||
| 322 | + | ||
| 323 | + if let Some(uid) = details.get("uid").and_then(parse_uid) { | ||
| 324 | + if let Some(name) = username_for_uid(uid) { | ||
| 325 | + return Some(name); | ||
| 326 | + } | ||
| 327 | + } | ||
| 328 | + } | ||
| 329 | + | ||
| 330 | + None | ||
| 331 | +} | ||
| 332 | + | ||
| 333 | +fn parse_uid(value: &OwnedValue) -> Option<u32> { | ||
| 334 | + if let Ok(uid) = u32::try_from(value) { | ||
| 335 | + return Some(uid); | ||
| 336 | + } | ||
| 337 | + if let Ok(uid) = u64::try_from(value) { | ||
| 338 | + return u32::try_from(uid).ok(); | ||
| 339 | + } | ||
| 340 | + if let Ok(uid) = i32::try_from(value) { | ||
| 341 | + return u32::try_from(uid).ok(); | ||
| 342 | + } | ||
| 343 | + | ||
| 344 | + None | ||
| 345 | +} | ||
| 346 | + | ||
| 347 | +fn username_for_uid(uid: u32) -> Option<String> { | ||
| 348 | + nix::unistd::User::from_uid(nix::unistd::Uid::from_raw(uid)) | ||
| 349 | + .ok() | ||
| 350 | + .flatten() | ||
| 351 | + .map(|user| user.name) | ||
| 352 | +} | ||
| 353 | + | ||
| 354 | +fn current_username() -> Option<String> { | ||
| 355 | + username_for_uid(nix::unistd::geteuid().as_raw()) | ||
| 356 | +} | ||
| 357 | + | ||
| 143 | #[derive(Debug, Clone)] | 358 | #[derive(Debug, Clone)] |
| 144 | struct PolkitAuthAgentObject { | 359 | struct PolkitAuthAgentObject { |
| 145 | runtime: Arc<PolkitRuntime>, | 360 | runtime: Arc<PolkitRuntime>, |
@@ -162,6 +377,8 @@ impl PolkitAuthAgentObject { | |||
| 162 | cookie: &str, | 377 | cookie: &str, |
| 163 | identities: Vec<Subject>, | 378 | identities: Vec<Subject>, |
| 164 | ) -> fdo::Result<()> { | 379 | ) -> fdo::Result<()> { |
| 380 | + let detail_count = details.len(); | ||
| 381 | + let identity_count = identities.len(); | ||
| 165 | let request = AuthRequest { | 382 | let request = AuthRequest { |
| 166 | action_id: action_id.to_string(), | 383 | action_id: action_id.to_string(), |
| 167 | message: message.to_string(), | 384 | message: message.to_string(), |
@@ -178,11 +395,20 @@ impl PolkitAuthAgentObject { | |||
| 178 | 395 | ||
| 179 | match queue_insert { | 396 | match queue_insert { |
| 180 | QueueInsert::Activated => { | 397 | QueueInsert::Activated => { |
| 181 | - tracing::info!(action_id = %action_id, "Started active polkit auth request"); | 398 | + tracing::info!( |
| 399 | + action_id = %action_id, | ||
| 400 | + icon_name = %icon_name, | ||
| 401 | + detail_count, | ||
| 402 | + identity_count, | ||
| 403 | + "Started active polkit auth request" | ||
| 404 | + ); | ||
| 182 | } | 405 | } |
| 183 | QueueInsert::Queued { position } => { | 406 | QueueInsert::Queued { position } => { |
| 184 | tracing::info!( | 407 | tracing::info!( |
| 185 | action_id = %action_id, | 408 | action_id = %action_id, |
| 409 | + icon_name = %icon_name, | ||
| 410 | + detail_count, | ||
| 411 | + identity_count, | ||
| 186 | queue_position = position, | 412 | queue_position = position, |
| 187 | "Queued polkit auth request" | 413 | "Queued polkit auth request" |
| 188 | ); | 414 | ); |
@@ -404,7 +630,7 @@ mod tests { | |||
| 404 | #[test] | 630 | #[test] |
| 405 | fn runtime_begin_authentication_updates_state_and_counts() { | 631 | fn runtime_begin_authentication_updates_state_and_counts() { |
| 406 | let auth_state = Arc::new(AuthState::default()); | 632 | let auth_state = Arc::new(AuthState::default()); |
| 407 | - let runtime = PolkitRuntime::new(Arc::clone(&auth_state)); | 633 | + let runtime = Arc::new(PolkitRuntime::new_without_worker(Arc::clone(&auth_state))); |
| 408 | 634 | ||
| 409 | let insert = runtime | 635 | let insert = runtime |
| 410 | .begin_authentication(fake_request("cookie-1")) | 636 | .begin_authentication(fake_request("cookie-1")) |
@@ -418,7 +644,7 @@ mod tests { | |||
| 418 | #[test] | 644 | #[test] |
| 419 | fn runtime_cancel_authentication_drops_active_request() { | 645 | fn runtime_cancel_authentication_drops_active_request() { |
| 420 | let auth_state = Arc::new(AuthState::default()); | 646 | let auth_state = Arc::new(AuthState::default()); |
| 421 | - let runtime = PolkitRuntime::new(Arc::clone(&auth_state)); | 647 | + let runtime = Arc::new(PolkitRuntime::new_without_worker(Arc::clone(&auth_state))); |
| 422 | runtime | 648 | runtime |
| 423 | .begin_authentication(fake_request("cookie-1")) | 649 | .begin_authentication(fake_request("cookie-1")) |
| 424 | .expect("begin"); | 650 | .expect("begin"); |
@@ -433,7 +659,7 @@ mod tests { | |||
| 433 | #[test] | 659 | #[test] |
| 434 | fn runtime_cancel_authentication_removes_queued_request() { | 660 | fn runtime_cancel_authentication_removes_queued_request() { |
| 435 | let auth_state = Arc::new(AuthState::default()); | 661 | let auth_state = Arc::new(AuthState::default()); |
| 436 | - let runtime = PolkitRuntime::new(Arc::clone(&auth_state)); | 662 | + let runtime = Arc::new(PolkitRuntime::new_without_worker(Arc::clone(&auth_state))); |
| 437 | runtime | 663 | runtime |
| 438 | .begin_authentication(fake_request("cookie-1")) | 664 | .begin_authentication(fake_request("cookie-1")) |
| 439 | .expect("begin first"); | 665 | .expect("begin first"); |
@@ -446,4 +672,17 @@ mod tests { | |||
| 446 | assert_eq!(auth_state.summary().active_requests, 1); | 672 | assert_eq!(auth_state.summary().active_requests, 1); |
| 447 | assert_eq!(auth_state.summary().queued_requests, 0); | 673 | assert_eq!(auth_state.summary().queued_requests, 0); |
| 448 | } | 674 | } |
| 675 | + | ||
| 676 | + #[test] | ||
| 677 | + fn resolve_identity_username_uses_uid_detail() { | ||
| 678 | + let mut details = HashMap::new(); | ||
| 679 | + details.insert( | ||
| 680 | + "uid".to_string(), | ||
| 681 | + OwnedValue::from(nix::unistd::geteuid().as_raw()), | ||
| 682 | + ); | ||
| 683 | + let identities = vec![("unix-user".to_string(), details)]; | ||
| 684 | + | ||
| 685 | + let resolved = resolve_identity_username(&identities); | ||
| 686 | + assert_eq!(resolved, current_username()); | ||
| 687 | + } | ||
| 449 | } | 688 | } |
garcard/src/daemon.rsmodified@@ -122,21 +122,25 @@ fn init_backend(config: &Config, auth_state: Arc<AuthState>) -> Result<Box<dyn A | |||
| 122 | Ok(backend) | 122 | Ok(backend) |
| 123 | } | 123 | } |
| 124 | AgentBackendMode::Polkit => { | 124 | AgentBackendMode::Polkit => { |
| 125 | - let mut backend: Box<dyn AuthAgentBackend> = | 125 | + let mut backend: Box<dyn AuthAgentBackend> = Box::new(PolkitAgent::new( |
| 126 | - Box::new(PolkitAgent::new(PolkitBackendConfig { | 126 | + PolkitBackendConfig { |
| 127 | object_path: config.polkit_object_path.clone(), | 127 | object_path: config.polkit_object_path.clone(), |
| 128 | locale: config.locale.clone(), | 128 | locale: config.locale.clone(), |
| 129 | - }, Arc::clone(&auth_state))?); | 129 | + }, |
| 130 | + Arc::clone(&auth_state), | ||
| 131 | + )?); | ||
| 130 | backend.register()?; | 132 | backend.register()?; |
| 131 | Ok(backend) | 133 | Ok(backend) |
| 132 | } | 134 | } |
| 133 | AgentBackendMode::Auto => { | 135 | AgentBackendMode::Auto => { |
| 134 | let attempt = (|| -> Result<Box<dyn AuthAgentBackend>> { | 136 | let attempt = (|| -> Result<Box<dyn AuthAgentBackend>> { |
| 135 | - let mut backend: Box<dyn AuthAgentBackend> = | 137 | + let mut backend: Box<dyn AuthAgentBackend> = Box::new(PolkitAgent::new( |
| 136 | - Box::new(PolkitAgent::new(PolkitBackendConfig { | 138 | + PolkitBackendConfig { |
| 137 | object_path: config.polkit_object_path.clone(), | 139 | object_path: config.polkit_object_path.clone(), |
| 138 | locale: config.locale.clone(), | 140 | locale: config.locale.clone(), |
| 139 | - }, Arc::clone(&auth_state))?); | 141 | + }, |
| 142 | + Arc::clone(&auth_state), | ||
| 143 | + )?); | ||
| 140 | backend.register()?; | 144 | backend.register()?; |
| 141 | Ok(backend) | 145 | Ok(backend) |
| 142 | })(); | 146 | })(); |
garcard/src/main.rsmodified@@ -2,6 +2,7 @@ mod agent; | |||
| 2 | mod config; | 2 | mod config; |
| 3 | mod daemon; | 3 | mod daemon; |
| 4 | mod polkit_helper; | 4 | mod polkit_helper; |
| 5 | +mod prompt; | ||
| 5 | mod state; | 6 | mod state; |
| 6 | 7 | ||
| 7 | use anyhow::Result; | 8 | use anyhow::Result; |
garcard/src/polkit_helper.rsmodified@@ -86,14 +86,20 @@ impl HelperSocketClient { | |||
| 86 | 86 | ||
| 87 | match parse_helper_line(&line)? { | 87 | match parse_helper_line(&line)? { |
| 88 | HelperEvent::PromptHidden(prompt) => { | 88 | HelperEvent::PromptHidden(prompt) => { |
| 89 | - match prompts.prompt_secret(&prompt).context("prompt handler failed")? { | 89 | + match prompts |
| 90 | + .prompt_secret(&prompt) | ||
| 91 | + .context("prompt handler failed")? | ||
| 92 | + { | ||
| 90 | Some(response) => write_line(&mut stream, &sanitize_response(&response)) | 93 | Some(response) => write_line(&mut stream, &sanitize_response(&response)) |
| 91 | .context("failed to send helper secret response")?, | 94 | .context("failed to send helper secret response")?, |
| 92 | None => return Ok(HelperOutcome::Canceled), | 95 | None => return Ok(HelperOutcome::Canceled), |
| 93 | } | 96 | } |
| 94 | } | 97 | } |
| 95 | HelperEvent::PromptVisible(prompt) => { | 98 | HelperEvent::PromptVisible(prompt) => { |
| 96 | - match prompts.prompt_plain(&prompt).context("prompt handler failed")? { | 99 | + match prompts |
| 100 | + .prompt_plain(&prompt) | ||
| 101 | + .context("prompt handler failed")? | ||
| 102 | + { | ||
| 97 | Some(response) => write_line(&mut stream, &sanitize_response(&response)) | 103 | Some(response) => write_line(&mut stream, &sanitize_response(&response)) |
| 98 | .context("failed to send helper visible response")?, | 104 | .context("failed to send helper visible response")?, |
| 99 | None => return Ok(HelperOutcome::Canceled), | 105 | None => return Ok(HelperOutcome::Canceled), |
garcard/src/prompt.rsadded@@ -0,0 +1,110 @@ | |||
| 1 | +use crate::polkit_helper::PromptProvider; | ||
| 2 | +use anyhow::{Context, Result}; | ||
| 3 | +use std::process::Command; | ||
| 4 | + | ||
| 5 | +const DEFAULT_ASK_TIMEOUT_SECS: u64 = 120; | ||
| 6 | + | ||
| 7 | +#[derive(Debug, Clone)] | ||
| 8 | +pub struct CommandPrompt { | ||
| 9 | + prompt_command: Option<String>, | ||
| 10 | +} | ||
| 11 | + | ||
| 12 | +impl Default for CommandPrompt { | ||
| 13 | + fn default() -> Self { | ||
| 14 | + Self { | ||
| 15 | + prompt_command: std::env::var("GARCARD_PROMPT_COMMAND").ok(), | ||
| 16 | + } | ||
| 17 | + } | ||
| 18 | +} | ||
| 19 | + | ||
| 20 | +impl CommandPrompt { | ||
| 21 | + fn run_prompt(&self, prompt: &str, visible: bool) -> Result<Option<String>> { | ||
| 22 | + if let Some(command) = self.prompt_command.as_deref() { | ||
| 23 | + return run_custom_prompt_command(command, prompt, visible); | ||
| 24 | + } | ||
| 25 | + | ||
| 26 | + run_systemd_ask_password(prompt, visible) | ||
| 27 | + } | ||
| 28 | +} | ||
| 29 | + | ||
| 30 | +impl PromptProvider for CommandPrompt { | ||
| 31 | + fn prompt_secret(&mut self, prompt: &str) -> Result<Option<String>> { | ||
| 32 | + self.run_prompt(prompt, false) | ||
| 33 | + } | ||
| 34 | + | ||
| 35 | + fn prompt_plain(&mut self, prompt: &str) -> Result<Option<String>> { | ||
| 36 | + self.run_prompt(prompt, true) | ||
| 37 | + } | ||
| 38 | + | ||
| 39 | + fn show_error(&mut self, message: &str) -> Result<()> { | ||
| 40 | + tracing::warn!("polkit helper message: {}", message); | ||
| 41 | + Ok(()) | ||
| 42 | + } | ||
| 43 | + | ||
| 44 | + fn show_info(&mut self, message: &str) -> Result<()> { | ||
| 45 | + tracing::info!("polkit helper message: {}", message); | ||
| 46 | + Ok(()) | ||
| 47 | + } | ||
| 48 | +} | ||
| 49 | + | ||
| 50 | +fn run_custom_prompt_command(command: &str, prompt: &str, visible: bool) -> Result<Option<String>> { | ||
| 51 | + let mode = if visible { "plain" } else { "secret" }; | ||
| 52 | + let output = Command::new("sh") | ||
| 53 | + .arg("-c") | ||
| 54 | + .arg(command) | ||
| 55 | + .env("GARCARD_PROMPT", prompt) | ||
| 56 | + .env("GARCARD_PROMPT_MODE", mode) | ||
| 57 | + .output() | ||
| 58 | + .with_context(|| format!("failed to run custom prompt command: {}", command))?; | ||
| 59 | + | ||
| 60 | + if !output.status.success() { | ||
| 61 | + return Ok(None); | ||
| 62 | + } | ||
| 63 | + | ||
| 64 | + Ok(extract_response(&output.stdout)) | ||
| 65 | +} | ||
| 66 | + | ||
| 67 | +fn run_systemd_ask_password(prompt: &str, visible: bool) -> Result<Option<String>> { | ||
| 68 | + let mut command = Command::new("systemd-ask-password"); | ||
| 69 | + command.arg("--user"); | ||
| 70 | + command.arg("--no-tty"); | ||
| 71 | + command.arg(format!("--timeout={}", DEFAULT_ASK_TIMEOUT_SECS)); | ||
| 72 | + if visible { | ||
| 73 | + command.arg("--echo=yes"); | ||
| 74 | + } | ||
| 75 | + command.arg(prompt); | ||
| 76 | + | ||
| 77 | + let output = command | ||
| 78 | + .output() | ||
| 79 | + .context("failed to run systemd-ask-password")?; | ||
| 80 | + if !output.status.success() { | ||
| 81 | + return Ok(None); | ||
| 82 | + } | ||
| 83 | + | ||
| 84 | + Ok(extract_response(&output.stdout)) | ||
| 85 | +} | ||
| 86 | + | ||
| 87 | +fn extract_response(stdout: &[u8]) -> Option<String> { | ||
| 88 | + let response = String::from_utf8_lossy(stdout).trim().to_string(); | ||
| 89 | + if response.is_empty() { | ||
| 90 | + None | ||
| 91 | + } else { | ||
| 92 | + Some(response) | ||
| 93 | + } | ||
| 94 | +} | ||
| 95 | + | ||
| 96 | +#[cfg(test)] | ||
| 97 | +mod tests { | ||
| 98 | + use super::*; | ||
| 99 | + | ||
| 100 | + #[test] | ||
| 101 | + fn extract_response_trims_whitespace() { | ||
| 102 | + let response = extract_response(b" hello world \n"); | ||
| 103 | + assert_eq!(response.as_deref(), Some("hello world")); | ||
| 104 | + } | ||
| 105 | + | ||
| 106 | + #[test] | ||
| 107 | + fn extract_response_rejects_empty_value() { | ||
| 108 | + assert_eq!(extract_response(b" \n\t"), None); | ||
| 109 | + } | ||
| 110 | +} | ||
garcard/src/state.rsmodified@@ -7,6 +7,7 @@ use std::sync::atomic::{AtomicUsize, Ordering}; | |||
| 7 | use std::time::Instant; | 7 | use std::time::Instant; |
| 8 | 8 | ||
| 9 | /// Typed phases for auth flow transitions. | 9 | /// Typed phases for auth flow transitions. |
| 10 | +#[allow(dead_code)] | ||
| 10 | #[derive(Debug, Clone, Copy, PartialEq, Eq)] | 11 | #[derive(Debug, Clone, Copy, PartialEq, Eq)] |
| 11 | pub enum AuthPhase { | 12 | pub enum AuthPhase { |
| 12 | Idle, | 13 | Idle, |
@@ -30,21 +31,6 @@ impl AuthPhase { | |||
| 30 | Self::Timeout => "timeout", | 31 | Self::Timeout => "timeout", |
| 31 | } | 32 | } |
| 32 | } | 33 | } |
| 33 | - | ||
| 34 | - pub fn from_label(raw: &str) -> Option<Self> { | ||
| 35 | - match raw.trim().to_ascii_lowercase().as_str() { | ||
| 36 | - "idle" => Some(Self::Idle), | ||
| 37 | - "pending" | "pending_prompt" | "pending-prompt" | "pending prompt" => { | ||
| 38 | - Some(Self::PendingPrompt) | ||
| 39 | - } | ||
| 40 | - "verifying" => Some(Self::Verifying), | ||
| 41 | - "success" => Some(Self::Success), | ||
| 42 | - "failure" | "failed" => Some(Self::Failure), | ||
| 43 | - "canceled" | "cancelled" => Some(Self::Canceled), | ||
| 44 | - "timeout" | "timed_out" => Some(Self::Timeout), | ||
| 45 | - _ => None, | ||
| 46 | - } | ||
| 47 | - } | ||
| 48 | } | 34 | } |
| 49 | 35 | ||
| 50 | impl fmt::Display for AuthPhase { | 36 | impl fmt::Display for AuthPhase { |
@@ -94,7 +80,10 @@ impl Default for AuthState { | |||
| 94 | 80 | ||
| 95 | impl AuthState { | 81 | impl AuthState { |
| 96 | pub fn phase(&self) -> AuthPhase { | 82 | pub fn phase(&self) -> AuthPhase { |
| 97 | - self.current_phase.read().map(|phase| *phase).unwrap_or(AuthPhase::Idle) | 83 | + self.current_phase |
| 84 | + .read() | ||
| 85 | + .map(|phase| *phase) | ||
| 86 | + .unwrap_or(AuthPhase::Idle) | ||
| 98 | } | 87 | } |
| 99 | 88 | ||
| 100 | pub fn set_phase(&self, next: AuthPhase) { | 89 | pub fn set_phase(&self, next: AuthPhase) { |
@@ -115,12 +104,6 @@ impl AuthState { | |||
| 115 | false | 104 | false |
| 116 | } | 105 | } |
| 117 | 106 | ||
| 118 | - pub fn set_state(&self, next: impl AsRef<str>) { | ||
| 119 | - if let Some(phase) = AuthPhase::from_label(next.as_ref()) { | ||
| 120 | - self.set_phase(phase); | ||
| 121 | - } | ||
| 122 | - } | ||
| 123 | - | ||
| 124 | pub fn set_active_requests(&self, count: usize) { | 107 | pub fn set_active_requests(&self, count: usize) { |
| 125 | self.active_requests.store(count, Ordering::Relaxed); | 108 | self.active_requests.store(count, Ordering::Relaxed); |
| 126 | } | 109 | } |
@@ -182,11 +165,18 @@ impl<T> AuthQueue<T> { | |||
| 182 | self.active.as_ref() | 165 | self.active.as_ref() |
| 183 | } | 166 | } |
| 184 | 167 | ||
| 185 | - pub fn active_mut(&mut self) -> Option<&mut T> { | 168 | + pub fn take_active_if<F>(&mut self, mut predicate: F) -> Option<T> |
| 186 | - self.active.as_mut() | 169 | + where |
| 170 | + F: FnMut(&T) -> bool, | ||
| 171 | + { | ||
| 172 | + if self.active.as_ref().is_some_and(&mut predicate) { | ||
| 173 | + return self.complete_active(); | ||
| 174 | + } | ||
| 175 | + | ||
| 176 | + None | ||
| 187 | } | 177 | } |
| 188 | 178 | ||
| 189 | - pub fn take_active_if<F>(&mut self, mut predicate: F) -> Option<T> | 179 | + pub fn complete_active_if<F>(&mut self, mut predicate: F) -> Option<T> |
| 190 | where | 180 | where |
| 191 | F: FnMut(&T) -> bool, | 181 | F: FnMut(&T) -> bool, |
| 192 | { | 182 | { |
@@ -215,10 +205,6 @@ impl<T> AuthQueue<T> { | |||
| 215 | finished | 205 | finished |
| 216 | } | 206 | } |
| 217 | 207 | ||
| 218 | - pub fn cancel_active(&mut self) -> Option<T> { | ||
| 219 | - self.complete_active() | ||
| 220 | - } | ||
| 221 | - | ||
| 222 | pub fn active_len(&self) -> usize { | 208 | pub fn active_len(&self) -> usize { |
| 223 | usize::from(self.active.is_some()) | 209 | usize::from(self.active.is_some()) |
| 224 | } | 210 | } |
@@ -262,7 +248,11 @@ impl RuntimeState { | |||
| 262 | Self::with_auth(socket_path, backend_name, Arc::new(AuthState::default())) | 248 | Self::with_auth(socket_path, backend_name, Arc::new(AuthState::default())) |
| 263 | } | 249 | } |
| 264 | 250 | ||
| 265 | - pub fn with_auth(socket_path: String, backend_name: &'static str, auth: Arc<AuthState>) -> Self { | 251 | + pub fn with_auth( |
| 252 | + socket_path: String, | ||
| 253 | + backend_name: &'static str, | ||
| 254 | + auth: Arc<AuthState>, | ||
| 255 | + ) -> Self { | ||
| 266 | auth.set_phase(AuthPhase::Idle); | 256 | auth.set_phase(AuthPhase::Idle); |
| 267 | auth.set_active_requests(0); | 257 | auth.set_active_requests(0); |
| 268 | auth.set_queued_requests(0); | 258 | auth.set_queued_requests(0); |