view src/libpam/handle.rs @ 144:56b559b7ecea

Big rename: separate concepts of Transaction from Handle. - An application that uses PAM creates a Transaction. - The Transaction has a Handle. Currently, a module still get something called a "handle", but that's probably going to change soon.
author Paul Fisher <paul@pfish.zone>
date Sun, 06 Jul 2025 11:59:26 -0400
parents ebb71a412b58
children 1bc52025156b
line wrap: on
line source

use super::conversation::{OwnedConversation, PamConv};
use crate::constants::{ErrorCode, Result};
use crate::conv::Exchange;
use crate::environ::EnvironMapMut;
use crate::handle::PamShared;
use crate::libpam::environ::{LibPamEnviron, LibPamEnvironMut};
use crate::libpam::memory;
use crate::logging::{Level, Location};
use crate::{
    guide, linklist, stdlinks, Conversation, EnvironMap, Flags, PamHandleModule, Transaction,
};
use libpam_sys_helpers::constants;
use num_enum::{IntoPrimitive, TryFromPrimitive};
use std::cell::Cell;
use std::ffi::{c_char, c_int, CString, OsStr, OsString};
use std::mem::ManuallyDrop;
use std::os::unix::ffi::OsStrExt;
use std::ptr;
use std::ptr::NonNull;

/// An owned PAM handle.
pub struct LibPamTransaction<C: Conversation> {
    /// The handle itself.
    handle: ManuallyDrop<LibPamHandle>,
    /// The last return value from the handle.
    last_return: Cell<Result<()>>,
    /// If set, the Conversation that this PAM handle owns.
    ///
    /// We have to hold on to this because the PAM specification doesn't
    /// actually say what the PAM library should do with a passed-in
    /// conversation. Linux-PAM copies the contents of the `pam_conv` struct
    /// that you pass in to `pam_start`, but OpenPAM uses the pointer itself,
    /// so you have to keep it in one place.
    conversation: Box<OwnedConversation<C>>,
}

#[derive(Debug, PartialEq)]
pub struct TransactionBuilder {
    service_name: OsString,
    username: Option<OsString>,
}

impl TransactionBuilder {
    /// Updates the service name.
    pub fn service_name(mut self, service_name: OsString) -> Self {
        self.service_name = service_name;
        self
    }
    /// Sets the username. Setting this will avoid the need for an extra
    /// round trip through the conversation and may otherwise improve
    /// the login experience.
    pub fn username(mut self, username: OsString) -> Self {
        self.username = Some(username);
        self
    }
    /// Builds a PAM handle and starts the transaction.
    pub fn build(self, conv: impl Conversation) -> Result<LibPamTransaction<impl Conversation>> {
        LibPamTransaction::start(self.service_name, self.username, conv)
    }
}

impl<C: Conversation> LibPamTransaction<C> {
    /// Creates a builder to start a PAM transaction for the given service.
    ///
    /// The service name is what controls the steps and checks PAM goes through
    /// when authenticating a user. This corresponds to the configuration file
    /// named <code>/etc/pam.d/<var>service_name</var></code>.
    ///
    /// # References
    #[doc = linklist!(pam_start: adg, _std)]
    ///
    #[doc = stdlinks!(3 pam_start)]
    #[doc = guide!(adg: "adg-interface-by-app-expected.html#adg-pam_start")]
    pub fn build_with_service(service_name: OsString) -> TransactionBuilder {
        TransactionBuilder {
            service_name,
            username: None,
        }
    }

    fn start(service_name: OsString, username: Option<OsString>, conversation: C) -> Result<Self> {
        let conv = Box::new(OwnedConversation::new(conversation));
        let service_cstr = CString::new(service_name.as_bytes()).expect("null is forbidden");
        let username_cstr = memory::option_cstr_os(username.as_deref());
        let username_cstr = memory::prompt_ptr(username_cstr.as_deref());

        let mut handle: *mut libpam_sys::pam_handle = ptr::null_mut();
        // SAFETY: We've set everything up properly to call `pam_start`.
        // The returned value will be a valid pointer provided the result is OK.
        let result = unsafe {
            libpam_sys::pam_start(
                service_cstr.as_ptr(),
                username_cstr,
                (conv.as_ref() as *const OwnedConversation<C>)
                    .cast_mut()
                    .cast(),
                &mut handle,
            )
        };
        ErrorCode::result_from(result)?;
        let handle = NonNull::new(handle).ok_or(ErrorCode::BufferError)?;
        Ok(Self {
            handle: ManuallyDrop::new(LibPamHandle(handle)),
            last_return: Cell::new(Ok(())),
            conversation: conv,
        })
    }

