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