Mercurial > crates > nonstick
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 } |