    /// "Quietly" closes the PAM session on an owned PAM handle.
    ///
    /// This internally calls `pam_end` with the appropriate error code.
    ///
    /// # References
    #[doc = linklist!(pam_end: adg, _std)]
    ///
    #[doc = guide!(adg: "adg-interface-by-app-expected.html#adg-pam_end")]
    #[doc = stdlinks!(3 pam_end)]

    fn end_quiet(self) {}
}

macro_rules! wrap {
    (fn $name:ident { $pam_func:ident }) => {
        fn $name(&mut self, flags: Flags) -> Result<()> {
            ErrorCode::result_from(unsafe { libpam_sys::$pam_func(self.0.as_mut(), flags.bits()) })
        }
    };
}

impl Transaction for LibPamHandle {
    wrap!(fn authenticate { pam_authenticate });
    wrap!(fn account_management { pam_acct_mgmt });
    wrap!(fn change_authtok { pam_chauthtok });
}

// TODO: pam_authenticate - app
//       pam_setcred - app
//       pam_acct_mgmt - app
//       pam_chauthtok - app
//       pam_open_session - app
//       pam_close_session - app
//       pam_putenv - shared
//       pam_getenv - shared
//       pam_getenvlist - shared

impl<C: Conversation> Drop for LibPamTransaction<C> {
    /// Closes the PAM session on an owned PAM handle.
    ///
    /// This internally calls `pam_end` with the appropriate error code.
    ///
    /// # References
    #[doc = linklist!(pam_end: adg, _std)]
    ///
    #[doc = guide!(adg: "adg-interface-by-app-expected.html#adg-pam_end")]
    #[doc = stdlinks!(3 pam_end)]
    fn drop(&mut self) {
        unsafe {
            libpam_sys::pam_end(
                self.handle.raw_mut(),
                ErrorCode::result_to_c(self.last_return.get()),
            );
        }
    }
}

macro_rules! delegate {
    // First have the kind that save the result after delegation.
    (fn $meth:ident(&self $(, $param:ident: $typ:ty)*) -> Result<$ret:ty>) => {
        fn $meth(&self $(, $param: $typ)*) -> Result<$ret> {
            let result = self.handle.$meth($($param),*);
            self.last_return.set(split(&result));
            result
        }
    };
    (fn $meth:ident(&mut self $(, $param:ident: $typ:ty)*) -> Result<$ret:ty>) => {
        fn $meth(&mut self $(, $param: $typ)*) -> Result<$ret> {
            let result = self.handle.$meth($($param),*);
            self.last_return.set(split(&result));
            result
        }
    };
    // Then have the kind that are just raw delegates
    (fn $meth:ident(&self $(, $param:ident: $typ:ty)*) -> $ret:ty) => {
        fn $meth(&self $(, $param: $typ)*) -> $ret {
            self.handle.$meth($($param),*)
        }
    };
    (fn $meth:ident(&mut self $(, $param:ident: $typ:ty)*) -> $ret:ty) => {
        fn $meth(&mut self $(, $param: $typ)*) -> $ret {
            self.handle.$meth($($param),*)
        }
    };
    // Then have item getters / setters
    (get = $get:ident$(, set = $set:ident)?) => {
        delegate!(fn $get(&self) -> Result<Option<OsString>>);
        $(delegate!(set = $set);)?
    };
    (set = $set:ident) => {
        delegate!(fn $set(&mut self, value: Option<&OsStr>) -> Result<()>);
    };
}

fn split<T>(result: &Result<T>) -> Result<()> {
    result.as_ref().map(drop).map_err(|&e| e)
}

