Mercurial > crates > nonstick
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 + ); + } +}