summaryrefslogtreecommitdiffstats
path: root/src/lib.rs
diff options
context:
space:
mode:
authorTobias Wiese <tobias@tobiaswiese.com>2025-12-03 19:10:02 +0100
committerTobias Wiese <tobias@tobiaswiese.com>2025-12-03 21:08:16 +0100
commita464bec09a922d7f9040e512b9ed04794a28d174 (patch)
treeceff760ba1b79b4200704cf76c2de4921cc9c2fa /src/lib.rs
Initial commit1.0.0
Signed-off-by: Tobias Wiese <tobias@tobiaswiese.com>
Diffstat (limited to 'src/lib.rs')
-rw-r--r--src/lib.rs107
1 files changed, 107 insertions, 0 deletions
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::<Bool>::as_expression(
+ !flags.contains(PamFlags::DISALLOW_NULL_AUTHTOK),
+ )))
+ .load::<String>(&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<String>) -> PamError {
+ authenticate(pam, flags, &args)
+ .err()
+ .unwrap_or(PamError::SUCCESS)
+ }
+}
+
+pam_module!(PamTokenPg);
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+}