From bf959a2534d23c5c667ea3914bc57d6ef2aeb2b2 Mon Sep 17 00:00:00 2001 From: Tobias Wiese Date: Wed, 3 Dec 2025 21:49:13 +0100 Subject: Initial commit Signed-off-by: Tobias Wiese --- src/lib.rs | 107 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 107 insertions(+) create mode 100644 src/lib.rs (limited to 'src/lib.rs') diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..9edc99b --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,107 @@ +use constant_time_eq::constant_time_eq; +use diesel::expression::AsExpression; +use diesel::pg::PgConnection; +use diesel::prelude::*; +use diesel::sql_types::Bool; +use pamsm::{Pam, PamError, PamFlags, PamLibExt, PamResult, PamServiceModule, pam_module}; + +use self::db::*; + +mod db; + +struct PamTokenPg; + +fn authenticate(pam: Pam, flags: PamFlags, args: &[String]) -> PamResult<()> { + let mut dsn = None; + let mut table = None; + let mut schema = None; + + macro_rules! parse_options { + ($args:expr, $($opt:ident),+$(,)?) => { + for arg in $args { + if false { unreachable!() } + $(else if arg.starts_with(concat!(stringify!($opt), "=")) { + let value = &arg[(stringify!($opt).len() + 1)..]; + if $opt.is_none() { + $opt = Some(value); + } else { + pam.syslog(pamsm::LogLvl::ERR, concat!( + "improperly configured: option '", + stringify!($opt), + "' specified multiple times", + ))?; + return Err(PamError::SYSTEM_ERR); + } + })+ + else { + pam.syslog( + pamsm::LogLvl::ERR, + &format!("improperly configured: invalid option '{}'", arg), + )?; + return Err(PamError::SYSTEM_ERR); + } + } + } + } + parse_options!(args, dsn, table, schema); + let Some(dsn) = dsn else { + pam.syslog( + pamsm::LogLvl::ERR, + "improperly configured: missing required option 'dsn'", + )?; + return Err(PamError::SYSTEM_ERR); + }; + let Some(table) = table else { + pam.syslog( + pamsm::LogLvl::ERR, + "improperly configured: missing required option 'table'", + )?; + return Err(PamError::SYSTEM_ERR); + }; + let table = db::Table::new(table, schema); + + let pam_user = pam + .get_user(None) + .and_then(|user| user.ok_or(PamError::AUTH_ERR))? + .to_str() + .map_err(|_| PamError::SERVICE_ERR)?; + let pam_authtok = pam + .get_authtok(None) + .and_then(|authtok| authtok.ok_or(PamError::AUTH_ERR))? + .to_bytes(); + + let mut conn = PgConnection::establish(dsn).map_err(|_| PamError::AUTHINFO_UNAVAIL)?; + + let user_tokens = table + .select(token) + .filter(username.eq(pam_user)) + .filter(token.ne("").or(AsExpression::::as_expression( + !flags.contains(PamFlags::DISALLOW_NULL_AUTHTOK), + ))) + .load::(&mut conn) + .map_err(|_| PamError::AUTHINFO_UNAVAIL)?; + + for user_token in user_tokens { + let user_token = user_token.as_bytes(); + if constant_time_eq(user_token, pam_authtok) { + return Ok(()); + } + } + + Err(PamError::AUTH_ERR) +} + +impl PamServiceModule for PamTokenPg { + fn authenticate(pam: Pam, flags: PamFlags, args: Vec) -> PamError { + authenticate(pam, flags, &args) + .err() + .unwrap_or(PamError::SUCCESS) + } +} + +pam_module!(PamTokenPg); + +#[cfg(test)] +mod tests { + use super::*; +} -- cgit v1.2.3