pam_client/
context.rs

1//! PAM context and related helpers
2
3/***********************************************************************
4 * (c) 2021-2022 Christoph Grenz <christophg+gitorious @ grenz-bonn.de>*
5 *                                                                     *
6 * This Source Code Form is subject to the terms of the Mozilla Public *
7 * License, v. 2.0. If a copy of the MPL was not distributed with this *
8 * file, You can obtain one at http://mozilla.org/MPL/2.0/.            *
9 ***********************************************************************/
10
11use crate::env_list::EnvList;
12use crate::error::{Error, ErrorCode};
13use crate::ffi::to_pam_conv;
14use crate::session::{Session, SessionToken};
15use crate::{char_ptr_to_str, ConversationHandler};
16extern crate libc;
17extern crate pam_sys;
18
19use crate::{ExtResult, Flag, Result, PAM_SUCCESS};
20
21use libc::{c_char, c_int, c_void};
22use pam_sys::pam_handle_t as RawPamHandle;
23use pam_sys::{
24	pam_acct_mgmt, pam_authenticate, pam_chauthtok, pam_close_session, pam_end, pam_get_item,
25	pam_getenv, pam_getenvlist, pam_open_session, pam_putenv, pam_set_item, pam_setcred, pam_start,
26};
27use std::cell::Cell;
28use std::ffi::{CStr, CString, OsStr};
29use std::mem::take;
30use std::os::unix::ffi::OsStrExt;
31use std::ptr::NonNull;
32use std::{ptr, slice};
33
34/// Internal: Builds getters/setters for string-typed PAM items.
35macro_rules! impl_pam_str_item {
36	($name:ident, $set_name:ident, $item_type:expr$(, $doc:literal$(, $extdoc:literal)?)?$(,)?) => {
37		$(#[doc = "Returns "]#[doc = $doc]$(#[doc = "\n\n"]#[doc = $extdoc])?)?
38		pub fn $name(&self) -> Result<String> {
39			let ptr = self.get_item($item_type as c_int)?;
40			if ptr.is_null() {
41				return Err(Error::new(self.handle(), ErrorCode::PERM_DENIED));
42			}
43			let string = unsafe { CStr::from_ptr(ptr.cast()) }.to_string_lossy().into_owned();
44			return Ok(string);
45		}
46
47		$(#[doc = "Sets "]#[doc = $doc])?
48		pub fn $set_name(&mut self, value: Option<&str>) -> Result<()> {
49			match value {
50				None => unsafe { self.set_item($item_type as c_int, ptr::null()) },
51				Some(string) => {
52					let cstring = CString::new(string).map_err(|_| Error::new(self.handle(), ErrorCode::BUF_ERR))?;
53					unsafe { self.set_item($item_type as c_int, cstring.as_ptr().cast()) }
54				}
55			}
56		}
57	}
58}
59
60#[derive(Debug, Clone, Copy)]
61pub(crate) struct PamHandle(NonNull<RawPamHandle>);
62
63/// PAM handle wrapper
64impl PamHandle {
65	/// Create a PAM handle from a raw handle pointer.
66	///
67	/// # Safety
68	/// The argument `ptr` must be NULL or a valid non-dangling PAM handle created by `pam_start`.
69	#[inline]
70	pub unsafe fn new(ptr: *mut RawPamHandle) -> Option<Self> {
71		NonNull::new(ptr).map(Self)
72	}
73
74	#[inline]
75	pub const fn as_ptr(self) -> *mut RawPamHandle {
76		self.0.as_ptr()
77	}
78}
79
80impl From<PamHandle> for *mut RawPamHandle {
81	#[inline]
82	fn from(handle: PamHandle) -> Self {
83		handle.as_ptr()
84	}
85}
86
87impl From<PamHandle> for *const RawPamHandle {
88	#[inline]
89	fn from(handle: PamHandle) -> Self {
90		handle.as_ptr()
91	}
92}
93
94/// Special struct for the `PAM_XAUTHDATA` pam item
95///
96/// Uses const pointers as `pam_set_item` makes a copy of the data and
97/// never mutates through the pointers and `pam_get_item` by API contract
98/// states that returned data should not be modified.
99#[cfg(any(target_os = "linux", doc))]
100#[repr(C)]
101#[derive(Debug)]
102struct XAuthData {
103	/// Length of `name` in bytes excluding the trailing NUL
104	pub namelen: c_int,
105	/// Name of the authentication method as a null terminated string
106	pub name: *const c_char,
107	/// Length of `data` in bytes
108	pub datalen: c_int,
109	/// Authentication method-specific data
110	pub data: *const c_char,
111}
112
113/// Main struct for PAM interaction
114///
115/// Manages a PAM context holding the transaction state.
116///
117/// See the [crate documentation][`crate`] for examples.
118pub struct Context<ConvT>
119where
120	ConvT: ConversationHandler,
121{
122	handle: Option<PamHandle>,
123	// Needs to be boxed, as we give a long-living pointer to it to C code.
124	conversation: Box<ConvT>,
125	last_status: Cell<c_int>,
126}
127
128impl<ConvT> Context<ConvT>
129where
130	ConvT: ConversationHandler,
131{
132	/// Creates a PAM context and starts a PAM transaction.
133	///
134	/// # Parameters
135	/// - `service` – Name of the service. The policy for the service will be
136	///   read from the file /etc/pam.d/*service_name*, falling back to
137	///   /etc/pam.conf.
138	/// - `username` – Name of the target user. If `None`, the user will be
139	///   asked through the conversation handler if neccessary.
140	/// - `conversation` – A conversation handler through which the user can be
141	///   asked for his username, password, etc. Use
142	///   [`conv_cli::Conversation`][`crate::conv_cli::Conversation`] for a command line
143	///   default implementation, [`conv_mock::Conversation`][`crate::conv_mock::Conversation`]
144	///   for fixed credentials or implement the [`ConversationHandler`] trait
145	///   for custom behaviour.
146	///
147	/// # Errors
148	/// Expected error codes include:
149	/// - `ErrorCode::ABORT` – General failure
150	/// - `ErrorCode::BUF_ERR` – Memory allocation failure or null byte in
151	///   parameter.
152	/// - `ErrorCode::SYSTEM_ERR` – Other system error
153	#[rustversion::attr(since(1.48), doc(alias = "pam_start"))]
154	pub fn new(service: &str, username: Option<&str>, conversation: ConvT) -> Result<Self> {
155		// Wrap `conversation` in a box and delegate to `from_boxed_conv`
156		Self::from_boxed_conv(service, username, Box::new(conversation))
157	}
158
159	/// Creates a PAM context and starts a PAM transaction taking a boxed
160	/// conversation handler.
161	///
162	/// See [`new()`][`Self::new()`] for details.
163	pub fn from_boxed_conv(
164		service: &str,
165		username: Option<&str>,
166		mut boxed_conv: Box<ConvT>,
167	) -> Result<Self> {
168		let mut handle: *mut RawPamHandle = ptr::null_mut();
169
170		let c_service = CString::new(service).map_err(|_| Error::from(ErrorCode::BUF_ERR))?;
171		let c_username = match username {
172			None => None,
173			Some(name) => Some(CString::new(name).map_err(|_| Error::from(ErrorCode::BUF_ERR))?),
174		};
175
176		// Create callback struct for C code
177		let pam_conv = to_pam_conv(&mut boxed_conv);
178
179		// Start the PAM context
180		match unsafe {
181			pam_start(
182				c_service.as_ptr(),
183				c_username.as_ref().map_or(ptr::null(), |s| s.as_ptr()),
184				&pam_conv,
185				&mut handle,
186			)
187		} {
188			PAM_SUCCESS => {
189				// A null pointer should never happen on PAM_SUCCESS, but we need to check to make sure.
190				// Safety: The handle came from `pam_start` so it must be valid.
191				let handle = unsafe { PamHandle::new(handle) }
192					.ok_or_else(|| Error::from(ErrorCode::ABORT))?;
193				boxed_conv.init(username);
194				Ok(Self {
195					handle: Some(handle),
196					conversation: boxed_conv,
197					last_status: Cell::new(PAM_SUCCESS),
198				})
199			}
200			code => Err(ErrorCode::from_repr(code)
201				.unwrap_or(ErrorCode::ABORT)
202				.into()),
203		}
204	}
205
206	/// Internal: Gets the PAM handle.
207	///
208	/// # Panics
209	/// Panics if there is no handle (e.g. the handle was removed with [`Option::take`]).
210	#[inline]
211	pub(crate) fn handle(&self) -> PamHandle {
212		self.handle.unwrap()
213	}
214
215	/// Internal: Wraps a `ErrorCode` into a `Result` and sets `last_status`.
216	#[inline]
217	pub(crate) fn wrap_pam_return(&self, status: c_int) -> Result<()> {
218		self.last_status.set(status);
219		match status {
220			PAM_SUCCESS => Ok(()),
221			code => Err(Error::new(
222				self.handle(),
223				ErrorCode::from_repr(code).unwrap_or(ErrorCode::ABORT),
224			)),
225		}
226	}
227
228	/// Returns a reference to the conversation handler.
229	pub fn conversation(&self) -> &ConvT {
230		&self.conversation
231	}
232
233	/// Returns a mutable reference to the conversation handler.
234	pub fn conversation_mut(&mut self) -> &mut ConvT {
235		&mut self.conversation
236	}
237
238	/// Returns raw PAM information.
239	///
240	/// If possible, use the convenience wrappers [`service()`][`Self::service()`],
241	/// [`user()`][`Self::user()`], … instead.
242	///
243	/// # Errors
244	/// Expected error codes include:
245	/// - `BAD_ITEM` – Unsupported, undefined or inaccessible item
246	/// - `BUF_ERR` – Memory buffer error
247	/// - `PERM_DENIED` – The value was NULL/None
248	#[rustversion::attr(since(1.48), doc(alias = "pam_get_item"))]
249	pub fn get_item(&self, item_type: c_int) -> Result<*const c_void> {
250		let mut result: *const c_void = ptr::null();
251		self.wrap_pam_return(unsafe {
252			pam_get_item(self.handle().into(), item_type, &mut result)
253		})?;
254		Ok(result)
255	}
256
257	/// Updates raw PAM information.
258	///
259	/// If possible, use the convenience wrappers
260	/// [`set_service()`][`Self::set_service()`],
261	/// [`set_user()`][`Self::set_user()`], … instead.
262	///
263	/// # Errors
264	/// Expected error codes include:
265	/// - `BAD_ITEM` – Unsupported, undefined or inaccessible item
266	/// - `BUF_ERR` – Memory buffer error
267	///
268	/// # Safety
269	/// This method is unsafe. You must guarantee that the data pointed to by
270	/// `value` matches the type the PAM library expects. E.g. a null terminated
271	/// `*const c_char` for `PAM_SERVICE` or `*const PamXAuthData` for
272	/// `PAM_XAUTHDATA`.
273	#[rustversion::attr(since(1.48), doc(alias = "pam_set_item"))]
274	pub unsafe fn set_item(&mut self, item_type: c_int, value: *const c_void) -> Result<()> {
275		self.wrap_pam_return(pam_set_item(self.handle().into(), item_type, &*value))
276	}
277
278	impl_pam_str_item!(
279		service,
280		set_service,
281		pam_sys::PAM_SERVICE,
282		"the service name"
283	);
284	impl_pam_str_item!(user, set_user, pam_sys::PAM_USER, "the username of the entity under whose identity service will be given",
285		"This value can be mapped by any module in the PAM stack, so don't assume it stays unchanged after calling other methods on `Self`.");
286	impl_pam_str_item!(
287		user_prompt,
288		set_user_prompt,
289		pam_sys::PAM_USER_PROMPT,
290		"the string used when prompting for a user's name"
291	);
292	impl_pam_str_item!(tty, set_tty, pam_sys::PAM_TTY, "the terminal name");
293	impl_pam_str_item!(
294		ruser,
295		set_ruser,
296		pam_sys::PAM_RUSER,
297		"the requesting user name"
298	);
299	impl_pam_str_item!(
300		rhost,
301		set_rhost,
302		pam_sys::PAM_RHOST,
303		"the requesting hostname"
304	);
305	#[cfg(any(target_os = "linux", doc))]
306	impl_pam_str_item!(
307		authtok_type,
308		set_authtok_type,
309		pam_sys::PAM_AUTHTOK_TYPE,
310		"the default password type in the prompt (Linux specific)",
311		"E.g. \"UNIX\" for \"Enter UNIX password:\""
312	);
313	#[cfg(any(target_os = "linux", doc))]
314	impl_pam_str_item!(
315		xdisplay,
316		set_xdisplay,
317		pam_sys::PAM_XDISPLAY,
318		"the name of the X display (Linux specific)"
319	);
320
321	/// Returns X authentication data as (name, value) pair (Linux specific).
322	#[cfg(any(target_os = "linux", doc))]
323	pub fn xauthdata(&self) -> Result<(&CStr, &[u8])> {
324		let handle = self.handle();
325		let ptr = self
326			.get_item(pam_sys::PAM_XAUTHDATA as c_int)?
327			.cast::<XAuthData>();
328		if ptr.is_null() {
329			return Err(Error::new(handle, ErrorCode::PERM_DENIED));
330		}
331		let data = unsafe { &*ptr };
332
333		// Safety checks: validate the length are non-negative and that
334		// the pointers are non-null
335		if data.namelen < 0 || data.datalen < 0 || data.name.is_null() || data.data.is_null() {
336			return Err(Error::new(handle, ErrorCode::BUF_ERR));
337		}
338
339		#[allow(clippy::cast_sign_loss)]
340		Ok((
341			CStr::from_bytes_with_nul(unsafe {
342				slice::from_raw_parts(data.name.cast(), data.namelen as usize + 1)
343			})
344			.map_err(|_| Error::new(handle, ErrorCode::BUF_ERR))?,
345			unsafe { slice::from_raw_parts(data.data.cast(), data.datalen as usize) },
346		))
347	}
348
349	/// Sets X authentication data (Linux specific).
350	///
351	/// # Errors
352	/// Expected error codes include:
353	/// - `BAD_ITEM` – Unsupported item
354	/// - `BUF_ERR` – Memory buffer error
355	#[cfg(any(target_os = "linux", doc))]
356	pub fn set_xauthdata(&mut self, value: Option<(&CStr, &[u8])>) -> Result<()> {
357		match value {
358			None => unsafe { self.set_item(pam_sys::PAM_XAUTHDATA as c_int, ptr::null()) },
359			Some((name, data)) => {
360				let name_bytes = name.to_bytes_with_nul();
361
362				if name_bytes.len() > i32::MAX as usize || data.len() > i32::MAX as usize {
363					return Err(Error::new(self.handle(), ErrorCode::BUF_ERR));
364				}
365
366				#[allow(clippy::cast_possible_truncation, clippy::cast_possible_wrap)]
367				let xauthdata = XAuthData {
368					namelen: name_bytes.len() as i32 - 1,
369					name: name_bytes.as_ptr().cast(),
370					datalen: data.len() as i32,
371					data: data.as_ptr().cast(),
372				};
373				unsafe {
374					self.set_item(
375						pam_sys::PAM_XAUTHDATA as c_int,
376						&xauthdata as *const _ as *const c_void,
377					)
378				}
379			}
380		}
381	}
382
383	/// Returns the value of a PAM environment variable.
384	///
385	/// Searches the environment list in this PAM context for an
386	/// item with key `name` and returns the value, if it exists.
387	#[must_use]
388	#[rustversion::attr(since(1.48), doc(alias = "pam_getenv"))]
389	pub fn getenv(&self, name: impl AsRef<OsStr>) -> Option<&str> {
390		let c_name = match CString::new(name.as_ref().as_bytes()) {
391			Err(_) => return None,
392			Ok(s) => s,
393		};
394		char_ptr_to_str(unsafe { pam_getenv(self.handle().into(), c_name.as_ptr()) })
395	}
396
397	/// Sets or unsets a PAM environment variable.
398	///
399	/// Modifies the environment list in this PAM context. The `name_value`
400	/// argument can take one of the following forms:
401	/// - *NAME*=*value* – Set a variable to a given value. If it was already
402	///   set it is overwritten.
403	/// - *NAME*= – Set a variable to the empty value. If it was already set
404	///   it is overwritten.
405	/// - *NAME* – Delete a variable, if it exists.
406	#[rustversion::attr(since(1.48), doc(alias = "pam_putenv"))]
407	pub fn putenv(&mut self, name_value: impl AsRef<OsStr>) -> Result<()> {
408		let c_name_value = CString::new(name_value.as_ref().as_bytes())
409			.map_err(|_| Error::from(ErrorCode::BUF_ERR))?;
410		self.wrap_pam_return(unsafe { pam_putenv(self.handle().into(), c_name_value.as_ptr()) })
411	}
412
413	/// Returns a copy of the PAM environment in this context.
414	///
415	/// The contained variables represent the contents of the regular
416	/// environment variables of the authenticated user when service is
417	/// granted.
418	///
419	/// The returned [`EnvList`] type is designed to ease handing the
420	/// environment to [`std::process::Command::envs()`] and
421	/// `nix::unistd::execve()`.
422	#[must_use]
423	#[rustversion::attr(since(1.48), doc(alias = "pam_getenvlist"))]
424	pub fn envlist(&self) -> EnvList {
425		unsafe { EnvList::new(pam_getenvlist(self.handle().into()).cast()) }
426	}
427
428	/// Authenticates a user.
429	///
430	/// The conversation handler may be called to ask the user for their name
431	/// (especially if no initial username was provided), their password and
432	/// possibly other tokens if e.g. two-factor authentication is required.
433	/// Conversely the conversation handler may not be called if authentication
434	/// is handled by other means, e.g. a fingerprint scanner.
435	///
436	/// Relevant `flags` are [`Flag::NONE`], [`Flag::SILENT`] and
437	/// [`Flag::DISALLOW_NULL_AUTHTOK`] (don't authenticate empty
438	/// passwords).
439	///
440	/// # Errors
441	/// Expected error codes include:
442	/// - `ABORT` – Serious failure; the application should exit.
443	/// - `AUTH_ERR` – The user was not authenticated.
444	/// - `CRED_INSUFFICIENT` – The application does not have sufficient
445	///   credentials to authenticate the user.
446	/// - `AUTHINFO_UNAVAIL` – Could not retrieve authentication information
447	///   due to e.g. network failure.
448	/// - `MAXTRIES` – At least one module reached its retry limit. Do not
449	/// - try again.
450	/// - `USER_UNKNOWN` – User not known.
451	/// - `INCOMPLETE` – The conversation handler returned `CONV_AGAIN`. Call
452	///   again after the asynchronous conversation finished.
453	#[rustversion::attr(since(1.48), doc(alias = "pam_authenticate"))]
454	pub fn authenticate(&mut self, flags: Flag) -> Result<()> {
455		self.wrap_pam_return(unsafe { pam_authenticate(self.handle().into(), flags.bits()) })
456	}
457
458	/// Validates user account authorization.
459	///
460	/// Determines if the account is valid, not expired, and verifies other
461	/// access restrictions. Usually used directly after authentication.
462	/// The conversation handler may be called by some PAM module.
463	///
464	/// Relevant `flags` are [`Flag::NONE`], [`Flag::SILENT`] and
465	/// [`Flag::DISALLOW_NULL_AUTHTOK`] (demand password change on empty
466	/// passwords).
467	///
468	/// # Errors
469	/// Expected error codes include:
470	/// - `ACCT_EXPIRED` – Account has expired.
471	/// - `AUTH_ERR` – Authentication failure.
472	/// - `NEW_AUTHTOK_REQD` – Password has expired. Use [`chauthtok()`] to let
473	///   the user change their password or abort.
474	/// - `PERM_DENIED` – Permission denied
475	/// - `USER_UNKNOWN` – User not known
476	/// - `INCOMPLETE` – The conversation handler returned `CONV_AGAIN`. Call
477	///   again after the asynchronous conversation finished.
478	///
479	/// [`chauthtok()`]: `Self::chauthtok`
480	#[rustversion::attr(since(1.48), doc(alias = "pam_acct_mgmt"))]
481	pub fn acct_mgmt(&mut self, flags: Flag) -> Result<()> {
482		self.wrap_pam_return(unsafe { pam_acct_mgmt(self.handle().into(), flags.bits()) })
483	}
484
485	/// Fully reinitializes the user's credentials (if established).
486	///
487	/// Reinitializes credentials like Kerberos tokens for when a session
488	/// is already managed by another process. This is e.g. used in
489	/// lockscreen applications to refresh the credentials of the desktop
490	/// session.
491	///
492	/// Relevant `flags` are [`Flag::NONE`] and [`Flag::SILENT`].
493	///
494	/// # Errors
495	/// Expected error codes include:
496	/// - `BUF_ERR` – Memory allocation error
497	/// - `CRED_ERR` – Setting credentials failed
498	/// - `CRED_UNAVAIL` – Failed to retrieve credentials
499	/// - `SYSTEM_ERR` – Other system error
500	/// - `USER_UNKNOWN` – User not known
501	pub fn reinitialize_credentials(&mut self, flags: Flag) -> Result<()> {
502		self.wrap_pam_return(unsafe {
503			pam_setcred(
504				self.handle().into(),
505				(Flag::REINITIALIZE_CRED | flags).bits(),
506			)
507		})
508	}
509
510	/// Changes a users password.
511	///
512	/// The conversation handler will be used to request the new password
513	/// and might query for the old one.
514	///
515	/// Relevant `flags` are [`Flag::NONE`], [`Flag::SILENT`] and
516	/// [`Flag::CHANGE_EXPIRED_AUTHTOK`] (only initiate change for
517	/// expired passwords).
518	///
519	/// # Errors
520	/// Expected error codes include:
521	/// - `AUTHTOK_ERR` – Unable to obtain the new password
522	/// - `AUTHTOK_RECOVERY_ERR` – Unable to obtain the old password
523	/// - `AUTHTOK_LOCK_BUSY` – Authentication token is currently locked
524	/// - `AUTHTOK_DISABLE_AGING` – Password aging is disabled (password may
525	///   be unchangeable in at least one module)
526	/// - `PERM_DENIED` – Permission denied
527	/// - `TRY_AGAIN` – Not all modules were able to prepare an authentication
528	///   token update. Nothing was changed.
529	/// - `USER_UNKNOWN` – User not known
530	/// - `INCOMPLETE` – The conversation handler returned `CONV_AGAIN`. Call
531	///   again after the asynchronous conversation finished.
532	#[rustversion::attr(since(1.48), doc(alias = "pam_chauthtok"))]
533	pub fn chauthtok(&mut self, flags: Flag) -> Result<()> {
534		self.wrap_pam_return(unsafe { pam_chauthtok(self.handle().into(), flags.bits()) })
535	}
536
537	/// Sets up a user session.
538	///
539	/// Establishes user credentials and performs various tasks to prepare
540	/// a login session, may create the home directory on first login, mount
541	/// user-specific directories, log access times, etc. The application
542	/// must usually have sufficient privileges to perform this task (e.g.
543	/// have EUID 0). The returned [`Session`] object closes the session and
544	/// deletes the established credentials on drop.
545	///
546	/// The user should already be [authenticated] and [authorized] at this
547	/// point, but this isn't enforced or strictly neccessary if the user
548	/// was authenticated by other means. In that case the conversation
549	/// handler might be called by e.g. crypt-mount modules to get a password.
550	///
551	/// Relevant `flags` are [`Flag::NONE`] and [`Flag::SILENT`].
552	///
553	/// # Errors
554	/// Expected error codes include:
555	/// - `ABORT` – Serious failure; the application should exit
556	/// - `BUF_ERR` – Memory allocation error
557	/// - `SESSION_ERR` – Some session initialization failed
558	/// - `CRED_ERR` – Setting credentials failed
559	/// - `CRED_UNAVAIL` – Failed to retrieve credentials
560	/// - `SYSTEM_ERR` – Other system error
561	/// - `USER_UNKNOWN` – User not known
562	/// - `INCOMPLETE` – The conversation handler returned `CONV_AGAIN`. Call
563	///   again after the asynchronous conversation finished.
564	///
565	/// [authenticated]: `Self::authenticate()`
566	/// [authorized]: `Self::acct_mgmt()`
567	#[rustversion::attr(since(1.48), doc(alias = "pam_open_session"))]
568	pub fn open_session(&mut self, flags: Flag) -> Result<Session<ConvT>> {
569		let handle = self.handle().as_ptr();
570		self.wrap_pam_return(unsafe {
571			pam_setcred(handle, (Flag::ESTABLISH_CRED | flags).bits())
572		})?;
573
574		if let Err(e) = self.wrap_pam_return(unsafe { pam_open_session(handle, flags.bits()) }) {
575			let _ = self.wrap_pam_return(unsafe {
576				pam_setcred(handle, (Flag::DELETE_CRED | flags).bits())
577			});
578			return Err(e);
579		}
580
581		// Reinitialize credentials after session opening. With this we try
582		// to circumvent different assumptions of PAM modules about when
583		// `setcred` is called, as the documentations of different PAM
584		// implementations differ. (OpenSSH does something similar too).
585		if let Err(e) = self.wrap_pam_return(unsafe {
586			pam_setcred(handle, (Flag::REINITIALIZE_CRED | flags).bits())
587		}) {
588			let _ = self.wrap_pam_return(unsafe { pam_close_session(handle, flags.bits()) });
589			let _ = self.wrap_pam_return(unsafe {
590				pam_setcred(handle, (Flag::DELETE_CRED | flags).bits())
591			});
592			return Err(e);
593		}
594
595		Ok(Session::new(self, true))
596	}
597
598	/// Maintains user credentials but don't set up a full user session.
599	///
600	/// Establishes user credentials and returns a [`Session`] object that
601	/// deletes the credentials on drop. It doesn't open a PAM session.
602	///
603	/// The user should already be [authenticated] and [authorized] at this
604	/// point, but this isn't enforced or strictly neccessary.
605	///
606	/// Depending on the platform this use case may not be fully supported.
607	///
608	/// Relevant `flags` are [`Flag::NONE`] and [`Flag::SILENT`].
609	///
610	/// # Errors
611	/// Expected error codes include:
612	/// - `BUF_ERR` – Memory allocation error
613	/// - `CRED_ERR` – Setting credentials failed
614	/// - `CRED_UNAVAIL` – Failed to retrieve credentials
615	/// - `SYSTEM_ERR` – Other system error
616	/// - `USER_UNKNOWN` – User not known
617	///
618	/// [authenticated]: Self::authenticate()
619	/// [authorized]: Self::acct_mgmt()
620	pub fn open_pseudo_session(&mut self, flags: Flag) -> Result<Session<ConvT>> {
621		self.wrap_pam_return(unsafe {
622			pam_setcred(self.handle().into(), (Flag::ESTABLISH_CRED | flags).bits())
623		})?;
624
625		Ok(Session::new(self, false))
626	}
627
628	/// Resume a session from a [`SessionToken`].
629	pub fn unleak_session(&mut self, token: SessionToken) -> Session<ConvT> {
630		Session::new(self, matches!(token, SessionToken::FullSession))
631	}
632}
633
634impl<ConvT> Context<ConvT>
635where
636	ConvT: ConversationHandler + Default,
637{
638	/// Swap the conversation handler.
639	///
640	/// Consumes the context, returns the new context and the old conversation
641	/// handler.
642	///
643	/// # Errors
644	/// Expected error codes include:
645	/// - `BAD_ITEM` – Swapping the conversation handler is unsupported
646	/// - `BUF_ERR` – Memory buffer error
647	///
648	/// The error payload contains the old context and the new handler.
649	pub fn replace_conversation<T: ConversationHandler>(
650		self,
651		new_handler: T,
652	) -> ExtResult<(Context<T>, ConvT), (Self, T)> {
653		match self.replace_conversation_boxed(new_handler.into()) {
654			Ok((context, boxed_old_conv)) => Ok((context, *boxed_old_conv)),
655			Err(error) => Err(error.map(|(ctx, b_conv)| (ctx, *b_conv))),
656		}
657	}
658
659	/// Swap the conversation handler (boxed variant).
660	///
661	/// See [`replace_conversation()`][`Self::replace_conversation()`].
662	pub fn replace_conversation_boxed<T: ConversationHandler>(
663		mut self,
664		mut new_handler: Box<T>,
665	) -> ExtResult<(Context<T>, Box<ConvT>), (Self, Box<T>)> {
666		// Get current username for handler initialization
667		let username = match self.user() {
668			Ok(u) => Some(u),
669			Err(e) => {
670				if e.code() != ErrorCode::PERM_DENIED {
671					return Err(e.into_with_payload((self, new_handler)));
672				}
673				None
674			}
675		};
676		// Create callback struct for C code
677		let pam_conv = to_pam_conv(&mut new_handler);
678		if let Err(e) = unsafe {
679			self.set_item(
680				pam_sys::PAM_CONV as c_int,
681				&pam_conv as *const _ as *const _,
682			)
683		} {
684			Err(e.into_with_payload((self, new_handler)))
685		} else {
686			// Initialize handler
687			new_handler.init(username);
688			// Create new context and return it
689			Ok((
690				Context::<T> {
691					handle: self.handle.take(),
692					conversation: new_handler,
693					last_status: Cell::new(self.last_status.replace(PAM_SUCCESS)),
694				},
695				take(&mut self.conversation),
696			))
697		}
698	}
699}
700
701/// Destructor ending the PAM transaction and releasing the PAM context
702impl<ConvT> Drop for Context<ConvT>
703where
704	ConvT: ConversationHandler,
705{
706	#[rustversion::attr(since(1.48), doc(alias = "pam_end"))]
707	fn drop(&mut self) {
708		if let Some(handle) = self.handle {
709			unsafe { pam_end(handle.into(), self.last_status.get()) };
710		}
711	}
712}
713
714// `Send` should be possible, as long as `ConvT` is `Send` too, as all memory
715// access is bound to an unique instance of `Context` (no copy/clone) and we
716// keep interior mutability bound to having a reference to the instance.
717unsafe impl<ConvT> Send for Context<ConvT> where ConvT: ConversationHandler + Send {}
718
719#[cfg(test)]
720mod tests {
721	use super::*;
722	use std::ffi::{OsStr, OsString};
723
724	#[test]
725	fn test_basic() {
726		let mut context =
727			Context::new("test", Some("user"), crate::conv_null::Conversation::new()).unwrap();
728		// Check if user name and service name are correctly saved
729		assert_eq!(context.service().unwrap(), "test");
730		assert_eq!(context.user().unwrap(), "user");
731		// Check basic properties of PamHandle
732		let h = context.handle();
733		assert_eq!(&h.clone().0, &h.0);
734		assert!(format!("{:?}", h).contains(&format!("{:?}", h.as_ptr())));
735		// Check setting/getting of string items.
736		context.set_user_prompt(Some("Who art thou? ")).unwrap();
737		assert_eq!(context.user_prompt().unwrap(), "Who art thou? ");
738		context.set_tty(Some("/dev/tty")).unwrap();
739		assert_eq!(context.tty().unwrap(), "/dev/tty");
740		context.set_ruser(Some("nobody")).unwrap();
741		assert_eq!(context.ruser().unwrap(), "nobody");
742		context.set_rhost(Some("nowhere")).unwrap();
743		assert_eq!(context.rhost().unwrap(), "nowhere");
744		// Check linux specific items
745		#[cfg(target_os = "linux")]
746		{
747			context.set_authtok_type(Some("TEST")).unwrap();
748			assert_eq!(context.authtok_type().unwrap(), "TEST");
749			context.set_xdisplay(Some(":0")).unwrap();
750			assert_eq!(context.xdisplay().unwrap(), ":0");
751			let xauthname = CString::new("TEST_DATA").unwrap();
752			let xauthdata = [];
753			let _ = context.xauthdata();
754			context
755				.set_xauthdata(Some((&xauthname, &xauthdata)))
756				.unwrap();
757			let (resultname, resultdata) = context.xauthdata().unwrap();
758			assert_eq!(resultname, xauthname.as_c_str());
759			assert_eq!(resultdata, &xauthdata);
760		};
761		// Check accessing the conversation handler
762		assert_eq!(
763			context.conversation_mut() as *mut _ as *const _,
764			context.conversation() as *const _
765		);
766		context
767			.conversation_mut()
768			.text_info(&CString::new("").unwrap());
769		// Check getting an unaccessible item
770		assert!(context.get_item(pam_sys::PAM_AUTHTOK as c_int).is_err());
771		// Check environment setting/getting
772		context.putenv("TEST=1").unwrap();
773		context.putenv("TEST2=2").unwrap();
774		let _ = context.putenv("\0=\0").unwrap_err();
775		assert_eq!(context.getenv("TEST").unwrap(), "1");
776		assert!(context.getenv("TESTNONEXIST").is_none());
777		let env = context.envlist();
778		assert!(env.len() > 0);
779		let _ = env.get("TEST").unwrap();
780		let _ = env.get("TESTNONEXIST").is_none();
781		for (key, value) in env.iter_tuples() {
782			if key.to_string_lossy() == "TEST" {
783				assert_eq!(value.to_string_lossy(), "1");
784			}
785		}
786		assert!(format!("{:?}", &env.iter_tuples()).contains("EnvItem"));
787		for item in &env {
788			let string = item.to_string();
789			if string.starts_with("TEST=") {
790				assert_eq!(string, "TEST=1");
791				assert!(format!("{:?}", &item).contains("EnvItem"));
792			} else if string.starts_with("TEST2=") {
793				let (_, v): (&OsStr, &OsStr) = item.into();
794				assert_eq!(v.to_string_lossy(), "2");
795			}
796			let _ = item.as_ref();
797		}
798		let _ = format!("{:?}", &env);
799		assert_eq!(env.is_empty(), false);
800		assert_eq!(env.len(), env.as_ref().len());
801		assert_eq!(env.as_ref(), context.envlist().as_ref());
802		assert_eq!(
803			env.as_ref().partial_cmp(context.envlist().as_ref()),
804			Some(std::cmp::Ordering::Equal)
805		);
806		assert_eq!(
807			env.as_ref().cmp(context.envlist().as_ref()),
808			std::cmp::Ordering::Equal
809		);
810		assert_eq!(&env["TEST"], "1");
811		assert_eq!(env.len(), env.iter_tuples().size_hint().0);
812		let list: std::vec::Vec<&CStr> = (&env).into();
813		assert_eq!(list.len(), env.len());
814		let list: std::vec::Vec<(&OsStr, _)> = (&env).into();
815		assert_eq!(list.len(), env.len());
816		let map: std::collections::HashMap<&OsStr, _> = (&env).into();
817		assert_eq!(map.len(), map.len());
818		assert_eq!(
819			map.get(&OsString::from("TEST".to_string()).as_ref()),
820			Some(&OsString::from("1".to_string()).as_ref())
821		);
822		assert!(env.to_string().contains("TEST=1"));
823		let list: std::vec::Vec<(std::ffi::OsString, _)> = context.envlist().into();
824		assert_eq!(list.len(), env.len());
825		let list: std::vec::Vec<CString> = context.envlist().into();
826		assert_eq!(list.len(), env.len());
827		let map: std::collections::HashMap<_, _> = context.envlist().into();
828		assert_eq!(map.len(), env.len());
829		assert_eq!(
830			map.get(&OsString::from("TEST".to_string())),
831			Some(&OsString::from("1".to_string()))
832		);
833		drop(context)
834	}
835
836	#[test]
837	fn test_conv_replace() {
838		let mut context =
839			Context::new("test", Some("user"), crate::conv_null::Conversation::new()).unwrap();
840		// Set username
841		context.set_user(Some("anybody")).unwrap();
842		// Replace conversation handler
843		let (mut context, old_conv) = context
844			.replace_conversation(crate::conv_mock::Conversation::default())
845			.unwrap();
846		// Check if set username was propagated to the new handler
847		assert_eq!(context.conversation().username, "anybody");
848		context.set_user(None).unwrap();
849		let (context, _) = context.replace_conversation(old_conv).unwrap();
850		// Check if username stays None after being set througout a replace.
851		assert!(context.user().is_err());
852	}
853
854	/// Shallowly tests a full authentication + password change + session cycle.
855	///
856	/// This will fail if the environment is not appropriately
857	/// prepared and the test process has no elevated rights.
858	/// Currently it is only checked if some function crashes
859	/// or panics, not if the authentication succeeds.
860	#[test]
861	#[cfg_attr(not(feature = "full_test"), ignore)]
862	fn test_full() {
863		let mut context = Context::new(
864			"test_rust_pam_client",
865			Some("nobody"),
866			crate::conv_null::Conversation::new(),
867		)
868		.unwrap();
869		let _ = context.authenticate(Flag::SILENT);
870		let _ = context.acct_mgmt(Flag::SILENT);
871		let _ = context.chauthtok(Flag::CHANGE_EXPIRED_AUTHTOK);
872		let _ = context.reinitialize_credentials(Flag::SILENT | Flag::NONE);
873		drop(context.open_session(Flag::SILENT));
874		drop(context.open_pseudo_session(Flag::SILENT));
875	}
876
877	/// Shallowly tests a full session cycle without authentication.
878	///
879	/// This will fail if the environment is not appropriately
880	/// prepared and the test process has no elevated rights.
881	/// Currently it is only checked if some function crashes
882	/// or panics, not if anything really succeeds.
883	///
884	/// Some operating systems besides Linux don't support this mode of operation.
885	#[test]
886	#[cfg_attr(not(feature = "full_test"), ignore)]
887	fn test_full_unauth() {
888		let mut context = Context::new(
889			"test_rust_pam_client",
890			Some("nobody"),
891			crate::conv_null::Conversation::new(),
892		)
893		.unwrap();
894		let _ = context.acct_mgmt(Flag::SILENT);
895		let _ = context.chauthtok(Flag::CHANGE_EXPIRED_AUTHTOK);
896		let _ = context.reinitialize_credentials(Flag::SILENT | Flag::NONE);
897		if let Ok(mut session) = context.open_session(Flag::SILENT) {
898			let _ = session.refresh_credentials(Flag::SILENT);
899			let _ = session.reinitialize_credentials(Flag::SILENT);
900			let _ = session.envlist();
901			let _ = session.close(Flag::SILENT);
902		};
903		if let Ok(mut session) = context.open_pseudo_session(Flag::SILENT) {
904			let _ = session.refresh_credentials(Flag::SILENT);
905		};
906	}
907}