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
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 }