view src/conv.rs @ 71:58f9d2a4df38

Reorganize everything again??? - Splits ffi/memory stuff into a bunch of stuff in the pam_ffi module. - Builds infrastructure for passing Messages and Responses. - Adds tests for some things at least.
author Paul Fisher <paul@pfish.zone>
date Tue, 03 Jun 2025 21:54:58 -0400
parents 9f8381a1c09c
children 47eb242a4f88
line wrap: on
line source

//! The PAM conversation and associated Stuff.

// Temporarily allowed until we get the actual conversation functions hooked up.
#![allow(dead_code)]

use crate::constants::Result;
use crate::pam_ffi::Message;
use secure_string::SecureString;
// TODO: In most cases, we should be passing around references to strings
// or binary data. Right now we don't because that turns type inference and
// trait definitions/implementations into a HUGE MESS.
//
// Ideally, we would be using some kind of `TD: TextData` and `BD: BinaryData`
// associated types in the various Conversation traits to avoid copying
// when unnecessary.

/// The responses that PAM will return from a request.
#[derive(Debug, PartialEq, derive_more::From)]
pub enum Response {
    /// Used to fill in list entries where there is no response expected.
    ///
    /// Used in response to:
    ///
    /// - [`Error`](Message::Error)
    /// - [`Info`](Message::Info)
    NoResponse,
    /// A response with text data from the user.
    ///
    /// Used in response to:
    ///
    /// - [`Prompt`](Message::Prompt)
    /// - [`RadioPrompt`](Message::RadioPrompt) (a Linux-PAM extension)
    Text(String),
    /// A response to a masked request with text data from the user.
    ///
    /// Used in response to:
    ///
    /// - [`MaskedPrompt`](Message::MaskedPrompt)
    MaskedText(SecureString),
    /// A response to a binary request (a Linux-PAM extension).
    ///
    /// Used in response to:
    ///
    /// - [`BinaryPrompt`](Message::BinaryPrompt)
    Binary(BinaryData),
}

/// A channel for PAM modules to request information from the user.
///
/// This trait is used by both applications and PAM modules:
///
/// - Applications implement Conversation and provide a user interface
///   to allow the user to respond to PAM questions.
/// - Modules call a Conversation implementation to request information
///   or send information to the user.
pub trait Conversation {
    /// Sends messages to the user.
    ///
    /// The returned Vec of messages always contains exactly as many entries
    /// as there were messages in the request; one corresponding to each.
    fn send(&mut self, messages: &[Message]) -> Result<Vec<Response>>;
}

/// Trait that an application can implement if they want to handle messages
/// one at a time.
pub trait DemuxedConversation {
    /// Prompts the user for some text.
    fn prompt(&mut self, request: &str) -> Result<String>;
    /// Prompts the user for some text, but hides their typing.
    fn masked_prompt(&mut self, request: &str) -> Result<SecureString>;
    /// Prompts the user for a radio option (a Linux-PAM extension).
    ///
    /// The Linux-PAM documentation doesn't give the format of the response.
    fn radio_prompt(&mut self, request: &str) -> Result<String>;
    /// Alerts the user to an error.
    fn error(&mut self, message: &str);
    /// Sends an informational message to the user.
    fn info(&mut self, message: &str);
    /// Requests binary data from the user (a Linux-PAM extension).
    fn binary_prompt(&mut self, data: &[u8], data_type: u8) -> Result<BinaryData>;
}

impl<D: DemuxedConversation> Conversation for D {
    fn send(&mut self, messages: &[Message]) -> Result<Vec<Response>> {
        messages
            .iter()
            .map(|msg| match *msg {
                Message::Prompt(prompt) => self.prompt(prompt).map(Response::from),
                Message::MaskedPrompt(prompt) => self.masked_prompt(prompt).map(Response::from),
                Message::RadioPrompt(prompt) => self.radio_prompt(prompt).map(Response::from),
                Message::Info(message) => {
                    self.info(message);
                    Ok(Response::NoResponse)
                }
                Message::Error(message) => {
                    self.error(message);
                    Ok(Response::NoResponse)
                }
                Message::BinaryPrompt { data_type, data } => {
                    self.binary_prompt(data, data_type).map(Response::from)
                }
            })
            .collect()
    }
}

/// Owned binary data.
#[derive(Debug, PartialEq)]
pub struct BinaryData {
    data: Vec<u8>,
    data_type: u8,
}

impl BinaryData {
    pub fn new(data: Vec<u8>, data_type: u8) -> Self {
        Self { data, data_type }
    }
    pub fn data(&self) -> &[u8] {
        &self.data
    }
    pub fn data_type(&self) -> u8 {
        self.data_type
    }
}

impl From<BinaryData> for Vec<u8> {
    /// Extracts the inner vector from the BinaryData.
    fn from(value: BinaryData) -> Self {
        value.data
    }
}

#[cfg(test)]
mod tests {
    use super::{Conversation, DemuxedConversation, Message, Response, SecureString};
    use crate::constants::ErrorCode;

    #[test]
    fn test_demux() {
        #[derive(Default)]
        struct DemuxTester {
            error_ran: bool,
            info_ran: bool,
        }

        impl DemuxedConversation for DemuxTester {
            fn prompt(&mut self, request: &str) -> crate::Result<String> {
                match request {
                    "what" => Ok("whatwhat".to_owned()),
                    "give_err" => Err(ErrorCode::PermissionDenied),
                    _ => panic!("unexpected prompt!"),
                }
            }
            fn masked_prompt(&mut self, request: &str) -> crate::Result<SecureString> {
                assert_eq!("reveal", request);
                Ok(SecureString::from("my secrets"))
            }
            fn radio_prompt(&mut self, request: &str) -> crate::Result<String> {
                assert_eq!("channel?", request);
                Ok("zero".to_owned())
            }
            fn error(&mut self, message: &str) {
                self.error_ran = true;
                assert_eq!("whoopsie", message);
            }
            fn info(&mut self, message: &str) {
                self.info_ran = true;
                assert_eq!("did you know", message);
            }
            fn binary_prompt(
                &mut self,
                data: &[u8],
                data_type: u8,
            ) -> crate::Result<super::BinaryData> {
                assert_eq!(&[10, 9, 8], data);
                assert_eq!(66, data_type);
                Ok(super::BinaryData::new(vec![5, 5, 5], 5))
            }
        }

        let mut tester = DemuxTester::default();

        assert_eq!(
            vec![
                Response::Text("whatwhat".to_owned()),
                Response::MaskedText("my secrets".into()),
                Response::NoResponse,
                Response::NoResponse,
            ],
            tester
                .send(&[
                    Message::Prompt("what"),
                    Message::MaskedPrompt("reveal"),
                    Message::Error("whoopsie"),
                    Message::Info("did you know"),
                ])
                .unwrap()
        );
        assert!(tester.error_ran);
        assert!(tester.info_ran);

        assert_eq!(
            ErrorCode::PermissionDenied,
            tester.send(&[Message::Prompt("give_err")]).unwrap_err(),
        );

        // Test the Linux-PAM extensions separately.

        assert_eq!(
            vec![
                Response::Text("zero".to_owned()),
                Response::Binary(super::BinaryData::new(vec![5, 5, 5], 5)),
            ],
            tester
                .send(&[
                    Message::RadioPrompt("channel?"),
                    Message::BinaryPrompt {
                        data: &[10, 9, 8],
                        data_type: 66
                    },
                ])
                .unwrap()
        );
    }
}