gardesk/garcard / 517f24c

Browse files

process auth queue via polkit helper

Authored by mfwolffe <wolffemf@dukes.jmu.edu>
SHA
517f24c7914bfe6e8291c15306a73cd51a58a85a
Parents
81f4555
Tree
559dc1d

7 changed files

StatusFile+-
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);