@@ -417,7 +417,11 @@ impl PolkitRuntime { |
| 417 | action_id = %request.action_id, | 417 | action_id = %request.action_id, |
| 418 | "Authentication request canceled before helper attempt" | 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 | tracing::info!( | 426 | tracing::info!( |
| 423 | context = %prompt_context, | 427 | context = %prompt_context, |
@@ -1889,6 +1893,137 @@ mod tests { |
| 1889 | let _ = std::fs::remove_file(&socket_path); | 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 | #[test] | 2027 | #[test] |
| 1893 | fn render_prompt_context_includes_policy_details() { | 2028 | fn render_prompt_context_includes_policy_details() { |
| 1894 | let mut details = HashMap::new(); | 2029 | let mut details = HashMap::new(); |
@@ -2039,8 +2174,11 @@ mod tests { |
| 2039 | retention_options: vec![RetentionPolicy::OneShot], | 2174 | retention_options: vec![RetentionPolicy::OneShot], |
| 2040 | }; | 2175 | }; |
| 2041 | | 2176 | |
| 2042 | - let outcome = | 2177 | + let outcome = runtime.finalize_auth_attempt( |
| 2043 | - runtime.finalize_auth_attempt(&request, HelperOutcome::Denied, Some(RetentionPolicy::OneShot)); | 2178 | + &request, |
| | 2179 | + HelperOutcome::Denied, |
| | 2180 | + Some(RetentionPolicy::OneShot), |
| | 2181 | + ); |
| 2044 | assert_eq!(outcome, HelperOutcome::Denied); | 2182 | assert_eq!(outcome, HelperOutcome::Denied); |
| 2045 | | 2183 | |
| 2046 | let summary = auth_state.summary(); | 2184 | let summary = auth_state.summary(); |