gardesk/garcard / bb1d2bc

Browse files

Consume callback details and honor cancels

Authored by mfwolffe <wolffemf@dukes.jmu.edu>
SHA
bb1d2bc35c1fda6cba0583db9807f7452ec44196
Parents
5d9670c
Tree
75d0a52

1 changed file

StatusFile+-
M garcard/src/agent.rs 217 10
garcard/src/agent.rsmodified
@@ -4,7 +4,7 @@ use crate::polkit_helper::{
44
 use crate::prompt::CommandPrompt;
55
 use crate::state::{AuthPhase, AuthQueue, AuthState, QueueInsert};
66
 use anyhow::{Context, Result};
7
-use std::collections::HashMap;
7
+use std::collections::{HashMap, HashSet};
88
 use std::path::PathBuf;
99
 use std::sync::atomic::{AtomicBool, Ordering};
1010
 use std::sync::{Arc, Condvar, Mutex};
@@ -70,6 +70,7 @@ struct ActiveRequest {
7070
     message: String,
7171
     icon_name: String,
7272
     detail_count: usize,
73
+    details: Details,
7374
     cookie: String,
7475
     username: String,
7576
 }
@@ -79,6 +80,7 @@ struct PolkitRuntime {
7980
     auth_state: Arc<AuthState>,
8081
     queue: Mutex<AuthQueue<AuthRequest>>,
8182
     outcomes: Mutex<HashMap<String, HelperOutcome>>,
83
+    canceled_cookies: Mutex<HashSet<String>>,
8284
     outcome_signal: Condvar,
8385
     helper_client: HelperSocketClient,
8486
     processing: AtomicBool,
@@ -95,6 +97,70 @@ impl RetryPromptProvider for CommandPrompt {
9597
     }
9698
 }
9799
 
100
+struct CancellationAwarePrompt<'a, P> {
101
+    inner: &'a mut P,
102
+    runtime: &'a PolkitRuntime,
103
+    cookie: &'a str,
104
+}
105
+
106
+impl<'a, P> CancellationAwarePrompt<'a, P> {
107
+    fn new(inner: &'a mut P, runtime: &'a PolkitRuntime, cookie: &'a str) -> Self {
108
+        Self {
109
+            inner,
110
+            runtime,
111
+            cookie,
112
+        }
113
+    }
114
+
115
+    fn canceled(&self) -> bool {
116
+        self.runtime.is_canceled(self.cookie)
117
+    }
118
+}
119
+
120
+impl<P: PromptProvider> PromptProvider for CancellationAwarePrompt<'_, P> {
121
+    fn prompt_secret(&mut self, prompt: &str) -> Result<crate::polkit_helper::PromptResponse> {
122
+        if self.canceled() {
123
+            return Ok(crate::polkit_helper::PromptResponse::Canceled);
124
+        }
125
+        self.inner.prompt_secret(prompt)
126
+    }
127
+
128
+    fn prompt_plain(&mut self, prompt: &str) -> Result<crate::polkit_helper::PromptResponse> {
129
+        if self.canceled() {
130
+            return Ok(crate::polkit_helper::PromptResponse::Canceled);
131
+        }
132
+        self.inner.prompt_plain(prompt)
133
+    }
134
+
135
+    fn show_error(&mut self, message: &str) -> Result<()> {
136
+        if self.canceled() {
137
+            return Ok(());
138
+        }
139
+        self.inner.show_error(message)
140
+    }
141
+
142
+    fn show_info(&mut self, message: &str) -> Result<()> {
143
+        if self.canceled() {
144
+            return Ok(());
145
+        }
146
+        self.inner.show_info(message)
147
+    }
148
+
149
+    fn auth_succeeded(&mut self) -> Result<()> {
150
+        if self.canceled() {
151
+            return Ok(());
152
+        }
153
+        self.inner.auth_succeeded()
154
+    }
155
+
156
+    fn auth_failed(&mut self, message: &str) -> Result<()> {
157
+        if self.canceled() {
158
+            return Ok(());
159
+        }
160
+        self.inner.auth_failed(message)
161
+    }
162
+}
163
+
98164
 impl PolkitRuntime {
99165
     fn new(auth_state: Arc<AuthState>) -> Self {
100166
         Self::new_with_worker(auth_state, true)
@@ -113,6 +179,7 @@ impl PolkitRuntime {
113179
             auth_state,
114180
             queue: Mutex::new(AuthQueue::default()),
115181
             outcomes: Mutex::new(HashMap::new()),
182
+            canceled_cookies: Mutex::new(HashSet::new()),
116183
             outcome_signal: Condvar::new(),
117184
             helper_client: HelperSocketClient::new(helper_socket),
118185
             processing: AtomicBool::new(false),
@@ -121,6 +188,7 @@ impl PolkitRuntime {
121188
     }
122189
 
123190
     fn begin_authentication(self: &Arc<Self>, request: AuthRequest) -> Result<QueueInsert> {
191
+        let cookie = request.cookie.clone();
124192
         let (insert, active, queued) = {
125193
             let mut queue = self
126194
                 .queue
@@ -130,6 +198,7 @@ impl PolkitRuntime {
130198
             let (active, queued) = queue.counts();
131199
             (insert, active, queued)
132200
         };
201
+        self.clear_canceled(&cookie);
133202
 
134203
         self.auth_state.sync_queue_counts(active, queued);
135204
         if matches!(
@@ -169,6 +238,7 @@ impl PolkitRuntime {
169238
         };
170239
 
171240
         if canceled_active || removed_queued {
241
+            self.mark_canceled(cookie);
172242
             self.record_outcome(cookie, HelperOutcome::Canceled);
173243
         }
174244
 
@@ -262,6 +332,7 @@ impl PolkitRuntime {
262332
             message: request.message.clone(),
263333
             icon_name: request.icon_name.clone(),
264334
             detail_count: request.details.len(),
335
+            details: request.details.clone(),
265336
             cookie: request.cookie.clone(),
266337
             username,
267338
         })
@@ -277,14 +348,17 @@ impl PolkitRuntime {
277348
         request: &ActiveRequest,
278349
         prompts: &mut P,
279350
     ) -> HelperOutcome {
280
-        let prompt_context = if request.message.is_empty() {
281
-            request.action_id.as_str()
282
-        } else {
283
-            request.message.as_str()
284
-        };
351
+        let prompt_context = render_prompt_context(request);
285352
         let max_attempts = auth_max_attempts();
286353
 
287354
         for attempt in 1..=max_attempts {
355
+            if self.is_canceled(&request.cookie) {
356
+                tracing::info!(
357
+                    action_id = %request.action_id,
358
+                    "Authentication request canceled before helper attempt"
359
+                );
360
+                return HelperOutcome::Canceled;
361
+            }
288362
             tracing::info!(
289363
                 context = %prompt_context,
290364
                 attempt,
@@ -292,10 +366,20 @@ impl PolkitRuntime {
292366
                 "Starting helper authentication dialog"
293367
             );
294368
 
295
-            match self
296
-                .helper_client
297
-                .authenticate(&request.username, &request.cookie, prompts)
298
-            {
369
+            let mut cancel_aware =
370
+                CancellationAwarePrompt::new(prompts, self, request.cookie.as_str());
371
+            match self.helper_client.authenticate(
372
+                &request.username,
373
+                &request.cookie,
374
+                &mut cancel_aware,
375
+            ) {
376
+                Ok(outcome) if self.is_canceled(&request.cookie) => {
377
+                    tracing::info!(
378
+                        action_id = %request.action_id,
379
+                        "Authentication request canceled during helper attempt"
380
+                    );
381
+                    return HelperOutcome::Canceled;
382
+                }
299383
                 Ok(HelperOutcome::Denied) if attempt < max_attempts => {
300384
                     prompts.on_retry();
301385
                     self.auth_state.set_phase(AuthPhase::PendingPrompt);
@@ -352,6 +436,7 @@ impl PolkitRuntime {
352436
             }
353437
             return;
354438
         }
439
+        self.clear_canceled(cookie);
355440
         self.record_outcome(cookie, outcome);
356441
 
357442
         if active > 0 {
@@ -380,6 +465,29 @@ impl PolkitRuntime {
380465
         self.outcome_signal.notify_all();
381466
     }
382467
 
468
+    fn mark_canceled(&self, cookie: &str) {
469
+        if let Ok(mut canceled) = self.canceled_cookies.lock() {
470
+            canceled.insert(cookie.to_string());
471
+        } else {
472
+            tracing::error!("auth canceled-cookie set lock poisoned");
473
+        }
474
+    }
475
+
476
+    fn clear_canceled(&self, cookie: &str) {
477
+        if let Ok(mut canceled) = self.canceled_cookies.lock() {
478
+            canceled.remove(cookie);
479
+        } else {
480
+            tracing::error!("auth canceled-cookie set lock poisoned");
481
+        }
482
+    }
483
+
484
+    fn is_canceled(&self, cookie: &str) -> bool {
485
+        self.canceled_cookies
486
+            .lock()
487
+            .map(|canceled| canceled.contains(cookie))
488
+            .unwrap_or(false)
489
+    }
490
+
383491
     fn wait_for_completion(&self, cookie: &str) -> Result<HelperOutcome> {
384492
         let mut outcomes = self
385493
             .outcomes
@@ -403,12 +511,59 @@ impl PolkitRuntime {
403511
         if let Ok(mut outcomes) = self.outcomes.lock() {
404512
             outcomes.clear();
405513
         }
514
+        if let Ok(mut canceled) = self.canceled_cookies.lock() {
515
+            canceled.clear();
516
+        }
406517
         self.processing.store(false, Ordering::Release);
407518
         self.auth_state.set_phase(AuthPhase::Idle);
408519
         self.auth_state.sync_queue_counts(0, 0);
409520
     }
410521
 }
411522
 
523
+fn render_prompt_context(request: &ActiveRequest) -> String {
524
+    let mut lines = Vec::new();
525
+    let message = request.message.trim();
526
+    if !message.is_empty() {
527
+        lines.push(message.to_string());
528
+    } else {
529
+        lines.push("Authentication is required".to_string());
530
+    }
531
+
532
+    lines.push(format!("Action: {}", request.action_id));
533
+    if !request.icon_name.trim().is_empty() {
534
+        lines.push(format!("Icon: {}", request.icon_name.trim()));
535
+    }
536
+
537
+    let detail_keys = [
538
+        "program",
539
+        "command_line",
540
+        "unit",
541
+        "verb",
542
+        "polkit.retains_authorization_after_challenge",
543
+    ];
544
+    for key in detail_keys {
545
+        if let Some(value) = request.details.get(key) {
546
+            let trimmed = value.trim();
547
+            if !trimmed.is_empty() {
548
+                lines.push(format!("{}: {}", detail_key_label(key), trimmed));
549
+            }
550
+        }
551
+    }
552
+
553
+    lines.join("\n")
554
+}
555
+
556
+fn detail_key_label(key: &str) -> &'static str {
557
+    match key {
558
+        "program" => "Program",
559
+        "command_line" => "Command",
560
+        "unit" => "Unit",
561
+        "verb" => "Verb",
562
+        "polkit.retains_authorization_after_challenge" => "Retains authorization",
563
+        _ => "Detail",
564
+    }
565
+}
566
+
412567
 fn resolve_identity_username(identities: &[Subject]) -> Option<String> {
413568
     let current = current_username();
414569
     if let Some(current_name) = current.as_deref() {
@@ -1108,6 +1263,7 @@ mod tests {
11081263
             auth_state: Arc::new(AuthState::default()),
11091264
             queue: Mutex::new(AuthQueue::default()),
11101265
             outcomes: Mutex::new(HashMap::new()),
1266
+            canceled_cookies: Mutex::new(HashSet::new()),
11111267
             outcome_signal: Condvar::new(),
11121268
             helper_client: HelperSocketClient::new(&socket_path),
11131269
             processing: AtomicBool::new(false),
@@ -1118,6 +1274,7 @@ mod tests {
11181274
             message: "Authenticate".to_string(),
11191275
             icon_name: "dialog-password".to_string(),
11201276
             detail_count: 0,
1277
+            details: HashMap::new(),
11211278
             cookie: "cookie-1".to_string(),
11221279
             username: "alice".to_string(),
11231280
         };
@@ -1136,6 +1293,56 @@ mod tests {
11361293
         let _ = std::fs::remove_file(&socket_path);
11371294
     }
11381295
 
1296
+    #[test]
1297
+    fn render_prompt_context_includes_policy_details() {
1298
+        let mut details = HashMap::new();
1299
+        details.insert("program".to_string(), "/usr/bin/meson".to_string());
1300
+        details.insert("command_line".to_string(), "meson install".to_string());
1301
+        details.insert(
1302
+            "polkit.retains_authorization_after_challenge".to_string(),
1303
+            "1".to_string(),
1304
+        );
1305
+        let request = ActiveRequest {
1306
+            action_id: "com.mesonbuild.install.run".to_string(),
1307
+            message: "Authentication is required to install this project".to_string(),
1308
+            icon_name: "preferences-system".to_string(),
1309
+            detail_count: details.len(),
1310
+            details,
1311
+            cookie: "cookie-ctx".to_string(),
1312
+            username: "alice".to_string(),
1313
+        };
1314
+
1315
+        let context = render_prompt_context(&request);
1316
+        assert!(context.contains("Authentication is required to install this project"));
1317
+        assert!(context.contains("Action: com.mesonbuild.install.run"));
1318
+        assert!(context.contains("Icon: preferences-system"));
1319
+        assert!(context.contains("Program: /usr/bin/meson"));
1320
+        assert!(context.contains("Command: meson install"));
1321
+        assert!(context.contains("Retains authorization: 1"));
1322
+    }
1323
+
1324
+    #[test]
1325
+    fn cancellation_aware_prompt_short_circuits_prompt_and_feedback() {
1326
+        let runtime = PolkitRuntime::new_without_worker(Arc::new(AuthState::default()));
1327
+        runtime.mark_canceled("cookie-1");
1328
+
1329
+        let mut prompts =
1330
+            SequencedPrompt::new(vec![PromptResponse::Submitted("correct horse".to_string())]);
1331
+        {
1332
+            let mut wrapped = CancellationAwarePrompt::new(&mut prompts, &runtime, "cookie-1");
1333
+            let response = wrapped
1334
+                .prompt_secret("Password:")
1335
+                .expect("prompt response should be canceled");
1336
+            assert_eq!(response, PromptResponse::Canceled);
1337
+            wrapped
1338
+                .auth_succeeded()
1339
+                .expect("suppressed success callback");
1340
+        }
1341
+
1342
+        assert_eq!(prompts.prompt_count, 0);
1343
+        assert_eq!(prompts.success_count, 0);
1344
+    }
1345
+
11391346
     #[test]
11401347
     fn resolve_identity_username_uses_uid_detail() {
11411348
         let mut details = HashMap::new();