From e119687e9ca2f9d08ab3540587c9709520ded205 Mon Sep 17 00:00:00 2001 From: James Bacon Date: Wed, 22 Apr 2026 13:40:09 +0100 Subject: [PATCH 01/19] Adding Diesel for some actual database integration --- Cargo.lock | 353 ++++++++++++++++++++++++++++++++++++++-- Cargo.toml | 1 + src/token/mod.rs | 1 + src/token/repository.rs | 46 +++++- src/token/schema.rs | 8 + 5 files changed, 397 insertions(+), 12 deletions(-) create mode 100644 src/token/schema.rs diff --git a/Cargo.lock b/Cargo.lock index 833e081..4951fb2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -179,6 +179,16 @@ version = "1.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" +[[package]] +name = "cc" +version = "1.2.55" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47b26a0954ae34af09b50f0de26458fa95369a0d478d8236d3f93082b219bd29" +dependencies = [ + "find-msvc-tools", + "shlex", +] + [[package]] name = "cfg-if" version = "1.0.4" @@ -204,6 +214,87 @@ dependencies = [ "typenum", ] +[[package]] +name = "darling" +version = "0.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9cdf337090841a411e2a7f3deb9187445851f91b309c0c0a29e05f74a00a48c0" +dependencies = [ + "darling_core", + "darling_macro", +] + +[[package]] +name = "darling_core" +version = "0.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1247195ecd7e3c85f83c8d2a366e4210d588e802133e1e355180a9870b517ea4" +dependencies = [ + "fnv", + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn", +] + +[[package]] +name = "darling_macro" +version = "0.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d38308df82d1080de0afee5d069fa14b0326a88c14f15c5ccda35b4a6c414c81" +dependencies = [ + "darling_core", + "quote", + "syn", +] + +[[package]] +name = "deranged" +version = "0.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c" +dependencies = [ + "powerfmt", +] + +[[package]] +name = "diesel" +version = "2.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9b6c2fc184a6fb6ebcf5f9a5e3bbfa84d8fd268cdfcce4ed508979a6259494d" +dependencies = [ + "diesel_derives", + "downcast-rs", + "libsqlite3-sys", + "r2d2", + "sqlite-wasm-rs", + "time", + "uuid", +] + +[[package]] +name = "diesel_derives" +version = "2.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47618bf0fac06bb670c036e48404c26a865e6a71af4114dfd97dfe89936e404e" +dependencies = [ + "diesel_table_macro_syntax", + "dsl_auto_type", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "diesel_table_macro_syntax" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe2444076b48641147115697648dc743c2c00b61adade0f01ce67133c7babe8c" +dependencies = [ + "syn", +] + [[package]] name = "digest" version = "0.10.7" @@ -215,6 +306,32 @@ dependencies = [ "subtle", ] +[[package]] +name = "downcast-rs" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "117240f60069e65410b3ae1bb213295bd828f707b5bec6596a1afc8793ce0cbc" + +[[package]] +name = "dsl_auto_type" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd122633e4bef06db27737f21d3738fb89c8f6d5360d6d9d7635dda142a7757e" +dependencies = [ + "darling", + "either", + "heck", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "either" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" + [[package]] name = "equivalent" version = "1.0.2" @@ -231,12 +348,30 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "find-msvc-tools" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + [[package]] name = "foldhash" version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" +[[package]] +name = "foldhash" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" + [[package]] name = "form_urlencoded" version = "1.2.2" @@ -308,7 +443,7 @@ version = "0.15.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" dependencies = [ - "foldhash", + "foldhash 0.1.5", ] [[package]] @@ -316,6 +451,9 @@ name = "hashbrown" version = "0.16.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" +dependencies = [ + "foldhash 0.2.0", +] [[package]] name = "headers" @@ -434,6 +572,12 @@ version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" +[[package]] +name = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + [[package]] name = "indexmap" version = "2.13.0" @@ -454,9 +598,9 @@ checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2" [[package]] name = "js-sys" -version = "0.3.85" +version = "0.3.91" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8c942ebf8e95485ca0d52d97da7c5a2c387d0e7f0ba4c35e93bfcaee045955b3" +checksum = "b49715b7073f385ba4bc528e5747d02e66cb39c6146efb66b781f131f0fb399c" dependencies = [ "once_cell", "wasm-bindgen", @@ -474,6 +618,25 @@ version = "0.2.182" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6800badb6cb2082ffd7b6a67e6125bb39f18782f793520caee8cb8846be06112" +[[package]] +name = "libsqlite3-sys" +version = "0.35.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "133c182a6a2c87864fe97778797e46c7e999672690dc9fa3ee8e241aa4a9c13f" +dependencies = [ + "pkg-config", + "vcpkg", +] + +[[package]] +name = "lock_api" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" +dependencies = [ + "scopeguard", +] + [[package]] name = "log" version = "0.4.29" @@ -509,6 +672,12 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "num-conv" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf97ec579c3c42f953ef76dbf8d55ac91fb219dde70e49aa4a6b7d74e9919050" + [[package]] name = "oauth-api-rust" version = "0.1.0" @@ -518,6 +687,7 @@ dependencies = [ "axum", "axum-extra", "base64", + "diesel", "form_urlencoded", "http-body-util", "serde", @@ -533,6 +703,29 @@ version = "1.21.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" +[[package]] +name = "parking_lot" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-link", +] + [[package]] name = "password-hash" version = "0.5.0" @@ -562,6 +755,18 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" +[[package]] +name = "pkg-config" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" + +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + [[package]] name = "prettyplease" version = "0.2.37" @@ -596,12 +801,32 @@ version = "5.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" +[[package]] +name = "r2d2" +version = "0.8.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51de85fb3fb6524929c8a2eb85e6b6d363de4e8c48f9e2c2eac4944abc181c93" +dependencies = [ + "log", + "parking_lot", + "scheduled-thread-pool", +] + [[package]] name = "rand_core" version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +[[package]] +name = "redox_syscall" +version = "0.5.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" +dependencies = [ + "bitflags", +] + [[package]] name = "regex" version = "1.12.3" @@ -631,6 +856,16 @@ version = "0.8.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a96887878f22d7bad8a3b6dc5b7440e0ada9a245242924394987b21cf2210a4c" +[[package]] +name = "rsqlite-vfs" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8a1f2315036ef6b1fbacd1972e8ee7688030b0a2121edfc2a6550febd41574d" +dependencies = [ + "hashbrown 0.16.1", + "thiserror", +] + [[package]] name = "rustversion" version = "1.0.22" @@ -652,6 +887,21 @@ dependencies = [ "winapi-util", ] +[[package]] +name = "scheduled-thread-pool" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3cbc66816425a074528352f5789333ecff06ca41b36b0b0efdfbb29edc391a19" +dependencies = [ + "parking_lot", +] + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + [[package]] name = "semver" version = "1.0.27" @@ -735,6 +985,12 @@ dependencies = [ "digest", ] +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + [[package]] name = "signal-hook-registry" version = "1.4.8" @@ -761,6 +1017,24 @@ dependencies = [ "windows-sys 0.60.2", ] +[[package]] +name = "sqlite-wasm-rs" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f4206ed3a67690b9c29b77d728f6acc3ce78f16bf846d83c94f76400320181b" +dependencies = [ + "cc", + "js-sys", + "rsqlite-vfs", + "wasm-bindgen", +] + +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + [[package]] name = "subtle" version = "2.6.1" @@ -784,6 +1058,57 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" +[[package]] +name = "thiserror" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "time" +version = "0.3.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "743bd48c283afc0388f9b8827b976905fb217ad9e647fae3a379a9283c4def2c" +dependencies = [ + "deranged", + "itoa", + "num-conv", + "powerfmt", + "serde_core", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7694e1cfe791f8d31026952abf09c69ca6f6fa4e1a1229e18988f06a04a12dca" + +[[package]] +name = "time-macros" +version = "0.2.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e70e4c5a0e0a8a4823ad65dfe1a6930e4f4d756dcd9dd7939022b5e8c501215" +dependencies = [ + "num-conv", + "time-core", +] + [[package]] name = "tokio" version = "1.49.0" @@ -888,6 +1213,12 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "vcpkg" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" + [[package]] name = "version_check" version = "0.9.5" @@ -930,9 +1261,9 @@ dependencies = [ [[package]] name = "wasm-bindgen" -version = "0.2.108" +version = "0.2.114" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "64024a30ec1e37399cf85a7ffefebdb72205ca1c972291c51512360d90bd8566" +checksum = "6532f9a5c1ece3798cb1c2cfdba640b9b3ba884f5db45973a6f442510a87d38e" dependencies = [ "cfg-if", "once_cell", @@ -943,9 +1274,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.108" +version = "0.2.114" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "008b239d9c740232e71bd39e8ef6429d27097518b6b30bdf9086833bd5b6d608" +checksum = "18a2d50fcf105fb33bb15f00e7a77b772945a2ee45dcf454961fd843e74c18e6" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -953,9 +1284,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.108" +version = "0.2.114" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5256bae2d58f54820e6490f9839c49780dff84c65aeab9e772f15d5f0e913a55" +checksum = "03ce4caeaac547cdf713d280eda22a730824dd11e6b8c3ca9e42247b25c631e3" dependencies = [ "bumpalo", "proc-macro2", @@ -966,9 +1297,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-shared" -version = "0.2.108" +version = "0.2.114" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1f01b580c9ac74c8d8f0c0e4afb04eeef2acf145458e52c03845ee9cd23e3d12" +checksum = "75a326b8c223ee17883a4251907455a2431acc2791c98c26279376490c378c16" dependencies = [ "unicode-ident", ] diff --git a/Cargo.toml b/Cargo.toml index a1bfe6f..14ac476 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -12,6 +12,7 @@ tokio = { version = "1.49.0", features = ["macros", "rt-multi-thread", "signal"] uuid = { version = "1.20.0", features = ["v4", "serde"] } form_urlencoded = "1.2.2" tower = "0.5.3" +diesel = { version = "2.3.6", features = ["sqlite", "returning_clauses_for_sqlite_3_35", "uuid", "r2d2"] } [dev-dependencies] assertables = "9.8.6" diff --git a/src/token/mod.rs b/src/token/mod.rs index 84ae2ff..26cae8e 100644 --- a/src/token/mod.rs +++ b/src/token/mod.rs @@ -1,4 +1,5 @@ pub mod repository; +mod schema; use serde::Serialize; use uuid::Uuid; diff --git a/src/token/repository.rs b/src/token/repository.rs index 602a4dc..a022674 100644 --- a/src/token/repository.rs +++ b/src/token/repository.rs @@ -1,9 +1,15 @@ use crate::token::Token; use std::collections::HashMap; +use std::io; +use std::marker::PhantomData; use std::sync::{Arc, Mutex}; +use diesel::r2d2::{ConnectionManager, Pool, R2D2Connection}; +use diesel::{QueryDsl, RunQueryDsl, SqliteConnection}; +use diesel::query_builder::AsQuery; use uuid::Uuid; +use crate::token::schema::access_tokens::dsl::access_tokens; -pub trait TokenRepository: Send + Sync + Clone { +pub trait TokenRepository: Clone + Send + Sync { fn get_token(&self, id: Uuid) -> Option; fn save_token(&self, token: &T); } @@ -31,3 +37,41 @@ where self.map.lock().unwrap().insert(token.id(), token.clone()); } } + +#[derive(Clone)] +pub struct DieselTokenRepository where C: R2D2Connection + 'static { + _token_type: PhantomData, + pool: Pool>, +} +impl DieselTokenRepository { + pub fn new>(database_url: S) -> DieselTokenRepository { + DieselTokenRepository { + _token_type: PhantomData, + pool: Pool::builder() + .test_on_check_out(true) + .build(ConnectionManager::::new(database_url)) + .expect("Could not build connection pool."), + } + } +} + +impl TokenRepository for DieselTokenRepository +where + T: Token + Clone + Send + Sync, + C: R2D2Connection + Clone +{ + fn get_token(&self, token: Uuid) -> Option { + use super::schema::access_tokens::dsl::*; + + // self.pool.get().map(|conn| { + // let token = token.to_string(); + // access_tokens.find(token).single_value() + // }) + + None + } + + fn save_token(&self, token: &T) { + todo!() + } +} diff --git a/src/token/schema.rs b/src/token/schema.rs new file mode 100644 index 0000000..34d6f79 --- /dev/null +++ b/src/token/schema.rs @@ -0,0 +1,8 @@ +diesel::table! { + access_tokens (id) { + id -> Text, // TODO - Convert to UUID via DB converters + client_id -> Text, + created_at -> Timestamp, + updated_at -> Timestamp, + } +} From 430debd76f0a55833e6b5b5e4c468427da307469 Mon Sep 17 00:00:00 2001 From: James Bacon Date: Tue, 28 Apr 2026 13:26:59 +0100 Subject: [PATCH 02/19] Access Token now has SQLX and Diesel attempts Still pondering what fits better to the data models --- .gitignore | 2 + Cargo.lock | 1385 ++++++++++++++++- Cargo.toml | 8 +- README.md | 12 +- .../down.sql | 1 + .../up.sql | 14 + src/client/client_principal.rs | 6 +- src/client/configuration.rs | 3 +- src/client/secret.rs | 1 + src/graceful_shutdown.rs | 2 +- src/main.rs | 37 +- src/scope/mod.rs | 2 +- src/token/mod.rs | 20 +- src/token/repository.rs | 151 +- src/token/schema.rs | 9 +- src/token_exchange/grant/password.rs | 36 +- src/token_exchange/response.rs | 5 +- src/token_exchange/route.rs | 12 +- src/token_introspection/mod.rs | 2 + src/token_introspection/request.rs | 46 + src/token_introspection/response.rs | 114 ++ src/token_introspection/route.rs | 69 +- src/util/mod.rs | 1 + src/util/uuid_wrapper.rs | 56 + src/util/value_struct.rs | 17 +- 25 files changed, 1849 insertions(+), 162 deletions(-) create mode 100644 migrations/access_tokens/20260428095532_create_access_tokens/down.sql create mode 100644 migrations/access_tokens/20260428095532_create_access_tokens/up.sql create mode 100644 src/token_introspection/request.rs create mode 100644 src/token_introspection/response.rs create mode 100644 src/util/uuid_wrapper.rs diff --git a/.gitignore b/.gitignore index ea8c4bf..bd787a3 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,3 @@ /target + +.idea \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock index 0669ef8..56bdd82 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -11,11 +11,26 @@ dependencies = [ "memchr", ] +[[package]] +name = "allocator-api2" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" + +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + [[package]] name = "anyhow" -version = "1.0.101" +version = "1.0.102" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f0e0fee31ef5ed1ba1316088939cea399010ed7731dba877ed44aeb407a75ea" +checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" [[package]] name = "argon2" @@ -39,12 +54,27 @@ dependencies = [ "walkdir", ] +[[package]] +name = "atoi" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f28d99ec8bfea296261ca1af174f24225171fea9664ba9003cbebee704810528" +dependencies = [ + "num-traits", +] + [[package]] name = "atomic-waker" version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" +[[package]] +name = "autocfg" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" + [[package]] name = "axum" version = "0.8.8" @@ -148,6 +178,9 @@ name = "bitflags" version = "2.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af" +dependencies = [ + "serde_core", +] [[package]] name = "blake2" @@ -173,6 +206,12 @@ version = "3.19.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5dd9dc738b7a8311c7ade152424974d8115f2cdad61e8dab8dac9f2362298510" +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + [[package]] name = "bytes" version = "1.11.1" @@ -181,9 +220,9 @@ checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" [[package]] name = "cc" -version = "1.2.55" +version = "1.2.61" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "47b26a0954ae34af09b50f0de26458fa95369a0d478d8236d3f93082b219bd29" +checksum = "d16d90359e986641506914ba71350897565610e87ce0ad9e6f28569db3dd5c6d" dependencies = [ "find-msvc-tools", "shlex", @@ -195,6 +234,41 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" +[[package]] +name = "chrono" +version = "0.4.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c673075a2e0e5f4a1dde27ce9dee1ea4558c7ffe648f576438a20ca1d2acc4b0" +dependencies = [ + "iana-time-zone", + "js-sys", + "num-traits", + "serde", + "wasm-bindgen", + "windows-link", +] + +[[package]] +name = "concurrent-queue" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "const-oid" +version = "0.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" + +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + [[package]] name = "cpufeatures" version = "0.2.17" @@ -204,6 +278,36 @@ dependencies = [ "libc", ] +[[package]] +name = "crc" +version = "3.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5eb8a2a1cd12ab0d987a5d5e825195d372001a4094a0376319d5a0ad71c1ba0d" +dependencies = [ + "crc-catalog", +] + +[[package]] +name = "crc-catalog" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "217698eaf96b4a3f0bc4f3662aaa55bdf913cd54d7204591faa790070c6d0853" + +[[package]] +name = "crossbeam-queue" +version = "0.3.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f58bbc28f91df819d0aa2a2c00cd19754769c2fad90579b3592b1c9ba7a3115" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" + [[package]] name = "crypto-common" version = "0.1.7" @@ -249,6 +353,17 @@ dependencies = [ "syn", ] +[[package]] +name = "der" +version = "0.7.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7c1832837b905bbfb5101e07cc24c8deddf52f93225eee6ead5f4d63d53ddcb" +dependencies = [ + "const-oid", + "pem-rfc7468", + "zeroize", +] + [[package]] name = "deranged" version = "0.5.8" @@ -260,24 +375,24 @@ dependencies = [ [[package]] name = "diesel" -version = "2.3.6" +version = "2.3.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d9b6c2fc184a6fb6ebcf5f9a5e3bbfa84d8fd268cdfcce4ed508979a6259494d" +checksum = "78df0e4e8c596662edb07fbfbb7f23769cca35049827df5f909084d956b6aeaf" dependencies = [ + "chrono", "diesel_derives", "downcast-rs", "libsqlite3-sys", "r2d2", "sqlite-wasm-rs", "time", - "uuid", ] [[package]] name = "diesel_derives" -version = "2.3.7" +version = "2.3.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "47618bf0fac06bb670c036e48404c26a865e6a71af4114dfd97dfe89936e404e" +checksum = "0b79402bd1cfb25b65650f0f4901d0e79c095729e2139c8ab779d025968c7099" dependencies = [ "diesel_table_macro_syntax", "dsl_auto_type", @@ -286,6 +401,17 @@ dependencies = [ "syn", ] +[[package]] +name = "diesel_migrations" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28d0f4a98124ba6d4ca75da535f65984badec16a003b6e2f94a01e31a79490b8" +dependencies = [ + "diesel", + "migrations_internals", + "migrations_macros", +] + [[package]] name = "diesel_table_macro_syntax" version = "0.3.0" @@ -302,10 +428,28 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" dependencies = [ "block-buffer", + "const-oid", "crypto-common", "subtle", ] +[[package]] +name = "displaydoc" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "dotenvy" +version = "0.15.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1aaf95b3e5c8f23aa320147307562d361db0ae0d51242340f558153b4eb2439b" + [[package]] name = "downcast-rs" version = "2.0.2" @@ -331,6 +475,9 @@ name = "either" version = "1.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" +dependencies = [ + "serde", +] [[package]] name = "equivalent" @@ -348,12 +495,45 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "etcetera" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "136d1b5283a1ab77bd9257427ffd09d8667ced0570b6f938942bc7568ed5b943" +dependencies = [ + "cfg-if", + "home", + "windows-sys 0.48.0", +] + +[[package]] +name = "event-listener" +version = "5.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13b66accf52311f30a0db42147dadea9850cb48cd070028831ae5f5d4b856ab" +dependencies = [ + "concurrent-queue", + "parking", + "pin-project-lite", +] + [[package]] name = "find-msvc-tools" version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" +[[package]] +name = "flume" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da0e4dd2a88388a1f4ccc7c9ce104604dab68d9f408dc34cd45823d5a9069095" +dependencies = [ + "futures-core", + "futures-sink", + "spin", +] + [[package]] name = "fnv" version = "1.0.7" @@ -388,6 +568,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" dependencies = [ "futures-core", + "futures-sink", ] [[package]] @@ -396,6 +577,40 @@ version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" +[[package]] +name = "futures-executor" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-intrusive" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d930c203dd0b6ff06e0201a4a2fe9149b43c684fd4420555b26d21b1a02956f" +dependencies = [ + "futures-core", + "lock_api", + "parking_lot", +] + +[[package]] +name = "futures-io" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cecba35d7ad927e23624b22ad55235f2239cfa44fd10428eecbeba6d6a717718" + +[[package]] +name = "futures-sink" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c39754e157331b013978ec91992bde1ac089843443c49cbc7f46150b0fad0893" + [[package]] name = "futures-task" version = "0.3.31" @@ -409,9 +624,13 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" dependencies = [ "futures-core", + "futures-io", + "futures-sink", "futures-task", + "memchr", "pin-project-lite", "pin-utils", + "slab", ] [[package]] @@ -424,6 +643,17 @@ dependencies = [ "version_check", ] +[[package]] +name = "getrandom" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + [[package]] name = "getrandom" version = "0.4.1" @@ -443,6 +673,8 @@ version = "0.15.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" dependencies = [ + "allocator-api2", + "equivalent", "foldhash 0.1.5", ] @@ -455,6 +687,15 @@ dependencies = [ "foldhash 0.2.0", ] +[[package]] +name = "hashlink" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7382cf6263419f2d8df38c55d7da83da5c18aef87fc7a7fc1fb1e344edfe14c1" +dependencies = [ + "hashbrown 0.15.5", +] + [[package]] name = "headers" version = "0.4.1" @@ -485,6 +726,39 @@ version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] +name = "hkdf" +version = "0.12.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b5f8eb2ad728638ea2c7d47a21db23b7b58a72ed6a38256b8a1849f15fbbdf7" +dependencies = [ + "hmac", +] + +[[package]] +name = "hmac" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" +dependencies = [ + "digest", +] + +[[package]] +name = "home" +version = "0.5.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc627f471c528ff0c4a49e1d5e60450c8f6461dd6d10ba9dcd3a61d3dff7728d" +dependencies = [ + "windows-sys 0.61.2", +] + [[package]] name = "http" version = "1.4.0" @@ -566,6 +840,112 @@ dependencies = [ "tower-service", ] +[[package]] +name = "iana-time-zone" +version = "0.1.65" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e31bc9ad994ba00e440a8aa5c9ef0ec67d5cb5e5cb0cc7f8b744a35b389cc470" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "log", + "wasm-bindgen", + "windows-core", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + +[[package]] +name = "icu_collections" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2984d1cd16c883d7935b9e07e44071dca8d917fd52ecc02c04d5fa0b5a3f191c" +dependencies = [ + "displaydoc", + "potential_utf", + "utf8_iter", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locale_core" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92219b62b3e2b4d88ac5119f8904c10f8f61bf7e95b640d25ba3075e6cac2c29" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_normalizer" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c56e5ee99d6e3d33bd91c5d85458b6005a22140021cc324cea84dd0e72cff3b4" +dependencies = [ + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da3be0ae77ea334f4da67c12f149704f19f81d1adf7c51cf482943e84a2bad38" + +[[package]] +name = "icu_properties" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bee3b67d0ea5c2cca5003417989af8996f8604e34fb9ddf96208a033901e70de" +dependencies = [ + "icu_collections", + "icu_locale_core", + "icu_properties_data", + "icu_provider", + "zerotrie", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e2bbb201e0c04f7b4b3e14382af113e17ba4f63e2c9d2ee626b720cbce54a14" + +[[package]] +name = "icu_provider" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "139c4cf31c8b5f33d7e199446eff9c1e02decfc2f0eec2c8d71f65befa45b421" +dependencies = [ + "displaydoc", + "icu_locale_core", + "writeable", + "yoke", + "zerofrom", + "zerotrie", + "zerovec", +] + [[package]] name = "id-arena" version = "2.3.0" @@ -578,6 +958,27 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" +[[package]] +name = "idna" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" +dependencies = [ + "idna_adapter", + "smallvec", + "utf8_iter", +] + +[[package]] +name = "idna_adapter" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3acae9609540aa318d1bc588455225fb2085b9ed0c4f6bd0d9d5bcd86f1a0344" +dependencies = [ + "icu_normalizer", + "icu_properties", +] + [[package]] name = "indexmap" version = "2.13.0" @@ -606,6 +1007,15 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" +dependencies = [ + "spin", +] + [[package]] name = "leb128fmt" version = "0.1.0" @@ -618,16 +1028,41 @@ version = "0.2.182" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6800badb6cb2082ffd7b6a67e6125bb39f18782f793520caee8cb8846be06112" +[[package]] +name = "libm" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981" + +[[package]] +name = "libredox" +version = "0.1.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e02f3bb43d335493c96bf3fd3a321600bf6bd07ed34bc64118e9293bdffea46c" +dependencies = [ + "bitflags", + "libc", + "plain", + "redox_syscall 0.7.4", +] + [[package]] name = "libsqlite3-sys" -version = "0.35.0" +version = "0.30.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "133c182a6a2c87864fe97778797e46c7e999672690dc9fa3ee8e241aa4a9c13f" +checksum = "2e99fb7a497b1e3339bc746195567ed8d3e24945ecd636e3619d20b9de9e9149" dependencies = [ + "cc", "pkg-config", "vcpkg", ] +[[package]] +name = "litemap" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92daf443525c4cce67b150400bc2316076100ce0b3686209eb8cf3c31612e6f0" + [[package]] name = "lock_api" version = "0.4.14" @@ -650,13 +1085,44 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "47e1ffaa40ddd1f3ed91f717a33c8c0ee23fff369e3aa8772b9605cc1d22f4c3" [[package]] -name = "memchr" -version = "2.8.0" +name = "md-5" +version = "0.10.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" - -[[package]] -name = "mime" +checksum = "d89e7ee0cfbedfc4da3340218492196241d89eefb6dab27de5df917a6d2e78cf" +dependencies = [ + "cfg-if", + "digest", +] + +[[package]] +name = "memchr" +version = "2.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" + +[[package]] +name = "migrations_internals" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "36c791ecdf977c99f45f23280405d7723727470f6689a5e6dbf513ac547ae10d" +dependencies = [ + "serde", + "toml", +] + +[[package]] +name = "migrations_macros" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "36fc5ac76be324cfd2d3f2cf0fdf5d5d3c4f14ed8aaebadb09e304ba42282703" +dependencies = [ + "migrations_internals", + "proc-macro2", + "quote", +] + +[[package]] +name = "mime" version = "0.3.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" @@ -672,28 +1138,80 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "num-bigint-dig" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e661dda6640fad38e827a6d4a310ff4763082116fe217f279885c97f511bb0b7" +dependencies = [ + "lazy_static", + "libm", + "num-integer", + "num-iter", + "num-traits", + "rand", + "smallvec", + "zeroize", +] + [[package]] name = "num-conv" -version = "0.2.0" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf97ec579c3c42f953ef76dbf8d55ac91fb219dde70e49aa4a6b7d74e9919050" +checksum = "c6673768db2d862beb9b39a78fdcb1a69439615d5794a1be50caa9bc92c81967" + +[[package]] +name = "num-integer" +version = "0.1.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-iter" +version = "0.1.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1429034a0490724d0075ebb2bc9e875d6503c3cf69e235a8941aa757d83ef5bf" +dependencies = [ + "autocfg", + "num-integer", + "num-traits", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", + "libm", +] [[package]] name = "oauth-api-rust" version = "0.1.0" dependencies = [ + "anyhow", "argon2", "assertables", "axum", "axum-extra", "base64", + "chrono", "diesel", + "diesel_migrations", "form_urlencoded", "http-body-util", "serde", "serde_json", + "sqlx", + "thiserror", "tokio", "tower", + "trait-variant", "uuid", ] @@ -703,6 +1221,12 @@ version = "1.21.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" +[[package]] +name = "parking" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba" + [[package]] name = "parking_lot" version = "0.12.5" @@ -721,7 +1245,7 @@ checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" dependencies = [ "cfg-if", "libc", - "redox_syscall", + "redox_syscall 0.5.18", "smallvec", "windows-link", ] @@ -737,6 +1261,15 @@ dependencies = [ "subtle", ] +[[package]] +name = "pem-rfc7468" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88b39c9bfcfc231068454382784bb460aae594343fb030d46e9f50a645418412" +dependencies = [ + "base64ct", +] + [[package]] name = "percent-encoding" version = "2.3.2" @@ -755,18 +1288,63 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" +[[package]] +name = "pkcs1" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8ffb9f10fa047879315e6625af03c164b16962a5368d724ed16323b68ace47f" +dependencies = [ + "der", + "pkcs8", + "spki", +] + +[[package]] +name = "pkcs8" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7" +dependencies = [ + "der", + "spki", +] + [[package]] name = "pkg-config" version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" +[[package]] +name = "plain" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4596b6d070b27117e987119b4dac604f3c58cfb0b191112e24771b2faeac1a6" + +[[package]] +name = "potential_utf" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0103b1cef7ec0cf76490e969665504990193874ea05c85ff9bab8b911d0a0564" +dependencies = [ + "zerovec", +] + [[package]] name = "powerfmt" version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" +[[package]] +name = "ppv-lite86" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +dependencies = [ + "zerocopy", +] + [[package]] name = "prettyplease" version = "0.2.37" @@ -812,11 +1390,35 @@ dependencies = [ "scheduled-thread-pool", ] +[[package]] +name = "rand" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ca0ecfa931c29007047d1bc58e623ab12e5590e8c7cc53200d5202b69266d8a" +dependencies = [ + "libc", + "rand_chacha", + "rand_core", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core", +] + [[package]] name = "rand_core" version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom 0.2.17", +] [[package]] name = "redox_syscall" @@ -827,6 +1429,15 @@ dependencies = [ "bitflags", ] +[[package]] +name = "redox_syscall" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f450ad9c3b1da563fb6948a8e0fb0fb9269711c9c73d9ea1de5058c79c8d643a" +dependencies = [ + "bitflags", +] + [[package]] name = "regex" version = "1.12.3" @@ -856,6 +1467,26 @@ version = "0.8.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a96887878f22d7bad8a3b6dc5b7440e0ada9a245242924394987b21cf2210a4c" +[[package]] +name = "rsa" +version = "0.9.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8573f03f5883dcaebdfcf4725caa1ecb9c15b2ef50c43a07b816e06799bb12d" +dependencies = [ + "const-oid", + "digest", + "num-bigint-dig", + "num-integer", + "num-traits", + "pkcs1", + "pkcs8", + "rand_core", + "signature", + "spki", + "subtle", + "zeroize", +] + [[package]] name = "rsqlite-vfs" version = "0.1.0" @@ -962,6 +1593,15 @@ dependencies = [ "serde_core", ] +[[package]] +name = "serde_spanned" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6662b5879511e06e8999a8a235d848113e942c9124f211511b16466ee2995f26" +dependencies = [ + "serde_core", +] + [[package]] name = "serde_urlencoded" version = "0.7.1" @@ -985,6 +1625,17 @@ dependencies = [ "digest", ] +[[package]] +name = "sha2" +version = "0.10.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + [[package]] name = "shlex" version = "1.3.0" @@ -1001,32 +1652,283 @@ dependencies = [ "libc", ] +[[package]] +name = "signature" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" +dependencies = [ + "digest", + "rand_core", +] + +[[package]] +name = "slab" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" + [[package]] name = "smallvec" version = "1.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" +dependencies = [ + "serde", +] + +[[package]] +name = "socket2" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "86f4aa3ad99f2088c990dfa82d367e19cb29268ed67c574d10d0a4bfe71f07e0" +dependencies = [ + "libc", + "windows-sys 0.60.2", +] + +[[package]] +name = "spin" +version = "0.9.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" +dependencies = [ + "lock_api", +] + +[[package]] +name = "spki" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d" +dependencies = [ + "base64ct", + "der", +] + +[[package]] +name = "sqlite-wasm-rs" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b2c760607300407ddeaee518acf28c795661b7108c75421303dbefb237d3a36" +dependencies = [ + "cc", + "js-sys", + "rsqlite-vfs", + "wasm-bindgen", +] + +[[package]] +name = "sqlx" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fefb893899429669dcdd979aff487bd78f4064e5e7907e4269081e0ef7d97dc" +dependencies = [ + "sqlx-core", + "sqlx-macros", + "sqlx-mysql", + "sqlx-postgres", + "sqlx-sqlite", +] + +[[package]] +name = "sqlx-core" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee6798b1838b6a0f69c007c133b8df5866302197e404e8b6ee8ed3e3a5e68dc6" +dependencies = [ + "base64", + "bytes", + "crc", + "crossbeam-queue", + "either", + "event-listener", + "futures-core", + "futures-intrusive", + "futures-io", + "futures-util", + "hashbrown 0.15.5", + "hashlink", + "indexmap", + "log", + "memchr", + "once_cell", + "percent-encoding", + "serde", + "serde_json", + "sha2", + "smallvec", + "thiserror", + "time", + "tokio", + "tokio-stream", + "tracing", + "url", + "uuid", +] + +[[package]] +name = "sqlx-macros" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2d452988ccaacfbf5e0bdbc348fb91d7c8af5bee192173ac3636b5fb6e6715d" +dependencies = [ + "proc-macro2", + "quote", + "sqlx-core", + "sqlx-macros-core", + "syn", +] + +[[package]] +name = "sqlx-macros-core" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19a9c1841124ac5a61741f96e1d9e2ec77424bf323962dd894bdb93f37d5219b" +dependencies = [ + "dotenvy", + "either", + "heck", + "hex", + "once_cell", + "proc-macro2", + "quote", + "serde", + "serde_json", + "sha2", + "sqlx-core", + "sqlx-mysql", + "sqlx-postgres", + "sqlx-sqlite", + "syn", + "tokio", + "url", +] + +[[package]] +name = "sqlx-mysql" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa003f0038df784eb8fecbbac13affe3da23b45194bd57dba231c8f48199c526" +dependencies = [ + "atoi", + "base64", + "bitflags", + "byteorder", + "bytes", + "crc", + "digest", + "dotenvy", + "either", + "futures-channel", + "futures-core", + "futures-io", + "futures-util", + "generic-array", + "hex", + "hkdf", + "hmac", + "itoa", + "log", + "md-5", + "memchr", + "once_cell", + "percent-encoding", + "rand", + "rsa", + "serde", + "sha1", + "sha2", + "smallvec", + "sqlx-core", + "stringprep", + "thiserror", + "time", + "tracing", + "uuid", + "whoami", +] + +[[package]] +name = "sqlx-postgres" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db58fcd5a53cf07c184b154801ff91347e4c30d17a3562a635ff028ad5deda46" +dependencies = [ + "atoi", + "base64", + "bitflags", + "byteorder", + "crc", + "dotenvy", + "etcetera", + "futures-channel", + "futures-core", + "futures-util", + "hex", + "hkdf", + "hmac", + "home", + "itoa", + "log", + "md-5", + "memchr", + "once_cell", + "rand", + "serde", + "serde_json", + "sha2", + "smallvec", + "sqlx-core", + "stringprep", + "thiserror", + "time", + "tracing", + "uuid", + "whoami", +] + +[[package]] +name = "sqlx-sqlite" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2d12fe70b2c1b4401038055f90f151b78208de1f9f89a7dbfd41587a10c3eea" +dependencies = [ + "atoi", + "flume", + "futures-channel", + "futures-core", + "futures-executor", + "futures-intrusive", + "futures-util", + "libsqlite3-sys", + "log", + "percent-encoding", + "serde", + "serde_urlencoded", + "sqlx-core", + "thiserror", + "time", + "tracing", + "url", + "uuid", +] [[package]] -name = "socket2" -version = "0.6.2" +name = "stable_deref_trait" +version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "86f4aa3ad99f2088c990dfa82d367e19cb29268ed67c574d10d0a4bfe71f07e0" -dependencies = [ - "libc", - "windows-sys 0.60.2", -] +checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" [[package]] -name = "sqlite-wasm-rs" -version = "0.5.2" +name = "stringprep" +version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f4206ed3a67690b9c29b77d728f6acc3ce78f16bf846d83c94f76400320181b" +checksum = "7b4df3d392d81bd458a8a621b8bffbd2302a12ffe288a9d931670948749463b1" dependencies = [ - "cc", - "js-sys", - "rsqlite-vfs", - "wasm-bindgen", + "unicode-bidi", + "unicode-normalization", + "unicode-properties", ] [[package]] @@ -1058,6 +1960,17 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" +[[package]] +name = "synstructure" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "thiserror" version = "2.0.18" @@ -1109,12 +2022,38 @@ dependencies = [ "time-core", ] +[[package]] +name = "tinystr" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8323304221c2a851516f22236c5722a72eaa19749016521d6dff0824447d96d" +dependencies = [ + "displaydoc", + "zerovec", +] + +[[package]] +name = "tinyvec" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e61e67053d25a4e82c844e8424039d9745781b3fc4f32b8d55ed50f5f667ef3" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" + [[package]] name = "tokio" version = "1.50.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "27ad5e34374e03cfffefc301becb44e9dc3c17584f414349ebe29ed26661822d" dependencies = [ + "bytes", "libc", "mio", "pin-project-lite", @@ -1135,6 +2074,48 @@ dependencies = [ "syn", ] +[[package]] +name = "tokio-stream" +version = "0.1.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32da49809aab5c3bc678af03902d4ccddea2a87d028d86392a4b1560c6906c70" +dependencies = [ + "futures-core", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "toml" +version = "0.9.12+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf92845e79fc2e2def6a5d828f0801e29a2f8acc037becc5ab08595c7d5e9863" +dependencies = [ + "serde_core", + "serde_spanned", + "toml_datetime", + "toml_parser", + "winnow 0.7.15", +] + +[[package]] +name = "toml_datetime" +version = "0.7.5+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92e1cfed4a3038bc5a127e35a2d360f145e1f4b971b551a2ba5fd7aedf7e1347" +dependencies = [ + "serde_core", +] + +[[package]] +name = "toml_parser" +version = "1.1.2+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2abe9b86193656635d2411dc43050282ca48aa31c2451210f4202550afb7526" +dependencies = [ + "winnow 1.0.2", +] + [[package]] name = "tower" version = "0.5.3" @@ -1171,9 +2152,21 @@ checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" dependencies = [ "log", "pin-project-lite", + "tracing-attributes", "tracing-core", ] +[[package]] +name = "tracing-attributes" +version = "0.1.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "tracing-core" version = "0.1.36" @@ -1183,31 +2176,81 @@ dependencies = [ "once_cell", ] +[[package]] +name = "trait-variant" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70977707304198400eb4835a78f6a9f928bf41bba420deb8fdb175cd965d77a7" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "typenum" version = "1.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb" +[[package]] +name = "unicode-bidi" +version = "0.3.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c1cb5db39152898a79168971543b1cb5020dff7fe43c8dc468b0885f5e29df5" + [[package]] name = "unicode-ident" version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "537dd038a89878be9b64dd4bd1b260315c1bb94f4d784956b81e27a088d9a09e" +[[package]] +name = "unicode-normalization" +version = "0.1.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5fd4f6878c9cb28d874b009da9e8d183b5abc80117c40bbd187a1fde336be6e8" +dependencies = [ + "tinyvec", +] + +[[package]] +name = "unicode-properties" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7df058c713841ad818f1dc5d3fd88063241cc61f49f5fbea4b951e8cf5a8d71d" + [[package]] name = "unicode-xid" version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" +[[package]] +name = "url" +version = "2.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", + "serde", +] + +[[package]] +name = "utf8_iter" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" + [[package]] name = "uuid" version = "1.23.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5ac8b6f42ead25368cf5b098aeb3dc8a1a2c05a3eee8a9a1a68c640edbfc79d9" dependencies = [ - "getrandom", + "getrandom 0.4.1", "js-sys", "serde_core", "wasm-bindgen", @@ -1259,6 +2302,12 @@ dependencies = [ "wit-bindgen", ] +[[package]] +name = "wasite" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8dad83b4f25e74f184f64c43b150b91efe7647395b42289f38e50566d82855b" + [[package]] name = "wasm-bindgen" version = "0.2.114" @@ -1338,6 +2387,16 @@ dependencies = [ "semver", ] +[[package]] +name = "whoami" +version = "1.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d4a4db5077702ca3015d3d02d74974948aba2ad9e12ab7df718ee64ccd7e97d" +dependencies = [ + "libredox", + "wasite", +] + [[package]] name = "winapi-util" version = "0.1.11" @@ -1347,19 +2406,81 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "windows-core" +version = "0.62.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-link", + "windows-result", + "windows-strings", +] + +[[package]] +name = "windows-implement" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "windows-interface" +version = "0.59.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "windows-link" version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" +[[package]] +name = "windows-result" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-strings" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-sys" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" +dependencies = [ + "windows-targets 0.48.5", +] + [[package]] name = "windows-sys" version = "0.60.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" dependencies = [ - "windows-targets", + "windows-targets 0.53.5", ] [[package]] @@ -1371,6 +2492,21 @@ dependencies = [ "windows-link", ] +[[package]] +name = "windows-targets" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" +dependencies = [ + "windows_aarch64_gnullvm 0.48.5", + "windows_aarch64_msvc 0.48.5", + "windows_i686_gnu 0.48.5", + "windows_i686_msvc 0.48.5", + "windows_x86_64_gnu 0.48.5", + "windows_x86_64_gnullvm 0.48.5", + "windows_x86_64_msvc 0.48.5", +] + [[package]] name = "windows-targets" version = "0.53.5" @@ -1378,28 +2514,46 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" dependencies = [ "windows-link", - "windows_aarch64_gnullvm", - "windows_aarch64_msvc", - "windows_i686_gnu", + "windows_aarch64_gnullvm 0.53.1", + "windows_aarch64_msvc 0.53.1", + "windows_i686_gnu 0.53.1", "windows_i686_gnullvm", - "windows_i686_msvc", - "windows_x86_64_gnu", - "windows_x86_64_gnullvm", - "windows_x86_64_msvc", + "windows_i686_msvc 0.53.1", + "windows_x86_64_gnu 0.53.1", + "windows_x86_64_gnullvm 0.53.1", + "windows_x86_64_msvc 0.53.1", ] +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" + [[package]] name = "windows_aarch64_gnullvm" version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" +[[package]] +name = "windows_aarch64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" + [[package]] name = "windows_aarch64_msvc" version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" +[[package]] +name = "windows_i686_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" + [[package]] name = "windows_i686_gnu" version = "0.53.1" @@ -1412,30 +2566,66 @@ version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" +[[package]] +name = "windows_i686_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" + [[package]] name = "windows_i686_msvc" version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" +[[package]] +name = "windows_x86_64_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" + [[package]] name = "windows_x86_64_gnu" version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" + [[package]] name = "windows_x86_64_gnullvm" version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" +[[package]] +name = "windows_x86_64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" + [[package]] name = "windows_x86_64_msvc" version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" +[[package]] +name = "winnow" +version = "0.7.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df79d97927682d2fd8adb29682d1140b343be4ac0f08fd68b7765d9c059d3945" + +[[package]] +name = "winnow" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ee1708bef14716a11bae175f579062d4554d95be2c6829f518df847b7b3fdd0" + [[package]] name = "wit-bindgen" version = "0.51.0" @@ -1524,6 +2714,115 @@ dependencies = [ "wasmparser", ] +[[package]] +name = "writeable" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ffae5123b2d3fc086436f8834ae3ab053a283cfac8fe0a0b8eaae044768a4c4" + +[[package]] +name = "yoke" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "abe8c5fda708d9ca3df187cae8bfb9ceda00dd96231bed36e445a1a48e66f9ca" +dependencies = [ + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de844c262c8848816172cef550288e7dc6c7b7814b4ee56b3e1553f275f1858e" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] + +[[package]] +name = "zerocopy" +version = "0.8.48" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eed437bf9d6692032087e337407a86f04cd8d6a16a37199ed57949d415bd68e9" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.48" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70e3cd084b1788766f53af483dd21f93881ff30d7320490ec3ef7526d203bad4" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "zerofrom" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69faa1f2a1ea75661980b013019ed6687ed0e83d069bc1114e2cc74c6c04c4df" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11532158c46691caf0f2593ea8358fed6bbf68a0315e80aae9bd41fbade684a1" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] + +[[package]] +name = "zeroize" +version = "1.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" + +[[package]] +name = "zerotrie" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f9152d31db0792fa83f70fb2f83148effb5c1f5b8c7686c3459e361d9bc20bf" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", +] + +[[package]] +name = "zerovec" +version = "0.11.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90f911cbc359ab6af17377d242225f4d75119aec87ea711a880987b18cd7b239" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "625dc425cab0dca6dc3c3319506e6593dcb08a9f387ea3b284dbd52a92c40555" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "zmij" version = "1.0.21" diff --git a/Cargo.toml b/Cargo.toml index 7418ec3..5258431 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -12,7 +12,13 @@ tokio = { version = "1.50.0", features = ["macros", "rt-multi-thread", "signal"] uuid = { version = "1.23.0", features = ["v4", "serde"] } form_urlencoded = "1.2.2" tower = "0.5.3" -diesel = { version = "2.3.6", features = ["sqlite", "returning_clauses_for_sqlite_3_35", "uuid", "r2d2"] } +diesel = { version = "2.3.6", features = ["sqlite", "returning_clauses_for_sqlite_3_35", "r2d2", "chrono"] } +chrono = { version = "0.4.44", features = ["serde"] } +sqlx = { version = "0.8.6", features = [ "runtime-tokio", "sqlite", "uuid", "time", "derive", "macros" ] } +anyhow = "1.0.102" +thiserror = "2.0.18" +trait-variant = "0.1.2" +diesel_migrations = "2.3.2" [dev-dependencies] assertables = "9.8.6" diff --git a/README.md b/README.md index 511e412..1f4014c 100644 --- a/README.md +++ b/README.md @@ -63,7 +63,12 @@ cargo run Hit the token exchange endpoint with a password grant _(yeah its deprecated; but it's a quick lazy way to start)_. ```bash -curl -vvv -X POST -H 'Content-Type: application/x-www-form-urlencoded' -u 'aardvark:badger' -d 'grant_type=password&scope=basic&username=aardvark&password=P%4055w0rd' http://127.0.0.1:8080/token +curl -v -X POST -H 'Content-Type: application/x-www-form-urlencoded' -u 'aardvark:badger' -d 'grant_type=password&scope=basic&username=aardvark&password=P%4055w0rd' http://127.0.0.1:8080/token +``` + +Hit the token introspection endpoint with the token just issued. +```bash +curl -v -X POST -H 'Content-Type: application/x-www-form-urlencoded' -u 'aardvark:badger' -d 'token=483d83dd-92a2-4d73-817f-8fd0e7203cb3' http://127.0.0.1:8080/introspect ``` ## Reading materials @@ -73,3 +78,8 @@ curl -vvv -X POST -H 'Content-Type: application/x-www-form-urlencoded' -u 'aardv * https://docs.rs/axum/latest/axum/middleware/index.html * https://docs.rs/axum/latest/axum/error_handling/index.html * https://docs.rs/tower/latest/tower +* https://crates.io/crates/anyhow +* https://crates.io/crates/thiserror +* https://github.com/launchbadge/sqlx +* https://docs.diesel.rs/2.0.x/diesel/index.html +* https://obito.fr/posts/2022/12/use-uuid-in-sqlite-database-with-rust-diesel.rs/ diff --git a/migrations/access_tokens/20260428095532_create_access_tokens/down.sql b/migrations/access_tokens/20260428095532_create_access_tokens/down.sql new file mode 100644 index 0000000..6a8d853 --- /dev/null +++ b/migrations/access_tokens/20260428095532_create_access_tokens/down.sql @@ -0,0 +1 @@ +DROP TABLE access_tokens; \ No newline at end of file diff --git a/migrations/access_tokens/20260428095532_create_access_tokens/up.sql b/migrations/access_tokens/20260428095532_create_access_tokens/up.sql new file mode 100644 index 0000000..8490ba9 --- /dev/null +++ b/migrations/access_tokens/20260428095532_create_access_tokens/up.sql @@ -0,0 +1,14 @@ +CREATE TABLE access_tokens +( + id BLOB(16) PRIMARY KEY NOT NULL CHECK(LENGTH(id) = 16), + username VARCHAR(255) NOT NULL, + client_id VARCHAR(255) NOT NULL, + scopes VARCHAR(16) NOT NULL, -- TODO - Consider refactoring into a foreign reference + issued_at TIMESTAMP NOT NULL, + expires_at TIMESTAMP NOT NULL, + not_before TIMESTAMP NOT NULL +); + +CREATE INDEX access_tokens_username_idx ON access_tokens (username); +CREATE INDEX access_tokens_client_id_idx ON access_tokens (client_id); +CREATE INDEX access_tokens_expires_at_idx ON access_tokens (expires_at); diff --git a/src/client/client_principal.rs b/src/client/client_principal.rs index 02071b3..c4fa582 100644 --- a/src/client/client_principal.rs +++ b/src/client/client_principal.rs @@ -44,9 +44,9 @@ macro_rules! define_principal { impl $struct_name { - // pub fn id(&self) -> &crate::client::ClientId { - // &self.configuration.client_id - // } + pub fn id(&self) -> &crate::client::ClientId { + &self.configuration.client_id + } pub fn can_perform_action(&self, action: &crate::client::ClientAction) -> bool { self.configuration.allowed_actions.contains(action) diff --git a/src/client/configuration.rs b/src/client/configuration.rs index c62c4c1..e51413b 100644 --- a/src/client/configuration.rs +++ b/src/client/configuration.rs @@ -14,6 +14,7 @@ pub struct ClientConfiguration { pub allowed_grant_types: HashSet, } +// TODO - #[trait_variant::make(Send)] pub trait ClientConfigurationRepository: Send + Sync + Clone { fn find_by_id(&self, client_id: &ClientId) -> Option; fn find_by_client_id(&self, client_id: &str) -> Option; @@ -33,7 +34,7 @@ impl InMemoryClientConfigurationRepository { client_id: ClientId(String::from("aardvark")), client_type: ClientType::Confidential, redirect_uris: HashSet::from([]), - allowed_scopes: HashSet::from([Scope::Basic]), + allowed_scopes: HashSet::from([Scope::Basic, Scope::Read, Scope::Write]), allowed_actions: HashSet::from([ClientAction::Introspect]), allowed_grant_types: HashSet::from([GrantType::Password]), }), diff --git a/src/client/secret.rs b/src/client/secret.rs index 5a6ebc4..6c4cd60 100644 --- a/src/client/secret.rs +++ b/src/client/secret.rs @@ -11,6 +11,7 @@ pub struct ClientSecret { pub hashed_secret: String, } +// TODO - #[trait_variant::make(Send)] pub trait ClientSecretRepository: Send + Sync + Clone { fn find_by_id(&self, id: &Uuid) -> Option; fn find_all_by_client(&self, client_id: &ClientId) -> Vec; diff --git a/src/graceful_shutdown.rs b/src/graceful_shutdown.rs index 473b28c..4cbd803 100644 --- a/src/graceful_shutdown.rs +++ b/src/graceful_shutdown.rs @@ -1,5 +1,5 @@ #![allow( - // Allowed because we need to panic if we cannot register the signal handler at start up + // Allowed because we need to panic if we cannot register the signal handler at startup clippy::expect_used, )] diff --git a/src/main.rs b/src/main.rs index 7a1a273..8280fdd 100644 --- a/src/main.rs +++ b/src/main.rs @@ -14,17 +14,18 @@ mod graceful_shutdown; mod client; mod util; +use std::error::Error; use axum::{serve, Router}; -use std::io; use tokio::net::TcpListener; use client::authentication::ClientAuthenticationService; use client::configuration::InMemoryClientConfigurationRepository; use client::secret::InMemoryClientSecretRepository; -use token::AccessToken; -use token::repository::InMemoryTokenRepository; +use token::repository::DieselSqliteAccessTokenRepository; use token_exchange::TokenExchangeState; use token_introspection::TokenIntrospectionState; +use anyhow::{Context, Result}; + // TODO List: // - Token endpoint // - Client authentication @@ -42,13 +43,26 @@ use token_introspection::TokenIntrospectionState; // - Sessions [authenticate/authenticated] // - Access Log // - Database support +// - Error handling, including 500s #[tokio::main] -async fn main() -> io::Result<()> { +async fn main() -> Result<(), Box> { // TODO - Do we bother with services, or just continue with passing the repositories directly? - let access_token_repository = InMemoryTokenRepository::::new(); - let client_secret_repository = InMemoryClientSecretRepository::new(); - let client_configuration_repository = InMemoryClientConfigurationRepository::new(); + // let access_token_repository = InMemoryTokenRepository::::new(); + + // let access_token_repository = SqlxSqliteTokenRepository + // :: + // ::new("file:target/db/access_tokens.sqlite3) + // .await?; + + let access_token_repository = DieselSqliteAccessTokenRepository + ::new("file:target/db/access_tokens.sqlite3") + .await?; + + access_token_repository.run_diesel_migrations().await?; + + let client_secret_repository = InMemoryClientSecretRepository::new(); // "file:target/db/client_secrets.sqlite3" + let client_configuration_repository = InMemoryClientConfigurationRepository::new(); // "file:target/db/client_configurations.sqlite3" let client_authenticator = ClientAuthenticationService::new( client_secret_repository.clone(), @@ -67,10 +81,15 @@ async fn main() -> io::Result<()> { // TODO - Extract into configuration let tcp_listener = TcpListener::bind("127.0.0.1:8080") // Change :8080 to :0 for a random port number - .await?; + .await + .with_context(|| "Failed to bind to TCP listener")?; + + let local_address = tcp_listener + .local_addr() + .with_context(|| "Failed to get local tcp address")?; println!(); - println!("Listening on http://{}", tcp_listener.local_addr()?); + println!("Listening on http://{}", local_address); println!(); serve(tcp_listener, application) diff --git a/src/scope/mod.rs b/src/scope/mod.rs index 8949562..cff39c0 100644 --- a/src/scope/mod.rs +++ b/src/scope/mod.rs @@ -15,7 +15,7 @@ enum_with_from_str! { } } -#[derive(Eq, PartialEq)] +#[derive(Eq, PartialEq, Clone)] #[cfg_attr(test, derive(Debug))] pub struct Scopes(pub HashSet); diff --git a/src/token/mod.rs b/src/token/mod.rs index 26cae8e..89de559 100644 --- a/src/token/mod.rs +++ b/src/token/mod.rs @@ -2,10 +2,10 @@ pub mod repository; mod schema; use serde::Serialize; -use uuid::Uuid; +use crate::util::uuid_wrapper::UuidWrapper; -pub trait Token { - fn id(&self) -> Uuid; +pub trait Token: Send + Sync + Clone { + fn id(&self) -> UuidWrapper; } #[cfg_attr(test, derive(Debug))] @@ -17,12 +17,22 @@ pub enum TokenType { } #[derive(Serialize, Clone)] +#[derive(sqlx::FromRow)] +#[derive(diesel::Queryable, diesel::Selectable, diesel::Insertable)] +#[diesel(table_name = schema::access_tokens)] +#[diesel(check_for_backend(diesel::sqlite::Sqlite))] pub struct AccessToken { - pub id: Uuid + pub id: UuidWrapper, + pub username: String, // TODO - Use AuthenticatedUser + pub client_id: String, // TODO - Use ClientId + pub scopes: String, // TODO - Use Scopes + pub issued_at: chrono::NaiveDateTime, + pub expires_at: chrono::NaiveDateTime, + pub not_before: chrono::NaiveDateTime, } impl Token for AccessToken { - fn id(&self) -> Uuid { + fn id(&self) -> UuidWrapper { self.id } } diff --git a/src/token/repository.rs b/src/token/repository.rs index 428f503..15db981 100644 --- a/src/token/repository.rs +++ b/src/token/repository.rs @@ -1,78 +1,147 @@ -use crate::token::Token; +use crate::token::{AccessToken, Token}; use std::collections::HashMap; -use std::io; +use std::error::Error; use std::marker::PhantomData; use std::sync::{Arc, Mutex, MutexGuard}; -use diesel::r2d2::{ConnectionManager, Pool, R2D2Connection}; -use diesel::{QueryDsl, RunQueryDsl, SqliteConnection}; -use diesel::query_builder::AsQuery; -use uuid::Uuid; -use crate::token::schema::access_tokens::dsl::access_tokens; - -pub trait TokenRepository: Send + Sync + Clone { - fn get_token(&self, id: Uuid) -> Option; - fn save_token(&self, token: &T); +use anyhow::{Context, Result}; +use diesel::r2d2::ConnectionManager; +use diesel::{RunQueryDsl, SqliteConnection}; +use sqlx::{FromRow, Pool, Sqlite}; +use sqlx::sqlite::{SqlitePoolOptions, SqliteRow}; +use crate::util::uuid_wrapper::UuidWrapper; + +#[trait_variant::make(Send)] +pub trait TokenRepository: Sync + Clone { + async fn get_token(&self, id: UuidWrapper) -> Result>; + async fn save_token(&self, token: &T) -> Result<()>; } #[derive(Clone, Default)] pub struct InMemoryTokenRepository { - store: Arc>>, + store: Arc>>, } impl InMemoryTokenRepository { pub fn new() -> Self { Self { store: Arc::new(Mutex::new(HashMap::new())) } } - fn lock_store(&self) -> MutexGuard<'_, HashMap> { + fn lock_store(&self) -> MutexGuard<'_, HashMap> { self.store.lock().unwrap_or_else(|poisoned| poisoned.into_inner()) } } -impl TokenRepository for InMemoryTokenRepository +impl TokenRepository for InMemoryTokenRepository { - fn get_token(&self, id: Uuid) -> Option { - self.lock_store().get(&id).cloned() + async fn get_token(&self, id: UuidWrapper) -> Result> { + Ok(self.lock_store().get(&id).cloned()) } - fn save_token(&self, token: &T) { + async fn save_token(&self, token: &T) -> Result<()> { self.lock_store().insert(token.id(), token.clone()); + Ok(()) } } #[derive(Clone)] -pub struct DieselTokenRepository where C: R2D2Connection + 'static { - _token_type: PhantomData, - pool: Pool>, +pub struct DieselSqliteAccessTokenRepository { + pool: diesel::r2d2::Pool>, } -impl DieselTokenRepository { - pub fn new>(database_url: S) -> DieselTokenRepository { - DieselTokenRepository { - _token_type: PhantomData, - pool: Pool::builder() - .test_on_check_out(true) - .build(ConnectionManager::::new(database_url)) - .expect("Could not build connection pool."), - } + +impl DieselSqliteAccessTokenRepository { + pub async fn new(database_url: &str) -> Result { + + let manager = ConnectionManager::::new(database_url); + + let pool = diesel::r2d2::Pool::builder() + .test_on_check_out(true) + .build(manager) + .with_context(|| format!("Failed to create Diesel sqlite database pool: {}", database_url))?; + + Ok(Self { + pool + }) + } + pub async fn run_diesel_migrations(&self) -> Result<(), Box> { + use diesel_migrations::{embed_migrations, EmbeddedMigrations, MigrationHarness}; + const ACCESS_TOKEN_MIGRATIONS: EmbeddedMigrations = embed_migrations!("migrations/access_tokens"); + self.pool.get()?.run_pending_migrations(ACCESS_TOKEN_MIGRATIONS)?; + Ok(()) } } -impl TokenRepository for DieselTokenRepository -where - T: Token + Clone + Send + Sync, - C: R2D2Connection + Clone -{ - fn get_token(&self, token: Uuid) -> Option { +impl TokenRepository for DieselSqliteAccessTokenRepository { + + async fn get_token(&self, token: UuidWrapper) -> Result> { + use super::schema::access_tokens; + use super::schema::access_tokens::dsl::*; + use diesel::prelude::*; + + let connection = &mut self.pool.get() + .with_context(|| "Failed to get connection from pool")?; + + let result = access_tokens::table + .filter(id.eq(token)) + .first::(connection) + .optional() + .with_context(|| "Error querying access token database")?; + + Ok(result) + } + + async fn save_token(&self, token: &AccessToken) -> Result<()> { + use super::schema::access_tokens::dsl::*; + use diesel::dsl::insert_into; + + let connection = &mut self.pool.get() + .with_context(|| "Failed to get connection from pool")?; + + insert_into(access_tokens) + .values(token) + .execute(connection) + .with_context(|| "Error saving access token to database")?; + + Ok(()) + } +} + +#[derive(Clone)] +pub struct SqlxSqliteTokenRepository { + _token_type: PhantomData, + pool: Pool, +} + +impl SqlxSqliteTokenRepository { + pub async fn new(database_url: &str) -> Result> { + Ok( + Self { + _token_type: PhantomData, + pool: SqlitePoolOptions::new() + .min_connections(1) + .max_connections(5) + .connect(database_url) + .await + .with_context(|| + format!("Failed to create SQLX sqlite database pool: {}", database_url) + )? + } + ) + } +} + +impl FromRow<'r, SqliteRow>> TokenRepository for SqlxSqliteTokenRepository { + + async fn get_token(&self, token: UuidWrapper) -> Result> { - // self.pool.get().map(|conn| { - // let token = token.to_string(); - // access_tokens.find(token).single_value() - // }) + let result = sqlx::query_as::("SELECT * FROM access_tokens WHERE id = ?") + .bind(token) + .fetch_optional(&self.pool) + .await?; - None + Ok(result) } - fn save_token(&self, token: &T) { + async fn save_token(&self, token: &T) -> Result<()> { todo!() } } diff --git a/src/token/schema.rs b/src/token/schema.rs index 34d6f79..ed9bd1d 100644 --- a/src/token/schema.rs +++ b/src/token/schema.rs @@ -1,8 +1,11 @@ diesel::table! { access_tokens (id) { - id -> Text, // TODO - Convert to UUID via DB converters + id -> Binary, + username -> Text, client_id -> Text, - created_at -> Timestamp, - updated_at -> Timestamp, + scopes -> Text, + issued_at -> Timestamp, + expires_at -> Timestamp, + not_before -> Timestamp, } } diff --git a/src/token_exchange/grant/password.rs b/src/token_exchange/grant/password.rs index eb558b5..88895f3 100644 --- a/src/token_exchange/grant/password.rs +++ b/src/token_exchange/grant/password.rs @@ -1,4 +1,5 @@ use std::collections::HashMap; +use chrono::{Duration, Utc}; use serde::Deserialize; use ClientPrincipal::Confidential; use GrantType::Password; @@ -10,6 +11,9 @@ use crate::token_exchange::response::{ErrorType, TokenExchangeResponse}; use crate::token_exchange::route::TokenExchangeState; use crate::scope::Scopes; use crate::scope::parser::parse_scopes; +use crate::util::value_struct::ValueStruct; +use anyhow::Result; +use crate::util::uuid_wrapper::UuidWrapper; #[derive(Deserialize, Eq, PartialEq)] #[cfg_attr(test, derive(Debug))] @@ -23,28 +27,38 @@ pub struct PasswordGrantRequest { pub async fn handle_password_grant( state: TokenExchangeState, request: PasswordGrantRequest -) -> TokenExchangeResponse +) -> Result where A: TokenRepository, C: ClientAuthenticator, { // TODO - Implement it... + + let issued_at = Utc::now(); let access_token = AccessToken { - id: uuid::Uuid::new_v4(), + id: UuidWrapper::random(), + username: request.username, + client_id: request.principal.id().value().clone(), + scopes: request.scopes.clone().map(|s|s.0.iter().map(|scope| scope.to_string()).collect::>().join(" ")).unwrap_or_else(String::new), + issued_at: issued_at.naive_utc(), + expires_at: (issued_at + Duration::hours(2)).naive_utc(), + not_before: (issued_at - Duration::minutes(1)).naive_utc(), }; - state.access_token_repository.save_token(&access_token); + state.access_token_repository.save_token(&access_token).await?; - TokenExchangeResponse::Success { - access_token: access_token.id, - token_type: TokenType::Bearer, - expires_in: 7200, - refresh_token: Some(uuid::Uuid::new_v4()), - scope: request.scopes, - state: None, - } + Ok( + TokenExchangeResponse::Success { + access_token: access_token.id, + token_type: TokenType::Bearer, + expires_in: 7200, + refresh_token: Some(UuidWrapper::random()), + scope: request.scopes, + state: None, + } + ) } pub fn validate_password_grant(principal: ClientPrincipal, request: HashMap) -> Result { diff --git a/src/token_exchange/response.rs b/src/token_exchange/response.rs index ca0e53b..18c7719 100644 --- a/src/token_exchange/response.rs +++ b/src/token_exchange/response.rs @@ -1,6 +1,7 @@ use serde::Serialize; use crate::scope::Scopes; use crate::token::TokenType; +use crate::util::uuid_wrapper::UuidWrapper; #[cfg_attr(test, derive(Debug))] #[derive(Serialize, Eq, PartialEq)] @@ -10,7 +11,7 @@ pub enum TokenExchangeResponse { Success { // The access token issued by the authorization server. - access_token: uuid::Uuid, + access_token: UuidWrapper, // The type of the token issued as described in // https://www.rfc-editor.org/rfc/rfc6749#section-7.1 @@ -26,7 +27,7 @@ pub enum TokenExchangeResponse { // access tokens using the same authorization grant as described in // https://www.rfc-editor.org/rfc/rfc6749#section-6 #[serde(skip_serializing_if = "Option::is_none")] - refresh_token: Option, + refresh_token: Option, // OPTIONAL if identical to the scope requested by the client; otherwise, // REQUIRED. The scope of the access token as described by diff --git a/src/token_exchange/route.rs b/src/token_exchange/route.rs index f8661e7..28057de 100644 --- a/src/token_exchange/route.rs +++ b/src/token_exchange/route.rs @@ -1,8 +1,9 @@ +use anyhow::Result; use axum::extract::State; use axum::http::StatusCode; use axum::{middleware, Router}; use axum::routing::post; -use axum::response::Json; +use axum::response::{IntoResponse, Json}; use middleware::from_fn_with_state; use crate::client::authentication::ClientAuthenticator; use crate::client::middleware::require_client_authentication; @@ -33,11 +34,14 @@ pub struct TokenExchangeState, C: ClientAuthenti async fn token_exchange_handler, C: ClientAuthenticator>( State(state): State>, TokenExchangeForm(request): TokenExchangeForm, -) -> (StatusCode, Json) { +) -> Result<(StatusCode, Json), StatusCode> { let result = match request { TokenExchangeRequest::Password(password_grant_request) => { - handle_password_grant(state, password_grant_request).await + match handle_password_grant(state, password_grant_request).await { + Ok(response) => response, + Err(_) => Err(StatusCode::INTERNAL_SERVER_ERROR)?, // TODO - Add error logging + } }, }; @@ -46,7 +50,7 @@ async fn token_exchange_handler, C: ClientAuthen TokenExchangeResponse::Success { .. } => StatusCode::OK, }; - (status, Json(result)) + Ok((status, Json(result))) } #[cfg(test)] diff --git a/src/token_introspection/mod.rs b/src/token_introspection/mod.rs index 029a400..a5ff9f1 100644 --- a/src/token_introspection/mod.rs +++ b/src/token_introspection/mod.rs @@ -1,4 +1,6 @@ mod route; mod middleware; +mod request; +mod response; pub use route::*; diff --git a/src/token_introspection/request.rs b/src/token_introspection/request.rs new file mode 100644 index 0000000..d01f861 --- /dev/null +++ b/src/token_introspection/request.rs @@ -0,0 +1,46 @@ +use std::collections::HashMap; +use axum::extract::{FromRequest, Request}; +use axum::extract::rejection::FormRejection; +use axum::{Form, Json}; +use axum::http::StatusCode; +use axum::response::{IntoResponse, Response}; +use uuid::Uuid; +use crate::token_introspection::response::{ErrorType, TokenIntrospectionResponse}; + +pub struct TokenIntrospectionRequest { + pub(crate) token: Uuid, +} + +pub struct TokenIntrospectionForm(pub TokenIntrospectionRequest); + +impl FromRequest for TokenIntrospectionForm { + type Rejection = Response; + async fn from_request(req: Request, state: &S) -> Result { + match Form::>::from_request(req, state).await { + Err(rejection) => Err(handle_form_rejection(rejection)), + Ok(Form(request)) => match validate_request(request) { + Err(failure) => Err(handle_validation_failure(failure)), + Ok(valid) => Ok(valid), + } + } + } +} + +fn validate_request(request: HashMap) -> Result { + match request.get("token").map(|s| Uuid::parse_str(s)) { + None => Err(TokenIntrospectionResponse::missing_parameter("token")), + Some(Err(_)) => Err(TokenIntrospectionResponse::invalid_parameter("token")), + Some(Ok(token)) => Ok(TokenIntrospectionForm(TokenIntrospectionRequest { token })), + } +} + +fn handle_validation_failure(failure: TokenIntrospectionResponse) -> Response { + (StatusCode::BAD_REQUEST, Json(failure)).into_response() +} + +fn handle_form_rejection(rejection: FormRejection) -> Response { + (rejection.status(), Json(TokenIntrospectionResponse::Invalid { + error: ErrorType::InvalidRequest, + error_description: Some(rejection.body_text()), + })).into_response() +} \ No newline at end of file diff --git a/src/token_introspection/response.rs b/src/token_introspection/response.rs new file mode 100644 index 0000000..5e4afbd --- /dev/null +++ b/src/token_introspection/response.rs @@ -0,0 +1,114 @@ +use serde::Serialize; +use crate::client::ClientId; +use crate::token::TokenType; + +#[cfg_attr(test, derive(Debug))] +#[derive(Serialize)] +#[serde(untagged)] +pub enum TokenIntrospectionResponse { + Active { + /** + * Boolean indicator of whether the presented token is currently active. + * + * The specifics of a token's "active" state will vary depending on the implementation of + * the authorization server and the information it keeps about its tokens, but a "true" + * value return for the "active" property will generally indicate that a given token has + * been issued by this authorization server, has not been revoked by the resource owner, + * and is within its given time window of validity (e.g., after its issuance time and before + * its expiration time). + * + * See Section 4 for information on implementation of such checks [https://www.rfc-editor.org/rfc/rfc7662#section-4]. + */ + active: bool, + + /** + * A JSON string containing a space-separated list of scopes associated with this token, + * in the format described in Section 3.3 of OAuth 2.0 [https://www.rfc-editor.org/rfc/rfc6749#section-3.3]. + */ + #[serde(skip_serializing_if = "Option::is_none")] + scope: Option, // TODO - Use an internal type and setup serialization for it. + + /** + * Client identifier for the OAuth 2.0 client that requested this token. + */ + #[serde(skip_serializing_if = "Option::is_none")] + client_id: Option, + + /** + * Human-readable identifier for the resource owner who authorized this token. + */ + #[serde(skip_serializing_if = "Option::is_none")] + username: Option, // TODO - Use an internal value struct AuthenticatedUsername + + /** + * Type of the token as defined in Section 5.1 of OAuth 2.0 [https://www.rfc-editor.org/rfc/rfc6749#section-5.1]. + */ + #[serde(skip_serializing_if = "Option::is_none")] + token_type: Option, + + /** + * Integer timestamp, measured in the number of seconds since January 1 1970 UTC, + * indicating when this token will expire, as defined in JWT [https://www.rfc-editor.org/rfc/rfc7519] + */ + #[serde(rename = "exp", skip_serializing_if = "Option::is_none")] + expires_at: Option, + + /** + * Integer timestamp, measured in the number of seconds since January 1 1970 UTC, + * indicating when this token was originally issued, as defined in JWT [https://www.rfc-editor.org/rfc/rfc7519] + */ + #[serde(rename = "iat", skip_serializing_if = "Option::is_none")] + issued_at: Option, + + /** + * Integer timestamp, measured in the number of seconds since January 1 1970 UTC, + * indicating when this token is not to be used before, as defined in JWT [https://www.rfc-editor.org/rfc/rfc7519] + */ + #[serde(rename = "nbf", skip_serializing_if = "Option::is_none")] + not_before: Option, + + }, + Inactive { + /** + * Boolean indicator of whether the presented token is currently active. + */ + active: bool, + }, + Invalid { + /** + * A single ASCII error code from the defined list. + */ + error: ErrorType, + + /** + * Human-readable ASCII text providing additional information, used + * to assist the client developer in understanding the error that occurred. + */ + #[serde(skip_serializing_if = "Option::is_none")] + error_description: Option, + }, +} + +#[cfg_attr(test, derive(Debug))] +#[derive(Serialize)] +#[serde(rename_all = "snake_case")] +pub enum ErrorType { + InvalidRequest, + UnauthorizedClient, +} + +impl TokenIntrospectionResponse { + pub fn missing_parameter(parameter: &str) -> Self { + TokenIntrospectionResponse::Invalid { + error: ErrorType::InvalidRequest, + error_description: Some(format!("missing parameter: {parameter}")), + } + } + + pub fn invalid_parameter(parameter: &str) -> Self { + TokenIntrospectionResponse::Invalid { + error: ErrorType::InvalidRequest, + error_description: Some(format!("invalid parameter: {parameter}")), + } + } +} \ No newline at end of file diff --git a/src/token_introspection/route.rs b/src/token_introspection/route.rs index 1392bd8..9f042d8 100644 --- a/src/token_introspection/route.rs +++ b/src/token_introspection/route.rs @@ -1,28 +1,37 @@ -use axum::http::StatusCode; -use axum::{middleware, Extension, Json, Router}; -use axum::extract::State; -use axum::routing::post; -use middleware::from_fn_with_state; -use serde::Serialize; -use tower::ServiceBuilder; use crate::client::authentication::ClientAuthenticator; -use crate::client::{ClientAction, ConfidentialClient}; use crate::client::middleware::require_confidential_client_authentication; -use crate::token::AccessToken; +use crate::client::{ClientAction, ConfidentialClient}; use crate::token::repository::TokenRepository; +use crate::token::{AccessToken, TokenType}; use crate::token_introspection::middleware::require_confidential_client_action; +use crate::token_introspection::request::TokenIntrospectionForm; +use crate::token_introspection::response::TokenIntrospectionResponse; +use anyhow::Result; +use axum::extract::State; +use axum::http::StatusCode; +use axum::response::IntoResponse; +use axum::routing::post; +use axum::{middleware, Extension, Json, Router}; +use middleware::from_fn_with_state; +use tower::ServiceBuilder; pub fn route(state: TokenIntrospectionState) -> Router where A: TokenRepository + 'static, - C: ClientAuthenticator + 'static + C: ClientAuthenticator + 'static, { Router::new() .route("/introspect", post(token_introspection_handler)) .route_layer( ServiceBuilder::new() - .layer(from_fn_with_state(state.client_authenticator.clone(), require_confidential_client_authentication::)) - .layer(from_fn_with_state(ClientAction::Introspect, require_confidential_client_action)) + .layer(from_fn_with_state( + state.client_authenticator.clone(), + require_confidential_client_authentication::, + )) + .layer(from_fn_with_state( + ClientAction::Introspect, + require_confidential_client_action, + )), ) .with_state(state) } @@ -33,21 +42,35 @@ pub struct TokenIntrospectionState, C: ClientAut pub client_authenticator: C, } -async fn token_introspection_handler, C: ClientAuthenticator>( +async fn token_introspection_handler, C: ClientAuthenticator>( State(state): State>, - Extension(client) : Extension, -) -> (StatusCode, Json) { + Extension(client): Extension, + TokenIntrospectionForm(request): TokenIntrospectionForm, +) -> Result { // TODO - Validate request // TODO - Actually implement + // TODO - Check for expired + // TODO - Check for not valid yet - match state.access_token_repository.get_token(uuid::Uuid::new_v4()) { - Some(_) => (StatusCode::OK, Json(TokenIntrospectionResponse { active: true })), - None => (StatusCode::OK, Json(TokenIntrospectionResponse { active: false })), + match state + .access_token_repository + .get_token(request.token.into()) + .await + { + Err(_) => Err(StatusCode::INTERNAL_SERVER_ERROR), // TODO - Add error logging + Ok(None) => Ok(Json(TokenIntrospectionResponse::Inactive { + active: false, + })), + Ok(Some(token)) => Ok(Json(TokenIntrospectionResponse::Active { + active: true, + scope: Some(token.scopes), + client_id: Some(token.client_id.into()), + username: Some(token.username), + token_type: Some(TokenType::Bearer), + expires_at: Some(token.expires_at.and_utc().timestamp()), + issued_at: Some(token.issued_at.and_utc().timestamp()), + not_before: Some(token.not_before.and_utc().timestamp()), + })), } } - -#[derive(Serialize)] -struct TokenIntrospectionResponse { - active: bool, -} diff --git a/src/util/mod.rs b/src/util/mod.rs index 98ba953..6757176 100644 --- a/src/util/mod.rs +++ b/src/util/mod.rs @@ -3,3 +3,4 @@ pub mod disable_serialization; pub mod enum_with_from_str; pub mod map_of; pub mod value_struct; +pub mod uuid_wrapper; \ No newline at end of file diff --git a/src/util/uuid_wrapper.rs b/src/util/uuid_wrapper.rs new file mode 100644 index 0000000..231e7a9 --- /dev/null +++ b/src/util/uuid_wrapper.rs @@ -0,0 +1,56 @@ +use diesel::sql_types::Binary; +use diesel::backend::Backend; +use diesel::deserialize::FromSql; +use diesel::serialize::{Output, ToSql}; +use diesel::sqlite::Sqlite; +use uuid::Uuid; + +#[derive(Debug, Clone, Copy, Hash, Eq, PartialEq)] +#[derive(serde::Serialize)] +#[serde(transparent)] +#[derive(sqlx::Type)] +#[sqlx(transparent)] +#[derive(diesel::FromSqlRow, diesel::AsExpression)] +#[diesel(sql_type = Binary)] +pub struct UuidWrapper(pub Uuid); + +impl UuidWrapper { + pub fn new(uuid: Uuid) -> Self { + Self(uuid) + } + pub fn random() -> Self { + Self(Uuid::new_v4()) + } +} + +impl From for Uuid { + fn from(uuid: UuidWrapper) -> Self { + uuid.0 + } +} + +impl From for UuidWrapper { + fn from(uuid: Uuid) -> Self { + Self(uuid) + } +} + +impl std::fmt::Display for UuidWrapper { + fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(formatter, "{}", self.0) + } +} + +impl ToSql for UuidWrapper { + fn to_sql<'b>(&'b self, out: &mut Output<'b, '_, Sqlite>) -> diesel::serialize::Result { + <[u8] as ToSql>::to_sql(self.0.as_bytes(), out) + } +} + +impl FromSql for UuidWrapper { + fn from_sql(bytes: ::RawValue<'_>) -> diesel::deserialize::Result { + let raw = as FromSql>::from_sql(bytes)?; + let uuid = Uuid::from_slice(&raw).map(Self)?; + Ok(uuid) + } +} diff --git a/src/util/value_struct.rs b/src/util/value_struct.rs index 1532b5b..6822b76 100644 --- a/src/util/value_struct.rs +++ b/src/util/value_struct.rs @@ -1,7 +1,6 @@ pub trait ValueStruct { type ValueType; fn value(&self) -> &Self::ValueType; - fn into_value(self) -> Self::ValueType; } #[macro_export] @@ -13,6 +12,10 @@ macro_rules! value_struct { $(#[$m])* #[non_exhaustive] #[derive(Clone, Hash, Eq, PartialEq)] + #[derive(serde::Serialize)] + #[serde(transparent)] + #[derive(sqlx::Type)] + #[sqlx(transparent)] #[cfg_attr(test, derive(Debug))] $vis struct $struct_name($field_type); @@ -23,11 +26,6 @@ macro_rules! value_struct { fn value(&self) -> &Self::ValueType { &self.0 } - - #[inline] - fn into_value(self) -> Self::ValueType { - self.0 - } } impl std::convert::From<$field_type> for $struct_name { @@ -41,12 +39,5 @@ macro_rules! value_struct { $struct_name(value.clone()) } } - - // TODO - Rethink this as it'll break once ValueType is not a string - impl serde::Serialize for $struct_name { - fn serialize(&self, serializer: S) -> Result { - serializer.serialize_str(&self.0) - } - } }; } From 1341c310b40269363554f2715da8a681c2903761 Mon Sep 17 00:00:00 2001 From: James Bacon Date: Thu, 30 Apr 2026 17:35:14 +0100 Subject: [PATCH 03/19] Enabling clippy::pedantic --- src/client/configuration.rs | 4 ++-- src/client/secret.rs | 4 ++-- src/graceful_shutdown.rs | 4 ++-- src/main.rs | 11 ++++++----- src/token/repository.rs | 8 ++++---- src/token_introspection/response.rs | 14 +++++++------- 6 files changed, 23 insertions(+), 22 deletions(-) diff --git a/src/client/configuration.rs b/src/client/configuration.rs index e51413b..670ed17 100644 --- a/src/client/configuration.rs +++ b/src/client/configuration.rs @@ -1,5 +1,5 @@ use std::collections::{HashMap, HashSet}; -use std::sync::{Arc, Mutex, MutexGuard}; +use std::sync::{Arc, Mutex, MutexGuard, PoisonError}; use crate::client::{ClientAction, ClientId, ClientType, GrantType}; use crate::scope::Scope; @@ -53,7 +53,7 @@ impl InMemoryClientConfigurationRepository { (configuration.client_id.clone(), configuration) } fn lock_store(&self) -> MutexGuard<'_, HashMap> { - self.store.lock().unwrap_or_else(|poisoned| poisoned.into_inner()) + self.store.lock().unwrap_or_else(PoisonError::into_inner) } } diff --git a/src/client/secret.rs b/src/client/secret.rs index 6c4cd60..e891281 100644 --- a/src/client/secret.rs +++ b/src/client/secret.rs @@ -1,5 +1,5 @@ use std::collections::HashMap; -use std::sync::{Arc, Mutex, MutexGuard}; +use std::sync::{Arc, Mutex, MutexGuard, PoisonError}; use uuid::Uuid; use crate::client::ClientId; use crate::util::value_struct::ValueStruct; @@ -58,7 +58,7 @@ impl InMemoryClientSecretRepository { } fn lock_store(&self) -> MutexGuard<'_, HashMap> { - self.store.lock().unwrap_or_else(|poisoned| poisoned.into_inner()) + self.store.lock().unwrap_or_else(PoisonError::into_inner) } } diff --git a/src/graceful_shutdown.rs b/src/graceful_shutdown.rs index 4cbd803..7dc05f3 100644 --- a/src/graceful_shutdown.rs +++ b/src/graceful_shutdown.rs @@ -35,7 +35,7 @@ pub async fn signal() { let terminate = std::future::pending::<()>(); tokio::select! { - _ = ctrl_c => {}, - _ = terminate => {}, + () = ctrl_c => {}, + () = terminate => {}, } } diff --git a/src/main.rs b/src/main.rs index 8280fdd..a68c96e 100644 --- a/src/main.rs +++ b/src/main.rs @@ -5,7 +5,9 @@ clippy::expect_used, clippy::panic, )] - +#![warn( + clippy::pedantic, +)] mod scope; mod token; mod token_exchange; @@ -56,10 +58,9 @@ async fn main() -> Result<(), Box> { // .await?; let access_token_repository = DieselSqliteAccessTokenRepository - ::new("file:target/db/access_tokens.sqlite3") - .await?; + ::new("file:target/db/access_tokens.sqlite3")?; - access_token_repository.run_diesel_migrations().await?; + access_token_repository.run_diesel_migrations()?; let client_secret_repository = InMemoryClientSecretRepository::new(); // "file:target/db/client_secrets.sqlite3" let client_configuration_repository = InMemoryClientConfigurationRepository::new(); // "file:target/db/client_configurations.sqlite3" @@ -89,7 +90,7 @@ async fn main() -> Result<(), Box> { .with_context(|| "Failed to get local tcp address")?; println!(); - println!("Listening on http://{}", local_address); + println!("Listening on http://{local_address}"); println!(); serve(tcp_listener, application) diff --git a/src/token/repository.rs b/src/token/repository.rs index 15db981..81ac4b2 100644 --- a/src/token/repository.rs +++ b/src/token/repository.rs @@ -2,7 +2,7 @@ use crate::token::{AccessToken, Token}; use std::collections::HashMap; use std::error::Error; use std::marker::PhantomData; -use std::sync::{Arc, Mutex, MutexGuard}; +use std::sync::{Arc, Mutex, MutexGuard, PoisonError}; use anyhow::{Context, Result}; use diesel::r2d2::ConnectionManager; use diesel::{RunQueryDsl, SqliteConnection}; @@ -26,7 +26,7 @@ impl InMemoryTokenRepository { Self { store: Arc::new(Mutex::new(HashMap::new())) } } fn lock_store(&self) -> MutexGuard<'_, HashMap> { - self.store.lock().unwrap_or_else(|poisoned| poisoned.into_inner()) + self.store.lock().unwrap_or_else(PoisonError::into_inner) } } @@ -48,7 +48,7 @@ pub struct DieselSqliteAccessTokenRepository { } impl DieselSqliteAccessTokenRepository { - pub async fn new(database_url: &str) -> Result { + pub fn new(database_url: &str) -> Result { let manager = ConnectionManager::::new(database_url); @@ -61,7 +61,7 @@ impl DieselSqliteAccessTokenRepository { pool }) } - pub async fn run_diesel_migrations(&self) -> Result<(), Box> { + pub fn run_diesel_migrations(&self) -> Result<(), Box> { use diesel_migrations::{embed_migrations, EmbeddedMigrations, MigrationHarness}; const ACCESS_TOKEN_MIGRATIONS: EmbeddedMigrations = embed_migrations!("migrations/access_tokens"); self.pool.get()?.run_pending_migrations(ACCESS_TOKEN_MIGRATIONS)?; diff --git a/src/token_introspection/response.rs b/src/token_introspection/response.rs index 5e4afbd..d9006fa 100644 --- a/src/token_introspection/response.rs +++ b/src/token_introspection/response.rs @@ -17,13 +17,13 @@ pub enum TokenIntrospectionResponse { * and is within its given time window of validity (e.g., after its issuance time and before * its expiration time). * - * See Section 4 for information on implementation of such checks [https://www.rfc-editor.org/rfc/rfc7662#section-4]. + * See Section 4 for information on implementation of such checks . */ active: bool, /** * A JSON string containing a space-separated list of scopes associated with this token, - * in the format described in Section 3.3 of OAuth 2.0 [https://www.rfc-editor.org/rfc/rfc6749#section-3.3]. + * in the format described in Section 3.3 of OAuth 2.0 . */ #[serde(skip_serializing_if = "Option::is_none")] scope: Option, // TODO - Use an internal type and setup serialization for it. @@ -41,28 +41,28 @@ pub enum TokenIntrospectionResponse { username: Option, // TODO - Use an internal value struct AuthenticatedUsername /** - * Type of the token as defined in Section 5.1 of OAuth 2.0 [https://www.rfc-editor.org/rfc/rfc6749#section-5.1]. + * Type of the token as defined in Section 5.1 of OAuth 2.0 . */ #[serde(skip_serializing_if = "Option::is_none")] token_type: Option, /** * Integer timestamp, measured in the number of seconds since January 1 1970 UTC, - * indicating when this token will expire, as defined in JWT [https://www.rfc-editor.org/rfc/rfc7519] + * indicating when this token will expire, as defined in JWT */ #[serde(rename = "exp", skip_serializing_if = "Option::is_none")] expires_at: Option, /** * Integer timestamp, measured in the number of seconds since January 1 1970 UTC, - * indicating when this token was originally issued, as defined in JWT [https://www.rfc-editor.org/rfc/rfc7519] + * indicating when this token was originally issued, as defined in JWT */ #[serde(rename = "iat", skip_serializing_if = "Option::is_none")] issued_at: Option, /** * Integer timestamp, measured in the number of seconds since January 1 1970 UTC, - * indicating when this token is not to be used before, as defined in JWT [https://www.rfc-editor.org/rfc/rfc7519] + * indicating when this token is not to be used before, as defined in JWT */ #[serde(rename = "nbf", skip_serializing_if = "Option::is_none")] not_before: Option, @@ -94,7 +94,7 @@ pub enum TokenIntrospectionResponse { #[serde(rename_all = "snake_case")] pub enum ErrorType { InvalidRequest, - UnauthorizedClient, + //UnauthorizedClient, } impl TokenIntrospectionResponse { From 6458c0117525c3ed7fd766b4927de35cf4a84be1 Mon Sep 17 00:00:00 2001 From: James Bacon Date: Wed, 6 May 2026 10:02:52 +0100 Subject: [PATCH 04/19] SQLX approach now works end (insert) to end (select) --- Cargo.lock | 8 +++---- Cargo.toml | 2 +- src/main.rs | 13 +++++------ src/token/repository.rs | 50 ++++++++++++++++++++++++++--------------- 4 files changed, 43 insertions(+), 30 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 56bdd82..9662bb1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1739,6 +1739,7 @@ checksum = "ee6798b1838b6a0f69c007c133b8df5866302197e404e8b6ee8ed3e3a5e68dc6" dependencies = [ "base64", "bytes", + "chrono", "crc", "crossbeam-queue", "either", @@ -1759,7 +1760,6 @@ dependencies = [ "sha2", "smallvec", "thiserror", - "time", "tokio", "tokio-stream", "tracing", @@ -1816,6 +1816,7 @@ dependencies = [ "bitflags", "byteorder", "bytes", + "chrono", "crc", "digest", "dotenvy", @@ -1843,7 +1844,6 @@ dependencies = [ "sqlx-core", "stringprep", "thiserror", - "time", "tracing", "uuid", "whoami", @@ -1859,6 +1859,7 @@ dependencies = [ "base64", "bitflags", "byteorder", + "chrono", "crc", "dotenvy", "etcetera", @@ -1882,7 +1883,6 @@ dependencies = [ "sqlx-core", "stringprep", "thiserror", - "time", "tracing", "uuid", "whoami", @@ -1895,6 +1895,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2d12fe70b2c1b4401038055f90f151b78208de1f9f89a7dbfd41587a10c3eea" dependencies = [ "atoi", + "chrono", "flume", "futures-channel", "futures-core", @@ -1908,7 +1909,6 @@ dependencies = [ "serde_urlencoded", "sqlx-core", "thiserror", - "time", "tracing", "url", "uuid", diff --git a/Cargo.toml b/Cargo.toml index 5258431..07629e4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -14,7 +14,7 @@ form_urlencoded = "1.2.2" tower = "0.5.3" diesel = { version = "2.3.6", features = ["sqlite", "returning_clauses_for_sqlite_3_35", "r2d2", "chrono"] } chrono = { version = "0.4.44", features = ["serde"] } -sqlx = { version = "0.8.6", features = [ "runtime-tokio", "sqlite", "uuid", "time", "derive", "macros" ] } +sqlx = { version = "0.8.6", features = [ "runtime-tokio", "sqlite", "uuid", "chrono", "derive", "macros" ] } anyhow = "1.0.102" thiserror = "2.0.18" trait-variant = "0.1.2" diff --git a/src/main.rs b/src/main.rs index a68c96e..d9a2e41 100644 --- a/src/main.rs +++ b/src/main.rs @@ -52,15 +52,14 @@ async fn main() -> Result<(), Box> { // TODO - Do we bother with services, or just continue with passing the repositories directly? // let access_token_repository = InMemoryTokenRepository::::new(); - // let access_token_repository = SqlxSqliteTokenRepository - // :: - // ::new("file:target/db/access_tokens.sqlite3) - // .await?; + let access_token_repository = token::repository::SqlxSqliteAccessTokenRepository + ::new("file:target/db/access_tokens.sqlite3") + .await?; - let access_token_repository = DieselSqliteAccessTokenRepository - ::new("file:target/db/access_tokens.sqlite3")?; + // let access_token_repository = DieselSqliteAccessTokenRepository + // ::new("file:target/db/access_tokens.sqlite3")?; - access_token_repository.run_diesel_migrations()?; + //access_token_repository.run_diesel_migrations()?; let client_secret_repository = InMemoryClientSecretRepository::new(); // "file:target/db/client_secrets.sqlite3" let client_configuration_repository = InMemoryClientConfigurationRepository::new(); // "file:target/db/client_configurations.sqlite3" diff --git a/src/token/repository.rs b/src/token/repository.rs index 81ac4b2..5b946c2 100644 --- a/src/token/repository.rs +++ b/src/token/repository.rs @@ -1,13 +1,12 @@ use crate::token::{AccessToken, Token}; use std::collections::HashMap; use std::error::Error; -use std::marker::PhantomData; use std::sync::{Arc, Mutex, MutexGuard, PoisonError}; use anyhow::{Context, Result}; use diesel::r2d2::ConnectionManager; use diesel::{RunQueryDsl, SqliteConnection}; -use sqlx::{FromRow, Pool, Sqlite}; -use sqlx::sqlite::{SqlitePoolOptions, SqliteRow}; +use sqlx::{Pool, Sqlite}; +use sqlx::sqlite::SqlitePoolOptions; use crate::util::uuid_wrapper::UuidWrapper; #[trait_variant::make(Send)] @@ -55,7 +54,7 @@ impl DieselSqliteAccessTokenRepository { let pool = diesel::r2d2::Pool::builder() .test_on_check_out(true) .build(manager) - .with_context(|| format!("Failed to create Diesel sqlite database pool: {}", database_url))?; + .with_context(|| format!("Failed to create Diesel sqlite database pool: {database_url}"))?; Ok(Self { pool @@ -73,7 +72,7 @@ impl TokenRepository for DieselSqliteAccessTokenRepository { async fn get_token(&self, token: UuidWrapper) -> Result> { use super::schema::access_tokens; - use super::schema::access_tokens::dsl::*; + use super::schema::access_tokens::dsl::id; use diesel::prelude::*; let connection = &mut self.pool.get() @@ -90,7 +89,7 @@ impl TokenRepository for DieselSqliteAccessTokenRepository { async fn save_token(&self, token: &AccessToken) -> Result<()> { - use super::schema::access_tokens::dsl::*; + use super::schema::access_tokens::dsl::access_tokens; use diesel::dsl::insert_into; let connection = &mut self.pool.get() @@ -106,42 +105,57 @@ impl TokenRepository for DieselSqliteAccessTokenRepository { } #[derive(Clone)] -pub struct SqlxSqliteTokenRepository { - _token_type: PhantomData, +pub struct SqlxSqliteAccessTokenRepository { pool: Pool, } -impl SqlxSqliteTokenRepository { - pub async fn new(database_url: &str) -> Result> { +impl SqlxSqliteAccessTokenRepository { + pub async fn new(database_url: &str) -> Result { Ok( Self { - _token_type: PhantomData, pool: SqlitePoolOptions::new() .min_connections(1) .max_connections(5) .connect(database_url) .await .with_context(|| - format!("Failed to create SQLX sqlite database pool: {}", database_url) + format!("Failed to create SQLX sqlite database pool: {database_url}") )? } ) } } -impl FromRow<'r, SqliteRow>> TokenRepository for SqlxSqliteTokenRepository { +impl TokenRepository for SqlxSqliteAccessTokenRepository { - async fn get_token(&self, token: UuidWrapper) -> Result> { + async fn get_token(&self, token: UuidWrapper) -> Result> { - let result = sqlx::query_as::("SELECT * FROM access_tokens WHERE id = ?") + let result = sqlx::query_as::<_, AccessToken>("SELECT * FROM access_tokens WHERE id = ?;") .bind(token) .fetch_optional(&self.pool) - .await?; + .await + .with_context(|| "Error querying access token database")?; Ok(result) } - async fn save_token(&self, token: &T) -> Result<()> { - todo!() + async fn save_token(&self, token: &AccessToken) -> Result<()> { + + sqlx::query(" + INSERT INTO access_tokens (id, username, client_id, scopes, issued_at, expires_at, not_before) + VALUES (?, ?, ?, ?, ?, ?, ?); + ") + .bind(token.id) + .bind(&token.username) + .bind(&token.client_id) + .bind(&token.scopes) + .bind(token.issued_at) + .bind(token.expires_at) + .bind(token.not_before) + .execute(&self.pool) + .await + .with_context(|| "Error saving access token to database")?; + + Ok(()) } } From 48c1fc9e009fe4bc2463b95acd0567484e5f955e Mon Sep 17 00:00:00 2001 From: James Bacon Date: Wed, 6 May 2026 12:27:11 +0100 Subject: [PATCH 05/19] Added async logic to the diesel based code. Added feature switches between diesel and sqlx, default is diesel. --- Cargo.lock | 78 +++++++++++++++++++++++++++------------- Cargo.toml | 12 +++++-- src/main.rs | 20 +++++++---- src/token/mod.rs | 9 ++--- src/token/repository.rs | 70 +++++++++++++++++++++++++++--------- src/util/uuid_wrapper.rs | 24 ++++++++----- src/util/value_struct.rs | 4 +-- 7 files changed, 151 insertions(+), 66 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 9662bb1..7b044bf 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -353,6 +353,23 @@ dependencies = [ "syn", ] +[[package]] +name = "deadpool" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "883466cb8db62725aee5f4a6011e8a5d42912b42632df32aad57fc91127c6e04" +dependencies = [ + "deadpool-runtime", + "num_cpus", + "tokio", +] + +[[package]] +name = "deadpool-runtime" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2657f61fb1dd8bf37a8d51093cc7cee4e77125b22f7753f49b289f831bec2bae" + [[package]] name = "der" version = "0.7.10" @@ -375,19 +392,33 @@ dependencies = [ [[package]] name = "diesel" -version = "2.3.8" +version = "2.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78df0e4e8c596662edb07fbfbb7f23769cca35049827df5f909084d956b6aeaf" +checksum = "9940fb8467a0a06312218ed384185cb8536aa10d8ec017d0ce7fad2c1bd882d5" dependencies = [ "chrono", "diesel_derives", "downcast-rs", "libsqlite3-sys", - "r2d2", "sqlite-wasm-rs", "time", ] +[[package]] +name = "diesel-async" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c20ddcc6737cecdaef3dfecb2796bdfe3002456521189d30be8e4c5a1bc821d" +dependencies = [ + "deadpool", + "diesel", + "diesel_migrations", + "futures-core", + "futures-util", + "pin-project-lite", + "tokio", +] + [[package]] name = "diesel_derives" version = "2.3.8" @@ -726,6 +757,12 @@ version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" +[[package]] +name = "hermit-abi" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c" + [[package]] name = "hex" version = "0.4.3" @@ -1190,6 +1227,16 @@ dependencies = [ "libm", ] +[[package]] +name = "num_cpus" +version = "1.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91df4bbde75afed763b708b7eee1e8e7651e02d97f6d5dd763e89367e957b23b" +dependencies = [ + "hermit-abi", + "libc", +] + [[package]] name = "oauth-api-rust" version = "0.1.0" @@ -1202,6 +1249,7 @@ dependencies = [ "base64", "chrono", "diesel", + "diesel-async", "diesel_migrations", "form_urlencoded", "http-body-util", @@ -1278,9 +1326,9 @@ checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" [[package]] name = "pin-project-lite" -version = "0.2.16" +version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" [[package]] name = "pin-utils" @@ -1379,17 +1427,6 @@ version = "5.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" -[[package]] -name = "r2d2" -version = "0.8.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "51de85fb3fb6524929c8a2eb85e6b6d363de4e8c48f9e2c2eac4944abc181c93" -dependencies = [ - "log", - "parking_lot", - "scheduled-thread-pool", -] - [[package]] name = "rand" version = "0.8.6" @@ -1518,15 +1555,6 @@ dependencies = [ "winapi-util", ] -[[package]] -name = "scheduled-thread-pool" -version = "0.2.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3cbc66816425a074528352f5789333ecff06ca41b36b0b0efdfbb29edc391a19" -dependencies = [ - "parking_lot", -] - [[package]] name = "scopeguard" version = "1.2.0" diff --git a/Cargo.toml b/Cargo.toml index 07629e4..b4bf75d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,6 +3,11 @@ name = "oauth-api-rust" version = "0.1.0" edition = "2024" +[features] +default = ["diesel"] +sqlx = ["dep:sqlx"] +diesel = ["dep:diesel", "dep:diesel-async", "dep:diesel_migrations"] + [dependencies] argon2 = "0.5.3" axum = { version = "0.8.8", features = ["macros"] } @@ -12,13 +17,14 @@ tokio = { version = "1.50.0", features = ["macros", "rt-multi-thread", "signal"] uuid = { version = "1.23.0", features = ["v4", "serde"] } form_urlencoded = "1.2.2" tower = "0.5.3" -diesel = { version = "2.3.6", features = ["sqlite", "returning_clauses_for_sqlite_3_35", "r2d2", "chrono"] } +diesel = { version = "2.3.9", optional = true, features = ["returning_clauses_for_sqlite_3_35", "chrono"] } +diesel-async = { version = "0.9.0", optional = true, features = ["sqlite", "deadpool", "migrations"] } +diesel_migrations = { version = "2.3.2", optional = true } chrono = { version = "0.4.44", features = ["serde"] } -sqlx = { version = "0.8.6", features = [ "runtime-tokio", "sqlite", "uuid", "chrono", "derive", "macros" ] } +sqlx = { version = "0.8.6", optional = true, features = [ "runtime-tokio", "sqlite", "uuid", "chrono", "derive", "macros" ] } anyhow = "1.0.102" thiserror = "2.0.18" trait-variant = "0.1.2" -diesel_migrations = "2.3.2" [dev-dependencies] assertables = "9.8.6" diff --git a/src/main.rs b/src/main.rs index d9a2e41..a3fbd27 100644 --- a/src/main.rs +++ b/src/main.rs @@ -22,7 +22,6 @@ use tokio::net::TcpListener; use client::authentication::ClientAuthenticationService; use client::configuration::InMemoryClientConfigurationRepository; use client::secret::InMemoryClientSecretRepository; -use token::repository::DieselSqliteAccessTokenRepository; use token_exchange::TokenExchangeState; use token_introspection::TokenIntrospectionState; @@ -49,17 +48,26 @@ use anyhow::{Context, Result}; #[tokio::main] async fn main() -> Result<(), Box> { - // TODO - Do we bother with services, or just continue with passing the repositories directly? - // let access_token_repository = InMemoryTokenRepository::::new(); + // TODO - Do we bother with services? + // or just continue with passing the repositories directly? + // or do we just pass connection pools and do await with services and repositories? + #[cfg(not(any(feature = "diesel", feature = "sqlx")))] + let access_token_repository = token::repository::InMemoryTokenRepository + :: + ::new(); + + #[cfg(feature = "sqlx")] let access_token_repository = token::repository::SqlxSqliteAccessTokenRepository ::new("file:target/db/access_tokens.sqlite3") .await?; - // let access_token_repository = DieselSqliteAccessTokenRepository - // ::new("file:target/db/access_tokens.sqlite3")?; + #[cfg(feature = "diesel")] + let access_token_repository = token::repository::DieselSqliteAccessTokenRepository + ::new("file:target/db/access_tokens.sqlite3")?; - //access_token_repository.run_diesel_migrations()?; + #[cfg(feature = "diesel")] + access_token_repository.run_diesel_migrations().await?; let client_secret_repository = InMemoryClientSecretRepository::new(); // "file:target/db/client_secrets.sqlite3" let client_configuration_repository = InMemoryClientConfigurationRepository::new(); // "file:target/db/client_configurations.sqlite3" diff --git a/src/token/mod.rs b/src/token/mod.rs index 89de559..5cf1e6a 100644 --- a/src/token/mod.rs +++ b/src/token/mod.rs @@ -1,4 +1,5 @@ pub mod repository; +#[cfg(feature = "diesel")] mod schema; use serde::Serialize; @@ -17,10 +18,10 @@ pub enum TokenType { } #[derive(Serialize, Clone)] -#[derive(sqlx::FromRow)] -#[derive(diesel::Queryable, diesel::Selectable, diesel::Insertable)] -#[diesel(table_name = schema::access_tokens)] -#[diesel(check_for_backend(diesel::sqlite::Sqlite))] +#[cfg_attr(feature = "sqlx", derive(sqlx::FromRow))] +#[cfg_attr(feature = "diesel", derive(diesel::Queryable, diesel::Selectable, diesel::Insertable))] +#[cfg_attr(feature = "diesel", diesel(table_name = schema::access_tokens))] +#[cfg_attr(feature = "diesel", diesel(check_for_backend(diesel::sqlite::Sqlite)))] pub struct AccessToken { pub id: UuidWrapper, pub username: String, // TODO - Use AuthenticatedUser diff --git a/src/token/repository.rs b/src/token/repository.rs index 5b946c2..937aef4 100644 --- a/src/token/repository.rs +++ b/src/token/repository.rs @@ -1,14 +1,25 @@ -use crate::token::{AccessToken, Token}; +use crate::token::Token; use std::collections::HashMap; -use std::error::Error; use std::sync::{Arc, Mutex, MutexGuard, PoisonError}; -use anyhow::{Context, Result}; -use diesel::r2d2::ConnectionManager; -use diesel::{RunQueryDsl, SqliteConnection}; -use sqlx::{Pool, Sqlite}; -use sqlx::sqlite::SqlitePoolOptions; +use anyhow::Result; use crate::util::uuid_wrapper::UuidWrapper; +#[cfg(any(feature = "diesel", feature = "sqlx"))] +use { + crate::token::AccessToken, + anyhow::Context, +}; + +#[cfg(feature = "diesel")] +use { + std::error::Error, + diesel::prelude::*, + diesel_async::RunQueryDsl, + diesel_async::pooled_connection::AsyncDieselConnectionManager, + diesel_async::pooled_connection::deadpool::Pool, + diesel_async::sync_connection_wrapper::SyncConnectionWrapper, +}; + #[trait_variant::make(Send)] pub trait TokenRepository: Sync + Clone { async fn get_token(&self, id: UuidWrapper) -> Result>; @@ -41,46 +52,65 @@ impl TokenRepository for InMemoryTokenRepository } } +#[cfg(feature = "diesel")] +type AsyncSqliteConnection = SyncConnectionWrapper; +#[cfg(feature = "diesel")] +type AsyncSqlitePool = Pool; + #[derive(Clone)] +#[cfg(feature = "diesel")] pub struct DieselSqliteAccessTokenRepository { - pool: diesel::r2d2::Pool>, + pool: AsyncSqlitePool, } +#[cfg(feature = "diesel")] impl DieselSqliteAccessTokenRepository { pub fn new(database_url: &str) -> Result { - let manager = ConnectionManager::::new(database_url); + let manager = AsyncDieselConnectionManager::::new(database_url); - let pool = diesel::r2d2::Pool::builder() - .test_on_check_out(true) - .build(manager) + let pool = AsyncSqlitePool::builder(manager) + .max_size(5) + .build() .with_context(|| format!("Failed to create Diesel sqlite database pool: {database_url}"))?; Ok(Self { pool }) } - pub fn run_diesel_migrations(&self) -> Result<(), Box> { + pub async fn run_diesel_migrations(&self) -> Result<(), Box> { + use diesel_async::AsyncMigrationHarness; use diesel_migrations::{embed_migrations, EmbeddedMigrations, MigrationHarness}; const ACCESS_TOKEN_MIGRATIONS: EmbeddedMigrations = embed_migrations!("migrations/access_tokens"); - self.pool.get()?.run_pending_migrations(ACCESS_TOKEN_MIGRATIONS)?; + + let connection = self.pool + .get() + .await + .with_context(|| "Failed to get connection from pool")?; + + let mut harness = AsyncMigrationHarness::new(connection); + + harness.run_pending_migrations(ACCESS_TOKEN_MIGRATIONS)?; + Ok(()) } } +#[cfg(feature = "diesel")] impl TokenRepository for DieselSqliteAccessTokenRepository { async fn get_token(&self, token: UuidWrapper) -> Result> { use super::schema::access_tokens; use super::schema::access_tokens::dsl::id; - use diesel::prelude::*; let connection = &mut self.pool.get() + .await .with_context(|| "Failed to get connection from pool")?; let result = access_tokens::table .filter(id.eq(token)) .first::(connection) + .await .optional() .with_context(|| "Error querying access token database")?; @@ -91,13 +121,16 @@ impl TokenRepository for DieselSqliteAccessTokenRepository { use super::schema::access_tokens::dsl::access_tokens; use diesel::dsl::insert_into; + use diesel_async::RunQueryDsl; let connection = &mut self.pool.get() + .await .with_context(|| "Failed to get connection from pool")?; insert_into(access_tokens) .values(token) .execute(connection) + .await .with_context(|| "Error saving access token to database")?; Ok(()) @@ -105,15 +138,17 @@ impl TokenRepository for DieselSqliteAccessTokenRepository { } #[derive(Clone)] +#[cfg(feature = "sqlx")] pub struct SqlxSqliteAccessTokenRepository { - pool: Pool, + pool: sqlx::Pool, } +#[cfg(feature = "sqlx")] impl SqlxSqliteAccessTokenRepository { pub async fn new(database_url: &str) -> Result { Ok( Self { - pool: SqlitePoolOptions::new() + pool: sqlx::sqlite::SqlitePoolOptions::new() .min_connections(1) .max_connections(5) .connect(database_url) @@ -126,6 +161,7 @@ impl SqlxSqliteAccessTokenRepository { } } +#[cfg(feature = "sqlx")] impl TokenRepository for SqlxSqliteAccessTokenRepository { async fn get_token(&self, token: UuidWrapper) -> Result> { diff --git a/src/util/uuid_wrapper.rs b/src/util/uuid_wrapper.rs index 231e7a9..944194a 100644 --- a/src/util/uuid_wrapper.rs +++ b/src/util/uuid_wrapper.rs @@ -1,17 +1,21 @@ -use diesel::sql_types::Binary; -use diesel::backend::Backend; -use diesel::deserialize::FromSql; -use diesel::serialize::{Output, ToSql}; -use diesel::sqlite::Sqlite; +#[cfg(feature = "diesel")] +use { + diesel::sql_types::Binary, + diesel::backend::Backend, + diesel::deserialize::FromSql, + diesel::serialize::{Output, ToSql}, + diesel::sqlite::Sqlite, +}; + use uuid::Uuid; #[derive(Debug, Clone, Copy, Hash, Eq, PartialEq)] #[derive(serde::Serialize)] #[serde(transparent)] -#[derive(sqlx::Type)] -#[sqlx(transparent)] -#[derive(diesel::FromSqlRow, diesel::AsExpression)] -#[diesel(sql_type = Binary)] +#[cfg_attr(feature = "sqlx", derive(sqlx::Type))] +#[cfg_attr(feature = "sqlx", sqlx(transparent))] +#[cfg_attr(feature = "diesel", derive(diesel::FromSqlRow, diesel::AsExpression))] +#[cfg_attr(feature = "diesel", diesel(sql_type = diesel::sql_types::Binary))] pub struct UuidWrapper(pub Uuid); impl UuidWrapper { @@ -41,12 +45,14 @@ impl std::fmt::Display for UuidWrapper { } } +#[cfg(feature = "diesel")] impl ToSql for UuidWrapper { fn to_sql<'b>(&'b self, out: &mut Output<'b, '_, Sqlite>) -> diesel::serialize::Result { <[u8] as ToSql>::to_sql(self.0.as_bytes(), out) } } +#[cfg(feature = "diesel")] impl FromSql for UuidWrapper { fn from_sql(bytes: ::RawValue<'_>) -> diesel::deserialize::Result { let raw = as FromSql>::from_sql(bytes)?; diff --git a/src/util/value_struct.rs b/src/util/value_struct.rs index 6822b76..11ecb8c 100644 --- a/src/util/value_struct.rs +++ b/src/util/value_struct.rs @@ -14,8 +14,8 @@ macro_rules! value_struct { #[derive(Clone, Hash, Eq, PartialEq)] #[derive(serde::Serialize)] #[serde(transparent)] - #[derive(sqlx::Type)] - #[sqlx(transparent)] + #[cfg_attr(feature = "sqlx", derive(sqlx::Type))] + #[cfg_attr(feature = "sqlx", sqlx(transparent))] #[cfg_attr(test, derive(Debug))] $vis struct $struct_name($field_type); From 6c86f15c22d2a00e01899ce58102d965352293a5 Mon Sep 17 00:00:00 2001 From: James Bacon Date: Wed, 6 May 2026 12:40:03 +0100 Subject: [PATCH 06/19] Clippy only targets the main slice of code, since other features are example/experimental. --- .github/workflows/rust.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index 465bb75..b9575b4 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -60,7 +60,7 @@ jobs: - name: Clippy shell: bash run: >- - cargo clippy -q --all-targets --all-features --message-format=json + cargo clippy -q --message-format=json | jq -r 'select(.reason == "compiler-message") | .message | . as $message From 8513f82a6a9025afeb6157df1f97b8737fa96cc3 Mon Sep 17 00:00:00 2001 From: James Bacon Date: Wed, 6 May 2026 14:08:43 +0100 Subject: [PATCH 07/19] Tackled all Clippy warnings Furture PRs should always resolve warnings --- src/client/authentication.rs | 5 +-- src/client/client_principal.rs | 7 ++-- src/client/middleware.rs | 7 ++-- src/client/secret.rs | 20 ++++++------ src/main.rs | 3 +- src/scope/mod.rs | 2 +- src/scope/parser.rs | 4 +-- src/token/mod.rs | 10 ------ src/token/repository.rs | 32 +++++++++++-------- .../grant/authorization_code.rs | 18 +++++------ src/token_exchange/grant/password.rs | 32 +++++++++---------- src/token_exchange/request.rs | 20 ++++++------ src/token_exchange/response.rs | 4 +-- src/token_exchange/route.rs | 4 +-- src/token_introspection/request.rs | 8 ++--- src/token_introspection/route.rs | 2 +- src/util/map_of.rs | 1 + src/util/value_struct.rs | 2 +- 18 files changed, 86 insertions(+), 95 deletions(-) diff --git a/src/client/authentication.rs b/src/client/authentication.rs index 4e3705c..eaeac89 100644 --- a/src/client/authentication.rs +++ b/src/client/authentication.rs @@ -45,10 +45,7 @@ where let maybe_secret = secrets.iter() .find(|secret| { - let hash = match PasswordHash::new(&secret.hashed_secret) { - Err(_) => return false, - Ok(hash) => hash, - }; + let Ok(hash) = PasswordHash::new(&secret.hashed_secret) else { return false }; Argon2::default().verify_password(client_secret, &hash).is_ok() }); diff --git a/src/client/client_principal.rs b/src/client/client_principal.rs index c4fa582..2b4b652 100644 --- a/src/client/client_principal.rs +++ b/src/client/client_principal.rs @@ -42,6 +42,7 @@ macro_rules! define_principal { configuration: crate::client::configuration::ClientConfiguration, } + #[allow(dead_code)] // TODO - Remove once more is implemented impl $struct_name { pub fn id(&self) -> &crate::client::ClientId { @@ -60,9 +61,9 @@ macro_rules! define_principal { self.configuration.allowed_scopes.contains(scope) } - // pub fn has_redirect_uri(&self, redirect_uri: &str) -> bool { - // self.configuration.redirect_uris.contains(redirect_uri) - // } + pub fn has_redirect_uri(&self, redirect_uri: &str) -> bool { + self.configuration.redirect_uris.contains(redirect_uri) + } } disable_deserialization!($struct_name); diff --git a/src/client/middleware.rs b/src/client/middleware.rs index 26624fa..04e5845 100644 --- a/src/client/middleware.rs +++ b/src/client/middleware.rs @@ -51,11 +51,8 @@ pub async fn require_client_authentication( .map(|(_, v)| v.into_owned()); let principal = match (maybe_basic_auth, maybe_client_id) { - // Both are present → reject per RFC 6749 §2.3 - (Some(_), Some(_)) => return Err(StatusCode::UNAUTHORIZED), - - // Neither is present → reject - (None, None) => return Err(StatusCode::UNAUTHORIZED), + // Both are present → reject per RFC 6749 §2.3; or neither is present → reject + (Some(_), Some(_)) | (None, None) => return Err(StatusCode::UNAUTHORIZED), // Confidential client via Basic auth (Some(TypedHeader(Authorization(basic))), None) => { diff --git a/src/client/secret.rs b/src/client/secret.rs index e891281..d88fb9d 100644 --- a/src/client/secret.rs +++ b/src/client/secret.rs @@ -6,15 +6,15 @@ use crate::util::value_struct::ValueStruct; #[derive(Clone)] pub struct ClientSecret { - pub id: Uuid, + //pub id: Uuid, pub client_id: ClientId, pub hashed_secret: String, } // TODO - #[trait_variant::make(Send)] pub trait ClientSecretRepository: Send + Sync + Clone { - fn find_by_id(&self, id: &Uuid) -> Option; - fn find_all_by_client(&self, client_id: &ClientId) -> Vec; + //fn find_by_id(&self, id: &Uuid) -> Option; + //fn find_all_by_client(&self, client_id: &ClientId) -> Vec; fn find_all_by_client_id(&self, client_id: &str) -> Vec; } @@ -51,7 +51,7 @@ impl InMemoryClientSecretRepository { let client_secret_id = Uuid::new_v4(); (client_secret_id, ClientSecret { - id: client_secret_id, + //id: client_secret_id, client_id: ClientId(String::from(client_id)), hashed_secret: hashed }) @@ -63,12 +63,12 @@ impl InMemoryClientSecretRepository { } impl ClientSecretRepository for InMemoryClientSecretRepository { - fn find_by_id(&self, id: &Uuid) -> Option { - self.lock_store().get(id).cloned() - } - fn find_all_by_client(&self, client_id: &ClientId) -> Vec { - self.lock_store().values().filter(|secret| &secret.client_id == client_id).cloned().collect() - } + // fn find_by_id(&self, id: &Uuid) -> Option { + // self.lock_store().get(id).cloned() + // } + // fn find_all_by_client(&self, client_id: &ClientId) -> Vec { + // self.lock_store().values().filter(|secret| &secret.client_id == client_id).cloned().collect() + // } fn find_all_by_client_id(&self, client_id: &str) -> Vec { self.lock_store().values().filter(|secret| secret.client_id.value() == client_id).cloned().collect() } diff --git a/src/main.rs b/src/main.rs index a3fbd27..0dea474 100644 --- a/src/main.rs +++ b/src/main.rs @@ -53,8 +53,7 @@ async fn main() -> Result<(), Box> { // or do we just pass connection pools and do await with services and repositories? #[cfg(not(any(feature = "diesel", feature = "sqlx")))] - let access_token_repository = token::repository::InMemoryTokenRepository - :: + let access_token_repository = token::repository::InMemoryAccessTokenRepository ::new(); #[cfg(feature = "sqlx")] diff --git a/src/scope/mod.rs b/src/scope/mod.rs index cff39c0..3fc6c4a 100644 --- a/src/scope/mod.rs +++ b/src/scope/mod.rs @@ -23,7 +23,7 @@ impl Serialize for Scopes { // Serialize scopes as a space delimited list fn serialize(&self, serializer: S) -> Result { self.0.iter() - .map(|scope| scope.to_string()) + .map(ToString::to_string) .collect::>() .join(" ") .serialize(serializer) diff --git a/src/scope/parser.rs b/src/scope/parser.rs index 4afca10..397bb4b 100644 --- a/src/scope/parser.rs +++ b/src/scope/parser.rs @@ -11,8 +11,8 @@ pub fn parse_scopes(maybe_space_delimited_scopes: Option<&String>) -> Result>(); diff --git a/src/token/mod.rs b/src/token/mod.rs index 5cf1e6a..5f43e93 100644 --- a/src/token/mod.rs +++ b/src/token/mod.rs @@ -5,10 +5,6 @@ mod schema; use serde::Serialize; use crate::util::uuid_wrapper::UuidWrapper; -pub trait Token: Send + Sync + Clone { - fn id(&self) -> UuidWrapper; -} - #[cfg_attr(test, derive(Debug))] #[derive(Serialize, Eq, PartialEq)] #[serde(rename_all = "snake_case")] @@ -31,9 +27,3 @@ pub struct AccessToken { pub expires_at: chrono::NaiveDateTime, pub not_before: chrono::NaiveDateTime, } - -impl Token for AccessToken { - fn id(&self) -> UuidWrapper { - self.id - } -} diff --git a/src/token/repository.rs b/src/token/repository.rs index 937aef4..bb9209c 100644 --- a/src/token/repository.rs +++ b/src/token/repository.rs @@ -1,12 +1,15 @@ -use crate::token::Token; -use std::collections::HashMap; -use std::sync::{Arc, Mutex, MutexGuard, PoisonError}; +use crate::token::AccessToken; use anyhow::Result; use crate::util::uuid_wrapper::UuidWrapper; +#[cfg(any(test, not(any(feature = "diesel", feature = "sqlx"))))] +use { + std::collections::HashMap, + std::sync::{Arc, Mutex, MutexGuard, PoisonError}, +}; + #[cfg(any(feature = "diesel", feature = "sqlx"))] use { - crate::token::AccessToken, anyhow::Context, }; @@ -21,33 +24,36 @@ use { }; #[trait_variant::make(Send)] -pub trait TokenRepository: Sync + Clone { +pub trait TokenRepository: Sync + Clone { async fn get_token(&self, id: UuidWrapper) -> Result>; async fn save_token(&self, token: &T) -> Result<()>; } #[derive(Clone, Default)] -pub struct InMemoryTokenRepository { - store: Arc>>, +#[cfg(any(test, not(any(feature = "diesel", feature = "sqlx"))))] +pub struct InMemoryAccessTokenRepository { + store: Arc>>, } -impl InMemoryTokenRepository { +#[cfg(any(test, not(any(feature = "diesel", feature = "sqlx"))))] +impl InMemoryAccessTokenRepository { pub fn new() -> Self { Self { store: Arc::new(Mutex::new(HashMap::new())) } } - fn lock_store(&self) -> MutexGuard<'_, HashMap> { + fn lock_store(&self) -> MutexGuard<'_, HashMap> { self.store.lock().unwrap_or_else(PoisonError::into_inner) } } -impl TokenRepository for InMemoryTokenRepository +#[cfg(any(test, not(any(feature = "diesel", feature = "sqlx"))))] +impl TokenRepository for InMemoryAccessTokenRepository { - async fn get_token(&self, id: UuidWrapper) -> Result> { + async fn get_token(&self, id: UuidWrapper) -> Result> { Ok(self.lock_store().get(&id).cloned()) } - async fn save_token(&self, token: &T) -> Result<()> { - self.lock_store().insert(token.id(), token.clone()); + async fn save_token(&self, token: &AccessToken) -> Result<()> { + self.lock_store().insert(token.id, token.clone()); Ok(()) } } diff --git a/src/token_exchange/grant/authorization_code.rs b/src/token_exchange/grant/authorization_code.rs index 416f0d9..ea94830 100644 --- a/src/token_exchange/grant/authorization_code.rs +++ b/src/token_exchange/grant/authorization_code.rs @@ -1,9 +1,9 @@ -use serde::Deserialize; - -#[derive(Deserialize, Eq, PartialEq)] -#[cfg_attr(test, derive(Debug))] -pub struct AuthorizationCodeGrantRequest { - pub code: String, - pub redirect_uri: String, - pub code_verifier: Option, -} +// use serde::Deserialize; +// +// #[derive(Deserialize, Eq, PartialEq)] +// #[cfg_attr(test, derive(Debug))] +// pub struct AuthorizationCodeGrantRequest { +// pub code: String, +// pub redirect_uri: String, +// pub code_verifier: Option, +// } diff --git a/src/token_exchange/grant/password.rs b/src/token_exchange/grant/password.rs index 88895f3..9a2f77a 100644 --- a/src/token_exchange/grant/password.rs +++ b/src/token_exchange/grant/password.rs @@ -41,7 +41,7 @@ where id: UuidWrapper::random(), username: request.username, client_id: request.principal.id().value().clone(), - scopes: request.scopes.clone().map(|s|s.0.iter().map(|scope| scope.to_string()).collect::>().join(" ")).unwrap_or_else(String::new), + scopes: request.scopes.clone().map_or_else(String::new, |s|s.0.iter().map(ToString::to_string).collect::>().join(" ")), issued_at: issued_at.naive_utc(), expires_at: (issued_at + Duration::hours(2)).naive_utc(), not_before: (issued_at - Duration::minutes(1)).naive_utc(), @@ -61,12 +61,12 @@ where ) } -pub fn validate_password_grant(principal: ClientPrincipal, request: HashMap) -> Result { +pub fn validate_password_grant(principal: ClientPrincipal, request: &HashMap) -> Result { let client = match principal { Confidential(client) if client.can_perform_grant_type(&Password) => client, _ => Err(TokenExchangeResponse::Failure { error: ErrorType::UnauthorizedClient, - error_description: Some(format!("not authorized to: {:?}", Password)), + error_description: Some(format!("not authorized to: {Password:?}")), })?, }; @@ -124,7 +124,7 @@ mod unit_tests { fn should_return_invalid_request_for_a_public_client() { let result = validate_password_grant( ClientPrincipal::new_public_principal("aardvark"), - map_of! { + &map_of! { "username" => "aardvark", "password" => "", "scope" => "read write", @@ -150,7 +150,7 @@ mod unit_tests { allowed_actions: Default::default(), allowed_grant_types: Default::default(), }), - map_of! { + &map_of! { "username" => "aardvark", "password" => "", "scope" => "read write", @@ -169,7 +169,7 @@ mod unit_tests { fn should_return_invalid_request_on_missing_username() { let result = validate_password_grant( ClientPrincipal::new_confidential_principal("aardvark"), - map_of! { + &map_of! { "password" => "", "scope" => "read write", }, @@ -191,7 +191,7 @@ mod unit_tests { fn should_return_invalid_request_on_blank_username() { let result = validate_password_grant( ClientPrincipal::new_confidential_principal("aardvark"), - map_of! { + &map_of! { "username" => " ", "password" => "", "scope" => "read write", @@ -210,7 +210,7 @@ mod unit_tests { fn should_return_invalid_request_on_missing_password() { let result = validate_password_grant( ClientPrincipal::new_confidential_principal("aardvark"), - map_of! { + &map_of! { "username" => "aardvark", "scope" => "read write", }, @@ -232,7 +232,7 @@ mod unit_tests { fn should_return_invalid_request_on_blank_scope() { let result = validate_password_grant( ClientPrincipal::new_confidential_principal("aardvark"), - map_of! { + &map_of! { "username" => "aardvark", "password" => "", "scope" => " ", @@ -258,7 +258,7 @@ mod unit_tests { allowed_actions: Default::default(), allowed_grant_types: HashSet::from([Password]), }), - map_of! { + &map_of! { "username" => "aardvark", "password" => "", "scope" => "invalid", @@ -277,7 +277,7 @@ mod unit_tests { fn should_return_invalid_request_with_an_invalid_scope_and_a_valid_scope() { let result = validate_password_grant( ClientPrincipal::new_confidential_principal("aardvark"), - map_of! { "username" => "aardvark", + &map_of! { "username" => "aardvark", "password" => "", "scope" => "basic cicada", }, @@ -295,7 +295,7 @@ mod unit_tests { fn should_return_invalid_request_with_an_duplicated_valid_scopes() { let result = validate_password_grant( ClientPrincipal::new_confidential_principal("aardvark"), - map_of! { + &map_of! { "username" => "aardvark", "password" => "", "scope" => "basic basic", @@ -321,7 +321,7 @@ mod unit_tests { allowed_actions: Default::default(), allowed_grant_types: HashSet::from([Password]), }), - map_of! { + &map_of! { "username" => "aardvark", "password" => "", "scope" => "write", @@ -344,7 +344,7 @@ mod unit_tests { fn should_return_valid_request_if_only_scope_is_not_provided() { let result = validate_password_grant( ClientPrincipal::new_confidential_principal("aardvark"), - map_of! { + &map_of! { "username" => "aardvark", "password" => "", }, @@ -364,7 +364,7 @@ mod unit_tests { fn should_return_valid_request_if_only_one_scope_is_provided() { let result = validate_password_grant( ClientPrincipal::new_confidential_principal("aardvark"), - map_of! { + &map_of! { "username" => "aardvark", "password" => "", "scope" => "basic", @@ -385,7 +385,7 @@ mod unit_tests { fn should_return_valid_request_if_multiple_scopes_are_provided() { let result = validate_password_grant( ClientPrincipal::new_confidential_principal("aardvark"), - map_of! { + &map_of! { "username" => "aardvark", "password" => "", "scope" => "basic read write", diff --git a/src/token_exchange/request.rs b/src/token_exchange/request.rs index ba07f4a..9a57359 100644 --- a/src/token_exchange/request.rs +++ b/src/token_exchange/request.rs @@ -40,8 +40,8 @@ where }))?; match Form::>::from_request(req, state).await { - Err(rejection) => Err(handle_form_rejection(rejection)), - Ok(Form(request)) => match validate_grant_type(principal, request) { + Err(rejection) => Err(handle_form_rejection(&rejection)), + Ok(Form(request)) => match validate_grant_type(principal, &request) { Err(failure) => Err(handle_validation_failure(failure)), Ok(valid) => Ok(valid), } @@ -49,7 +49,7 @@ where } } -pub fn validate_grant_type(principal: ClientPrincipal, request: HashMap) -> Result { +pub fn validate_grant_type(principal: ClientPrincipal, request: &HashMap) -> Result { match request.get("grant_type").map(|s| s.parse::()) { None => Err(TokenExchangeResponse::missing_parameter("grant_type")), @@ -64,7 +64,7 @@ pub fn validate_grant_type(principal: ClientPrincipal, request: HashMap Err( TokenExchangeResponse::Failure { error: ErrorType::UnauthorizedClient, - error_description: Some(format!("not authorized to: {:?}", grant_type)), + error_description: Some(format!("not authorized to: {grant_type:?}")), } ), @@ -78,7 +78,7 @@ fn handle_validation_failure(failure: TokenExchangeResponse) -> Response { (StatusCode::BAD_REQUEST, Json(failure)).into_response() } -fn handle_form_rejection(rejection: FormRejection) -> Response { +fn handle_form_rejection(rejection: &FormRejection) -> Response { (rejection.status(), Json(TokenExchangeResponse::Failure { error: ErrorType::InvalidRequest, error_description: Some(rejection.body_text()), @@ -123,14 +123,14 @@ mod unit_tests { validate_err! { should_return_invalid_request_on_missing_grant_type, ClientPrincipal::new_confidential_principal("aardvark"), - input_parameters! {}, + &input_parameters! {}, TokenExchangeResponse::missing_parameter("grant_type") } validate_err! { should_return_invalid_request_on_blank_grant_type, ClientPrincipal::new_confidential_principal("aardvark"), - input_parameters! { "grant_type" => " " }, + &input_parameters! { "grant_type" => " " }, TokenExchangeResponse::Failure { error: ErrorType::UnsupportedGrantType, error_description: Some("unsupported: ".into()) @@ -140,7 +140,7 @@ mod unit_tests { validate_err! { should_return_invalid_request_on_unsupported_grant_type, ClientPrincipal::new_confidential_principal("aardvark"), - input_parameters! { "grant_type" => "aardvark" }, + &input_parameters! { "grant_type" => "aardvark" }, TokenExchangeResponse::Failure { error: ErrorType::UnsupportedGrantType, error_description: Some("unsupported: aardvark".into()) @@ -157,7 +157,7 @@ mod unit_tests { allowed_actions: Default::default(), allowed_grant_types: Default::default(), }), - input_parameters! { "grant_type" => "password" }, + &input_parameters! { "grant_type" => "password" }, TokenExchangeResponse::Failure { error: ErrorType::UnauthorizedClient, error_description: Some("not authorized to: Password".into()) @@ -167,7 +167,7 @@ mod unit_tests { validate_ok! { should_return_valid_request_for_password_grant_type, ClientPrincipal::new_confidential_principal("aardvark"), - input_parameters! { "grant_type" => "password", "username" => "aardvark", "password" => "" }, + &input_parameters! { "grant_type" => "password", "username" => "aardvark", "password" => "" }, TokenExchangeForm(Password(PasswordGrantRequest { principal: ClientPrincipal::new_confidential_client("aardvark"), username: "aardvark".into(), diff --git a/src/token_exchange/response.rs b/src/token_exchange/response.rs index 18c7719..d3a873b 100644 --- a/src/token_exchange/response.rs +++ b/src/token_exchange/response.rs @@ -88,13 +88,13 @@ pub enum ErrorType { // authorization server MUST respond with an HTTP 401 (Unauthorized) status code // and include the "WWW-Authenticate" response header field matching the // authentication scheme used by the client. - InvalidClient, + // TODO - InvalidClient, // The provided authorization grant (e.g., authorization code, // resource owner credentials) or refresh token is invalid, expired, revoked, // does not match the redirection URI used in the authorization request, or was // issued to another client. - InvalidGrant, + // TODO - InvalidGrant, // The requested scope is invalid, unknown, malformed, or exceeds // the scope granted by the resource owner. diff --git a/src/token_exchange/route.rs b/src/token_exchange/route.rs index 28057de..aa82708 100644 --- a/src/token_exchange/route.rs +++ b/src/token_exchange/route.rs @@ -3,7 +3,7 @@ use axum::extract::State; use axum::http::StatusCode; use axum::{middleware, Router}; use axum::routing::post; -use axum::response::{IntoResponse, Json}; +use axum::response::Json; use middleware::from_fn_with_state; use crate::client::authentication::ClientAuthenticator; use crate::client::middleware::require_client_authentication; @@ -78,7 +78,7 @@ mod integration_tests { macro_rules! under_test { () => { route(TokenExchangeState { - access_token_repository: crate::token::repository::InMemoryTokenRepository::new(), + access_token_repository: crate::token::repository::InMemoryAccessTokenRepository::new(), client_authenticator: crate::client::authentication::ClientAuthenticationService::new( crate::client::secret::InMemoryClientSecretRepository::new(), crate::client::configuration::InMemoryClientConfigurationRepository::new(), diff --git a/src/token_introspection/request.rs b/src/token_introspection/request.rs index d01f861..a43b575 100644 --- a/src/token_introspection/request.rs +++ b/src/token_introspection/request.rs @@ -17,8 +17,8 @@ impl FromRequest for TokenIntrospectionForm { type Rejection = Response; async fn from_request(req: Request, state: &S) -> Result { match Form::>::from_request(req, state).await { - Err(rejection) => Err(handle_form_rejection(rejection)), - Ok(Form(request)) => match validate_request(request) { + Err(rejection) => Err(handle_form_rejection(&rejection)), + Ok(Form(request)) => match validate_request(&request) { Err(failure) => Err(handle_validation_failure(failure)), Ok(valid) => Ok(valid), } @@ -26,7 +26,7 @@ impl FromRequest for TokenIntrospectionForm { } } -fn validate_request(request: HashMap) -> Result { +fn validate_request(request: &HashMap) -> Result { match request.get("token").map(|s| Uuid::parse_str(s)) { None => Err(TokenIntrospectionResponse::missing_parameter("token")), Some(Err(_)) => Err(TokenIntrospectionResponse::invalid_parameter("token")), @@ -38,7 +38,7 @@ fn handle_validation_failure(failure: TokenIntrospectionResponse) -> Response { (StatusCode::BAD_REQUEST, Json(failure)).into_response() } -fn handle_form_rejection(rejection: FormRejection) -> Response { +fn handle_form_rejection(rejection: &FormRejection) -> Response { (rejection.status(), Json(TokenIntrospectionResponse::Invalid { error: ErrorType::InvalidRequest, error_description: Some(rejection.body_text()), diff --git a/src/token_introspection/route.rs b/src/token_introspection/route.rs index 9f042d8..0cf09d4 100644 --- a/src/token_introspection/route.rs +++ b/src/token_introspection/route.rs @@ -44,7 +44,7 @@ pub struct TokenIntrospectionState, C: ClientAut async fn token_introspection_handler, C: ClientAuthenticator>( State(state): State>, - Extension(client): Extension, + Extension(_client): Extension, TokenIntrospectionForm(request): TokenIntrospectionForm, ) -> Result { diff --git a/src/util/map_of.rs b/src/util/map_of.rs index 8d93d8a..6e1f9eb 100644 --- a/src/util/map_of.rs +++ b/src/util/map_of.rs @@ -34,6 +34,7 @@ mod test { #[test] fn should_be_able_to_create_a_string_to_enum_map() { + #[allow(clippy::zero_sized_map_values)] let map: HashMap = map_of! { "password" => GrantType::Password, }; diff --git a/src/util/value_struct.rs b/src/util/value_struct.rs index 11ecb8c..751dba0 100644 --- a/src/util/value_struct.rs +++ b/src/util/value_struct.rs @@ -19,7 +19,7 @@ macro_rules! value_struct { #[cfg_attr(test, derive(Debug))] $vis struct $struct_name($field_type); - impl crate::util::value_struct::ValueStruct for $struct_name { + impl $crate::util::value_struct::ValueStruct for $struct_name { type ValueType = $field_type; #[inline] From 11853a45ce4243c696488dac42dbe337afae5724 Mon Sep 17 00:00:00 2001 From: James Bacon Date: Thu, 7 May 2026 16:29:56 +0100 Subject: [PATCH 08/19] Refactored token repository types into sub modules Also snuck in some unit tests --- Cargo.lock | 1 + Cargo.toml | 3 +- README.md | 2 +- src/main.rs | 6 +- src/token/mod.rs | 23 +++- src/token/repository.rs | 203 --------------------------------- src/token/repository/diesel.rs | 165 +++++++++++++++++++++++++++ src/token/repository/memory.rs | 82 +++++++++++++ src/token/repository/mod.rs | 19 +++ src/token/repository/sqlx.rs | 81 +++++++++++++ src/token_exchange/route.rs | 2 +- 11 files changed, 376 insertions(+), 211 deletions(-) delete mode 100644 src/token/repository.rs create mode 100644 src/token/repository/diesel.rs create mode 100644 src/token/repository/memory.rs create mode 100644 src/token/repository/mod.rs create mode 100644 src/token/repository/sqlx.rs diff --git a/Cargo.lock b/Cargo.lock index 5da2ae3..71553b8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1253,6 +1253,7 @@ dependencies = [ "diesel_migrations", "form_urlencoded", "http-body-util", + "libsqlite3-sys", "serde", "serde_json", "sqlx", diff --git a/Cargo.toml b/Cargo.toml index fd0e534..456295d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -6,7 +6,7 @@ edition = "2024" [features] default = ["diesel"] sqlx = ["dep:sqlx"] -diesel = ["dep:diesel", "dep:diesel-async", "dep:diesel_migrations"] +diesel = ["dep:diesel", "dep:diesel-async", "dep:diesel_migrations", "dep:libsqlite3-sys"] [dependencies] argon2 = "0.5.3" @@ -20,6 +20,7 @@ tower = "0.5.3" diesel = { version = "2.3.9", optional = true, features = ["returning_clauses_for_sqlite_3_35", "chrono"] } diesel-async = { version = "0.9.0", optional = true, features = ["sqlite", "deadpool", "migrations"] } diesel_migrations = { version = "2.3.2", optional = true } +libsqlite3-sys = { version = "*", optional = true, features = ["bundled"] } chrono = { version = "0.4.44", features = ["serde"] } sqlx = { version = "0.8.6", optional = true, features = [ "runtime-tokio", "sqlite", "uuid", "chrono", "derive", "macros" ] } anyhow = "1.0.102" diff --git a/README.md b/README.md index 1f4014c..f6b88cc 100644 --- a/README.md +++ b/README.md @@ -61,7 +61,7 @@ cargo run ### Checking its running -Hit the token exchange endpoint with a password grant _(yeah its deprecated; but it's a quick lazy way to start)_. +Hit the token exchange endpoint with a password grant _(yeah, it's deprecated; but it's a quick lazy way to start)_. ```bash curl -v -X POST -H 'Content-Type: application/x-www-form-urlencoded' -u 'aardvark:badger' -d 'grant_type=password&scope=basic&username=aardvark&password=P%4055w0rd' http://127.0.0.1:8080/token ``` diff --git a/src/main.rs b/src/main.rs index 0dea474..512b67e 100644 --- a/src/main.rs +++ b/src/main.rs @@ -53,16 +53,16 @@ async fn main() -> Result<(), Box> { // or do we just pass connection pools and do await with services and repositories? #[cfg(not(any(feature = "diesel", feature = "sqlx")))] - let access_token_repository = token::repository::InMemoryAccessTokenRepository + let access_token_repository = token::repository::memory::InMemoryAccessTokenRepository ::new(); #[cfg(feature = "sqlx")] - let access_token_repository = token::repository::SqlxSqliteAccessTokenRepository + let access_token_repository = token::repository::sqlx::SqlxSqliteAccessTokenRepository ::new("file:target/db/access_tokens.sqlite3") .await?; #[cfg(feature = "diesel")] - let access_token_repository = token::repository::DieselSqliteAccessTokenRepository + let access_token_repository = token::repository::diesel::DieselSqliteAccessTokenRepository ::new("file:target/db/access_tokens.sqlite3")?; #[cfg(feature = "diesel")] diff --git a/src/token/mod.rs b/src/token/mod.rs index 5f43e93..ca58bc9 100644 --- a/src/token/mod.rs +++ b/src/token/mod.rs @@ -13,9 +13,10 @@ pub enum TokenType { Bearer, } -#[derive(Serialize, Clone)] +#[derive(Serialize, Clone, Eq, PartialEq)] +#[cfg_attr(test, derive(Debug))] #[cfg_attr(feature = "sqlx", derive(sqlx::FromRow))] -#[cfg_attr(feature = "diesel", derive(diesel::Queryable, diesel::Selectable, diesel::Insertable))] +#[cfg_attr(feature = "diesel", derive(diesel::Queryable, diesel::Selectable, diesel::Insertable, diesel::Identifiable))] #[cfg_attr(feature = "diesel", diesel(table_name = schema::access_tokens))] #[cfg_attr(feature = "diesel", diesel(check_for_backend(diesel::sqlite::Sqlite)))] pub struct AccessToken { @@ -27,3 +28,21 @@ pub struct AccessToken { pub expires_at: chrono::NaiveDateTime, pub not_before: chrono::NaiveDateTime, } + +#[cfg(test)] +pub mod test_support { + use super::*; + impl AccessToken { + pub fn new() -> AccessToken { + AccessToken { + id: UuidWrapper::random(), + username: "aardvark".to_string(), + client_id: "badger".to_string(), + scopes: "basic".to_string(), + issued_at: chrono::Utc::now().naive_utc(), + expires_at: chrono::Utc::now().naive_utc(), + not_before: chrono::Utc::now().naive_utc(), + } + } + } +} \ No newline at end of file diff --git a/src/token/repository.rs b/src/token/repository.rs deleted file mode 100644 index bb9209c..0000000 --- a/src/token/repository.rs +++ /dev/null @@ -1,203 +0,0 @@ -use crate::token::AccessToken; -use anyhow::Result; -use crate::util::uuid_wrapper::UuidWrapper; - -#[cfg(any(test, not(any(feature = "diesel", feature = "sqlx"))))] -use { - std::collections::HashMap, - std::sync::{Arc, Mutex, MutexGuard, PoisonError}, -}; - -#[cfg(any(feature = "diesel", feature = "sqlx"))] -use { - anyhow::Context, -}; - -#[cfg(feature = "diesel")] -use { - std::error::Error, - diesel::prelude::*, - diesel_async::RunQueryDsl, - diesel_async::pooled_connection::AsyncDieselConnectionManager, - diesel_async::pooled_connection::deadpool::Pool, - diesel_async::sync_connection_wrapper::SyncConnectionWrapper, -}; - -#[trait_variant::make(Send)] -pub trait TokenRepository: Sync + Clone { - async fn get_token(&self, id: UuidWrapper) -> Result>; - async fn save_token(&self, token: &T) -> Result<()>; -} - -#[derive(Clone, Default)] -#[cfg(any(test, not(any(feature = "diesel", feature = "sqlx"))))] -pub struct InMemoryAccessTokenRepository { - store: Arc>>, -} - -#[cfg(any(test, not(any(feature = "diesel", feature = "sqlx"))))] -impl InMemoryAccessTokenRepository { - pub fn new() -> Self { - Self { store: Arc::new(Mutex::new(HashMap::new())) } - } - fn lock_store(&self) -> MutexGuard<'_, HashMap> { - self.store.lock().unwrap_or_else(PoisonError::into_inner) - } -} - -#[cfg(any(test, not(any(feature = "diesel", feature = "sqlx"))))] -impl TokenRepository for InMemoryAccessTokenRepository -{ - async fn get_token(&self, id: UuidWrapper) -> Result> { - Ok(self.lock_store().get(&id).cloned()) - } - - async fn save_token(&self, token: &AccessToken) -> Result<()> { - self.lock_store().insert(token.id, token.clone()); - Ok(()) - } -} - -#[cfg(feature = "diesel")] -type AsyncSqliteConnection = SyncConnectionWrapper; -#[cfg(feature = "diesel")] -type AsyncSqlitePool = Pool; - -#[derive(Clone)] -#[cfg(feature = "diesel")] -pub struct DieselSqliteAccessTokenRepository { - pool: AsyncSqlitePool, -} - -#[cfg(feature = "diesel")] -impl DieselSqliteAccessTokenRepository { - pub fn new(database_url: &str) -> Result { - - let manager = AsyncDieselConnectionManager::::new(database_url); - - let pool = AsyncSqlitePool::builder(manager) - .max_size(5) - .build() - .with_context(|| format!("Failed to create Diesel sqlite database pool: {database_url}"))?; - - Ok(Self { - pool - }) - } - pub async fn run_diesel_migrations(&self) -> Result<(), Box> { - use diesel_async::AsyncMigrationHarness; - use diesel_migrations::{embed_migrations, EmbeddedMigrations, MigrationHarness}; - const ACCESS_TOKEN_MIGRATIONS: EmbeddedMigrations = embed_migrations!("migrations/access_tokens"); - - let connection = self.pool - .get() - .await - .with_context(|| "Failed to get connection from pool")?; - - let mut harness = AsyncMigrationHarness::new(connection); - - harness.run_pending_migrations(ACCESS_TOKEN_MIGRATIONS)?; - - Ok(()) - } -} - -#[cfg(feature = "diesel")] -impl TokenRepository for DieselSqliteAccessTokenRepository { - - async fn get_token(&self, token: UuidWrapper) -> Result> { - use super::schema::access_tokens; - use super::schema::access_tokens::dsl::id; - - let connection = &mut self.pool.get() - .await - .with_context(|| "Failed to get connection from pool")?; - - let result = access_tokens::table - .filter(id.eq(token)) - .first::(connection) - .await - .optional() - .with_context(|| "Error querying access token database")?; - - Ok(result) - } - - async fn save_token(&self, token: &AccessToken) -> Result<()> { - - use super::schema::access_tokens::dsl::access_tokens; - use diesel::dsl::insert_into; - use diesel_async::RunQueryDsl; - - let connection = &mut self.pool.get() - .await - .with_context(|| "Failed to get connection from pool")?; - - insert_into(access_tokens) - .values(token) - .execute(connection) - .await - .with_context(|| "Error saving access token to database")?; - - Ok(()) - } -} - -#[derive(Clone)] -#[cfg(feature = "sqlx")] -pub struct SqlxSqliteAccessTokenRepository { - pool: sqlx::Pool, -} - -#[cfg(feature = "sqlx")] -impl SqlxSqliteAccessTokenRepository { - pub async fn new(database_url: &str) -> Result { - Ok( - Self { - pool: sqlx::sqlite::SqlitePoolOptions::new() - .min_connections(1) - .max_connections(5) - .connect(database_url) - .await - .with_context(|| - format!("Failed to create SQLX sqlite database pool: {database_url}") - )? - } - ) - } -} - -#[cfg(feature = "sqlx")] -impl TokenRepository for SqlxSqliteAccessTokenRepository { - - async fn get_token(&self, token: UuidWrapper) -> Result> { - - let result = sqlx::query_as::<_, AccessToken>("SELECT * FROM access_tokens WHERE id = ?;") - .bind(token) - .fetch_optional(&self.pool) - .await - .with_context(|| "Error querying access token database")?; - - Ok(result) - } - - async fn save_token(&self, token: &AccessToken) -> Result<()> { - - sqlx::query(" - INSERT INTO access_tokens (id, username, client_id, scopes, issued_at, expires_at, not_before) - VALUES (?, ?, ?, ?, ?, ?, ?); - ") - .bind(token.id) - .bind(&token.username) - .bind(&token.client_id) - .bind(&token.scopes) - .bind(token.issued_at) - .bind(token.expires_at) - .bind(token.not_before) - .execute(&self.pool) - .await - .with_context(|| "Error saving access token to database")?; - - Ok(()) - } -} diff --git a/src/token/repository/diesel.rs b/src/token/repository/diesel.rs new file mode 100644 index 0000000..e1f357f --- /dev/null +++ b/src/token/repository/diesel.rs @@ -0,0 +1,165 @@ +use anyhow::Result; +use anyhow::Context; + +use diesel::prelude::*; +use diesel_async::{AsyncConnection, RunQueryDsl}; +use diesel_async::pooled_connection::AsyncDieselConnectionManager; +use diesel_async::pooled_connection::deadpool::Pool; +use diesel_async::sync_connection_wrapper::SyncConnectionWrapper; + +use std::error::Error; + +use crate::token::schema::access_tokens::dsl::access_tokens; +use crate::token::schema::access_tokens::dsl::id; + +use crate::token::AccessToken; +use crate::token::repository::TokenRepository; +use crate::util::uuid_wrapper::UuidWrapper; + +type AsyncSqliteConnection = SyncConnectionWrapper; +type AsyncSqliteConnectionManager = AsyncDieselConnectionManager; +type AsyncSqlitePool = Pool; + +#[derive(Clone)] +pub struct DieselSqliteAccessTokenRepository { + pool: AsyncSqlitePool, +} + +impl DieselSqliteAccessTokenRepository { + pub fn new(database_url: &str) -> Result { + + let manager = AsyncSqliteConnectionManager::new(database_url); + + let pool = AsyncSqlitePool::builder(manager) + .max_size(10) + .build() + .with_context(|| format!("Failed to create Diesel sqlite database pool: {database_url}"))?; + + Ok(Self { + pool + }) + } + pub async fn run_diesel_migrations(&self) -> Result<(), Box> { + use diesel_async::AsyncMigrationHarness; + use diesel_migrations::{embed_migrations, EmbeddedMigrations, MigrationHarness}; + const ACCESS_TOKEN_MIGRATIONS: EmbeddedMigrations = embed_migrations!("migrations/access_tokens"); + + let connection = self.pool + .get() + .await + .with_context(|| "Failed to get connection from pool")?; + + let mut harness = AsyncMigrationHarness::new(connection); + + harness.run_pending_migrations(ACCESS_TOKEN_MIGRATIONS)?; + + Ok(()) + } +} + +impl TokenRepository for DieselSqliteAccessTokenRepository { + + async fn get_token(&self, token: UuidWrapper) -> Result> { + let connection = &mut self.pool.get() + .await + .with_context(|| "Failed to get connection from pool")?; + + let result = access_tokens + .filter(id.eq(token)) + .first::(connection) + .await + .optional() + .with_context(|| "Error querying access token database")?; + + Ok(result) + } + + async fn save_token(&self, token: &AccessToken) -> Result<()> { + let connection = &mut self.pool.get() + .await + .with_context(|| "Failed to get connection from pool")?; + + diesel::insert_into(access_tokens) + .values(token) + .execute(connection) + .await + .with_context(|| "Error saving access token to database")?; + + Ok(()) + } + + async fn delete_token(&self, token: UuidWrapper) -> Result<()> { + let connection = &mut self.pool.get() + .await + .with_context(|| "Failed to get connection from pool")?; + + connection.transaction(async |connection| { + diesel::delete(access_tokens) + .filter(id.eq(token)) + .execute(connection) + .await + .and_then(|deleted_rows| { + if deleted_rows == 1 || deleted_rows == 0 { + Ok(()) + } else { + Err(diesel::result::Error::RollbackTransaction) + } + }) + }) + .await + .with_context(|| "Error deleting access token from database") + } +} + +#[cfg(test)] +mod unit_tests { + + use super::*; + use assertables::*; + + impl std::fmt::Debug for DieselSqliteAccessTokenRepository { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("DieselSqliteAccessTokenRepository") + .field("pool.manager", &self.pool.manager()) + .finish() + } + } + + async fn under_test() -> DieselSqliteAccessTokenRepository { + let db = assert_ok!(DieselSqliteAccessTokenRepository::new(":memory:")); + assert_ok!(db.run_diesel_migrations().await); + db + } + + #[tokio::test(flavor = "multi_thread")] + async fn should_be_able_to_save_and_retrieve_a_token() { + let under_test = under_test().await; + let token = AccessToken::new(); + assert_ok!(under_test.save_token(&token).await); + assert_eq!(assert_some!(assert_ok!(under_test.get_token(token.id).await)), token); + } + + #[tokio::test(flavor = "multi_thread")] + async fn should_be_able_to_delete_a_token() { + let under_test = under_test().await; + let token = AccessToken::new(); + assert_ok!(under_test.save_token(&token).await); + assert_ok!(under_test.delete_token(token.id).await); + assert_none!(assert_ok!(under_test.get_token(token.id).await)); + } + + #[tokio::test(flavor = "multi_thread")] + async fn should_be_able_to_delete_a_token_when_non_existent() { + let under_test = under_test().await; + assert_ok!(under_test.delete_token(UuidWrapper::random()).await); + } + + #[tokio::test(flavor = "multi_thread")] + async fn should_be_able_to_clone_but_share_storage() { + let first = under_test().await; + let second = first.clone(); + let token = AccessToken::new(); + assert_ok!(first.save_token(&token).await); + assert_eq!(assert_some!(assert_ok!(second.get_token(token.id).await)), token); + } +} \ No newline at end of file diff --git a/src/token/repository/memory.rs b/src/token/repository/memory.rs new file mode 100644 index 0000000..a0c4288 --- /dev/null +++ b/src/token/repository/memory.rs @@ -0,0 +1,82 @@ +use anyhow::Result; +use std::collections::HashMap; +use std::sync::{Arc, Mutex, MutexGuard, PoisonError}; +use crate::token::AccessToken; +use crate::token::repository::TokenRepository; +use crate::util::uuid_wrapper::UuidWrapper; + +#[derive(Clone, Default)] +pub struct InMemoryAccessTokenRepository { + store: Arc>>, +} + +impl InMemoryAccessTokenRepository { + pub fn new() -> Self { + Self { store: Arc::new(Mutex::new(HashMap::new())) } + } + fn lock_store(&self) -> MutexGuard<'_, HashMap> { + self.store.lock().unwrap_or_else(PoisonError::into_inner) + } +} + +impl TokenRepository for InMemoryAccessTokenRepository +{ + async fn get_token(&self, id: UuidWrapper) -> Result> { + Ok(self.lock_store().get(&id).cloned()) + } + + async fn save_token(&self, token: &AccessToken) -> Result<()> { + self.lock_store().insert(token.id, token.clone()); + Ok(()) + } + + async fn delete_token(&self, id: UuidWrapper) -> Result<()> { + self.lock_store().remove(&id); + Ok(()) + } +} + +#[cfg(test)] +mod unit_tests { + + use super::*; + use assertables::*; + + #[tokio::test] + async fn should_initialise_empty() { + let under_test = InMemoryAccessTokenRepository::new(); + assert_is_empty!(under_test.lock_store()); + } + + #[tokio::test] + async fn should_be_able_to_save_and_retrieve_a_token() { + let under_test = InMemoryAccessTokenRepository::new(); + let token = AccessToken::new(); + assert_ok!(under_test.save_token(&token).await); + assert_eq!(assert_some!(assert_ok!(under_test.get_token(token.id).await)), token); + } + + #[tokio::test] + async fn should_be_able_to_delete_a_token() { + let under_test = InMemoryAccessTokenRepository::new(); + let token = AccessToken::new(); + assert_ok!(under_test.save_token(&token).await); + assert_ok!(under_test.delete_token(token.id).await); + assert_none!(assert_ok!(under_test.get_token(token.id).await)); + } + + #[tokio::test] + async fn should_be_able_to_delete_a_token_when_non_existent() { + let under_test = InMemoryAccessTokenRepository::new(); + assert_ok!(under_test.delete_token(UuidWrapper::random()).await); + } + + #[tokio::test] + async fn should_be_able_to_clone_but_share_storage() { + let first = InMemoryAccessTokenRepository::new(); + let second = first.clone(); + let token = AccessToken::new(); + assert_ok!(first.save_token(&token).await); + assert_eq!(assert_some!(assert_ok!(second.get_token(token.id).await)), token); + } +} \ No newline at end of file diff --git a/src/token/repository/mod.rs b/src/token/repository/mod.rs new file mode 100644 index 0000000..779f143 --- /dev/null +++ b/src/token/repository/mod.rs @@ -0,0 +1,19 @@ +#[cfg(any(test, not(any(feature = "diesel", feature = "sqlx"))))] +pub mod memory; + +#[cfg(feature = "sqlx")] +pub mod sqlx; + +#[cfg(feature = "diesel")] +pub mod diesel; + +use crate::util::uuid_wrapper::UuidWrapper; +use anyhow::Result; + +#[trait_variant::make(Send)] +pub trait TokenRepository: Sync + Clone { + async fn get_token(&self, id: UuidWrapper) -> Result>; + async fn save_token(&self, token: &T) -> Result<()>; + #[allow(dead_code)] // TODO - Remove after we implement token revocation. + async fn delete_token(&self, id: UuidWrapper) -> Result<()>; +} diff --git a/src/token/repository/sqlx.rs b/src/token/repository/sqlx.rs new file mode 100644 index 0000000..041544c --- /dev/null +++ b/src/token/repository/sqlx.rs @@ -0,0 +1,81 @@ +use anyhow::Context; +use anyhow::Result; +use diesel::QueryDsl; +use sqlx::Pool; +use sqlx::Sqlite; +use sqlx::sqlite::SqlitePoolOptions; + +use crate::token::AccessToken; +use crate::token::repository::TokenRepository; +use crate::util::uuid_wrapper::UuidWrapper; + +#[derive(Clone)] +pub struct SqlxSqliteAccessTokenRepository { + pool: Pool, +} + +impl SqlxSqliteAccessTokenRepository { + pub async fn new(database_url: &str) -> Result { + Ok( + Self { + pool: SqlitePoolOptions::new() + .min_connections(1) + .max_connections(5) + .connect(database_url) + .await + .with_context(|| + format!("Failed to create SQLX sqlite database pool: {database_url}") + )? + } + ) + } +} + +impl TokenRepository for SqlxSqliteAccessTokenRepository { + + async fn get_token(&self, token: UuidWrapper) -> Result> { + + let result = sqlx::query_as::<_, AccessToken>("SELECT * FROM access_tokens WHERE id = ?;") + .bind(token) + .fetch_optional(&self.pool) + .await + .with_context(|| "Error querying access token database")?; + + Ok(result) + } + + async fn save_token(&self, token: &AccessToken) -> Result<()> { + + sqlx::query(" + INSERT INTO access_tokens (id, username, client_id, scopes, issued_at, expires_at, not_before) + VALUES (?, ?, ?, ?, ?, ?, ?); + ") + .bind(token.id) + .bind(&token.username) + .bind(&token.client_id) + .bind(&token.scopes) + .bind(token.issued_at) + .bind(token.expires_at) + .bind(token.not_before) + .execute(&self.pool) + .await + .with_context(|| "Error saving access token to database")?; + + Ok(()) + } + + async fn delete_token(&self, token: UuidWrapper) -> Result<()> { + + sqlx::query(" + DELETE FROM access_tokens WHERE id IN ( + SELECT id FROM access_tokens WHERE id = ? LIMIT 1 + ); + ") + .bind(token) + .execute(&self.pool) + .await + .with_context(|| "Error deleting access token from database")?; + + Ok(()) + } +} \ No newline at end of file diff --git a/src/token_exchange/route.rs b/src/token_exchange/route.rs index aa82708..4204472 100644 --- a/src/token_exchange/route.rs +++ b/src/token_exchange/route.rs @@ -78,7 +78,7 @@ mod integration_tests { macro_rules! under_test { () => { route(TokenExchangeState { - access_token_repository: crate::token::repository::InMemoryAccessTokenRepository::new(), + access_token_repository: crate::token::repository::memory::InMemoryAccessTokenRepository::new(), client_authenticator: crate::client::authentication::ClientAuthenticationService::new( crate::client::secret::InMemoryClientSecretRepository::new(), crate::client::configuration::InMemoryClientConfigurationRepository::new(), From 2543b2c7b1fba0a17ba4c63aa0e4f6a09d538944 Mon Sep 17 00:00:00 2001 From: James Bacon Date: Thu, 7 May 2026 16:38:40 +0100 Subject: [PATCH 09/19] Purged sqlx --- Cargo.lock | 1133 +--------------------------------- Cargo.toml | 2 - README.md | 1 - src/main.rs | 7 +- src/token/mod.rs | 1 - src/token/repository/mod.rs | 5 +- src/token/repository/sqlx.rs | 81 --- src/util/uuid_wrapper.rs | 2 - src/util/value_struct.rs | 2 - 9 files changed, 10 insertions(+), 1224 deletions(-) delete mode 100644 src/token/repository/sqlx.rs diff --git a/Cargo.lock b/Cargo.lock index 71553b8..992ea1f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -11,12 +11,6 @@ dependencies = [ "memchr", ] -[[package]] -name = "allocator-api2" -version = "0.2.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" - [[package]] name = "android_system_properties" version = "0.1.5" @@ -54,15 +48,6 @@ dependencies = [ "walkdir", ] -[[package]] -name = "atoi" -version = "2.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f28d99ec8bfea296261ca1af174f24225171fea9664ba9003cbebee704810528" -dependencies = [ - "num-traits", -] - [[package]] name = "atomic-waker" version = "1.1.2" @@ -178,9 +163,6 @@ name = "bitflags" version = "2.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af" -dependencies = [ - "serde_core", -] [[package]] name = "blake2" @@ -206,12 +188,6 @@ version = "3.19.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5dd9dc738b7a8311c7ade152424974d8115f2cdad61e8dab8dac9f2362298510" -[[package]] -name = "byteorder" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" - [[package]] name = "bytes" version = "1.11.1" @@ -248,21 +224,6 @@ dependencies = [ "windows-link", ] -[[package]] -name = "concurrent-queue" -version = "2.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973" -dependencies = [ - "crossbeam-utils", -] - -[[package]] -name = "const-oid" -version = "0.9.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" - [[package]] name = "core-foundation-sys" version = "0.8.7" @@ -278,36 +239,6 @@ dependencies = [ "libc", ] -[[package]] -name = "crc" -version = "3.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5eb8a2a1cd12ab0d987a5d5e825195d372001a4094a0376319d5a0ad71c1ba0d" -dependencies = [ - "crc-catalog", -] - -[[package]] -name = "crc-catalog" -version = "2.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "217698eaf96b4a3f0bc4f3662aaa55bdf913cd54d7204591faa790070c6d0853" - -[[package]] -name = "crossbeam-queue" -version = "0.3.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0f58bbc28f91df819d0aa2a2c00cd19754769c2fad90579b3592b1c9ba7a3115" -dependencies = [ - "crossbeam-utils", -] - -[[package]] -name = "crossbeam-utils" -version = "0.8.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" - [[package]] name = "crypto-common" version = "0.1.7" @@ -370,17 +301,6 @@ version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2657f61fb1dd8bf37a8d51093cc7cee4e77125b22f7753f49b289f831bec2bae" -[[package]] -name = "der" -version = "0.7.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e7c1832837b905bbfb5101e07cc24c8deddf52f93225eee6ead5f4d63d53ddcb" -dependencies = [ - "const-oid", - "pem-rfc7468", - "zeroize", -] - [[package]] name = "deranged" version = "0.5.8" @@ -459,28 +379,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" dependencies = [ "block-buffer", - "const-oid", "crypto-common", "subtle", ] -[[package]] -name = "displaydoc" -version = "0.2.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "dotenvy" -version = "0.15.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1aaf95b3e5c8f23aa320147307562d361db0ae0d51242340f558153b4eb2439b" - [[package]] name = "downcast-rs" version = "2.0.2" @@ -506,9 +408,6 @@ name = "either" version = "1.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" -dependencies = [ - "serde", -] [[package]] name = "equivalent" @@ -523,29 +422,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ "libc", - "windows-sys 0.61.2", -] - -[[package]] -name = "etcetera" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "136d1b5283a1ab77bd9257427ffd09d8667ced0570b6f938942bc7568ed5b943" -dependencies = [ - "cfg-if", - "home", - "windows-sys 0.48.0", -] - -[[package]] -name = "event-listener" -version = "5.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e13b66accf52311f30a0db42147dadea9850cb48cd070028831ae5f5d4b856ab" -dependencies = [ - "concurrent-queue", - "parking", - "pin-project-lite", + "windows-sys", ] [[package]] @@ -554,17 +431,6 @@ version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" -[[package]] -name = "flume" -version = "0.11.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da0e4dd2a88388a1f4ccc7c9ce104604dab68d9f408dc34cd45823d5a9069095" -dependencies = [ - "futures-core", - "futures-sink", - "spin", -] - [[package]] name = "fnv" version = "1.0.7" @@ -599,7 +465,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" dependencies = [ "futures-core", - "futures-sink", ] [[package]] @@ -608,34 +473,6 @@ version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" -[[package]] -name = "futures-executor" -version = "0.3.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f" -dependencies = [ - "futures-core", - "futures-task", - "futures-util", -] - -[[package]] -name = "futures-intrusive" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d930c203dd0b6ff06e0201a4a2fe9149b43c684fd4420555b26d21b1a02956f" -dependencies = [ - "futures-core", - "lock_api", - "parking_lot", -] - -[[package]] -name = "futures-io" -version = "0.3.32" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cecba35d7ad927e23624b22ad55235f2239cfa44fd10428eecbeba6d6a717718" - [[package]] name = "futures-sink" version = "0.3.32" @@ -655,13 +492,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" dependencies = [ "futures-core", - "futures-io", "futures-sink", "futures-task", - "memchr", "pin-project-lite", "pin-utils", - "slab", ] [[package]] @@ -674,17 +508,6 @@ dependencies = [ "version_check", ] -[[package]] -name = "getrandom" -version = "0.2.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" -dependencies = [ - "cfg-if", - "libc", - "wasi", -] - [[package]] name = "getrandom" version = "0.4.1" @@ -704,8 +527,6 @@ version = "0.15.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" dependencies = [ - "allocator-api2", - "equivalent", "foldhash 0.1.5", ] @@ -718,15 +539,6 @@ dependencies = [ "foldhash 0.2.0", ] -[[package]] -name = "hashlink" -version = "0.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7382cf6263419f2d8df38c55d7da83da5c18aef87fc7a7fc1fb1e344edfe14c1" -dependencies = [ - "hashbrown 0.15.5", -] - [[package]] name = "headers" version = "0.4.1" @@ -763,39 +575,6 @@ version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c" -[[package]] -name = "hex" -version = "0.4.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" - -[[package]] -name = "hkdf" -version = "0.12.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b5f8eb2ad728638ea2c7d47a21db23b7b58a72ed6a38256b8a1849f15fbbdf7" -dependencies = [ - "hmac", -] - -[[package]] -name = "hmac" -version = "0.12.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" -dependencies = [ - "digest", -] - -[[package]] -name = "home" -version = "0.5.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc627f471c528ff0c4a49e1d5e60450c8f6461dd6d10ba9dcd3a61d3dff7728d" -dependencies = [ - "windows-sys 0.61.2", -] - [[package]] name = "http" version = "1.4.0" @@ -901,88 +680,6 @@ dependencies = [ "cc", ] -[[package]] -name = "icu_collections" -version = "2.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2984d1cd16c883d7935b9e07e44071dca8d917fd52ecc02c04d5fa0b5a3f191c" -dependencies = [ - "displaydoc", - "potential_utf", - "utf8_iter", - "yoke", - "zerofrom", - "zerovec", -] - -[[package]] -name = "icu_locale_core" -version = "2.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92219b62b3e2b4d88ac5119f8904c10f8f61bf7e95b640d25ba3075e6cac2c29" -dependencies = [ - "displaydoc", - "litemap", - "tinystr", - "writeable", - "zerovec", -] - -[[package]] -name = "icu_normalizer" -version = "2.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c56e5ee99d6e3d33bd91c5d85458b6005a22140021cc324cea84dd0e72cff3b4" -dependencies = [ - "icu_collections", - "icu_normalizer_data", - "icu_properties", - "icu_provider", - "smallvec", - "zerovec", -] - -[[package]] -name = "icu_normalizer_data" -version = "2.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da3be0ae77ea334f4da67c12f149704f19f81d1adf7c51cf482943e84a2bad38" - -[[package]] -name = "icu_properties" -version = "2.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bee3b67d0ea5c2cca5003417989af8996f8604e34fb9ddf96208a033901e70de" -dependencies = [ - "icu_collections", - "icu_locale_core", - "icu_properties_data", - "icu_provider", - "zerotrie", - "zerovec", -] - -[[package]] -name = "icu_properties_data" -version = "2.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e2bbb201e0c04f7b4b3e14382af113e17ba4f63e2c9d2ee626b720cbce54a14" - -[[package]] -name = "icu_provider" -version = "2.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "139c4cf31c8b5f33d7e199446eff9c1e02decfc2f0eec2c8d71f65befa45b421" -dependencies = [ - "displaydoc", - "icu_locale_core", - "writeable", - "yoke", - "zerofrom", - "zerotrie", - "zerovec", -] - [[package]] name = "id-arena" version = "2.3.0" @@ -995,27 +692,6 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" -[[package]] -name = "idna" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" -dependencies = [ - "idna_adapter", - "smallvec", - "utf8_iter", -] - -[[package]] -name = "idna_adapter" -version = "1.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cb68373c0d6620ef8105e855e7745e18b0d00d3bdb07fb532e434244cdb9a714" -dependencies = [ - "icu_normalizer", - "icu_properties", -] - [[package]] name = "indexmap" version = "2.13.0" @@ -1044,15 +720,6 @@ dependencies = [ "wasm-bindgen", ] -[[package]] -name = "lazy_static" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" -dependencies = [ - "spin", -] - [[package]] name = "leb128fmt" version = "0.1.0" @@ -1065,24 +732,6 @@ version = "0.2.186" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66" -[[package]] -name = "libm" -version = "0.2.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981" - -[[package]] -name = "libredox" -version = "0.1.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e02f3bb43d335493c96bf3fd3a321600bf6bd07ed34bc64118e9293bdffea46c" -dependencies = [ - "bitflags", - "libc", - "plain", - "redox_syscall 0.7.5", -] - [[package]] name = "libsqlite3-sys" version = "0.30.1" @@ -1094,21 +743,6 @@ dependencies = [ "vcpkg", ] -[[package]] -name = "litemap" -version = "0.8.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92daf443525c4cce67b150400bc2316076100ce0b3686209eb8cf3c31612e6f0" - -[[package]] -name = "lock_api" -version = "0.4.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" -dependencies = [ - "scopeguard", -] - [[package]] name = "log" version = "0.4.29" @@ -1121,16 +755,6 @@ version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "47e1ffaa40ddd1f3ed91f717a33c8c0ee23fff369e3aa8772b9605cc1d22f4c3" -[[package]] -name = "md-5" -version = "0.10.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d89e7ee0cfbedfc4da3340218492196241d89eefb6dab27de5df917a6d2e78cf" -dependencies = [ - "cfg-if", - "digest", -] - [[package]] name = "memchr" version = "2.8.0" @@ -1172,23 +796,7 @@ checksum = "50b7e5b27aa02a74bac8c3f23f448f8d87ff11f92d3aac1a6ed369ee08cc56c1" dependencies = [ "libc", "wasi", - "windows-sys 0.61.2", -] - -[[package]] -name = "num-bigint-dig" -version = "0.8.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e661dda6640fad38e827a6d4a310ff4763082116fe217f279885c97f511bb0b7" -dependencies = [ - "lazy_static", - "libm", - "num-integer", - "num-iter", - "num-traits", - "rand", - "smallvec", - "zeroize", + "windows-sys", ] [[package]] @@ -1197,26 +805,6 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c6673768db2d862beb9b39a78fdcb1a69439615d5794a1be50caa9bc92c81967" -[[package]] -name = "num-integer" -version = "0.1.46" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" -dependencies = [ - "num-traits", -] - -[[package]] -name = "num-iter" -version = "0.1.45" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1429034a0490724d0075ebb2bc9e875d6503c3cf69e235a8941aa757d83ef5bf" -dependencies = [ - "autocfg", - "num-integer", - "num-traits", -] - [[package]] name = "num-traits" version = "0.2.19" @@ -1224,7 +812,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" dependencies = [ "autocfg", - "libm", ] [[package]] @@ -1256,7 +843,6 @@ dependencies = [ "libsqlite3-sys", "serde", "serde_json", - "sqlx", "thiserror", "tokio", "tower", @@ -1270,35 +856,6 @@ version = "1.21.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" -[[package]] -name = "parking" -version = "2.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba" - -[[package]] -name = "parking_lot" -version = "0.12.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" -dependencies = [ - "lock_api", - "parking_lot_core", -] - -[[package]] -name = "parking_lot_core" -version = "0.9.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" -dependencies = [ - "cfg-if", - "libc", - "redox_syscall 0.5.18", - "smallvec", - "windows-link", -] - [[package]] name = "password-hash" version = "0.5.0" @@ -1311,17 +868,8 @@ dependencies = [ ] [[package]] -name = "pem-rfc7468" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "88b39c9bfcfc231068454382784bb460aae594343fb030d46e9f50a645418412" -dependencies = [ - "base64ct", -] - -[[package]] -name = "percent-encoding" -version = "2.3.2" +name = "percent-encoding" +version = "2.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" @@ -1337,63 +885,18 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" -[[package]] -name = "pkcs1" -version = "0.7.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c8ffb9f10fa047879315e6625af03c164b16962a5368d724ed16323b68ace47f" -dependencies = [ - "der", - "pkcs8", - "spki", -] - -[[package]] -name = "pkcs8" -version = "0.10.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7" -dependencies = [ - "der", - "spki", -] - [[package]] name = "pkg-config" version = "0.3.33" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "19f132c84eca552bf34cab8ec81f1c1dcc229b811638f9d283dceabe58c5569e" -[[package]] -name = "plain" -version = "0.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b4596b6d070b27117e987119b4dac604f3c58cfb0b191112e24771b2faeac1a6" - -[[package]] -name = "potential_utf" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0103b1cef7ec0cf76490e969665504990193874ea05c85ff9bab8b911d0a0564" -dependencies = [ - "zerovec", -] - [[package]] name = "powerfmt" version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" -[[package]] -name = "ppv-lite86" -version = "0.2.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" -dependencies = [ - "zerocopy", -] - [[package]] name = "prettyplease" version = "0.2.37" @@ -1428,53 +931,11 @@ version = "5.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" -[[package]] -name = "rand" -version = "0.8.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ca0ecfa931c29007047d1bc58e623ab12e5590e8c7cc53200d5202b69266d8a" -dependencies = [ - "libc", - "rand_chacha", - "rand_core", -] - -[[package]] -name = "rand_chacha" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" -dependencies = [ - "ppv-lite86", - "rand_core", -] - [[package]] name = "rand_core" version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" -dependencies = [ - "getrandom 0.2.17", -] - -[[package]] -name = "redox_syscall" -version = "0.5.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" -dependencies = [ - "bitflags", -] - -[[package]] -name = "redox_syscall" -version = "0.7.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4666a1a60d8412eab19d94f6d13dcc9cea0a5ef4fdf6a5db306537413c661b1b" -dependencies = [ - "bitflags", -] [[package]] name = "regex" @@ -1505,26 +966,6 @@ version = "0.8.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a96887878f22d7bad8a3b6dc5b7440e0ada9a245242924394987b21cf2210a4c" -[[package]] -name = "rsa" -version = "0.9.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8573f03f5883dcaebdfcf4725caa1ecb9c15b2ef50c43a07b816e06799bb12d" -dependencies = [ - "const-oid", - "digest", - "num-bigint-dig", - "num-integer", - "num-traits", - "pkcs1", - "pkcs8", - "rand_core", - "signature", - "spki", - "subtle", - "zeroize", -] - [[package]] name = "rsqlite-vfs" version = "0.1.0" @@ -1556,12 +997,6 @@ dependencies = [ "winapi-util", ] -[[package]] -name = "scopeguard" -version = "1.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" - [[package]] name = "semver" version = "1.0.27" @@ -1654,17 +1089,6 @@ dependencies = [ "digest", ] -[[package]] -name = "sha2" -version = "0.10.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" -dependencies = [ - "cfg-if", - "cpufeatures", - "digest", -] - [[package]] name = "shlex" version = "1.3.0" @@ -1681,30 +1105,11 @@ dependencies = [ "libc", ] -[[package]] -name = "signature" -version = "2.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" -dependencies = [ - "digest", - "rand_core", -] - -[[package]] -name = "slab" -version = "0.4.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" - [[package]] name = "smallvec" version = "1.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" -dependencies = [ - "serde", -] [[package]] name = "socket2" @@ -1713,26 +1118,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3a766e1110788c36f4fa1c2b71b387a7815aa65f88ce0229841826633d93723e" dependencies = [ "libc", - "windows-sys 0.61.2", -] - -[[package]] -name = "spin" -version = "0.9.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" -dependencies = [ - "lock_api", -] - -[[package]] -name = "spki" -version = "0.7.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d" -dependencies = [ - "base64ct", - "der", + "windows-sys", ] [[package]] @@ -1747,219 +1133,6 @@ dependencies = [ "wasm-bindgen", ] -[[package]] -name = "sqlx" -version = "0.8.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fefb893899429669dcdd979aff487bd78f4064e5e7907e4269081e0ef7d97dc" -dependencies = [ - "sqlx-core", - "sqlx-macros", - "sqlx-mysql", - "sqlx-postgres", - "sqlx-sqlite", -] - -[[package]] -name = "sqlx-core" -version = "0.8.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ee6798b1838b6a0f69c007c133b8df5866302197e404e8b6ee8ed3e3a5e68dc6" -dependencies = [ - "base64", - "bytes", - "chrono", - "crc", - "crossbeam-queue", - "either", - "event-listener", - "futures-core", - "futures-intrusive", - "futures-io", - "futures-util", - "hashbrown 0.15.5", - "hashlink", - "indexmap", - "log", - "memchr", - "once_cell", - "percent-encoding", - "serde", - "serde_json", - "sha2", - "smallvec", - "thiserror", - "tokio", - "tokio-stream", - "tracing", - "url", - "uuid", -] - -[[package]] -name = "sqlx-macros" -version = "0.8.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a2d452988ccaacfbf5e0bdbc348fb91d7c8af5bee192173ac3636b5fb6e6715d" -dependencies = [ - "proc-macro2", - "quote", - "sqlx-core", - "sqlx-macros-core", - "syn", -] - -[[package]] -name = "sqlx-macros-core" -version = "0.8.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "19a9c1841124ac5a61741f96e1d9e2ec77424bf323962dd894bdb93f37d5219b" -dependencies = [ - "dotenvy", - "either", - "heck", - "hex", - "once_cell", - "proc-macro2", - "quote", - "serde", - "serde_json", - "sha2", - "sqlx-core", - "sqlx-mysql", - "sqlx-postgres", - "sqlx-sqlite", - "syn", - "tokio", - "url", -] - -[[package]] -name = "sqlx-mysql" -version = "0.8.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aa003f0038df784eb8fecbbac13affe3da23b45194bd57dba231c8f48199c526" -dependencies = [ - "atoi", - "base64", - "bitflags", - "byteorder", - "bytes", - "chrono", - "crc", - "digest", - "dotenvy", - "either", - "futures-channel", - "futures-core", - "futures-io", - "futures-util", - "generic-array", - "hex", - "hkdf", - "hmac", - "itoa", - "log", - "md-5", - "memchr", - "once_cell", - "percent-encoding", - "rand", - "rsa", - "serde", - "sha1", - "sha2", - "smallvec", - "sqlx-core", - "stringprep", - "thiserror", - "tracing", - "uuid", - "whoami", -] - -[[package]] -name = "sqlx-postgres" -version = "0.8.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "db58fcd5a53cf07c184b154801ff91347e4c30d17a3562a635ff028ad5deda46" -dependencies = [ - "atoi", - "base64", - "bitflags", - "byteorder", - "chrono", - "crc", - "dotenvy", - "etcetera", - "futures-channel", - "futures-core", - "futures-util", - "hex", - "hkdf", - "hmac", - "home", - "itoa", - "log", - "md-5", - "memchr", - "once_cell", - "rand", - "serde", - "serde_json", - "sha2", - "smallvec", - "sqlx-core", - "stringprep", - "thiserror", - "tracing", - "uuid", - "whoami", -] - -[[package]] -name = "sqlx-sqlite" -version = "0.8.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c2d12fe70b2c1b4401038055f90f151b78208de1f9f89a7dbfd41587a10c3eea" -dependencies = [ - "atoi", - "chrono", - "flume", - "futures-channel", - "futures-core", - "futures-executor", - "futures-intrusive", - "futures-util", - "libsqlite3-sys", - "log", - "percent-encoding", - "serde", - "serde_urlencoded", - "sqlx-core", - "thiserror", - "tracing", - "url", - "uuid", -] - -[[package]] -name = "stable_deref_trait" -version = "1.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" - -[[package]] -name = "stringprep" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b4df3d392d81bd458a8a621b8bffbd2302a12ffe288a9d931670948749463b1" -dependencies = [ - "unicode-bidi", - "unicode-normalization", - "unicode-properties", -] - [[package]] name = "strsim" version = "0.11.1" @@ -1989,17 +1162,6 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" -[[package]] -name = "synstructure" -version = "0.13.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - [[package]] name = "thiserror" version = "2.0.18" @@ -2051,45 +1213,19 @@ dependencies = [ "time-core", ] -[[package]] -name = "tinystr" -version = "0.8.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c8323304221c2a851516f22236c5722a72eaa19749016521d6dff0824447d96d" -dependencies = [ - "displaydoc", - "zerovec", -] - -[[package]] -name = "tinyvec" -version = "1.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3e61e67053d25a4e82c844e8424039d9745781b3fc4f32b8d55ed50f5f667ef3" -dependencies = [ - "tinyvec_macros", -] - -[[package]] -name = "tinyvec_macros" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" - [[package]] name = "tokio" version = "1.52.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b67dee974fe86fd92cc45b7a95fdd2f99a36a6d7b0d431a231178d3d670bbcc6" dependencies = [ - "bytes", "libc", "mio", "pin-project-lite", "signal-hook-registry", "socket2", "tokio-macros", - "windows-sys 0.61.2", + "windows-sys", ] [[package]] @@ -2103,17 +1239,6 @@ dependencies = [ "syn", ] -[[package]] -name = "tokio-stream" -version = "0.1.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32da49809aab5c3bc678af03902d4ccddea2a87d028d86392a4b1560c6906c70" -dependencies = [ - "futures-core", - "pin-project-lite", - "tokio", -] - [[package]] name = "toml" version = "0.9.12+spec-1.1.0" @@ -2181,21 +1306,9 @@ checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" dependencies = [ "log", "pin-project-lite", - "tracing-attributes", "tracing-core", ] -[[package]] -name = "tracing-attributes" -version = "0.1.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - [[package]] name = "tracing-core" version = "0.1.36" @@ -2222,64 +1335,25 @@ version = "1.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb" -[[package]] -name = "unicode-bidi" -version = "0.3.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c1cb5db39152898a79168971543b1cb5020dff7fe43c8dc468b0885f5e29df5" - [[package]] name = "unicode-ident" version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "537dd038a89878be9b64dd4bd1b260315c1bb94f4d784956b81e27a088d9a09e" -[[package]] -name = "unicode-normalization" -version = "0.1.25" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5fd4f6878c9cb28d874b009da9e8d183b5abc80117c40bbd187a1fde336be6e8" -dependencies = [ - "tinyvec", -] - -[[package]] -name = "unicode-properties" -version = "0.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7df058c713841ad818f1dc5d3fd88063241cc61f49f5fbea4b951e8cf5a8d71d" - [[package]] name = "unicode-xid" version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" -[[package]] -name = "url" -version = "2.5.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed" -dependencies = [ - "form_urlencoded", - "idna", - "percent-encoding", - "serde", -] - -[[package]] -name = "utf8_iter" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" - [[package]] name = "uuid" version = "1.23.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ddd74a9687298c6858e9b88ec8935ec45d22e8fd5e6394fa1bd4e99a87789c76" dependencies = [ - "getrandom 0.4.1", + "getrandom", "js-sys", "serde_core", "wasm-bindgen", @@ -2331,12 +1405,6 @@ dependencies = [ "wit-bindgen", ] -[[package]] -name = "wasite" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8dad83b4f25e74f184f64c43b150b91efe7647395b42289f38e50566d82855b" - [[package]] name = "wasm-bindgen" version = "0.2.108" @@ -2416,23 +1484,13 @@ dependencies = [ "semver", ] -[[package]] -name = "whoami" -version = "1.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d4a4db5077702ca3015d3d02d74974948aba2ad9e12ab7df718ee64ccd7e97d" -dependencies = [ - "libredox", - "wasite", -] - [[package]] name = "winapi-util" version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" dependencies = [ - "windows-sys 0.61.2", + "windows-sys", ] [[package]] @@ -2494,15 +1552,6 @@ dependencies = [ "windows-link", ] -[[package]] -name = "windows-sys" -version = "0.48.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" -dependencies = [ - "windows-targets", -] - [[package]] name = "windows-sys" version = "0.61.2" @@ -2512,63 +1561,6 @@ dependencies = [ "windows-link", ] -[[package]] -name = "windows-targets" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" -dependencies = [ - "windows_aarch64_gnullvm", - "windows_aarch64_msvc", - "windows_i686_gnu", - "windows_i686_msvc", - "windows_x86_64_gnu", - "windows_x86_64_gnullvm", - "windows_x86_64_msvc", -] - -[[package]] -name = "windows_aarch64_gnullvm" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" - -[[package]] -name = "windows_aarch64_msvc" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" - -[[package]] -name = "windows_i686_gnu" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" - -[[package]] -name = "windows_i686_msvc" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" - -[[package]] -name = "windows_x86_64_gnu" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" - -[[package]] -name = "windows_x86_64_gnullvm" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" - -[[package]] -name = "windows_x86_64_msvc" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" - [[package]] name = "winnow" version = "0.7.15" @@ -2669,115 +1661,6 @@ dependencies = [ "wasmparser", ] -[[package]] -name = "writeable" -version = "0.6.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ffae5123b2d3fc086436f8834ae3ab053a283cfac8fe0a0b8eaae044768a4c4" - -[[package]] -name = "yoke" -version = "0.8.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "abe8c5fda708d9ca3df187cae8bfb9ceda00dd96231bed36e445a1a48e66f9ca" -dependencies = [ - "stable_deref_trait", - "yoke-derive", - "zerofrom", -] - -[[package]] -name = "yoke-derive" -version = "0.8.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "de844c262c8848816172cef550288e7dc6c7b7814b4ee56b3e1553f275f1858e" -dependencies = [ - "proc-macro2", - "quote", - "syn", - "synstructure", -] - -[[package]] -name = "zerocopy" -version = "0.8.48" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eed437bf9d6692032087e337407a86f04cd8d6a16a37199ed57949d415bd68e9" -dependencies = [ - "zerocopy-derive", -] - -[[package]] -name = "zerocopy-derive" -version = "0.8.48" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70e3cd084b1788766f53af483dd21f93881ff30d7320490ec3ef7526d203bad4" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "zerofrom" -version = "0.1.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "69faa1f2a1ea75661980b013019ed6687ed0e83d069bc1114e2cc74c6c04c4df" -dependencies = [ - "zerofrom-derive", -] - -[[package]] -name = "zerofrom-derive" -version = "0.1.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "11532158c46691caf0f2593ea8358fed6bbf68a0315e80aae9bd41fbade684a1" -dependencies = [ - "proc-macro2", - "quote", - "syn", - "synstructure", -] - -[[package]] -name = "zeroize" -version = "1.8.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" - -[[package]] -name = "zerotrie" -version = "0.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0f9152d31db0792fa83f70fb2f83148effb5c1f5b8c7686c3459e361d9bc20bf" -dependencies = [ - "displaydoc", - "yoke", - "zerofrom", -] - -[[package]] -name = "zerovec" -version = "0.11.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "90f911cbc359ab6af17377d242225f4d75119aec87ea711a880987b18cd7b239" -dependencies = [ - "yoke", - "zerofrom", - "zerovec-derive", -] - -[[package]] -name = "zerovec-derive" -version = "0.11.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "625dc425cab0dca6dc3c3319506e6593dcb08a9f387ea3b284dbd52a92c40555" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - [[package]] name = "zmij" version = "1.0.21" diff --git a/Cargo.toml b/Cargo.toml index 456295d..5ea07b6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -5,7 +5,6 @@ edition = "2024" [features] default = ["diesel"] -sqlx = ["dep:sqlx"] diesel = ["dep:diesel", "dep:diesel-async", "dep:diesel_migrations", "dep:libsqlite3-sys"] [dependencies] @@ -22,7 +21,6 @@ diesel-async = { version = "0.9.0", optional = true, features = ["sqlite", "dead diesel_migrations = { version = "2.3.2", optional = true } libsqlite3-sys = { version = "*", optional = true, features = ["bundled"] } chrono = { version = "0.4.44", features = ["serde"] } -sqlx = { version = "0.8.6", optional = true, features = [ "runtime-tokio", "sqlite", "uuid", "chrono", "derive", "macros" ] } anyhow = "1.0.102" thiserror = "2.0.18" trait-variant = "0.1.2" diff --git a/README.md b/README.md index f6b88cc..1b6675c 100644 --- a/README.md +++ b/README.md @@ -80,6 +80,5 @@ curl -v -X POST -H 'Content-Type: application/x-www-form-urlencoded' -u 'aardvar * https://docs.rs/tower/latest/tower * https://crates.io/crates/anyhow * https://crates.io/crates/thiserror -* https://github.com/launchbadge/sqlx * https://docs.diesel.rs/2.0.x/diesel/index.html * https://obito.fr/posts/2022/12/use-uuid-in-sqlite-database-with-rust-diesel.rs/ diff --git a/src/main.rs b/src/main.rs index 512b67e..7caed7f 100644 --- a/src/main.rs +++ b/src/main.rs @@ -52,15 +52,10 @@ async fn main() -> Result<(), Box> { // or just continue with passing the repositories directly? // or do we just pass connection pools and do await with services and repositories? - #[cfg(not(any(feature = "diesel", feature = "sqlx")))] + #[cfg(not(feature = "diesel"))] let access_token_repository = token::repository::memory::InMemoryAccessTokenRepository ::new(); - #[cfg(feature = "sqlx")] - let access_token_repository = token::repository::sqlx::SqlxSqliteAccessTokenRepository - ::new("file:target/db/access_tokens.sqlite3") - .await?; - #[cfg(feature = "diesel")] let access_token_repository = token::repository::diesel::DieselSqliteAccessTokenRepository ::new("file:target/db/access_tokens.sqlite3")?; diff --git a/src/token/mod.rs b/src/token/mod.rs index ca58bc9..d889f71 100644 --- a/src/token/mod.rs +++ b/src/token/mod.rs @@ -15,7 +15,6 @@ pub enum TokenType { #[derive(Serialize, Clone, Eq, PartialEq)] #[cfg_attr(test, derive(Debug))] -#[cfg_attr(feature = "sqlx", derive(sqlx::FromRow))] #[cfg_attr(feature = "diesel", derive(diesel::Queryable, diesel::Selectable, diesel::Insertable, diesel::Identifiable))] #[cfg_attr(feature = "diesel", diesel(table_name = schema::access_tokens))] #[cfg_attr(feature = "diesel", diesel(check_for_backend(diesel::sqlite::Sqlite)))] diff --git a/src/token/repository/mod.rs b/src/token/repository/mod.rs index 779f143..c5e65a7 100644 --- a/src/token/repository/mod.rs +++ b/src/token/repository/mod.rs @@ -1,9 +1,6 @@ -#[cfg(any(test, not(any(feature = "diesel", feature = "sqlx"))))] +#[cfg(any(test, not(feature = "diesel")))] pub mod memory; -#[cfg(feature = "sqlx")] -pub mod sqlx; - #[cfg(feature = "diesel")] pub mod diesel; diff --git a/src/token/repository/sqlx.rs b/src/token/repository/sqlx.rs deleted file mode 100644 index 041544c..0000000 --- a/src/token/repository/sqlx.rs +++ /dev/null @@ -1,81 +0,0 @@ -use anyhow::Context; -use anyhow::Result; -use diesel::QueryDsl; -use sqlx::Pool; -use sqlx::Sqlite; -use sqlx::sqlite::SqlitePoolOptions; - -use crate::token::AccessToken; -use crate::token::repository::TokenRepository; -use crate::util::uuid_wrapper::UuidWrapper; - -#[derive(Clone)] -pub struct SqlxSqliteAccessTokenRepository { - pool: Pool, -} - -impl SqlxSqliteAccessTokenRepository { - pub async fn new(database_url: &str) -> Result { - Ok( - Self { - pool: SqlitePoolOptions::new() - .min_connections(1) - .max_connections(5) - .connect(database_url) - .await - .with_context(|| - format!("Failed to create SQLX sqlite database pool: {database_url}") - )? - } - ) - } -} - -impl TokenRepository for SqlxSqliteAccessTokenRepository { - - async fn get_token(&self, token: UuidWrapper) -> Result> { - - let result = sqlx::query_as::<_, AccessToken>("SELECT * FROM access_tokens WHERE id = ?;") - .bind(token) - .fetch_optional(&self.pool) - .await - .with_context(|| "Error querying access token database")?; - - Ok(result) - } - - async fn save_token(&self, token: &AccessToken) -> Result<()> { - - sqlx::query(" - INSERT INTO access_tokens (id, username, client_id, scopes, issued_at, expires_at, not_before) - VALUES (?, ?, ?, ?, ?, ?, ?); - ") - .bind(token.id) - .bind(&token.username) - .bind(&token.client_id) - .bind(&token.scopes) - .bind(token.issued_at) - .bind(token.expires_at) - .bind(token.not_before) - .execute(&self.pool) - .await - .with_context(|| "Error saving access token to database")?; - - Ok(()) - } - - async fn delete_token(&self, token: UuidWrapper) -> Result<()> { - - sqlx::query(" - DELETE FROM access_tokens WHERE id IN ( - SELECT id FROM access_tokens WHERE id = ? LIMIT 1 - ); - ") - .bind(token) - .execute(&self.pool) - .await - .with_context(|| "Error deleting access token from database")?; - - Ok(()) - } -} \ No newline at end of file diff --git a/src/util/uuid_wrapper.rs b/src/util/uuid_wrapper.rs index 944194a..d1ab541 100644 --- a/src/util/uuid_wrapper.rs +++ b/src/util/uuid_wrapper.rs @@ -12,8 +12,6 @@ use uuid::Uuid; #[derive(Debug, Clone, Copy, Hash, Eq, PartialEq)] #[derive(serde::Serialize)] #[serde(transparent)] -#[cfg_attr(feature = "sqlx", derive(sqlx::Type))] -#[cfg_attr(feature = "sqlx", sqlx(transparent))] #[cfg_attr(feature = "diesel", derive(diesel::FromSqlRow, diesel::AsExpression))] #[cfg_attr(feature = "diesel", diesel(sql_type = diesel::sql_types::Binary))] pub struct UuidWrapper(pub Uuid); diff --git a/src/util/value_struct.rs b/src/util/value_struct.rs index 751dba0..0fbcede 100644 --- a/src/util/value_struct.rs +++ b/src/util/value_struct.rs @@ -14,8 +14,6 @@ macro_rules! value_struct { #[derive(Clone, Hash, Eq, PartialEq)] #[derive(serde::Serialize)] #[serde(transparent)] - #[cfg_attr(feature = "sqlx", derive(sqlx::Type))] - #[cfg_attr(feature = "sqlx", sqlx(transparent))] #[cfg_attr(test, derive(Debug))] $vis struct $struct_name($field_type); From c8d1630cad0d57020b88a385f936dd673f47598e Mon Sep 17 00:00:00 2001 From: James Bacon Date: Thu, 7 May 2026 17:16:10 +0100 Subject: [PATCH 10/19] Moved integration test to use Diesel in memory than the HashMap instance --- src/token/repository/diesel.rs | 29 +++++++++++++++++++---------- src/token/repository/mod.rs | 2 +- src/token_exchange/route.rs | 24 ++++++++++++------------ 3 files changed, 32 insertions(+), 23 deletions(-) diff --git a/src/token/repository/diesel.rs b/src/token/repository/diesel.rs index e1f357f..5027d60 100644 --- a/src/token/repository/diesel.rs +++ b/src/token/repository/diesel.rs @@ -111,6 +111,21 @@ impl TokenRepository for DieselSqliteAccessTokenRepository { } } +#[cfg(test)] +pub mod test_support { + use anyhow::anyhow; + use super::*; + impl DieselSqliteAccessTokenRepository { + pub async fn new_in_memory() -> Result { + let database = DieselSqliteAccessTokenRepository::new(":memory:")?; + match database.run_diesel_migrations().await { + Ok(()) => Ok(database), + Err(e) => Err(anyhow!("Failed to run migrations: {e}")), + } + } + } +} + #[cfg(test)] mod unit_tests { @@ -125,15 +140,9 @@ mod unit_tests { } } - async fn under_test() -> DieselSqliteAccessTokenRepository { - let db = assert_ok!(DieselSqliteAccessTokenRepository::new(":memory:")); - assert_ok!(db.run_diesel_migrations().await); - db - } - #[tokio::test(flavor = "multi_thread")] async fn should_be_able_to_save_and_retrieve_a_token() { - let under_test = under_test().await; + let under_test = assert_ok!(DieselSqliteAccessTokenRepository::new_in_memory().await); let token = AccessToken::new(); assert_ok!(under_test.save_token(&token).await); assert_eq!(assert_some!(assert_ok!(under_test.get_token(token.id).await)), token); @@ -141,7 +150,7 @@ mod unit_tests { #[tokio::test(flavor = "multi_thread")] async fn should_be_able_to_delete_a_token() { - let under_test = under_test().await; + let under_test = assert_ok!(DieselSqliteAccessTokenRepository::new_in_memory().await); let token = AccessToken::new(); assert_ok!(under_test.save_token(&token).await); assert_ok!(under_test.delete_token(token.id).await); @@ -150,13 +159,13 @@ mod unit_tests { #[tokio::test(flavor = "multi_thread")] async fn should_be_able_to_delete_a_token_when_non_existent() { - let under_test = under_test().await; + let under_test = assert_ok!(DieselSqliteAccessTokenRepository::new_in_memory().await); assert_ok!(under_test.delete_token(UuidWrapper::random()).await); } #[tokio::test(flavor = "multi_thread")] async fn should_be_able_to_clone_but_share_storage() { - let first = under_test().await; + let first = assert_ok!(DieselSqliteAccessTokenRepository::new_in_memory().await); let second = first.clone(); let token = AccessToken::new(); assert_ok!(first.save_token(&token).await); diff --git a/src/token/repository/mod.rs b/src/token/repository/mod.rs index c5e65a7..6fc9d1d 100644 --- a/src/token/repository/mod.rs +++ b/src/token/repository/mod.rs @@ -1,4 +1,4 @@ -#[cfg(any(test, not(feature = "diesel")))] +#[cfg(not(feature = "diesel"))] pub mod memory; #[cfg(feature = "diesel")] diff --git a/src/token_exchange/route.rs b/src/token_exchange/route.rs index 4204472..acb2522 100644 --- a/src/token_exchange/route.rs +++ b/src/token_exchange/route.rs @@ -78,7 +78,7 @@ mod integration_tests { macro_rules! under_test { () => { route(TokenExchangeState { - access_token_repository: crate::token::repository::memory::InMemoryAccessTokenRepository::new(), + access_token_repository: assert_ok!(crate::token::repository::diesel::DieselSqliteAccessTokenRepository::new_in_memory().await), client_authenticator: crate::client::authentication::ClientAuthenticationService::new( crate::client::secret::InMemoryClientSecretRepository::new(), crate::client::configuration::InMemoryClientConfigurationRepository::new(), @@ -102,7 +102,7 @@ mod integration_tests { macro_rules! http_method_test { ($($name:ident: $method:expr,)*) => { $( - #[tokio::test] + #[tokio::test(flavor = "multi_thread")] async fn $name() { let router = under_test!(); @@ -132,7 +132,7 @@ mod integration_tests { should_not_support_http_method_connect: Method::CONNECT, } - #[tokio::test] + #[tokio::test(flavor = "multi_thread")] async fn should_require_client_authentication_on_missing_authorization_header() { let router = under_test!(); @@ -149,7 +149,7 @@ mod integration_tests { assert_eq!(response.status(), StatusCode::UNAUTHORIZED); } - #[tokio::test] + #[tokio::test(flavor = "multi_thread")] async fn should_require_client_authentication_on_invalid_confidential_client_credentials() { let router = under_test!(); @@ -167,7 +167,7 @@ mod integration_tests { assert_eq!(response.status(), StatusCode::UNAUTHORIZED); } - #[tokio::test] + #[tokio::test(flavor = "multi_thread")] async fn should_require_client_authentication_on_invalid_public_client_credentials() { let router = under_test!(); @@ -184,7 +184,7 @@ mod integration_tests { assert_eq!(response.status(), StatusCode::UNAUTHORIZED); } - #[tokio::test] + #[tokio::test(flavor = "multi_thread")] async fn should_require_client_authentication_via_only_one_method() { let router = under_test!(); @@ -205,7 +205,7 @@ mod integration_tests { macro_rules! content_type_test { ($($name:ident: $value:expr,)*) => { $( - #[tokio::test] + #[tokio::test(flavor = "multi_thread")] async fn $name() { let router = under_test!(); @@ -237,7 +237,7 @@ mod integration_tests { mod invalid_token_request { use super::*; - #[tokio::test] + #[tokio::test(flavor = "multi_thread")] async fn should_return_bad_request_for_invalid_token_exchange_requests() { let router = under_test!(); @@ -264,7 +264,7 @@ mod integration_tests { mod success_token_request { use super::*; - #[tokio::test] + #[tokio::test(flavor = "multi_thread")] async fn should_return_ok_for_valid_password_grants() { let router = under_test!(); @@ -288,7 +288,7 @@ mod integration_tests { assert_none!(body.get("state")); } - #[tokio::test] + #[tokio::test(flavor = "multi_thread")] #[ignore = "authorization code not yet implemented"] // TODO - Re-enable once implemented async fn should_return_ok_for_valid_authorization_code_grants() { let router = under_test!(); @@ -312,7 +312,7 @@ mod integration_tests { assert_some_eq_x!(body.get("scope"), "basic"); } - #[tokio::test] + #[tokio::test(flavor = "multi_thread")] #[ignore = "refresh grant not yet implemented"] // TODO - Re-enable once implemented async fn should_return_ok_for_valid_refresh_token_grant() { let router = under_test!(); @@ -336,7 +336,7 @@ mod integration_tests { assert_some_eq_x!(body.get("scope"), "basic"); } - #[tokio::test] + #[tokio::test(flavor = "multi_thread")] #[ignore = "assertion grant not yet implemented"] // TODO - Re-enable once implemented async fn should_return_ok_for_valid_assertion_grant() { let router = under_test!(); From 27b26b49521ebef7f5ac97436ab31ef5c53f00c5 Mon Sep 17 00:00:00 2001 From: James Bacon Date: Mon, 11 May 2026 14:02:51 +0100 Subject: [PATCH 11/19] Committing to using Diesel full time. Refactored some of the Diesel logic. Added strum to provide enum utilities and replace hand crafted ones. Added some DB level checks. Added integration tests around scope field lengths. --- .gitignore | 6 +- Cargo.lock | 20 +++ Cargo.toml | 13 +- diesel.toml | 9 ++ .../down.sql | 0 .../up.sql | 10 ++ .../up.sql | 14 -- src/client/mod.rs | 21 ++- src/main.rs | 22 ++- src/{token => }/schema.rs | 2 + src/scope/mod.rs | 22 +-- src/token/mod.rs | 23 ++-- src/token/repository/diesel.rs | 125 ++++++++---------- src/token/repository/memory.rs | 82 ------------ src/token/repository/mod.rs | 4 - src/token_exchange/request.rs | 12 +- src/token_exchange/route.rs | 46 ++++--- src/util/diesel_migrations.rs | 20 +++ src/util/diesel_pool.rs | 14 ++ src/util/diesel_types.rs | 8 ++ src/util/enum_with_from_str.rs | 85 ------------ src/util/mod.rs | 6 +- src/util/uuid_wrapper.rs | 26 +--- 23 files changed, 237 insertions(+), 353 deletions(-) create mode 100644 diesel.toml rename migrations/{access_tokens/20260428095532_create_access_tokens => 2026-05-11-094743-0000_create_access_tokens}/down.sql (100%) create mode 100644 migrations/2026-05-11-094743-0000_create_access_tokens/up.sql delete mode 100644 migrations/access_tokens/20260428095532_create_access_tokens/up.sql rename src/{token => }/schema.rs (84%) delete mode 100644 src/token/repository/memory.rs create mode 100644 src/util/diesel_migrations.rs create mode 100644 src/util/diesel_pool.rs create mode 100644 src/util/diesel_types.rs delete mode 100644 src/util/enum_with_from_str.rs diff --git a/.gitignore b/.gitignore index bd787a3..2f480eb 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,7 @@ /target -.idea \ No newline at end of file +.idea + +.env + +/migrations/.diesel_lock diff --git a/Cargo.lock b/Cargo.lock index 992ea1f..065527e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -843,6 +843,8 @@ dependencies = [ "libsqlite3-sys", "serde", "serde_json", + "strum", + "strum_macros", "thiserror", "tokio", "tower", @@ -1139,6 +1141,24 @@ version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" +[[package]] +name = "strum" +version = "0.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9628de9b8791db39ceda2b119bbe13134770b56c138ec1d3af810d045c04f9bd" + +[[package]] +name = "strum_macros" +version = "0.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab85eea0270ee17587ed4156089e10b9e6880ee688791d45a905f5b1ca36f664" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "subtle" version = "2.6.1" diff --git a/Cargo.toml b/Cargo.toml index 5ea07b6..4b1f07b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,8 +4,7 @@ version = "0.1.0" edition = "2024" [features] -default = ["diesel"] -diesel = ["dep:diesel", "dep:diesel-async", "dep:diesel_migrations", "dep:libsqlite3-sys"] +default = [] [dependencies] argon2 = "0.5.3" @@ -16,14 +15,16 @@ tokio = { version = "1.52.1", features = ["macros", "rt-multi-thread", "signal"] uuid = { version = "1.23.1", features = ["v4", "serde"] } form_urlencoded = "1.2.2" tower = "0.5.3" -diesel = { version = "2.3.9", optional = true, features = ["returning_clauses_for_sqlite_3_35", "chrono"] } -diesel-async = { version = "0.9.0", optional = true, features = ["sqlite", "deadpool", "migrations"] } -diesel_migrations = { version = "2.3.2", optional = true } -libsqlite3-sys = { version = "*", optional = true, features = ["bundled"] } +diesel = { version = "2.3.9", features = ["returning_clauses_for_sqlite_3_35", "chrono"] } +diesel-async = { version = "0.9.0", features = ["sqlite", "deadpool", "migrations"] } +diesel_migrations = { version = "2.3.2" } +libsqlite3-sys = { version = "*", features = ["bundled"] } chrono = { version = "0.4.44", features = ["serde"] } anyhow = "1.0.102" thiserror = "2.0.18" trait-variant = "0.1.2" +strum = "0.28.0" +strum_macros = "0.28.0" [dev-dependencies] assertables = "9.9.0" diff --git a/diesel.toml b/diesel.toml new file mode 100644 index 0000000..a0d61bf --- /dev/null +++ b/diesel.toml @@ -0,0 +1,9 @@ +# For documentation on how to configure this file, +# see https://diesel.rs/guides/configuring-diesel-cli + +[print_schema] +file = "src/schema.rs" +custom_type_derives = ["diesel::query_builder::QueryId", "Clone"] + +[migrations_directory] +dir = "migrations" diff --git a/migrations/access_tokens/20260428095532_create_access_tokens/down.sql b/migrations/2026-05-11-094743-0000_create_access_tokens/down.sql similarity index 100% rename from migrations/access_tokens/20260428095532_create_access_tokens/down.sql rename to migrations/2026-05-11-094743-0000_create_access_tokens/down.sql diff --git a/migrations/2026-05-11-094743-0000_create_access_tokens/up.sql b/migrations/2026-05-11-094743-0000_create_access_tokens/up.sql new file mode 100644 index 0000000..60226b2 --- /dev/null +++ b/migrations/2026-05-11-094743-0000_create_access_tokens/up.sql @@ -0,0 +1,10 @@ +CREATE TABLE access_tokens +( + id BLOB(16) PRIMARY KEY NOT NULL CHECK (LENGTH(id) = 16), + username VARCHAR(64) NOT NULL CHECK (LENGTH(username) > 0 AND LENGTH(username) <= 64), + client_id VARCHAR(64) NOT NULL CHECK (LENGTH(client_id) > 0 AND LENGTH(client_id) <= 64), + scopes VARCHAR(16) NOT NULL CHECK (LENGTH(scopes) <= 16), -- TODO - Consider refactoring into a foreign reference + issued_at TIMESTAMP NOT NULL, + expires_at TIMESTAMP NOT NULL, + not_before TIMESTAMP NOT NULL +); diff --git a/migrations/access_tokens/20260428095532_create_access_tokens/up.sql b/migrations/access_tokens/20260428095532_create_access_tokens/up.sql deleted file mode 100644 index 8490ba9..0000000 --- a/migrations/access_tokens/20260428095532_create_access_tokens/up.sql +++ /dev/null @@ -1,14 +0,0 @@ -CREATE TABLE access_tokens -( - id BLOB(16) PRIMARY KEY NOT NULL CHECK(LENGTH(id) = 16), - username VARCHAR(255) NOT NULL, - client_id VARCHAR(255) NOT NULL, - scopes VARCHAR(16) NOT NULL, -- TODO - Consider refactoring into a foreign reference - issued_at TIMESTAMP NOT NULL, - expires_at TIMESTAMP NOT NULL, - not_before TIMESTAMP NOT NULL -); - -CREATE INDEX access_tokens_username_idx ON access_tokens (username); -CREATE INDEX access_tokens_client_id_idx ON access_tokens (client_id); -CREATE INDEX access_tokens_expires_at_idx ON access_tokens (expires_at); diff --git a/src/client/mod.rs b/src/client/mod.rs index 9959b40..75c402e 100644 --- a/src/client/mod.rs +++ b/src/client/mod.rs @@ -5,9 +5,9 @@ pub mod authentication; pub mod configuration; pub mod middleware; -use crate::value_struct; use crate::disable_deserialization; -use crate::enum_with_from_str; +use crate::value_struct; +use strum_macros::{Display, EnumString}; value_struct! { pub struct ClientId(String); @@ -30,13 +30,12 @@ pub enum ClientAction { // ProofKeyForCodeExchange, } -enum_with_from_str! { - #[derive(Debug, Hash, Eq, PartialEq, Clone)] - pub enum GrantType { - // AuthorizationCode: "authorization_code", - Password: "password", - // RefreshToken: "refresh_token", - } +#[derive(EnumString, Display, Debug, Hash, Eq, PartialEq, Clone)] +#[strum(serialize_all = "snake_case")] +pub enum GrantType { + // AuthorizationCode, + Password, + // RefreshToken } principal! { @@ -48,10 +47,10 @@ principal! { #[cfg(test)] pub mod test_support { - use std::collections::HashSet; - use crate::client::{ClientId, ClientPrincipal, ClientType, ConfidentialClient, GrantType, PublicClient}; use crate::client::configuration::ClientConfiguration; + use crate::client::{ClientId, ClientPrincipal, ClientType, ConfidentialClient, GrantType, PublicClient}; use crate::scope::Scope; + use std::collections::HashSet; impl ClientPrincipal { pub fn new_principal(configuration: ClientConfiguration) -> ClientPrincipal { diff --git a/src/main.rs b/src/main.rs index 7caed7f..6b3faae 100644 --- a/src/main.rs +++ b/src/main.rs @@ -15,8 +15,8 @@ mod token_introspection; mod graceful_shutdown; mod client; mod util; +mod schema; -use std::error::Error; use axum::{serve, Router}; use tokio::net::TcpListener; use client::authentication::ClientAuthenticationService; @@ -26,6 +26,9 @@ use token_exchange::TokenExchangeState; use token_introspection::TokenIntrospectionState; use anyhow::{Context, Result}; +use token::repository::diesel::DieselAccessTokenRepository; +use crate::util::diesel_migrations::run_diesel_migrations; +use crate::util::diesel_pool::create_pool; // TODO List: // - Token endpoint @@ -46,25 +49,20 @@ use anyhow::{Context, Result}; // - Database support // - Error handling, including 500s #[tokio::main] -async fn main() -> Result<(), Box> { +async fn main() -> Result<()> { // TODO - Do we bother with services? // or just continue with passing the repositories directly? // or do we just pass connection pools and do await with services and repositories? - #[cfg(not(feature = "diesel"))] - let access_token_repository = token::repository::memory::InMemoryAccessTokenRepository - ::new(); + let pool = create_pool("file:target/db/diesel.sqlite3")?; - #[cfg(feature = "diesel")] - let access_token_repository = token::repository::diesel::DieselSqliteAccessTokenRepository - ::new("file:target/db/access_tokens.sqlite3")?; + run_diesel_migrations(&pool).await?; - #[cfg(feature = "diesel")] - access_token_repository.run_diesel_migrations().await?; + let access_token_repository = DieselAccessTokenRepository::new(pool.clone()); - let client_secret_repository = InMemoryClientSecretRepository::new(); // "file:target/db/client_secrets.sqlite3" - let client_configuration_repository = InMemoryClientConfigurationRepository::new(); // "file:target/db/client_configurations.sqlite3" + let client_secret_repository = InMemoryClientSecretRepository::new(); + let client_configuration_repository = InMemoryClientConfigurationRepository::new(); let client_authenticator = ClientAuthenticationService::new( client_secret_repository.clone(), diff --git a/src/token/schema.rs b/src/schema.rs similarity index 84% rename from src/token/schema.rs rename to src/schema.rs index ed9bd1d..5868e86 100644 --- a/src/token/schema.rs +++ b/src/schema.rs @@ -1,3 +1,5 @@ +// @generated automatically by Diesel CLI. + diesel::table! { access_tokens (id) { id -> Binary, diff --git a/src/scope/mod.rs b/src/scope/mod.rs index 3fc6c4a..72702c0 100644 --- a/src/scope/mod.rs +++ b/src/scope/mod.rs @@ -1,18 +1,18 @@ pub mod parser; -use std::collections::HashSet; -use serde::{Serialize, Serializer}; use crate::disable_deserialization; -use crate::enum_with_from_str; +use serde::{Serialize, Serializer}; +use std::collections::HashSet; +use strum_macros::Display; +use strum_macros::EnumIter; +use strum_macros::EnumString; -enum_with_from_str! { - #[derive(Hash, Eq, PartialEq, Clone)] - #[cfg_attr(test, derive(Debug))] - pub enum Scope { - Basic: "basic", - Read: "read", - Write: "write", - } +#[derive(EnumString, EnumIter, Display, Debug, Hash, Eq, PartialEq, Clone)] +#[strum(serialize_all = "snake_case")] +pub enum Scope { + Basic, + Read, + Write, } #[derive(Eq, PartialEq, Clone)] diff --git a/src/token/mod.rs b/src/token/mod.rs index d889f71..a7cb78e 100644 --- a/src/token/mod.rs +++ b/src/token/mod.rs @@ -1,7 +1,7 @@ pub mod repository; -#[cfg(feature = "diesel")] -mod schema; +use chrono::NaiveDateTime; +use diesel::prelude::*; use serde::Serialize; use crate::util::uuid_wrapper::UuidWrapper; @@ -15,22 +15,23 @@ pub enum TokenType { #[derive(Serialize, Clone, Eq, PartialEq)] #[cfg_attr(test, derive(Debug))] -#[cfg_attr(feature = "diesel", derive(diesel::Queryable, diesel::Selectable, diesel::Insertable, diesel::Identifiable))] -#[cfg_attr(feature = "diesel", diesel(table_name = schema::access_tokens))] -#[cfg_attr(feature = "diesel", diesel(check_for_backend(diesel::sqlite::Sqlite)))] +#[derive(Queryable, Selectable, Insertable)] +#[diesel(table_name = crate::schema::access_tokens)] +#[diesel(check_for_backend(diesel::sqlite::Sqlite))] pub struct AccessToken { pub id: UuidWrapper, pub username: String, // TODO - Use AuthenticatedUser pub client_id: String, // TODO - Use ClientId pub scopes: String, // TODO - Use Scopes - pub issued_at: chrono::NaiveDateTime, - pub expires_at: chrono::NaiveDateTime, - pub not_before: chrono::NaiveDateTime, + pub issued_at: NaiveDateTime, + pub expires_at: NaiveDateTime, + pub not_before: NaiveDateTime, } #[cfg(test)] pub mod test_support { use super::*; + use chrono::Utc; impl AccessToken { pub fn new() -> AccessToken { AccessToken { @@ -38,9 +39,9 @@ pub mod test_support { username: "aardvark".to_string(), client_id: "badger".to_string(), scopes: "basic".to_string(), - issued_at: chrono::Utc::now().naive_utc(), - expires_at: chrono::Utc::now().naive_utc(), - not_before: chrono::Utc::now().naive_utc(), + issued_at: Utc::now().naive_utc(), + expires_at: Utc::now().naive_utc(), + not_before: Utc::now().naive_utc(), } } } diff --git a/src/token/repository/diesel.rs b/src/token/repository/diesel.rs index 5027d60..bd8df0d 100644 --- a/src/token/repository/diesel.rs +++ b/src/token/repository/diesel.rs @@ -1,63 +1,26 @@ -use anyhow::Result; +use crate::schema::access_tokens::dsl::access_tokens; +use crate::schema::access_tokens::dsl::id; +use crate::token::repository::TokenRepository; +use crate::token::AccessToken; +use crate::util::diesel_types::AsyncSqlitePool; +use crate::util::uuid_wrapper::UuidWrapper; use anyhow::Context; - +use anyhow::Result; use diesel::prelude::*; use diesel_async::{AsyncConnection, RunQueryDsl}; -use diesel_async::pooled_connection::AsyncDieselConnectionManager; -use diesel_async::pooled_connection::deadpool::Pool; -use diesel_async::sync_connection_wrapper::SyncConnectionWrapper; - -use std::error::Error; - -use crate::token::schema::access_tokens::dsl::access_tokens; -use crate::token::schema::access_tokens::dsl::id; - -use crate::token::AccessToken; -use crate::token::repository::TokenRepository; -use crate::util::uuid_wrapper::UuidWrapper; - -type AsyncSqliteConnection = SyncConnectionWrapper; -type AsyncSqliteConnectionManager = AsyncDieselConnectionManager; -type AsyncSqlitePool = Pool; #[derive(Clone)] -pub struct DieselSqliteAccessTokenRepository { +pub struct DieselAccessTokenRepository { pool: AsyncSqlitePool, } -impl DieselSqliteAccessTokenRepository { - pub fn new(database_url: &str) -> Result { - - let manager = AsyncSqliteConnectionManager::new(database_url); - - let pool = AsyncSqlitePool::builder(manager) - .max_size(10) - .build() - .with_context(|| format!("Failed to create Diesel sqlite database pool: {database_url}"))?; - - Ok(Self { - pool - }) - } - pub async fn run_diesel_migrations(&self) -> Result<(), Box> { - use diesel_async::AsyncMigrationHarness; - use diesel_migrations::{embed_migrations, EmbeddedMigrations, MigrationHarness}; - const ACCESS_TOKEN_MIGRATIONS: EmbeddedMigrations = embed_migrations!("migrations/access_tokens"); - - let connection = self.pool - .get() - .await - .with_context(|| "Failed to get connection from pool")?; - - let mut harness = AsyncMigrationHarness::new(connection); - - harness.run_pending_migrations(ACCESS_TOKEN_MIGRATIONS)?; - - Ok(()) +impl DieselAccessTokenRepository { + pub fn new(pool: AsyncSqlitePool) -> DieselAccessTokenRepository { + Self { pool } } } -impl TokenRepository for DieselSqliteAccessTokenRepository { +impl TokenRepository for DieselAccessTokenRepository { async fn get_token(&self, token: UuidWrapper) -> Result> { let connection = &mut self.pool.get() @@ -113,36 +76,34 @@ impl TokenRepository for DieselSqliteAccessTokenRepository { #[cfg(test)] pub mod test_support { - use anyhow::anyhow; use super::*; - impl DieselSqliteAccessTokenRepository { - pub async fn new_in_memory() -> Result { - let database = DieselSqliteAccessTokenRepository::new(":memory:")?; - match database.run_diesel_migrations().await { - Ok(()) => Ok(database), - Err(e) => Err(anyhow!("Failed to run migrations: {e}")), - } + use crate::util::diesel_migrations::run_diesel_migrations; + impl DieselAccessTokenRepository { + pub async fn new_in_memory() -> Result { + let pool = crate::util::diesel_pool::create_pool(":memory:")?; + run_diesel_migrations(&pool).await?; + Ok(DieselAccessTokenRepository::new(pool)) } } -} - -#[cfg(test)] -mod unit_tests { - - use super::*; - use assertables::*; - - impl std::fmt::Debug for DieselSqliteAccessTokenRepository { + impl std::fmt::Debug for DieselAccessTokenRepository { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.debug_struct("DieselSqliteAccessTokenRepository") .field("pool.manager", &self.pool.manager()) .finish() } } +} + +#[cfg(test)] +mod integration_tests { + use super::*; + use assertables::*; + use strum::IntoEnumIterator; + use crate::scope::Scope; #[tokio::test(flavor = "multi_thread")] async fn should_be_able_to_save_and_retrieve_a_token() { - let under_test = assert_ok!(DieselSqliteAccessTokenRepository::new_in_memory().await); + let under_test = assert_ok!(DieselAccessTokenRepository::new_in_memory().await); let token = AccessToken::new(); assert_ok!(under_test.save_token(&token).await); assert_eq!(assert_some!(assert_ok!(under_test.get_token(token.id).await)), token); @@ -150,7 +111,7 @@ mod unit_tests { #[tokio::test(flavor = "multi_thread")] async fn should_be_able_to_delete_a_token() { - let under_test = assert_ok!(DieselSqliteAccessTokenRepository::new_in_memory().await); + let under_test = assert_ok!(DieselAccessTokenRepository::new_in_memory().await); let token = AccessToken::new(); assert_ok!(under_test.save_token(&token).await); assert_ok!(under_test.delete_token(token.id).await); @@ -159,16 +120,40 @@ mod unit_tests { #[tokio::test(flavor = "multi_thread")] async fn should_be_able_to_delete_a_token_when_non_existent() { - let under_test = assert_ok!(DieselSqliteAccessTokenRepository::new_in_memory().await); + let under_test = assert_ok!(DieselAccessTokenRepository::new_in_memory().await); assert_ok!(under_test.delete_token(UuidWrapper::random()).await); } #[tokio::test(flavor = "multi_thread")] async fn should_be_able_to_clone_but_share_storage() { - let first = assert_ok!(DieselSqliteAccessTokenRepository::new_in_memory().await); + let first = assert_ok!(DieselAccessTokenRepository::new_in_memory().await); let second = first.clone(); let token = AccessToken::new(); assert_ok!(first.save_token(&token).await); assert_eq!(assert_some!(assert_ok!(second.get_token(token.id).await)), token); } + + #[tokio::test(flavor = "multi_thread")] + async fn should_be_able_to_save_a_token_with_all_scopes_assigned() { + let under_test = assert_ok!(DieselAccessTokenRepository::new_in_memory().await); + let mut token = AccessToken::new(); + token.scopes = Scope::iter() + .map(|scope| scope.to_string()) + .collect::>() + .join(" "); + assert_ok!(under_test.save_token(&token).await); + } + + #[tokio::test(flavor = "multi_thread")] + async fn should_not_be_able_to_save_a_token_with_too_many_scopes_assigned() { + let under_test = assert_ok!(DieselAccessTokenRepository::new_in_memory().await); + let mut token = AccessToken::new(); + let all_scopes = Scope::iter() + .map(|scope| scope.to_string()) + .collect::>() + .join(" "); + token.scopes = all_scopes.clone() + " extra_scope"; + let error = assert_err!(under_test.save_token(&token).await); + assert_eq!(error.root_cause().to_string(), format!("CHECK constraint failed: LENGTH(scopes) <= {}", String::len(&all_scopes))); + } } \ No newline at end of file diff --git a/src/token/repository/memory.rs b/src/token/repository/memory.rs deleted file mode 100644 index a0c4288..0000000 --- a/src/token/repository/memory.rs +++ /dev/null @@ -1,82 +0,0 @@ -use anyhow::Result; -use std::collections::HashMap; -use std::sync::{Arc, Mutex, MutexGuard, PoisonError}; -use crate::token::AccessToken; -use crate::token::repository::TokenRepository; -use crate::util::uuid_wrapper::UuidWrapper; - -#[derive(Clone, Default)] -pub struct InMemoryAccessTokenRepository { - store: Arc>>, -} - -impl InMemoryAccessTokenRepository { - pub fn new() -> Self { - Self { store: Arc::new(Mutex::new(HashMap::new())) } - } - fn lock_store(&self) -> MutexGuard<'_, HashMap> { - self.store.lock().unwrap_or_else(PoisonError::into_inner) - } -} - -impl TokenRepository for InMemoryAccessTokenRepository -{ - async fn get_token(&self, id: UuidWrapper) -> Result> { - Ok(self.lock_store().get(&id).cloned()) - } - - async fn save_token(&self, token: &AccessToken) -> Result<()> { - self.lock_store().insert(token.id, token.clone()); - Ok(()) - } - - async fn delete_token(&self, id: UuidWrapper) -> Result<()> { - self.lock_store().remove(&id); - Ok(()) - } -} - -#[cfg(test)] -mod unit_tests { - - use super::*; - use assertables::*; - - #[tokio::test] - async fn should_initialise_empty() { - let under_test = InMemoryAccessTokenRepository::new(); - assert_is_empty!(under_test.lock_store()); - } - - #[tokio::test] - async fn should_be_able_to_save_and_retrieve_a_token() { - let under_test = InMemoryAccessTokenRepository::new(); - let token = AccessToken::new(); - assert_ok!(under_test.save_token(&token).await); - assert_eq!(assert_some!(assert_ok!(under_test.get_token(token.id).await)), token); - } - - #[tokio::test] - async fn should_be_able_to_delete_a_token() { - let under_test = InMemoryAccessTokenRepository::new(); - let token = AccessToken::new(); - assert_ok!(under_test.save_token(&token).await); - assert_ok!(under_test.delete_token(token.id).await); - assert_none!(assert_ok!(under_test.get_token(token.id).await)); - } - - #[tokio::test] - async fn should_be_able_to_delete_a_token_when_non_existent() { - let under_test = InMemoryAccessTokenRepository::new(); - assert_ok!(under_test.delete_token(UuidWrapper::random()).await); - } - - #[tokio::test] - async fn should_be_able_to_clone_but_share_storage() { - let first = InMemoryAccessTokenRepository::new(); - let second = first.clone(); - let token = AccessToken::new(); - assert_ok!(first.save_token(&token).await); - assert_eq!(assert_some!(assert_ok!(second.get_token(token.id).await)), token); - } -} \ No newline at end of file diff --git a/src/token/repository/mod.rs b/src/token/repository/mod.rs index 6fc9d1d..78d63d1 100644 --- a/src/token/repository/mod.rs +++ b/src/token/repository/mod.rs @@ -1,7 +1,3 @@ -#[cfg(not(feature = "diesel"))] -pub mod memory; - -#[cfg(feature = "diesel")] pub mod diesel; use crate::util::uuid_wrapper::UuidWrapper; diff --git a/src/token_exchange/request.rs b/src/token_exchange/request.rs index 9a57359..cc4fa98 100644 --- a/src/token_exchange/request.rs +++ b/src/token_exchange/request.rs @@ -50,14 +50,20 @@ where } pub fn validate_grant_type(principal: ClientPrincipal, request: &HashMap) -> Result { - match request.get("grant_type").map(|s| s.parse::()) { + let maybe_grant_type = request + .get("grant_type") + .map(|s| s.trim()) + .filter(|s| !s.is_empty()) + .map(str::parse::); + + match maybe_grant_type { None => Err(TokenExchangeResponse::missing_parameter("grant_type")), - Some(Err(error_message)) => Err( + Some(Err(_)) => Err( TokenExchangeResponse::Failure { error: ErrorType::UnsupportedGrantType, - error_description: Some(error_message), + error_description: None, } ), diff --git a/src/token_exchange/route.rs b/src/token_exchange/route.rs index acb2522..87ae87f 100644 --- a/src/token_exchange/route.rs +++ b/src/token_exchange/route.rs @@ -67,6 +67,10 @@ mod integration_tests { use base64::prelude::*; use serde_json::Value; use tower::ServiceExt; + use crate::client::authentication::ClientAuthenticationService; + use crate::client::configuration::InMemoryClientConfigurationRepository; + use crate::client::secret::InMemoryClientSecretRepository; + use crate::token::repository::diesel::DieselAccessTokenRepository; // See: https://github.com/beercanx/oauth-api/blob/main/api/token/src/test/kotlin/uk/co/baconi/oauth/api/token/TokenRouteIntegrationTests.kt @@ -75,16 +79,14 @@ mod integration_tests { const TEST_CLIENT_USERNAME: &str = "aardvark"; const TEST_CLIENT_PASSWORD: &str = "badger"; - macro_rules! under_test { - () => { - route(TokenExchangeState { - access_token_repository: assert_ok!(crate::token::repository::diesel::DieselSqliteAccessTokenRepository::new_in_memory().await), - client_authenticator: crate::client::authentication::ClientAuthenticationService::new( - crate::client::secret::InMemoryClientSecretRepository::new(), - crate::client::configuration::InMemoryClientConfigurationRepository::new(), - ), - }) - }; + async fn under_test() -> Router<()> { + route(TokenExchangeState { + access_token_repository: assert_ok!(DieselAccessTokenRepository::new_in_memory().await), + client_authenticator: ClientAuthenticationService::new( + InMemoryClientSecretRepository::new(), + InMemoryClientConfigurationRepository::new(), + ), + }) } async fn extract_json_body(response: Response) -> HashMap { @@ -104,7 +106,7 @@ mod integration_tests { $( #[tokio::test(flavor = "multi_thread")] async fn $name() { - let router = under_test!(); + let router = under_test().await; let request = assert_ok!(Request::builder() .method($method) @@ -134,7 +136,7 @@ mod integration_tests { #[tokio::test(flavor = "multi_thread")] async fn should_require_client_authentication_on_missing_authorization_header() { - let router = under_test!(); + let router = under_test().await; let request = assert_ok!( Request::builder() @@ -151,7 +153,7 @@ mod integration_tests { #[tokio::test(flavor = "multi_thread")] async fn should_require_client_authentication_on_invalid_confidential_client_credentials() { - let router = under_test!(); + let router = under_test().await; let request = assert_ok!( Request::builder() @@ -169,7 +171,7 @@ mod integration_tests { #[tokio::test(flavor = "multi_thread")] async fn should_require_client_authentication_on_invalid_public_client_credentials() { - let router = under_test!(); + let router = under_test().await; let request = assert_ok!( Request::builder() @@ -186,7 +188,7 @@ mod integration_tests { #[tokio::test(flavor = "multi_thread")] async fn should_require_client_authentication_via_only_one_method() { - let router = under_test!(); + let router = under_test().await; let request = assert_ok!( Request::builder() @@ -207,7 +209,7 @@ mod integration_tests { $( #[tokio::test(flavor = "multi_thread")] async fn $name() { - let router = under_test!(); + let router = under_test().await; let (content_type, body) = $value; @@ -229,7 +231,7 @@ mod integration_tests { } content_type_test! { - should_not_support_application_xml: ("xml", r#"aardvark"#), + should_not_support_application_xml: ("xml", r"aardvark"), should_not_support_application_json: ("json", r#"{"grant_type":"aardvark"}"#), } } @@ -240,7 +242,7 @@ mod integration_tests { #[tokio::test(flavor = "multi_thread")] async fn should_return_bad_request_for_invalid_token_exchange_requests() { - let router = under_test!(); + let router = under_test().await; let request = assert_ok!( Request::builder() @@ -266,7 +268,7 @@ mod integration_tests { #[tokio::test(flavor = "multi_thread")] async fn should_return_ok_for_valid_password_grants() { - let router = under_test!(); + let router = under_test().await; let request = assert_ok!(Request::builder() .method(Method::POST) @@ -291,7 +293,7 @@ mod integration_tests { #[tokio::test(flavor = "multi_thread")] #[ignore = "authorization code not yet implemented"] // TODO - Re-enable once implemented async fn should_return_ok_for_valid_authorization_code_grants() { - let router = under_test!(); + let router = under_test().await; let request = assert_ok!(Request::builder() .method(Method::POST) @@ -315,7 +317,7 @@ mod integration_tests { #[tokio::test(flavor = "multi_thread")] #[ignore = "refresh grant not yet implemented"] // TODO - Re-enable once implemented async fn should_return_ok_for_valid_refresh_token_grant() { - let router = under_test!(); + let router = under_test().await; let request = assert_ok!(Request::builder() .method(Method::POST) @@ -339,7 +341,7 @@ mod integration_tests { #[tokio::test(flavor = "multi_thread")] #[ignore = "assertion grant not yet implemented"] // TODO - Re-enable once implemented async fn should_return_ok_for_valid_assertion_grant() { - let router = under_test!(); + let router = under_test().await; let request = assert_ok!( Request::builder() diff --git a/src/util/diesel_migrations.rs b/src/util/diesel_migrations.rs new file mode 100644 index 0000000..8041248 --- /dev/null +++ b/src/util/diesel_migrations.rs @@ -0,0 +1,20 @@ +use crate::util::diesel_types::AsyncSqlitePool; +use anyhow::anyhow; +use anyhow::Context; +use anyhow::Result; +use diesel_async::AsyncMigrationHarness; +use diesel_migrations::{embed_migrations, EmbeddedMigrations, MigrationHarness}; + +const MIGRATIONS: EmbeddedMigrations = embed_migrations!("migrations"); + +pub async fn run_diesel_migrations(pool: &AsyncSqlitePool) -> Result<()> { + let connection = pool + .get() + .await + .with_context(|| "Failed to get connection from pool")?; + + match AsyncMigrationHarness::new(connection).run_pending_migrations(MIGRATIONS) { + Ok(_) => Ok(()), + Err(e) => Err(anyhow!("Failed to run pending diesel migrations: {e}")), + } +} diff --git a/src/util/diesel_pool.rs b/src/util/diesel_pool.rs new file mode 100644 index 0000000..1757617 --- /dev/null +++ b/src/util/diesel_pool.rs @@ -0,0 +1,14 @@ +use anyhow::Context; +use anyhow::Result; +use crate::util::diesel_types::{AsyncSqliteConnectionManager, AsyncSqlitePool}; + +pub fn create_pool(database_url: &str) -> Result { + AsyncSqlitePool::builder(AsyncSqliteConnectionManager::new(database_url)) + // TODO - Add configuration options? + //.max_size(cpu_core_count * 2) + //.create_timeout(Some(std::time::Duration::from_secs(x))) + //.recycle_timeout(Some(std::time::Duration::from_secs(y))) + //.wait_timeout(Some(std::time::Duration::from_secs(z))) + .build() + .with_context(|| format!("Failed to create sqlite database pool: {database_url}")) +} \ No newline at end of file diff --git a/src/util/diesel_types.rs b/src/util/diesel_types.rs new file mode 100644 index 0000000..16a2023 --- /dev/null +++ b/src/util/diesel_types.rs @@ -0,0 +1,8 @@ +use diesel::SqliteConnection; +use diesel_async::pooled_connection::deadpool::Pool; +use diesel_async::pooled_connection::AsyncDieselConnectionManager; +use diesel_async::sync_connection_wrapper::SyncConnectionWrapper; + +pub type AsyncSqliteConnection = SyncConnectionWrapper; +pub type AsyncSqliteConnectionManager = AsyncDieselConnectionManager; +pub type AsyncSqlitePool = Pool; diff --git a/src/util/enum_with_from_str.rs b/src/util/enum_with_from_str.rs deleted file mode 100644 index 02d0719..0000000 --- a/src/util/enum_with_from_str.rs +++ /dev/null @@ -1,85 +0,0 @@ -#[macro_export] -macro_rules! enum_with_from_str { - ( - $(#[$m:meta])* - $vis:vis enum $enum_name:ident { - $($enum_value:ident: $enum_string_value:expr),+ - $(,)? - } - ) => { - $(#[$m])* - $vis enum $enum_name { - $($enum_value),+ - } - impl std::str::FromStr for $enum_name { - type Err = String; - fn from_str(value: &str) -> Result { - match value { - $($enum_string_value => Ok(Self::$enum_value),)+ - _ => Err(format!("unsupported: {value}")), - } - } - } - impl std::fmt::Display for $enum_name { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - $(Self::$enum_value => f.write_str($enum_string_value),)+ - } - } - } - } -} - -#[cfg(test)] -mod test { - use std::str::FromStr; - use assertables::*; - - enum_with_from_str! { - #[derive(Debug, Eq, PartialEq, Copy, Clone)] - pub enum TestEnum { - TestValue1: "test1", - TestValue2: "test_2", - } - } - - mod from_str { - use super::*; - - #[test] - fn should_return_ok_on_supported_values() { - assert_ok_eq_x!("test1".parse::(), TestEnum::TestValue1); - assert_ok_eq_x!(TestEnum::from_str("test1"), TestEnum::TestValue1); - assert_ok_eq_x!("test_2".parse::(), TestEnum::TestValue2); - assert_ok_eq_x!(TestEnum::from_str("test_2"), TestEnum::TestValue2); - } - - #[test] - fn should_return_err_on_unsupported_values() { - assert_eq!(assert_err!("aardvark".parse::()), "unsupported: aardvark"); - assert_eq!(assert_err!(TestEnum::from_str("aardvark")), "unsupported: aardvark"); - } - } - - mod to_string { - use super::*; - - #[test] - fn should_return_expected_string() { - assert_eq!(TestEnum::TestValue1.to_string(), "test1"); - assert_eq!(TestEnum::TestValue2.to_string(), "test_2"); - } - - #[test] - fn should_format_expected_display_string() { - assert_eq!(format!("{}", TestEnum::TestValue1), "test1"); - assert_eq!(format!("{}", TestEnum::TestValue2), "test_2"); - } - - #[test] - fn should_format_expected_debug_string() { - assert_eq!(format!("{:?}", TestEnum::TestValue1), "TestValue1"); - assert_eq!(format!("{:?}", TestEnum::TestValue2), "TestValue2"); - } - } -} \ No newline at end of file diff --git a/src/util/mod.rs b/src/util/mod.rs index 6757176..744fee6 100644 --- a/src/util/mod.rs +++ b/src/util/mod.rs @@ -1,6 +1,8 @@ pub mod disable_deserialization; pub mod disable_serialization; -pub mod enum_with_from_str; pub mod map_of; pub mod value_struct; -pub mod uuid_wrapper; \ No newline at end of file +pub mod uuid_wrapper; +pub mod diesel_migrations; +pub mod diesel_types; +pub mod diesel_pool; \ No newline at end of file diff --git a/src/util/uuid_wrapper.rs b/src/util/uuid_wrapper.rs index d1ab541..caa675c 100644 --- a/src/util/uuid_wrapper.rs +++ b/src/util/uuid_wrapper.rs @@ -1,19 +1,15 @@ -#[cfg(feature = "diesel")] -use { - diesel::sql_types::Binary, - diesel::backend::Backend, - diesel::deserialize::FromSql, - diesel::serialize::{Output, ToSql}, - diesel::sqlite::Sqlite, -}; - +use diesel::backend::Backend; +use diesel::deserialize::FromSql; +use diesel::serialize::{Output, ToSql}; +use diesel::sql_types::Binary; +use diesel::sqlite::Sqlite; use uuid::Uuid; #[derive(Debug, Clone, Copy, Hash, Eq, PartialEq)] #[derive(serde::Serialize)] #[serde(transparent)] -#[cfg_attr(feature = "diesel", derive(diesel::FromSqlRow, diesel::AsExpression))] -#[cfg_attr(feature = "diesel", diesel(sql_type = diesel::sql_types::Binary))] +#[derive(diesel::FromSqlRow, diesel::AsExpression)] +#[diesel(sql_type = Binary)] pub struct UuidWrapper(pub Uuid); impl UuidWrapper { @@ -37,20 +33,12 @@ impl From for UuidWrapper { } } -impl std::fmt::Display for UuidWrapper { - fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(formatter, "{}", self.0) - } -} - -#[cfg(feature = "diesel")] impl ToSql for UuidWrapper { fn to_sql<'b>(&'b self, out: &mut Output<'b, '_, Sqlite>) -> diesel::serialize::Result { <[u8] as ToSql>::to_sql(self.0.as_bytes(), out) } } -#[cfg(feature = "diesel")] impl FromSql for UuidWrapper { fn from_sql(bytes: ::RawValue<'_>) -> diesel::deserialize::Result { let raw = as FromSql>::from_sql(bytes)?; From 01706cea7638b188310bc24c6ba5a53628dd4723 Mon Sep 17 00:00:00 2001 From: James Bacon Date: Mon, 11 May 2026 14:07:51 +0100 Subject: [PATCH 12/19] Enabling clippy checking in workflow to check test code. --- .github/workflows/rust.yml | 2 +- src/client/mod.rs | 4 ++-- src/token_exchange/grant/password.rs | 18 +++++++++--------- src/token_exchange/request.rs | 11 ++++++----- src/token_exchange/route.rs | 2 +- 5 files changed, 19 insertions(+), 18 deletions(-) diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index b9575b4..916623d 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -60,7 +60,7 @@ jobs: - name: Clippy shell: bash run: >- - cargo clippy -q --message-format=json + cargo clippy -q --all-targets --message-format=json | jq -r 'select(.reason == "compiler-message") | .message | . as $message diff --git a/src/client/mod.rs b/src/client/mod.rs index 75c402e..56a2d84 100644 --- a/src/client/mod.rs +++ b/src/client/mod.rs @@ -83,9 +83,9 @@ pub mod test_support { ClientConfiguration { client_id: ClientId(client_id.into()), client_type, - redirect_uris: Default::default(), + redirect_uris: HashSet::default(), allowed_scopes: HashSet::from([Scope::Basic, Scope::Read, Scope::Write]), - allowed_actions: Default::default(), + allowed_actions: HashSet::default(), allowed_grant_types: HashSet::from([GrantType::Password]), } } diff --git a/src/token_exchange/grant/password.rs b/src/token_exchange/grant/password.rs index 9a2f77a..699279b 100644 --- a/src/token_exchange/grant/password.rs +++ b/src/token_exchange/grant/password.rs @@ -145,10 +145,10 @@ mod unit_tests { ClientPrincipal::new_principal(ClientConfiguration { client_id: String::from("unauthorised").into(), client_type: ClientType::Confidential, - redirect_uris: Default::default(), - allowed_scopes: Default::default(), - allowed_actions: Default::default(), - allowed_grant_types: Default::default(), + redirect_uris: HashSet::default(), + allowed_scopes: HashSet::default(), + allowed_actions: HashSet::default(), + allowed_grant_types: HashSet::default(), }), &map_of! { "username" => "aardvark", @@ -253,9 +253,9 @@ mod unit_tests { ClientPrincipal::new_principal(ClientConfiguration { client_id: String::from("aardvark").into(), client_type: ClientType::Confidential, - redirect_uris: Default::default(), - allowed_scopes: Default::default(), - allowed_actions: Default::default(), + redirect_uris: HashSet::default(), + allowed_scopes: HashSet::default(), + allowed_actions: HashSet::default(), allowed_grant_types: HashSet::from([Password]), }), &map_of! { @@ -316,9 +316,9 @@ mod unit_tests { ClientPrincipal::new_principal(ClientConfiguration { client_id: String::from("aardvark").into(), client_type: ClientType::Confidential, - redirect_uris: Default::default(), + redirect_uris: HashSet::default(), allowed_scopes: HashSet::from([Scope::Read]), - allowed_actions: Default::default(), + allowed_actions: HashSet::default(), allowed_grant_types: HashSet::from([Password]), }), &map_of! { diff --git a/src/token_exchange/request.rs b/src/token_exchange/request.rs index cc4fa98..af7c688 100644 --- a/src/token_exchange/request.rs +++ b/src/token_exchange/request.rs @@ -96,6 +96,7 @@ mod unit_tests { // See: https://github.com/beercanx/oauth-api/blob/main/api/token/src/test/kotlin/uk/co/baconi/oauth/api/token/TokenRequestValidationTest.kt + use std::collections::HashSet; use super::*; use assertables::*; use crate::client::ClientType; @@ -158,10 +159,10 @@ mod unit_tests { ClientPrincipal::new_principal(ClientConfiguration { client_id: String::from("invalid").into(), client_type: ClientType::Confidential, - redirect_uris: Default::default(), - allowed_scopes: Default::default(), - allowed_actions: Default::default(), - allowed_grant_types: Default::default(), + redirect_uris: HashSet::default(), + allowed_scopes: HashSet::default(), + allowed_actions: HashSet::default(), + allowed_grant_types: HashSet::default(), }), &input_parameters! { "grant_type" => "password" }, TokenExchangeResponse::Failure { @@ -177,7 +178,7 @@ mod unit_tests { TokenExchangeForm(Password(PasswordGrantRequest { principal: ClientPrincipal::new_confidential_client("aardvark"), username: "aardvark".into(), - password: "".into(), + password: String::new(), scopes: None, })) } diff --git a/src/token_exchange/route.rs b/src/token_exchange/route.rs index 87ae87f..2108511 100644 --- a/src/token_exchange/route.rs +++ b/src/token_exchange/route.rs @@ -95,7 +95,7 @@ mod integration_tests { } fn basic_auth(username: &str, password: &str) -> String { - format!("Basic {}", BASE64_STANDARD.encode(format!("{}:{}", username, password))) + format!("Basic {}", BASE64_STANDARD.encode(format!("{username}:{password}"))) } mod invalid_http_request { From edb6912465b8f8ed768bd3a19fc4fc0ce5290886 Mon Sep 17 00:00:00 2001 From: James Bacon Date: Mon, 11 May 2026 14:38:31 +0100 Subject: [PATCH 13/19] Putting back old behaviour while preserving strum changes --- src/token_exchange/request.rs | 44 ++++++++++++++--------------------- 1 file changed, 18 insertions(+), 26 deletions(-) diff --git a/src/token_exchange/request.rs b/src/token_exchange/request.rs index af7c688..ec4b256 100644 --- a/src/token_exchange/request.rs +++ b/src/token_exchange/request.rs @@ -50,33 +50,25 @@ where } pub fn validate_grant_type(principal: ClientPrincipal, request: &HashMap) -> Result { - let maybe_grant_type = request - .get("grant_type") - .map(|s| s.trim()) - .filter(|s| !s.is_empty()) - .map(str::parse::); - - match maybe_grant_type { - + match request.get("grant_type") { None => Err(TokenExchangeResponse::missing_parameter("grant_type")), - - Some(Err(_)) => Err( - TokenExchangeResponse::Failure { - error: ErrorType::UnsupportedGrantType, - error_description: None, - } - ), - - Some(Ok(grant_type)) if !principal.can_perform_grant_type(&grant_type) => Err( - TokenExchangeResponse::Failure { - error: ErrorType::UnauthorizedClient, - error_description: Some(format!("not authorized to: {grant_type:?}")), - } - ), - - Some(Ok(GrantType::Password)) => Ok(TokenExchangeForm( - Password(validate_password_grant(principal, request)?) - )), + Some(raw_grant_type) => match raw_grant_type.parse::() { + Err(_) => Err( + TokenExchangeResponse::Failure { + error: ErrorType::UnsupportedGrantType, + error_description: Some(format!("unsupported: {raw_grant_type}")), + } + ), + Ok(grant_type) if !principal.can_perform_grant_type(&grant_type) => Err( + TokenExchangeResponse::Failure { + error: ErrorType::UnauthorizedClient, + error_description: Some(format!("not authorized to: {grant_type:?}")), + } + ), + Ok(GrantType::Password) => Ok(TokenExchangeForm( + Password(validate_password_grant(principal, request)?) + )), + } } } From 71c78e77ca28a1246efad795e7fca7f3c57d07b6 Mon Sep 17 00:00:00 2001 From: James Bacon Date: Mon, 11 May 2026 14:47:33 +0100 Subject: [PATCH 14/19] Refactored token repository back into a file than directory format. --- src/main.rs | 11 +++-- .../{repository/diesel.rs => repository.rs} | 40 +++++++++++-------- src/token/repository/mod.rs | 12 ------ src/token_exchange/route.rs | 2 +- 4 files changed, 29 insertions(+), 36 deletions(-) rename src/token/{repository/diesel.rs => repository.rs} (86%) delete mode 100644 src/token/repository/mod.rs diff --git a/src/main.rs b/src/main.rs index 6b3faae..dcab6d3 100644 --- a/src/main.rs +++ b/src/main.rs @@ -17,18 +17,17 @@ mod client; mod util; mod schema; +use crate::util::diesel_migrations::run_diesel_migrations; +use crate::util::diesel_pool::create_pool; +use anyhow::{Context, Result}; use axum::{serve, Router}; -use tokio::net::TcpListener; use client::authentication::ClientAuthenticationService; use client::configuration::InMemoryClientConfigurationRepository; use client::secret::InMemoryClientSecretRepository; +use token::repository::DieselAccessTokenRepository; use token_exchange::TokenExchangeState; use token_introspection::TokenIntrospectionState; - -use anyhow::{Context, Result}; -use token::repository::diesel::DieselAccessTokenRepository; -use crate::util::diesel_migrations::run_diesel_migrations; -use crate::util::diesel_pool::create_pool; +use tokio::net::TcpListener; // TODO List: // - Token endpoint diff --git a/src/token/repository/diesel.rs b/src/token/repository.rs similarity index 86% rename from src/token/repository/diesel.rs rename to src/token/repository.rs index bd8df0d..f899ea9 100644 --- a/src/token/repository/diesel.rs +++ b/src/token/repository.rs @@ -1,14 +1,20 @@ use crate::schema::access_tokens::dsl::access_tokens; use crate::schema::access_tokens::dsl::id; -use crate::token::repository::TokenRepository; use crate::token::AccessToken; use crate::util::diesel_types::AsyncSqlitePool; use crate::util::uuid_wrapper::UuidWrapper; -use anyhow::Context; -use anyhow::Result; +use anyhow::{Context, Result}; use diesel::prelude::*; use diesel_async::{AsyncConnection, RunQueryDsl}; +#[trait_variant::make(Send)] +pub trait TokenRepository: Sync + Clone { + async fn get_token(&self, id: UuidWrapper) -> Result>; + async fn save_token(&self, token: &T) -> Result<()>; + #[allow(dead_code)] // TODO - Remove after we implement token revocation. + async fn delete_token(&self, id: UuidWrapper) -> Result<()>; +} + #[derive(Clone)] pub struct DieselAccessTokenRepository { pool: AsyncSqlitePool, @@ -57,18 +63,18 @@ impl TokenRepository for DieselAccessTokenRepository { .with_context(|| "Failed to get connection from pool")?; connection.transaction(async |connection| { - diesel::delete(access_tokens) - .filter(id.eq(token)) - .execute(connection) - .await - .and_then(|deleted_rows| { - if deleted_rows == 1 || deleted_rows == 0 { - Ok(()) - } else { - Err(diesel::result::Error::RollbackTransaction) - } - }) - }) + diesel::delete(access_tokens) + .filter(id.eq(token)) + .execute(connection) + .await + .and_then(|deleted_rows| { + if deleted_rows == 1 || deleted_rows == 0 { + Ok(()) + } else { + Err(diesel::result::Error::RollbackTransaction) + } + }) + }) .await .with_context(|| "Error deleting access token from database") } @@ -97,9 +103,9 @@ pub mod test_support { #[cfg(test)] mod integration_tests { use super::*; + use crate::scope::Scope; use assertables::*; use strum::IntoEnumIterator; - use crate::scope::Scope; #[tokio::test(flavor = "multi_thread")] async fn should_be_able_to_save_and_retrieve_a_token() { @@ -156,4 +162,4 @@ mod integration_tests { let error = assert_err!(under_test.save_token(&token).await); assert_eq!(error.root_cause().to_string(), format!("CHECK constraint failed: LENGTH(scopes) <= {}", String::len(&all_scopes))); } -} \ No newline at end of file +} diff --git a/src/token/repository/mod.rs b/src/token/repository/mod.rs deleted file mode 100644 index 78d63d1..0000000 --- a/src/token/repository/mod.rs +++ /dev/null @@ -1,12 +0,0 @@ -pub mod diesel; - -use crate::util::uuid_wrapper::UuidWrapper; -use anyhow::Result; - -#[trait_variant::make(Send)] -pub trait TokenRepository: Sync + Clone { - async fn get_token(&self, id: UuidWrapper) -> Result>; - async fn save_token(&self, token: &T) -> Result<()>; - #[allow(dead_code)] // TODO - Remove after we implement token revocation. - async fn delete_token(&self, id: UuidWrapper) -> Result<()>; -} diff --git a/src/token_exchange/route.rs b/src/token_exchange/route.rs index 2108511..386bdef 100644 --- a/src/token_exchange/route.rs +++ b/src/token_exchange/route.rs @@ -70,7 +70,7 @@ mod integration_tests { use crate::client::authentication::ClientAuthenticationService; use crate::client::configuration::InMemoryClientConfigurationRepository; use crate::client::secret::InMemoryClientSecretRepository; - use crate::token::repository::diesel::DieselAccessTokenRepository; + use crate::token::repository::DieselAccessTokenRepository; // See: https://github.com/beercanx/oauth-api/blob/main/api/token/src/test/kotlin/uk/co/baconi/oauth/api/token/TokenRouteIntegrationTests.kt From 6bdcdb8697bc78c75ab46b64a456ee28058a0294 Mon Sep 17 00:00:00 2001 From: James Bacon Date: Fri, 15 May 2026 19:01:12 +0100 Subject: [PATCH 15/19] Implemeted a diesel repository for client configuration stored in Sqlite3 --- Cargo.lock | 1 + Cargo.toml | 4 +- .../up.sql | 2 +- .../down.sql | 1 + .../up.sql | 9 + .../down.sql | 16 ++ .../up.sql | 16 ++ .../down.sql | 1 + .../up.sql | 4 + src/client/authentication.rs | 18 +- src/client/client_principal.rs | 8 +- src/client/configuration.rs | 175 +++++++++++++----- src/client/middleware.rs | 3 + src/client/mod.rs | 45 +++-- src/main.rs | 4 +- src/schema.rs | 15 ++ src/scope/mod.rs | 9 +- src/token/mod.rs | 7 +- src/token_exchange/grant/password.rs | 52 +++--- src/token_exchange/request.rs | 28 +-- src/token_exchange/response.rs | 5 +- src/token_exchange/route.rs | 4 +- src/util/diesel_from_sql_for_enum_strings.rs | 19 ++ src/util/diesel_from_sql_for_json_fields.rs | 18 ++ src/util/diesel_from_sql_for_value_structs.rs | 44 +++++ src/util/diesel_pool.rs | 40 +++- src/util/diesel_to_sql_for_value_structs.rs | 44 +++++ src/util/mod.rs | 12 +- src/util/value_struct.rs | 17 +- 29 files changed, 480 insertions(+), 141 deletions(-) create mode 100644 migrations/2026-05-13-144941-0000_create_client_configuration/down.sql create mode 100644 migrations/2026-05-13-144941-0000_create_client_configuration/up.sql create mode 100644 migrations/2026-05-14-103409-0000_add_client_id_fk_to_access_tokens/down.sql create mode 100644 migrations/2026-05-14-103409-0000_add_client_id_fk_to_access_tokens/up.sql create mode 100644 migrations/2026-05-14-140202-0000_insert_dumby_client_configuration/down.sql create mode 100644 migrations/2026-05-14-140202-0000_insert_dumby_client_configuration/up.sql create mode 100644 src/util/diesel_from_sql_for_enum_strings.rs create mode 100644 src/util/diesel_from_sql_for_json_fields.rs create mode 100644 src/util/diesel_from_sql_for_value_structs.rs create mode 100644 src/util/diesel_to_sql_for_value_structs.rs diff --git a/Cargo.lock b/Cargo.lock index 065527e..9f5c57b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -320,6 +320,7 @@ dependencies = [ "diesel_derives", "downcast-rs", "libsqlite3-sys", + "serde_json", "sqlite-wasm-rs", "time", ] diff --git a/Cargo.toml b/Cargo.toml index 4b1f07b..0d428c5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -11,11 +11,12 @@ argon2 = "0.5.3" axum = { version = "0.8.9", features = ["macros"] } axum-extra = { version = "0.12.5", features = ["typed-header"] } serde = { version = "1.0.228", features = ["derive"] } +serde_json = "1.0.149" tokio = { version = "1.52.1", features = ["macros", "rt-multi-thread", "signal"] } uuid = { version = "1.23.1", features = ["v4", "serde"] } form_urlencoded = "1.2.2" tower = "0.5.3" -diesel = { version = "2.3.9", features = ["returning_clauses_for_sqlite_3_35", "chrono"] } +diesel = { version = "2.3.9", features = ["returning_clauses_for_sqlite_3_35", "chrono", "serde_json"] } diesel-async = { version = "0.9.0", features = ["sqlite", "deadpool", "migrations"] } diesel_migrations = { version = "2.3.2" } libsqlite3-sys = { version = "*", features = ["bundled"] } @@ -30,4 +31,3 @@ strum_macros = "0.28.0" assertables = "9.9.0" base64 = "0.22.1" http-body-util = "0.1.3" -serde_json = "1.0.149" diff --git a/migrations/2026-05-11-094743-0000_create_access_tokens/up.sql b/migrations/2026-05-11-094743-0000_create_access_tokens/up.sql index 60226b2..7ca48a2 100644 --- a/migrations/2026-05-11-094743-0000_create_access_tokens/up.sql +++ b/migrations/2026-05-11-094743-0000_create_access_tokens/up.sql @@ -3,7 +3,7 @@ CREATE TABLE access_tokens id BLOB(16) PRIMARY KEY NOT NULL CHECK (LENGTH(id) = 16), username VARCHAR(64) NOT NULL CHECK (LENGTH(username) > 0 AND LENGTH(username) <= 64), client_id VARCHAR(64) NOT NULL CHECK (LENGTH(client_id) > 0 AND LENGTH(client_id) <= 64), - scopes VARCHAR(16) NOT NULL CHECK (LENGTH(scopes) <= 16), -- TODO - Consider refactoring into a foreign reference + scopes VARCHAR(16) NOT NULL CHECK (LENGTH(scopes) <= 16), issued_at TIMESTAMP NOT NULL, expires_at TIMESTAMP NOT NULL, not_before TIMESTAMP NOT NULL diff --git a/migrations/2026-05-13-144941-0000_create_client_configuration/down.sql b/migrations/2026-05-13-144941-0000_create_client_configuration/down.sql new file mode 100644 index 0000000..b82e6ea --- /dev/null +++ b/migrations/2026-05-13-144941-0000_create_client_configuration/down.sql @@ -0,0 +1 @@ +DROP TABLE IF EXISTS client_configurations; diff --git a/migrations/2026-05-13-144941-0000_create_client_configuration/up.sql b/migrations/2026-05-13-144941-0000_create_client_configuration/up.sql new file mode 100644 index 0000000..5f8e9c1 --- /dev/null +++ b/migrations/2026-05-13-144941-0000_create_client_configuration/up.sql @@ -0,0 +1,9 @@ +CREATE TABLE client_configurations +( + client_id VARCHAR(64) NOT NULL PRIMARY KEY CHECK (LENGTH(client_id) > 0 AND LENGTH(client_id) <= 64), + client_type VARCHAR(12) NOT NULL CHECK (client_type IN ('confidential', 'public')), + redirect_uris BINARY NOT NULL CHECK (json_valid(redirect_uris) AND json_type(redirect_uris) = 'array'), + allowed_scopes BINARY NOT NULL CHECK (json_valid(allowed_scopes) AND json_type(allowed_scopes) = 'array'), + allowed_actions BINARY NOT NULL CHECK (json_valid(allowed_actions) AND json_type(allowed_actions) = 'array'), + allowed_grant_types BINARY NOT NULL CHECK (json_valid(allowed_grant_types) AND json_type(allowed_grant_types) = 'array') +); diff --git a/migrations/2026-05-14-103409-0000_add_client_id_fk_to_access_tokens/down.sql b/migrations/2026-05-14-103409-0000_add_client_id_fk_to_access_tokens/down.sql new file mode 100644 index 0000000..9dc1e69 --- /dev/null +++ b/migrations/2026-05-14-103409-0000_add_client_id_fk_to_access_tokens/down.sql @@ -0,0 +1,16 @@ +CREATE TABLE access_tokens_temp +( + id BLOB(16) PRIMARY KEY NOT NULL CHECK (LENGTH(id) = 16), + username VARCHAR(64) NOT NULL CHECK (LENGTH(username) > 0 AND LENGTH(username) <= 64), + client_id VARCHAR(64) NOT NULL CHECK (LENGTH(client_id) > 0 AND LENGTH(client_id) <= 64), + scopes VARCHAR(16) NOT NULL CHECK (LENGTH(scopes) <= 16), + issued_at TIMESTAMP NOT NULL, + expires_at TIMESTAMP NOT NULL, + not_before TIMESTAMP NOT NULL +); + +INSERT INTO access_tokens_temp SELECT * FROM access_tokens; + +DROP TABLE access_tokens; + +ALTER TABLE access_tokens_temp RENAME TO access_tokens; \ No newline at end of file diff --git a/migrations/2026-05-14-103409-0000_add_client_id_fk_to_access_tokens/up.sql b/migrations/2026-05-14-103409-0000_add_client_id_fk_to_access_tokens/up.sql new file mode 100644 index 0000000..4355aaf --- /dev/null +++ b/migrations/2026-05-14-103409-0000_add_client_id_fk_to_access_tokens/up.sql @@ -0,0 +1,16 @@ +CREATE TABLE access_tokens_temp +( + id BLOB(16) PRIMARY KEY NOT NULL CHECK (LENGTH(id) = 16), + username VARCHAR(64) NOT NULL CHECK (LENGTH(username) > 0 AND LENGTH(username) <= 64), + client_id VARCHAR(64) NOT NULL REFERENCES client_configurations (client_id) ON DELETE CASCADE, + scopes VARCHAR(16) NOT NULL CHECK (LENGTH(scopes) <= 16), + issued_at TIMESTAMP NOT NULL, + expires_at TIMESTAMP NOT NULL, + not_before TIMESTAMP NOT NULL +); + +INSERT INTO access_tokens_temp SELECT * FROM access_tokens; + +DROP TABLE access_tokens; + +ALTER TABLE access_tokens_temp RENAME TO access_tokens; \ No newline at end of file diff --git a/migrations/2026-05-14-140202-0000_insert_dumby_client_configuration/down.sql b/migrations/2026-05-14-140202-0000_insert_dumby_client_configuration/down.sql new file mode 100644 index 0000000..16b1781 --- /dev/null +++ b/migrations/2026-05-14-140202-0000_insert_dumby_client_configuration/down.sql @@ -0,0 +1 @@ +DELETE FROM client_configurations WHERE client_id in ('aardvark', 'badger'); \ No newline at end of file diff --git a/migrations/2026-05-14-140202-0000_insert_dumby_client_configuration/up.sql b/migrations/2026-05-14-140202-0000_insert_dumby_client_configuration/up.sql new file mode 100644 index 0000000..81ed487 --- /dev/null +++ b/migrations/2026-05-14-140202-0000_insert_dumby_client_configuration/up.sql @@ -0,0 +1,4 @@ +-- TODO - Remove if this is used in a production setting +INSERT INTO client_configurations (client_id, client_type, redirect_uris, allowed_scopes, allowed_actions, allowed_grant_types) +VALUES ('aardvark', 'confidential', '[]', '["basic", "read", "write"]', '["introspect"]', '["password"]'), + ('badger', 'public', '[]', '["basic"]', '[]', '[]'); diff --git a/src/client/authentication.rs b/src/client/authentication.rs index eaeac89..d994849 100644 --- a/src/client/authentication.rs +++ b/src/client/authentication.rs @@ -3,9 +3,11 @@ use crate::client::{ClientType, ConfidentialClient, PublicClient}; use crate::client::configuration::ClientConfigurationRepository; use crate::client::secret::ClientSecretRepository; +// TODO - Bubble up low level DB errors don't report them as unauthorised +#[trait_variant::make(Send)] pub trait ClientAuthenticator: Send + Sync + Clone { - fn authenticate_as_public_client(&self, client_id: &str) -> Option; - fn authenticate_as_confidential_client(&self, client_id: &str, client_secret: &[u8]) -> Option; + async fn authenticate_as_public_client(&self, client_id: &str) -> Option; + async fn authenticate_as_confidential_client(&self, client_id: &str, client_secret: &[u8]) -> Option; } #[derive(Clone)] @@ -28,10 +30,10 @@ where S: ClientSecretRepository, C: ClientConfigurationRepository, { - fn authenticate_as_public_client(&self, client_id: &str) -> Option { + async fn authenticate_as_public_client(&self, client_id: &str) -> Option { - match self.client_configuration_repository.find_by_client_id(client_id) { - Some(configuration) if configuration.client_type == ClientType::Public => { + match self.client_configuration_repository.find_by_client_id(client_id).await { + Ok(Some(configuration)) if configuration.client_type == ClientType::Public => { Some(PublicClient { configuration }) }, _ => None @@ -39,7 +41,7 @@ where } // TODO - Do we flip to the lookup from config first, then credential checks? - fn authenticate_as_confidential_client(&self, client_id: &str, client_secret: &[u8]) -> Option { + async fn authenticate_as_confidential_client(&self, client_id: &str, client_secret: &[u8]) -> Option { let secrets = self.secret_repository.find_all_by_client_id(client_id); @@ -54,8 +56,8 @@ where Some(secret) => &secret.client_id, }; - match self.client_configuration_repository.find_by_id(client_id) { - Some(configuration) if configuration.client_type == ClientType::Confidential => { + match self.client_configuration_repository.find_by_id(client_id).await { + Ok(Some(configuration)) if configuration.client_type == ClientType::Confidential => { Some(ConfidentialClient { configuration }) }, _ => None diff --git a/src/client/client_principal.rs b/src/client/client_principal.rs index 2b4b652..4dd5781 100644 --- a/src/client/client_principal.rs +++ b/src/client/client_principal.rs @@ -50,19 +50,19 @@ macro_rules! define_principal { } pub fn can_perform_action(&self, action: &crate::client::ClientAction) -> bool { - self.configuration.allowed_actions.contains(action) + self.configuration.allowed_actions.0.contains(action) } pub fn can_perform_grant_type(&self, grant_type: &crate::client::GrantType) -> bool { - self.configuration.allowed_grant_types.contains(grant_type) + self.configuration.allowed_grant_types.0.contains(grant_type) } pub fn can_be_issued(&self, scope: &crate::scope::Scope) -> bool { - self.configuration.allowed_scopes.contains(scope) + self.configuration.allowed_scopes.0.contains(scope) } pub fn has_redirect_uri(&self, redirect_uri: &str) -> bool { - self.configuration.redirect_uris.contains(redirect_uri) + self.configuration.redirect_uris.0.contains(redirect_uri) } } diff --git a/src/client/configuration.rs b/src/client/configuration.rs index 670ed17..a342bbb 100644 --- a/src/client/configuration.rs +++ b/src/client/configuration.rs @@ -1,67 +1,150 @@ -use std::collections::{HashMap, HashSet}; -use std::sync::{Arc, Mutex, MutexGuard, PoisonError}; use crate::client::{ClientAction, ClientId, ClientType, GrantType}; +use crate::schema::client_configurations::dsl; use crate::scope::Scope; +use crate::util::diesel_types::AsyncSqlitePool; +use crate::{diesel_from_sql_for_json_fields, value_struct}; +use anyhow::{Context, Result}; +use diesel::prelude::*; +use diesel::{AsExpression, FromSqlRow, Queryable, Selectable}; +use diesel_async::RunQueryDsl; +use serde::Deserialize; +use std::collections::HashSet; -#[derive(Clone, Eq, PartialEq)] -#[cfg_attr(test, derive(Debug))] +value_struct! { + #[derive(Deserialize, FromSqlRow, AsExpression)] + #[diesel(sql_type = diesel::sql_types::Binary)] + pub struct RedirectUris(pub HashSet); +} + +value_struct! { + #[derive(Deserialize, FromSqlRow, AsExpression)] + #[diesel(sql_type = diesel::sql_types::Binary)] + pub struct AllowedScopes(pub HashSet); +} + +value_struct! { + #[derive(Deserialize, FromSqlRow, AsExpression)] + #[diesel(sql_type = diesel::sql_types::Binary)] + pub struct AllowedActions(pub HashSet); +} + +value_struct! { + #[derive(Deserialize, FromSqlRow, AsExpression)] + #[diesel(sql_type = diesel::sql_types::Binary)] + pub struct AllowedGrantTypes(pub HashSet); +} + +diesel_from_sql_for_json_fields! { + RedirectUris, + AllowedScopes, + AllowedActions, + AllowedGrantTypes, +} + +#[derive(Debug, Clone, Eq, PartialEq)] +#[derive(Queryable, Selectable)] +#[diesel(table_name = crate::schema::client_configurations)] +#[diesel(check_for_backend(diesel::sqlite::Sqlite))] pub struct ClientConfiguration { pub client_id: ClientId, pub client_type: ClientType, - pub redirect_uris: HashSet, - pub allowed_scopes: HashSet, - pub allowed_actions: HashSet, - pub allowed_grant_types: HashSet, + pub redirect_uris: RedirectUris, + pub allowed_scopes: AllowedScopes, + pub allowed_actions: AllowedActions, + pub allowed_grant_types: AllowedGrantTypes, } -// TODO - #[trait_variant::make(Send)] +#[trait_variant::make(Send)] pub trait ClientConfigurationRepository: Send + Sync + Clone { - fn find_by_id(&self, client_id: &ClientId) -> Option; - fn find_by_client_id(&self, client_id: &str) -> Option; + async fn find_by_id(&self, client_id: &ClientId) -> Result>; + async fn find_by_client_id(&self, client_id: &str) -> Result>; } -#[derive(Clone, Default)] -pub struct InMemoryClientConfigurationRepository { - store: Arc>>, +#[derive(Clone)] +pub struct DieselClientConfigurationRepository { + pool: AsyncSqlitePool, } -impl InMemoryClientConfigurationRepository { - pub fn new() -> Self { - Self { - store: Arc::new(Mutex::new(HashMap::from([ - // TODO - Remove once we've got a means of creating new clients - Self::create_entry(ClientConfiguration { - client_id: ClientId(String::from("aardvark")), - client_type: ClientType::Confidential, - redirect_uris: HashSet::from([]), - allowed_scopes: HashSet::from([Scope::Basic, Scope::Read, Scope::Write]), - allowed_actions: HashSet::from([ClientAction::Introspect]), - allowed_grant_types: HashSet::from([GrantType::Password]), - }), - Self::create_entry(ClientConfiguration { - client_id: ClientId(String::from("badger")), - client_type: ClientType::Public, - redirect_uris: HashSet::from([]), - allowed_scopes: HashSet::from([Scope::Basic]), - allowed_actions: HashSet::from([]), - allowed_grant_types: HashSet::from([]), - }) - ]))) - } +impl DieselClientConfigurationRepository { + pub fn new(pool: AsyncSqlitePool) -> DieselClientConfigurationRepository { + Self { pool } } - fn create_entry(configuration: ClientConfiguration) -> (ClientId, ClientConfiguration) { - (configuration.client_id.clone(), configuration) +} + +impl ClientConfigurationRepository for DieselClientConfigurationRepository { + async fn find_by_id(&self, client_id: &ClientId) -> Result> { + let connection = &mut self.pool.get() + .await + .with_context(|| "Failed to get connection from pool")?; + + let result = dsl::client_configurations + .filter(dsl::client_id.eq(client_id)) + .first::(connection) + .await + .optional() + .with_context(|| "Error querying client configuration database")?; + + Ok(result) } - fn lock_store(&self) -> MutexGuard<'_, HashMap> { - self.store.lock().unwrap_or_else(PoisonError::into_inner) + async fn find_by_client_id(&self, client_id: &str) -> Result> { + self.find_by_id(&ClientId(String::from(client_id))).await } } -impl ClientConfigurationRepository for InMemoryClientConfigurationRepository { - fn find_by_id(&self, client_id: &ClientId) -> Option { - self.lock_store().get(client_id).cloned() +#[cfg(test)] +pub mod test_support { + use super::*; + use crate::util::diesel_migrations::run_diesel_migrations; + impl DieselClientConfigurationRepository { + pub async fn new_in_memory() -> Result { + let pool = crate::util::diesel_pool::create_pool(":memory:")?; + run_diesel_migrations(&pool).await?; + Ok(DieselClientConfigurationRepository::new(pool)) + } } - fn find_by_client_id(&self, client_id: &str) -> Option { - self.find_by_id(&ClientId(String::from(client_id))) + impl std::fmt::Debug for DieselClientConfigurationRepository { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("DieselClientConfigurationRepository") + .field("pool.manager", &self.pool.manager()) + .finish() + } } } + +#[cfg(test)] +mod integration_tests { + use super::*; + use assertables::*; + + #[tokio::test(flavor = "multi_thread")] + async fn should_be_able_to_retrieve_configuration_by_id() { + let under_test = assert_ok!(DieselClientConfigurationRepository::new_in_memory().await); + let client_id = ClientId("aardvark".into()); + let result = assert_some!(assert_ok!(under_test.find_by_id(&client_id).await)); + assert_eq!(result.client_id, client_id); + assert_eq!(result.client_type, ClientType::Confidential); + assert_is_empty!(result.redirect_uris.0); + assert_contains!(result.allowed_actions.0, &ClientAction::Introspect); + assert_len_eq_x!(result.allowed_actions.0, 1); + assert_contains!(result.allowed_scopes.0, &Scope::Basic); + assert_contains!(result.allowed_scopes.0, &Scope::Read); + assert_contains!(result.allowed_scopes.0, &Scope::Write); + assert_len_eq_x!(result.allowed_scopes.0, 3); + assert_contains!(result.allowed_grant_types.0, &GrantType::Password); + assert_len_eq_x!(result.allowed_grant_types.0, 1); + } + + #[tokio::test(flavor = "multi_thread")] + async fn should_be_able_to_retrieve_configuration_by_client_id() { + let under_test = assert_ok!(DieselClientConfigurationRepository::new_in_memory().await); + let client_id = ClientId("badger".into()); + let result = assert_some!(assert_ok!(under_test.find_by_id(&client_id).await)); + assert_eq!(result.client_id, client_id); + assert_eq!(result.client_type, ClientType::Public); + assert_is_empty!(result.redirect_uris.0); + assert_is_empty!(result.allowed_actions.0); + assert_is_empty!(result.allowed_grant_types.0); + assert_contains!(result.allowed_scopes.0, &Scope::Basic); + assert_len_eq_x!(result.allowed_scopes.0, 1); + } +} \ No newline at end of file diff --git a/src/client/middleware.rs b/src/client/middleware.rs index 04e5845..c664fb6 100644 --- a/src/client/middleware.rs +++ b/src/client/middleware.rs @@ -20,6 +20,7 @@ pub async fn require_confidential_client_authentication( None => return Err(StatusCode::UNAUTHORIZED), Some(TypedHeader(Authorization(basic))) => { authenticator.authenticate_as_confidential_client(basic.username(), basic.password().as_bytes()) + .await //.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)? .ok_or(StatusCode::UNAUTHORIZED)? }, @@ -57,6 +58,7 @@ pub async fn require_client_authentication( // Confidential client via Basic auth (Some(TypedHeader(Authorization(basic))), None) => { authenticator.authenticate_as_confidential_client(basic.username(), basic.password().as_bytes()) + .await //.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)? .map(ClientPrincipal::Confidential) }, @@ -64,6 +66,7 @@ pub async fn require_client_authentication( // Public client via body client_id (None, Some(client_id)) => { authenticator.authenticate_as_public_client(&client_id) + .await //.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)? .map(ClientPrincipal::Public) }, diff --git a/src/client/mod.rs b/src/client/mod.rs index 56a2d84..b038145 100644 --- a/src/client/mod.rs +++ b/src/client/mod.rs @@ -5,33 +5,56 @@ pub mod authentication; pub mod configuration; pub mod middleware; -use crate::disable_deserialization; use crate::value_struct; +use crate::{diesel_from_sql_for_enum_strings, diesel_from_sql_for_value_structs, diesel_to_sql_for_value_structs, disable_deserialization}; +use diesel::sql_types::Text; +use diesel::{AsExpression, FromSqlRow}; +use serde::{Deserialize, Serialize}; use strum_macros::{Display, EnumString}; value_struct! { + #[derive(Hash, FromSqlRow, AsExpression)] + #[diesel(sql_type = diesel::sql_types::Text)] pub struct ClientId(String); } +diesel_from_sql_for_value_structs! { + #[sql_type(Text)] + ClientId(String); +} + +diesel_to_sql_for_value_structs! { + #[sql_type(Text)] + ClientId(String); +} + disable_deserialization!(ClientId); -#[derive(Hash, Eq, PartialEq, Clone)] -#[cfg_attr(test, derive(Debug))] +#[derive(EnumString, Display, Debug, Hash, Eq, PartialEq, Clone)] +#[strum(serialize_all = "snake_case")] +#[derive(FromSqlRow, AsExpression)] +#[diesel(sql_type = Text)] pub enum ClientType { Confidential, Public, } -#[derive(Hash, Eq, PartialEq, Clone)] -#[cfg_attr(test, derive(Debug))] +diesel_from_sql_for_enum_strings! { + ClientType +} + +#[derive(EnumString, Display, Serialize, Deserialize, Debug, Hash, Eq, PartialEq, Clone)] +#[strum(serialize_all = "snake_case")] +#[serde(rename_all = "snake_case")] pub enum ClientAction { // Authorize, Introspect, // ProofKeyForCodeExchange, } -#[derive(EnumString, Display, Debug, Hash, Eq, PartialEq, Clone)] +#[derive(EnumString, Display, Serialize, Deserialize, Debug, Hash, Eq, PartialEq, Clone)] #[strum(serialize_all = "snake_case")] +#[serde(rename_all = "snake_case")] pub enum GrantType { // AuthorizationCode, Password, @@ -47,7 +70,7 @@ principal! { #[cfg(test)] pub mod test_support { - use crate::client::configuration::ClientConfiguration; + use crate::client::configuration::{AllowedActions, AllowedGrantTypes, AllowedScopes, ClientConfiguration, RedirectUris}; use crate::client::{ClientId, ClientPrincipal, ClientType, ConfidentialClient, GrantType, PublicClient}; use crate::scope::Scope; use std::collections::HashSet; @@ -83,10 +106,10 @@ pub mod test_support { ClientConfiguration { client_id: ClientId(client_id.into()), client_type, - redirect_uris: HashSet::default(), - allowed_scopes: HashSet::from([Scope::Basic, Scope::Read, Scope::Write]), - allowed_actions: HashSet::default(), - allowed_grant_types: HashSet::from([GrantType::Password]), + redirect_uris: RedirectUris(HashSet::default()), + allowed_scopes: AllowedScopes(HashSet::from([Scope::Basic, Scope::Read, Scope::Write])), + allowed_actions: AllowedActions(HashSet::default()), + allowed_grant_types: AllowedGrantTypes(HashSet::from([GrantType::Password])), } } } diff --git a/src/main.rs b/src/main.rs index dcab6d3..973a3b7 100644 --- a/src/main.rs +++ b/src/main.rs @@ -22,7 +22,7 @@ use crate::util::diesel_pool::create_pool; use anyhow::{Context, Result}; use axum::{serve, Router}; use client::authentication::ClientAuthenticationService; -use client::configuration::InMemoryClientConfigurationRepository; +use client::configuration::DieselClientConfigurationRepository; use client::secret::InMemoryClientSecretRepository; use token::repository::DieselAccessTokenRepository; use token_exchange::TokenExchangeState; @@ -61,7 +61,7 @@ async fn main() -> Result<()> { let access_token_repository = DieselAccessTokenRepository::new(pool.clone()); let client_secret_repository = InMemoryClientSecretRepository::new(); - let client_configuration_repository = InMemoryClientConfigurationRepository::new(); + let client_configuration_repository = DieselClientConfigurationRepository::new(pool.clone()); let client_authenticator = ClientAuthenticationService::new( client_secret_repository.clone(), diff --git a/src/schema.rs b/src/schema.rs index 5868e86..d78068c 100644 --- a/src/schema.rs +++ b/src/schema.rs @@ -11,3 +11,18 @@ diesel::table! { not_before -> Timestamp, } } + +diesel::table! { + client_configurations (client_id) { + client_id -> Text, + client_type -> Text, + redirect_uris -> Binary, + allowed_scopes -> Binary, + allowed_actions -> Binary, + allowed_grant_types -> Binary, + } +} + +diesel::joinable!(access_tokens -> client_configurations (client_id)); + +diesel::allow_tables_to_appear_in_same_query!(access_tokens, client_configurations,); diff --git a/src/scope/mod.rs b/src/scope/mod.rs index 72702c0..0edaca1 100644 --- a/src/scope/mod.rs +++ b/src/scope/mod.rs @@ -1,22 +1,22 @@ pub mod parser; use crate::disable_deserialization; -use serde::{Serialize, Serializer}; +use serde::{Deserialize, Serialize, Serializer}; use std::collections::HashSet; use strum_macros::Display; use strum_macros::EnumIter; use strum_macros::EnumString; -#[derive(EnumString, EnumIter, Display, Debug, Hash, Eq, PartialEq, Clone)] +#[derive(EnumString, EnumIter, Display, Serialize, Deserialize, Debug, Hash, Eq, PartialEq, Clone)] #[strum(serialize_all = "snake_case")] +#[serde(rename_all = "snake_case")] pub enum Scope { Basic, Read, Write, } -#[derive(Eq, PartialEq, Clone)] -#[cfg_attr(test, derive(Debug))] +#[derive(Debug, Eq, PartialEq, Clone)] pub struct Scopes(pub HashSet); impl Serialize for Scopes { @@ -31,5 +31,4 @@ impl Serialize for Scopes { } // To enable us to trust Scope is valid, we don't allow direct deserialization of Scope. -disable_deserialization!(Scope); disable_deserialization!(Scopes); diff --git a/src/token/mod.rs b/src/token/mod.rs index a7cb78e..3d4f778 100644 --- a/src/token/mod.rs +++ b/src/token/mod.rs @@ -1,12 +1,13 @@ pub mod repository; +use crate::util::uuid_wrapper::UuidWrapper; use chrono::NaiveDateTime; use diesel::prelude::*; use serde::Serialize; -use crate::util::uuid_wrapper::UuidWrapper; +use strum_macros::{Display, EnumString}; -#[cfg_attr(test, derive(Debug))] -#[derive(Serialize, Eq, PartialEq)] +#[derive(Serialize, EnumString, Display, Debug, Eq, PartialEq)] +#[strum(serialize_all = "snake_case")] #[serde(rename_all = "snake_case")] pub enum TokenType { // https://www.rfc-editor.org/rfc/rfc6750 diff --git a/src/token_exchange/grant/password.rs b/src/token_exchange/grant/password.rs index 699279b..ac72a24 100644 --- a/src/token_exchange/grant/password.rs +++ b/src/token_exchange/grant/password.rs @@ -1,19 +1,19 @@ -use std::collections::HashMap; -use chrono::{Duration, Utc}; -use serde::Deserialize; -use ClientPrincipal::Confidential; -use GrantType::Password; use crate::client::authentication::ClientAuthenticator; use crate::client::{ClientPrincipal, ConfidentialClient, GrantType}; -use crate::token::{AccessToken, TokenType}; +use crate::scope::parser::parse_scopes; +use crate::scope::Scopes; use crate::token::repository::TokenRepository; +use crate::token::{AccessToken, TokenType}; use crate::token_exchange::response::{ErrorType, TokenExchangeResponse}; use crate::token_exchange::route::TokenExchangeState; -use crate::scope::Scopes; -use crate::scope::parser::parse_scopes; +use crate::util::uuid_wrapper::UuidWrapper; use crate::util::value_struct::ValueStruct; use anyhow::Result; -use crate::util::uuid_wrapper::UuidWrapper; +use chrono::{Duration, Utc}; +use serde::Deserialize; +use std::collections::HashMap; +use ClientPrincipal::Confidential; +use GrantType::Password; #[derive(Deserialize, Eq, PartialEq)] #[cfg_attr(test, derive(Debug))] @@ -109,16 +109,17 @@ mod unit_tests { // See: https://github.com/beercanx/oauth-api/blob/main/api/token/src/test/kotlin/uk/co/baconi/oauth/api/token/PasswordValidationTest.kt use super::*; - use assertables::*; - use std::collections::HashSet; - use crate::client::ClientType; use crate::client::configuration::ClientConfiguration; + use crate::client::ClientType; + use crate::map_of; use crate::scope::Scope; use crate::token_exchange::response::ErrorType; - use crate::map_of; + use assertables::*; + use std::collections::HashSet; mod client { use super::*; + use crate::client::configuration::{AllowedActions, AllowedGrantTypes, AllowedScopes, RedirectUris}; #[test] fn should_return_invalid_request_for_a_public_client() { @@ -145,10 +146,10 @@ mod unit_tests { ClientPrincipal::new_principal(ClientConfiguration { client_id: String::from("unauthorised").into(), client_type: ClientType::Confidential, - redirect_uris: HashSet::default(), - allowed_scopes: HashSet::default(), - allowed_actions: HashSet::default(), - allowed_grant_types: HashSet::default(), + redirect_uris: RedirectUris(HashSet::default()), + allowed_scopes: AllowedScopes(HashSet::default()), + allowed_actions: AllowedActions(HashSet::default()), + allowed_grant_types: AllowedGrantTypes(HashSet::default()), }), &map_of! { "username" => "aardvark", @@ -227,6 +228,7 @@ mod unit_tests { mod scope { use super::*; + use crate::client::configuration::{AllowedActions, AllowedGrantTypes, AllowedScopes, RedirectUris}; #[test] fn should_return_invalid_request_on_blank_scope() { @@ -253,10 +255,10 @@ mod unit_tests { ClientPrincipal::new_principal(ClientConfiguration { client_id: String::from("aardvark").into(), client_type: ClientType::Confidential, - redirect_uris: HashSet::default(), - allowed_scopes: HashSet::default(), - allowed_actions: HashSet::default(), - allowed_grant_types: HashSet::from([Password]), + redirect_uris: RedirectUris(HashSet::default()), + allowed_scopes: AllowedScopes(HashSet::default()), + allowed_actions: AllowedActions(HashSet::default()), + allowed_grant_types: AllowedGrantTypes(HashSet::from([Password])), }), &map_of! { "username" => "aardvark", @@ -316,10 +318,10 @@ mod unit_tests { ClientPrincipal::new_principal(ClientConfiguration { client_id: String::from("aardvark").into(), client_type: ClientType::Confidential, - redirect_uris: HashSet::default(), - allowed_scopes: HashSet::from([Scope::Read]), - allowed_actions: HashSet::default(), - allowed_grant_types: HashSet::from([Password]), + redirect_uris: RedirectUris(HashSet::default()), + allowed_scopes: AllowedScopes(HashSet::from([Scope::Read])), + allowed_actions: AllowedActions(HashSet::default()), + allowed_grant_types: AllowedGrantTypes(HashSet::from([Password])), }), &map_of! { "username" => "aardvark", diff --git a/src/token_exchange/request.rs b/src/token_exchange/request.rs index ec4b256..0a8ece2 100644 --- a/src/token_exchange/request.rs +++ b/src/token_exchange/request.rs @@ -1,14 +1,14 @@ -use std::collections::HashMap; -use axum::extract::{FromRequest, Request}; -use axum::extract::rejection::FormRejection; -use axum::{Form, Json}; -use axum::http::StatusCode; -use axum::response::{IntoResponse, Response}; -use serde::Deserialize; use crate::client::{ClientPrincipal, GrantType}; use crate::token_exchange::grant::password::{validate_password_grant, PasswordGrantRequest}; use crate::token_exchange::request::TokenExchangeRequest::Password; use crate::token_exchange::response::{ErrorType, TokenExchangeResponse}; +use axum::extract::rejection::FormRejection; +use axum::extract::{FromRequest, Request}; +use axum::http::StatusCode; +use axum::response::{IntoResponse, Response}; +use axum::{Form, Json}; +use serde::Deserialize; +use std::collections::HashMap; #[derive(Deserialize, Eq, PartialEq)] #[cfg_attr(test, derive(Debug))] @@ -88,12 +88,12 @@ mod unit_tests { // See: https://github.com/beercanx/oauth-api/blob/main/api/token/src/test/kotlin/uk/co/baconi/oauth/api/token/TokenRequestValidationTest.kt - use std::collections::HashSet; use super::*; - use assertables::*; + use crate::client::configuration::{AllowedActions, AllowedGrantTypes, AllowedScopes, ClientConfiguration, RedirectUris}; use crate::client::ClientType; - use crate::client::configuration::ClientConfiguration; use crate::token_exchange::request::validate_grant_type; + use assertables::*; + use std::collections::HashSet; macro_rules! input_parameters { ($($k:expr => $v:expr),* $(,)?) => {{ @@ -151,10 +151,10 @@ mod unit_tests { ClientPrincipal::new_principal(ClientConfiguration { client_id: String::from("invalid").into(), client_type: ClientType::Confidential, - redirect_uris: HashSet::default(), - allowed_scopes: HashSet::default(), - allowed_actions: HashSet::default(), - allowed_grant_types: HashSet::default(), + redirect_uris: RedirectUris(HashSet::default()), + allowed_scopes: AllowedScopes(HashSet::default()), + allowed_actions: AllowedActions(HashSet::default()), + allowed_grant_types: AllowedGrantTypes(HashSet::default()), }), &input_parameters! { "grant_type" => "password" }, TokenExchangeResponse::Failure { diff --git a/src/token_exchange/response.rs b/src/token_exchange/response.rs index d3a873b..84d80e5 100644 --- a/src/token_exchange/response.rs +++ b/src/token_exchange/response.rs @@ -1,7 +1,7 @@ -use serde::Serialize; use crate::scope::Scopes; use crate::token::TokenType; use crate::util::uuid_wrapper::UuidWrapper; +use serde::Serialize; #[cfg_attr(test, derive(Debug))] #[derive(Serialize, Eq, PartialEq)] @@ -69,8 +69,7 @@ impl TokenExchangeResponse { } } -#[cfg_attr(test, derive(Debug))] -#[derive(Serialize, Eq, PartialEq)] +#[derive(Serialize, Debug, Eq, PartialEq)] #[serde(rename_all = "snake_case")] pub enum ErrorType { diff --git a/src/token_exchange/route.rs b/src/token_exchange/route.rs index 386bdef..2d522c7 100644 --- a/src/token_exchange/route.rs +++ b/src/token_exchange/route.rs @@ -68,7 +68,7 @@ mod integration_tests { use serde_json::Value; use tower::ServiceExt; use crate::client::authentication::ClientAuthenticationService; - use crate::client::configuration::InMemoryClientConfigurationRepository; + use crate::client::configuration::DieselClientConfigurationRepository; use crate::client::secret::InMemoryClientSecretRepository; use crate::token::repository::DieselAccessTokenRepository; @@ -84,7 +84,7 @@ mod integration_tests { access_token_repository: assert_ok!(DieselAccessTokenRepository::new_in_memory().await), client_authenticator: ClientAuthenticationService::new( InMemoryClientSecretRepository::new(), - InMemoryClientConfigurationRepository::new(), + assert_ok!(DieselClientConfigurationRepository::new_in_memory().await), ), }) } diff --git a/src/util/diesel_from_sql_for_enum_strings.rs b/src/util/diesel_from_sql_for_enum_strings.rs new file mode 100644 index 0000000..0928ca7 --- /dev/null +++ b/src/util/diesel_from_sql_for_enum_strings.rs @@ -0,0 +1,19 @@ +#[macro_export] +macro_rules! diesel_from_sql_for_enum_strings { + ($($enum_type:ident)+) => { + $( + impl diesel::deserialize::FromSql for $enum_type + where + String: diesel::deserialize::FromSql, + $enum_type: std::str::FromStr + std::fmt::Display, + { + fn from_sql(raw: ::RawValue<'_>) -> diesel::deserialize::Result<$enum_type> { + use std::str::FromStr; + $enum_type::from_str(String::from_sql(raw)?.as_str()).map_err(|e| { + anyhow::anyhow!("Invalid {} Type: {}", stringify!($enum_type), e).into() + }) + } + } + )+ + }; +} \ No newline at end of file diff --git a/src/util/diesel_from_sql_for_json_fields.rs b/src/util/diesel_from_sql_for_json_fields.rs new file mode 100644 index 0000000..bf2387d --- /dev/null +++ b/src/util/diesel_from_sql_for_json_fields.rs @@ -0,0 +1,18 @@ +#[macro_export] +macro_rules! diesel_from_sql_for_json_fields { + ( + $($field_type:ident$(<$optional_inner_type:ident>)?),+ + $(,)? + ) => { + $( + impl diesel::deserialize::FromSql for $field_type$(<$optional_inner_type>)? { + fn from_sql(mut value: diesel::sqlite::SqliteValue<'_, '_, '_>) -> diesel::deserialize::Result<$field_type$(<$optional_inner_type>)?> { + serde_json::from_slice(value.read_blob()).map_err(|e| { + let field_type = concat!(stringify!($field_type$(<$optional_inner_type>)?)); + anyhow::anyhow!("Invalid {field_type}: {e}").into() + }) + } + } + )+ + }; +} diff --git a/src/util/diesel_from_sql_for_value_structs.rs b/src/util/diesel_from_sql_for_value_structs.rs new file mode 100644 index 0000000..17bb673 --- /dev/null +++ b/src/util/diesel_from_sql_for_value_structs.rs @@ -0,0 +1,44 @@ +#[macro_export] +macro_rules! diesel_from_sql_for_value_structs { + ( + $( + #[sql_type($sql_type:ty)] + $struct_name:ident($field_type:ident) + );+ + $(;)? + ) => { + $( + impl diesel::deserialize::FromSql<$sql_type, B> for $struct_name + where + $field_type: diesel::deserialize::FromSql<$sql_type, B> + { + fn from_sql(raw: ::RawValue<'_>) -> diesel::deserialize::Result<$struct_name> { + $field_type::from_sql(raw).map($struct_name) + } + } + )+ + }; +} + +#[cfg(test)] +#[allow(dead_code)] +mod test { + use crate::value_struct; + + value_struct! { + struct First(String); + } + + value_struct! { + struct Second(i32); + } + + diesel_from_sql_for_value_structs! { + + #[sql_type(diesel::sql_types::Text)] + First(String); + + #[sql_type(diesel::sql_types::BigInt)] + Second(i32); + } +} \ No newline at end of file diff --git a/src/util/diesel_pool.rs b/src/util/diesel_pool.rs index 1757617..453963c 100644 --- a/src/util/diesel_pool.rs +++ b/src/util/diesel_pool.rs @@ -1,9 +1,18 @@ +use crate::util::diesel_types::{AsyncSqliteConnectionManager, AsyncSqlitePool}; use anyhow::Context; use anyhow::Result; -use crate::util::diesel_types::{AsyncSqliteConnectionManager, AsyncSqlitePool}; pub fn create_pool(database_url: &str) -> Result { AsyncSqlitePool::builder(AsyncSqliteConnectionManager::new(database_url)) + // Example of how we might have had to enable foreign key constraints on SQLite: https://sqlite.org/foreignkeys.html#fk_enable + // .post_create(Hook::async_fn(|connection: &mut AsyncSqliteConnection, _| { + // Box::pin(async move { + // match sql_query("PRAGMA foreign_keys = ON;").execute(connection).await { + // Ok(_) => Ok(()), + // Err(err) => Err(HookError::Backend(PoolError::QueryError(err))), + // } + // }) + // })) // TODO - Add configuration options? //.max_size(cpu_core_count * 2) //.create_timeout(Some(std::time::Duration::from_secs(x))) @@ -11,4 +20,31 @@ pub fn create_pool(database_url: &str) -> Result { //.wait_timeout(Some(std::time::Duration::from_secs(z))) .build() .with_context(|| format!("Failed to create sqlite database pool: {database_url}")) -} \ No newline at end of file +} + +#[cfg(test)] +mod integration_tests { + use super::*; + use diesel::sql_query; + use diesel::sql_types::Integer; + use diesel::QueryableByName; + use diesel_async::RunQueryDsl; + + #[derive(QueryableByName)] + struct FkPragma { + #[diesel(sql_type = Integer)] + foreign_keys: i32, + } + + #[allow(clippy::unwrap_used)] + #[tokio::test(flavor = "multi_thread")] + async fn should_have_foreign_key_support_enabled() { + let pool = create_pool(":memory:").unwrap(); + let mut connection = pool.get().await.unwrap(); + let result = sql_query("PRAGMA foreign_keys;") + .get_result::(&mut connection) + .await + .unwrap(); + assert_eq!(result.foreign_keys, 1); + } +} diff --git a/src/util/diesel_to_sql_for_value_structs.rs b/src/util/diesel_to_sql_for_value_structs.rs new file mode 100644 index 0000000..c35c9f9 --- /dev/null +++ b/src/util/diesel_to_sql_for_value_structs.rs @@ -0,0 +1,44 @@ +#[macro_export] +macro_rules! diesel_to_sql_for_value_structs { + ( + $( + #[sql_type($sql_type:ty)] + $struct_name:ident($field_type:ident) + );+ + $(;)? + ) => { + $( + impl diesel::serialize::ToSql<$sql_type, B> for $struct_name + where + $field_type: diesel::serialize::ToSql<$sql_type, B> + { + fn to_sql<'b>(&'b self, out: &mut diesel::serialize::Output<'b, '_, B>) -> diesel::serialize::Result { + $field_type::to_sql(&self.0, out) + } + } + )+ + }; +} + +#[cfg(test)] +#[allow(dead_code)] +mod test { + use crate::value_struct; + + value_struct! { + struct First(String); + } + + value_struct! { + struct Second(i32); + } + + diesel_to_sql_for_value_structs! { + + #[sql_type(diesel::sql_types::Text)] + First(String); + + #[sql_type(diesel::sql_types::BigInt)] + Second(i32); + } +} \ No newline at end of file diff --git a/src/util/mod.rs b/src/util/mod.rs index 744fee6..e3e1421 100644 --- a/src/util/mod.rs +++ b/src/util/mod.rs @@ -1,8 +1,12 @@ +pub mod diesel_from_sql_for_enum_strings; +pub mod diesel_from_sql_for_json_fields; +pub mod diesel_from_sql_for_value_structs; +pub mod diesel_migrations; +pub mod diesel_pool; +pub mod diesel_to_sql_for_value_structs; +pub mod diesel_types; pub mod disable_deserialization; pub mod disable_serialization; pub mod map_of; -pub mod value_struct; pub mod uuid_wrapper; -pub mod diesel_migrations; -pub mod diesel_types; -pub mod diesel_pool; \ No newline at end of file +pub mod value_struct; \ No newline at end of file diff --git a/src/util/value_struct.rs b/src/util/value_struct.rs index 0fbcede..f7c7d65 100644 --- a/src/util/value_struct.rs +++ b/src/util/value_struct.rs @@ -7,18 +7,17 @@ pub trait ValueStruct { macro_rules! value_struct { ( $(#[$m:meta])* - $vis:vis struct $struct_name:ident($field_type:ident); + $vis:vis struct $struct_name:ident($field_vis:vis $field_type:ident$(<$optional_inner_type:ident>)?); ) => { $(#[$m])* #[non_exhaustive] - #[derive(Clone, Hash, Eq, PartialEq)] + #[derive(Debug, Clone, Eq, PartialEq)] #[derive(serde::Serialize)] #[serde(transparent)] - #[cfg_attr(test, derive(Debug))] - $vis struct $struct_name($field_type); + $vis struct $struct_name($field_vis $field_type$(<$optional_inner_type>)?); impl $crate::util::value_struct::ValueStruct for $struct_name { - type ValueType = $field_type; + type ValueType = $field_type$(<$optional_inner_type>)?; #[inline] fn value(&self) -> &Self::ValueType { @@ -26,14 +25,14 @@ macro_rules! value_struct { } } - impl std::convert::From<$field_type> for $struct_name { - fn from(value: $field_type) -> Self { + impl std::convert::From<$field_type$(<$optional_inner_type>)?> for $struct_name { + fn from(value: $field_type$(<$optional_inner_type>)?) -> Self { $struct_name(value) } } - impl std::convert::From<&$field_type> for $struct_name { - fn from(value: &$field_type) -> Self { + impl std::convert::From<&$field_type$(<$optional_inner_type>)?> for $struct_name { + fn from(value: &$field_type$(<$optional_inner_type>)?) -> Self { $struct_name(value.clone()) } } From 9782e50ccf62143d0c4fae2e7fccb4c0bf2229d3 Mon Sep 17 00:00:00 2001 From: James Bacon Date: Mon, 18 May 2026 12:38:27 +0100 Subject: [PATCH 16/19] Migrated access_tokens to using a json backed scopes field. --- .idea/runConfigurations/build.xml | 1 + .../down.sql | 24 ++++++++++ .../up.sql | 45 +++++++++++++++++++ src/client/client_principal.rs | 2 +- src/client/configuration.rs | 17 ++++--- src/client/mod.rs | 4 +- src/schema.rs | 2 +- src/scope/mod.rs | 14 +++++- src/token/mod.rs | 12 +++-- src/token/repository.rs | 21 ++------- src/token_exchange/grant/password.rs | 7 ++- src/token_introspection/request.rs | 8 ++-- src/token_introspection/response.rs | 5 ++- src/token_introspection/route.rs | 2 +- src/util/diesel_from_sql_for_json_fields.rs | 37 +++++++++++---- src/util/diesel_to_sql_for_json_fields.rs | 36 +++++++++++++++ src/util/mod.rs | 1 + 17 files changed, 180 insertions(+), 58 deletions(-) create mode 100644 migrations/2026-05-18-090442-0000_migrate_access_token_scopes_to_json_array/down.sql create mode 100644 migrations/2026-05-18-090442-0000_migrate_access_token_scopes_to_json_array/up.sql create mode 100644 src/util/diesel_to_sql_for_json_fields.rs diff --git a/.idea/runConfigurations/build.xml b/.idea/runConfigurations/build.xml index 32483de..29955ad 100644 --- a/.idea/runConfigurations/build.xml +++ b/.idea/runConfigurations/build.xml @@ -1,5 +1,6 @@ +