Skip to content

Commit

Permalink
API rework around a Converse trait
Browse files Browse the repository at this point in the history
  • Loading branch information
elinorbgr authored and 1wilkens committed Feb 8, 2019
1 parent cf6e24a commit bdf8c75
Show file tree
Hide file tree
Showing 5 changed files with 191 additions and 51 deletions.
3 changes: 3 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -19,3 +19,6 @@ travis-ci = { repository = "1wilkens/pam-auth" }
libc = "^0.2"
pam-sys = "^0.5"
users = "^0.8"

[dev-dependencies]
rpassword = "2.0"
37 changes: 37 additions & 0 deletions examples/spawn_bash.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
use std::io::{stdin, stdout, Write};
use std::os::unix::process::CommandExt;
use std::process::Command;

use pam_auth::Authenticator;
use users::get_user_by_name;
use rpassword::read_password_from_tty;

// A simple program that requests a login and a password and then spawns /bin/bash as the
// user who logged in.
//
// Note that this proto-"sudo" is very insecure and should not be used in any production setup,
// it is just an example to show how the PAM api works.

fn main() {
// First, prompt the user for a login and a password
print!("login: ");
stdout().flush().unwrap();
let mut login = String::new();
stdin().read_line(&mut login).unwrap();
login.pop(); // remove the trailing '\n'
let password = read_password_from_tty(Some("password: ")).unwrap();

// Now, setup the authenticator, we require the basic "system-auth" service
let mut authenticator = Authenticator::with_password("system-auth").expect("Failed to init PAM client!");
authenticator.get_handler().set_credentials(login.clone(), password);
authenticator.authenticate().expect("Authentication failed!");
authenticator.open_session().expect("Failed to open a session!");

// we now try to spawn `/bin/bash` as this user
// note that setting the uid/gid is likely to fail if this program is not already run as the
// proper user or as root
let user = get_user_by_name(&login).unwrap();
let error = Command::new("/bin/bash").uid(user.uid()).gid(user.primary_group_id()).exec();
// if exec() returned, this means there was an error:
println!("Error spawning bash: {:?}", error);
}
77 changes: 50 additions & 27 deletions src/authenticator.rs
Original file line number Diff line number Diff line change
@@ -1,53 +1,78 @@
use libc::c_void;
use pam::{self, PamConversation, PamFlag, PamHandle, PamReturnCode};
use pam::{self, PamFlag, PamHandle, PamReturnCode};
use users;

use std::{env, ptr};

use crate::{env::get_pam_env, ffi, PamResult};
use crate::{env::get_pam_env, ffi, PamResult, Converse, PamError, PasswordConv};

