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