pacsea/logic/password.rs
1//! Sudo password validation utilities.
2
3use std::process::Command;
4
5/// What: Returns true only when running in integration test context.
6///
7/// Inputs:
8/// - None (reads env var `PACSEA_INTEGRATION_TEST`).
9///
10/// Output:
11/// - `true` if `PACSEA_INTEGRATION_TEST=1` is set, `false` otherwise.
12///
13/// Details:
14/// - Used to gate `PACSEA_TEST_SUDO_PASSWORDLESS` so production never honors it.
15/// - Integration tests set this env var so the test override is applied.
16fn is_integration_test_context() -> bool {
17 std::env::var("PACSEA_INTEGRATION_TEST").is_ok_and(|v| v == "1")
18}
19
20/// What: Check if passwordless sudo is available for the current user.
21///
22/// Inputs:
23/// - None (checks system configuration).
24///
25/// Output:
26/// - `Ok(true)` if passwordless sudo is available, `Ok(false)` if not, or `Err(String)` on error.
27///
28/// # Errors
29///
30/// - Returns `Err` if the check cannot be executed (e.g., sudo not installed).
31///
32/// Details:
33/// - Uses `sudo -n true` to test if sudo can run without password.
34/// - `-n`: Non-interactive mode (fails if password required).
35/// - `true`: Simple command that always succeeds if sudo works.
36/// - Returns `Ok(false)` if sudo is not available or requires a password.
37/// - **Testing**: Only when `PACSEA_INTEGRATION_TEST=1` is set (test harness), the env var
38/// `PACSEA_TEST_SUDO_PASSWORDLESS` is honored: "1" = available, "0" = unavailable.
39/// In production (when `PACSEA_INTEGRATION_TEST` is not set), `PACSEA_TEST_SUDO_PASSWORDLESS`
40/// is ignored so the only way to enable passwordless sudo is via `use_passwordless_sudo` in settings.
41pub fn check_passwordless_sudo_available() -> Result<bool, String> {
42 // Honor test override only in integration test context so production never honors it
43 if is_integration_test_context()
44 && let Ok(val) = std::env::var("PACSEA_TEST_SUDO_PASSWORDLESS")
45 {
46 tracing::debug!(
47 "Using test override for passwordless sudo check: PACSEA_TEST_SUDO_PASSWORDLESS={}",
48 val
49 );
50 return Ok(val == "1");
51 }
52
53 let status = Command::new("sudo")
54 .args(["-n", "true"])
55 .stdin(std::process::Stdio::null())
56 .stdout(std::process::Stdio::null())
57 .stderr(std::process::Stdio::null())
58 .status()
59 .map_err(|e| format!("Failed to check passwordless sudo: {e}"))?;
60
61 Ok(status.success())
62}
63
64/// What: Check if passwordless sudo should be used based on settings and system availability.
65///
66/// Inputs:
67/// - `settings`: Reference to the application settings.
68///
69/// Output:
70/// - `true` if passwordless sudo should be used, `false` otherwise.
71///
72/// Details:
73/// - First checks if `use_passwordless_sudo` is enabled in settings (safety barrier).
74/// - If not enabled, returns `false` immediately without checking system availability.
75/// - If enabled, checks if passwordless sudo is actually available on the system.
76/// - Returns `true` only if both conditions are met.
77/// - Logs the decision for debugging purposes.
78/// - **Testing**: Only when `PACSEA_INTEGRATION_TEST=1` is set (test harness), the env var
79/// `PACSEA_TEST_SUDO_PASSWORDLESS` is honored. In production this override is disabled.
80#[must_use]
81pub fn should_use_passwordless_sudo(settings: &crate::theme::Settings) -> bool {
82 // Honor test override only in integration test context so production never honors it
83 if is_integration_test_context()
84 && let Ok(val) = std::env::var("PACSEA_TEST_SUDO_PASSWORDLESS")
85 {
86 tracing::debug!(
87 "Using test override for should_use_passwordless_sudo: PACSEA_TEST_SUDO_PASSWORDLESS={}",
88 val
89 );
90 return val == "1";
91 }
92
93 // Check if passwordless sudo is enabled in settings (safety barrier)
94 if !settings.use_passwordless_sudo {
95 tracing::debug!("Passwordless sudo disabled in settings, requiring password prompt");
96 return false;
97 }
98
99 // Check if passwordless sudo is available on the system
100 match check_passwordless_sudo_available() {
101 Ok(true) => {
102 tracing::info!("Passwordless sudo enabled in settings and available on system");
103 true
104 }
105 Ok(false) => {
106 tracing::debug!(
107 "Passwordless sudo enabled in settings but not available on system, requiring password prompt"
108 );
109 false
110 }
111 Err(e) => {
112 tracing::debug!(
113 "Passwordless sudo check failed ({}), requiring password prompt",
114 e
115 );
116 false
117 }
118 }
119}
120
121/// What: Validate a sudo password without executing any command.
122///
123/// Inputs:
124/// - `password`: Password to validate.
125///
126/// Output:
127/// - `Ok(true)` if password is valid, `Ok(false)` if invalid, or `Err(String)` on error.
128///
129/// # Errors
130///
131/// - Returns `Err` if the validation command cannot be executed (e.g., sudo not available).
132///
133/// Details:
134/// - First invalidates cached sudo credentials with `sudo -k` to ensure fresh validation.
135/// - Then executes `printf '%s\n' '<password>' | sudo -S -v` to test password validity.
136/// - Uses `printf` instead of `echo` for more reliable password handling.
137/// - Uses `sudo -v` which validates credentials without executing a command.
138/// - Returns `Ok(true)` if password is valid, `Ok(false)` if invalid.
139/// - Handles errors appropriately (e.g., if sudo is not available).
140pub fn validate_sudo_password(password: &str) -> Result<bool, String> {
141 use crate::install::shell_single_quote;
142
143 // Escape password for shell safety
144 let escaped_password = shell_single_quote(password);
145
146 // Build command: sudo -k ; printf '%s\n' '<password>' | sudo -S -v
147 // First, sudo -k invalidates any cached credentials to ensure fresh validation.
148 // Without this, cached credentials could cause validation to succeed even with wrong password.
149 // Use printf instead of echo for more reliable password handling.
150 // sudo -v validates credentials without executing a command.
151 let cmd = format!("sudo -k ; printf '%s\\n' {escaped_password} | sudo -S -v 2>&1");
152
153 // Execute command
154 let output = Command::new("sh")
155 .arg("-c")
156 .arg(&cmd)
157 .output()
158 .map_err(|e| format!("Failed to execute sudo validation: {e}"))?;
159
160 // Check exit code
161 // Exit code 0 means password is valid
162 // Non-zero exit code means password is invalid or other error
163 // This approach is language-independent as it relies on exit codes, not error messages
164 if output.status.success() {
165 Ok(true)
166 } else {
167 Ok(false)
168 }
169}
170
171#[cfg(test)]
172mod tests {
173 use super::*;
174
175 /// What: Check if passwordless sudo is configured (test helper).
176 ///
177 /// Inputs:
178 /// - None.
179 ///
180 /// Output:
181 /// - `true` if passwordless sudo is available, `false` otherwise.
182 ///
183 /// Details:
184 /// - Uses the public `check_passwordless_sudo_available()` function.
185 /// - Returns `false` if sudo is not available or requires a password.
186 fn is_passwordless_sudo() -> bool {
187 check_passwordless_sudo_available().unwrap_or(false)
188 }
189
190 #[test]
191 /// What: Test passwordless sudo check returns a valid result.
192 ///
193 /// Inputs:
194 /// - None.
195 ///
196 /// Output:
197 /// - Returns `Ok(bool)` without panicking.
198 ///
199 /// Details:
200 /// - Verifies the function returns a valid result (either true or false).
201 /// - Does not assert on the actual value since it depends on system configuration.
202 fn test_check_passwordless_sudo_available() {
203 let result = check_passwordless_sudo_available();
204 // Should return Ok with either true or false, depending on system config
205 assert!(result.is_ok());
206 }
207
208 #[test]
209 #[ignore = "Uses sudo with wrong password - may lock user out. Run with --ignored"]
210 /// What: Test password validation handles invalid passwords.
211 ///
212 /// Inputs:
213 /// - Invalid password string.
214 ///
215 /// Output:
216 /// - Returns `Ok(false)` for invalid password.
217 ///
218 /// Details:
219 /// - Verifies the function correctly identifies invalid passwords.
220 /// - Skips assertion if passwordless sudo is configured (common in CI).
221 /// - Marked as ignored to prevent user lockout from failed sudo attempts.
222 fn test_validate_sudo_password_invalid() {
223 // Skip test if passwordless sudo is configured (common in CI environments)
224 if is_passwordless_sudo() {
225 return;
226 }
227
228 // This test uses an obviously wrong password
229 // It should return Ok(false) without panicking
230 let result = validate_sudo_password("definitely_wrong_password_12345");
231 // Result may be Ok(false) or Err depending on system configuration
232 if let Ok(valid) = result {
233 // Should be false for invalid password
234 assert!(!valid);
235 } else {
236 // Error is acceptable (e.g., sudo not available)
237 }
238 }
239
240 #[test]
241 #[ignore = "Uses sudo with wrong password - may lock user out. Run with --ignored"]
242 /// What: Test password validation handles empty passwords.
243 ///
244 /// Inputs:
245 /// - Empty password string.
246 ///
247 /// Output:
248 /// - Returns `Ok(false)` for empty password.
249 ///
250 /// Details:
251 /// - Verifies the function correctly handles empty passwords.
252 /// - Skips assertion if passwordless sudo is configured (common in CI).
253 /// - Marked as ignored to prevent user lockout from failed sudo attempts.
254 fn test_validate_sudo_password_empty() {
255 // Skip test if passwordless sudo is configured (common in CI environments)
256 if is_passwordless_sudo() {
257 return;
258 }
259
260 let result = validate_sudo_password("");
261 // Empty password should be invalid
262 if let Ok(valid) = result {
263 assert!(!valid);
264 } else {
265 // Error is acceptable
266 }
267 }
268
269 #[test]
270 #[ignore = "Uses sudo with wrong password - may lock user out. Run with --ignored"]
271 /// What: Test password validation handles special characters.
272 ///
273 /// Inputs:
274 /// - Password with special characters that need escaping.
275 ///
276 /// Output:
277 /// - Handles special characters without panicking.
278 ///
279 /// Details:
280 /// - Verifies the function correctly escapes special characters in passwords.
281 /// - Marked as ignored to prevent user lockout from failed sudo attempts.
282 fn test_validate_sudo_password_special_chars() {
283 // Test with password containing special shell characters
284 let passwords = vec![
285 "pass'word",
286 "pass\"word",
287 "pass$word",
288 "pass`word",
289 "pass\\word",
290 ];
291 for pass in passwords {
292 let result = validate_sudo_password(pass);
293 // Just verify it doesn't panic
294 let _ = result;
295 }
296 }
297
298 #[test]
299 #[ignore = "Uses sudo with wrong password - may lock user out. Run with --ignored"]
300 /// What: Test password validation function signature.
301 ///
302 /// Inputs:
303 /// - Various password strings.
304 ///
305 /// Output:
306 /// - Returns Result<bool, String> as expected.
307 ///
308 /// Details:
309 /// - Verifies the function returns the correct type.
310 /// - Marked as ignored to prevent user lockout from failed sudo attempts.
311 fn test_validate_sudo_password_signature() {
312 let result: Result<bool, String> = validate_sudo_password("test");
313 // Verify it returns the correct type
314 let _ = result;
315 }
316}