/// Main struct to authenticate a user
/// Currently closes the session on drop() but this might change!
pub struct Authenticator<'a, 'b> {
///
/// You need to create an instance of it to start an authentication process. If you
/// want a simple password-based authentication, you can use `Authenticator::with_password`,
/// and to the following flow:
///
/// ```no_run
/// use pam_auth::Authenticator;
///
/// let mut authenticator = Authenticator::with_password("system-auth")
/// .expect("Failed to init PAM client.");
/// // Preset the login & password we will use for authentication
/// authenticator.get_handler().set_credentials("login", "password");
/// // actually try to authenticate:
/// authenticator.authenticate().expect("Authentication failed!");
/// // Now that we are authenticated, it's possible to open a sesssion:
/// authenticator.open_session().expect("Failed to open a session!");
/// ```
///
/// If you wish to customise the PAM conversation function, you should rather create your
/// authenticator with `Authenticator::with_handler`, providing a struct implementing the
/// `Converse` trait. You can then mutably access your conversation handler using the
/// `Authenticator::get_handler` method.
///
/// By default, the `Authenticator` will close any opened session when dropped. If you don't
/// want this, you can change its `close_on_drop` field to `False`.
pub struct Authenticator<'a, C: Converse> {
/// Flag indicating whether the Authenticator should close the session on drop
pub close_on_drop: bool,
handle: &'a mut PamHandle,
credentials: Box<[&'b str; 2]>,
converse: Box<C>,
is_authenticated: bool,
has_open_session: bool,
last_code: PamReturnCode,
}

impl<'a, 'b> Authenticator<'a, 'b> {
/// Creates a new Authenticator with a given service name
pub fn new(service: &str) -> Option<Authenticator> {
let creds = Box::new([""; 2]);
let conv = PamConversation {
conv: Some(ffi::converse),
data_ptr: creds.as_ptr() as *mut c_void,
};
impl<'a> Authenticator<'a, PasswordConv> {
/// Create a new `Authenticator` with a given service name and a password-based conversation
pub fn with_password(service: &str) -> PamResult<Authenticator<'a, PasswordConv>> {
Authenticator::with_handler(service, PasswordConv::new())
}
}

impl<'a, C: Converse> Authenticator<'a, C> {
/// Creates a new Authenticator with a given service name and conversation callback
pub fn with_handler(service: &str, converse: C) -> PamResult<Authenticator<'a, C>> {
let mut converse = Box::new(converse);
let conv = ffi::make_conversation(&mut *converse);
let mut handle: *mut PamHandle = ptr::null_mut();

match pam::start(service, None, &conv, &mut handle) {
PamReturnCode::SUCCESS => unsafe {
Some(Authenticator {
Ok(Authenticator {
close_on_drop: true,
handle: &mut *handle,
credentials: creds,
converse,
is_authenticated: false,
has_open_session: false,
last_code: PamReturnCode::SUCCESS,
})
},
_ => None,
code => Err(PamError(code)),
}
}

/// Set the credentials which should be used in the authentication process.
/// Currently only username/password combinations are supported
pub fn set_credentials(&mut self, user: &'b str, password: &'b str) {
self.credentials[0] = user;
self.credentials[1] = password;
/// Access the conversation handler of this Authenticator
pub fn get_handler(&mut self) -> &mut C {
&mut *self.converse
}

/// Perform the authentication with the provided credentials
Expand Down Expand Up @@ -109,10 +134,8 @@ impl<'a, 'b> Authenticator<'a, 'b> {
}
}

let user = users::get_user_by_name(self.credentials[0]).expect(&format!(
"Could not get user by name: {:?}",
self.credentials[0]
));
let user = users::get_user_by_name(self.converse.username())
.unwrap_or_else(|| panic!("Could not get user by name: {:?}", self.converse.username()));

// Set some common environment variables
self.set_env(
Expand Down Expand Up @@ -162,7 +185,7 @@ impl<'a, 'b> Authenticator<'a, 'b> {
}
}

impl<'a, 'b> Drop for Authenticator<'a, 'b> {
impl<'a, C: Converse> Drop for Authenticator<'a, C> {
fn drop(&mut self) {
if self.has_open_session && self.close_on_drop {
pam::close_session(self.handle, PamFlag::NONE);
Expand Down
50 changes: 26 additions & 24 deletions src/ffi.rs
Original file line number Diff line number Diff line change
@@ -1,60 +1,62 @@
use libc::{c_char, c_int, c_void, calloc, free, size_t, strdup};
use pam::{PamMessage, PamMessageStyle, PamResponse, PamReturnCode};
use libc::{calloc, free, strdup, c_int, c_void, size_t};
use pam::{PamMessage, PamMessageStyle, PamResponse, PamReturnCode, PamConversation};

use std::ffi::{CStr, CString};
use std::ffi::CStr;
use std::mem;
use std::slice;

pub extern "C" fn converse(
num_msg: c_int,
msg: *mut *mut PamMessage,
out_resp: *mut *mut PamResponse,
appdata_ptr: *mut c_void,
) -> c_int {
// allocate space for responses
use crate::Converse;

pub fn make_conversation<C: Converse>(user_converse: &mut C) -> PamConversation {
PamConversation {
conv: Some(converse::<C>),
data_ptr: user_converse as *mut C as *mut c_void
}
}

pub extern "C" fn converse<C: Converse>(num_msg: c_int,
msg: *mut *mut PamMessage,
out_resp: *mut *mut PamResponse,
appdata_ptr: *mut c_void)
-> c_int {
// allocate space for responses
let resp = unsafe {
calloc(num_msg as usize, mem::size_of::<PamResponse>() as size_t) as *mut PamResponse
};
if resp.is_null() {
return PamReturnCode::BUF_ERR as c_int;
}

let data: &[&str] = unsafe { slice::from_raw_parts(appdata_ptr as *const &str, 2) };
let handler = unsafe { &mut *(appdata_ptr as *mut C) };

let mut result: PamReturnCode = PamReturnCode::SUCCESS;
for i in 0..num_msg as isize {
unsafe {
// get indexed values
let m: &mut PamMessage = &mut **(msg.offset(i));
let r: &mut PamResponse = &mut *(resp.offset(i));
let msg = CStr::from_ptr(m.msg);
// match on msg_style
match PamMessageStyle::from(m.msg_style) {
// assume username is requested
PamMessageStyle::PROMPT_ECHO_ON => {
if let Ok(username) = CString::new(data[0]) {
r.resp = strdup(username.as_ptr() as *const c_char);
if let Ok(handler_response) = handler.prompt_echo(msg) {
r.resp = strdup(handler_response.as_ptr());
} else {
result = PamReturnCode::CONV_ERR;
}
}
// assume password is requested
PamMessageStyle::PROMPT_ECHO_OFF => {
if let Ok(password) = CString::new(data[1]) {
r.resp = strdup(password.as_ptr() as *const c_char);
if let Ok(handler_response) = handler.prompt_blind(msg) {
r.resp = strdup(handler_response.as_ptr());
} else {
result = PamReturnCode::CONV_ERR;
}
}
// an error occured
PamMessageStyle::ERROR_MSG => {
handler.error(msg);
result = PamReturnCode::CONV_ERR;
}
// print the message to stdout
PamMessageStyle::TEXT_INFO => {
println!(
"PAM_TEXT_INFO: {}",
String::from_utf8_lossy(CStr::from_ptr(m.msg).to_bytes())
);
handler.info(msg);
}
}
}
Expand Down
75 changes: 75 additions & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ mod authenticator;
mod env;
mod ffi;

use std::ffi::{CStr, CString};

pub use crate::authenticator::*;

pub struct PamError(pam::PamReturnCode);
Expand Down Expand Up @@ -32,3 +34,76 @@ impl From<pam::PamReturnCode> for PamError {
PamError(err)
}
}

/// A trait representing the PAM authentification conversation
///
/// PAM authentification is done as a conversation mechanism, in which PAM
/// asks several questions and the client (your code) answers them. This trait
/// is a representation of such a conversation, which one method for each message
/// PAM can send you.
///
/// This is the trait to implement if you want to customize the conversation with
/// PAM. If you just want a simple login/password authentication, you can use the
/// `PasswordConv` implementation provided by this crate.
pub trait Converse {
/// PAM requests a value that should be echoed to the user as they type it
///
/// This would typically be the username. The exact question is provided as the
/// `msg` argument if you wish to display it to your user.
fn prompt_echo(&mut self, msg: &CStr) -> ::std::result::Result<CString, ()>;
/// PAM requests a value that should be typed blindly by the user
///
/// This would typically be the password. The exact question is provided as the
/// `msg` argument if you wish to display it to your user.
fn prompt_blind(&mut self, msg: &CStr) -> ::std::result::Result<CString, ()>;
/// This is an informational message from PAM
fn info(&mut self, msg: &CStr);
/// This is an error message from PAM
fn error(&mut self, msg: &CStr);
/// Get the username that is being authenticated
///
/// This method is not a PAM callback, but is rather used by the `Authenticator` to
/// setup the environment when opening a session.
fn username(&self) -> &str;
}

/// A minimalistic conversation handler, that uses given login and password
///
/// This conversation handler is not really interactive, but simply returns to
/// PAM the value that have been set using the `set_credentials` method.
pub struct PasswordConv {
login: String,
passwd: String
}

impl PasswordConv {
/// Create a new `PasswordConv` handler
fn new() -> PasswordConv {
PasswordConv {
login: String::new(),
passwd: String::new()
}
}

/// Set the credentials that this handler will provide to PAM
pub fn set_credentials<U: Into<String>, V: Into<String>>(&mut self, login: U, password: V) {
self.login = login.into();
self.passwd = password.into();
}
}

impl Converse for PasswordConv {
fn prompt_echo(&mut self, _msg: &CStr) -> Result<CString, ()> {
CString::new(self.login.clone()).map_err(|_| ())
}
fn prompt_blind(&mut self, _msg: &CStr) -> Result<CString, ()> {
CString::new(self.passwd.clone()).map_err(|_| ())
}
fn info(&mut self, _msg: &CStr) {}
fn error(&mut self, msg: &CStr) {
eprintln!("[PAM ERROR] {}", msg.to_string_lossy());
}
fn username(&self) -> &str {
&self.login
}
}

0 comments on commit bdf8c75

Please sign in to comment.