impl<C: Conversation> PamShared for LibPamTransaction<C> {
    delegate!(fn log(&self, level: Level, location: Location<'_>, entry: &str) -> ());
    delegate!(fn environ(&self) -> impl EnvironMap);
    delegate!(fn environ_mut(&mut self) -> impl EnvironMapMut);
    delegate!(fn username(&mut self, prompt: Option<&OsStr>) -> Result<OsString>);
    delegate!(get = user_item, set = set_user_item);
    delegate!(get = service, set = set_service);
    delegate!(get = user_prompt, set = set_user_prompt);
    delegate!(get = tty_name, set = set_tty_name);
    delegate!(get = remote_user, set = set_remote_user);
    delegate!(get = remote_host, set = set_remote_host);
    delegate!(set = set_authtok_item);
    delegate!(set = set_old_authtok_item);
}

/// Macro to implement getting/setting a CStr-based item.
macro_rules! cstr_item {
    (get = $getter:ident, item = $item_type:path) => {
        fn $getter(&self) -> Result<Option<OsString>> {
            unsafe { self.get_cstr_item($item_type) }
        }
    };
    (set = $setter:ident, item = $item_type:path) => {
        fn $setter(&mut self, value: Option<&OsStr>) -> Result<()> {
            unsafe { self.set_cstr_item($item_type, value) }
        }
    };
}

/// An owned variation of a basic PAM handle.
///
/// This is the most basic version of a wrapped PAM handle. It's mostly used
/// as the inside of the [`LibPamTransaction`], but can also be used to "adopt"
/// a PAM handle created by another library.
///
/// If [`Self::end`] is not called, this will always call `pam_end` reporting
/// successful completion.
#[repr(transparent)]
pub struct LibPamHandle(NonNull<libpam_sys::pam_handle>);

impl LibPamHandle {
    /// Takes ownership of the pointer to the given PAM handle.
    ///
    /// **Do not use this just to get a reference to a PAM handle.**
    ///
    /// # Safety
    ///
    /// - The pointer must point to a valid PAM handle.
    /// - The conversation associated with the handle must remain valid
    ///   for as long as the handle is open.
    pub unsafe fn from_ptr(handle: NonNull<libpam_sys::pam_handle>) -> Self {
        Self(handle)
    }

    /// Ends the transaction, reporting `error_code` to cleanup callbacks.
    ///
    /// # References
    #[doc = linklist!(pam_end: adg, _std)]
    ///
    #[doc = guide!(adg: "adg-interface-by-app-expected.html#adg-pam_end")]
    #[doc = stdlinks!(3 pam_end)]
    pub fn end(self, result: Result<()>) {
        let mut me = ManuallyDrop::new(self);
        unsafe { libpam_sys::pam_end(me.raw_mut(), ErrorCode::result_to_c(result)) };
    }

    #[cfg_attr(
        not(pam_impl = "LinuxPam"),
        doc = "Exactly equivalent to [`Self::end`], except on Linux-PAM."
    )]
    #[cfg_attr(
        pam_impl = "LinuxPam",
        doc = "Ends the transaction \"quietly\", reporting `error_code` to cleanup callbacks."
    )]
    ///
    /// On Linux-PAM only, this sets the
    /// [`PAM_DATA_SILENT`](libpam_sys::PAM_DATA_SILENT) flag on the flags
    /// passed to the cleanup callbacks. This conventionally means that this
    /// `pam_end` call is occurring on a forked process, and that a session
    /// may still be open on the parent process, and modules "should not treat
    /// the call too seriously".
    ///
    /// # References
    #[doc = linklist!(pam_end: adg, _std)]
    ///
    #[doc = guide!(adg: "adg-interface-by-app-expected.html#adg-pam_end")]
    #[doc = stdlinks!(3 pam_end)]
    pub fn end_quiet(self, result: Result<()>) {
        let mut me = ManuallyDrop::new(self);
        let result = ErrorCode::result_to_c(result);
        #[cfg(pam_impl = "LinuxPam")]
        let result = result | libpam_sys::PAM_DATA_SILENT;
        unsafe {
            libpam_sys::pam_end(me.raw_mut(), result);
        }
    }

    /// Consumes this and gives you back the raw PAM handle.
    pub fn into_inner(self) -> NonNull<libpam_sys::pam_handle> {
        let me = ManuallyDrop::new(self);
        me.0
    }

    /// Gets a reference to the inner PAM handle.
    pub fn raw_ref(&self) -> &libpam_sys::pam_handle {
        unsafe { self.0.as_ref() }
    }
    /// Gets a mutable reference to the inner PAM handle.
    pub fn raw_mut(&mut self) -> &mut libpam_sys::pam_handle {
        unsafe { self.0.as_mut() }
    }
}

