changeset 146:1bc52025156b

Split PAM items into their own separate struct. To trim down the number of methods on `PamShared`, this puts all the Items into their own struct(s). This also makes the split between authtok/authtok_item easier to understand.
author Paul Fisher <paul@pfish.zone>
date Sun, 06 Jul 2025 19:10:26 -0400
parents 8f964b701652
children 4d7333337569
files src/_doc.rs src/constants.rs src/conv.rs src/handle.rs src/items.rs src/lib.rs src/libpam/handle.rs src/libpam/items.rs src/libpam/mod.rs src/libpam/module.rs src/module.rs testharness/src/lib.rs
diffstat 12 files changed, 406 insertions(+), 294 deletions(-) [+]
line wrap: on
line diff
--- a/src/_doc.rs	Sun Jul 06 19:04:57 2025 -0400
+++ b/src/_doc.rs	Sun Jul 06 19:10:26 2025 -0400
@@ -7,7 +7,7 @@
 /// # Examples
 ///
 /// ```ignore
-/// # use nonstick::{linklist, stdlinks};
+/// # use nonstick::_doc::{linklist, stdlinks};
 /// /// Here is a list of links:
 /// ///
 /// #[doc = linklist!(pam_get_authtok: man7, manbsd)]
@@ -21,34 +21,34 @@
     ($func:ident: adg$(, $rest:ident)*) => {
         concat!(
             "- [Application Developers' Guide on `", stringify!($func), "`][adg]\n",
-            $crate::linklist!($func: $($rest),*)
+            $crate::_doc::linklist!($func: $($rest),*)
         )
     };
     ($func:ident: mwg$(, $rest:ident)*) => {
         concat!(
             "- [Module Writers' Guide on `", stringify!($func), "`][mwg]\n",
-            $crate::linklist!($func: $($rest),*)
+            $crate::_doc::linklist!($func: $($rest),*)
         )
     };
     ($func:ident: _std$(, $rest:ident)*) => {
-        $crate::linklist!($func: man7, manbsd, xsso$(, $rest)*)
+        $crate::_doc::linklist!($func: man7, manbsd, xsso$(, $rest)*)
     };
     ($func:ident: man7$(, $rest:ident)*) => {
         concat!(
             "- [Linux-PAM manpage for `", stringify!($func), "`][man7]\n",
-            $crate::linklist!($func: $($rest),*)
+            $crate::_doc::linklist!($func: $($rest),*)
         )
     };
     ($func:ident: manbsd$(, $rest:ident)*) => {
         concat!(
             "- [OpenPAM manpage for `", stringify!($func), "`][manbsd]\n",
-            $crate::linklist!($func: $($rest),*)
+            $crate::_doc::linklist!($func: $($rest),*)
         )
     };
     ($func:ident: xsso$(, $rest:ident)*) => {
         concat!(
             "- [X/SSO spec for `", stringify!($func), "`][xsso]",
-            $crate::linklist!($func: $($rest),*)
+            $crate::_doc::linklist!($func: $($rest),*)
         )
     };
     ($func:ident:$(,)?) => { "" };
@@ -81,7 +81,7 @@
 /// # Examples
 ///
 /// ```ignore
-/// # use nonstick::man7;
+/// # use nonstick::_doc::man7;
 /// /// This contains a [link to the man page for malloc][man7].
 /// #[doc = man7!(3 malloc)]
 /// # fn do_whatever() {}
