changeset 166:2f5913131295

Separate flag/action flags into flags and action. This also individualizes the type of flag for each PAM function, so that you can only call a function with the right flags and values.
author Paul Fisher <paul@pfish.zone>
date Tue, 15 Jul 2025 00:32:24 -0400
parents c4b1e280463c
children 0cabe7b94a4f
files src/constants.rs src/handle.rs src/lib.rs src/libpam/handle.rs src/libpam/module.rs src/module.rs testharness/src/bin/testharness.rs testharness/src/lib.rs
diffstat 8 files changed, 310 insertions(+), 243 deletions(-) [+]
line wrap: on
line diff
--- a/src/constants.rs	Mon Jul 14 18:56:55 2025 -0400
+++ b/src/constants.rs	Tue Jul 15 00:32:24 2025 -0400
@@ -2,6 +2,7 @@
 
 use crate::_doc::{linklist, man7, manbsd, xsso};
 use bitflags::bitflags;
+use libpam_sys_consts::constants;
 use num_enum::{IntoPrimitive, TryFromPrimitive};
 use std::error::Error;
 use std::ffi::c_int;
@@ -9,99 +10,142 @@
 use std::fmt::{Display, Formatter};
 use std::result::Result as StdResult;
 
-/// Values for constants not provided by certain PAM implementations.
-///
-/// **The values of these constants are deliberately selected _not_ to match
-/// any PAM implementations. Applications should always use the symbolic value
-/// and not a magic number.**
-mod pam_constants {
-    pub use libpam_sys_consts::constants::*;
-
-    macro_rules! define {
-        ($(#[$attr:meta])* $($name:ident = $value:expr;)+) => {
-            define!(
-                @meta { $(#[$attr])* }
-                $(pub const $name: i32 = $value;)+
-            );
-        };
-        (@meta $m:tt $($i:item)+) => { define!(@expand $($m $i)+); };
-        (@expand $({ $(#[$m:meta])* } $i:item)+) => {$($(#[$m])* $i)+};
+/// Creates a bitflags! macro, with an extra SILENT element.
+macro_rules! pam_flags {
+    (
+        $(#[$m:meta])*
+        $name:ident {
+            $($inner:tt)*
+        }
+    ) => {
+        bitflags! {
+            $(#[$m])*
+            #[derive(Clone, Copy, Debug, Default, PartialEq)]
+            #[repr(transparent)]
+            pub struct $name: c_int {
+                /// The module should not generate any messages.
+                const SILENT = constants::PAM_SILENT;
+                $($inner)*
+            }
+        }
     }
+}
 
-    define!(
-        /// A fictitious constant for testing purposes.
-        #[cfg(not(pam_impl = "OpenPam"))]
-        PAM_BAD_CONSTANT = 513;
-        PAM_BAD_FEATURE = 514;
-    );
+pam_flags! {
+    /// Flags for authentication and account management.
+    AuthnFlags {
+        /// The module should return [AuthError](ErrorCode::AuthError)
+        /// if the user has an empty authentication token, rather than
+        /// allowing them to log in.
+        const DISALLOW_NULL_AUTHTOK = constants::PAM_DISALLOW_NULL_AUTHTOK;
+    }
+}
 
-    define!(
-        /// A fictitious constant for testing purposes.
-        #[cfg(not(any(pam_impl = "LinuxPam", pam_impl = "OpenPam")))]
-        PAM_BAD_ITEM = 515;
-        PAM_MODULE_UNKNOWN = 516;
-    );
+pam_flags! {
+    /// Flags for changing the authentication token.
+    AuthtokFlags {
+        /// Indicates that the user's authentication token should
+        /// only be changed if it is expired. If not passed,
+        /// the authentication token should be changed unconditionally.
+        const CHANGE_EXPIRED_AUTHTOK = constants::PAM_CHANGE_EXPIRED_AUTHTOK;
 
-    define!(
-        /// A fictitious constant for testing purposes.
-        #[cfg(not(pam_impl = "LinuxPam"))]
-        PAM_CONV_AGAIN = 517;
-        PAM_INCOMPLETE = 518;
-    );
+        /// Don't check if the password is any good (Sun only).
+        #[cfg(pam_impl = "Sun")]
+        const NO_AUTHTOK_CHECK = constants::PAM_NO_AUTHTOK_CHECK;
+    }
+}
+
+pam_flags! {
+    /// Common flag(s) shared by all PAM actions.
+    BaseFlags {}
 }
 
-bitflags! {
-    /// The available PAM flags.
-    ///
-    /// See `/usr/include/security/_pam_types.h` and
-    /// See `/usr/include/security/pam_modules.h` for more details.
-    #[derive(Debug, Default, PartialEq)]
-    #[repr(transparent)]
-    pub struct Flags: c_int {
-        /// The module should not generate any messages.
-        const SILENT = pam_constants::PAM_SILENT;
+#[cfg(feature = "openpam-ext")]
+const BAD_CONST: ErrorCode = ErrorCode::BadConstant;
+#[cfg(not(feature = "openpam-ext"))]
+const BAD_CONST: ErrorCode = ErrorCode::SystemError;
 
-        /// The module should return [ErrorCode::AuthError]
-        /// if the user has an empty authentication token
-        /// rather than immediately accepting them.
-        const DISALLOW_NULL_AUTHTOK = pam_constants::PAM_DISALLOW_NULL_AUTHTOK;
+macro_rules! flag_enum {
+    (
+        $(#[$m:meta])*
+        $name:ident {
+            $(
+                $(#[$item_m:meta])*
+                $item_name:ident = $item_value:expr,
+            )*
+        }
+    ) => {
+        $(#[$m])*
+        #[derive(Clone, Copy, Debug, PartialEq, TryFromPrimitive, IntoPrimitive)]
+        #[repr(i32)]
+        pub enum $name {
+            $(
+                $(#[$item_m])*
+                $item_name = $item_value,
+            )*
+        }
 
-        // Flag used for `set_credentials`.
+        impl $name {
+            const ALL_VALUES: i32 = 0 $( | $item_value)*;
+
+            fn split(value: i32) -> Result<(Option<Self>, i32)> {
+                let me = value & Self::ALL_VALUES;
+                let them = value & !Self::ALL_VALUES;
+                let me = match me {
+                    0 => None,
+                    n => Some(Self::try_from(n).map_err(|_| BAD_CONST)?),
+                };
+                Ok((me, them))
+            }
+        }
+    }
+}
 
-        /// Set user credentials for an authentication service.
-        const ESTABLISH_CREDENTIALS = pam_constants::PAM_ESTABLISH_CRED;
-        /// Delete user credentials associated with
-        /// an authentication service.
-        const DELETE_CREDENTIALS = pam_constants::PAM_DELETE_CRED;
-        /// Reinitialize user credentials.
-        const REINITIALIZE_CREDENTIALS = pam_constants::PAM_REINITIALIZE_CRED;
-        /// Extend the lifetime of user credentials.
-        const REFRESH_CREDENTIALS = pam_constants::PAM_REFRESH_CRED;
+flag_enum! {
+    /// The credential management action that should take place.
+    CredAction {
+        /// Set the user's credentials from this module. Default if unspecified.
+        Establish = constants::PAM_ESTABLISH_CRED,
+        /// Revoke the user's credentials established by this module.
+        Delete = constants::PAM_DELETE_CRED,
+        /// Fully reinitialize the user's credentials from this module.
+        Reinitialize = constants::PAM_REINITIALIZE_CRED,
+        /// Extend the lifetime of the user's credentials from this module.
+        Refresh = constants::PAM_REFRESH_CRED,
+    }
+}
 
-        // Flags used for password changing.
+impl CredAction {
+    /// Separates this enum from the remaining [`BaseFlags`].
+    pub fn extract(value: i32) -> Result<(Self, BaseFlags)> {
+        Self::split(value)
+            .map(|(act, rest)| (act.unwrap_or_default(), BaseFlags::from_bits_retain(rest)))
+    }
+}
 
-        /// The password service should only update those passwords
-        /// that have aged. If this flag is _not_ passed,
-        /// the password service should update all passwords.
-        ///
-        /// This flag is only used by `change_authtok`.
-        const CHANGE_EXPIRED_AUTHTOK = pam_constants::PAM_CHANGE_EXPIRED_AUTHTOK;
-        /// This is a preliminary check for password changing.
-        /// The password should not be changed.
-        ///
-        /// This is only used between PAM and a module.
-        /// Applications may not use this flag.
-        ///
-        /// This flag is only used by `change_authtok`.
-        const PRELIMINARY_CHECK = pam_constants::PAM_PRELIM_CHECK;
-        /// The password should actuallyPR be updated.
-        /// This and [Self::PRELIMINARY_CHECK] are mutually exclusive.
-        ///
-        /// This is only used between PAM and a module.
-        /// Applications may not use this flag.
-        ///
-        /// This flag is only used by `change_authtok`.
-        const UPDATE_AUTHTOK = pam_constants::PAM_UPDATE_AUTHTOK;
+impl Default for CredAction {
+    fn default() -> Self {
+        Self::Establish
+    }
+}
+
+flag_enum! {
+    AuthtokAction {
+        /// This is a preliminary call to check if we're ready to change passwords
+        /// and that the new password is acceptable.
+        PreliminaryCheck = constants::PAM_PRELIM_CHECK,
+        /// You should actually update the password.
+        Update = constants::PAM_UPDATE_AUTHTOK,
+    }
+}
+
+impl AuthtokAction {
+    /// Separates this enum from the remaining [`AuthtokFlags`].
+    pub fn extract(value: i32) -> Result<(Self, AuthtokFlags)> {
+        match Self::split(value)? {
+            (Some(act), rest) => Ok((act, AuthtokFlags::from_bits_retain(rest))),
+            (None, _) => Err(BAD_CONST),
+        }
     }
 }
 
@@ -124,41 +168,49 @@
 #[non_exhaustive] // C might give us anything!
 #[repr(i32)]
 pub enum ErrorCode {
-    OpenError = pam_constants::PAM_OPEN_ERR,
-    SymbolError = pam_constants::PAM_SYMBOL_ERR,
-    ServiceError = pam_constants::PAM_SERVICE_ERR,
-    SystemError = pam_constants::PAM_SYSTEM_ERR,
-    BufferError = pam_constants::PAM_BUF_ERR,
-    PermissionDenied = pam_constants::PAM_PERM_DENIED,
-    AuthenticationError = pam_constants::PAM_AUTH_ERR,
-    CredentialsInsufficient = pam_constants::PAM_CRED_INSUFFICIENT,
-    AuthInfoUnavailable = pam_constants::PAM_AUTHINFO_UNAVAIL,
-    UserUnknown = pam_constants::PAM_USER_UNKNOWN,
-    MaxTries = pam_constants::PAM_MAXTRIES,
-    NewAuthTokRequired = pam_constants::PAM_NEW_AUTHTOK_REQD,
-    AccountExpired = pam_constants::PAM_ACCT_EXPIRED,
-    SessionError = pam_constants::PAM_SESSION_ERR,
-    CredentialsUnavailable = pam_constants::PAM_CRED_UNAVAIL,
-    CredentialsExpired = pam_constants::PAM_CRED_EXPIRED,
-    CredentialsError = pam_constants::PAM_CRED_ERR,
-    NoModuleData = pam_constants::PAM_NO_MODULE_DATA,
-    ConversationError = pam_constants::PAM_CONV_ERR,
-    AuthTokError = pam_constants::PAM_AUTHTOK_ERR,
-    AuthTokRecoveryError = pam_constants::PAM_AUTHTOK_RECOVERY_ERR,
-    AuthTokLockBusy = pam_constants::PAM_AUTHTOK_LOCK_BUSY,
-    AuthTokDisableAging = pam_constants::PAM_AUTHTOK_DISABLE_AGING,
-    TryAgain = pam_constants::PAM_TRY_AGAIN,
-    Ignore = pam_constants::PAM_IGNORE,
-    Abort = pam_constants::PAM_ABORT,
-    AuthTokExpired = pam_constants::PAM_AUTHTOK_EXPIRED,
+    OpenError = constants::PAM_OPEN_ERR,
+    SymbolError = constants::PAM_SYMBOL_ERR,
+    ServiceError = constants::PAM_SERVICE_ERR,
+    SystemError = constants::PAM_SYSTEM_ERR,
+    BufferError = constants::PAM_BUF_ERR,
+    PermissionDenied = constants::PAM_PERM_DENIED,
+    AuthenticationError = constants::PAM_AUTH_ERR,
+    CredentialsInsufficient = constants::PAM_CRED_INSUFFICIENT,
+    AuthInfoUnavailable = constants::PAM_AUTHINFO_UNAVAIL,
+    UserUnknown = constants::PAM_USER_UNKNOWN,
+    MaxTries = constants::PAM_MAXTRIES,
+    NewAuthTokRequired = constants::PAM_NEW_AUTHTOK_REQD,
+    AccountExpired = constants::PAM_ACCT_EXPIRED,
+    SessionError = constants::PAM_SESSION_ERR,
+    CredentialsUnavailable = constants::PAM_CRED_UNAVAIL,
+    CredentialsExpired = constants::PAM_CRED_EXPIRED,
+    CredentialsError = constants::PAM_CRED_ERR,
+    NoModuleData = constants::PAM_NO_MODULE_DATA,
+    ConversationError = constants::PAM_CONV_ERR,
+    AuthTokError = constants::PAM_AUTHTOK_ERR,
+    AuthTokRecoveryError = constants::PAM_AUTHTOK_RECOVERY_ERR,
+    AuthTokLockBusy = constants::PAM_AUTHTOK_LOCK_BUSY,
+    AuthTokDisableAging = constants::PAM_AUTHTOK_DISABLE_AGING,
+    TryAgain = constants::PAM_TRY_AGAIN,
+    Ignore = constants::PAM_IGNORE,
+    Abort = constants::PAM_ABORT,
+    AuthTokExpired = constants::PAM_AUTHTOK_EXPIRED,
     #[cfg(feature = "basic-ext")]
-    ModuleUnknown = pam_constants::PAM_MODULE_UNKNOWN,
+    ModuleUnknown = constants::PAM_MODULE_UNKNOWN,
     #[cfg(feature = "basic-ext")]
-    BadItem = pam_constants::PAM_BAD_ITEM,
+    BadItem = constants::PAM_BAD_ITEM,
+    #[cfg(feature = "linux-pam-ext")]
+    ConversationAgain = constants::PAM_CONV_AGAIN,
     #[cfg(feature = "linux-pam-ext")]
-    ConversationAgain = pam_constants::PAM_CONV_AGAIN,
-    #[cfg(feature = "linux-pam-ext")]
-    Incomplete = pam_constants::PAM_INCOMPLETE,
+    Incomplete = constants::PAM_INCOMPLETE,
+    #[cfg(feature = "openpam-ext")]
+    DomainUnknown = constants::PAM_DOMAIN_UNKNOWN,
+    #[cfg(feature = "openpam-ext")]
+    BadHandle = constants::PAM_BAD_HANDLE,
+    #[cfg(feature = "openpam-ext")]
+    BadFeature = constants::PAM_BAD_FEATURE,
+    #[cfg(feature = "openpam-ext")]
+    BadConstant = constants::PAM_BAD_CONSTANT,
 }
 
 /// A PAM-specific Result type with an [ErrorCode] error.
@@ -228,13 +280,35 @@
     fn test_enums() {
         assert_eq!(Ok(()), ErrorCode::result_from(0));
         assert_eq!(
-            pam_constants::PAM_SESSION_ERR,
+            constants::PAM_SESSION_ERR,
             ErrorCode::result_to_c::<()>(Err(ErrorCode::SessionError))
         );
         assert_eq!(
             Err(ErrorCode::Abort),
-            ErrorCode::result_from(pam_constants::PAM_ABORT)
+            ErrorCode::result_from(constants::PAM_ABORT)
         );
         assert_eq!(Err(ErrorCode::SystemError), ErrorCode::result_from(423));
     }
+
+    #[test]
+    fn test_flag_enums() {
+        AuthtokAction::extract(-1).expect_err("too many set");
+        AuthtokAction::extract(0).expect_err("too few set");
+        assert_eq!(
+            Ok((
+                AuthtokAction::Update,
+                AuthtokFlags::from_bits_retain(0x7fff0000)
+            )),
+            AuthtokAction::extract(0x7fff0000 | constants::PAM_UPDATE_AUTHTOK)
+        );
+        CredAction::extract(0xffff).expect_err("too many set");
+        assert_eq!(
+            Ok((CredAction::Establish, BaseFlags::empty())),
+            CredAction::extract(0)
+        );
+        assert_eq!(
+            Ok((CredAction::Delete, BaseFlags::from_bits_retain(0x55000000))),
+            CredAction::extract(0x55000000 | constants::PAM_DELETE_CRED)
+        );
+    }
 }
--- a/src/handle.rs	Mon Jul 14 18:56:55 2025 -0400
+++ b/src/handle.rs	Tue Jul 15 00:32:24 2025 -0400
@@ -1,7 +1,7 @@
 //! 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::constants::{AuthnFlags, AuthtokFlags, Result};
 use crate::conv::Conversation;
 use crate::environ::{EnvironMap, EnvironMapMut};
 use crate::items::{getter, Items, ItemsMut};
@@ -119,7 +119,7 @@
     ///
     #[doc = guide!(adg: "adg-interface-by-app-expected.html#adg-pam_authenticate")]
     #[doc = stdlinks!(3 pam_authenticate)]
-    fn authenticate(&mut self, flags: Flags) -> Result<()>;
+    fn authenticate(&mut self, flags: AuthnFlags) -> Result<()>;
 
     /// Verifies the validity of the user's account (and other stuff).
     ///
@@ -132,7 +132,7 @@
     ///
     #[doc = guide!(adg: "adg-interface-by-app-expected.html#adg-pam_acct_mgmt")]
     #[doc = stdlinks!(3 pam_acct_mgmt)]
-    fn account_management(&mut self, flags: Flags) -> Result<()>;
+    fn account_management(&mut self, flags: AuthnFlags) -> Result<()>;
 
     /// Changes the authentication token.
     ///
@@ -141,7 +141,7 @@
     ///
     #[doc = guide!(adg: "adg-interface-by-app-expected.html#adg-pam_chauthtok")]
     #[doc = stdlinks!(3 pam_chauthtok)]
-    fn change_authtok(&mut self, flags: Flags) -> Result<()>;
+    fn change_authtok(&mut self, flags: AuthtokFlags) -> Result<()>;
 }
 
 /// Functionality of a PAM handle that can be expected by a PAM module.
--- a/src/lib.rs	Mon Jul 14 18:56:55 2025 -0400
+++ b/src/lib.rs	Tue Jul 15 00:32:24 2025 -0400
@@ -43,7 +43,9 @@
 pub use crate::libpam::{LibPamHandle, LibPamTransaction};
 #[doc(inline)]
 pub use crate::{
-    constants::{ErrorCode, Flags, Result},
+    constants::{
+        AuthnFlags, AuthtokAction, AuthtokFlags, BaseFlags, CredAction, ErrorCode, Result,
+    },
     conv::{BinaryData, Conversation, ConversationAdapter},
     environ::{EnvironMap, EnvironMapMut},
     handle::{ModuleClient, PamShared, Transaction},
--- a/src/libpam/handle.rs	Mon Jul 14 18:56:55 2025 -0400
+++ b/src/libpam/handle.rs	Tue Jul 15 00:32:24 2025 -0400
@@ -9,7 +9,7 @@
 use crate::libpam::items::{LibPamItems, LibPamItemsMut};
 use crate::libpam::{items, memory};
 use crate::logging::{Level, Location, Logger};
-use crate::{Conversation, EnvironMap, Flags, ModuleClient, Transaction};
+use crate::{AuthnFlags, AuthtokFlags, Conversation, EnvironMap, ModuleClient, Transaction};
 use libpam_sys_consts::constants;
 use num_enum::{IntoPrimitive, TryFromPrimitive};
 use std::any::TypeId;
@@ -153,8 +153,8 @@
 }
 
 macro_rules! wrap {
-    (fn $name:ident { $pam_func:ident }) => {
-        fn $name(&mut self, flags: Flags) -> Result<()> {
+    (fn $name:ident($ftype:ident) { $pam_func:ident }) => {
+        fn $name(&mut self, flags: $ftype) -> Result<()> {
             ErrorCode::result_from(unsafe {
                 libpam_sys::$pam_func((self as *mut Self).cast(), flags.bits())
             })
@@ -163,9 +163,9 @@
 }
 
 impl Transaction for LibPamHandle {
-    wrap!(fn authenticate { pam_authenticate });
-    wrap!(fn account_management { pam_acct_mgmt });
-    wrap!(fn change_authtok { pam_chauthtok });
+    wrap!(fn authenticate(AuthnFlags) { pam_authenticate });
+    wrap!(fn account_management(AuthnFlags) { pam_acct_mgmt });
+    wrap!(fn change_authtok(AuthtokFlags) { pam_chauthtok });
 }
 
 // TODO: pam_setcred - app
@@ -233,9 +233,9 @@
 }
 
 impl<C: Conversation> Transaction for LibPamTransaction<C> {
-    delegate!(fn authenticate(&mut self, flags: Flags) -> Result<()>);
-    delegate!(fn account_management(&mut self, flags: Flags) -> Result<()>);
-    delegate!(fn change_authtok(&mut self, flags: Flags) -> Result<()>);
+    delegate!(fn authenticate(&mut self, flags: AuthnFlags) -> Result<()>);
+    delegate!(fn account_management(&mut self, flags: AuthnFlags) -> Result<()>);
+    delegate!(fn change_authtok(&mut self, flags: AuthtokFlags) -> Result<()>);
 }
 
 impl<C: Conversation> PamShared for LibPamTransaction<C> {
--- a/src/libpam/module.rs	Mon Jul 14 18:56:55 2025 -0400
+++ b/src/libpam/module.rs	Tue Jul 15 00:32:24 2025 -0400
@@ -11,7 +11,7 @@
 ///
 /// ```no_run
 /// use nonstick::{
-///     pam_hooks, ConversationAdapter, Flags, LibPamTransaction, ModuleClient, PamModule,
+///     pam_hooks, ConversationAdapter, AuthnFlags, LibPamTransaction, ModuleClient, PamModule,
 ///     Result as PamResult,
 /// };
 /// use std::ffi::CStr;
@@ -21,7 +21,7 @@
 /// pam_hooks!(MyPamModule);
 ///
 /// impl<T: ModuleClient> PamModule<T> for MyPamModule {
-///     fn authenticate(handle: &mut T, args: Vec<&CStr>, flags: Flags) -> PamResult<()> {
+///     fn authenticate(handle: &mut T, args: Vec<&CStr>, flags: AuthnFlags) -> PamResult<()> {
 ///         let password = handle.authtok(Some("what's your password?".as_ref()))?;
 ///         let response =
 ///             format!("If you say your password is {password:?}, who am I to disagree?");
@@ -29,7 +29,7 @@
 ///         Ok(())
 ///     }
 ///
-///     fn account_management(handle: &mut T, args: Vec<&CStr>, flags: Flags) -> PamResult<()> {
+///     fn account_management(handle: &mut T, args: Vec<&CStr>, flags: AuthnFlags) -> PamResult<()> {
 ///         let username = handle.username(None)?;
 ///         let response = format!("Hello {username:?}! I trust you unconditionally.");
 ///         handle.info_msg(&response);
@@ -42,96 +42,98 @@
     ($ident:ident) => {
         mod _pam_hooks_scope {
             use std::ffi::{c_char, c_int, c_void, CStr};
-            use $crate::{ErrorCode, Flags, LibPamHandle, PamModule};
+            use $crate::{
+                AuthnFlags, AuthtokAction, BaseFlags, CredAction, ErrorCode, LibPamHandle,
+                PamModule,
+            };
+
+            macro_rules! handle {
+                ($pamh:ident) => {
+                    match unsafe { $pamh.cast::<LibPamHandle>().as_mut() } {
+                        Some(handle) => handle,
+                        None => return ErrorCode::Ignore.into(),
+                    }
+                };
+            }
 
             #[no_mangle]
             extern "C" fn pam_sm_acct_mgmt(
                 pamh: *mut c_void,
-                flags: Flags,
+                flags: AuthnFlags,
                 argc: c_int,
                 argv: *const *const c_char,
             ) -> c_int {
-                if let Some(handle) = unsafe { pamh.cast::<LibPamHandle>().as_mut() } {
-                    let args = extract_argv(argc, argv);
-                    ErrorCode::result_to_c(super::$ident::account_management(handle, args, flags))
-                } else {
-                    ErrorCode::Ignore as c_int
-                }
+                let handle = handle!(pamh);
+                let args = extract_argv(argc, argv);
+                ErrorCode::result_to_c(super::$ident::account_management(handle, args, flags))
             }
 
             #[no_mangle]
             extern "C" fn pam_sm_authenticate(
                 pamh: *mut c_void,
-                flags: Flags,
+                flags: AuthnFlags,
                 argc: c_int,
                 argv: *const *const c_char,
             ) -> c_int {
-                if let Some(handle) = unsafe { pamh.cast::<LibPamHandle>().as_mut() } {
-                    let args = extract_argv(argc, argv);
-                    ErrorCode::result_to_c(super::$ident::authenticate(handle, args, flags))
-                } else {
-                    ErrorCode::Ignore as c_int
-                }
+                let handle = handle!(pamh);
+                let args = extract_argv(argc, argv);
+                ErrorCode::result_to_c(super::$ident::authenticate(handle, args, flags))
             }
 
             #[no_mangle]
             extern "C" fn pam_sm_chauthtok(
                 pamh: *mut c_void,
-                flags: Flags,
+                flags: c_int,
                 argc: c_int,
                 argv: *const *const c_char,
             ) -> c_int {
-                if let Some(handle) = unsafe { pamh.cast::<LibPamHandle>().as_mut() } {
-                    let args = extract_argv(argc, argv);
-                    ErrorCode::result_to_c(super::$ident::change_authtok(handle, args, flags))
-                } else {
-                    ErrorCode::Ignore as c_int
-                }
+                let handle = handle!(pamh);
+                let (action, flags) = match AuthtokAction::extract(flags) {
+                    Ok(val) => val,
+                    Err(e) => return e.into(),
+                };
+                let args = extract_argv(argc, argv);
+                ErrorCode::result_to_c(super::$ident::change_authtok(handle, args, action, flags))
             }
 
             #[no_mangle]
             extern "C" fn pam_sm_close_session(
                 pamh: *mut c_void,
-                flags: Flags,
+                flags: BaseFlags,
                 argc: c_int,
                 argv: *const *const c_char,
             ) -> c_int {
-                if let Some(handle) = unsafe { pamh.cast::<LibPamHandle>().as_mut() } {
-                    let args = extract_argv(argc, argv);
-                    ErrorCode::result_to_c(super::$ident::close_session(handle, args, flags))
-                } else {
-                    ErrorCode::Ignore as c_int
-                }
+                let handle = handle!(pamh);
+                let args = extract_argv(argc, argv);
+                ErrorCode::result_to_c(super::$ident::close_session(handle, args, flags))
             }
 
             #[no_mangle]
             extern "C" fn pam_sm_open_session(
                 pamh: *mut c_void,
-                flags: Flags,
+                flags: BaseFlags,
                 argc: c_int,
                 argv: *const *const c_char,
             ) -> c_int {
+                let handle = handle!(pamh);
                 let args = extract_argv(argc, argv);
-                if let Some(handle) = unsafe { pamh.cast::<LibPamHandle>().as_mut() } {
-                    ErrorCode::result_to_c(super::$ident::open_session(handle, args, flags))
-                } else {
-                    ErrorCode::Ignore as c_int
-                }
+                ErrorCode::result_to_c(super::$ident::open_session(handle, args, flags))
             }
 
             #[no_mangle]
             extern "C" fn pam_sm_setcred(
                 pamh: *mut c_void,
-                flags: Flags,
+                flags: c_int,
                 argc: c_int,
                 argv: *const *const c_char,
             ) -> c_int {
+                let handle = handle!(pamh);
+                let (action, flags) = match CredAction::extract(flags) {
+                    Ok(val) => val,
+                    Err(e) => return e.into(),
+                };
                 let args = extract_argv(argc, argv);
-                if let Some(handle) = unsafe { pamh.cast::<LibPamHandle>().as_mut() } {
-                    ErrorCode::result_to_c(super::$ident::set_credentials(handle, args, flags))
-                } else {
-                    ErrorCode::Ignore as c_int
-                }
+                ErrorCode::result_to_c(super::$ident::set_credentials(handle, args, action, flags))
             }
 
             /// Turns `argc`/`argv` into a [Vec] of [CStr]s.
--- a/src/module.rs	Mon Jul 14 18:56:55 2025 -0400
+++ b/src/module.rs	Tue Jul 15 00:32:24 2025 -0400
@@ -3,7 +3,9 @@
 // Temporarily allowed until we get the actual conversation functions hooked up.
 #![allow(dead_code)]
 
-use crate::constants::{ErrorCode, Flags, Result};
+use crate::constants::{
+    AuthnFlags, AuthtokAction, AuthtokFlags, BaseFlags, CredAction, ErrorCode, Result,
+};
 use crate::handle::ModuleClient;
 use std::ffi::CStr;
 
@@ -35,13 +37,6 @@
     /// See [the Module Writer's Guide entry for `pam_sm_authenticate`][mwg]
     /// for more information.
     ///
-    /// # Valid flags
-    ///
-    /// This function may be called with the following flags set:
-    ///
-    /// - [`Flags::SILENT`]
-    /// - [`Flags::DISALLOW_NULL_AUTHTOK`]
-    ///
     /// # Returns
     ///
     /// If the password check was successful, return `Ok(())`.
@@ -59,7 +54,7 @@
     ///   They should not try again.
     ///
     /// [mwg]: https://www.chiark.greenend.org.uk/doc/libpam-doc/html/mwg-expected-of-module-auth.html#mwg-pam_sm_authenticate
-    fn authenticate(handle: &mut T, args: Vec<&CStr>, flags: Flags) -> Result<()> {
+    fn authenticate(handle: &mut T, args: Vec<&CStr>, flags: AuthnFlags) -> Result<()> {
         Err(ErrorCode::Ignore)
     }
 
@@ -79,13 +74,6 @@
     /// See [the Module Writer's Guide entry for `pam_sm_acct_mgmt`][mwg]
     /// for more information.
     ///
-    /// # Valid flags
-    ///
-    /// This function may be called with the following flags set:
-    ///
-    /// - [`Flags::SILENT`]
-    /// - [`Flags::DISALLOW_NULL_AUTHTOK`]
-    ///
     /// # Returns
     ///
     /// If the user should be allowed to log in, return `Ok(())`.
@@ -101,7 +89,7 @@
     /// - [`ErrorCode::UserUnknown`]: The supplied username is not known by this service.
     ///
     /// [mwg]: https://www.chiark.greenend.org.uk/doc/libpam-doc/html/mwg-expected-of-module-acct.html#mwg-pam_sm_acct_mgmt
-    fn account_management(handle: &mut T, args: Vec<&CStr>, flags: Flags) -> Result<()> {
+    fn account_management(handle: &mut T, args: Vec<&CStr>, flags: AuthnFlags) -> Result<()> {
         Err(ErrorCode::Ignore)
     }
 
@@ -137,7 +125,12 @@
     /// - [`ErrorCode::UserUnknown`]: The supplied username is not known by this service.
     ///
     /// [mwg]: https://www.chiark.greenend.org.uk/doc/libpam-doc/html/mwg-expected-of-module-auth.html#mwg-pam_sm_setcred
-    fn set_credentials(handle: &mut T, args: Vec<&CStr>, flags: Flags) -> Result<()> {
+    fn set_credentials(
+        handle: &mut T,
+        args: Vec<&CStr>,
+        action: CredAction,
+        flags: BaseFlags,
+    ) -> Result<()> {
         Err(ErrorCode::Ignore)
     }
 
@@ -155,18 +148,6 @@
     /// See [the Module Writer's Guide entry for `pam_sm_chauthtok`][mwg]
     /// for more information.
     ///
-    /// # Valid flags
-    ///
-    /// This function may be called with the following flags set:
-    ///
-    /// - [`Flags::SILENT`]
-    /// - [`Flags::CHANGE_EXPIRED_AUTHTOK`]: This module should only change
-    ///   any expired passwords, and leave non-expired passwords alone.
-    ///   If present, it _must_ be combined with one of the following.
-    /// - [`Flags::PRELIMINARY_CHECK`]: Don't actually change the password,
-    ///   just check if the new one is valid.
-    /// - [`Flags::UPDATE_AUTHTOK`]: Do actually change the password.
-    ///
     /// # Returns
     ///
     /// If the authentication token was changed successfully
@@ -185,7 +166,12 @@
     /// - [`ErrorCode::UserUnknown`]: The supplied username is not known by this service.
     ///
     /// [mwg]: https://www.chiark.greenend.org.uk/doc/libpam-doc/html/mwg-expected-of-module-chauthtok.html#mwg-pam_sm_chauthtok
-    fn change_authtok(handle: &mut T, args: Vec<&CStr>, flags: Flags) -> Result<()> {
+    fn change_authtok(
+        handle: &mut T,
+        args: Vec<&CStr>,
+        action: AuthtokAction,
+        flags: AuthtokFlags,
+    ) -> Result<()> {
         Err(ErrorCode::Ignore)
     }
 
@@ -196,10 +182,6 @@
     /// See [the Module Writer's Guide entry for `pam_sm_open_session`][mwg]
     /// for more information.
     ///
-    /// # Valid flags
-    ///
-    /// The only valid flag is [`Flags::SILENT`].
-    ///
     /// # Returns
     ///
     /// If the session was opened successfully, return `Ok(())`.
@@ -209,7 +191,7 @@
     /// - [`ErrorCode::SessionError`]: Cannot make an entry for this session.
     ///
     /// [mwg]: https://www.chiark.greenend.org.uk/doc/libpam-doc/html/mwg-expected-of-module-session.html#mwg-pam_sm_open_session
-    fn open_session(handle: &mut T, args: Vec<&CStr>, flags: Flags) -> Result<()> {
+    fn open_session(handle: &mut T, args: Vec<&CStr>, flags: BaseFlags) -> Result<()> {
         Err(ErrorCode::Ignore)
     }
 
@@ -218,10 +200,6 @@
     /// See [the Module Writer's Guide entry for `pam_sm_close_session`][mwg]
     /// for more information.
     ///
-    /// # Valid flags
-    ///
-    /// The only valid flag is [`Flags::SILENT`].
-    ///
     /// # Returns
     ///
     /// If the session was closed successfully, return `Ok(())`.
@@ -231,7 +209,7 @@
     /// - [`ErrorCode::SessionError`]: Cannot remove an entry for this session.
     ///
     /// [mwg]: https://www.chiark.greenend.org.uk/doc/libpam-doc/html/mwg-expected-of-module-session.html#mwg-pam_sm_close_session
-    fn close_session(handle: &mut T, args: Vec<&CStr>, flags: Flags) -> Result<()> {
+    fn close_session(handle: &mut T, args: Vec<&CStr>, flags: BaseFlags) -> Result<()> {
         Err(ErrorCode::Ignore)
     }
 }
--- a/testharness/src/bin/testharness.rs	Mon Jul 14 18:56:55 2025 -0400
+++ b/testharness/src/bin/testharness.rs	Tue Jul 15 00:32:24 2025 -0400
@@ -3,7 +3,9 @@
 use nonstick::conv::Exchange;
 use nonstick::items::Items;
 use nonstick::libpam::TransactionBuilder;
-use nonstick::{Conversation, ErrorCode, Flags, LibPamTransaction, PamShared, Transaction};
+use nonstick::{
+    AuthnFlags, AuthtokFlags, Conversation, ErrorCode, LibPamTransaction, PamShared, Transaction,
+};
 use std::cell::Cell;
 use std::ffi::OsString;
 use std::os::unix::ffi::OsStrExt;
@@ -93,7 +95,7 @@
         ..Default::default()
     };
     let mut tx = harness.start();
-    let auth = tx.authenticate(Flags::empty());
+    let auth = tx.authenticate(AuthnFlags::empty());
     assert_eq!(auth, Err(ErrorCode::UserUnknown));
 }
 
@@ -103,19 +105,20 @@
         ..Default::default()
     };
     let mut tx = harness.start();
-    let auth = tx.authenticate(Flags::empty());
+    let auth = tx.authenticate(AuthnFlags::empty());
     assert_eq!(auth, Err(ErrorCode::AuthenticationError));
 }
 
 fn test_correct() {
     let harness = TestHarness::default();
     let mut tx = harness.start();
-    tx.authenticate(Flags::empty()).unwrap();
+    tx.authenticate(AuthnFlags::empty()).unwrap();
     assert_eq!(tx.items().user().unwrap().unwrap(), "updated-in-process");
-    let result = tx.account_management(Flags::empty());
+    let result = tx.account_management(AuthnFlags::empty());
     assert_eq!(result, Err(ErrorCode::NewAuthTokRequired));
     harness.changing_password.set(true);
-    let change = tx.change_authtok(Flags::CHANGE_EXPIRED_AUTHTOK);
+    let change = tx.change_authtok(AuthtokFlags::CHANGE_EXPIRED_AUTHTOK);
     assert_eq!(change, Err(ErrorCode::TryAgain));
-    tx.change_authtok(Flags::CHANGE_EXPIRED_AUTHTOK).unwrap();
+    tx.change_authtok(AuthtokFlags::CHANGE_EXPIRED_AUTHTOK)
+        .unwrap();
 }
--- a/testharness/src/lib.rs	Mon Jul 14 18:56:55 2025 -0400
+++ b/testharness/src/lib.rs	Tue Jul 15 00:32:24 2025 -0400
@@ -5,14 +5,17 @@
 extern crate nonstick;
 
 use nonstick::conv::{ErrorMsg, InfoMsg, MaskedQAndA, QAndA};
-use nonstick::{error, info, pam_hooks, ErrorCode, Flags, ModuleClient, PamModule};
+use nonstick::{
+    error, info, pam_hooks, AuthnFlags, AuthtokAction, AuthtokFlags, ErrorCode, ModuleClient,
+    PamModule,
+};
 use std::ffi::{CStr, OsString};
 use std::os::unix::ffi::OsStrExt;
 
 struct TestHarness;
 
 impl<M: ModuleClient> PamModule<M> for TestHarness {
-    fn authenticate(handle: &mut M, args: Vec<&CStr>, _: Flags) -> nonstick::Result<()> {
+    fn authenticate(handle: &mut M, args: Vec<&CStr>, _: AuthnFlags) -> nonstick::Result<()> {
         let strings: Vec<_> = args.iter().map(|&a| Vec::from(a.to_bytes())).collect();
         if strings != vec![Vec::from(b"param"), Vec::from(b"param2")] {
             return Err(ErrorCode::SystemError);
@@ -51,7 +54,7 @@
         }
     }
 
-    fn account_management(handle: &mut M, _: Vec<&CStr>, _: Flags) -> nonstick::Result<()> {
+    fn account_management(handle: &mut M, _: Vec<&CStr>, _: AuthnFlags) -> nonstick::Result<()> {
         let value: &Cell<i32> = match handle.username(None)?.as_bytes() {
             b"initial" => return Err(ErrorCode::AccountExpired),
             b"updated-in-process" => handle.get_module_data("florgus"),
@@ -77,26 +80,31 @@
         }
     }
 
-    fn change_authtok(handle: &mut M, _: Vec<&CStr>, flags: Flags) -> nonstick::Result<()> {
-        if flags.contains(Flags::PRELIMINARY_CHECK) {
-            let password = handle.authtok(None)?;
-            if password.as_bytes() != b"acceptable" {
-                return Err(ErrorCode::PermissionDenied);
+    fn change_authtok(
+        handle: &mut M,
+        _: Vec<&CStr>,
+        action: AuthtokAction,
+        _flags: AuthtokFlags,
+    ) -> nonstick::Result<()> {
+        match action {
+            AuthtokAction::PreliminaryCheck => {
+                let password = handle.authtok(None)?;
+                if password.as_bytes() != b"acceptable" {
+                    return Err(ErrorCode::PermissionDenied);
+                }
+                handle.set_module_data("checked_pass", password)
             }
-            handle.set_module_data("checked_pass", password)
-        } else if flags.contains(Flags::UPDATE_AUTHTOK) {
-            let password = handle.authtok(None)?;
-            let checked: &OsString = handle
-                .get_module_data("checked_pass")
-                .ok_or(ErrorCode::SystemError)?;
-            if password != *checked {
-                error!(handle, "password mismatch? {password:?} {checked:?}");
-                return Err(ErrorCode::AuthenticationError);
+            AuthtokAction::Update => {
+                let password = handle.authtok(None)?;
+                let checked: &OsString = handle
+                    .get_module_data("checked_pass")
+                    .ok_or(ErrorCode::SystemError)?;
+                if password != *checked {
+                    error!(handle, "password mismatch? {password:?} {checked:?}");
+                    return Err(ErrorCode::AuthenticationError);
+                }
+                Ok(())
             }
-            Ok(())
-        } else {
-            error!(handle, "invalid flag state: {flags:?}");
-            Err(ErrorCode::SystemError)
         }
     }
 }