@@ -57,6 +57,18 @@ pub struct PolkitBackendConfig { |
| 57 | 57 | type Subject = (String, HashMap<String, OwnedValue>); |
| 58 | 58 | type Details = HashMap<String, String>; |
| 59 | 59 | type TemporaryAuthorization = (String, String, Subject, u64, u64); |
| 60 | +type ActionDescription = ( |
| 61 | + String, |
| 62 | + String, |
| 63 | + String, |
| 64 | + String, |
| 65 | + String, |
| 66 | + String, |
| 67 | + u32, |
| 68 | + u32, |
| 69 | + u32, |
| 70 | + Details, |
| 71 | +); |
| 60 | 72 | |
| 61 | 73 | #[derive(Debug, Clone, serde::Serialize)] |
| 62 | 74 | pub struct TemporaryAuthorizationRecord { |
@@ -396,7 +408,8 @@ impl PolkitRuntime { |
| 396 | 408 | { |
| 397 | 409 | identity_options.insert(0, username.clone()); |
| 398 | 410 | } |
| 399 | | - let retention_options = retention_options_from_details(&request.details); |
| 411 | + let retention_options = |
| 412 | + retention_options_with_policy_fallback(&request.action_id, &request.details); |
| 400 | 413 | |
| 401 | 414 | tracing::debug!( |
| 402 | 415 | identity_summary = %summarize_identities(&request.identities), |
@@ -1189,6 +1202,61 @@ fn retention_options_from_details(details: &Details) -> Vec<RetentionPolicy> { |
| 1189 | 1202 | options |
| 1190 | 1203 | } |
| 1191 | 1204 | |
| 1205 | +fn retention_options_with_policy_fallback( |
| 1206 | + action_id: &str, |
| 1207 | + details: &Details, |
| 1208 | +) -> Vec<RetentionPolicy> { |
| 1209 | + let options = retention_options_from_details(details); |
| 1210 | + if options.len() > 1 { |
| 1211 | + return options; |
| 1212 | + } |
| 1213 | + |
| 1214 | + match retention_options_from_action_policy(action_id) { |
| 1215 | + Ok(Some(policy_options)) if policy_options.len() > 1 => policy_options, |
| 1216 | + Ok(_) => options, |
| 1217 | + Err(err) => { |
| 1218 | + tracing::debug!( |
| 1219 | + action_id = %action_id, |
| 1220 | + error = %err, |
| 1221 | + "Unable to infer retention options from polkit action policy" |
| 1222 | + ); |
| 1223 | + options |
| 1224 | + } |
| 1225 | + } |
| 1226 | +} |
| 1227 | + |
| 1228 | +fn retention_options_from_action_policy(action_id: &str) -> Result<Option<Vec<RetentionPolicy>>> { |
| 1229 | + let connection = Connection::system().context("failed to connect to system bus")?; |
| 1230 | + let proxy = PolkitAgent::proxy(&connection)?; |
| 1231 | + let actions: Vec<ActionDescription> = proxy.call("EnumerateActions", &"")?; |
| 1232 | + Ok(retention_options_from_action_descriptions( |
| 1233 | + action_id, &actions, |
| 1234 | + )) |
| 1235 | +} |
| 1236 | + |
| 1237 | +fn retention_options_from_action_descriptions( |
| 1238 | + action_id: &str, |
| 1239 | + actions: &[ActionDescription], |
| 1240 | +) -> Option<Vec<RetentionPolicy>> { |
| 1241 | + let mut options = vec![RetentionPolicy::OneShot]; |
| 1242 | + let (_, _, _, _, _, _, implicit_any, implicit_inactive, implicit_active, _) = |
| 1243 | + actions.iter().find(|(id, ..)| id == action_id)?; |
| 1244 | + |
| 1245 | + if implicit_authorization_supports_retention(*implicit_any) |
| 1246 | + || implicit_authorization_supports_retention(*implicit_inactive) |
| 1247 | + || implicit_authorization_supports_retention(*implicit_active) |
| 1248 | + { |
| 1249 | + options.push(RetentionPolicy::Session); |
| 1250 | + } |
| 1251 | + |
| 1252 | + Some(options) |
| 1253 | +} |
| 1254 | + |
| 1255 | +fn implicit_authorization_supports_retention(value: u32) -> bool { |
| 1256 | + // 3/4 are *_RETAINED variants in PolkitImplicitAuthorization. |
| 1257 | + value == 3 || value == 4 |
| 1258 | +} |
| 1259 | + |
| 1192 | 1260 | fn first_detail_value(details: &Details, keys: &[&str]) -> Option<String> { |
| 1193 | 1261 | for key in keys { |
| 1194 | 1262 | let Some(value) = details.get(*key) else { |
@@ -2311,6 +2379,47 @@ mod tests { |
| 2311 | 2379 | ); |
| 2312 | 2380 | } |
| 2313 | 2381 | |
| 2382 | + #[test] |
| 2383 | + fn retention_options_from_action_descriptions_support_retained_policies() { |
| 2384 | + let actions = vec![( |
| 2385 | + "com.gardesk.test".to_string(), |
| 2386 | + "desc".to_string(), |
| 2387 | + "msg".to_string(), |
| 2388 | + "vendor".to_string(), |
| 2389 | + "url".to_string(), |
| 2390 | + "icon".to_string(), |
| 2391 | + 0_u32, |
| 2392 | + 0_u32, |
| 2393 | + 4_u32, |
| 2394 | + HashMap::new(), |
| 2395 | + )]; |
| 2396 | + |
| 2397 | + let options = retention_options_from_action_descriptions("com.gardesk.test", &actions) |
| 2398 | + .expect("action should resolve"); |
| 2399 | + assert_eq!( |
| 2400 | + options, |
| 2401 | + vec![RetentionPolicy::OneShot, RetentionPolicy::Session] |
| 2402 | + ); |
| 2403 | + } |
| 2404 | + |
| 2405 | + #[test] |
| 2406 | + fn retention_options_from_action_descriptions_handle_missing_action() { |
| 2407 | + let actions = vec![( |
| 2408 | + "com.gardesk.other".to_string(), |
| 2409 | + "desc".to_string(), |
| 2410 | + "msg".to_string(), |
| 2411 | + "vendor".to_string(), |
| 2412 | + "url".to_string(), |
| 2413 | + "icon".to_string(), |
| 2414 | + 0_u32, |
| 2415 | + 0_u32, |
| 2416 | + 0_u32, |
| 2417 | + HashMap::new(), |
| 2418 | + )]; |
| 2419 | + |
| 2420 | + assert!(retention_options_from_action_descriptions("com.gardesk.test", &actions).is_none()); |
| 2421 | + } |
| 2422 | + |
| 2314 | 2423 | #[test] |
| 2315 | 2424 | fn parse_retention_selection_accepts_index_and_label() { |
| 2316 | 2425 | let options = vec![ |