@@ -95,7 +95,7 @@
 /// ```
 macro_rules! man7 {
     ($n:literal $fn:ident $($anchor:literal)?) => {
-        $crate::man7!(man7: $n $fn $($anchor)?)
+        $crate::_doc::man7!(man7: $n $fn $($anchor)?)
     };
     ($name:ident: $n:literal $fn:ident $($anchor:literal)?) => {
         concat!(
@@ -111,7 +111,7 @@
 /// # Examples
 ///
 /// ```ignore
-/// # use nonstick::manbsd;
+/// # use nonstick::_doc::manbsd;
 /// // Both of these formulations create a link named `manbsd`.
 /// #[doc = manbsd!(3 fn_name)]
 /// #[doc = manbsd!(5 thing_name "SECTION")]
@@ -121,7 +121,7 @@
 /// ```
 macro_rules! manbsd {
     ($n:literal $func:ident $($anchor:literal)?) => {
-        $crate::manbsd!(manbsd: $n $func $($anchor)?)
+        $crate::_doc::manbsd!(manbsd: $n $func $($anchor)?)
     };
     ($name:ident: $n:literal $func:ident $($anchor:literal)?) => {
         concat!("[", stringify!($name), "]: ",
@@ -136,7 +136,7 @@
 /// # Examples
 ///
 /// ```ignore
-/// # use nonstick::xsso;
+/// # use nonstick::_doc::xsso;
 /// /// This docstring will [link to the X/SSO spec for `pam_set_item`][xsso].
 /// ///
 /// #[doc = xsso!(pam_set_item)]
@@ -150,8 +150,8 @@
 /// # fn do_whatever() {}
 /// ```
 macro_rules! xsso {
-    ($func:ident) => { $crate::xsso!(xsso: concat!(stringify!($func), ".htm")) };
-    ($page:literal) => { $crate::xsso!(xsso: $page) };
+    ($func:ident) => { $crate::_doc::xsso!(xsso: concat!(stringify!($func), ".htm")) };
+    ($page:literal) => { $crate::_doc::xsso!(xsso: $page) };
     ($name:ident: $page:expr) => {
         concat!("[", stringify!($name), "]: https://pubs.opengroup.org/onlinepubs/8329799/", $page)
     };
@@ -164,7 +164,7 @@
 /// # Examples
 ///
 /// ```ignore
-/// # use nonstick::stdlinks;
+/// # use nonstick::_doc::stdlinks;
 /// /// Check out [this][man7], [that][manbsd], or [the other][xsso].
 /// ///
 /// #[doc = stdlinks!(3 pam_get_item)]
@@ -172,7 +172,10 @@
 /// ```
 macro_rules! stdlinks {
     ($n:literal $func:ident) => {
-        concat!($crate::man7!($n $func), "\n", $crate::manbsd!($n $func), "\n", $crate::xsso!($func))
+        concat!(
+            $crate::_doc::man7!($n $func), "\n",
+            $crate::_doc::manbsd!($n $func), "\n",
+            $crate::_doc::xsso!($func))
     };
 }
 
--- a/src/constants.rs	Sun Jul 06 19:04:57 2025 -0400
+++ b/src/constants.rs	Sun Jul 06 19:10:26 2025 -0400
@@ -1,6 +1,6 @@
 //! Constants and enum values from the PAM library.
 
-use crate::{linklist, man7, manbsd, xsso};
+use crate::_doc::{linklist, man7, manbsd, xsso};
 use bitflags::bitflags;
 use num_enum::{IntoPrimitive, TryFromPrimitive};
 use std::error::Error;
--- a/src/conv.rs	Sun Jul 06 19:04:57 2025 -0400
+++ b/src/conv.rs	Sun Jul 06 19:10:26 2025 -0400
@@ -412,7 +412,7 @@
     };
 }
 
-impl<C: Conversation> ConversationAdapter for C {
+impl<C: Conversation + ?Sized> ConversationAdapter for C {
     conv_fn!(prompt(message: OsStr) -> OsString { QAndA });
     conv_fn!(masked_prompt(message: OsStr) -> OsString { MaskedQAndA } );
     conv_fn!(error_msg(message: OsStr) { ErrorMsg });
--- a/src/handle.rs	Sun Jul 06 19:04:57 2025 -0400
+++ b/src/handle.rs	Sun Jul 06 19:10:26 2025 -0400
@@ -1,59 +1,13 @@
 //! The wrapper types and traits for handles into the PAM library.
 
+use crate::_doc::{guide, linklist, man7, manbsd, stdlinks};
 use crate::constants::{Flags, Result};
 use crate::conv::Conversation;
 use crate::environ::{EnvironMap, EnvironMapMut};
+use crate::items::{getter, Items, ItemsMut};
 use crate::logging::{Level, Location};
-use crate::{guide, linklist, man7, manbsd, stdlinks};
 use std::ffi::{OsStr, OsString};
 
-macro_rules! trait_item {
-    ($(#[$md:meta])* get = $getter:ident, item = $item:literal $(, see = $see:path)?) => {
-        $(#[$md])*
-        #[doc = ""]
-        #[doc = concat!("Gets the `", $item, "` of the PAM handle.")]
-        $(
-            #[doc = concat!("See [`", stringify!($see), "`].")]
-        )?
-        ///
-        /// Returns a reference to the item's value, owned by PAM.
-        /// The item is assumed to be valid UTF-8 text.
-        /// If it is not, `ConversationError` is returned.
-        ///
-        /// # References
-        ///
-        #[doc = linklist!(pam_get_item: mwg, adg, _std)]
-        ///
-        #[doc = guide!(adg: "adg-interface-by-app-expected.html#adg-pam_get_item")]
-        #[doc = guide!(mwg: "mwg-expected-by-module-item.html#mwg-pam_get_item")]
-        #[doc = stdlinks!(3 pam_get_item)]
-        fn $getter(&self) -> Result<Option<OsString>>;
-    };
-    ($(#[$md:meta])* set = $setter:ident, item = $item:literal $(, see = $see:path)?) => {
-        $(#[$md])*
-        #[doc = ""]
-        #[doc = concat!("Sets the `", $item, "` from the PAM handle.")]
-        $(
-            #[doc = concat!("See [`", stringify!($see), "`].")]
-        )?
-        ///
-        /// Sets the item's value. PAM copies the string's contents.
-        ///
-        /// # Panics
-        ///
-        /// If the string contains a nul byte, this will panic.
-        ///
-        /// # References
-        ///
-        #[doc = linklist!(pam_set_item: mwg, adg, _std)]
-        ///
-        #[doc = guide!(adg: "adg-interface-by-app-expected.html#adg-pam_set_item")]
-        #[doc = guide!(mwg: "mwg-expected-by-module-item.html#mwg-pam_set_item")]
-        #[doc = stdlinks!(3 pam_set_item)]
-        fn $setter(&mut self, value: Option<&OsStr>) -> Result<()>;
-    };
-}
-
 /// Functionality for both PAM applications and PAM modules.
 ///
 /// This base trait includes features of a PAM handle that are available
@@ -127,138 +81,53 @@
     #[doc = guide!(mwg: "mwg-expected-by-module-item.html#mwg-pam_get_user")]
     fn username(&mut self, prompt: Option<&OsStr>) -> Result<OsString>;
 
-    /// The contents of the environment to set, read-only.
+    /// The contents of the environment to set for the logged-in user.
+    ///
+    /// # References
+    ///
+    #[doc = linklist!(pam_getenv: adg, mwg, _std)]
+    ///
+    #[doc = stdlinks!(3 pam_getenv)]
+    #[doc = guide!(adg: "adg-interface-by-app-expected.html#adg-pam_getenv")]
+    #[doc = guide!(mwg: "mwg-expected-by-module-item.html#adg-pam_getenv")]
     fn environ(&self) -> impl EnvironMap;
 
-    /// A writable version of the environment.
+    /// A writable map of the environment to set for the logged-in user.
+    ///
+    /// # References
+    ///
+    #[doc = linklist!(pam_putenv: adg, mwg, _std)]
+    ///
+    #[doc = stdlinks!(3 pam_putenv)]
+    #[doc = guide!(adg: "adg-interface-by-app-expected.html#adg-pam_putenv")]
+    #[doc = guide!(mwg: "mwg-expected-by-module-item.html#adg-pam_putenv")]
     fn environ_mut(&mut self) -> impl EnvironMapMut;
 
-    trait_item!(
-        /// The identity of the user for whom service is being requested.
-        ///
-        /// Unlike [`username`](Self::username), this will simply get
-        /// the current state of the user item, and not request the username.
-        /// While PAM usually sets this automatically in the `username` call,
-        /// it may be changed by a module during the PAM transaction.
-        /// Applications should check it after each step of the PAM process.
-        get = user_item,
-        item = "PAM_USER",
-        see = Self::username
-    );
-    trait_item!(
-        /// Sets the identity of the logging-in user.
-        ///
-        /// Usually this will be set during the course of
-        /// a [`username`](Self::username) call, but you may set it manually
-        /// or change it during the PAM process.
-        set = set_user_item,
-        item = "PAM_USER",
-        see = Self::user_item
-    );
-
-    trait_item!(
-        /// The service name, which identifies the PAM stack which is used
-        /// to perform authentication.
-        get = service,
-        item = "PAM_SERVICE"
-    );
-    trait_item!(
-        /// Sets the service name. It's probably a bad idea to change this.
-        set = set_service,
-        item = "PAM_SERVICE",
-        see = Self::service
-    );
-
-    trait_item!(
-        /// The string used to prompt for a user's name.
-        /// By default, this is a localized version of `login: `.
-        get = user_prompt,
-        item = "PAM_USER_PROMPT"
-    );
-    trait_item!(
-        /// Sets the string used to prompt for a user's name.
-        set = set_user_prompt,
-        item = "PAM_USER_PROMPT",
-        see = Self::user_prompt
-    );
+    /// Gets Items, data shared by PAM, the application, and modules.
+    ///
+    /// Certain Items should not be accessed by a PAM application;
+    /// those are available directly on [`ModuleClient`] for use
+    /// by PAM modules only.
+    ///
+    /// # References
+    ///
+    #[doc = linklist!(pam_get_item: mwg, adg, _std)]
+    ///
+    #[doc = guide!(adg: "adg-interface-by-app-expected.html#adg-pam_get_item")]
+    #[doc = guide!(mwg: "mwg-expected-by-module-item.html#mwg-pam_get_item")]
+    #[doc = stdlinks!(3 pam_get_item)]
+    fn items(&self) -> impl Items;
 
-    trait_item!(
-        /// The device path of the TTY being used to log in.
-        ///
-        /// This is the terminal the user is logging in on,
-        /// specified as the full device path (e.g. `/dev/tty0`).
-        /// Very old applications may use this instead of `PAM_XDISPLAY`.
-        get = tty_name,
-        item = "PAM_TTY"
-    );
-    trait_item!(
-        /// Sets the path to the terminal where the user is logging on.
-        set = set_tty_name,
-        item = "PAM_TTY",
-        see = Self::tty_name
-    );
-
-    trait_item!(
-        /// If set, the identity of the remote user logging in.
-        ///
-        /// This is only as trustworthy as the application calling PAM.
-        get = remote_user,
-        item = "PAM_RUSER",
-        see = Self::remote_host
-    );
-    trait_item!(
-        /// Sets the identity of the remote user logging in.
-        ///
-        /// This may be set by the application before making calls
-        /// into a PAM transaction.
-        set = set_remote_user,
-        item = "PAM_RUSER",
-        see = Self::remote_user
-    );
-
-    trait_item!(
-        /// If set, the remote location where the user is coming from.
-        ///
-        /// This is only as trustworthy as the application calling PAM.
-        /// This can be combined with [`Self::remote_user`] to identify
-        /// the account the user is attempting to log in from,
-        /// with `remote_user@remote_host`.
-        ///
-        /// If unset, "it is unclear where the authentication request
-        /// is originating from."
-        get = remote_host,
-        item = "PAM_RHOST",
-        see = Self::remote_user
-    );
-    trait_item!(
-        /// Sets the location where the user is coming from.
-        ///
-        /// This may be set by the application before making calls
-        /// into a PAM transaction.
-        set = set_remote_host,
-        item = "PAM_RHOST",
-        see = Self::remote_host
-    );
-
-    trait_item!(
-        /// Gets the user's authentication token (e.g., password).
-        ///
-        /// This is usually set automatically when
-        /// [`authtok`](PamHandleModule::authtok) is called,
-        /// but can be manually set.
-        set = set_authtok_item,
-        item = "PAM_AUTHTOK",
-        see = PamHandleModule::authtok_item
-    );
-
-    trait_item!(
-        /// Sets the user's "old authentication token" when changing passwords.
-        ///
-        /// This is usually set automatically by PAM.
-        set = set_old_authtok_item,
-        item = "PAM_OLDAUTHTOK",
-        see = PamHandleModule::old_authtok_item
-    );
+    /// Read-write access to PAM Items.
+    ///
+    /// # References
+    ///
+    #[doc = linklist!(pam_set_item: mwg, adg, _std)]
+    ///
+    #[doc = guide!(adg: "adg-interface-by-app-expected.html#adg-pam_set_item")]
+    #[doc = guide!(mwg: "mwg-expected-by-module-item.html#mwg-pam_set_item")]
+    #[doc = stdlinks!(3 pam_set_item)]
+    fn items_mut(&mut self) -> impl ItemsMut;
 }
 
 /// Functionality of a PAM handle that can be expected by a PAM application.
@@ -316,7 +185,7 @@
 ///
 /// Like [`PamShared`], this is intended to allow creating mock implementations
 /// of PAM for testing PAM modules.
-pub trait PamHandleModule: Conversation + PamShared {
+pub trait ModuleClient: Conversation + PamShared {
     /// Retrieves the authentication token from the user.
     ///
     /// This should only be used by *authentication* and *password-change*
@@ -329,8 +198,8 @@
     /// # Example
     ///
     /// ```no_run
-    /// # use nonstick::handle::PamHandleModule;
-    /// # fn _doc(handle: &mut impl PamHandleModule) -> Result<(), Box<dyn std::error::Error>> {
+    /// # use nonstick::handle::ModuleClient;
+    /// # fn _doc(handle: &mut impl ModuleClient) -> Result<(), Box<dyn std::error::Error>> {
     /// // Get the user's password using the default prompt.
     /// let pass = handle.authtok(None)?;
     /// // Get the user's password using a custom prompt.
@@ -344,31 +213,63 @@
 
     /// Retrieves the user's old authentication token when changing passwords.
     ///
+    /// This should only be used by a *password-change* module.
     ///
+    /// # References
+    ///
+    #[doc = linklist!(pam_get_authtok: man7, manbsd)]
+    ///
+    /// # Example
+    ///
+    /// ```no_run
+    /// # use nonstick::handle::ModuleClient;
+    /// # fn _doc(handle: &mut impl ModuleClient) -> Result<(), Box<dyn std::error::Error>> {
+    /// // Get the user's password using the default prompt.
+    /// let pass = handle.old_authtok(None)?;
+    /// // Get the user's password using a custom prompt.
+    /// let pass = handle.old_authtok(Some("Reveal your secrets!".as_ref()))?;
+    /// Ok(())
+    /// # }
+    /// ```
+    ///
+    #[doc = stdlinks!(3 pam_get_authtok)]
     fn old_authtok(&mut self, prompt: Option<&OsStr>) -> Result<OsString>;
 
-    trait_item!(
+    getter!(
         /// Gets the user's authentication token (e.g., password).
         ///
-        /// This is normally set automatically by PAM when calling
-        /// [`authtok`](Self::authtok), but can be set explicitly.
+        /// This is normally set automatically by PAM through [`Self::authtok`],
+        /// but this will get its value (if set) without prompting the user.
         ///
         /// Like `authtok`, this should only ever be called
         /// by *authentication* and *password-change* PAM modules.
-        get = authtok_item,
-        item = "PAM_AUTHTOK",
-        see = Self::authtok
+        ///
+        /// # References
+        ///
+        #[doc = linklist!(pam_set_item: mwg, adg, _std)]
+        ///
+        #[doc = guide!(adg: "adg-interface-by-app-expected.html#adg-pam_set_item")]
+        #[doc = guide!(mwg: "mwg-expected-by-module-item.html#mwg-pam_set_item")]
+        #[doc = stdlinks!(3 pam_set_item)]
+        authtok_item("PAM_AUTHTOK", see = Self::authtok)
     );
 
-    trait_item!(
+    getter!(
         /// Gets the user's old authentication token when changing passwords.
         ///
-        /// This is normally set automatically by PAM when calling
-        /// [`old_authtok`](Self::old_authtok), but can be set explicitly.
+        /// This is normally set automatically by PAM through
+        /// [`Self::old_authtok`], but this will get its value (if set)
+        /// without prompting the user.
         ///
         /// This should only ever be called by *password-change* PAM modules.
-        get = old_authtok_item,
-        item = "PAM_OLDAUTHTOK",
-        see = PamShared::set_old_authtok_item
+        ///
+        /// # References
+        ///
+        #[doc = linklist!(pam_set_item: mwg, adg, _std)]
+        ///
+        #[doc = guide!(adg: "adg-interface-by-app-expected.html#adg-pam_set_item")]
+        #[doc = guide!(mwg: "mwg-expected-by-module-item.html#mwg-pam_set_item")]
+        #[doc = stdlinks!(3 pam_set_item)]
+        old_authtok_item("PAM_OLDAUTHTOK", see = ItemsMut::set_old_authtok)
     );
 }
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/items.rs	Sun Jul 06 19:10:26 2025 -0400
@@ -0,0 +1,169 @@
+use crate::_doc::{guide, linklist, stdlinks};
+use crate::constants::Result;
+#[cfg(doc)]
+use crate::handle::{ModuleClient, PamShared};
+use std::ffi::{OsStr, OsString};
+
+macro_rules! getter {
+    ($(#[$md:meta])* $getter:ident($item:literal $(, see = $see:path)?)) => {
+        $(#[$md])*
+        #[doc = ""]
+        #[doc = concat!("Gets the `", $item, "` of the PAM handle.")]
+        $(
+            #[doc = concat!("See [`", stringify!($see), "`].")]
+        )?
+        fn $getter(&self) -> Result<Option<OsString>>;
+    };
+}
+
+pub(crate) use getter;
+macro_rules! setter {
+    ($(#[$md:meta])* $setter:ident($item:literal $(, see = $see:path)?)) => {
+        $(#[$md])*
+        #[doc = ""]
+        #[doc = concat!("Sets the `", $item, "` from the PAM handle.")]
+        $(
+            #[doc = concat!("See [`", stringify!($see), "`].")]
+        )?
+        ///
+        /// Sets the item's value. PAM copies the string's contents.
+        ///
+        /// # Panics
+        ///
+        /// If the string contains a nul byte, this will panic.
+        ///
+        fn $setter(&mut self, value: Option<&OsStr>) -> Result<()>;
+    };
+}
+
+/// Provides access to Items, pieces of data shared by the PAM application,
+/// modules, and the framework itself.
+///
+/// # References
+///
+#[doc = linklist!(pam_get_item: mwg, adg, _std)]
+///
+#[doc = guide!(adg: "adg-interface-by-app-expected.html#adg-pam_get_item")]
+#[doc = guide!(mwg: "mwg-expected-by-module-item.html#mwg-pam_get_item")]
+#[doc = stdlinks!(3 pam_get_item)]
+pub trait Items<'a> {
+    getter!(
+        /// The identity of the user for whom service is being requested.
+        ///
+        /// Unlike [`username`](PamShared::username), this will simply get
+        /// the current state of the user item, and not request the username.
+        /// While PAM usually sets this automatically in the `username` call,
+        /// it may be changed by a module during the PAM transaction.
+        /// Applications should check it after each step of the PAM process.
+        user("PAM_USER", see = PamShared::username)
+    );
+
+    getter!(
+        /// If set, the identity of the remote user logging in.
+        ///
+        /// This is only as trustworthy as the application calling PAM.
+        remote_user("PAM_RUSER", see = Self::remote_host)
+    );
+
+    getter!(
+        /// If set, the remote location where the user is coming from.
+        ///
+        /// This is only as trustworthy as the application calling PAM.
+        /// This can be combined with [`Self::remote_user`] to identify
+        /// the account the user is attempting to log in from,
+        /// with `remote_user@remote_host`.
+        ///
+        /// If unset, "it is unclear where the authentication request
+        /// is originating from."
+        remote_host("PAM_RHOST", see = Self::remote_user)
+    );
+
+    getter!(
+        /// The service name, which identifies the PAM stack which is used
+        /// to perform authentication.
+        service("PAM_SERVICE")
+    );
+
+    getter!(
+        /// The string used to prompt for a user's name.
+        /// By default, this is a localized version of `login: `.
+        user_prompt("PAM_USER_PROMPT")
+    );
+
+    getter!(
+        /// The device path of the TTY being used to log in.
+        ///
+        /// This is the terminal the user is logging in on,
+        /// specified as the full device path (e.g. `/dev/tty0`).
+        /// Very old applications may use this instead of `PAM_XDISPLAY`.
+        tty_name("PAM_TTY")
+    );
+}
+
+/// Provides write access to PAM Items, data shared by the PAM application,
+/// the framework, and modules.
+///
+/// # References
+///
+#[doc = linklist!(pam_set_item: mwg, adg, _std)]
+///
+#[doc = guide!(adg: "adg-interface-by-app-expected.html#adg-pam_set_item")]
+#[doc = guide!(mwg: "mwg-expected-by-module-item.html#mwg-pam_set_item")]
+#[doc = stdlinks!(3 pam_set_item)]
+pub trait ItemsMut<'a>: Items<'a> {
+    setter!(
+        /// Sets the identity of the logging-in user.
+        ///
+        /// Usually this will be set during the course of
+        /// a [`username`](PamShared::username) call, but you may set it manually
+        /// or change it during the PAM process.
+        set_user("PAM_USER", see = Items::user)
+    );
+
+    setter!(
+        /// Sets the service name. It's probably a bad idea to change this.
+        set_service("PAM_SERVICE", see = Items::service)
+    );
+
+    setter!(
+        /// Sets the string used to prompt for a user's name.
+        set_user_prompt("PAM_USER_PROMPT", see = Items::user_prompt)
+    );
+
+    setter!(
+        /// Sets the path to the terminal where the user is logging on.
+        set_tty_name("PAM_TTY", see = Items::tty_name)
+    );
+
+    setter!(
+        /// Sets the identity of the remote user logging in.
+        ///
+        /// This may be set by the application before making calls
+        /// into a PAM transaction.
+        set_remote_user("PAM_RUSER", see = Items::remote_user)
+    );
+
+    setter!(
+        /// Sets the location where the user is coming from.
+        ///
+        /// This may be set by the application before making calls
+        /// into a PAM transaction.
+        set_remote_host("PAM_RHOST", see = Items::remote_host)
+    );
+
+    setter!(
+        /// Gets the user's authentication token (e.g., password).
+        ///
+        /// This is usually set automatically when
+        /// [`authtok`](ModuleClient::authtok) is called,
+        /// but can be manually set.
+        set_authtok("PAM_AUTHTOK", see = ModuleClient::authtok_item)
+    );
+    setter!(
+        /// Sets the user's "old authentication token" when changing passwords.
+        ///
+        /// This is usually set automatically by PAM when
+        /// [`old_authtok`](ModuleClient::old_authtok) is called.
+        set_old_authtok("PAM_OLDAUTHTOK", see = ModuleClient::old_authtok_item)
+    );
+}
--- a/src/lib.rs	Sun Jul 06 19:04:57 2025 -0400
+++ b/src/lib.rs	Sun Jul 06 19:10:26 2025 -0400
@@ -32,10 +32,10 @@
 pub mod handle;
 
 mod _doc;
-pub(crate) use _doc::*;
 mod environ;
+pub mod items;
 #[cfg(feature = "link")]
-mod libpam;
+pub mod libpam;
 pub mod logging;
 
 #[cfg(feature = "link")]
@@ -46,6 +46,6 @@
     constants::{ErrorCode, Flags, Result},
     conv::{BinaryData, Conversation, ConversationAdapter},
     environ::{EnvironMap, EnvironMapMut},
-    handle::{PamHandleModule, PamShared, Transaction},
+    handle::{ModuleClient, PamShared, Transaction},
     module::PamModule,
 };
--- a/src/libpam/handle.rs	Sun Jul 06 19:04:57 2025 -0400
+++ b/src/libpam/handle.rs	Sun Jul 06 19:10:26 2025 -0400
@@ -1,14 +1,15 @@
 use super::conversation::{OwnedConversation, PamConv};
+use crate::_doc::{guide, linklist, stdlinks};
 use crate::constants::{ErrorCode, Result};
 use crate::conv::Exchange;
 use crate::environ::EnvironMapMut;
 use crate::handle::PamShared;
+use crate::items::{Items, ItemsMut};
 use crate::libpam::environ::{LibPamEnviron, LibPamEnvironMut};
-use crate::libpam::memory;
+use crate::libpam::items::{LibPamItems, LibPamItemsMut};
+use crate::libpam::{items, memory};
 use crate::logging::{Level, Location};
-use crate::{
-    guide, linklist, stdlinks, Conversation, EnvironMap, Flags, PamHandleModule, Transaction,
-};
+use crate::{Conversation, EnvironMap, Flags, ModuleClient, Transaction};
 use libpam_sys_helpers::constants;
 use num_enum::{IntoPrimitive, TryFromPrimitive};
 use std::cell::Cell;
@@ -209,28 +210,8 @@
     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) }
-        }
-    };
+    delegate!(fn items(&self) -> impl Items);
+    delegate!(fn items_mut(&mut self) -> impl ItemsMut);
 }
 
 /// An owned variation of a basic PAM handle.
@@ -377,20 +358,13 @@
         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);
+    fn items(&self) -> impl Items {
+        LibPamItems(self)
+    }
+
+    fn items_mut(&mut self) -> impl ItemsMut {
+        LibPamItemsMut(self)
+    }
 }
 
 impl Conversation for LibPamHandle {
@@ -406,7 +380,7 @@
     }
 }
 
-impl PamHandleModule for LibPamHandle {
+impl ModuleClient for LibPamHandle {
     fn authtok(&mut self, prompt: Option<&OsStr>) -> Result<OsString> {
         self.get_authtok(prompt, ItemType::AuthTok)
     }
@@ -415,8 +389,12 @@
         self.get_authtok(prompt, ItemType::OldAuthTok)
     }
 
-    cstr_item!(get = authtok_item, item = ItemType::AuthTok);
-    cstr_item!(get = old_authtok_item, item = ItemType::OldAuthTok);
+    fn authtok_item(&self) -> Result<Option<OsString>> {
+        unsafe { items::get_cstr_item(self, ItemType::AuthTok) }
+    }
+    fn old_authtok_item(&self) -> Result<Option<OsString>> {
+        unsafe { items::get_cstr_item(self, ItemType::OldAuthTok) }
+    }
 }
 
 /// Function called at the end of a PAM session that is called to clean up
@@ -454,36 +432,6 @@
         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();
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/libpam/items.rs	Sun Jul 06 19:10:26 2025 -0400
@@ -0,0 +1,90 @@
+use crate::constants::ErrorCode;
+use crate::constants::Result;
+use crate::items::{Items, ItemsMut};
+use crate::libpam::handle::ItemType;
+use crate::libpam::handle::LibPamHandle;
+use crate::libpam::memory;
+use std::ffi::{c_int, OsStr, OsString};
+use std::ptr;
+
+pub struct LibPamItems<'a>(pub &'a LibPamHandle);
+pub struct LibPamItemsMut<'a>(pub &'a mut LibPamHandle);
+
+/// 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 { get_cstr_item(&self.0, $item_type) }
+        }
+    };
+    (set = $setter:ident, item = $item_type:path) => {
+        fn $setter(&mut self, value: Option<&OsStr>) -> Result<()> {
+            unsafe { set_cstr_item(&mut self.0, $item_type, value) }
+        }
+    };
+}
+
+impl Items<'_> for LibPamItems<'_> {
+    cstr_item!(get = user, item = ItemType::User);
+    cstr_item!(get = service, item = ItemType::Service);
+    cstr_item!(get = user_prompt, item = ItemType::UserPrompt);
+    cstr_item!(get = tty_name, item = ItemType::Tty);
+    cstr_item!(get = remote_user, item = ItemType::RemoteUser);
+    cstr_item!(get = remote_host, item = ItemType::RemoteHost);
+}
+
+impl Items<'_> for LibPamItemsMut<'_> {
+    cstr_item!(get = user, item = ItemType::User);
+    cstr_item!(get = service, item = ItemType::Service);
+    cstr_item!(get = user_prompt, item = ItemType::UserPrompt);
+    cstr_item!(get = tty_name, item = ItemType::Tty);
+    cstr_item!(get = remote_user, item = ItemType::RemoteUser);
+    cstr_item!(get = remote_host, item = ItemType::RemoteHost);
+}
+
+impl ItemsMut<'_> for LibPamItemsMut<'_> {
+    cstr_item!(set = set_user, item = ItemType::User);
+    cstr_item!(set = set_service, item = ItemType::Service);
+    cstr_item!(set = set_user_prompt, item = ItemType::UserPrompt);
+    cstr_item!(set = set_tty_name, item = ItemType::Tty);
+    cstr_item!(set = set_remote_user, item = ItemType::RemoteUser);
+    cstr_item!(set = set_remote_host, item = ItemType::RemoteHost);
+    cstr_item!(set = set_authtok, item = ItemType::AuthTok);
+    cstr_item!(set = set_old_authtok, item = ItemType::OldAuthTok);
+}
+
+/// Gets a C string item.
+///
+/// # Safety
+///
+/// You better be requesting an item which is a C string.
+pub unsafe fn get_cstr_item(
+    hdl: &LibPamHandle,
+    item_type: ItemType,
+) -> crate::Result<Option<OsString>> {
+    let mut output = ptr::null();
+    let ret = unsafe { libpam_sys::pam_get_item(hdl.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.
+pub unsafe fn set_cstr_item(
+    hdl: &mut LibPamHandle,
+    item_type: ItemType,
+    data: Option<&OsStr>,
+) -> crate::Result<()> {
+    let data_str = memory::option_cstr_os(data);
+    let ret = unsafe {
+        libpam_sys::pam_set_item(
+            hdl.raw_mut(),
+            item_type as c_int,
+            memory::prompt_ptr(data_str.as_deref()).cast(),
+        )
+    };
+    ErrorCode::result_from(ret)
+}
--- a/src/libpam/mod.rs	Sun Jul 06 19:04:57 2025 -0400
+++ b/src/libpam/mod.rs	Sun Jul 06 19:10:26 2025 -0400
@@ -10,9 +10,10 @@
 mod conversation;
 mod environ;
 mod handle;
+mod items;
 mod memory;
 mod module;
 mod question;
 
 #[doc(inline)]
-pub use handle::{LibPamHandle, LibPamTransaction};
+pub use handle::{LibPamHandle, LibPamTransaction, TransactionBuilder};
--- a/src/libpam/module.rs	Sun Jul 06 19:04:57 2025 -0400
+++ b/src/libpam/module.rs	Sun Jul 06 19:10:26 2025 -0400
@@ -11,7 +11,7 @@
 ///
 /// ```no_run
 /// use nonstick::{
-///     pam_hooks, ConversationAdapter, Flags, LibPamTransaction, PamHandleModule, PamModule,
+///     pam_hooks, ConversationAdapter, Flags, LibPamTransaction, ModuleClient, PamModule,
 ///     Result as PamResult,
 /// };
 /// use std::ffi::CStr;
@@ -20,7 +20,7 @@
 /// struct MyPamModule;
 /// pam_hooks!(MyPamModule);
 ///
-/// impl<T: PamHandleModule> PamModule<T> for MyPamModule {
+/// impl<T: ModuleClient> PamModule<T> for MyPamModule {
 ///     fn authenticate(handle: &mut T, args: Vec<&CStr>, flags: Flags) -> PamResult<()> {
 ///         let password = handle.authtok(Some("what's your password?".as_ref()))?;
 ///         let response =
@@ -151,9 +151,9 @@
 #[cfg(test)]
 mod tests {
     // Compile-time test that the `pam_hooks` macro compiles.
-    use crate::{PamHandleModule, PamModule};
+    use crate::{ModuleClient, PamModule};
     struct Foo;
-    impl<T: PamHandleModule> PamModule<T> for Foo {}
+    impl<T: ModuleClient> PamModule<T> for Foo {}
 
     pam_hooks!(Foo);
 }
--- a/src/module.rs	Sun Jul 06 19:04:57 2025 -0400
+++ b/src/module.rs	Sun Jul 06 19:10:26 2025 -0400
@@ -4,7 +4,7 @@
 #![allow(dead_code)]
 
 use crate::constants::{ErrorCode, Flags, Result};
-use crate::handle::PamHandleModule;
+use crate::handle::ModuleClient;
 use std::ffi::CStr;
 
 /// A trait for a PAM module to implement.
@@ -21,7 +21,7 @@
 /// [manpage]: https://www.man7.org/linux/man-pages/man3/pam.3.html
 /// [mwg]: https://www.chiark.greenend.org.uk/doc/libpam-doc/html/Linux-PAM_MWG.html
 #[allow(unused_variables)]
-pub trait PamModule<T: PamHandleModule> {
+pub trait PamModule<T: ModuleClient> {
     // Functions for auth modules.
 
     /// Authenticate the user.
@@ -29,7 +29,7 @@
     /// This is probably the first thing you want to implement.
     /// In most cases, you will want to get the user and password,
     /// using [`PamShared::username`](crate::PamShared::username)
-    /// and [`PamHandleModule::authtok`],
+    /// and [`ModuleClient::authtok`],
     /// and verify them against something.
     ///
     /// See [the Module Writer's Guide entry for `pam_sm_authenticate`][mwg]
--- a/testharness/src/lib.rs	Sun Jul 06 19:04:57 2025 -0400
+++ b/testharness/src/lib.rs	Sun Jul 06 19:10:26 2025 -0400
@@ -1,12 +1,12 @@
 //! The nonstick library
 extern crate nonstick;
 
-use nonstick::{pam_hooks, Flags, PamHandleModule, PamModule};
+use nonstick::{pam_hooks, Flags, ModuleClient, PamModule};
 use std::ffi::CStr;
 
 struct TestHarness;
 
-impl<M: PamHandleModule> PamModule<M> for TestHarness {
+impl<M: ModuleClient> PamModule<M> for TestHarness {
     fn authenticate(_handle: &mut M, _args: Vec<&CStr>, _flags: Flags) -> nonstick::Result<()> {
         Ok(())
     }