Mercurial > crates > nonstick
view src/conv.rs @ 71:58f9d2a4df38
Reorganize everything again???
- Splits ffi/memory stuff into a bunch of stuff in the pam_ffi module.
- Builds infrastructure for passing Messages and Responses.
- Adds tests for some things at least.
author | Paul Fisher <paul@pfish.zone> |
---|---|
date | Tue, 03 Jun 2025 21:54:58 -0400 |
parents | 9f8381a1c09c |
children | 47eb242a4f88 |
line wrap: on
line source
//! The PAM conversation and associated Stuff. // Temporarily allowed until we get the actual conversation functions hooked up. #![allow(dead_code)] use crate::constants::Result; use crate::pam_ffi::Message; use secure_string::SecureString; // TODO: In most cases, we should be passing around references to strings // or binary data. Right now we don't because that turns type inference and // trait definitions/implementations into a HUGE MESS. // // Ideally, we would be using some kind of `TD: TextData` and `BD: BinaryData` // associated types in the various Conversation traits to avoid copying // when unnecessary. /// The responses that PAM will return from a request. #[derive(Debug, PartialEq, derive_more::From)] pub enum Response { /// Used to fill in list entries where there is no response expected. /// /// Used in response to: /// /// - [`Error`](Message::Error) /// - [`Info`](Message::Info) NoResponse, /// A response with text data from the user. /// /// Used in response to: /// /// - [`Prompt`](Message::Prompt) /// - [`RadioPrompt`](Message::RadioPrompt) (a Linux-PAM extension) Text(String), /// A response to a masked request with text data from the user. /// /// Used in response to: /// /// - [`MaskedPrompt`](Message::MaskedPrompt) MaskedText(SecureString), /// A response to a binary request (a Linux-PAM extension). /// /// Used in response to: /// /// - [`BinaryPrompt`](Message::BinaryPrompt) Binary(BinaryData), } /// A channel for PAM modules to request information from the user. /// /// This trait is used by both applications and PAM modules: /// /// - Applications implement Conversation and provide a user interface /// to allow the user to respond to PAM questions. /// - Modules call a Conversation implementation to request information /// or send information to the user. pub trait Conversation { /// Sends messages to the user. /// /// The returned Vec of messages always contains exactly as many entries /// as there were messages in the request; one corresponding to each. fn send(&mut self, messages: &[Message]) -> Result<Vec<Response>>; } /// Trait that an application can implement if they want to handle messages /// one at a time. pub trait DemuxedConversation { /// Prompts the user for some text. fn prompt(&mut self, request: &str) -> Result<String>; /// Prompts the user for some text, but hides their typing. fn masked_prompt(&mut self, request: &str) -> Result<SecureString>; /// Prompts the user for a radio option (a Linux-PAM extension). /// /// The Linux-PAM documentation doesn't give the format of the response. fn radio_prompt(&mut self, request: &str) -> Result<String>; /// Alerts the user to an error. fn error(&mut self, message: &str); /// Sends an informational message to the user. fn info(&mut self, message: &str); /// Requests binary data from the user (a Linux-PAM extension). fn binary_prompt(&mut self, data: &[u8], data_type: u8) -> Result<BinaryData>; } impl<D: DemuxedConversation> Conversation for D { fn send(&mut self, messages: &[Message]) -> Result<Vec<Response>> { messages .iter() .map(|msg| match *msg { Message::Prompt(prompt) => self.prompt(prompt).map(Response::from), Message::MaskedPrompt(prompt) => self.masked_prompt(prompt).map(Response::from), Message::RadioPrompt(prompt) => self.radio_prompt(prompt).map(Response::from), Message::Info(message) => { self.info(message); Ok(Response::NoResponse) } Message::Error(message) => { self.error(message); Ok(Response::NoResponse) } Message::BinaryPrompt { data_type, data } => { self.binary_prompt(data, data_type).map(Response::from) } }) .collect() } } /// Owned binary data. #[derive(Debug, PartialEq)] pub struct BinaryData { data: Vec<u8>, data_type: u8, } impl BinaryData { pub fn new(data: Vec<u8>, data_type: u8) -> Self { Self { data, data_type } } pub fn data(&self) -> &[u8] { &self.data } pub fn data_type(&self) -> u8 { self.data_type } } impl From<BinaryData> for Vec<u8> { /// Extracts the inner vector from the BinaryData. fn from(value: BinaryData) -> Self { value.data } } #[cfg(test)] mod tests { use super::{Conversation, DemuxedConversation, Message, Response, SecureString}; use crate::constants::ErrorCode; #[test] fn test_demux() { #[derive(Default)] struct DemuxTester { error_ran: bool, info_ran: bool, } impl DemuxedConversation for DemuxTester { fn prompt(&mut self, request: &str) -> crate::Result<String> { match request { "what" => Ok("whatwhat".to_owned()), "give_err" => Err(ErrorCode::PermissionDenied), _ => panic!("unexpected prompt!"), } } fn masked_prompt(&mut self, request: &str) -> crate::Result<SecureString> { assert_eq!("reveal", request); Ok(SecureString::from("my secrets")) } fn radio_prompt(&mut self, request: &str) -> crate::Result<String> { assert_eq!("channel?", request); Ok("zero".to_owned()) } fn error(&mut self, message: &str) { self.error_ran = true; assert_eq!("whoopsie", message); } fn info(&mut self, message: &str) { self.info_ran = true; assert_eq!("did you know", message); } fn binary_prompt( &mut self, data: &[u8], data_type: u8, ) -> crate::Result<super::BinaryData> { assert_eq!(&[10, 9, 8], data); assert_eq!(66, data_type); Ok(super::BinaryData::new(vec![5, 5, 5], 5)) } } let mut tester = DemuxTester::default(); assert_eq!( vec![ Response::Text("whatwhat".to_owned()), Response::MaskedText("my secrets".into()), Response::NoResponse, Response::NoResponse, ], tester .send(&[ Message::Prompt("what"), Message::MaskedPrompt("reveal"), Message::Error("whoopsie"), Message::Info("did you know"), ]) .unwrap() ); assert!(tester.error_ran); assert!(tester.info_ran); assert_eq!( ErrorCode::PermissionDenied, tester.send(&[Message::Prompt("give_err")]).unwrap_err(), ); // Test the Linux-PAM extensions separately. assert_eq!( vec![ Response::Text("zero".to_owned()), Response::Binary(super::BinaryData::new(vec![5, 5, 5], 5)), ], tester .send(&[ Message::RadioPrompt("channel?"), Message::BinaryPrompt { data: &[10, 9, 8], data_type: 66 }, ]) .unwrap() ); } }