Mercurial > crates > nonstick
comparison src/libpam/question.rs @ 130:80c07e5ab22f
Transfer over (almost) completely to using libpam-sys.
This reimplements everything in nonstick on top of the new -sys crate.
We don't yet use libpam-sys's helpers for binary message payloads. Soon.
| author | Paul Fisher <paul@pfish.zone> |
|---|---|
| date | Tue, 01 Jul 2025 06:11:43 -0400 |
| parents | 178310336596 |
| children | 33b9622ed6d2 |
comparison
equal
deleted
inserted
replaced
| 129:5b2de52dd8b2 | 130:80c07e5ab22f |
|---|---|
| 1 //! Data and types dealing with PAM messages. | 1 //! Data and types dealing with PAM messages. |
| 2 | 2 |
| 3 #[cfg(feature = "linux-pam-ext")] | 3 #[cfg(feature = "linux-pam-ext")] |
| 4 use crate::conv::{BinaryQAndA, RadioQAndA}; | 4 use crate::conv::{BinaryQAndA, RadioQAndA}; |
| 5 use crate::conv::{ErrorMsg, InfoMsg, MaskedQAndA, Message, QAndA}; | 5 use crate::conv::{ErrorMsg, Exchange, InfoMsg, MaskedQAndA, QAndA}; |
| 6 use crate::libpam::conversation::OwnedMessage; | 6 use crate::libpam::conversation::OwnedExchange; |
| 7 use crate::libpam::memory::{CBinaryData, CHeapBox, CHeapString, Immovable}; | 7 use crate::libpam::memory::{CBinaryData, CHeapBox, CHeapString}; |
| 8 use crate::libpam::pam_ffi; | |
| 9 pub use crate::libpam::pam_ffi::Question; | |
| 10 use crate::ErrorCode; | 8 use crate::ErrorCode; |
| 11 use crate::Result; | 9 use crate::Result; |
| 12 use num_enum::{IntoPrimitive, TryFromPrimitive}; | 10 use num_enum::{IntoPrimitive, TryFromPrimitive}; |
| 13 use std::cell::Cell; | 11 use std::ffi::{c_int, c_void, CStr}; |
| 14 use std::ffi::{c_void, CStr}; | 12 |
| 15 use std::pin::Pin; | 13 mod style_const { |
| 16 use std::{ptr, slice}; | 14 pub use libpam_sys::*; |
| 17 | 15 #[cfg(not(feature = "link"))] |
| 18 /// Abstraction of a collection of questions to be sent in a PAM conversation. | 16 #[cfg_pam_impl(not("LinuxPam"))] |
| 19 /// | 17 pub const PAM_RADIO_TYPE: i32 = 897; |
| 20 /// The PAM C API conversation function looks like this: | 18 #[cfg(not(feature = "link"))] |
| 21 /// | 19 #[cfg_pam_impl(not("LinuxPam"))] |
| 22 /// ```c | 20 pub const PAM_BINARY_PROMPT: i32 = 10010101; |
| 23 /// int pam_conv( | |
| 24 /// int count, | |
| 25 /// const struct pam_message **questions, | |
| 26 /// struct pam_response **answers, | |
| 27 /// void *appdata_ptr, | |
| 28 /// ) | |
| 29 /// ``` | |
| 30 /// | |
| 31 /// On Linux-PAM and other compatible implementations, `questions` | |
| 32 /// is treated as a pointer-to-pointers, like `int argc, char **argv`. | |
| 33 /// (In this situation, the value of `Questions.indirect` is | |
| 34 /// the pointer passed to `pam_conv`.) | |
| 35 /// | |
| 36 /// ```text | |
| 37 /// points to ┌───────────────┐ ╔═ Question ═╗ | |
| 38 /// questions ┄┄┄┄┄┄┄┄┄┄> │ questions[0] ┄┼┄┄┄┄> ║ style ║ | |
| 39 /// │ questions[1] ┄┼┄┄┄╮ ║ data ┄┄┄┄┄┄╫┄┄> ... | |
| 40 /// │ ... │ ┆ ╚════════════╝ | |
| 41 /// ┆ | |
| 42 /// ┆ ╔═ Question ═╗ | |
| 43 /// ╰┄┄> ║ style ║ | |
| 44 /// ║ data ┄┄┄┄┄┄╫┄┄> ... | |
| 45 /// ╚════════════╝ | |
| 46 /// ``` | |
| 47 /// | |
| 48 /// On OpenPAM and other compatible implementations (like Solaris), | |
| 49 /// `messages` is a pointer-to-pointer-to-array. This appears to be | |
| 50 /// the correct implementation as required by the XSSO specification. | |
| 51 /// | |
| 52 /// ```text | |
| 53 /// points to ┌─────────────┐ ╔═ Question[] ═╗ | |
| 54 /// questions ┄┄┄┄┄┄┄┄┄┄> │ *questions ┄┼┄┄┄┄┄> ║ style ║ | |
| 55 /// └─────────────┘ ║ data ┄┄┄┄┄┄┄┄╫┄┄> ... | |
| 56 /// ╟──────────────╢ | |
| 57 /// ║ style ║ | |
| 58 /// ║ data ┄┄┄┄┄┄┄┄╫┄┄> ... | |
| 59 /// ╟──────────────╢ | |
| 60 /// ║ ... ║ | |
| 61 /// ``` | |
| 62 pub trait QuestionsTrait { | |
| 63 /// Allocates memory for this indirector and all its members. | |
| 64 fn new(messages: &[Message]) -> Result<Self> | |
| 65 where | |
| 66 Self: Sized; | |
| 67 | |
| 68 /// Gets the pointer that is passed . | |
| 69 fn ptr(self: Pin<&Self>) -> *const *const Question; | |
| 70 | |
| 71 /// Converts a pointer into a borrowed list of Questions. | |
| 72 /// | |
| 73 /// # Safety | |
| 74 /// | |
| 75 /// You have to provide a valid pointer. | |
| 76 unsafe fn borrow_ptr<'a>( | |
| 77 ptr: *const *const Question, | |
| 78 count: usize, | |
| 79 ) -> impl Iterator<Item = &'a Question>; | |
| 80 } | |
| 81 | |
| 82 #[cfg(pam_impl = "linux-pam")] | |
| 83 pub type Questions = LinuxPamQuestions; | |
| 84 | |
| 85 #[cfg(not(pam_impl = "linux-pam"))] | |
| 86 pub type Questions = XSsoQuestions; | |
| 87 | |
| 88 /// The XSSO standard version of the pointer train to questions. | |
| 89 #[derive(Debug)] | |
| 90 #[repr(C)] | |
| 91 pub struct XSsoQuestions { | |
| 92 /// Points to the memory address where the meat of `questions` is. | |
| 93 /// **The memory layout of Vec is not specified**, and we need to return | |
| 94 /// a pointer to the pointer, hence we have to store it here. | |
| 95 pointer: Cell<*const Question>, | |
| 96 questions: Vec<Question>, | |
| 97 _marker: Immovable, | |
| 98 } | |
| 99 | |
| 100 impl XSsoQuestions { | |
| 101 fn len(&self) -> usize { | |
| 102 self.questions.len() | |
| 103 } | |
| 104 fn iter_mut(&mut self) -> impl Iterator<Item = &mut Question> { | |
| 105 self.questions.iter_mut() | |
| 106 } | |
| 107 } | |
| 108 | |
| 109 impl QuestionsTrait for XSsoQuestions { | |
| 110 fn new(messages: &[Message]) -> Result<Self> { | |
| 111 let questions: Result<Vec<_>> = messages.iter().map(Question::try_from).collect(); | |
| 112 let questions = questions?; | |
| 113 Ok(Self { | |
| 114 pointer: Cell::new(ptr::null()), | |
| 115 questions, | |
| 116 _marker: Default::default(), | |
| 117 }) | |
| 118 } | |
| 119 | |
| 120 fn ptr(self: Pin<&Self>) -> *const *const Question { | |
| 121 let me = self.get_ref(); | |
| 122 me.pointer.set(self.questions.as_ptr()); | |
| 123 me.pointer.as_ptr() | |
| 124 } | |
| 125 | |
| 126 unsafe fn borrow_ptr<'a>( | |
| 127 ptr: *const *const Question, | |
| 128 count: usize, | |
| 129 ) -> impl Iterator<Item = &'a Question> { | |
| 130 slice::from_raw_parts(*ptr, count).iter() | |
| 131 } | |
| 132 } | |
| 133 | |
| 134 /// The Linux version of the pointer train to questions. | |
| 135 #[derive(Debug)] | |
| 136 #[repr(C)] | |
| 137 pub struct LinuxPamQuestions { | |
| 138 #[allow(clippy::vec_box)] // we need to box vec items. | |
| 139 /// The place where the questions are. | |
| 140 questions: Vec<Box<Question>>, | |
| 141 } | |
| 142 | |
| 143 impl LinuxPamQuestions { | |
| 144 fn len(&self) -> usize { | |
| 145 self.questions.len() | |
| 146 } | |
| 147 | |
| 148 fn iter_mut(&mut self) -> impl Iterator<Item = &mut Question> { | |
| 149 self.questions.iter_mut().map(AsMut::as_mut) | |
| 150 } | |
| 151 } | |
| 152 | |
| 153 impl QuestionsTrait for LinuxPamQuestions { | |
| 154 fn new(messages: &[Message]) -> Result<Self> { | |
| 155 let questions: Result<_> = messages | |
| 156 .iter() | |
| 157 .map(|msg| Question::try_from(msg).map(Box::new)) | |
| 158 .collect(); | |
| 159 Ok(Self { | |
| 160 questions: questions?, | |
| 161 }) | |
| 162 } | |
| 163 | |
| 164 fn ptr(self: Pin<&Self>) -> *const *const Question { | |
| 165 self.questions.as_ptr().cast() | |
| 166 } | |
| 167 | |
| 168 unsafe fn borrow_ptr<'a>( | |
| 169 ptr: *const *const Question, | |
| 170 count: usize, | |
| 171 ) -> impl Iterator<Item = &'a Question> { | |
| 172 slice::from_raw_parts(ptr.cast::<&Question>(), count) | |
| 173 .iter() | |
| 174 .copied() | |
| 175 } | |
| 176 } | 21 } |
| 177 | 22 |
| 178 /// The C enum values for messages shown to the user. | 23 /// The C enum values for messages shown to the user. |
| 179 #[derive(Debug, PartialEq, TryFromPrimitive, IntoPrimitive)] | 24 #[derive(Debug, PartialEq, TryFromPrimitive, IntoPrimitive)] |
| 180 #[repr(i32)] | 25 #[repr(i32)] |
| 181 enum Style { | 26 enum Style { |
| 182 /// Requests information from the user; will be masked when typing. | 27 /// Requests information from the user; will be masked when typing. |
| 183 PromptEchoOff = pam_ffi::PAM_PROMPT_ECHO_OFF, | 28 PromptEchoOff = style_const::PAM_PROMPT_ECHO_OFF, |
| 184 /// Requests information from the user; will not be masked. | 29 /// Requests information from the user; will not be masked. |
| 185 PromptEchoOn = pam_ffi::PAM_PROMPT_ECHO_ON, | 30 PromptEchoOn = style_const::PAM_PROMPT_ECHO_ON, |
| 186 /// An error message. | 31 /// An error message. |
| 187 ErrorMsg = pam_ffi::PAM_ERROR_MSG, | 32 ErrorMsg = style_const::PAM_ERROR_MSG, |
| 188 /// An informational message. | 33 /// An informational message. |
| 189 TextInfo = pam_ffi::PAM_TEXT_INFO, | 34 TextInfo = style_const::PAM_TEXT_INFO, |
| 190 /// Yes/No/Maybe conditionals. A Linux-PAM extension. | 35 /// Yes/No/Maybe conditionals. A Linux-PAM extension. |
| 191 #[cfg(feature = "linux-pam-ext")] | 36 #[cfg(feature = "linux-pam-ext")] |
| 192 RadioType = pam_ffi::PAM_RADIO_TYPE, | 37 RadioType = style_const::PAM_RADIO_TYPE, |
| 193 /// For server–client non-human interaction. | 38 /// For server–client non-human interaction. |
| 194 /// | 39 /// |
| 195 /// NOT part of the X/Open PAM specification. | 40 /// NOT part of the X/Open PAM specification. |
| 196 /// A Linux-PAM extension. | 41 /// A Linux-PAM extension. |
| 197 #[cfg(feature = "linux-pam-ext")] | 42 #[cfg(feature = "linux-pam-ext")] |
| 198 BinaryPrompt = pam_ffi::PAM_BINARY_PROMPT, | 43 BinaryPrompt = style_const::PAM_BINARY_PROMPT, |
| 44 } | |
| 45 | |
| 46 /// A question sent by PAM or a module to an application. | |
| 47 /// | |
| 48 /// PAM refers to this as a "message", but we call it a question | |
| 49 /// to avoid confusion with [`Message`](crate::conv::Exchange). | |
| 50 /// | |
| 51 /// This question, and its internal data, is owned by its creator | |
| 52 /// (either the module or PAM itself). | |
| 53 #[repr(C)] | |
| 54 #[derive(Debug)] | |
| 55 pub struct Question { | |
| 56 /// The style of message to request. | |
| 57 pub style: c_int, | |
| 58 /// A description of the data requested. | |
| 59 /// | |
| 60 /// For most requests, this will be an owned [`CStr`], | |
| 61 /// but for requests with style `PAM_BINARY_PROMPT`, | |
| 62 /// this will be `CBinaryData` (a Linux-PAM extension). | |
| 63 pub data: Option<CHeapBox<c_void>>, | |
| 199 } | 64 } |
| 200 | 65 |
| 201 impl Question { | 66 impl Question { |
| 202 /// Gets this message's data pointer as a string. | 67 /// Gets this message's data pointer as a string. |
| 203 /// | 68 /// |
| 220 .map(|data| CBinaryData::data(CHeapBox::as_ptr(data).cast())) | 85 .map(|data| CBinaryData::data(CHeapBox::as_ptr(data).cast())) |
| 221 .unwrap_or_default() | 86 .unwrap_or_default() |
| 222 } | 87 } |
| 223 } | 88 } |
| 224 | 89 |
| 225 impl TryFrom<&Message<'_>> for Question { | 90 impl TryFrom<&Exchange<'_>> for Question { |
| 226 type Error = ErrorCode; | 91 type Error = ErrorCode; |
| 227 fn try_from(msg: &Message) -> Result<Self> { | 92 fn try_from(msg: &Exchange) -> Result<Self> { |
| 228 let alloc = |style, text| -> Result<_> { | 93 let alloc = |style, text| -> Result<_> { |
| 229 Ok((style, unsafe { | 94 Ok((style, unsafe { |
| 230 CHeapBox::cast(CHeapString::new(text)?.into_box()) | 95 CHeapBox::cast(CHeapString::new(text)?.into_box()) |
| 231 })) | 96 })) |
| 232 }; | 97 }; |
| 233 // We will only allocate heap data if we have a valid input. | 98 // We will only allocate heap data if we have a valid input. |
| 234 let (style, data): (_, CHeapBox<c_void>) = match *msg { | 99 let (style, data): (_, CHeapBox<c_void>) = match *msg { |
| 235 Message::MaskedPrompt(p) => alloc(Style::PromptEchoOff, p.question()), | 100 Exchange::MaskedPrompt(p) => alloc(Style::PromptEchoOff, p.question()), |
| 236 Message::Prompt(p) => alloc(Style::PromptEchoOn, p.question()), | 101 Exchange::Prompt(p) => alloc(Style::PromptEchoOn, p.question()), |
| 237 Message::Error(p) => alloc(Style::ErrorMsg, p.question()), | 102 Exchange::Error(p) => alloc(Style::ErrorMsg, p.question()), |
| 238 Message::Info(p) => alloc(Style::TextInfo, p.question()), | 103 Exchange::Info(p) => alloc(Style::TextInfo, p.question()), |
| 239 #[cfg(feature = "linux-pam-ext")] | 104 #[cfg(feature = "linux-pam-ext")] |
| 240 Message::RadioPrompt(p) => alloc(Style::RadioType, p.question()), | 105 Exchange::RadioPrompt(p) => alloc(Style::RadioType, p.question()), |
| 241 #[cfg(feature = "linux-pam-ext")] | 106 #[cfg(feature = "linux-pam-ext")] |
| 242 Message::BinaryPrompt(p) => Ok((Style::BinaryPrompt, unsafe { | 107 Exchange::BinaryPrompt(p) => Ok((Style::BinaryPrompt, unsafe { |
| 243 CHeapBox::cast(CBinaryData::alloc(p.question())?) | 108 CHeapBox::cast(CBinaryData::alloc(p.question())?) |
| 244 })), | 109 })), |
| 245 #[cfg(not(feature = "linux-pam-ext"))] | 110 #[cfg(not(feature = "linux-pam-ext"))] |
| 246 Message::RadioPrompt(_) | Message::BinaryPrompt(_) => Err(ErrorCode::ConversationError), | 111 Exchange::RadioPrompt(_) | Exchange::BinaryPrompt(_) => { |
| 112 Err(ErrorCode::ConversationError) | |
| 113 } | |
| 247 }?; | 114 }?; |
| 248 Ok(Self { | 115 Ok(Self { |
| 249 style: style.into(), | 116 style: style.into(), |
| 250 data: Some(data), | 117 data: Some(data), |
| 251 }) | 118 }) |
| 282 }; | 149 }; |
| 283 } | 150 } |
| 284 } | 151 } |
| 285 } | 152 } |
| 286 | 153 |
| 287 impl<'a> TryFrom<&'a Question> for OwnedMessage<'a> { | 154 impl<'a> TryFrom<&'a Question> for OwnedExchange<'a> { |
| 288 type Error = ErrorCode; | 155 type Error = ErrorCode; |
| 289 fn try_from(question: &'a Question) -> Result<Self> { | 156 fn try_from(question: &'a Question) -> Result<Self> { |
| 290 let style: Style = question | 157 let style: Style = question |
| 291 .style | 158 .style |
| 292 .try_into() | 159 .try_into() |
| 311 } | 178 } |
| 312 } | 179 } |
| 313 | 180 |
| 314 #[cfg(test)] | 181 #[cfg(test)] |
| 315 mod tests { | 182 mod tests { |
| 183 use super::*; | |
| 316 | 184 |
| 317 macro_rules! assert_matches { | 185 macro_rules! assert_matches { |
| 318 ($id:ident => $variant:path, $q:expr) => { | 186 (($variant:path, $q:expr), $input:expr) => { |
| 319 if let $variant($id) = $id { | 187 let input = $input; |
| 320 assert_eq!($q, $id.question()); | 188 let exc = input.exchange(); |
| 189 if let $variant(msg) = exc { | |
| 190 assert_eq!($q, msg.question()); | |
| 321 } else { | 191 } else { |
| 322 panic!("mismatched enum variant {x:?}", x = $id); | 192 panic!( |
| 193 "want enum variant {v}, got {exc:?}", | |
| 194 v = stringify!($variant) | |
| 195 ); | |
| 323 } | 196 } |
| 324 }; | 197 }; |
| 325 } | 198 } |
| 326 | 199 |
| 327 macro_rules! tests { ($fn_name:ident<$typ:ident>) => { | 200 // TODO: Test TryFrom<Exchange> for Question/OwnedQuestion. |
| 328 mod $fn_name { | 201 |
| 329 use super::super::*; | 202 #[test] |
| 330 #[test] | 203 fn standard() { |
| 331 fn standard() { | 204 assert_matches!( |
| 332 let interrogation = Box::pin(<$typ>::new(&[ | 205 (Exchange::MaskedPrompt, "hocus pocus"), |
| 333 MaskedQAndA::new("hocus pocus").message(), | 206 MaskedQAndA::new("hocus pocus") |
| 334 QAndA::new("what").message(), | 207 ); |
| 335 QAndA::new("who").message(), | 208 assert_matches!((Exchange::Prompt, "what"), QAndA::new("what")); |
| 336 InfoMsg::new("hey").message(), | 209 assert_matches!((Exchange::Prompt, "who"), QAndA::new("who")); |
| 337 ErrorMsg::new("gasp").message(), | 210 assert_matches!((Exchange::Info, "hey"), InfoMsg::new("hey")); |
| 338 ]) | 211 assert_matches!((Exchange::Error, "gasp"), ErrorMsg::new("gasp")); |
| 339 .unwrap()); | 212 } |
| 340 let indirect = interrogation.as_ref().ptr(); | 213 |
| 341 | 214 #[test] |
| 342 let remade = unsafe { $typ::borrow_ptr(indirect, interrogation.len()) }; | 215 #[cfg(feature = "linux-pam-ext")] |
| 343 let messages: Vec<OwnedMessage> = remade | 216 fn linux_extensions() { |
| 344 .map(TryInto::try_into) | 217 assert_matches!( |
| 345 .collect::<Result<_>>() | 218 (Exchange::BinaryPrompt, (&[5, 4, 3, 2, 1][..], 66)), |
| 346 .unwrap(); | 219 BinaryQAndA::new((&[5, 4, 3, 2, 1], 66)) |
| 347 let [masked, what, who, hey, gasp] = messages.try_into().unwrap(); | 220 ); |
| 348 assert_matches!(masked => OwnedMessage::MaskedPrompt, "hocus pocus"); | 221 assert_matches!( |
| 349 assert_matches!(what => OwnedMessage::Prompt, "what"); | 222 (Exchange::RadioPrompt, "you must choose"), |
| 350 assert_matches!(who => OwnedMessage::Prompt, "who"); | 223 RadioQAndA::new("you must choose") |
| 351 assert_matches!(hey => OwnedMessage::Info, "hey"); | 224 ); |
| 352 assert_matches!(gasp => OwnedMessage::Error, "gasp"); | 225 } |
| 353 } | 226 } |
| 354 | |
| 355 #[test] | |
| 356 #[cfg(not(feature = "linux-pam-ext"))] | |
| 357 fn no_linux_extensions() { | |
| 358 use crate::conv::{BinaryQAndA, RadioQAndA}; | |
| 359 <$typ>::new(&[ | |
| 360 BinaryQAndA::new((&[5, 4, 3, 2, 1], 66)).message(), | |
| 361 RadioQAndA::new("you must choose").message(), | |
| 362 ]).unwrap_err(); | |
| 363 } | |
| 364 | |
| 365 #[test] | |
| 366 #[cfg(feature = "linux-pam-ext")] | |
| 367 fn linux_extensions() { | |
| 368 let interrogation = Box::pin(<$typ>::new(&[ | |
| 369 BinaryQAndA::new((&[5, 4, 3, 2, 1], 66)).message(), | |
| 370 RadioQAndA::new("you must choose").message(), | |
| 371 ]).unwrap()); | |
| 372 let indirect = interrogation.as_ref().ptr(); | |
| 373 | |
| 374 let remade = unsafe { $typ::borrow_ptr(indirect, interrogation.len()) }; | |
| 375 let messages: Vec<OwnedMessage> = remade | |
| 376 .map(TryInto::try_into) | |
| 377 .collect::<Result<_>>() | |
| 378 .unwrap(); | |
| 379 let [bin, choose] = messages.try_into().unwrap(); | |
| 380 assert_matches!(bin => OwnedMessage::BinaryPrompt, (&[5, 4, 3, 2, 1][..], 66)); | |
| 381 assert_matches!(choose => OwnedMessage::RadioPrompt, "you must choose"); | |
| 382 } | |
| 383 } | |
| 384 }} | |
| 385 | |
| 386 tests!(test_xsso<XSsoQuestions>); | |
| 387 tests!(test_linux<LinuxPamQuestions>); | |
| 388 } |
