@@ -91,30 +91,58 @@ impl HelperSocketClient { |
| 91 | 91 | cookie: &str, |
| 92 | 92 | prompts: &mut P, |
| 93 | 93 | ) -> Result<HelperOutcome> { |
| 94 | + let username_line = sanitize_control_line(username); |
| 95 | + let cookie_line = sanitize_control_line(cookie); |
| 94 | 96 | let conversation_backend = helper_conversation_backend(); |
| 95 | 97 | tracing::debug!( |
| 96 | 98 | backend = %conversation_backend.as_str(), |
| 97 | 99 | env_key = HELPER_CONVERSATION_BACKEND_ENV, |
| 98 | 100 | "Selected polkit helper conversation backend" |
| 99 | 101 | ); |
| 100 | | - match conversation_backend { |
| 101 | | - HelperConversationBackend::Auto | HelperConversationBackend::HelperProtocol => { |
| 102 | | - self.authenticate_with_helper_protocol(username, cookie, prompts) |
| 102 | + self.authenticate_with_backend(&username_line, &cookie_line, prompts, conversation_backend) |
| 103 | + } |
| 104 | + |
| 105 | + fn authenticate_with_backend<P: PromptProvider>( |
| 106 | + &self, |
| 107 | + username_line: &str, |
| 108 | + cookie_line: &str, |
| 109 | + prompts: &mut P, |
| 110 | + backend: HelperConversationBackend, |
| 111 | + ) -> Result<HelperOutcome> { |
| 112 | + match backend { |
| 113 | + HelperConversationBackend::HelperProtocol => { |
| 114 | + self.authenticate_with_helper_protocol(username_line, cookie_line, prompts) |
| 103 | 115 | } |
| 104 | 116 | HelperConversationBackend::SessionApi => { |
| 105 | | - self.authenticate_via_session_api(username, cookie, prompts) |
| 117 | + self.authenticate_via_session_api(username_line, cookie_line, prompts) |
| 118 | + } |
| 119 | + HelperConversationBackend::Auto => { |
| 120 | + match self.authenticate_via_session_api(username_line, cookie_line, prompts) { |
| 121 | + Ok(outcome) => return Ok(outcome), |
| 122 | + Err(err) if is_session_api_unavailable_error(&err) => { |
| 123 | + tracing::debug!( |
| 124 | + error = %err, |
| 125 | + "Session-api conversation backend unavailable; falling back to helper protocol backend" |
| 126 | + ); |
| 127 | + } |
| 128 | + Err(err) => { |
| 129 | + tracing::warn!( |
| 130 | + error = %err, |
| 131 | + "Session-api conversation backend failed; falling back to helper protocol backend" |
| 132 | + ); |
| 133 | + } |
| 134 | + } |
| 135 | + self.authenticate_with_helper_protocol(username_line, cookie_line, prompts) |
| 106 | 136 | } |
| 107 | 137 | } |
| 108 | 138 | } |
| 109 | 139 | |
| 110 | 140 | fn authenticate_with_helper_protocol<P: PromptProvider>( |
| 111 | 141 | &self, |
| 112 | | - username: &str, |
| 113 | | - cookie: &str, |
| 142 | + username_line: &str, |
| 143 | + cookie_line: &str, |
| 114 | 144 | prompts: &mut P, |
| 115 | 145 | ) -> Result<HelperOutcome> { |
| 116 | | - let username_line = sanitize_control_line(username); |
| 117 | | - let cookie_line = sanitize_control_line(cookie); |
| 118 | 146 | let transport = helper_transport_mode(); |
| 119 | 147 | tracing::debug!( |
| 120 | 148 | transport = %transport.as_str(), |
@@ -161,11 +189,22 @@ impl HelperSocketClient { |
| 161 | 189 | |
| 162 | 190 | fn authenticate_via_session_api<P: PromptProvider>( |
| 163 | 191 | &self, |
| 164 | | - _username: &str, |
| 165 | | - _cookie: &str, |
| 166 | | - _prompts: &mut P, |
| 192 | + username_line: &str, |
| 193 | + cookie_line: &str, |
| 194 | + prompts: &mut P, |
| 167 | 195 | ) -> Result<HelperOutcome> { |
| 168 | | - anyhow::bail!("session-api helper conversation backend is not yet implemented"); |
| 196 | + let helper = resolve_direct_helper_path() |
| 197 | + .ok_or_else(|| anyhow::Error::new(SessionApiUnavailableError))?; |
| 198 | + tracing::debug!( |
| 199 | + helper = %helper.display(), |
| 200 | + "Using session-api conversation backend via direct helper process" |
| 201 | + ); |
| 202 | + self.authenticate_via_helper_process_with_helper( |
| 203 | + &helper, |
| 204 | + username_line, |
| 205 | + cookie_line, |
| 206 | + prompts, |
| 207 | + ) |
| 169 | 208 | } |
| 170 | 209 | |
| 171 | 210 | fn authenticate_auto_transport<P: PromptProvider>( |
@@ -612,6 +651,23 @@ fn is_no_initial_helper_response_error(err: &anyhow::Error) -> bool { |
| 612 | 651 | err.downcast_ref::<NoInitialHelperResponseError>().is_some() |
| 613 | 652 | } |
| 614 | 653 | |
| 654 | +#[derive(Debug)] |
| 655 | +struct SessionApiUnavailableError; |
| 656 | + |
| 657 | +impl std::fmt::Display for SessionApiUnavailableError { |
| 658 | + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { |
| 659 | + f.write_str( |
| 660 | + "session-api helper conversation backend unavailable: no root setuid helper found", |
| 661 | + ) |
| 662 | + } |
| 663 | +} |
| 664 | + |
| 665 | +impl std::error::Error for SessionApiUnavailableError {} |
| 666 | + |
| 667 | +fn is_session_api_unavailable_error(err: &anyhow::Error) -> bool { |
| 668 | + err.downcast_ref::<SessionApiUnavailableError>().is_some() |
| 669 | +} |
| 670 | + |
| 615 | 671 | fn write_line(stream: &mut impl Write, value: &str) -> Result<()> { |
| 616 | 672 | stream.write_all(value.as_bytes())?; |
| 617 | 673 | stream.write_all(b"\n")?; |
@@ -1003,18 +1059,67 @@ mod tests { |
| 1003 | 1059 | } |
| 1004 | 1060 | |
| 1005 | 1061 | #[test] |
| 1006 | | - fn session_backend_reports_not_implemented() { |
| 1062 | + fn session_backend_reports_unavailable_without_setuid_helper() { |
| 1007 | 1063 | let client = HelperSocketClient::new(temp_socket_path()); |
| 1008 | 1064 | let mut prompts = FakePrompt::default(); |
| 1009 | 1065 | let err = client |
| 1010 | 1066 | .authenticate_via_session_api("alice", "cookie-session", &mut prompts) |
| 1011 | | - .expect_err("session backend should be unimplemented"); |
| 1067 | + .expect_err("session backend should be unavailable"); |
| 1012 | 1068 | assert!( |
| 1013 | 1069 | err.to_string() |
| 1014 | | - .contains("session-api helper conversation backend is not yet implemented") |
| 1070 | + .contains("session-api helper conversation backend unavailable") |
| 1015 | 1071 | ); |
| 1016 | 1072 | } |
| 1017 | 1073 | |
| 1074 | + #[test] |
| 1075 | + fn auto_backend_falls_back_to_helper_protocol_when_session_unavailable() { |
| 1076 | + let socket_path = temp_socket_path(); |
| 1077 | + let listener = UnixListener::bind(&socket_path).expect("bind test socket"); |
| 1078 | + |
| 1079 | + let server = thread::spawn(move || { |
| 1080 | + let (mut stream, _) = listener.accept().expect("accept"); |
| 1081 | + let read_stream = stream.try_clone().expect("clone"); |
| 1082 | + let mut reader = BufReader::new(read_stream); |
| 1083 | + |
| 1084 | + let mut first_line = String::new(); |
| 1085 | + reader.read_line(&mut first_line).expect("read first line"); |
| 1086 | + if first_line.trim() == "alice" { |
| 1087 | + let mut cookie = String::new(); |
| 1088 | + reader.read_line(&mut cookie).expect("read cookie"); |
| 1089 | + } |
| 1090 | + |
| 1091 | + stream |
| 1092 | + .write_all(b"PAM_PROMPT_ECHO_OFF Password:\n") |
| 1093 | + .expect("write prompt"); |
| 1094 | + stream.flush().expect("flush prompt"); |
| 1095 | + |
| 1096 | + let mut secret = String::new(); |
| 1097 | + reader.read_line(&mut secret).expect("read secret"); |
| 1098 | + stream.write_all(b"SUCCESS\n").expect("write success"); |
| 1099 | + stream.flush().expect("flush success"); |
| 1100 | + }); |
| 1101 | + |
| 1102 | + let client = HelperSocketClient::new(&socket_path); |
| 1103 | + let mut prompts = FakePrompt { |
| 1104 | + secret_response: PromptResponse::Submitted("correct horse".to_string()), |
| 1105 | + ..FakePrompt::default() |
| 1106 | + }; |
| 1107 | + |
| 1108 | + let outcome = client |
| 1109 | + .authenticate_with_backend( |
| 1110 | + "alice", |
| 1111 | + "cookie-auto-fallback", |
| 1112 | + &mut prompts, |
| 1113 | + HelperConversationBackend::Auto, |
| 1114 | + ) |
| 1115 | + .expect("auto backend fallback should succeed"); |
| 1116 | + assert_eq!(outcome, HelperOutcome::Authorized); |
| 1117 | + assert_eq!(prompts.success_count, 1); |
| 1118 | + |
| 1119 | + server.join().expect("server join"); |
| 1120 | + let _ = std::fs::remove_file(&socket_path); |
| 1121 | + } |
| 1122 | + |
| 1018 | 1123 | #[test] |
| 1019 | 1124 | fn helper_client_drives_prompt_round_trip() { |
| 1020 | 1125 | let socket_path = temp_socket_path(); |