view src/constants.rs @ 74:c7c596e6388f

Make conversations type-safe (last big reorg) (REAL) (NOT CLICKBAIT) In previous versions of Conversation, you could send messages and then return messages of the wrong type or in the wrong order or whatever. The receiver would then have to make sure that there were the right number of messages and that each message was the right type. That's annoying. This change makes the `Message` enum a two-way channel, where the asker puts their question into it, and then the answerer (the conversation) puts the answer in and returns control to the asker. The asker then only has to pull the Answer of the type they wanted out of the message.
author Paul Fisher <paul@pfish.zone>
date Fri, 06 Jun 2025 22:21:17 -0400
parents 58f9d2a4df38
children 351bdc13005e
line wrap: on
line source

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

use bitflags::bitflags;
use libc::{c_int, c_uint};
use num_derive::FromPrimitive;
use num_traits::FromPrimitive;
use std::any;
use std::marker::PhantomData;
use std::result::Result as StdResult;

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, PartialEq)]
    #[repr(transparent)]
    pub struct Flags: c_uint {
        /// The module should not generate any messages.
        const SILENT = 0x8000;

        /// The module should return [ErrorCode::AuthError]
        /// if the user has an empty authentication token
        /// rather than immediately accepting them.
        const DISALLOW_NULL_AUTHTOK = 0x0001;

        // Flag used for `set_credentials`.

        /// Set user credentials for an authentication service.
        const ESTABLISH_CREDENTIALS = 0x0002;
        /// Delete user credentials associated with
        /// an authentication service.
        const DELETE_CREDENTIALS = 0x0004;
        /// Reinitialize user credentials.
        const REINITIALIZE_CREDENTIALS = 0x0008;
        /// Extend the lifetime of user credentials.
        const REFRESH_CREDENTIALS = 0x0010;

        // Flags used for password changing.

        /// 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 = 0x0020;

        /// 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 = 0x4000;
        /// 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 = 0x2000;
    }
}

/// The Linux-PAM error return values. Success is an Ok [Result].
///
/// Most abbreviations (except `AuthTok` and `Max`) are now full words.
/// For more detailed information, see
/// `/usr/include/security/_pam_types.h`.
#[allow(non_camel_case_types, dead_code)]
#[derive(Copy, Clone, Debug, PartialEq, thiserror::Error, FromPrimitive)]
#[non_exhaustive] // C might give us anything!
pub enum ErrorCode {
    #[error("dlopen() failure when dynamically loading a service module")]
    OpenError = 1,
    #[error("symbol not found")]
    SymbolError = 2,
    #[error("error in service module")]
    ServiceError = 3,
    #[error("system error")]
    SystemError = 4,
    #[error("memory buffer error")]
    BufferError = 5,
    #[error("permission denied")]
    PermissionDenied = 6,
    #[error("authentication failure")]
    AuthenticationError = 7,
    #[error("cannot access authentication data due to insufficient credentials")]
    CredentialsInsufficient = 8,
    #[error("underlying authentication service cannot retrieve authentication information")]
    AuthInfoUnavailable = 9,
    #[error("user not known to the underlying authentication module")]
    UserUnknown = 10,
    #[error("retry limit reached; do not attempt further")]
    MaxTries = 11,
    #[error("new authentication token required")]
    NewAuthTokRequired = 12,
    #[error("user account has expired")]
    AccountExpired = 13,
    #[error("cannot make/remove an entry for the specified session")]
    SessionError = 14,
    #[error("underlying authentication service cannot retrieve user credentials")]
    CredentialsUnavailable = 15,
    #[error("user credentials expired")]
    CredentialsExpired = 16,
    #[error("failure setting user credentials")]
    CredentialsError = 17,
    #[error("no module-specific data is present")]
    NoModuleData = 18,
    #[error("conversation error")]
    ConversationError = 19,
    #[error("authentication token manipulation error")]
    AuthTokError = 20,
    #[error("authentication information cannot be recovered")]
    AuthTokRecoveryError = 21,
    #[error("authentication token lock busy")]
    AuthTokLockBusy = 22,
    #[error("authentication token aging disabled")]
    AuthTokDisableAging = 23,
    #[error("preliminary password check failed")]
    TryAgain = 24,
    #[error("ignore underlying account module, regardless of control flag")]
    Ignore = 25,
    #[error("critical error; this module should fail now")]
    Abort = 26,
    #[error("authentication token has expired")]
    AuthTokExpired = 27,
    #[error("module is not known")]
    ModuleUnknown = 28,
    #[error("bad item passed to pam_[whatever]_item")]
    BadItem = 29,
    #[error("conversation function is event-driven and data is not available yet")]
    ConversationAgain = 30,
    #[error("call this function again to complete authentication stack")]
    Incomplete = 31,
}

/// A PAM-specific Result type with an [ErrorCode] error.
pub type Result<T> = StdResult<T, 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)),
        }
    }
}

impl TryFrom<c_int> for ErrorCode {
    type Error = InvalidEnum<Self>;

    fn try_from(value: c_int) -> StdResult<Self, Self::Error> {
        Self::from_i32(value).ok_or(value.into())
    }
}

impl From<ErrorCode> for c_int {
    fn from(val: ErrorCode) -> Self {
        val as Self
    }
}

/// Error returned when attempting to coerce an invalid C integer into an enum.
#[derive(thiserror::Error)]
#[error("{0} is not a valid {type}", type = any::type_name::<T>())]
#[derive(Debug, PartialEq)]
pub struct InvalidEnum<T>(c_int, PhantomData<T>);

impl<T> From<InvalidEnum<T>> for c_int {
    fn from(value: InvalidEnum<T>) -> Self {
        value.0
    }
}

impl<T> From<c_int> for InvalidEnum<T> {
    fn from(value: c_int) -> Self {
        Self(value, PhantomData)
    }
}

/// Returned when text that should not have any `\0` bytes in it does.
/// Analogous to [`std::ffi::NulError`], but the data it was created from
/// is borrowed.
#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_enums() {
        assert_eq!(Ok(ErrorCode::ServiceError), 3.try_into());
        assert_eq!(Err(InvalidEnum::from(999)), ErrorCode::try_from(999));
        assert_eq!(Ok(()), ErrorCode::result_from(0));
        assert_eq!(Err(ErrorCode::Abort), ErrorCode::result_from(26));
        assert_eq!(Err(ErrorCode::SystemError), ErrorCode::result_from(423));
        assert!(InvalidEnum::<ErrorCode>(33, PhantomData)
            .to_string()
            .starts_with("33 is not a valid "));
    }
}