use std::collections::HashMap;
use std::ffi::OsString;
use std::fs::File;
use crate::common::{error::Error, Context};
use crate::log::{auth_warn, dev_info, user_warn};
use crate::pam::{CLIConverser, Converser, PamContext, PamError, PamErrorType, PamResult};
use crate::system::{
time::Duration,
timestamp::{RecordScope, SessionRecordFile, TouchResult},
Process, WithProcess,
};
use super::pipeline::AuthPlugin;
pub fn determine_record_scope(process: &Process) -> Option<RecordScope> {
let tty = Process::tty_device_id(WithProcess::Current);
if let Ok(Some(tty_device)) = tty {
if let Ok(init_time) = Process::starting_time(WithProcess::Other(process.session_id)) {
Some(RecordScope::Tty {
tty_device,
session_pid: process.session_id,
init_time,
})
} else {
auth_warn!("Could not get terminal foreground process starting time");
None
}
} else if let Some(parent_pid) = process.parent_pid {
if let Ok(init_time) = Process::starting_time(WithProcess::Other(parent_pid)) {
Some(RecordScope::Ppid {
group_pid: parent_pid,
init_time,
})
} else {
auth_warn!("Could not get parent process starting time");
None
}
} else {
None
}
}
fn determine_auth_status(
record_for: Option<RecordScope>,
context: &Context,
) -> (bool, Option<SessionRecordFile<File>>) {
if let (true, Some(record_for)) = (context.use_session_records, record_for) {
match SessionRecordFile::open_for_user(&context.current_user.name, Duration::minutes(15)) {
Ok(mut sr) => {
match sr.touch(record_for, context.current_user.uid) {
Ok(TouchResult::Updated { .. }) => (false, Some(sr)),
Ok(TouchResult::NotFound | TouchResult::Outdated { .. }) => (true, Some(sr)),
Err(e) => {
auth_warn!("Unexpected error while reading session information: {e}");
(true, None)
}
}
}
Err(e) => {
auth_warn!("Could not use session information: {e}");
(true, None)
}
}
} else {
(true, None)
}
}
type PamBuilder<C> = dyn Fn(&Context) -> PamResult<PamContext<C>>;
pub struct PamAuthenticator<C: Converser> {
builder: Box<PamBuilder<C>>,
pam: Option<PamContext<C>>,
}
impl<C: Converser> PamAuthenticator<C> {
fn new(
initializer: impl Fn(&Context) -> PamResult<PamContext<C>> + 'static,
) -> PamAuthenticator<C> {
PamAuthenticator {
builder: Box::new(initializer),
pam: None,
}
}
}
impl PamAuthenticator<CLIConverser> {
pub fn new_cli() -> PamAuthenticator<CLIConverser> {
PamAuthenticator::new(|context| {
let mut pam = PamContext::builder_cli("sudo", context.stdin, context.non_interactive)
.target_user(&context.current_user.name)
.service_name("sudo")
.build()?;
pam.mark_silent(true);
pam.mark_allow_null_auth_token(false);
Ok(pam)
})
}
}
impl<C: Converser> AuthPlugin for PamAuthenticator<C> {
fn init(&mut self, context: &Context) -> Result<(), Error> {
self.pam = Some((self.builder)(context)?);
Ok(())
}
fn authenticate(&mut self, context: &Context, mut max_tries: u16) -> Result<(), Error> {
let pam = self
.pam
.as_mut()
.expect("Pam must be initialized before authenticate");
pam.set_user(&context.current_user.name)?;
let scope = determine_record_scope(&context.process);
let (must_authenticate, records_file) = determine_auth_status(scope, context);
if must_authenticate {
let mut current_try = 0;
loop {
current_try += 1;
match pam.authenticate() {
Ok(_) => break,
Err(PamError::Pam(PamErrorType::MaxTries, _)) => {
return Err(Error::MaxAuthAttempts(current_try));
}
Err(PamError::Pam(PamErrorType::AuthError, _)) => {
max_tries -= 1;
if max_tries == 0 {
return Err(Error::MaxAuthAttempts(current_try));
} else if context.non_interactive {
return Err(Error::Authentication("interaction required".to_string()));
} else {
user_warn!("Authentication failed, try again.");
}
}
Err(e) => {
return Err(e.into());
}
}
}
if let (Some(mut session_records), Some(scope)) = (records_file, scope) {
match session_records.create(scope, context.current_user.uid) {
Ok(_) => (),
Err(e) => {
auth_warn!("Could not update session record file with new record: {e}");
}
}
}
}
Ok(())
}
fn pre_exec(&mut self, context: &Context) -> Result<HashMap<OsString, OsString>, Error> {
let pam = self
.pam
.as_mut()
.expect("Pam must be initialized before pre_exec");
pam.validate_account_or_change_auth_token()?;
pam.set_user(&context.target_user.name)?;
if let Err(e) = pam.credentials_reinitialize() {
dev_info!(
"PAM gave an error while trying to re-initialize credentials: {:?}",
e
);
}
pam.open_session()?;
let env_vars = pam.env()?;
Ok(env_vars)
}
fn cleanup(&mut self) {
let pam = self
.pam
.as_mut()
.expect("Pam must be initialized before cleanup");
let _ = pam.close_session();
}
}