comparison src/conv.rs @ 87:05291b601f0a

Well and truly separate the Linux extensions. This separates the Linux extensions on the libpam side, and disables the two enums on the interface side. Users can still call the Linux extensions from non-Linux PAM impls, but they'll get a conversation error back.
author Paul Fisher <paul@pfish.zone>
date Tue, 10 Jun 2025 04:40:01 -0400
parents 2128123b9406
children
comparison
equal deleted inserted replaced
86:23162cd399aa 87:05291b601f0a
15 /// that will be presented to the user. 15 /// that will be presented to the user.
16 #[non_exhaustive] 16 #[non_exhaustive]
17 pub enum Message<'a> { 17 pub enum Message<'a> {
18 Prompt(&'a QAndA<'a>), 18 Prompt(&'a QAndA<'a>),
19 MaskedPrompt(&'a MaskedQAndA<'a>), 19 MaskedPrompt(&'a MaskedQAndA<'a>),
20 Error(&'a ErrorMsg<'a>),
21 Info(&'a InfoMsg<'a>),
20 RadioPrompt(&'a RadioQAndA<'a>), 22 RadioPrompt(&'a RadioQAndA<'a>),
21 BinaryPrompt(&'a BinaryQAndA<'a>), 23 BinaryPrompt(&'a BinaryQAndA<'a>),
22 Error(&'a ErrorMsg<'a>),
23 Info(&'a InfoMsg<'a>),
24 } 24 }
25 25
26 impl Message<'_> { 26 impl Message<'_> {
27 /// Sets an error answer on this question, without having to inspect it. 27 /// Sets an error answer on this question, without having to inspect it.
28 /// 28 ///
48 /// } 48 /// }
49 pub fn set_error(&self, err: ErrorCode) { 49 pub fn set_error(&self, err: ErrorCode) {
50 match *self { 50 match *self {
51 Message::Prompt(m) => m.set_answer(Err(err)), 51 Message::Prompt(m) => m.set_answer(Err(err)),
52 Message::MaskedPrompt(m) => m.set_answer(Err(err)), 52 Message::MaskedPrompt(m) => m.set_answer(Err(err)),
53 Message::Error(m) => m.set_answer(Err(err)),
54 Message::Info(m) => m.set_answer(Err(err)),
53 Message::RadioPrompt(m) => m.set_answer(Err(err)), 55 Message::RadioPrompt(m) => m.set_answer(Err(err)),
54 Message::BinaryPrompt(m) => m.set_answer(Err(err)), 56 Message::BinaryPrompt(m) => m.set_answer(Err(err)),
55 Message::Error(m) => m.set_answer(Err(err)),
56 Message::Info(m) => m.set_answer(Err(err)),
57 } 57 }
58 } 58 }
59 } 59 }
60 60
61 macro_rules! q_and_a { 61 macro_rules! q_and_a {
62 ($name:ident<'a, Q=$qt:ty, A=$at:ty>, $val:path, $($doc:literal)*) => { 62 ($(#[$m:meta])* $name:ident<'a, Q=$qt:ty, A=$at:ty>, $val:path) => {
63 $( 63 $(#[$m])*
64 #[doc = $doc]
65 )*
66 pub struct $name<'a> { 64 pub struct $name<'a> {
67 q: $qt, 65 q: $qt,
68 a: Cell<Result<$at>>, 66 a: Cell<Result<$at>>,
69 } 67 }
70 68
69 $(#[$m])*
71 impl<'a> $name<'a> { 70 impl<'a> $name<'a> {
72 #[doc = concat!("Creates a `", stringify!($t), "` to be sent to the user.")] 71 #[doc = concat!("Creates a `", stringify!($t), "` to be sent to the user.")]
73 pub fn new(question: $qt) -> Self { 72 pub fn new(question: $qt) -> Self {
74 Self { 73 Self {
75 q: question, 74 q: question,
106 } 105 }
107 } 106 }
108 107
109 // shout out to stackoverflow user ballpointben for this lazy impl: 108 // shout out to stackoverflow user ballpointben for this lazy impl:
110 // https://stackoverflow.com/a/78871280/39808 109 // https://stackoverflow.com/a/78871280/39808
110 $(#[$m])*
111 impl fmt::Debug for $name<'_> { 111 impl fmt::Debug for $name<'_> {
112 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> StdResult<(), fmt::Error> { 112 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> StdResult<(), fmt::Error> {
113 #[derive(Debug)] 113 #[derive(Debug)]
114 struct $name<'a> { q: $qt } 114 struct $name<'a> { q: $qt }
115 fmt::Debug::fmt(&$name { q: self.q }, f) 115 fmt::Debug::fmt(&$name { q: self.q }, f)
117 } 117 }
118 }; 118 };
119 } 119 }
120 120
121 q_and_a!( 121 q_and_a!(
122 /// A Q&A that asks the user for text and does not show it while typing.
123 ///
124 /// In other words, a password entry prompt.
122 MaskedQAndA<'a, Q=&'a str, A=SecureString>, 125 MaskedQAndA<'a, Q=&'a str, A=SecureString>,
123 Message::MaskedPrompt, 126 Message::MaskedPrompt
124 "A Q&A that asks the user for text and does not show it while typing."
125 ""
126 "In other words, a password entry prompt."
127 ); 127 );
128 128
129 q_and_a!( 129 q_and_a!(
130 /// A standard Q&A prompt that asks the user for text.
131 ///
132 /// This is the normal "ask a person a question" prompt.
133 /// When the user types, their input will be shown to them.
134 /// It can be used for things like usernames.
130 QAndA<'a, Q=&'a str, A=String>, 135 QAndA<'a, Q=&'a str, A=String>,
131 Message::Prompt, 136 Message::Prompt
132 "A standard Q&A prompt that asks the user for text."
133 ""
134 "This is the normal \"ask a person a question\" prompt."
135 "When the user types, their input will be shown to them."
136 "It can be used for things like usernames."
137 ); 137 );
138 138
139 q_and_a!( 139 q_and_a!(
140 /// A Q&A for "radio button"–style data. (Linux-PAM extension)
141 ///
142 /// This message type is theoretically useful for "yes/no/maybe"
143 /// questions, but nowhere in the documentation is it specified
144 /// what the format of the answer will be, or how this should be shown.
140 RadioQAndA<'a, Q=&'a str, A=String>, 145 RadioQAndA<'a, Q=&'a str, A=String>,
141 Message::RadioPrompt, 146 Message::RadioPrompt
142 "A Q&A for \"radio button\"–style data. (Linux-PAM extension)"
143 ""
144 "This message type is theoretically useful for \"yes/no/maybe\""
145 "questions, but nowhere in the documentation is it specified"
146 "what the format of the answer will be, or how this should be shown."
147 ); 147 );
148 148
149 q_and_a!( 149 q_and_a!(
150 /// Asks for binary data. (Linux-PAM extension)
151 ///
152 /// This sends a binary message to the client application.
153 /// It can be used to communicate with non-human logins,
154 /// or to enable things like security keys.
155 ///
156 /// The `data_type` tag is a value that is simply passed through
157 /// to the application. PAM does not define any meaning for it.
150 BinaryQAndA<'a, Q=(&'a [u8], u8), A=BinaryData>, 158 BinaryQAndA<'a, Q=(&'a [u8], u8), A=BinaryData>,
151 Message::BinaryPrompt, 159 Message::BinaryPrompt
152 "Asks for binary data. (Linux-PAM extension)"
153 ""
154 "This sends a binary message to the client application."
155 "It can be used to communicate with non-human logins,"
156 "or to enable things like security keys."
157 ""
158 "The `data_type` tag is a value that is simply passed through"
159 "to the application. PAM does not define any meaning for it."
160 ); 160 );
161 161
162 /// Owned binary data. 162 /// Owned binary data.
163 #[derive(Debug, Default, PartialEq)] 163 #[derive(Debug, Default, PartialEq)]
164 pub struct BinaryData { 164 pub struct BinaryData {
200 (&value.data, value.data_type) 200 (&value.data, value.data_type)
201 } 201 }
202 } 202 }
203 203
204 q_and_a!( 204 q_and_a!(
205 /// A message containing information to be passed to the user.
206 ///
207 /// While this does not have an answer, [`Conversation`] implementations
208 /// should still call [`set_answer`][`QAndA::set_answer`] to verify that
209 /// the message has been displayed (or actively discarded).
205 InfoMsg<'a, Q = &'a str, A = ()>, 210 InfoMsg<'a, Q = &'a str, A = ()>,
206 Message::Info, 211 Message::Info
207 "A message containing information to be passed to the user."
208 ""
209 "While this does not have an answer, [`Conversation`] implementations"
210 "should still call [`set_answer`][`QAndA::set_answer`] to verify that"
211 "the message has been displayed (or actively discarded)."
212 ); 212 );
213 213
214 q_and_a!( 214 q_and_a!(
215 /// An error message to be passed to the user.
216 ///
217 /// While this does not have an answer, [`Conversation`] implementations
218 /// should still call [`set_answer`][`QAndA::set_answer`] to verify that
219 /// the message has been displayed (or actively discarded).
215 ErrorMsg<'a, Q = &'a str, A = ()>, 220 ErrorMsg<'a, Q = &'a str, A = ()>,
216 Message::Error, 221 Message::Error
217 "An error message to be passed to the user."
218 ""
219 "While this does not have an answer, [`Conversation`] implementations"
220 "should still call [`set_answer`][`QAndA::set_answer`] to verify that"
221 "the message has been displayed (or actively discarded)."
222 ); 222 );
223 223
224 /// A channel for PAM modules to request information from the user. 224 /// A channel for PAM modules to request information from the user.
225 /// 225 ///
226 /// This trait is used by both applications and PAM modules: 226 /// This trait is used by both applications and PAM modules:
313 /// # fn prompt(&mut self, request: &str) -> Result<String> { 313 /// # fn prompt(&mut self, request: &str) -> Result<String> {
314 /// # todo!() 314 /// # todo!()
315 /// # } 315 /// # }
316 /// # 316 /// #
317 /// # fn masked_prompt(&mut self, request: &str) -> Result<SecureString> { 317 /// # fn masked_prompt(&mut self, request: &str) -> Result<SecureString> {
318 /// # todo!()
319 /// # }
320 /// #
321 /// # fn radio_prompt(&mut self, request: &str) -> Result<String> {
322 /// # todo!() 318 /// # todo!()
323 /// # } 319 /// # }
324 /// # 320 /// #
325 /// # fn error_msg(&mut self, message: &str) { 321 /// # fn error_msg(&mut self, message: &str) {
326 /// # todo!() 322 /// # todo!()
328 /// # 324 /// #
329 /// # fn info_msg(&mut self, message: &str) { 325 /// # fn info_msg(&mut self, message: &str) {
330 /// # todo!() 326 /// # todo!()
331 /// # } 327 /// # }
332 /// # 328 /// #
329 /// # fn radio_prompt(&mut self, request: &str) -> Result<String> {
330 /// # todo!()
331 /// # }
332 /// #
333 /// # fn binary_prompt(&mut self, (data, data_type): (&[u8], u8)) -> Result<BinaryData> { 333 /// # fn binary_prompt(&mut self, (data, data_type): (&[u8], u8)) -> Result<BinaryData> {
334 /// # todo!() 334 /// # todo!()
335 /// # } 335 /// # }
336 /// } 336 /// }
337 /// 337 ///
354 } 354 }
355 /// Prompts the user for something. 355 /// Prompts the user for something.
356 fn prompt(&mut self, request: &str) -> Result<String>; 356 fn prompt(&mut self, request: &str) -> Result<String>;
357 /// Prompts the user for something, but hides what the user types. 357 /// Prompts the user for something, but hides what the user types.
358 fn masked_prompt(&mut self, request: &str) -> Result<SecureString>; 358 fn masked_prompt(&mut self, request: &str) -> Result<SecureString>;
359 /// Prompts the user for a yes/no/maybe conditional (a Linux-PAM extension).
360 ///
361 /// PAM documentation doesn't define the format of the response.
362 fn radio_prompt(&mut self, request: &str) -> Result<String>;
363 /// Alerts the user to an error. 359 /// Alerts the user to an error.
364 fn error_msg(&mut self, message: &str); 360 fn error_msg(&mut self, message: &str);
365 /// Sends an informational message to the user. 361 /// Sends an informational message to the user.
366 fn info_msg(&mut self, message: &str); 362 fn info_msg(&mut self, message: &str);
367 /// Requests binary data from the user (a Linux-PAM extension). 363 /// \[Linux extension] Prompts the user for a yes/no/maybe conditional.
368 fn binary_prompt(&mut self, data_and_type: (&[u8], u8)) -> Result<BinaryData>; 364 ///
365 /// PAM documentation doesn't define the format of the response.
366 ///
367 /// When called on an implementation that doesn't support radio prompts,
368 /// this will return [`ErrorCode::ConversationError`].
369 /// If implemented on an implementation that doesn't support radio prompts,
370 /// this will never be called.
371 fn radio_prompt(&mut self, request: &str) -> Result<String> {
372 let _ = request;
373 Err(ErrorCode::ConversationError)
374 }
375 /// \[Linux extension] Requests binary data from the user.
376 ///
377 /// When called on an implementation that doesn't support radio prompts,
378 /// this will return [`ErrorCode::ConversationError`].
379 /// If implemented on an implementation that doesn't support radio prompts,
380 /// this will never be called.
381 fn binary_prompt(&mut self, data_and_type: (&[u8], u8)) -> Result<BinaryData> {
382 let _ = data_and_type;
383 Err(ErrorCode::ConversationError)
384 }
369 } 385 }
370 386
371 macro_rules! conv_fn { 387 macro_rules! conv_fn {
372 ($fn_name:ident($($param:tt: $pt:ty),+) -> $resp_type:ty { $msg:ty }) => { 388 ($(#[$m:meta])* $fn_name:ident($($param:tt: $pt:ty),+) -> $resp_type:ty { $msg:ty }) => {
389 $(#[$m])*
373 fn $fn_name(&mut self, $($param: $pt),*) -> Result<$resp_type> { 390 fn $fn_name(&mut self, $($param: $pt),*) -> Result<$resp_type> {
374 let prompt = <$msg>::new($($param),*); 391 let prompt = <$msg>::new($($param),*);
375 self.communicate(&[prompt.message()]); 392 self.communicate(&[prompt.message()]);
376 prompt.answer() 393 prompt.answer()
377 } 394 }
378 }; 395 };
379 ($fn_name:ident($($param:tt: $pt:ty),+) { $msg:ty }) => { 396 ($(#[$m:meta])*$fn_name:ident($($param:tt: $pt:ty),+) { $msg:ty }) => {
397 $(#[$m])*
380 fn $fn_name(&mut self, $($param: $pt),*) { 398 fn $fn_name(&mut self, $($param: $pt),*) {
381 self.communicate(&[<$msg>::new($($param),*).message()]); 399 self.communicate(&[<$msg>::new($($param),*).message()]);
382 } 400 }
383 }; 401 };
384 } 402 }
385 403
386 impl<C: Conversation> SimpleConversation for C { 404 impl<C: Conversation> SimpleConversation for C {
387 conv_fn!(prompt(message: &str) -> String { QAndA }); 405 conv_fn!(prompt(message: &str) -> String { QAndA });
388 conv_fn!(masked_prompt(message: &str) -> SecureString { MaskedQAndA } ); 406 conv_fn!(masked_prompt(message: &str) -> SecureString { MaskedQAndA } );
389 conv_fn!(radio_prompt(message: &str) -> String { RadioQAndA });
390 conv_fn!(error_msg(message: &str) { ErrorMsg }); 407 conv_fn!(error_msg(message: &str) { ErrorMsg });
391 conv_fn!(info_msg(message: &str) { InfoMsg }); 408 conv_fn!(info_msg(message: &str) { InfoMsg });
409 conv_fn!(radio_prompt(message: &str) -> String { RadioQAndA });
392 conv_fn!(binary_prompt((data, data_type): (&[u8], u8)) -> BinaryData { BinaryQAndA }); 410 conv_fn!(binary_prompt((data, data_type): (&[u8], u8)) -> BinaryData { BinaryQAndA });
393 } 411 }
394 412
395 /// A [`Conversation`] which asks the questions one at a time. 413 /// A [`Conversation`] which asks the questions one at a time.
396 /// 414 ///
425 } 443 }
426 } 444 }
427 445
428 #[cfg(test)] 446 #[cfg(test)]
429 mod tests { 447 mod tests {
430 use super::{ 448 use super::*;
431 BinaryQAndA, Conversation, ErrorMsg, InfoMsg, MaskedQAndA, Message, QAndA, RadioQAndA,
432 Result, SecureString, SimpleConversation,
433 };
434 use crate::constants::ErrorCode; 449 use crate::constants::ErrorCode;
435 use crate::BinaryData;
436 450
437 #[test] 451 #[test]
438 fn test_demux() { 452 fn test_demux() {
439 #[derive(Default)] 453 #[derive(Default)]
440 struct DemuxTester { 454 struct DemuxTester {
452 } 466 }
453 fn masked_prompt(&mut self, request: &str) -> Result<SecureString> { 467 fn masked_prompt(&mut self, request: &str) -> Result<SecureString> {
454 assert_eq!("reveal", request); 468 assert_eq!("reveal", request);
455 Ok(SecureString::from("my secrets")) 469 Ok(SecureString::from("my secrets"))
456 } 470 }
471 fn error_msg(&mut self, message: &str) {
472 self.error_ran = true;
473 assert_eq!("whoopsie", message);
474 }
475 fn info_msg(&mut self, message: &str) {
476 self.info_ran = true;
477 assert_eq!("did you know", message);
478 }
457 fn radio_prompt(&mut self, request: &str) -> Result<String> { 479 fn radio_prompt(&mut self, request: &str) -> Result<String> {
458 assert_eq!("channel?", request); 480 assert_eq!("channel?", request);
459 Ok("zero".to_owned()) 481 Ok("zero".to_owned())
460 }
461 fn error_msg(&mut self, message: &str) {
462 self.error_ran = true;
463 assert_eq!("whoopsie", message);
464 }
465 fn info_msg(&mut self, message: &str) {
466 self.info_ran = true;
467 assert_eq!("did you know", message);
468 } 482 }
469 fn binary_prompt(&mut self, data_and_type: (&[u8], u8)) -> Result<BinaryData> { 483 fn binary_prompt(&mut self, data_and_type: (&[u8], u8)) -> Result<BinaryData> {
470 assert_eq!((&[10, 9, 8][..], 66), data_and_type); 484 assert_eq!((&[10, 9, 8][..], 66), data_and_type);
471 Ok(BinaryData::new(vec![5, 5, 5], 5)) 485 Ok(BinaryData::new(vec![5, 5, 5], 5))
472 } 486 }
499 assert_eq!(ErrorCode::PermissionDenied, has_err.answer().unwrap_err()); 513 assert_eq!(ErrorCode::PermissionDenied, has_err.answer().unwrap_err());
500 assert!(tester.error_ran); 514 assert!(tester.error_ran);
501 assert!(tester.info_ran); 515 assert!(tester.info_ran);
502 516
503 // Test the Linux extensions separately. 517 // Test the Linux extensions separately.
504 518 {
505 let mut conv = tester.as_conversation(); 519 let mut conv = tester.as_conversation();
506 520
507 let radio = RadioQAndA::new("channel?"); 521 let radio = RadioQAndA::new("channel?");
508 let bin = BinaryQAndA::new((&[10, 9, 8], 66)); 522 let bin = BinaryQAndA::new((&[10, 9, 8], 66));
509 conv.communicate(&[radio.message(), bin.message()]); 523 conv.communicate(&[radio.message(), bin.message()]);
510 524
511 assert_eq!("zero", radio.answer().unwrap()); 525 assert_eq!("zero", radio.answer().unwrap());
512 assert_eq!(BinaryData::from(([5, 5, 5], 5)), bin.answer().unwrap()); 526 assert_eq!(BinaryData::from(([5, 5, 5], 5)), bin.answer().unwrap());
527 }
513 } 528 }
514 529
515 fn test_mux() { 530 fn test_mux() {
516 struct MuxTester; 531 struct MuxTester;
517 532
561 SecureString::from("open sesame"), 576 SecureString::from("open sesame"),
562 tester.masked_prompt("password!").unwrap() 577 tester.masked_prompt("password!").unwrap()
563 ); 578 );
564 tester.error_msg("oh no"); 579 tester.error_msg("oh no");
565 tester.info_msg("let me tell you"); 580 tester.info_msg("let me tell you");
581 // Linux-PAM extensions. Always implemented, but separate for clarity.
566 { 582 {
567 assert_eq!("yes", tester.radio_prompt("radio?").unwrap()); 583 assert_eq!("yes", tester.radio_prompt("radio?").unwrap());
568 assert_eq!( 584 assert_eq!(
569 BinaryData::new(vec![3, 2, 1], 42), 585 BinaryData::new(vec![3, 2, 1], 42),
570 tester.binary_prompt((&[1, 2, 3], 69)).unwrap(), 586 tester.binary_prompt((&[1, 2, 3], 69)).unwrap(),