summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--.gitignore1
-rw-r--r--Cargo.lock215
-rw-r--r--Cargo.toml19
-rw-r--r--src/db.rs149
-rw-r--r--src/lib.rs107
5 files changed, 491 insertions, 0 deletions
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..ea8c4bf
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1 @@
+/target
diff --git a/Cargo.lock b/Cargo.lock
new file mode 100644
index 0000000..19465b9
--- /dev/null
+++ b/Cargo.lock
@@ -0,0 +1,215 @@
+# This file is automatically @generated by Cargo.
+# It is not intended for manual editing.
+version = 4
+
+[[package]]
+name = "bitflags"
+version = "1.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "Could not get crate checksum"
+
+[[package]]
+name = "bitflags"
+version = "2.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "Could not get crate checksum"
+
+[[package]]
+name = "byteorder"
+version = "1.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b"
+
+[[package]]
+name = "constant_time_eq"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "Could not get crate checksum"
+
+[[package]]
+name = "darling"
+version = "0.20.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6f63b86c8a8826a49b8c21f08a2d07338eec8d900540f8630dc76284be802989"
+dependencies = [
+ "darling_core",
+ "darling_macro",
+]
+
+[[package]]
+name = "darling_core"
+version = "0.20.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "95133861a8032aaea082871032f5815eb9e98cef03fa916ab4500513994df9e5"
+dependencies = [
+ "fnv",
+ "ident_case",
+ "proc-macro2",
+ "quote",
+ "strsim",
+ "syn",
+]
+
+[[package]]
+name = "darling_macro"
+version = "0.20.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d336a2a514f6ccccaa3e09b02d41d35330c07ddf03a62165fcec10bb561c7806"
+dependencies = [
+ "darling_core",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "diesel"
+version = "2.2.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "Could not get crate checksum"
+dependencies = [
+ "bitflags 2.8.0",
+ "byteorder",
+ "diesel_derives",
+ "itoa",
+ "pq-sys",
+]
+
+[[package]]
+name = "diesel_derives"
+version = "2.2.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e7f2c3de51e2ba6bf2a648285696137aaf0f5f487bcbea93972fe8a364e131a4"
+dependencies = [
+ "diesel_table_macro_syntax",
+ "dsl_auto_type",
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "diesel_table_macro_syntax"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "209c735641a413bc68c4923a9d6ad4bcb3ca306b794edaa7eb0b3228a99ffb25"
+dependencies = [
+ "syn",
+]
+
+[[package]]
+name = "dsl_auto_type"
+version = "0.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "Could not get crate checksum"
+dependencies = [
+ "darling",
+ "either",
+ "heck",
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "either"
+version = "1.13.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0"
+
+[[package]]
+name = "fnv"
+version = "1.0.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
+
+[[package]]
+name = "heck"
+version = "0.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8"
+
+[[package]]
+name = "ident_case"
+version = "1.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39"
+
+[[package]]
+name = "itoa"
+version = "1.0.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d75a2a4b1b190afb6f5425f10f6a8f959d2ea0b9c2b1d79553551850539e4674"
+
+[[package]]
+name = "pam_tokenpg"
+version = "1.0.0"
+dependencies = [
+ "constant_time_eq",
+ "diesel",
+ "pamsm",
+]
+
+[[package]]
+name = "pamsm"
+version = "0.5.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "aad7ddca63c73e80eb4ace88e130c9b513da6ec1284becd9fc1fc385a9a72a64"
+dependencies = [
+ "bitflags 1.3.2",
+]
+
+[[package]]
+name = "pq-sys"
+version = "0.4.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "Could not get crate checksum"
+dependencies = [
+ "vcpkg",
+]
+
+[[package]]
+name = "proc-macro2"
+version = "1.0.92"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "37d3544b3f2748c54e147655edb5025752e2303145b5aefb3c3ea2c78b973bb0"
+dependencies = [
+ "unicode-ident",
+]
+
+[[package]]
+name = "quote"
+version = "1.0.40"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d"
+dependencies = [
+ "proc-macro2",
+]
+
+[[package]]
+name = "strsim"
+version = "0.11.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
+
+[[package]]
+name = "syn"
+version = "2.0.98"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "Could not get crate checksum"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "unicode-ident",
+]
+
+[[package]]
+name = "unicode-ident"
+version = "1.0.13"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e91b56cd4cadaeb79bbf1a5645f6b4f8dc5bde8834ad5894a8db35fda9efa1fe"
+
+[[package]]
+name = "vcpkg"
+version = "0.2.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3fc439f2794e98976c88a2a2dafce96b930fe8010b0a256b3c2199a773933168"
diff --git a/Cargo.toml b/Cargo.toml
new file mode 100644
index 0000000..254966e
--- /dev/null
+++ b/Cargo.toml
@@ -0,0 +1,19 @@
+[package]
+name = "pam_tokenpg"
+version = "1.0.0"
+authors = ["Tobias Wiese <tobias@tobiaswiese.com>"]
+edition = "2024"
+
+[lib]
+crate-type = ["cdylib"]
+
+[profile.dev]
+panic = "abort"
+
+[profile.release]
+panic = "abort"
+
+[dependencies]
+pamsm = { version = "0.5.5", features = ["libpam"] }
+diesel = { version = "2.2.7", features = ["postgres"] }
+constant_time_eq = "0.3.1"
diff --git a/src/db.rs b/src/db.rs
new file mode 100644
index 0000000..b088f0a
--- /dev/null
+++ b/src/db.rs
@@ -0,0 +1,149 @@
+#![allow(non_camel_case_types)]
+
+use diesel::backend::Backend;
+use diesel::expression::{
+ IsContainedInGroupBy, ValidGrouping, is_aggregate, is_contained_in_group_by,
+};
+use diesel::internal::table_macro::{FromClause, SelectStatement};
+use diesel::prelude::*;
+use diesel::query_builder::{AsQuery, AstPass, QueryFragment, QueryId};
+use diesel::query_source::{AppearsInFromClause, Once, Table as TableTrait};
+use diesel::sql_types::{Text, Uuid};
+
+pub use self::columns::*;
+
+pub mod columns {
+ use super::*;
+
+ macro_rules! col {
+ ($col:ident, $sql_type:ty) => {
+ pub struct $col;
+
+ impl Expression for $col {
+ type SqlType = $sql_type;
+ }
+
+ impl<'a, QS> AppearsOnTable<QS> for $col where
+ QS: AppearsInFromClause<super::Table<'a>, Count = Once>
+ {
+ }
+
+ impl<'a> SelectableExpression<super::Table<'a>> for $col {}
+
+ impl ValidGrouping<()> for $col {
+ type IsAggregate = is_aggregate::No;
+ }
+
+ impl<GB> ValidGrouping<GB> for $col
+ where
+ GB: IsContainedInGroupBy<$col, Output = is_contained_in_group_by::Yes>,
+ {
+ type IsAggregate = is_aggregate::Yes;
+ }
+
+ impl<'a> Column for $col {
+ type Table = Table<'static>;
+
+ const NAME: &'static str = stringify!($col);
+ }
+
+ impl<DB> QueryFragment<DB> for $col
+ where
+ DB: Backend,
+ {
+ fn walk_ast<'b>(&'b self, mut pass: AstPass<'_, 'b, DB>) -> QueryResult<()> {
+ pass.push_identifier(<$col as Column>::NAME)?;
+ Ok(())
+ }
+ }
+
+ impl QueryId for $col {
+ type QueryId = $col;
+
+ const HAS_STATIC_QUERY_ID: bool = true;
+ }
+ };
+ }
+
+ col!(id, Uuid);
+ col!(username, Text);
+ col!(token, Text);
+}
+
+//pub const all_columns: <Table<'static> as TableTrait>::AllColumns = (id, username, token);
+
+pub type SqlType = (Uuid, Text, Text);
+
+#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq)]
+pub struct Table<'a> {
+ name: &'a str,
+ schema: Option<&'a str>,
+}
+
+impl<'a> Table<'a> {
+ pub fn new(name: &'a str, schema: Option<&'a str>) -> Self {
+ Self { name, schema }
+ }
+}
+
+impl QuerySource for Table<'_> {
+ type FromClause = Self;
+ type DefaultSelection = <Self as TableTrait>::AllColumns;
+
+ fn from_clause(&self) -> Self::FromClause {
+ self.clone()
+ }
+
+ fn default_selection(&self) -> Self::DefaultSelection {
+ <Self as TableTrait>::all_columns()
+ }
+}
+
+impl AsQuery for Table<'_> {
+ type SqlType = SqlType;
+ type Query = SelectStatement<FromClause<Self>>;
+
+ fn as_query(self) -> Self::Query {
+ SelectStatement::simple(self)
+ }
+}
+
+impl TableTrait for Table<'_>
+where
+ Self: QuerySource + AsQuery,
+{
+ type PrimaryKey = id;
+ type AllColumns = (id, username, token);
+
+ fn primary_key(&self) -> Self::PrimaryKey {
+ id
+ }
+
+ fn all_columns() -> Self::AllColumns {
+ (id, username, token)
+ }
+}
+
+impl<DB> QueryFragment<DB> for Table<'_>
+where
+ DB: Backend,
+{
+ fn walk_ast<'b>(&'b self, mut pass: AstPass<'_, 'b, DB>) -> QueryResult<()> {
+ if let Some(ref schema) = self.schema {
+ pass.push_identifier(schema)?;
+ pass.push_sql(".");
+ }
+ pass.push_identifier(self.name)?;
+ Ok(())
+ }
+}
+
+impl QueryId for Table<'_> {
+ type QueryId = ();
+
+ const HAS_STATIC_QUERY_ID: bool = false;
+}
+
+impl AppearsInFromClause<Table<'_>> for Table<'_> {
+ type Count = Once;
+}
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::*;
+}