pam_client/
conv_mock.rs

1//! Simple non-interactive conversation handler
2
3/***********************************************************************
4 * (c) 2021 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
11#![forbid(unsafe_code)]
12
13use super::ConversationHandler;
14use crate::error::ErrorCode;
15use std::ffi::{CStr, CString};
16use std::iter::FusedIterator;
17use std::vec;
18
19/// Elements in [`Conversation::log`]
20#[derive(Debug, Clone)]
21#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
22pub enum LogEntry {
23	Info(CString),
24	Error(CString),
25}
26
27/// Non-interactive implementation of `ConversationHandler`
28///
29/// When a PAM module asks for a non-secret string, [`username`][`Self::username`]
30/// will be returned and when a secret string is asked for,
31/// [`password`][`Self::password`] will be returned.
32///
33/// All info and error messages will be recorded in [`log`][`Self::log`].
34///
35/// # Limitations
36///
37/// This is enough to handle many authentication flows non-interactively, but
38/// flows with two-factor-authentication and things like
39/// [`chauthok()`][`crate::Context::chauthtok()`] will most definitely fail.
40///
41/// Please also note that UTF-8 encoding is assumed for both username and
42/// password, so this handler may fail to authenticate on legacy non-UTF-8
43/// systems when one of the strings contains non-ASCII characters.
44#[derive(Debug, Clone)]
45#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
46pub struct Conversation {
47	/// The username to use
48	pub username: String,
49	/// The password to use
50	pub password: String,
51	/// All received info/error messages
52	pub log: vec::Vec<LogEntry>,
53}
54
55impl Conversation {
56	/// Creates a new CLI conversation handler
57	///
58	/// If [`username`][`Self::username`] isn't manually set to a non-empty
59	/// string, it will be automatically set to the `Context`s default
60	/// username on context initialization.
61	#[must_use]
62	pub const fn new() -> Self {
63		Self {
64			username: String::new(),
65			password: String::new(),
66			log: vec::Vec::new(),
67		}
68	}
69
70	/// Creatse a new CLI conversation handler with preset credentials
71	#[must_use]
72	pub fn with_credentials(username: impl Into<String>, password: impl Into<String>) -> Self {
73		Self {
74			username: username.into(),
75			password: password.into(),
76			log: vec::Vec::new(),
77		}
78	}
79
80	/// Clears the error/info log
81	pub fn clear_log(&mut self) {
82		self.log.clear();
83	}
84
85	/// Lists only errors from the log
86	pub fn errors(&self) -> impl Iterator<Item = &CString> + FusedIterator {
87		self.log.iter().filter_map(|x| match x {
88			LogEntry::Info(_) => None,
89			LogEntry::Error(msg) => Some(msg),
90		})
91	}
92
93	/// Lists only info messages from the log
94	pub fn infos(&self) -> impl Iterator<Item = &CString> + FusedIterator {
95		self.log.iter().filter_map(|x| match x {
96			LogEntry::Info(msg) => Some(msg),
97			LogEntry::Error(_) => None,
98		})
99	}
100}
101
102impl Default for Conversation {
103	fn default() -> Self {
104		Self::new()
105	}
106}
107
108impl ConversationHandler for Conversation {
109	fn init(&mut self, default_user: Option<impl AsRef<str>>) {
110		if let Some(user) = default_user {
111			if self.username.is_empty() {
112				self.username = user.as_ref().to_string();
113			}
114		}
115	}
116
117	fn prompt_echo_on(&mut self, _msg: &CStr) -> Result<CString, ErrorCode> {
118		CString::new(self.username.clone()).map_err(|_| ErrorCode::CONV_ERR)
119	}
120
121	fn prompt_echo_off(&mut self, _msg: &CStr) -> Result<CString, ErrorCode> {
122		CString::new(self.password.clone()).map_err(|_| ErrorCode::CONV_ERR)
123	}
124
125	fn text_info(&mut self, msg: &CStr) {
126		self.log.push(LogEntry::Info(msg.to_owned()));
127	}
128
129	fn error_msg(&mut self, msg: &CStr) {
130		self.log.push(LogEntry::Error(msg.to_owned()));
131	}
132
133	fn radio_prompt(&mut self, _msg: &CStr) -> Result<bool, ErrorCode> {
134		Ok(false)
135	}
136}
137
138#[cfg(test)]
139mod tests {
140	use super::*;
141
142	#[test]
143	fn test() {
144		let text = CString::new("test").unwrap();
145		let mut c = Conversation::default();
146		let _ = c.clone();
147		assert!(c.prompt_echo_on(&text).is_ok());
148		assert!(c.prompt_echo_off(&text).is_ok());
149		assert!(c.radio_prompt(&text).ok() == Some(false));
150		assert!(c.binary_prompt(0, &[]).is_err());
151		c.text_info(&text);
152		c.error_msg(&text);
153		assert_eq!(c.log.len(), 2);
154		let v: std::vec::Vec<&CString> = c.errors().collect();
155		assert_eq!(v.len(), 1);
156		let v: std::vec::Vec<&CString> = c.infos().collect();
157		assert_eq!(v.len(), 1);
158		assert!(format!("{:?}", &c).contains("test"));
159	}
160}