comparison src/module.rs @ 70:9f8381a1c09c

Implement low-level conversation primitives. This change does two primary things: 1. Introduces new Conversation traits, to be implemented both by the library and by PAM client applications. 2. Builds the memory-management infrastructure for passing messages through the conversation. ...and it adds tests for both of the above, including ASAN tests.
author Paul Fisher <paul@pfish.zone>
date Tue, 03 Jun 2025 01:21:59 -0400
parents a674799a5cd3
children 58f9d2a4df38
comparison
equal deleted inserted replaced
69:8f3ae0c7ab92 70:9f8381a1c09c
1 //! Functions and types useful for implementing a PAM module. 1 //! Functions and types useful for implementing a PAM module.
2 2
3 // Temporarily allowed until we get the actual conversation functions hooked up.
4 #![allow(dead_code)]
5
3 use crate::constants::{ErrorCode, Flags, Result}; 6 use crate::constants::{ErrorCode, Flags, Result};
7 use crate::conv::BinaryData;
8 use crate::conv::{Conversation, Message, Response};
4 use crate::handle::PamModuleHandle; 9 use crate::handle::PamModuleHandle;
10 use secure_string::SecureString;
5 use std::ffi::CStr; 11 use std::ffi::CStr;
6 12
7 /// A trait for a PAM module to implement. 13 /// A trait for a PAM module to implement.
8 /// 14 ///
9 /// The default implementations of all these hooks tell PAM to ignore them 15 /// The default implementations of all these hooks tell PAM to ignore them
228 /// - [`ErrorCode::SessionError`]: Cannot remove an entry for this session. 234 /// - [`ErrorCode::SessionError`]: Cannot remove an entry for this session.
229 /// 235 ///
230 /// [mwg]: https://www.chiark.greenend.org.uk/doc/libpam-doc/html/mwg-expected-of-module-session.html#mwg-pam_sm_close_session 236 /// [mwg]: https://www.chiark.greenend.org.uk/doc/libpam-doc/html/mwg-expected-of-module-session.html#mwg-pam_sm_close_session
231 fn close_session(handle: &mut T, args: Vec<&CStr>, flags: Flags) -> Result<()> { 237 fn close_session(handle: &mut T, args: Vec<&CStr>, flags: Flags) -> Result<()> {
232 Err(ErrorCode::Ignore) 238 Err(ErrorCode::Ignore)
239 }
240 }
241
242 /// Provides methods to make it easier to send exactly one message.
243 ///
244 /// This is primarily used by PAM modules, so that a module that only needs
245 /// one piece of information at a time doesn't have a ton of boilerplate.
246 /// You may also find it useful for testing PAM application libraries.
247 ///
248 /// ```
249 /// # use nonstick::Result;
250 /// # use nonstick::conv::Conversation;
251 /// # use nonstick::module::ConversationMux;
252 /// # fn _do_test(conv: impl Conversation) -> Result<()> {
253 /// let mut mux = ConversationMux(conv);
254 /// let token = mux.masked_prompt("enter your one-time token")?;
255 /// # Ok(())
256 /// # }
257 pub struct ConversationMux<C: Conversation>(pub C);
258
259 impl<C: Conversation> Conversation for ConversationMux<C> {
260 fn send(&mut self, messages: &[Message]) -> Result<Vec<Response>> {
261 self.0.send(messages)
262 }
263 }
264
265 impl<C: Conversation> ConversationMux<C> {
266 /// Prompts the user for something.
267 pub fn prompt(&mut self, request: &str) -> Result<String> {
268 let resp = self.send(&[Message::Prompt(request)])?.pop();
269 match resp {
270 Some(Response::Text(s)) => Ok(s),
271 _ => Err(ErrorCode::ConversationError),
272 }
273 }
274
275 /// Prompts the user for something, but hides what the user types.
276 pub fn masked_prompt(&mut self, request: &str) -> Result<SecureString> {
277 let resp = self.send(&[Message::MaskedPrompt(request)])?.pop();
278 match resp {
279 Some(Response::MaskedText(s)) => Ok(s),
280 _ => Err(ErrorCode::ConversationError),
281 }
282 }
283
284 /// Prompts the user for a yes/no/maybe conditional (a Linux-PAM extension).
285 ///
286 /// PAM documentation doesn't define the format of the response.
287 pub fn radio_prompt(&mut self, request: &str) -> Result<String> {
288 let resp = self.send(&[Message::RadioPrompt(request)])?.pop();
289 match resp {
290 Some(Response::Text(s)) => Ok(s),
291 _ => Err(ErrorCode::ConversationError),
292 }
293 }
294
295 /// Alerts the user to an error.
296 pub fn error(&mut self, message: &str) {
297 let _ = self.send(&[Message::Error(message)]);
298 }
299
300 /// Sends an informational message to the user.
301 pub fn info(&mut self, message: &str) {
302 let _ = self.send(&[Message::Info(message)]);
303 }
304
305 /// Requests binary data from the user (a Linux-PAM extension).
306 pub fn binary_prompt(&mut self, data: &[u8], data_type: u8) -> Result<BinaryData> {
307 let resp = self
308 .send(&[Message::BinaryPrompt { data, data_type }])?
309 .pop();
310 match resp {
311 Some(Response::Binary(d)) => Ok(d),
312 _ => Err(ErrorCode::ConversationError),
313 }
233 } 314 }
234 } 315 }
235 316
236 /// Generates the dynamic library entry points for a [PamModule] implementation. 317 /// Generates the dynamic library entry points for a [PamModule] implementation.
237 /// 318 ///
377 } 458 }
378 }; 459 };
379 } 460 }
380 461
381 #[cfg(test)] 462 #[cfg(test)]
382 pub mod test { 463 mod test {
383 use crate::module::{PamModule, PamModuleHandle}; 464 use super::{
384 465 Conversation, ConversationMux, ErrorCode, Message, Response, Result, SecureString,
385 struct Foo; 466 };
386 impl<T: PamModuleHandle> PamModule<T> for Foo {} 467
387 468 /// Compile-time test that the `pam_hooks` macro compiles.
388 pam_hooks!(Foo); 469 mod hooks {
470 use super::super::{PamModule, PamModuleHandle};
471 struct Foo;
472 impl<T: PamModuleHandle> PamModule<T> for Foo {}
473
474 pam_hooks!(Foo);
475 }
476
477 #[test]
478 fn test_mux() {
479 struct MuxTester;
480
481 impl Conversation for MuxTester {
482 fn send(&mut self, messages: &[Message]) -> Result<Vec<Response>> {
483 if let [msg] = messages {
484 match msg {
485 Message::Info(info) => {
486 assert_eq!("let me tell you", *info);
487 Ok(vec![Response::NoResponse])
488 }
489 Message::Error(error) => {
490 assert_eq!("oh no", *error);
491 Ok(vec![Response::NoResponse])
492 }
493 Message::Prompt("should_error") => Err(ErrorCode::BufferError),
494 Message::Prompt(ask) => {
495 assert_eq!("question", *ask);
496 Ok(vec![Response::Text("answer".to_owned())])
497 }
498 Message::MaskedPrompt("return_wrong_type") => {
499 Ok(vec![Response::NoResponse])
500 }
501 Message::MaskedPrompt(ask) => {
502 assert_eq!("password!", *ask);
503 Ok(vec![Response::MaskedText(SecureString::from(
504 "open sesame",
505 ))])
506 }
507 Message::BinaryPrompt { data, data_type } => {
508 assert_eq!(&[1, 2, 3], data);
509 assert_eq!(69, *data_type);
510 Ok(vec![Response::Binary(super::BinaryData::new(
511 vec![3, 2, 1],
512 42,
513 ))])
514 }
515 Message::RadioPrompt(ask) => {
516 assert_eq!("radio?", *ask);
517 Ok(vec![Response::Text("yes".to_owned())])
518 }
519 }
520 } else {
521 panic!("messages is the wrong size ({len})", len = messages.len())
522 }
523 }
524 }
525
526 let mut mux = ConversationMux(MuxTester);
527 assert_eq!("answer", mux.prompt("question").unwrap());
528 assert_eq!(
529 SecureString::from("open sesame"),
530 mux.masked_prompt("password!").unwrap()
531 );
532 mux.error("oh no");
533 mux.info("let me tell you");
534 {
535 assert_eq!("yes", mux.radio_prompt("radio?").unwrap());
536 assert_eq!(
537 super::BinaryData::new(vec![3, 2, 1], 42),
538 mux.binary_prompt(&[1, 2, 3], 69).unwrap(),
539 )
540 }
541 assert_eq!(
542 ErrorCode::BufferError,
543 mux.prompt("should_error").unwrap_err(),
544 );
545 assert_eq!(
546 ErrorCode::ConversationError,
547 mux.masked_prompt("return_wrong_type").unwrap_err()
548 )
549 }
389 } 550 }