@@ -664,6 +664,8 @@ mod tests { |
| 664 | 664 | plain_response: PromptResponse, |
| 665 | 665 | infos: Vec<String>, |
| 666 | 666 | errors: Vec<String>, |
| 667 | + success_count: usize, |
| 668 | + failure_messages: Vec<String>, |
| 667 | 669 | } |
| 668 | 670 | |
| 669 | 671 | impl Default for FakePrompt { |
@@ -673,6 +675,8 @@ mod tests { |
| 673 | 675 | plain_response: PromptResponse::Canceled, |
| 674 | 676 | infos: Vec::new(), |
| 675 | 677 | errors: Vec::new(), |
| 678 | + success_count: 0, |
| 679 | + failure_messages: Vec::new(), |
| 676 | 680 | } |
| 677 | 681 | } |
| 678 | 682 | } |
@@ -695,6 +699,16 @@ mod tests { |
| 695 | 699 | self.infos.push(message.to_string()); |
| 696 | 700 | Ok(()) |
| 697 | 701 | } |
| 702 | + |
| 703 | + fn auth_succeeded(&mut self) -> Result<()> { |
| 704 | + self.success_count += 1; |
| 705 | + Ok(()) |
| 706 | + } |
| 707 | + |
| 708 | + fn auth_failed(&mut self, message: &str) -> Result<()> { |
| 709 | + self.failure_messages.push(message.to_string()); |
| 710 | + Ok(()) |
| 711 | + } |
| 698 | 712 | } |
| 699 | 713 | |
| 700 | 714 | fn temp_socket_path() -> PathBuf { |
@@ -818,15 +832,15 @@ mod tests { |
| 818 | 832 | let client = HelperSocketClient::new(&socket_path); |
| 819 | 833 | let mut prompts = FakePrompt { |
| 820 | 834 | secret_response: PromptResponse::Submitted("correct horse".to_string()), |
| 821 | | - plain_response: PromptResponse::Canceled, |
| 822 | | - infos: Vec::new(), |
| 823 | | - errors: Vec::new(), |
| 835 | + ..FakePrompt::default() |
| 824 | 836 | }; |
| 825 | 837 | |
| 826 | 838 | let result = client |
| 827 | 839 | .authenticate("alice", "cookie-123", &mut prompts) |
| 828 | 840 | .expect("client auth"); |
| 829 | 841 | assert_eq!(result, HelperOutcome::Authorized); |
| 842 | + assert_eq!(prompts.success_count, 1); |
| 843 | + assert!(prompts.failure_messages.is_empty()); |
| 830 | 844 | server.join().expect("server join"); |
| 831 | 845 | |
| 832 | 846 | let lines = transcript.lock().expect("lock transcript"); |
@@ -861,9 +875,7 @@ mod tests { |
| 861 | 875 | let client = HelperSocketClient::new(&socket_path); |
| 862 | 876 | let mut prompts = FakePrompt { |
| 863 | 877 | secret_response: PromptResponse::TimedOut, |
| 864 | | - plain_response: PromptResponse::Canceled, |
| 865 | | - infos: Vec::new(), |
| 866 | | - errors: Vec::new(), |
| 878 | + ..FakePrompt::default() |
| 867 | 879 | }; |
| 868 | 880 | |
| 869 | 881 | let outcome = client |
@@ -875,6 +887,103 @@ mod tests { |
| 875 | 887 | let _ = std::fs::remove_file(&socket_path); |
| 876 | 888 | } |
| 877 | 889 | |
| 890 | + #[test] |
| 891 | + fn helper_client_reports_failure_callback_for_denied_attempt() { |
| 892 | + let socket_path = temp_socket_path(); |
| 893 | + let listener = UnixListener::bind(&socket_path).expect("bind test socket"); |
| 894 | + |
| 895 | + let server = thread::spawn(move || { |
| 896 | + let (mut stream, _) = listener.accept().expect("accept"); |
| 897 | + let read_stream = stream.try_clone().expect("clone"); |
| 898 | + let mut reader = BufReader::new(read_stream); |
| 899 | + |
| 900 | + let mut first_line = String::new(); |
| 901 | + reader.read_line(&mut first_line).expect("read first line"); |
| 902 | + if first_line.trim() == "alice" { |
| 903 | + let mut cookie = String::new(); |
| 904 | + reader.read_line(&mut cookie).expect("read cookie"); |
| 905 | + } |
| 906 | + |
| 907 | + stream |
| 908 | + .write_all(b"PAM_PROMPT_ECHO_OFF Password:\n") |
| 909 | + .expect("write prompt"); |
| 910 | + stream.flush().expect("flush prompt"); |
| 911 | + |
| 912 | + let mut secret = String::new(); |
| 913 | + reader.read_line(&mut secret).expect("read secret"); |
| 914 | + |
| 915 | + stream.write_all(b"FAILURE\n").expect("write failure"); |
| 916 | + stream.flush().expect("flush failure"); |
| 917 | + }); |
| 918 | + |
| 919 | + let client = HelperSocketClient::new(&socket_path); |
| 920 | + let mut prompts = FakePrompt { |
| 921 | + secret_response: PromptResponse::Submitted("wrong horse".to_string()), |
| 922 | + ..FakePrompt::default() |
| 923 | + }; |
| 924 | + |
| 925 | + let outcome = client |
| 926 | + .authenticate("alice", "cookie-failure", &mut prompts) |
| 927 | + .expect("authenticate failure"); |
| 928 | + assert_eq!(outcome, HelperOutcome::Denied); |
| 929 | + assert_eq!(prompts.success_count, 0); |
| 930 | + assert_eq!(prompts.failure_messages, vec!["Authentication failed"]); |
| 931 | + |
| 932 | + server.join().expect("server join"); |
| 933 | + let _ = std::fs::remove_file(&socket_path); |
| 934 | + } |
| 935 | + |
| 936 | + #[test] |
| 937 | + fn helper_client_allows_success_after_diagnostic_error_line() { |
| 938 | + let socket_path = temp_socket_path(); |
| 939 | + let listener = UnixListener::bind(&socket_path).expect("bind test socket"); |
| 940 | + |
| 941 | + let server = thread::spawn(move || { |
| 942 | + let (mut stream, _) = listener.accept().expect("accept"); |
| 943 | + let read_stream = stream.try_clone().expect("clone"); |
| 944 | + let mut reader = BufReader::new(read_stream); |
| 945 | + |
| 946 | + let mut first_line = String::new(); |
| 947 | + reader.read_line(&mut first_line).expect("read first line"); |
| 948 | + if first_line.trim() == "alice" { |
| 949 | + let mut cookie = String::new(); |
| 950 | + reader.read_line(&mut cookie).expect("read cookie"); |
| 951 | + } |
| 952 | + |
| 953 | + stream |
| 954 | + .write_all(b"PAM_PROMPT_ECHO_OFF Password:\n") |
| 955 | + .expect("write prompt"); |
| 956 | + stream.flush().expect("flush prompt"); |
| 957 | + |
| 958 | + let mut secret = String::new(); |
| 959 | + reader.read_line(&mut secret).expect("read secret"); |
| 960 | + |
| 961 | + stream |
| 962 | + .write_all(b"PAM_ERROR_MSG previous attempt failed\n") |
| 963 | + .expect("write helper diagnostic"); |
| 964 | + stream.flush().expect("flush helper diagnostic"); |
| 965 | + stream.write_all(b"SUCCESS\n").expect("write success"); |
| 966 | + stream.flush().expect("flush success"); |
| 967 | + }); |
| 968 | + |
| 969 | + let client = HelperSocketClient::new(&socket_path); |
| 970 | + let mut prompts = FakePrompt { |
| 971 | + secret_response: PromptResponse::Submitted("correct horse".to_string()), |
| 972 | + ..FakePrompt::default() |
| 973 | + }; |
| 974 | + |
| 975 | + let outcome = client |
| 976 | + .authenticate("alice", "cookie-success", &mut prompts) |
| 977 | + .expect("authenticate success"); |
| 978 | + assert_eq!(outcome, HelperOutcome::Authorized); |
| 979 | + assert_eq!(prompts.success_count, 1); |
| 980 | + assert!(prompts.failure_messages.is_empty()); |
| 981 | + assert_eq!(prompts.errors, vec!["previous attempt failed"]); |
| 982 | + |
| 983 | + server.join().expect("server join"); |
| 984 | + let _ = std::fs::remove_file(&socket_path); |
| 985 | + } |
| 986 | + |
| 878 | 987 | #[test] |
| 879 | 988 | fn scrub_string_clears_input() { |
| 880 | 989 | let mut value = "top-secret".to_string(); |