Mercurial > crates > nonstick
view src/libpam/question.rs @ 97:efe2f5f8b5b2
Implement "stateless" application-side PAM calls.
This introduces `authenticate`, `account_management`, and `change_authtok`.
These are the three PAM operations that are stateless (i.e., they don't start
a session or modify global credentials).
author | Paul Fisher <paul@pfish.zone> |
---|---|
date | Mon, 23 Jun 2025 19:10:34 -0400 |
parents | efc2b56c8928 |
children | b87100c5eed4 |
line wrap: on
line source
//! Data and types dealing with PAM messages. #[cfg(feature = "linux-pam-extensions")] use crate::conv::{BinaryQAndA, RadioQAndA}; use crate::conv::{ErrorMsg, InfoMsg, MaskedQAndA, Message, QAndA}; use crate::libpam::conversation::OwnedMessage; use crate::libpam::memory::{CBinaryData, Immovable}; pub use crate::libpam::pam_ffi::Question; use crate::libpam::{memory, pam_ffi}; use crate::ErrorCode; use crate::Result; use num_enum::{IntoPrimitive, TryFromPrimitive}; use std::ffi::{c_void, CStr}; use std::ptr::NonNull; use std::{ptr, slice}; /// Abstraction of a collection of questions to be sent in a PAM conversation. /// /// The PAM C API conversation function looks like this: /// /// ```c /// int pam_conv( /// int count, /// const struct pam_message **questions, /// struct pam_response **answers, /// void *appdata_ptr, /// ) /// ``` /// /// On Linux-PAM and other compatible implementations, `questions` /// is treated as a pointer-to-pointers, like `int argc, char **argv`. /// (In this situation, the value of `Questions.indirect` is /// the pointer passed to `pam_conv`.) /// /// ```text /// points to ┌───────────────┐ ╔═ Question ═╗ /// questions ┄┄┄┄┄┄┄┄┄┄> │ questions[0] ┄┼┄┄┄┄> ║ style ║ /// │ questions[1] ┄┼┄┄┄╮ ║ data ┄┄┄┄┄┄╫┄┄> ... /// │ ... │ ┆ ╚════════════╝ /// ┆ /// ┆ ╔═ Question ═╗ /// ╰┄┄> ║ style ║ /// ║ data ┄┄┄┄┄┄╫┄┄> ... /// ╚════════════╝ /// ``` /// /// On OpenPAM and other compatible implementations (like Solaris), /// `messages` is a pointer-to-pointer-to-array. This appears to be /// the correct implementation as required by the XSSO specification. /// /// ```text /// points to ┌─────────────┐ ╔═ Question[] ═╗ /// questions ┄┄┄┄┄┄┄┄┄┄> │ *questions ┄┼┄┄┄┄┄> ║ style ║ /// └─────────────┘ ║ data ┄┄┄┄┄┄┄┄╫┄┄> ... /// ╟──────────────╢ /// ║ style ║ /// ║ data ┄┄┄┄┄┄┄┄╫┄┄> ... /// ╟──────────────╢ /// ║ ... ║ /// ``` pub trait QuestionsTrait { /// Allocates memory for this indirector and all its members. fn new(messages: &[Message]) -> Result<Self> where Self: Sized; /// Gets the pointer that is passed . fn ptr(&self) -> *const *const Question; /// Converts a pointer into a borrowed list of Questions. /// /// # Safety /// /// You have to provide a valid pointer. unsafe fn borrow_ptr<'a>( ptr: *const *const Question, count: usize, ) -> impl Iterator<Item = &'a Question>; } #[cfg(pam_impl = "linux-pam")] pub type Questions = LinuxPamQuestions; #[cfg(not(pam_impl = "linux-pam"))] pub type Questions = XSsoQuestions; /// The XSSO standard version of the pointer train to questions. #[derive(Debug)] #[repr(C)] pub struct XSsoQuestions { /// Points to the memory address where the meat of `questions` is. /// **The memory layout of Vec is not specified**, and we need to return /// a pointer to the pointer, hence we have to store it here. pointer: *const Question, questions: Vec<Question>, _marker: Immovable, } impl XSsoQuestions { fn len(&self) -> usize { self.questions.len() } fn iter_mut(&mut self) -> impl Iterator<Item = &mut Question> { self.questions.iter_mut() } } impl QuestionsTrait for XSsoQuestions { fn new(messages: &[Message]) -> Result<Self> { let questions: Result<Vec<_>> = messages.iter().map(Question::try_from).collect(); let questions = questions?; Ok(Self { pointer: questions.as_ptr(), questions, _marker: Default::default(), }) } fn ptr(&self) -> *const *const Question { &self.pointer as *const *const Question } unsafe fn borrow_ptr<'a>( ptr: *const *const Question, count: usize, ) -> impl Iterator<Item = &'a Question> { slice::from_raw_parts(*ptr, count).iter() } } /// The Linux version of the pointer train to questions. #[derive(Debug)] #[repr(C)] pub struct LinuxPamQuestions { #[allow(clippy::vec_box)] // we need to do this. /// The place where the questions are. questions: Vec<Box<Question>>, _marker: Immovable, } impl LinuxPamQuestions { fn len(&self) -> usize { self.questions.len() } fn iter_mut(&mut self) -> impl Iterator<Item = &mut Question> { self.questions.iter_mut().map(AsMut::as_mut) } } impl QuestionsTrait for LinuxPamQuestions { fn new(messages: &[Message]) -> Result<Self> { let questions: Result<_> = messages .iter() .map(|msg| Question::try_from(msg).map(Box::new)) .collect(); Ok(Self { questions: questions?, _marker: Default::default(), }) } fn ptr(&self) -> *const *const Question { self.questions.as_ptr().cast() } unsafe fn borrow_ptr<'a>( ptr: *const *const Question, count: usize, ) -> impl Iterator<Item = &'a Question> { slice::from_raw_parts(ptr.cast::<&Question>(), count) .iter() .copied() } } /// The C enum values for messages shown to the user. #[derive(Debug, PartialEq, TryFromPrimitive, IntoPrimitive)] #[repr(u32)] enum Style { /// Requests information from the user; will be masked when typing. PromptEchoOff = pam_ffi::PAM_PROMPT_ECHO_OFF, /// Requests information from the user; will not be masked. PromptEchoOn = pam_ffi::PAM_PROMPT_ECHO_ON, /// An error message. ErrorMsg = pam_ffi::PAM_ERROR_MSG, /// An informational message. TextInfo = pam_ffi::PAM_TEXT_INFO, /// Yes/No/Maybe conditionals. A Linux-PAM extension. #[cfg(feature = "linux-pam-extensions")] RadioType = pam_ffi::PAM_RADIO_TYPE, /// For server–client non-human interaction. /// /// NOT part of the X/Open PAM specification. /// A Linux-PAM extension. #[cfg(feature = "linux-pam-extensions")] BinaryPrompt = pam_ffi::PAM_BINARY_PROMPT, } impl Question { /// Gets this message's data pointer as a string. /// /// # Safety /// /// It's up to you to pass this only on types with a string value. unsafe fn string_data(&self) -> Result<&str> { if self.data.is_null() { Ok("") } else { CStr::from_ptr(self.data.cast()) .to_str() .map_err(|_| ErrorCode::ConversationError) } } /// Gets this message's data pointer as borrowed binary data. unsafe fn binary_data(&self) -> (&[u8], u8) { NonNull::new(self.data) .map(|nn| nn.cast()) .map(|ptr| CBinaryData::data(ptr)) .unwrap_or_default() } } impl TryFrom<&Message<'_>> for Question { type Error = ErrorCode; fn try_from(msg: &Message) -> Result<Self> { let alloc = |style, text| Ok((style, memory::malloc_str(text)?.cast())); // We will only allocate heap data if we have a valid input. let (style, data): (_, NonNull<c_void>) = match *msg { Message::MaskedPrompt(p) => alloc(Style::PromptEchoOff, p.question()), Message::Prompt(p) => alloc(Style::PromptEchoOn, p.question()), Message::Error(p) => alloc(Style::ErrorMsg, p.question()), Message::Info(p) => alloc(Style::TextInfo, p.question()), #[cfg(feature = "linux-pam-extensions")] Message::RadioPrompt(p) => alloc(Style::RadioType, p.question()), #[cfg(feature = "linux-pam-extensions")] Message::BinaryPrompt(p) => Ok(( Style::BinaryPrompt, CBinaryData::alloc(p.question())?.cast(), )), #[cfg(not(feature = "linux-pam-extensions"))] Message::RadioPrompt(_) | Message::BinaryPrompt(_) => Err(ErrorCode::ConversationError), }?; Ok(Self { style: style.into(), data: data.as_ptr(), _marker: Default::default(), }) } } impl Drop for Question { fn drop(&mut self) { // SAFETY: We either created this data or we got it from PAM. // After this function is done, it will be zeroed out. unsafe { // This is nice-to-have. We'll try to zero out the data // in the Question. If it's not a supported format, we skip it. if let Ok(style) = Style::try_from(self.style) { match style { #[cfg(feature = "linux-pam-extensions")] Style::BinaryPrompt => { if let Some(d) = NonNull::new(self.data) { CBinaryData::zero_contents(d.cast()) } } #[cfg(feature = "linux-pam-extensions")] Style::RadioType => memory::zero_c_string(self.data.cast()), Style::TextInfo | Style::ErrorMsg | Style::PromptEchoOff | Style::PromptEchoOn => memory::zero_c_string(self.data.cast()), } }; memory::free(self.data); self.data = ptr::null_mut(); } } } impl<'a> TryFrom<&'a Question> for OwnedMessage<'a> { type Error = ErrorCode; fn try_from(question: &'a Question) -> Result<Self> { let style: Style = question .style .try_into() .map_err(|_| ErrorCode::ConversationError)?; // SAFETY: In all cases below, we're creating questions based on // known types that we get from PAM and the inner types it should have. let prompt = unsafe { match style { Style::PromptEchoOff => { Self::MaskedPrompt(MaskedQAndA::new(question.string_data()?)) } Style::PromptEchoOn => Self::Prompt(QAndA::new(question.string_data()?)), Style::ErrorMsg => Self::Error(ErrorMsg::new(question.string_data()?)), Style::TextInfo => Self::Info(InfoMsg::new(question.string_data()?)), #[cfg(feature = "linux-pam-extensions")] Style::RadioType => Self::RadioPrompt(RadioQAndA::new(question.string_data()?)), #[cfg(feature = "linux-pam-extensions")] Style::BinaryPrompt => Self::BinaryPrompt(BinaryQAndA::new(question.binary_data())), } }; Ok(prompt) } } #[cfg(test)] mod tests { macro_rules! assert_matches { ($id:ident => $variant:path, $q:expr) => { if let $variant($id) = $id { assert_eq!($q, $id.question()); } else { panic!("mismatched enum variant {x:?}", x = $id); } }; } macro_rules! tests { ($fn_name:ident<$typ:ident>) => { mod $fn_name { use super::super::*; #[test] fn standard() { let interrogation = <$typ>::new(&[ MaskedQAndA::new("hocus pocus").message(), QAndA::new("what").message(), QAndA::new("who").message(), InfoMsg::new("hey").message(), ErrorMsg::new("gasp").message(), ]) .unwrap(); let indirect = interrogation.ptr(); let remade = unsafe { $typ::borrow_ptr(indirect, interrogation.len()) }; let messages: Vec<OwnedMessage> = remade .map(TryInto::try_into) .collect::<Result<_>>() .unwrap(); let [masked, what, who, hey, gasp] = messages.try_into().unwrap(); assert_matches!(masked => OwnedMessage::MaskedPrompt, "hocus pocus"); assert_matches!(what => OwnedMessage::Prompt, "what"); assert_matches!(who => OwnedMessage::Prompt, "who"); assert_matches!(hey => OwnedMessage::Info, "hey"); assert_matches!(gasp => OwnedMessage::Error, "gasp"); } #[test] #[cfg(not(feature = "linux-pam-extensions"))] fn no_linux_extensions() { use crate::conv::{BinaryQAndA, RadioQAndA}; <$typ>::new(&[ BinaryQAndA::new((&[5, 4, 3, 2, 1], 66)).message(), RadioQAndA::new("you must choose").message(), ]).unwrap_err(); } #[test] #[cfg(feature = "linux-pam-extensions")] fn linux_extensions() { let interrogation = <$typ>::new(&[ BinaryQAndA::new((&[5, 4, 3, 2, 1], 66)).message(), RadioQAndA::new("you must choose").message(), ]).unwrap(); let indirect = interrogation.ptr(); let remade = unsafe { $typ::borrow_ptr(indirect, interrogation.len()) }; let messages: Vec<OwnedMessage> = remade .map(TryInto::try_into) .collect::<Result<_>>() .unwrap(); let [bin, choose] = messages.try_into().unwrap(); assert_matches!(bin => OwnedMessage::BinaryPrompt, (&[5, 4, 3, 2, 1][..], 66)); assert_matches!(choose => OwnedMessage::RadioPrompt, "you must choose"); } } }} tests!(test_xsso<XSsoQuestions>); tests!(test_linux<LinuxPamQuestions>); }