changeset 92:5ddbcada30f2

Add the ability to log against a PAM handle. PAM impls provide a way to log to syslog. This exposes it via nonstick.
author Paul Fisher <paul@pfish.zone>
date Sun, 22 Jun 2025 19:29:32 -0400
parents 039aae9a01f7
children efc2b56c8928
files Cargo.toml build.rs src/constants.rs src/handle.rs src/lib.rs src/libpam/handle.rs src/libpam/mod.rs src/libpam/pam_ffi.rs src/logging.rs
diffstat 9 files changed, 265 insertions(+), 16 deletions(-) [+]
line wrap: on
line diff
--- a/Cargo.toml	Wed Jun 18 16:30:41 2025 -0400
+++ b/Cargo.toml	Sun Jun 22 19:29:32 2025 -0400
@@ -22,8 +22,8 @@
 bitflags = "2.9.0"
 libc = "0.2.97"
 num_enum = "0.7.3"
+regex = "1.11.1"
 secure-string = "0.3.0"
-thiserror = "2.0.12"
 
 [build-dependencies]
 bindgen = "0.72.0"
--- a/build.rs	Wed Jun 18 16:30:41 2025 -0400
+++ b/build.rs	Sun Jun 22 19:29:32 2025 -0400
@@ -23,12 +23,13 @@
         let linux_builder = common_builder
             .clone()
             // This function is not available in OpenPAM.
