@@ -417,7 +417,11 @@ impl PolkitRuntime { |
| 417 | 417 | action_id = %request.action_id, |
| 418 | 418 | "Authentication request canceled before helper attempt" |
| 419 | 419 | ); |
| 420 | | - return self.finalize_auth_attempt(request, HelperOutcome::Canceled, Some(retention)); |
| 420 | + return self.finalize_auth_attempt( |
| 421 | + request, |
| 422 | + HelperOutcome::Canceled, |
| 423 | + Some(retention), |
| 424 | + ); |
| 421 | 425 | } |
| 422 | 426 | tracing::info!( |
| 423 | 427 | context = %prompt_context, |
@@ -1889,6 +1893,137 @@ mod tests { |
| 1889 | 1893 | let _ = std::fs::remove_file(&socket_path); |
| 1890 | 1894 | } |
| 1891 | 1895 | |
| 1896 | + #[test] |
| 1897 | + fn authenticate_active_request_terminal_outcome_matrix() { |
| 1898 | + struct Case { |
| 1899 | + label: &'static str, |
| 1900 | + responses: Vec<PromptResponse>, |
| 1901 | + server_results: Vec<Option<&'static str>>, |
| 1902 | + expected_outcome: HelperOutcome, |
| 1903 | + expected_success_count: usize, |
| 1904 | + expected_failure_count: usize, |
| 1905 | + } |
| 1906 | + |
| 1907 | + let cases = vec![ |
| 1908 | + Case { |
| 1909 | + label: "success", |
| 1910 | + responses: vec![PromptResponse::Submitted("correct horse".to_string())], |
| 1911 | + server_results: vec![Some("SUCCESS")], |
| 1912 | + expected_outcome: HelperOutcome::Authorized, |
| 1913 | + expected_success_count: 1, |
| 1914 | + expected_failure_count: 0, |
| 1915 | + }, |
| 1916 | + Case { |
| 1917 | + label: "failure", |
| 1918 | + responses: vec![ |
| 1919 | + PromptResponse::Submitted("wrong horse".to_string()), |
| 1920 | + PromptResponse::Submitted("wrong horse".to_string()), |
| 1921 | + PromptResponse::Submitted("wrong horse".to_string()), |
| 1922 | + ], |
| 1923 | + server_results: vec![Some("FAILURE"), Some("FAILURE"), Some("FAILURE")], |
| 1924 | + expected_outcome: HelperOutcome::Denied, |
| 1925 | + expected_success_count: 0, |
| 1926 | + expected_failure_count: 3, |
| 1927 | + }, |
| 1928 | + Case { |
| 1929 | + label: "canceled", |
| 1930 | + responses: vec![PromptResponse::Canceled], |
| 1931 | + server_results: vec![None], |
| 1932 | + expected_outcome: HelperOutcome::Canceled, |
| 1933 | + expected_success_count: 0, |
| 1934 | + expected_failure_count: 0, |
| 1935 | + }, |
| 1936 | + Case { |
| 1937 | + label: "timeout", |
| 1938 | + responses: vec![PromptResponse::TimedOut], |
| 1939 | + server_results: vec![None], |
| 1940 | + expected_outcome: HelperOutcome::Timeout, |
| 1941 | + expected_success_count: 0, |
| 1942 | + expected_failure_count: 0, |
| 1943 | + }, |
| 1944 | + ]; |
| 1945 | + |
| 1946 | + for case in cases { |
| 1947 | + let socket_path = temp_socket_path(); |
| 1948 | + let listener = UnixListener::bind(&socket_path).expect("bind test socket"); |
| 1949 | + let server_results = case.server_results.clone(); |
| 1950 | + |
| 1951 | + let server = thread::spawn(move || { |
| 1952 | + for expected_result in server_results { |
| 1953 | + let (mut stream, _) = listener.accept().expect("accept"); |
| 1954 | + let read_stream = stream.try_clone().expect("clone"); |
| 1955 | + let mut reader = BufReader::new(read_stream); |
| 1956 | + |
| 1957 | + let mut first_line = String::new(); |
| 1958 | + reader.read_line(&mut first_line).expect("read first line"); |
| 1959 | + if first_line.trim() == "operator" { |
| 1960 | + let mut cookie = String::new(); |
| 1961 | + reader.read_line(&mut cookie).expect("read cookie"); |
| 1962 | + } |
| 1963 | + |
| 1964 | + stream |
| 1965 | + .write_all(b"PAM_PROMPT_ECHO_OFF Password:\n") |
| 1966 | + .expect("write prompt"); |
| 1967 | + stream.flush().expect("flush prompt"); |
| 1968 | + |
| 1969 | + let mut secret = String::new(); |
| 1970 | + let read = reader.read_line(&mut secret).expect("read secret"); |
| 1971 | + if let Some(result) = expected_result { |
| 1972 | + assert!( |
| 1973 | + read > 0, |
| 1974 | + "expected submitted secret response for {}", |
| 1975 | + result |
| 1976 | + ); |
| 1977 | + stream |
| 1978 | + .write_all(format!("{}\n", result).as_bytes()) |
| 1979 | + .expect("write result"); |
| 1980 | + stream.flush().expect("flush result"); |
| 1981 | + } |
| 1982 | + } |
| 1983 | + }); |
| 1984 | + |
| 1985 | + let runtime = PolkitRuntime { |
| 1986 | + auth_state: Arc::new(AuthState::default()), |
| 1987 | + queue: Mutex::new(AuthQueue::default()), |
| 1988 | + outcomes: Mutex::new(HashMap::new()), |
| 1989 | + canceled_cookies: Mutex::new(HashSet::new()), |
| 1990 | + outcome_signal: Condvar::new(), |
| 1991 | + helper_client: HelperSocketClient::new(&socket_path), |
| 1992 | + processing: AtomicBool::new(false), |
| 1993 | + worker_enabled: false, |
| 1994 | + }; |
| 1995 | + let active = ActiveRequest { |
| 1996 | + action_id: "org.gardesk.test".to_string(), |
| 1997 | + message: "Authenticate".to_string(), |
| 1998 | + icon_name: "dialog-password".to_string(), |
| 1999 | + detail_count: 0, |
| 2000 | + details: HashMap::new(), |
| 2001 | + cookie: format!("cookie-{}", case.label), |
| 2002 | + username: "operator".to_string(), |
| 2003 | + identity_options: vec!["operator".to_string()], |
| 2004 | + retention_options: vec![RetentionPolicy::OneShot], |
| 2005 | + }; |
| 2006 | + let mut prompts = SequencedPrompt::new(case.responses); |
| 2007 | + |
| 2008 | + let outcome = runtime.authenticate_active_request_with_prompts(&active, &mut prompts); |
| 2009 | + assert_eq!(outcome, case.expected_outcome, "case {}", case.label); |
| 2010 | + assert_eq!( |
| 2011 | + prompts.success_count, case.expected_success_count, |
| 2012 | + "case {}", |
| 2013 | + case.label |
| 2014 | + ); |
| 2015 | + assert_eq!( |
| 2016 | + prompts.failure_messages.len(), |
| 2017 | + case.expected_failure_count, |
| 2018 | + "case {}", |
| 2019 | + case.label |
| 2020 | + ); |
| 2021 | + |
| 2022 | + server.join().expect("server join"); |
| 2023 | + let _ = std::fs::remove_file(&socket_path); |
| 2024 | + } |
| 2025 | + } |
| 2026 | + |
| 1892 | 2027 | #[test] |
| 1893 | 2028 | fn render_prompt_context_includes_policy_details() { |
| 1894 | 2029 | let mut details = HashMap::new(); |
@@ -2039,8 +2174,11 @@ mod tests { |
| 2039 | 2174 | retention_options: vec![RetentionPolicy::OneShot], |
| 2040 | 2175 | }; |
| 2041 | 2176 | |
| 2042 | | - let outcome = |
| 2043 | | - runtime.finalize_auth_attempt(&request, HelperOutcome::Denied, Some(RetentionPolicy::OneShot)); |
| 2177 | + let outcome = runtime.finalize_auth_attempt( |
| 2178 | + &request, |
| 2179 | + HelperOutcome::Denied, |
| 2180 | + Some(RetentionPolicy::OneShot), |
| 2181 | + ); |
| 2044 | 2182 | assert_eq!(outcome, HelperOutcome::Denied); |
| 2045 | 2183 | |
| 2046 | 2184 | let summary = auth_state.summary(); |