changeset 97:efe2f5f8b5b2

Implement "stateless" application-side PAM calls. This introduces `authenticate`, `account_management`, and `change_authtok`. These are the three PAM operations that are stateless (i.e., they don't start a session or modify global credentials).
author Paul Fisher <paul@pfish.zone>
date Mon, 23 Jun 2025 19:10:34 -0400
parents f3e260f9ddcb
children b87100c5eed4
files build.rs src/constants.rs src/handle.rs src/libpam/conversation.rs src/libpam/handle.rs src/libpam/pam_ffi.rs
diffstat 6 files changed, 90 insertions(+), 10 deletions(-) [+]
line wrap: on
line diff
--- a/build.rs	Mon Jun 23 14:26:34 2025 -0400
+++ b/build.rs	Mon Jun 23 19:10:34 2025 -0400
@@ -18,6 +18,9 @@
             .allowlist_function("pam_get_authtok")
             .allowlist_function("pam_end")
             .allowlist_function("pam_strerror")
+            .allowlist_function("pam_authenticate")
+            .allowlist_function("pam_chauthtok")
+            .allowlist_function("pam_acct_mgmt")
             .default_macro_constant_type(MacroTypeVariation::Unsigned);
 
         let linux_builder = common_builder
--- a/src/constants.rs	Mon Jun 23 14:26:34 2025 -0400
+++ b/src/constants.rs	Mon Jun 23 19:10:34 2025 -0400
@@ -95,7 +95,7 @@
     ///
     /// See `/usr/include/security/_pam_types.h` and
     /// See `/usr/include/security/pam_modules.h` for more details.
-    #[derive(Debug, PartialEq)]
+    #[derive(Debug, Default, PartialEq)]
     #[repr(transparent)]
     pub struct Flags: c_uint {
         /// The module should not generate any messages.
--- a/src/handle.rs	Mon Jun 23 14:26:34 2025 -0400
+++ b/src/handle.rs	Mon Jun 23 19:10:34 2025 -0400
@@ -1,6 +1,6 @@
 //! The wrapper types and traits for handles into the PAM library.
 
-use crate::constants::Result;
+use crate::constants::{Flags, Result};
 use crate::conv::Conversation;
 use crate::logging::Level;
 
@@ -248,7 +248,14 @@
 /// Like [`PamShared`], this is intended to allow creating mock implementations
 /// of PAM for testing PAM applications.
 pub trait PamHandleApplication: PamShared {
-    // reserved!
+    /// Starts the authentication process for the user.
+    fn authenticate(&mut self, flags: Flags) -> Result<()>;
+    
+    /// Does "account management".
+    fn account_management(&mut self, flags: Flags) -> Result<()>;
+    
+    /// Changes the authentication token.
+    fn change_authtok(&mut self, flags: Flags) -> Result<()>;
 }
 
 /// Functionality of a PAM handle that can be expected by a PAM module.
--- a/src/libpam/conversation.rs	Mon Jun 23 14:26:34 2025 -0400
+++ b/src/libpam/conversation.rs	Mon Jun 23 19:10:34 2025 -0400
@@ -15,10 +15,10 @@
 use std::result::Result as StdResult;
 
 impl LibPamConversation<'_> {
-    fn wrap<C: Conversation>(conv: &mut C) -> Self {
+    pub fn wrap<C: Conversation>(conv: &C) -> Self {
         Self {
             callback: Self::wrapper_callback::<C>,
-            appdata: (conv as *mut C).cast(),
+            appdata: (conv as *const C).cast(),
             life: PhantomData,
             _marker: Immovable(PhantomData),
         }
@@ -31,13 +31,13 @@
         count: c_int,
         questions: *const *const Question,
         answers: *mut *mut Answer,
-        me: *mut AppData,
+        me: *const AppData,
     ) -> c_int {
         let internal = || {
             // Collect all our pointers
             let conv = me
                 .cast::<C>()
-                .as_mut()
+                .as_ref()
                 .ok_or(ErrorCode::ConversationError)?;
             let indirect = Questions::borrow_ptr(questions, count as usize);
             let answers_ptr = answers.as_mut().ok_or(ErrorCode::ConversationError)?;
--- a/src/libpam/handle.rs	Mon Jun 23 14:26:34 2025 -0400
+++ b/src/libpam/handle.rs	Mon Jun 23 19:10:34 2025 -0400
@@ -5,7 +5,7 @@
 pub use crate::libpam::pam_ffi::LibPamHandle;
 use crate::libpam::{memory, pam_ffi};
 use crate::logging::Level;
-use crate::{Conversation, PamHandleModule};
+use crate::{Conversation, Flags, PamHandleApplication, PamHandleModule};
 use num_enum::{IntoPrimitive, TryFromPrimitive};
 use std::cell::Cell;
 use std::ffi::{c_char, c_int, CString};
@@ -36,6 +36,76 @@
     _conversation_lifetime: PhantomData<&'a mut ()>,
 }
 
+#[derive(Debug, PartialEq)]
+pub struct HandleBuilder {
+    service_name: String,
+    username: Option<String>,
+}
+
+impl HandleBuilder {
+    /// Creates a new HandleBuilder for the given service.
+    fn new(service_name: String) -> Self {
+        Self{service_name, username: Default::default()}
+    }
+    /// Updates the service name.
+    pub fn service_name(mut self, service_name: String) -> Self {
+        self.service_name = service_name; self
+    }
+    /// Updates the username.
+    pub fn username(mut self, username: String) -> Self {
+        self.username = Some(username); self
+    }
+
+    pub fn build(self, conv: &impl Conversation) -> Result<OwnedLibPamHandle> {
+        OwnedLibPamHandle::start(self.service_name, self.username, conv)
+    }
+}
+
+impl OwnedLibPamHandle<'_> {
+    pub fn build_with_service(service_name: String) -> HandleBuilder {
+        HandleBuilder::new(service_name)
+    }
+    fn start(service_name: String, username: Option<String>, conversation: &impl Conversation) -> Result<Self> {
+        let conv = LibPamConversation::wrap(conversation);
+        let service_cstr = CString::new(service_name).map_err(|_| ErrorCode::ConversationError)?;
+        let username_cstr = memory::prompt_ptr(memory::option_cstr(username.as_deref())?.as_ref());
+
+        let mut handle: *mut LibPamHandle = ptr::null_mut();
+        // SAFETY: We've set everything up properly to call `pam_start`.
+        // The returned value will be a valid pointer provided the result is OK.
+        let result = unsafe { pam_ffi::pam_start(service_cstr.as_ptr(), username_cstr, &conv, &mut handle) };
+        ErrorCode::result_from(result)?;
+        Ok(Self{
+            handle: HandleWrap(handle),
+            last_return: Cell::new(Ok(())),
+            _conversation_lifetime: Default::default(),
+        })
+    }
+}
+
+impl PamHandleApplication for OwnedLibPamHandle<'_> {
+    fn authenticate(&mut self, flags: Flags) -> Result<()> {
+        let ret = unsafe { pam_ffi::pam_authenticate(self.handle.0, flags.bits() as c_int)};
+        let result = ErrorCode::result_from(ret);
+        self.last_return.set(result);
+        result
+    }
+
+    fn account_management(&mut self, flags: Flags) -> Result<()> {
+        let ret = unsafe { pam_ffi::pam_acct_mgmt(self.handle.0, flags.bits() as c_int)};
+        let result = ErrorCode::result_from(ret);
+        self.last_return.set(result);
+        result
+    }
+
+    fn change_authtok(&mut self, flags: Flags) -> Result<()> {
+        let ret = unsafe { pam_ffi::pam_chauthtok(self.handle.0, flags.bits() as c_int)};
+        let result = ErrorCode::result_from(ret);
+        self.last_return.set(result);
+        result
+    }
+}
+
 // TODO: pam_authenticate - app
 //       pam_setcred - app
 //       pam_acct_mgmt - app
--- a/src/libpam/pam_ffi.rs	Mon Jun 23 14:26:34 2025 -0400
+++ b/src/libpam/pam_ffi.rs	Mon Jun 23 19:10:34 2025 -0400
@@ -76,7 +76,7 @@
     num_msg: c_int,
     questions: *const *const Question,
     answers: *mut *mut Answer,
-    appdata: *mut AppData,
+    appdata: *const AppData,
 ) -> c_int;
 
 /// The type used by PAM to call back into a conversation.
@@ -86,7 +86,7 @@
     pub callback: ConversationCallback,
     /// The pointer that will be passed as the last parameter
     /// to the conversation callback.
-    pub appdata: *mut AppData,
+    pub appdata: *const AppData,
     pub life: PhantomData<&'a mut ()>,
     pub _marker: Immovable,
 }