Mercurial > crates > nonstick
diff src/handle.rs @ 72:47eb242a4f88
Fill out the PamHandle trait.
This updates the PamHandle trait to have methods for each Item,
and implements them on the LibPamHandle.
author | Paul Fisher <paul@pfish.zone> |
---|---|
date | Wed, 04 Jun 2025 03:53:36 -0400 |
parents | 58f9d2a4df38 |
children | ac6881304c78 |
line wrap: on
line diff
--- a/src/handle.rs Tue Jun 03 21:54:58 2025 -0400 +++ b/src/handle.rs Wed Jun 04 03:53:36 2025 -0400 @@ -1,101 +1,223 @@ //! The wrapper types and traits for handles into the PAM library. use crate::constants::{ErrorCode, Result}; -use crate::items::{Item, ItemType}; +use crate::conv::Conversation; +use crate::items::ItemType; +use crate::module::ConversationMux; use crate::pam_ffi; -use crate::pam_ffi::memory; -use secure_string::SecureString; -use std::ffi::{c_char, c_int, c_void, CString}; +use crate::pam_ffi::{memory, LibPamConversation, LibPamHandle}; +use std::ffi::{c_char, c_int}; use std::{mem, ptr}; +macro_rules! trait_item { + (get = $getter:ident, item = $item:literal $(, see = $see:path)? $(, $($doc:literal)*)?) => { + $( + $(#[doc = $doc])* + #[doc = ""] + )? + #[doc = concat!("Gets the `", $item, "` of the PAM handle.")] + $( + #[doc = concat!("See [`", stringify!($see), "`].")] + )? + #[doc = ""] + #[doc = "Returns a reference to the item's value, owned by PAM."] + #[doc = "The item is assumed to be valid UTF-8 text."] + #[doc = "If it is not, `ConversationError` is returned."] + #[doc = ""] + #[doc = "See the [`pam_get_item`][man] manual page,"] + #[doc = "[`pam_get_item` in the Module Writers' Guide][mwg], or"] + #[doc = "[`pam_get_item` in the Application Developers' Guide][adg]."] + #[doc = ""] + #[doc = "[man]: https://www.man7.org/linux/man-pages/man3/pam_get_item.3.html"] + #[doc = "[adg]: https://www.chiark.greenend.org.uk/doc/libpam-doc/html/adg-interface-by-app-expected.html#adg-pam_get_item"] + #[doc = "[mwg]: https://www.chiark.greenend.org.uk/doc/libpam-doc/html/mwg-expected-by-module-item.html#mwg-pam_get_item"] + fn $getter(&mut self) -> Result<Option<&str>>; + }; + (set = $setter:ident, item = $item:literal $(, see = $see:path)? $(, $($doc:literal)*)?) => { + $( + $(#[doc = $doc])* + #[doc = ""] + )? + #[doc = concat!("Sets the `", $item, "` from the PAM handle.")] + $( + #[doc = concat!("See [`", stringify!($see), "`].")] + )? + #[doc = ""] + #[doc = "Sets the item's value. PAM copies the string's contents."] + #[doc = "If the string contains a null byte, this will return "] + #[doc = "a `ConversationError`."] + #[doc = ""] + #[doc = "See the [`pam_set_item`][man] manual page,"] + #[doc = "[`pam_set_item` in the Module Writers' Guide][mwg], or"] + #[doc = "[`pam_set_item` in the Application Developers' Guide][adg]."] + #[doc = ""] + #[doc = "[man]: https://www.man7.org/linux/man-pages/man3/pam_set_item.3.html"] + #[doc = "[adg]: https://www.chiark.greenend.org.uk/doc/libpam-doc/html/adg-interface-by-app-expected.html#adg-pam_set_item"] + #[doc = "[mwg]: https://www.chiark.greenend.org.uk/doc/libpam-doc/html/mwg-expected-by-module-item.html#mwg-pam_set_item"] + fn $setter(&mut self, value: Option<&str>) -> Result<()>; + }; +} + /// Features of a PAM handle that are available to applications and modules. /// /// You probably want [`LibPamHandle`]. This trait is intended to allow creating /// mock PAM handle types used for testing PAM modules and applications. pub trait PamHandle { + type Conv: Conversation; /// Retrieves the name of the user who is authenticating or logging in. /// - /// This is effectively like `handle.get_item::<Item::User>()`. + /// If the username has previously been obtained, this uses that username; + /// otherwise it prompts the user with the first of these that is present: + /// + /// 1. The prompt string passed to this function. + /// 2. The string returned by `get_user_prompt_item`. + /// 3. The default prompt, `login: ` + /// /// See the [`pam_get_user` manual page][man] /// or [`pam_get_user` in the Module Writer's Guide][mwg]. /// /// # Example /// /// ```no_run - /// # use nonstick::PamHandle; - /// # fn _doc(handle: &mut impl PamHandle) -> Result<(), Box<dyn std::error::Error>> { + /// # use nonstick::PamModuleHandle; + /// # fn _doc(handle: &mut impl PamModuleHandle) -> Result<(), Box<dyn std::error::Error>> { /// // Get the username using the default prompt. /// let user = handle.get_user(None)?; /// // Get the username using a custom prompt. - /// let user = handle.get_user(Some("who ARE you even???"))?; + /// // If this were actually called right after the above, + /// // both user and user_2 would have the same value. + /// let user_2 = handle.get_user(Some("who ARE you even???"))?; /// # Ok(()) /// # } /// ``` /// /// [man]: https://www.man7.org/linux/man-pages/man3/pam_get_user.3.html /// [mwg]: https://www.chiark.greenend.org.uk/doc/libpam-doc/html/mwg-expected-by-module-item.html#mwg-pam_get_user - fn get_user(&mut self, prompt: Option<&str>) -> Result<String>; + fn get_user(&mut self, prompt: Option<&str>) -> Result<Option<&str>>; + + trait_item!( + get = user_item, + item = "PAM_USER", + "The identity of the user for whom service is being requested." + "" + "While PAM usually sets this automatically during the course of " + "a [`get_user`](Self::get_user) call, it may be changed by a module " + "over the course of the PAM transaction." + "Applications should check it after each step of the PAM process." + ); + trait_item!( + set = set_user_item, + item = "PAM_USER", + see = Self::user_item, + "Sets the identity of the logging-in user." + "" + "Usually this will be set during the course of " + "a [`get_user`](Self::get_user) call, but you may set it manually " + "or change it during the PAM process." + ); - /// Retrieves the authentication token from the user. - /// - /// This is essentially like `handle.get_item::<Item::AuthTok>()`. - /// - /// See the [`pam_get_authtok` manual page][man] - /// or [`pam_get_item` in the Module Writer's Guide][mwg]. - /// - /// # Example - /// - /// ```no_run - /// # use nonstick::PamHandle; - /// # fn _doc(handle: &mut impl PamHandle) -> Result<(), Box<dyn std::error::Error>> { - /// // Get the user's password using the default prompt. - /// let pass = handle.get_authtok(None)?; - /// // Get the user's password using a custom prompt. - /// let pass = handle.get_authtok(Some("Reveal your secrets!"))?; - /// Ok(()) - /// # } - /// ``` - /// - /// [man]: https://www.man7.org/linux/man-pages/man3/pam_get_authtok.3.html - /// [mwg]: https://www.chiark.greenend.org.uk/doc/libpam-doc/html/mwg-expected-by-module-item.html#mwg-pam_get_item - fn get_authtok(&mut self, prompt: Option<&str>) -> Result<SecureString>; + trait_item!( + get = service, + item = "PAM_SERVICE", + "The service name, which identifies the PAM stack which is used " + "to perform authentication." + ); + trait_item!( + set = set_service, + item = "PAM_SERVICE", + see = Self::service, + "The service name, which identifies the PAM stack which is used " + "to perform authentication. It's probably a bad idea to change this." + ); + + trait_item!( + get = user_prompt, + item = "PAM_USER_PROMPT", + "The string used to prompt for a user's name." + "By default, this is a localized version of `login: `." + ); + trait_item!( + set = set_user_prompt, + item = "PAM_USER_PROMPT", + see = Self::user_prompt, + "Sets the string used to prompt for a user's name." + ); - /// Retrieves an [Item] that has been set, possibly by the PAM client. - /// - /// These items are *references to PAM memory* - /// which are *owned by the PAM session* - /// and you should never modify them. - /// - /// See the [`pam_get_item` manual page][man] - /// or [`pam_get_item` in the Module Writer's Guide][mwg]. - /// - /// # Example - /// - /// ```no_run - /// # use nonstick::PamHandle; - /// use nonstick::items::Service; - /// - /// # fn _doc(pam_handle: &mut impl PamHandle) -> Result<(), Box<dyn std::error::Error>> { - /// let svc: Option<Service> = pam_handle.get_item()?; - /// match svc { - /// Some(name) => eprintln!("The calling service name is {:?}", name.to_string_lossy()), - /// None => eprintln!("Who knows what the calling service is?"), - /// } - /// # Ok(()) - /// # } - /// ``` - /// - /// [man]: https://www.man7.org/linux/man-pages/man3/pam_get_item.3.html - /// [mwg]: https://www.chiark.greenend.org.uk/doc/libpam-doc/html/mwg-expected-by-module-item.html#mwg-pam_get_item - fn get_item<T: Item>(&mut self) -> Result<Option<T>>; + trait_item!( + get = tty_name, + item = "PAM_TTY", + "\"The terminal name prefixed by /dev/ for device files.\"" + "" + "This is the terminal the user is logging in on." + "Very old applications may use this instead of `PAM_XDISPLAY`." + ); + trait_item!( + set = set_tty_name, + item = "PAM_TTY", + see = Self::tty_name, + "Sets the terminal name." + "" + "(TODO: See if libpam sets this itself or if the application does.)" + ); + + trait_item!( + get = remote_user, + item = "PAM_RUSER", + "If set, the identity of the remote user logging in." + "" + "This is only as trustworthy as the application calling PAM." + "Also see [`remote_host`](Self::remote_host)." + ); + trait_item!( + set = set_remote_user, + item = "PAM_RUSER", + "Sets the identity of the remote user logging in." + "" + "This is usually set by the application before making calls " + "into a PAM session. (TODO: check this!)" + ); - /// Sets an item in the PAM context. It can be retrieved using [`get_item`](Self::get_item). - /// - /// See the [`pam_set_item` manual page][man] - /// or [`pam_set_item` in the Module Writer's Guide][mwg]. - /// - /// [man]: https://www.man7.org/linux/man-pages/man3/pam_set_item.3.html - /// [mwg]: https://www.chiark.greenend.org.uk/doc/libpam-doc/html/mwg-expected-by-module-item.html#mwg-pam_set_item - fn set_item<T: Item>(&mut self, item: T) -> Result<()>; + trait_item!( + get = remote_host, + item = "PAM_RHOST", + "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.\"" + ); + trait_item!( + set = set_remote_host, + item = "PAM_RHOST", + see = Self::remote_host, + "Sets the location where the user is coming from." + "" + "This is usually set by the application before making calls " + "into a PAM session. (TODO: check this!)" + ); + + trait_item!( + set = set_authtok_item, + item = "PAM_AUTHTOK", + see = PamModuleHandle::authtok_item, + "Sets the user's authentication token (e.g., password)." + "" + "This is usually set automatically when " + "[`get_authtok`](PamModuleHandle::get_authtok) is called, " + "but can be manually set." + ); + + trait_item!( + set = set_old_authtok_item, + item = "PAM_OLDAUTHTOK", + see = PamModuleHandle::old_authtok_item, + "Sets the user's \"old authentication token\" when changing passwords." + "" + "This is usually set automatically by PAM." + ); } /// Functionality of a PAM handle that can be expected by a PAM application. @@ -128,6 +250,9 @@ /// /// [man]: https://www.man7.org/linux/man-pages/man3/pam_end.3.html fn close(self, status: Result<()>) -> Result<()>; + + /// Uses a new PAM conversation. + fn set_conversation(&mut self, conversation: Self::Conv) -> Result<()>; } /// Functionality of a PAM handle that can be expected by a PAM module. @@ -138,131 +263,215 @@ /// Like [`PamHandle`], this is intended to allow creating mock implementations /// of PAM for testing PAM modules. pub trait PamModuleHandle: PamHandle { - /// Gets some pointer, identified by `key`, that has been set previously - /// using [`set_data`](Self::set_data). + /// Gets a channel for communication with the user. + /// + /// The Conversation is the conduit which you use for all communication + /// with the user. + fn conversation(&mut self) -> Result<ConversationMux<'_, Self::Conv>>; + + /// Retrieves the authentication token from the user. + /// + /// This should only be used by *authentication* and *password-change* + /// PAM modules. + /// + /// See the [`pam_get_authtok` manual page][man] + /// or [`pam_get_item` in the Module Writer's Guide][mwg]. + /// + /// # Example + /// + /// ```no_run + /// # use nonstick::PamModuleHandle; + /// # fn _doc(handle: &mut impl PamModuleHandle) -> Result<(), Box<dyn std::error::Error>> { + /// // Get the user's password using the default prompt. + /// let pass = handle.get_authtok(None)?; + /// // Get the user's password using a custom prompt. + /// let pass = handle.get_authtok(Some("Reveal your secrets!"))?; + /// Ok(()) + /// # } + /// ``` /// - /// The data, if present, is still owned by the current PAM session. - /// - /// See the [`pam_get_data` manual page][man] - /// or [`pam_get_data` in the Module Writer's Guide][mwg]. + /// [man]: https://www.man7.org/linux/man-pages/man3/pam_get_authtok.3.html + /// [mwg]: https://www.chiark.greenend.org.uk/doc/libpam-doc/html/mwg-expected-by-module-item.html#mwg-pam_get_item + fn get_authtok(&mut self, prompt: Option<&str>) -> Result<Option<&str>>; + + trait_item!( + get = authtok_item, + item = "PAM_AUTHTOK", + see = Self::get_authtok, + "Gets the user's authentication token (e.g., password)." + "" + "This is normally set automatically by PAM when calling " + "[`get_authtok`](Self::get_authtok), but can be set explicitly." + "" + "Like `get_authtok`, this should only ever be called " + "by *authentication* and *password-change* PAM modules." + ); + + trait_item!( + get = old_authtok_item, + item = "PAM_OLDAUTHTOK", + see = PamHandle::set_old_authtok_item, + "Gets the user's old authentication token when changing passwords." + "" + "This should only ever be called by *password-change* PAM modules." + ); + + /* + TODO: Re-enable this at some point. + /// Gets some pointer, identified by `key`, that has been set previously + /// using [`set_data`](Self::set_data). + /// + /// The data, if present, is still owned by the current PAM session. + /// + /// See the [`pam_get_data` manual page][man] + /// or [`pam_get_data` in the Module Writer's Guide][mwg]. + /// + /// # Safety + /// + /// The data stored under the provided key must be of type `T`, + /// otherwise you'll get back a completely invalid `&T` + /// and further behavior is undefined. + /// + /// [man]: https://www.man7.org/linux/man-pages/man3/pam_get_data.3.html + /// [mwg]: https://www.chiark.greenend.org.uk/doc/libpam-doc/html/mwg-expected-by-module-item.html#mwg-pam_get_data + unsafe fn get_data<T>(&mut self, key: &str) -> Result<Option<&T>>; + + /// Stores a pointer that can be retrieved later with [`get_data`](Self::get_data). + /// + /// This data is accessible to this module and other PAM modules + /// (using the provided `key`), but is *not* accessible to the application. + /// The PAM session takes ownership of the data, and it will be dropped + /// when the session ends. + /// + /// See the [`pam_set_data` manual page][man] + /// or [`pam_set_data` in the Module Writer's Guide][mwg]. + /// + /// [man]: https://www.man7.org/linux/man-pages/man3/pam_set_data.3.html + /// [mwg]: https://www.chiark.greenend.org.uk/doc/libpam-doc/html/mwg-expected-by-module-item.html#mwg-pam_set_data + fn set_data<T>(&mut self, key: &str, data: Box<T>) -> Result<()>; + */ +} + + +impl LibPamHandle { + /// Gets a C string item. /// /// # Safety /// - /// The data stored under the provided key must be of type `T`, - /// otherwise you'll get back a completely invalid `&T` - /// and further behavior is undefined. + /// You better be requesting an item which is a C string. + unsafe fn get_cstr_item(&mut self, item_type: ItemType) -> Result<Option<&str>> { + let mut output = ptr::null(); + let ret = unsafe { pam_ffi::pam_get_item(self, item_type as c_int, &mut output) }; + ErrorCode::result_from(ret)?; + memory::wrap_string(output.cast()) + } + + /// Sets a C string item. /// - /// [man]: https://www.man7.org/linux/man-pages/man3/pam_get_data.3.html - /// [mwg]: https://www.chiark.greenend.org.uk/doc/libpam-doc/html/mwg-expected-by-module-item.html#mwg-pam_get_data - unsafe fn get_data<T>(&mut self, key: &str) -> Result<Option<&T>>; - - /// Stores a pointer that can be retrieved later with [`get_data`](Self::get_data). - /// - /// This data is accessible to this module and other PAM modules - /// (using the provided `key`), but is *not* accessible to the application. - /// The PAM session takes ownership of the data, and it will be dropped - /// when the session ends. + /// # Safety /// - /// See the [`pam_set_data` manual page][man] - /// or [`pam_set_data` in the Module Writer's Guide][mwg]. - /// - /// [man]: https://www.man7.org/linux/man-pages/man3/pam_set_data.3.html - /// [mwg]: https://www.chiark.greenend.org.uk/doc/libpam-doc/html/mwg-expected-by-module-item.html#mwg-pam_set_data - fn set_data<T>(&mut self, key: &str, data: Box<T>) -> Result<()>; + /// You better be setting an item which is a C string. + unsafe fn set_cstr_item(&mut self, item_type: ItemType, data: Option<&str>) -> Result<()> { + let data_str = memory::option_cstr(data)?; + let ret = unsafe { + pam_ffi::pam_set_item( + self, + item_type as c_int, + memory::prompt_ptr(data_str.as_ref()).cast(), + ) + }; + ErrorCode::result_from(ret) + } } -/// A [`PamHandle`] backed by `libpam`, i.e., a real PAM handle. -/// -/// This structure wraps an opaque PAM handle and gives you a nice Rusty -/// interface to use PAM. -#[repr(C)] -pub struct LibPamHandle(pam_ffi::Handle); - impl Drop for LibPamHandle { /// Ends the PAM session with a zero error code. /// You probably want to call [`close`](Self::close) instead of /// letting this drop by itself. fn drop(&mut self) { unsafe { - pam_ffi::pam_end(&mut self.0, 0); + pam_ffi::pam_end(self, 0); } } } +macro_rules! cstr_item { + (get = $getter:ident, item = $item_type:path) => { + fn $getter(&mut self) -> Result<Option<&str>> { + unsafe { self.get_cstr_item($item_type) } + } + }; + (set = $setter:ident, item = $item_type:path) => { + fn $setter(&mut self, value: Option<&str>) -> Result<()> { + unsafe { self.set_cstr_item($item_type, value) } + } + }; +} + impl PamHandle for LibPamHandle { - fn get_user(&mut self, prompt: Option<&str>) -> crate::Result<String> { + type Conv = LibPamConversation; + fn get_user(&mut self, prompt: Option<&str>) -> Result<Option<&str>> { let prompt = memory::option_cstr(prompt)?; - let mut output: *const c_char = ptr::null_mut(); + let mut output: *const c_char = ptr::null(); let ret = unsafe { - pam_ffi::pam_get_user(&self.0, &mut output, memory::prompt_ptr(prompt.as_ref())) + pam_ffi::pam_get_user(self, &mut output, memory::prompt_ptr(prompt.as_ref())) }; ErrorCode::result_from(ret)?; - unsafe {memory::copy_pam_string(output)} + unsafe { memory::wrap_string(output) } } - fn get_authtok(&mut self, prompt: Option<&str>) -> crate::Result<SecureString> { + 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); +} + +impl PamApplicationHandle for LibPamHandle { + fn close(mut self, status: Result<()>) -> Result<()> { + let result = unsafe { pam_ffi::pam_end(&mut self, ErrorCode::result_to_c(status)) }; + // Since we've already `pam_end`ed this session, we don't want it to be + // double-freed on drop. + mem::forget(self); + ErrorCode::result_from(result) + } + + fn set_conversation(&mut self, conversation: Self::Conv) -> Result<()> { + todo!() + } +} + +impl PamModuleHandle for LibPamHandle { + fn conversation(&mut self) -> Result<ConversationMux<'_, Self::Conv>> { + todo!() + } + + fn get_authtok(&mut self, prompt: Option<&str>) -> Result<Option<&str>> { let prompt = memory::option_cstr(prompt)?; let mut output: *const c_char = ptr::null_mut(); let res = unsafe { pam_ffi::pam_get_authtok( - &self.0, + self, ItemType::AuthTok.into(), &mut output, memory::prompt_ptr(prompt.as_ref()), ) }; ErrorCode::result_from(res)?; - unsafe {memory::copy_pam_string(output)}.map(SecureString::from) - } - - fn get_item<T: Item>(&mut self) -> Result<Option<T>> { - let mut ptr: *const c_void = ptr::null(); - let out = unsafe { - let ret = pam_ffi::pam_get_item(&self.0, T::type_id().into(), &mut ptr); - ErrorCode::result_from(ret)?; - (ptr as *const T::Raw).as_ref().map(|p| T::from_raw(p)) - }; - Ok(out) + unsafe { memory::wrap_string(output) } } - fn set_item<T: Item>(&mut self, item: T) -> Result<()> { - let ret = unsafe { - pam_ffi::pam_set_item(&mut self.0, T::type_id().into(), item.into_raw().cast()) - }; - ErrorCode::result_from(ret) - } -} - -impl PamApplicationHandle for LibPamHandle { - fn close(mut self, status: Result<()>) -> Result<()> { - let result = unsafe { pam_ffi::pam_end(&mut self.0, ErrorCode::result_to_c(status)) }; - // Since we've already `pam_end`ed this session, we don't want it to be - // double-freed on drop. - mem::forget(self); - ErrorCode::result_from(result) - } -} - -impl PamModuleHandle for LibPamHandle { - unsafe fn get_data<T>(&mut self, key: &str) -> crate::Result<Option<&T>> { - let c_key = CString::new(key).map_err(|_| ErrorCode::ConversationError)?; - let mut ptr: *const c_void = ptr::null(); - ErrorCode::result_from(pam_ffi::pam_get_data(&self.0, c_key.as_ptr(), &mut ptr))?; - Ok((ptr as *const T).as_ref()) - } - - fn set_data<T>(&mut self, key: &str, data: Box<T>) -> crate::Result<()> { - let c_key = CString::new(key).map_err(|_| ErrorCode::ConversationError)?; - let ret = unsafe { - pam_ffi::pam_set_data( - &mut self.0, - c_key.as_ptr(), - Box::into_raw(data).cast(), - set_data_cleanup::<T>, - ) - }; - ErrorCode::result_from(ret) - } + cstr_item!(get = authtok_item, item = ItemType::AuthTok); + cstr_item!(get = old_authtok_item, item = ItemType::OldAuthTok); } /// Function called at the end of a PAM session that is called to clean up