impl Drop for LibPamHandle {
    fn drop(&mut self) {
        unsafe { libpam_sys::pam_end(self.0.as_mut(), 0) };
    }
}

impl PamShared for LibPamHandle {
    #[cfg(any())]
    fn log(&self, level: Level, loc: Location<'_>, entry: &str) {
        let entry = match CString::new(entry).or_else(|_| CString::new(dbg!(entry))) {
            Ok(cstr) => cstr,
            _ => return,
        };
        #[cfg(pam_impl = "LinuxPam")]
        {
            _ = loc;
            // SAFETY: We're calling this function with a known value.
            unsafe {
                libpam_sys::pam_syslog(self, level as c_int, "%s\0".as_ptr().cast(), entry.as_ptr())
            }
        }
        #[cfg(pam_impl = "OpenPam")]
        {
            let func = CString::new(loc.function).unwrap_or(CString::default());
            // SAFETY: We're calling this function with a known value.
            unsafe {
                libpam_sys::_openpam_log(
                    level as c_int,
                    func.as_ptr(),
                    "%s\0".as_ptr().cast(),
                    entry.as_ptr(),
                )
            }
        }
    }

    fn log(&self, _level: Level, _loc: Location<'_>, _entry: &str) {}

    fn username(&mut self, prompt: Option<&OsStr>) -> Result<OsString> {
        let prompt = memory::option_cstr_os(prompt);
        let mut output: *const c_char = ptr::null();
        let ret = unsafe {
            libpam_sys::pam_get_user(
                self.raw_mut(),
                &mut output,
                memory::prompt_ptr(prompt.as_deref()),
            )
        };
        ErrorCode::result_from(ret)?;
        Ok(unsafe { memory::copy_pam_string(output).ok_or(ErrorCode::ConversationError)? })
    }

    fn environ(&self) -> impl EnvironMap {
        LibPamEnviron::new(self)
    }

    fn environ_mut(&mut self) -> impl EnvironMapMut {
        LibPamEnvironMut::new(self)
    }

    cstr_item!(get = user_item, item = ItemType::User);
    cstr_item!(set = set_user_item, item = ItemType::User);
    cstr_item!(get = service, item = ItemType::Service);
    cstr_item!(set = set_service, item = ItemType::Service);
    cstr_item!(get = user_prompt, item = ItemType::UserPrompt);
    cstr_item!(set = set_user_prompt, item = ItemType::UserPrompt);
    cstr_item!(get = tty_name, item = ItemType::Tty);
    cstr_item!(set = set_tty_name, item = ItemType::Tty);
    cstr_item!(get = remote_user, item = ItemType::RemoteUser);
    cstr_item!(set = set_remote_user, item = ItemType::RemoteUser);
    cstr_item!(get = remote_host, item = ItemType::RemoteHost);
    cstr_item!(set = set_remote_host, item = ItemType::RemoteHost);
    cstr_item!(set = set_authtok_item, item = ItemType::AuthTok);
    cstr_item!(set = set_old_authtok_item, item = ItemType::OldAuthTok);
}

impl Conversation for LibPamHandle {
    fn communicate(&self, messages: &[Exchange]) {
        match self.conversation_item() {
            Ok(conv) => conv.communicate(messages),
            Err(e) => {
                for msg in messages {
                    msg.set_error(e)
                }
            }
        }
    }
}

impl PamHandleModule for LibPamHandle {
    fn authtok(&mut self, prompt: Option<&OsStr>) -> Result<OsString> {
        self.get_authtok(prompt, ItemType::AuthTok)
    }

    fn old_authtok(&mut self, prompt: Option<&OsStr>) -> Result<OsString> {
        self.get_authtok(prompt, ItemType::OldAuthTok)
    }

