diff options
| -rw-r--r-- | .gitignore | 1 | ||||
| -rw-r--r-- | Cargo.lock | 215 | ||||
| -rw-r--r-- | Cargo.toml | 19 | ||||
| -rw-r--r-- | src/db.rs | 149 | ||||
| -rw-r--r-- | src/lib.rs | 107 |
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::*; +} |
