Mercurial > crates > nonstick
view src/conv.rs @ 73:ac6881304c78
Do conversations, along with way too much stuff.
This implements conversations, along with all the memory management
brouhaha that goes along with it. The conversation now lives directly
on the handle rather than being a thing you have to get from it
and then call manually. It Turns Out this makes things a lot easier!
I guess we reorganized things again. For the last time. For real.
I promise.
This all passes ASAN, so it seems Pretty Good!
author | Paul Fisher <paul@pfish.zone> |
---|---|
date | Thu, 05 Jun 2025 03:41:38 -0400 |
parents | 47eb242a4f88 |
children | c7c596e6388f |
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::ErrorCode; 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 types of message and request that can be sent to a user. /// /// The data within each enum value is the prompt (or other information) /// that will be presented to the user. #[derive(Clone, Copy, Debug)] pub enum Message<'a> { /// Requests information from the user; will be masked when typing. /// /// Response: [`MaskedText`](Response::MaskedText) MaskedPrompt(&'a str), /// Requests information from the user; will not be masked. /// /// Response: [`Text`](Response::Text) Prompt(&'a str), /// "Yes/No/Maybe conditionals" (a Linux-PAM extension). /// /// Response: [`Text`](Response::Text) /// (Linux-PAM documentation doesn't define its contents.) RadioPrompt(&'a str), /// Raises an error message to the user. /// /// Response: [`NoResponse`](Response::NoResponse) Error(&'a str), /// Sends an informational message to the user. /// /// Response: [`NoResponse`](Response::NoResponse) Info(&'a str), /// Requests binary data from the client (a Linux-PAM extension). /// /// This is used for non-human or non-keyboard prompts (security key?). /// NOT part of the X/Open PAM specification. /// /// Response: [`Binary`](Response::Binary) BinaryPrompt { /// Some binary data. data: &'a [u8], /// A "type" that you can use for signalling. Has no strict definition in PAM. data_type: u8, }, } /// 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), } /// The function type for a conversation. /// /// A macro to save typing `FnMut(&[Message]) -> Result<Vec<Response>>`. #[macro_export] macro_rules! conv_type { () => {FnMut(&[Message]) -> Result<Vec<Response>>}; (impl) => { impl FnMut(&[Message]) -> Result<Vec<Response>> } } /// 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 converse(&mut self, messages: &[Message]) -> Result<Vec<Response>>; } fn conversation_func(func: conv_type!(impl)) -> impl Conversation { Convo(func) } struct Convo<C: FnMut(&[Message]) -> Result<Vec<Response>>>(C); impl<C: FnMut(&[Message]) -> Result<Vec<Response>>> Conversation for Convo<C> { fn converse(&mut self, messages: &[Message]) -> Result<Vec<Response>> { self.0(messages) } } /// Provides methods to make it easier to send exactly one message. /// /// This is primarily used by PAM modules, so that a module that only needs /// one piece of information at a time doesn't have a ton of boilerplate. /// You may also find it useful for testing PAM application libraries. /// /// ``` /// # use nonstick::{PamHandleModule, Conversation, Result}; /// # fn _do_test(mut pam_handle: impl PamHandleModule) -> Result<()> { /// use nonstick::ConversationMux; /// /// let token = pam_handle.masked_prompt("enter your one-time token")?; /// # Ok(()) /// # } pub trait ConversationMux { /// Prompts the user for something. fn prompt(&mut self, request: &str) -> Result<String>; /// Prompts the user for something, but hides what the user types. fn masked_prompt(&mut self, request: &str) -> Result<SecureString>; /// Prompts the user for a yes/no/maybe conditional (a Linux-PAM extension). /// /// PAM documentation doesn't define 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<C: Conversation> ConversationMux for C { /// Prompts the user for something. fn prompt(&mut self, request: &str) -> Result<String> { let resp = self.converse(&[Message::Prompt(request)])?.pop(); match resp { Some(Response::Text(s)) => Ok(s), _ => Err(ErrorCode::ConversationError), } } /// Prompts the user for something, but hides what the user types. fn masked_prompt(&mut self, request: &str) -> Result<SecureString> { let resp = self.converse(&[Message::MaskedPrompt(request)])?.pop(); match resp { Some(Response::MaskedText(s)) => Ok(s), _ => Err(ErrorCode::ConversationError), } } /// Prompts the user for a yes/no/maybe conditional (a Linux-PAM extension). /// /// PAM documentation doesn't define the format of the response. fn radio_prompt(&mut self, request: &str) -> Result<String> { let resp = self.converse(&[Message::RadioPrompt(request)])?.pop(); match resp { Some(Response::Text(s)) => Ok(s), _ => Err(ErrorCode::ConversationError), } } /// Alerts the user to an error. fn error(&mut self, message: &str) { let _ = self.converse(&[Message::Error(message)]); } /// Sends an informational message to the user. fn info(&mut self, message: &str) { let _ = self.converse(&[Message::Info(message)]); } /// Requests binary data from the user (a Linux-PAM extension). fn binary_prompt(&mut self, data: &[u8], data_type: u8) -> Result<BinaryData> { let resp = self .converse(&[Message::BinaryPrompt { data, data_type }])? .pop(); match resp { Some(Response::Binary(d)) => Ok(d), _ => Err(ErrorCode::ConversationError), } } } /// 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<DM> Conversation for DM where DM: DemuxedConversation, { fn converse(&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 .converse(&[ 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.converse(&[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 .converse(&[ Message::RadioPrompt("channel?"), Message::BinaryPrompt { data: &[10, 9, 8], data_type: 66 }, ]) .unwrap() ); } #[test] fn test_mux() { use super::ConversationMux; struct MuxTester; impl Conversation for MuxTester { fn converse(&mut self, messages: &[Message]) -> crate::Result<Vec<Response>> { if let [msg] = messages { match msg { Message::Info(info) => { assert_eq!("let me tell you", *info); Ok(vec![Response::NoResponse]) } Message::Error(error) => { assert_eq!("oh no", *error); Ok(vec![Response::NoResponse]) } Message::Prompt("should_error") => Err(ErrorCode::BufferError), Message::Prompt(ask) => { assert_eq!("question", *ask); Ok(vec![Response::Text("answer".to_owned())]) } Message::MaskedPrompt("return_wrong_type") => { Ok(vec![Response::NoResponse]) } Message::MaskedPrompt(ask) => { assert_eq!("password!", *ask); Ok(vec![Response::MaskedText(SecureString::from( "open sesame", ))]) } Message::BinaryPrompt { data, data_type } => { assert_eq!(&[1, 2, 3], data); assert_eq!(69, *data_type); Ok(vec![Response::Binary(super::BinaryData::new( vec![3, 2, 1], 42, ))]) } Message::RadioPrompt(ask) => { assert_eq!("radio?", *ask); Ok(vec![Response::Text("yes".to_owned())]) } } } else { panic!("messages is the wrong size ({len})", len = messages.len()) } } } let mut tester = MuxTester; assert_eq!("answer", tester.prompt("question").unwrap()); assert_eq!( SecureString::from("open sesame"), tester.masked_prompt("password!").unwrap() ); tester.error("oh no"); tester.info("let me tell you"); { assert_eq!("yes", tester.radio_prompt("radio?").unwrap()); assert_eq!( super::BinaryData::new(vec![3, 2, 1], 42), tester.binary_prompt(&[1, 2, 3], 69).unwrap(), ) } assert_eq!( ErrorCode::BufferError, tester.prompt("should_error").unwrap_err(), ); assert_eq!( ErrorCode::ConversationError, tester.masked_prompt("return_wrong_type").unwrap_err() ) } }