    cstr_item!(get = authtok_item, item = ItemType::AuthTok);
    cstr_item!(get = old_authtok_item, item = ItemType::OldAuthTok);
}

/// Function called at the end of a PAM session that is called to clean up
/// a value previously provided to PAM in a `pam_set_data` call.
///
/// You should never call this yourself.
extern "C" fn set_data_cleanup<T>(_: *const libc::c_void, c_data: *mut libc::c_void, _: c_int) {
    unsafe {
        let _data: Box<T> = Box::from_raw(c_data.cast());
    }
}

// Implementations of internal functions.
impl LibPamHandle {
    #[cfg(any(pam_impl = "LinuxPam", pam_impl = "OpenPam"))]
    fn get_authtok(&mut self, prompt: Option<&OsStr>, item_type: ItemType) -> Result<OsString> {
        let prompt = memory::option_cstr_os(prompt);
        let mut output: *const c_char = ptr::null_mut();
        // SAFETY: We're calling this with known-good values.
        let res = unsafe {
            libpam_sys::pam_get_authtok(
                self.raw_mut(),
                item_type.into(),
                &mut output,
                memory::prompt_ptr(prompt.as_deref()),
            )
        };
        ErrorCode::result_from(res)?;
        // SAFETY: We got this string from PAM.
        unsafe { memory::copy_pam_string(output) }.ok_or(ErrorCode::ConversationError)
    }

    #[cfg(not(any(pam_impl = "LinuxPam", pam_impl = "OpenPam")))]
    fn get_authtok(&mut self, prompt: Option<&str>, item_type: ItemType) -> Result<String> {
        Err(ErrorCode::ConversationError)
    }

    /// Gets a C string item.
    ///
    /// # Safety
    ///
    /// You better be requesting an item which is a C string.
    unsafe fn get_cstr_item(&self, item_type: ItemType) -> Result<Option<OsString>> {
        let mut output = ptr::null();
        let ret =
            unsafe { libpam_sys::pam_get_item(self.raw_ref(), item_type as c_int, &mut output) };
        ErrorCode::result_from(ret)?;
        Ok(memory::copy_pam_string(output.cast()))
    }

    /// Sets a C string item.
    ///
    /// # Safety
    ///
    /// You better be setting an item which is a C string.
    unsafe fn set_cstr_item(&mut self, item_type: ItemType, data: Option<&OsStr>) -> Result<()> {
        let data_str = memory::option_cstr_os(data);
        let ret = unsafe {
            libpam_sys::pam_set_item(
                self.raw_mut(),
                item_type as c_int,
                memory::prompt_ptr(data_str.as_deref()).cast(),
            )
        };
        ErrorCode::result_from(ret)
    }

    /// Gets the `PAM_CONV` item from the handle.
    fn conversation_item(&self) -> Result<&PamConv> {
        let output: *const PamConv = ptr::null_mut();
        let result = unsafe {
            libpam_sys::pam_get_item(
                self.raw_ref(),
                ItemType::Conversation.into(),
                &mut output.cast(),
            )
        };
        ErrorCode::result_from(result)?;
        // SAFETY: We got this result from PAM, and we're checking if it's null.
        unsafe { output.as_ref() }.ok_or(ErrorCode::ConversationError)
    }
}

/// Identifies what is being gotten or set with `pam_get_item`
/// or `pam_set_item`.
#[derive(TryFromPrimitive, IntoPrimitive)]
#[repr(i32)]
#[non_exhaustive] // because C could give us anything!
pub enum ItemType {
    /// The PAM service name.
    Service = constants::PAM_SERVICE,
    /// The user's login name.
    User = constants::PAM_USER,
    /// The TTY name.
    Tty = constants::PAM_TTY,
    /// The remote host (if applicable).
    RemoteHost = constants::PAM_RHOST,
    /// The conversation struct (not a CStr-based item).
    Conversation = constants::PAM_CONV,
    /// The authentication token (password).
    AuthTok = constants::PAM_AUTHTOK,
    /// The old authentication token (when changing passwords).
    OldAuthTok = constants::PAM_OLDAUTHTOK,
    /// The remote user's name.
    RemoteUser = constants::PAM_RUSER,
    /// The prompt shown when requesting a username.
    UserPrompt = constants::PAM_USER_PROMPT,
    #[cfg(feature = "linux-pam-ext")]
    /// App-supplied function to override failure delays.
    FailDelay = constants::PAM_FAIL_DELAY,
    #[cfg(feature = "linux-pam-ext")]
    /// X display name.
    XDisplay = constants::PAM_XDISPLAY,
    #[cfg(feature = "linux-pam-ext")]
    /// X server authentication data.
    XAuthData = constants::PAM_XAUTHDATA,
    #[cfg(feature = "linux-pam-ext")]
    /// The type of `pam_get_authtok`.
    AuthTokType = constants::PAM_AUTHTOK_TYPE,
}