-            // We don't use it, but we include it so that if the user
-            // tries to run this against the wrong PAM library, it fails.
-            .allowlist_function("pam_start_confdir")
+            // That means if somebody tries to run a binary compiled for
+            // Linux-PAM against a different impl, it will fail.
+            .allowlist_function("pam_syslog")
             .header_contents(
                 "linux-pam.h",
                 r#"
+                #include <syslog.h> // for log levels
                 #include <security/_pam_types.h>
                 #include <security/pam_appl.h>
                 #include <security/pam_ext.h>
@@ -38,9 +39,9 @@
         let openpam_builder = common_builder
             .clone()
             // This function is not available in Linux-PAM.
-            // We don't use it, but we include it so that if the user
-            // tries to run this against the wrong PAM library, it fails.
-            .allowlist_function("pam_setenv")
+            // That means if somebody tries to run a binary compiled for
+            // OpenPAM against a different impl, it will fail.
+            .allowlist_function("openpam_log")
             .header_contents(
                 "openpam.h",
                 r#"
--- a/src/constants.rs	Wed Jun 18 16:30:41 2025 -0400
+++ b/src/constants.rs	Sun Jun 22 19:29:32 2025 -0400
@@ -22,6 +22,8 @@
 /// and not a magic number.**
 #[cfg(not(feature = "link"))]
 mod pam_ffi {
+    use std::ffi::c_uint;
+
     macro_rules! define {
         ($(#[$attr:meta])* $($name:ident = $value:expr),+) => {
             define!(
@@ -81,7 +83,9 @@
         PAM_USER_UNKNOWN = 553
     );
 
-    fn strerror(val: c_uint) -> Option<&'static str> {
+    /// Dummy implementation of strerror so that it always returns None.
+    pub fn strerror(val: c_uint) -> Option<&'static str> {
+        _ = val;
         None
     }
 }
--- a/src/handle.rs	Wed Jun 18 16:30:41 2025 -0400
+++ b/src/handle.rs	Sun Jun 22 19:29:32 2025 -0400
@@ -2,6 +2,7 @@
 
 use crate::constants::Result;
 use crate::conv::Conversation;
+use crate::logging::Level;
 
 macro_rules! trait_item {
     ($(#[$md:meta])* get = $getter:ident, item = $item:literal $(, see = $see:path)?) => {
@@ -56,6 +57,31 @@
 /// This trait is intended to allow creating mock PAM handle types
 /// to test PAM modules and applications.
 pub trait PamShared {
+    /// Logs something via this PAM handle.
+    ///
+    /// You probably want to use one of the logging macros,
+    /// like [`error!`], [`warning!`], [`info!`], or [`debug!`].
+    ///
+    /// In most PAM implementations, this will go to syslog.
+    ///
+    /// # Example
+    ///
+    /// ```no_run
+    /// # use nonstick::{PamShared};
+    /// # use nonstick::logging::Level;
+    /// # let pam_hdl: Box<dyn PamShared> = todo!();
+    /// # let delay_ms = 100;
+    /// # let url = "https://zombo.com";
+    /// // Usually, instead of calling this manually, just use the macros.
+    /// nonstick::error!(pam_hdl, "something bad happened!");
+    /// nonstick::warn!(pam_hdl, "loading information took {delay_ms} ms");
+    /// nonstick::info!(pam_hdl, "using network backend");
+    /// nonstick::debug!(pam_hdl, "sending GET request to {url}");
+    /// // But if you really want to, you can call this yourself:
+    /// pam_hdl.log(Level::Warning, "this is unnecessarily verbose");
+    /// ```
+    fn log(&self, level: Level, entry: &str);
+
     /// Retrieves the name of the user who is authenticating or logging in.
     ///
     /// If the username has previously been obtained, this uses that username;
--- a/src/lib.rs	Wed Jun 18 16:30:41 2025 -0400
+++ b/src/lib.rs	Sun Jun 22 19:29:32 2025 -0400
@@ -33,6 +33,7 @@
 
 #[cfg(feature = "link")]
 mod libpam;
+pub mod logging;
 
 #[cfg(feature = "link")]
 pub use crate::libpam::{LibPamHandle, OwnedLibPamHandle};
--- a/src/libpam/handle.rs	Wed Jun 18 16:30:41 2025 -0400
+++ b/src/libpam/handle.rs	Sun Jun 22 19:29:32 2025 -0400
@@ -4,14 +4,16 @@
 use crate::handle::PamShared;
 pub use crate::libpam::pam_ffi::LibPamHandle;
 use crate::libpam::{memory, pam_ffi};
+use crate::logging::Level;
 use crate::{Conversation, PamHandleModule};
 use num_enum::{IntoPrimitive, TryFromPrimitive};
 use std::cell::Cell;
-use std::ffi::{c_char, c_int};
+use std::ffi::{c_char, c_int, CString};
 use std::marker::PhantomData;
 use std::ops::{Deref, DerefMut};
 use std::ptr;
 
+/// Owner for a PAM handle.
 struct HandleWrap(*mut LibPamHandle);
 
 impl Deref for HandleWrap {
@@ -60,6 +62,7 @@
     }
 }
 
+/// 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<&str>> {
@@ -74,6 +77,27 @@
 }
 
 impl PamShared for LibPamHandle {
+    fn log(&self, level: Level, entry: &str) {
+        let entry = match CString::new(entry).or_else(|_| CString::new(dbg!(entry))) {
+            Ok(cstr) => cstr,
+            _ => return,
+        };
+        #[cfg(pam_impl = "linux-pam")]
+        {
+            // SAFETY: We're calling this function with a known value.
+            unsafe {
+                pam_ffi::pam_syslog(self, level as c_int, c"%s".as_ptr().cast(), entry.as_ptr())
+            }
+        }
+        #[cfg(pam_impl = "openpam")]
+        {
+            // SAFETY: We're calling this function with a known value.
+            unsafe {
+                pam_ffi::openpam_log(self, level as c_int, c"%s".as_ptr().cast(), entry.as_ptr())
+            }
+        }
+    }
+
     fn username(&mut self, prompt: Option<&str>) -> Result<&str> {
         let prompt = memory::option_cstr(prompt)?;
         let mut output: *const c_char = ptr::null();
@@ -224,6 +248,9 @@
 }
 
 impl PamShared for OwnedLibPamHandle<'_> {
+    fn log(&self, level: Level, entry: &str) {
+        self.handle.log(level, entry)
+    }
     delegate!(fn username(&mut self, prompt: Option<&str>) -> Result<&str>);
     delegate!(get = user_item, set = set_user_item);
     delegate!(get = service, set = set_service);
--- a/src/libpam/mod.rs	Wed Jun 18 16:30:41 2025 -0400
+++ b/src/libpam/mod.rs	Sun Jun 22 19:29:32 2025 -0400
@@ -1,10 +1,8 @@
-//! The PAM library FFI and helpers for managing it.
+//! The implementation of the PAM interface that wraps `libpam`.
 //!
-//! This includes the functions provided by PAM and the data structures
-//! used by PAM, as well as a few low-level abstractions for dealing with
-//! those data structures.
-//!
-//! Everything in here is hazmat.
+//! While you're going to want to write PAM modules and applications against
+//! the interfaces in [the `handle` module](crate::handle) for testability,
+//! this is (probably) what will be used behind the scenes.
 
 #![allow(dead_code)]
 
--- a/src/libpam/pam_ffi.rs	Wed Jun 18 16:30:41 2025 -0400
+++ b/src/libpam/pam_ffi.rs	Sun Jun 22 19:29:32 2025 -0400
@@ -1,6 +1,6 @@
 //! The types that are directly represented in PAM function signatures.
 
-#![allow(non_camel_case_types)]
+#![allow(non_camel_case_types, non_upper_case_globals)]
 
 use crate::libpam::memory::Immovable;
 use std::ffi::{c_int, c_uint, c_void, CStr};
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/logging.rs	Sun Jun 22 19:29:32 2025 -0400
@@ -0,0 +1,192 @@
+//! PAM logging variables and macros.
+//!
+//! PAM implementations usually include the ability to log to syslog in a way
+//! that is associated with the log entry itself. This module defines the enums
+//! and macros for logging.
+//!
+//! For more details, see [`PamShared::log`](crate::PamShared::log).
+//!
+//! We can't use [the `log` crate](https://docs.rs/log) because that requires
+//! that any `Log` implementors be `Sync` and `Send`, and a PAM handle
+//! may be neither. Furthermore, PAM handles are passed to PAM modules in
+//! dynamic libraries, and `log` doesn't work across dynamic linking boundaries.
+//!
+//! A `PamShared` implementation may still use the `log` crate on the backend,
+//! and may even itself implement `log::Log`, but that interface is not exposed
+//! to the generic PAM user.
+
+#[cfg(feature = "link")]
+mod levels {
+    pub use internal::*;
+    #[cfg(pam_impl = "linux-pam")]
+    mod internal {
+        use crate::libpam::pam_ffi;
+        pub const ERROR: u32 = pam_ffi::LOG_ERR;
+        pub const WARN: u32 = pam_ffi::LOG_WARNING;
+        pub const INFO: u32 = pam_ffi::LOG_INFO;
+        pub const DEBUG: u32 = pam_ffi::LOG_DEBUG;
+    }
+    #[cfg(pam_impl = "openpam")]
+    mod internal {
+        use crate::libpam::pam_ffi;
+        pub const ERROR: u32 = pam_ffi::PAM_LOG_ERROR;
+        pub const WARN: u32 = pam_ffi::PAM_LOG_NOTICE;
+        pub const INFO: u32 = pam_ffi::PAM_LOG_VERBOSE;
+        pub const DEBUG: u32 = pam_ffi::PAM_LOG_DEBUG;
+    }
+}
+
+#[cfg(not(feature = "link"))]
+mod levels {
+    pub const ERROR: u32 = 2255887;
+    pub const WARN: u32 = 7265000;
+    pub const INFO: u32 = 7762323;
+    pub const DEBUG: u32 = 8675309;
+}
+
+/// An entry to be added to the log.
+///
+/// The levels are in descending order of importance and correspond roughly
+/// to the similarly-named levels in the `log` crate.
+///
+/// In all implementations, these are ordered such that `Error`, `Warning`,
+/// `Info`, and `Debug` are in ascending order.
+#[derive(Debug, PartialEq, Ord, PartialOrd, Eq)]
+#[repr(u32)]
+pub enum Level {
+    Error = levels::ERROR,
+    Warning = levels::WARN,
+    Info = levels::INFO,
+    Debug = levels::DEBUG,
+}
+
+/// Here's the guts of the logger thingy. You shouldn't be using this!
+#[doc(hidden)]
+#[macro_export]
+macro_rules! __log_internal {
+    ($handle:expr, $level:ident, $($arg:tt)+) => {
+        $handle.log($crate::logging::Level::$level, &format!($($arg)+));
+    }
+}
+
+/// Logs a message at error level via the given PAM handle.
+///
+/// This supports `format!`-style formatting.
+///
+/// # Example
+///
+/// ```no_run
+/// # let pam_handle: Box<dyn nonstick::PamShared> = unimplemented!();
+/// # let load_error = "xxx";
+/// nonstick::error!(pam_handle, "error loading data from data source: {load_error}");
+/// // Will log a message like "error loading data from data source: timed out"
+/// // at ERROR level on syslog.
+/// ```
+#[macro_export]
+macro_rules! error { ($handle:expr, $($arg:tt)+) => { $crate::__log_internal!($handle, Error, $($arg)+);}}
+
+/// Logs a message at warning level via the given PAM handle.
+///
+/// This supports `format!`-style formatting.
+///
+/// # Example
+///
+/// ```no_run
+/// # let pam_handle: Box<dyn nonstick::PamShared> = unimplemented!();
+/// # let latency_ms = "xxx";
+/// nonstick::warn!(pam_handle, "loading took too long: {latency_ms} ms");
+/// // Will log a message like "loading took too long: 495 ms"
+/// // at WARN level on syslog.
+/// ```
+#[macro_export]
+macro_rules! warn { ($handle:expr, $($arg:tt)+) => { $crate::__log_internal!($handle, Warning, $($arg)+);}}
+
+/// Logs a message at info level via the given PAM handle.
+///
+/// This supports `format!`-style formatting.
+///
+/// # Example
+///
+/// ```no_run
+/// # let pam_handle: Box<dyn nonstick::PamShared> = unimplemented!();
+/// nonstick::info!(pam_handle, "using remote backend to load user data");
+/// // Will log a message like "using remote backend to load user data"
+/// // at INFO level on syslog.
+/// ```
+#[macro_export]
+macro_rules! info { ($handle:expr, $($arg:tt)+) => { $crate::__log_internal!($handle, Info, $($arg)+);}}
+
+/// Logs a message at debug level via the given PAM handle.
+///
+/// This level specially includes file/line/column information.
+/// This supports `format!`-style formatting.
+///
+/// # Example
+///
+/// ```no_run
+/// # let pam_handle: Box<dyn nonstick::PamShared> = unimplemented!();
+/// # let userinfo_url = "https://zombo.com/";
+/// nonstick::debug!(pam_handle, "making HTTP GET request to {userinfo_url}");
+/// // Will log a message like
+/// // "pam_http/lib.rs:39:14: making HTTP GET request to https://zombo.com/"
+/// // at DEBUG level on syslog.
+/// ```
+#[macro_export]
+macro_rules! debug {($handle:expr, $($arg:tt)+) => {
+    $crate::__log_internal!(
+        $handle, Debug,
+        "{}:{}:{}: {}", file!(), line!(), column!(), format_args!($($arg)+),
+    );
+}}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+    use regex::Regex;
+    use std::cell::RefCell;
+
+    #[test]
+    fn test_order() {
+        assert!(Level::Error < Level::Warning);
+        assert!(Level::Warning < Level::Info);
+        assert!(Level::Info < Level::Debug);
+    }
+
+    #[test]
+    fn test_logging() {
+        struct Logger(RefCell<Vec<(Level, String)>>);
+
+        impl Logger {
+            fn log(&self, level: Level, text: &str) {
+                self.0.borrow_mut().push((level, text.to_owned()))
+            }
+        }
+
+        let logger = Logger(Default::default());
+
+        let something = Level::Error;
+        error!(logger, "here is another thing: {}", 99);
+        warn!(logger, "watch out!");
+        info!(logger, "here is some info: {info}", info = "information");
+        debug!(logger, "here is something: {something:?}");
+
+        let mut logged = logger.0.into_inner();
+
+        let (last_level, last_string) = logged.pop().unwrap();
+        assert_eq!(Level::Debug, last_level);
+        let expr = Regex::new(r"^[^:]+:\d+:\d+: here is something: Error$").unwrap();
+        assert!(
+            expr.is_match(&last_string),
+            "{last_string:?} did not match {expr:?}"
+        );
+
+        assert_eq!(
+            vec![
+                (Level::Error, "here is another thing: 99".to_owned()),
+                (Level::Warning, "watch out!".to_owned()),
+                (Level::Info, "here is some info: information".to_owned()),
+            ],
+            logged
+        );
+    }
+}