view src/constants.rs @ 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 4b3a5095f68c
children f052e2417195
line wrap: on
line source

//! Constants and enum values from the PAM library.

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;
use std::fmt;
use std::fmt::{Display, Formatter};
use std::result::Result as StdResult;

/// 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)*
            }
        }
    }
}

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;
    }
}

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;

        /// 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 {}
}

#[cfg(feature = "openpam-ext")]
const BAD_CONST: ErrorCode = ErrorCode::BadConstant;
#[cfg(not(feature = "openpam-ext"))]
const BAD_CONST: ErrorCode = ErrorCode::SystemError;

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,
            )*
        }

        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))
            }
        }
    }
}

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,
    }
}

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)))
    }
}

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),
        }
    }
}

/// The PAM error return codes.
///
/// These are returned by most PAM functions if an error of some kind occurs.
///
/// Instead of being an error code, success is represented by an Ok [`Result`].
///
/// # References
///
#[doc = linklist!(pam: man7, manbsd)]
/// - [X/SSO error code specification][xsso]
///
#[doc = man7!(3 pam "RETURN_VALUES")]
#[doc = manbsd!(3 pam "RETURN%20VALUES")]
#[doc = xsso!("chap5.htm#tagcjh_06_02")]
#[allow(non_camel_case_types, dead_code)]
#[derive(Copy, Clone, Debug, PartialEq, TryFromPrimitive, IntoPrimitive)]
#[non_exhaustive] // C might give us anything!
#[repr(i32)]
pub enum ErrorCode {
    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 = constants::PAM_MODULE_UNKNOWN,
    #[cfg(feature = "basic-ext")]
    BadItem = constants::PAM_BAD_ITEM,
    #[cfg(feature = "linux-pam-ext")]
    ConversationAgain = constants::PAM_CONV_AGAIN,
    #[cfg(feature = "linux-pam-ext")]
    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.
pub type Result<T> = StdResult<T, ErrorCode>;

impl Display for ErrorCode {
    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
        match strerror((*self).into()) {
            Some(err) => f.write_str(err),
            None => self.fmt_internal(f),
        }
    }
}

impl Error for ErrorCode {}

impl ErrorCode {
    /// Converts this [Result] into a C-compatible result code.
    pub fn result_to_c<T>(value: Result<T>) -> c_int {
        match value {
            Ok(_) => 0, // PAM_SUCCESS
            Err(otherwise) => otherwise.into(),
        }
    }

    /// Converts a C result code into a [Result], with success as Ok.
    /// Invalid values are returned as a [Self::SystemError].
    pub fn result_from(value: c_int) -> Result<()> {
        match value {
            0 => Ok(()),
            value => Err(value.try_into().unwrap_or(Self::SystemError)),
        }
    }

    /// A basic Display implementation for if we don't link against PAM.
    fn fmt_internal(self, f: &mut Formatter<'_>) -> fmt::Result {
        let n: c_int = self.into();
        write!(f, "PAM error: {self:?} ({n})")
    }
}

/// Gets a string version of an error message.
#[cfg(feature = "link")]
pub fn strerror(code: c_int) -> Option<&'static str> {
    use std::ffi::CStr;
    use std::ptr;
    // SAFETY: PAM impls don't care about the PAM handle and always return
    // static strings.
    let strerror = unsafe { libpam_sys::pam_strerror(ptr::null(), code as c_int) };
    // SAFETY: We just got this back from PAM and we checked if it's null.
    (!strerror.is_null())
        .then(|| unsafe { CStr::from_ptr(strerror) }.to_str().ok())
        .flatten()
}

/// Dummy implementation of strerror so that it always returns None.
#[cfg(not(feature = "link"))]
pub fn strerror(_: c_int) -> Option<&'static str> {
    None
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_enums() {
        assert_eq!(Ok(()), ErrorCode::result_from(0));
        assert_eq!(
            constants::PAM_SESSION_ERR,
            ErrorCode::result_to_c::<()>(Err(ErrorCode::SessionError))
        );
        assert_eq!(
            Err(ErrorCode::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)
        );
    }
}