diff src/handle.rs @ 66:a674799a5cd3

Make `PamHandle` and `PamModuleHandle` traits. This creates traits for PAM functionality and pulls the definitions of that functionality out of the original `PamHandle` (renamed to `LibPamHandle`) and into those traits. This supports testing PAM module implementations using mock PAM library implementations. Also uses a better representation of opaque pointers.
author Paul Fisher <paul@pfish.zone>
date Tue, 27 May 2025 14:37:28 -0400
parents bbe84835d6db
children
line wrap: on
line diff
--- a/src/handle.rs	Thu May 22 02:08:10 2025 -0400
+++ b/src/handle.rs	Tue May 27 14:37:28 2025 -0400
@@ -1,18 +1,17 @@
-//! Where [PamHandle] lives.
+//! The wrapper types and traits for handles into the PAM library.
+use crate::constants::{ErrorCode, Result};
 use crate::items::{Item, ItemType};
-use crate::{memory, pam_ffi, ErrorCode};
+use crate::{memory, pam_ffi};
 use libc::c_char;
 use secure_string::SecureString;
 use std::ffi::{c_int, CString};
+use std::mem;
 
-/// Your interface to a PAM handle.
+/// Features of a PAM handle that are available to applications and modules.
 ///
-/// This structure wraps an opaque PAM-provided pointer and gives you
-/// a safe and familiar struct-based API to interact with PAM.
-#[repr(transparent)]
-pub struct PamHandle(*mut libc::c_void);
-
-impl PamHandle {
+/// You probably want [`LibPamHandle`]. This trait is intended to allow creating
+/// mock PAM handle types used for testing PAM modules and applications.
+pub trait PamHandle {
     /// Retrieves the name of the user who is authenticating or logging in.
     ///
     /// This is effectively like `handle.get_item::<Item::User>()`.
@@ -23,7 +22,7 @@
     ///
     /// ```no_run
     /// # use nonstick::PamHandle;
-    /// # fn _doc(handle: &PamHandle) -> Result<(), Box<dyn std::error::Error>> {
+    /// # fn _doc(handle: &impl PamHandle) -> Result<(), Box<dyn std::error::Error>> {
     /// // Get the username using the default prompt.
     /// let user = handle.get_user(None)?;
     /// // Get the username using a custom prompt.
@@ -31,18 +30,10 @@
     /// # Ok(())
     /// # }
     /// ```
-    /// 
+    ///
     /// [man]: https://www.man7.org/linux/man-pages/man3/pam_get_user.3.html
     /// [mwg]: https://www.chiark.greenend.org.uk/doc/libpam-doc/html/mwg-expected-by-module-item.html#mwg-pam_get_user
-    pub fn get_user(&self, prompt: Option<&str>) -> crate::Result<String> {
-        let prompt = memory::option_cstr(prompt)?;
-        let mut output: *const c_char = std::ptr::null_mut();
-        let ret = unsafe {
-            pam_ffi::pam_get_user(self.0, &mut output, memory::prompt_ptr(prompt.as_ref()))
-        };
-        ErrorCode::result_from(ret)?;
-        memory::copy_pam_string(output)
-    }
+    fn get_user(&self, prompt: Option<&str>) -> Result<String>;
 
     /// Retrieves the authentication token from the user.
     ///
@@ -55,7 +46,7 @@
     ///
     /// ```no_run
     /// # use nonstick::PamHandle;
-    /// # fn _doc(handle: &PamHandle) -> Result<(), Box<dyn std::error::Error>> {
+    /// # fn _doc(handle: &impl PamHandle) -> Result<(), Box<dyn std::error::Error>> {
     /// // Get the user's password using the default prompt.
     /// let pass = handle.get_authtok(None)?;
     /// // Get the user's password using a custom prompt.
@@ -63,23 +54,10 @@
     /// Ok(())
     /// # }
     /// ```
-    /// 
+    ///
     /// [man]: https://www.man7.org/linux/man-pages/man3/pam_get_authtok.3.html
     /// [mwg]: https://www.chiark.greenend.org.uk/doc/libpam-doc/html/mwg-expected-by-module-item.html#mwg-pam_get_item
-    pub fn get_authtok(&self, prompt: Option<&str>) -> crate::Result<SecureString> {
-        let prompt = memory::option_cstr(prompt)?;
-        let mut output: *const c_char = std::ptr::null_mut();
-        let res = unsafe {
-            pam_ffi::pam_get_authtok(
-                self.0,
-                ItemType::AuthTok.into(),
-                &mut output,
-                memory::prompt_ptr(prompt.as_ref()),
-            )
-        };
-        ErrorCode::result_from(res)?;
-        memory::copy_pam_string(output).map(SecureString::from)
-    }
+    fn get_authtok(&self, prompt: Option<&str>) -> Result<SecureString>;
 
     /// Retrieves an [Item] that has been set, possibly by the PAM client.
     ///
@@ -96,7 +74,7 @@
     /// # use nonstick::PamHandle;
     /// use nonstick::items::Service;
     ///
-    /// # fn _doc(pam_handle: &PamHandle) -> Result<(), Box<dyn std::error::Error>> {
+    /// # fn _doc(pam_handle: &impl PamHandle) -> Result<(), Box<dyn std::error::Error>> {
     /// let svc: Option<Service> = pam_handle.get_item()?;
     /// match svc {
     ///     Some(name) => eprintln!("The calling service name is {:?}", name.to_string_lossy()),
@@ -108,19 +86,7 @@
     ///
     /// [man]: https://www.man7.org/linux/man-pages/man3/pam_get_item.3.html
     /// [mwg]: https://www.chiark.greenend.org.uk/doc/libpam-doc/html/mwg-expected-by-module-item.html#mwg-pam_get_item
-    pub fn get_item<T: Item>(&self) -> crate::Result<Option<T>> {
-        let mut ptr: *const libc::c_void = std::ptr::null();
-        let out = unsafe {
-            let ret = pam_ffi::pam_get_item(self.0, T::type_id().into(), &mut ptr);
-            ErrorCode::result_from(ret)?;
-            let typed_ptr: *const T::Raw = ptr.cast();
-            match typed_ptr.is_null() {
-                true => None,
-                false => Some(T::from_raw(typed_ptr)),
-            }
-        };
-        Ok(out)
-    }
+    fn get_item<T: Item>(&self) -> Result<Option<T>>;
 
     /// Sets an item in the PAM context. It can be retrieved using [`get_item`](Self::get_item).
     ///
@@ -129,12 +95,40 @@
     ///
     /// [man]: https://www.man7.org/linux/man-pages/man3/pam_set_item.3.html
     /// [mwg]: https://www.chiark.greenend.org.uk/doc/libpam-doc/html/mwg-expected-by-module-item.html#mwg-pam_set_item
-    pub fn set_item<T: Item>(&mut self, item: T) -> crate::Result<()> {
-        let ret =
-            unsafe { pam_ffi::pam_set_item(self.0, T::type_id().into(), item.into_raw().cast()) };
-        ErrorCode::result_from(ret)
-    }
+    fn set_item<T: Item>(&mut self, item: T) -> Result<()>;
 
+    /// Closes the PAM session on an owned PAM handle.
+    ///
+    /// This should be called with the result of the application's last call
+    /// into PAM services. Since this is only applicable to *owned* PAM handles,
+    /// a PAM module should never call this (and it will never be handed
+    /// an owned `PamHandle` that it can `close`).
+    ///
+    /// See the [`pam_end` manual page][man] for more information.
+    ///
+    /// ```no_run
+    /// # use nonstick::PamHandle;
+    /// # use std::error::Error;
+    /// # fn _doc(handle: impl PamHandle, auth_result: nonstick::Result<()>) -> Result<(), Box<dyn Error>> {
+    /// // Earlier: authentication was performed and the result was stored
+    /// // into auth_result.
+    /// handle.close(auth_result)?;
+    /// # Ok(())
+    /// # }
+    /// ```
+    ///
+    /// [man]: https://www.man7.org/linux/man-pages/man3/pam_end.3.html
+    fn close(self, status: Result<()>) -> Result<()>;
+}
+
+/// Functionality of a PAM handle that can be expected by a PAM module.
+///
+/// If you are not writing a PAM module (e.g., you are writing an application),
+/// you should not use any of the functionality exposed by this trait.
+///
+/// Like [`PamHandle`], this is intended to allow creating mock implementations
+/// of PAM for testing PAM modules.
+pub trait PamModuleHandle: PamHandle {
     /// Gets some pointer, identified by `key`, that has been set previously
     /// using [`set_data`](Self::set_data).
     ///
@@ -151,18 +145,7 @@
     ///
     /// [man]: https://www.man7.org/linux/man-pages/man3/pam_get_data.3.html
     /// [mwg]: https://www.chiark.greenend.org.uk/doc/libpam-doc/html/mwg-expected-by-module-item.html#mwg-pam_get_data
-    pub unsafe fn get_data<T>(&self, key: &str) -> crate::Result<Option<&T>> {
-        let c_key = CString::new(key).map_err(|_| ErrorCode::ConversationError)?;
-        let mut ptr: *const libc::c_void = std::ptr::null();
-        ErrorCode::result_from(pam_ffi::pam_get_data(self.0, c_key.as_ptr(), &mut ptr))?;
-        match ptr.is_null() {
-            true => Ok(None),
-            false => {
-                let typed_ptr = ptr.cast();
-                Ok(Some(&*typed_ptr))
-            }
-        }
-    }
+    unsafe fn get_data<T>(&self, key: &str) -> Result<Option<&T>>;
 
     /// Stores a pointer that can be retrieved later with [`get_data`](Self::get_data).
     ///
@@ -176,33 +159,126 @@
     ///
     /// [man]: https://www.man7.org/linux/man-pages/man3/pam_set_data.3.html
     /// [mwg]: https://www.chiark.greenend.org.uk/doc/libpam-doc/html/mwg-expected-by-module-item.html#mwg-pam_set_data
-    pub fn set_data<T>(&mut self, key: &str, data: Box<T>) -> crate::Result<()> {
-        let c_key = CString::new(key).map_err(|_| ErrorCode::ConversationError)?;
+    fn set_data<T>(&mut self, key: &str, data: Box<T>) -> Result<()>;
+}
+
+/// A [`PamHandle`] backed by `libpam`, i.e., a real PAM handle.
+///
+/// This structure wraps an opaque PAM handle and gives you a nice Rusty
+/// interface to use PAM.
+#[repr(C)]
+pub struct LibPamHandle(pam_ffi::Handle);
+
+impl LibPamHandle {
+    /// Converts a pointer passed from PAM into a borrowed handle.
+    ///
+    /// # Safety
+    ///
+    /// It is your responsibility to provide a valid pointer.
+    pub unsafe fn from_ptr<'a>(ptr: *mut libc::c_void) -> &'a mut LibPamHandle {
+        &mut *(ptr as *mut LibPamHandle)
+    }
+}
+
+impl Drop for LibPamHandle {
+    /// Ends the PAM session with a zero error code.
+    fn drop(&mut self) {
+        unsafe {
+            pam_ffi::pam_end(&mut self.0, 0);
+        }
+    }
+}
+
+impl PamHandle for LibPamHandle {
+    fn get_user(&self, prompt: Option<&str>) -> crate::Result<String> {
+        let prompt = memory::option_cstr(prompt)?;
+        let mut output: *const c_char = std::ptr::null_mut();
         let ret = unsafe {
-            pam_ffi::pam_set_data(
-                self.0,
-                c_key.as_ptr(),
-                Box::into_raw(data).cast(),
-                Self::set_data_cleanup::<T>,
+            pam_ffi::pam_get_user(&self.0, &mut output, memory::prompt_ptr(prompt.as_ref()))
+        };
+        ErrorCode::result_from(ret)?;
+        memory::copy_pam_string(output)
+    }
+
+    fn get_authtok(&self, prompt: Option<&str>) -> crate::Result<SecureString> {
+        let prompt = memory::option_cstr(prompt)?;
+        let mut output: *const c_char = std::ptr::null_mut();
+        let res = unsafe {
+            pam_ffi::pam_get_authtok(
+                &self.0,
+                ItemType::AuthTok.into(),
+                &mut output,
+                memory::prompt_ptr(prompt.as_ref()),
             )
         };
+        ErrorCode::result_from(res)?;
+        memory::copy_pam_string(output).map(SecureString::from)
+    }
+
+    fn get_item<T: Item>(&self) -> crate::Result<Option<T>> {
+        let mut ptr: *const libc::c_void = std::ptr::null();
+        let out = unsafe {
+            let ret = pam_ffi::pam_get_item(&self.0, T::type_id().into(), &mut ptr);
+            ErrorCode::result_from(ret)?;
+            let typed_ptr: *const T::Raw = ptr.cast();
+            match typed_ptr.is_null() {
+                true => None,
+                false => Some(T::from_raw(typed_ptr)),
+            }
+        };
+        Ok(out)
+    }
+
+    fn set_item<T: Item>(&mut self, item: T) -> crate::Result<()> {
+        let ret = unsafe {
+            pam_ffi::pam_set_item(&mut self.0, T::type_id().into(), item.into_raw().cast())
+        };
         ErrorCode::result_from(ret)
     }
 
-    /// 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());
-        }
+    fn close(mut self, status: Result<()>) -> Result<()> {
+        let result = unsafe { pam_ffi::pam_end(&mut self.0, ErrorCode::result_to_c(status)) };
+        // Since we've already `pam_end`ed this session, we don't want it to be
+        // double-freed on drop.
+        mem::forget(self);
+        ErrorCode::result_from(result)
     }
 }
 
-impl From<*mut libc::c_void> for PamHandle {
-    /// Wraps an internal Handle pointer.
-    fn from(value: *mut libc::c_void) -> Self {
-        Self(value)
+impl PamModuleHandle for LibPamHandle {
+    unsafe fn get_data<T>(&self, key: &str) -> crate::Result<Option<&T>> {
+        let c_key = CString::new(key).map_err(|_| ErrorCode::ConversationError)?;
+        let mut ptr: *const libc::c_void = std::ptr::null();
+        ErrorCode::result_from(pam_ffi::pam_get_data(&self.0, c_key.as_ptr(), &mut ptr))?;
+        match ptr.is_null() {
+            true => Ok(None),
+            false => {
+                let typed_ptr = ptr.cast();
+                Ok(Some(&*typed_ptr))
+            }
+        }
+    }
+
+    fn set_data<T>(&mut self, key: &str, data: Box<T>) -> crate::Result<()> {
+        let c_key = CString::new(key).map_err(|_| ErrorCode::ConversationError)?;
+        let ret = unsafe {
+            pam_ffi::pam_set_data(
+                &mut self.0,
+                c_key.as_ptr(),
+                Box::into_raw(data).cast(),
+                set_data_cleanup::<T>,
+            )
+        };
+        ErrorCode::result_from(ret)
     }
 }
+
+/// 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());
+    }
+}