@@ -91,30 +91,58 @@ impl HelperSocketClient { |
| 91 | cookie: &str, | 91 | cookie: &str, |
| 92 | prompts: &mut P, | 92 | prompts: &mut P, |
| 93 | ) -> Result<HelperOutcome> { | 93 | ) -> Result<HelperOutcome> { |
| | 94 | + let username_line = sanitize_control_line(username); |
| | 95 | + let cookie_line = sanitize_control_line(cookie); |
| 94 | let conversation_backend = helper_conversation_backend(); | 96 | let conversation_backend = helper_conversation_backend(); |
| 95 | tracing::debug!( | 97 | tracing::debug!( |
| 96 | backend = %conversation_backend.as_str(), | 98 | backend = %conversation_backend.as_str(), |
| 97 | env_key = HELPER_CONVERSATION_BACKEND_ENV, | 99 | env_key = HELPER_CONVERSATION_BACKEND_ENV, |
| 98 | "Selected polkit helper conversation backend" | 100 | "Selected polkit helper conversation backend" |
| 99 | ); | 101 | ); |
| 100 | - match conversation_backend { | 102 | + self.authenticate_with_backend(&username_line, &cookie_line, prompts, conversation_backend) |
| 101 | - HelperConversationBackend::Auto | HelperConversationBackend::HelperProtocol => { | 103 | + } |
| 102 | - self.authenticate_with_helper_protocol(username, cookie, prompts) | 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 | HelperConversationBackend::SessionApi => { | 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 | fn authenticate_with_helper_protocol<P: PromptProvider>( | 140 | fn authenticate_with_helper_protocol<P: PromptProvider>( |
| 111 | &self, | 141 | &self, |
| 112 | - username: &str, | 142 | + username_line: &str, |
| 113 | - cookie: &str, | 143 | + cookie_line: &str, |
| 114 | prompts: &mut P, | 144 | prompts: &mut P, |
| 115 | ) -> Result<HelperOutcome> { | 145 | ) -> Result<HelperOutcome> { |
| 116 | - let username_line = sanitize_control_line(username); | | |
| 117 | - let cookie_line = sanitize_control_line(cookie); | | |
| 118 | let transport = helper_transport_mode(); | 146 | let transport = helper_transport_mode(); |
| 119 | tracing::debug!( | 147 | tracing::debug!( |
| 120 | transport = %transport.as_str(), | 148 | transport = %transport.as_str(), |
@@ -161,11 +189,22 @@ impl HelperSocketClient { |
| 161 | | 189 | |
| 162 | fn authenticate_via_session_api<P: PromptProvider>( | 190 | fn authenticate_via_session_api<P: PromptProvider>( |
| 163 | &self, | 191 | &self, |
| 164 | - _username: &str, | 192 | + username_line: &str, |
| 165 | - _cookie: &str, | 193 | + cookie_line: &str, |
| 166 | - _prompts: &mut P, | 194 | + prompts: &mut P, |
| 167 | ) -> Result<HelperOutcome> { | 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 | fn authenticate_auto_transport<P: PromptProvider>( | 210 | fn authenticate_auto_transport<P: PromptProvider>( |
@@ -612,6 +651,23 @@ fn is_no_initial_helper_response_error(err: &anyhow::Error) -> bool { |
| 612 | err.downcast_ref::<NoInitialHelperResponseError>().is_some() | 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 | fn write_line(stream: &mut impl Write, value: &str) -> Result<()> { | 671 | fn write_line(stream: &mut impl Write, value: &str) -> Result<()> { |
| 616 | stream.write_all(value.as_bytes())?; | 672 | stream.write_all(value.as_bytes())?; |
| 617 | stream.write_all(b"\n")?; | 673 | stream.write_all(b"\n")?; |
@@ -1003,18 +1059,67 @@ mod tests { |
| 1003 | } | 1059 | } |
| 1004 | | 1060 | |
| 1005 | #[test] | 1061 | #[test] |
| 1006 | - fn session_backend_reports_not_implemented() { | 1062 | + fn session_backend_reports_unavailable_without_setuid_helper() { |
| 1007 | let client = HelperSocketClient::new(temp_socket_path()); | 1063 | let client = HelperSocketClient::new(temp_socket_path()); |
| 1008 | let mut prompts = FakePrompt::default(); | 1064 | let mut prompts = FakePrompt::default(); |
| 1009 | let err = client | 1065 | let err = client |
| 1010 | .authenticate_via_session_api("alice", "cookie-session", &mut prompts) | 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 | assert!( | 1068 | assert!( |
| 1013 | err.to_string() | 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 | #[test] | 1123 | #[test] |
| 1019 | fn helper_client_drives_prompt_round_trip() { | 1124 | fn helper_client_drives_prompt_round_trip() { |
| 1020 | let socket_path = temp_socket_path(); | 1125 | let socket_path = temp_socket_path(); |