Browse Source

Untested migration to microrm 0.5-dev.

Kestrel 1 tuần trước cách đây
mục cha
commit
d0eea21e6d
22 tập tin đã thay đổi với 515 bổ sung297 xóa
  1. 5 8
      Cargo.lock
  2. 1 1
      Cargo.toml
  3. 80 31
      src/cli.rs
  4. 4 3
      src/cli/client.rs
  5. 10 6
      src/cli/group.rs
  6. 9 5
      src/cli/role.rs
  7. 9 5
      src/cli/scope.rs
  8. 30 23
      src/cli/user.rs
  9. 6 2
      src/client.rs
  10. 29 17
      src/client_management.rs
  11. 15 11
      src/ext.rs
  12. 5 2
      src/ext/github.rs
  13. 55 33
      src/key.rs
  14. 27 15
      src/realm.rs
  15. 2 2
      src/schema.rs
  16. 10 3
      src/server.rs
  17. 5 3
      src/server/oidc.rs
  18. 31 20
      src/server/oidc/authorize.rs
  19. 34 18
      src/server/oidc/token.rs
  20. 89 59
      src/server/session.rs
  21. 23 9
      src/server/um.rs
  22. 36 21
      src/user.rs

+ 5 - 8
Cargo.lock

@@ -1,6 +1,6 @@
 # This file is automatically @generated by Cargo.
 # It is not intended for manual editing.
-version = 3
+version = 4
 
 [[package]]
 name = "addr2line"
@@ -1554,25 +1554,22 @@ checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3"
 
 [[package]]
 name = "microrm"
-version = "0.4.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "db4126053877d00d90eb5bf850ac87b72094d9eff4a5d76dcecbd1c7ea161814"
+version = "0.5.0-rc.3"
 dependencies = [
  "clap",
- "itertools",
+ "lazy_static",
  "libsqlite3-sys",
  "log",
  "microrm-macros",
  "serde",
  "serde_json",
+ "sha2 0.10.8",
  "time 0.3.36",
 ]
 
 [[package]]
 name = "microrm-macros"
-version = "0.4.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "80eb88bc6673048db8d83f8c57f8eb180f9755aa5e3018db3b7609cc800bc337"
+version = "0.5.0-rc.3"
 dependencies = [
  "convert_case",
  "proc-macro2",

+ 1 - 1
Cargo.toml

@@ -29,7 +29,7 @@ bincode = "1.3"
 toml = "0.8.2"
 
 # Data storage dependencies
-microrm = { version = "0.4.2", features = ["clap", "bundled_sqlite"] }
+microrm = { version = "0.5.0-dev", features = ["clap", "bundled_sqlite", "time"], path = "../microrm/microrm" }
 serde_bytes = { version = "0.11.6" }
 
 # Public API/server dependencies

+ 80 - 31
src/cli.rs

@@ -81,6 +81,7 @@ enum Command {
 pub struct RunArgs {
     pub config: Config,
     pub db: UIDCDatabase,
+    pub pool: microrm::ConnectionPool,
     pub realm: Stored<schema::Realm>,
 }
 
@@ -91,55 +92,95 @@ impl RootArgs {
         let config: Config =
             toml::from_str(config_contents.as_str()).expect("couldn't parse configuration file");
 
+        let (pool, db) = microrm::ConnectionPool::open::<UIDCDatabase>(&config.db_path)
+            .map_err(|e| UIDCError::AbortString(format!("Error accessing database: {e:?}")))?;
+
+        let mut lease = pool.acquire()?;
+
         if let Command::Init = self.command {
-            return self.init(config).await;
+            return self.init(db, &mut lease, config).await;
         }
 
-        let db = UIDCDatabase::open_path(&config.db_path)
-            .map_err(|e| UIDCError::AbortString(format!("Error accessing database: {:?}", e)))?;
+        // let db = UIDCDatabase::open_path(&config.db_path)
+        //    .map_err(|e| UIDCError::AbortString(format!("Error accessing database: {:?}", e)))?;
 
         let realm = db
             .realms
             .keyed(&self.realm)
-            .get()?
+            .get(&mut lease)?
             .ok_or(UIDCError::Abort("no such realm"))?;
 
-        let ra = RunArgs { config, db, realm };
-
         match self.command {
             Command::Init => unreachable!(),
             // Command::Config(v) => v.run(ra).await,
-            Command::Key(v) => v.run(ra).await,
-            Command::Client { cmd } => cmd.perform(&ra.realm, &ra.realm.clients),
-            Command::Scope { cmd } => cmd.perform(&ra.realm, &ra.realm.scopes),
-            Command::Group { cmd } => cmd.perform(&ra.realm, &ra.realm.groups),
-            Command::Serve(v) => v.run(ra).await,
-            Command::Token { cmd } => cmd.run(ra).await,
-            Command::Role { cmd } => cmd.perform(&ra.realm, &ra.realm.roles),
-            Command::User { cmd } => cmd.perform(&ra, &ra.realm.users),
+            Command::Key(v) => {
+                drop(lease);
+                let ra = RunArgs {
+                    config,
+                    db,
+                    pool,
+                    realm,
+                };
+                v.run(ra).await
+            }
+            Command::Serve(v) => {
+                drop(lease);
+                let ra = RunArgs {
+                    config,
+                    db,
+                    pool,
+                    realm,
+                };
+                v.run(ra).await
+            }
+            Command::Token { cmd } => {
+                drop(lease);
+                let ra = RunArgs {
+                    config,
+                    db,
+                    pool,
+                    realm,
+                };
+                cmd.run(ra).await
+            }
+            Command::Client { cmd } => cmd.perform(&realm, &mut lease, &realm.clients),
+            Command::Scope { cmd } => cmd.perform(&realm, &mut lease, &realm.scopes),
+            Command::Group { cmd } => cmd.perform(&realm, &mut lease, &realm.groups),
+            Command::Role { cmd } => cmd.perform(&realm, &mut lease, &realm.roles),
+            Command::User { cmd } => {
+                cmd.perform(&(realm.clone(), config), &mut lease, &realm.users)
+            }
         }
     }
 
-    async fn init(&self, config: Config) -> Result<(), UIDCError> {
+    async fn init<'l>(
+        &self,
+        db: UIDCDatabase,
+        lease: &mut microrm::ConnectionLease<'l>,
+        config: Config,
+    ) -> Result<(), UIDCError> {
         // first check to see if the database is already vaguely set up
-        let db = UIDCDatabase::open_path(&config.db_path)
-            .map_err(|e| UIDCError::AbortString(format!("Error accessing database: {:?}", e)))?;
+        /*let db = UIDCDatabase::open_path(&config.db_path)
+        .map_err(|e| UIDCError::AbortString(format!("Error accessing database: {:?}", e)))?;*/
 
         log::info!("Initializing!");
 
-        if db.realms.keyed("primary").get()?.is_some() {
+        if db.realms.keyed("primary").get(lease)?.is_some() {
             log::warn!("Already initialized with primary realm!");
             return Ok(());
         }
 
         // create primary realm
-        let primary = db.realms.insert_and_return(schema::Realm {
-            shortname: "primary".into(),
-            ..Default::default()
-        })?;
+        let primary = db.realms.insert_and_return(
+            lease,
+            schema::Realm {
+                shortname: "primary".into(),
+                ..Default::default()
+            },
+        )?;
 
         // add a HMAC key for refresh tokens
-        key::generate_in(&primary, KeyType::HMac(key::HMacType::Sha256))?;
+        key::generate_in(lease, &primary, KeyType::HMac(key::HMacType::Sha256))?;
 
         Ok(())
     }
@@ -165,10 +206,11 @@ struct KeyArgs {
 
 impl KeyArgs {
     async fn run(self, args: RunArgs) -> Result<(), UIDCError> {
+        let mut lease = args.pool.acquire()?;
         match &self.command {
-            KeyCommand::List => key::list(&args.realm),
+            KeyCommand::List => key::list(&mut lease, &args.realm),
             KeyCommand::Generate { key_type } => {
-                key::generate_in(&args.realm, *key_type)?;
+                key::generate_in(&mut lease, &args.realm, *key_type)?;
                 Ok(())
             }
             KeyCommand::Types => {
@@ -177,7 +219,7 @@ impl KeyArgs {
                 }
                 Ok(())
             }
-            KeyCommand::Remove { key_id } => key::remove(&args.realm, key_id),
+            KeyCommand::Remove { key_id } => key::remove(&mut lease, &args.realm, key_id),
         }
     }
 }
@@ -231,6 +273,7 @@ struct ServerArgs {
 impl ServerArgs {
     async fn run(self, args: RunArgs) -> Result<(), UIDCError> {
         server::run_server(
+            args.pool,
             args.db,
             args.config,
             self.bind.as_deref().unwrap_or("127.0.0.1"),
@@ -265,20 +308,21 @@ enum TokenCommand {
 
 impl TokenCommand {
     async fn run(self, args: RunArgs) -> Result<(), UIDCError> {
-        let get_stored = |client_name: &str, user_name: &str| {
+        let mut lease = args.pool.acquire()?;
+        let mut get_stored = |client_name: &str, user_name: &str| {
             let stored_client = args
                 .realm
                 .clients
                 .with(schema::Client::Shortname, client_name)
                 .first()
-                .get()?
+                .get(&mut lease)?
                 .ok_or(UIDCError::Abort("no such client"))?;
             let stored_user = args
                 .realm
                 .users
                 .with(schema::User::Username, user_name)
                 .first()
-                .get()?
+                .get(&mut lease)?
                 .ok_or(UIDCError::Abort("no such user"))?;
             Result::<_, UIDCError>::Ok((stored_client, stored_user))
         };
@@ -291,8 +335,12 @@ impl TokenCommand {
             } => {
                 let (stored_client, stored_user) = get_stored(client.as_str(), username.as_str())?;
                 let realm = RealmHelper::new(args.config, args.realm);
-                let token =
-                    realm.generate_access_token(&stored_client, &stored_user, scopes.split(' '))?;
+                let token = realm.generate_access_token(
+                    &mut lease,
+                    &stored_client,
+                    &stored_user,
+                    scopes.split(' '),
+                )?;
                 println!("{}", token);
                 Ok(())
             }
@@ -304,6 +352,7 @@ impl TokenCommand {
                 let (stored_client, stored_user) = get_stored(client.as_str(), username.as_str())?;
                 let realm = RealmHelper::new(args.config, args.realm);
                 let token = realm.generate_refresh_token(
+                    &mut lease,
                     &stored_client,
                     &stored_user,
                     scopes.split(' '),

+ 4 - 3
src/cli/client.rs

@@ -22,18 +22,19 @@ impl microrm::cli::EntityInterface for ClientInterface {
     fn run_custom(
         ctx: &Self::Context,
         cmd: Self::CustomCommand,
+        lease: &mut microrm::ConnectionLease,
         _query_ctx: impl Queryable<EntityOutput = Self::Entity> + Insertable<Self::Entity>,
     ) -> Result<(), Self::Error> {
         match cmd {
             ClientCommands::Create { name, key_type } => {
                 let kt = key::KeyType::from_str(key_type.as_str())?;
-                client_management::create(ctx, &name, kt)?;
+                client_management::create(lease, ctx, &name, kt)?;
             }
             ClientCommands::RotateSecret { name } => {
-                client_management::rotate_secret(ctx, &name)?;
+                client_management::rotate_secret(lease, ctx, &name)?;
             }
             ClientCommands::AddRedirect { name, pattern } => {
-                client_management::add_redirect(ctx, name.as_str(), pattern.as_str())?;
+                client_management::add_redirect(lease, ctx, name.as_str(), pattern.as_str())?;
             }
         }
 

+ 10 - 6
src/cli/group.rs

@@ -18,16 +18,20 @@ impl microrm::cli::EntityInterface for GroupInterface {
     fn run_custom(
         ctx: &Self::Context,
         cmd: Self::CustomCommand,
+        lease: &mut microrm::ConnectionLease,
         query_ctx: impl Queryable<EntityOutput = Self::Entity> + Insertable<Self::Entity>,
     ) -> Result<(), Self::Error> {
         match cmd {
             GroupCommands::Create { name } => {
-                query_ctx.insert(schema::Group {
-                    realm: ctx.id(),
-                    shortname: name,
-                    users: Default::default(),
-                    roles: Default::default(),
-                })?;
+                query_ctx.insert(
+                    lease,
+                    schema::Group {
+                        realm: ctx.id(),
+                        shortname: name,
+                        users: Default::default(),
+                        roles: Default::default(),
+                    },
+                )?;
             }
         }
 

+ 9 - 5
src/cli/role.rs

@@ -18,15 +18,19 @@ impl microrm::cli::EntityInterface for RoleInterface {
     fn run_custom(
         ctx: &Self::Context,
         cmd: Self::CustomCommand,
+        lease: &mut microrm::ConnectionLease,
         query_ctx: impl Queryable<EntityOutput = Self::Entity> + Insertable<Self::Entity>,
     ) -> Result<(), Self::Error> {
         match cmd {
             RoleCommands::Create { name } => {
-                query_ctx.insert(schema::Role {
-                    realm: ctx.id(),
-                    shortname: name,
-                    groups: Default::default(),
-                })?;
+                query_ctx.insert(
+                    lease,
+                    schema::Role {
+                        realm: ctx.id(),
+                        shortname: name,
+                        groups: Default::default(),
+                    },
+                )?;
             }
         }
 

+ 9 - 5
src/cli/scope.rs

@@ -18,15 +18,19 @@ impl microrm::cli::EntityInterface for ScopeInterface {
     fn run_custom(
         ctx: &Self::Context,
         cmd: Self::CustomCommand,
+        lease: &mut microrm::ConnectionLease,
         query_ctx: impl Queryable<EntityOutput = Self::Entity> + Insertable<Self::Entity>,
     ) -> Result<(), Self::Error> {
         match cmd {
             ScopeCommands::Create { name } => {
-                query_ctx.insert(schema::Scope {
-                    realm: ctx.id(),
-                    shortname: name,
-                    roles: Default::default(),
-                })?;
+                query_ctx.insert(
+                    lease,
+                    schema::Scope {
+                        realm: ctx.id(),
+                        shortname: name,
+                        roles: Default::default(),
+                    },
+                )?;
             }
         }
 

+ 30 - 23
src/cli/user.rs

@@ -33,23 +33,27 @@ pub enum UserCommands {
 impl microrm::cli::EntityInterface for UserInterface {
     type Error = UIDCError;
     type Entity = schema::User;
-    type Context = super::RunArgs; // microrm::schema::Stored<schema::Realm>;
+    type Context = (microrm::Stored<schema::Realm>, crate::config::Config);
     type CustomCommand = UserCommands;
 
     fn run_custom(
         ctx: &Self::Context,
         cmd: Self::CustomCommand,
+        lease: &mut microrm::ConnectionLease,
         query_ctx: impl Queryable<EntityOutput = Self::Entity> + Insertable<Self::Entity>,
     ) -> Result<(), Self::Error> {
         match cmd {
             UserCommands::Create { username } => {
-                query_ctx.insert(schema::User {
-                    realm: ctx.realm.id(),
-                    username,
-                    pending_external_auths: Default::default(),
-                    auth: Default::default(),
-                    groups: Default::default(),
-                })?;
+                query_ctx.insert(
+                    lease,
+                    schema::User {
+                        realm: ctx.0.id(),
+                        username,
+                        pending_external_auths: Default::default(),
+                        auth: Default::default(),
+                        groups: Default::default(),
+                    },
+                )?;
             }
             UserCommands::UpdateAuth {
                 username,
@@ -59,7 +63,7 @@ impl microrm::cli::EntityInterface for UserInterface {
                 let user = query_ctx
                     .with(schema::User::Username, &username)
                     .first()
-                    .get()?
+                    .get(lease)?
                     .ok_or(Self::Error::no_such_entity("user", username))?;
 
                 if password == 0 && totp == 0 {
@@ -68,7 +72,7 @@ impl microrm::cli::EntityInterface for UserInterface {
                 }
                 if password > 0 {
                     let raw_pass = rpassword::prompt_password("Enter new user password: ").unwrap();
-                    user.set_new_password(raw_pass.as_bytes())?;
+                    user.set_new_password(lease, raw_pass.as_bytes())?;
                 }
                 if totp > 0 {
                     let (new_secret, new_uri) = user.generate_totp_with_uri()?;
@@ -89,20 +93,20 @@ impl microrm::cli::EntityInterface for UserInterface {
                             break;
                         }
                     }
-                    user.set_new_totp(new_secret.as_slice())?;
+                    user.set_new_totp(lease, new_secret.as_slice())?;
                 }
             }
             UserCommands::RegisterExternalAuth { username, provider } => {
                 let mut user = query_ctx
                     .with(schema::User::Username, &username)
                     .first()
-                    .get()?
+                    .get(lease)?
                     .ok_or(Self::Error::no_such_entity("user", username))?;
 
                 user.pending_external_auths.as_mut().push(provider);
                 user.pending_external_auths.as_mut().sort();
                 user.pending_external_auths.as_mut().dedup();
-                user.sync().expect("couldn't sync user model");
+                user.sync(lease).expect("couldn't sync user model");
             }
             UserCommands::OnetimeLink {
                 username,
@@ -112,7 +116,7 @@ impl microrm::cli::EntityInterface for UserInterface {
                 let user = query_ctx
                     .with(schema::User::Username, &username)
                     .first()
-                    .get_ids()?
+                    .get_ids(lease)?
                     .ok_or(Self::Error::no_such_entity("user", username))?;
 
                 let rng = ring::rand::SystemRandom::new();
@@ -126,20 +130,23 @@ impl microrm::cli::EntityInterface for UserInterface {
                     .checked_add(time::Duration::days(7))
                     .unwrap();
 
-                ctx.realm
+                ctx.0
                     .single_use_auth
-                    .insert(schema::SingleUseAuth {
-                        code: code.clone(),
-                        user,
-                        expiry,
-                    })
+                    .insert(
+                        lease,
+                        schema::SingleUseAuth {
+                            code: code.clone(),
+                            user,
+                            expiry,
+                        },
+                    )
                     .expect("couldn't insert single-use authentication code?");
 
                 let target = tide::http::Url::parse_with_params(
                     format!(
                         "{base}/{realm}/v1/session/onetime",
-                        base = ctx.config.base_url,
-                        realm = ctx.realm.shortname
+                        base = ctx.1.base_url,
+                        realm = ctx.0.shortname
                     )
                     .as_str(),
                     &[
@@ -171,7 +178,7 @@ impl microrm::cli::EntityInterface for UserInterface {
         _role: microrm::cli::ValueRole,
     ) -> String {
         if field == "realm" {
-            format!("{}", ctx.realm.id().into_raw())
+            format!("{}", ctx.0.id().into_raw())
         } else {
             unreachable!()
         }

+ 6 - 2
src/client.rs

@@ -4,8 +4,12 @@ use microrm::prelude::*;
 pub trait ClientExt {
     fn client_instance(&self) -> &schema::Client;
 
-    fn check_redirect(&self, uri: &str) -> Result<bool, UIDCError> {
-        for redirect in self.client_instance().redirects.get()?.into_iter() {
+    fn check_redirect(
+        &self,
+        lease: &mut microrm::ConnectionLease,
+        uri: &str,
+    ) -> Result<bool, UIDCError> {
+        for redirect in self.client_instance().redirects.get(lease)?.into_iter() {
             let pattern = glob::Pattern::new(redirect.redirect_pattern.as_str())
                 .map_err(|_| UIDCError::Abort("couldn't parse redirect URI as glob pattern"))?;
             if pattern.matches(uri) {

+ 29 - 17
src/client_management.rs

@@ -5,6 +5,7 @@ use crate::{
 use microrm::prelude::*;
 
 pub fn create(
+    lease: &mut microrm::ConnectionLease,
     realm: &microrm::Stored<schema::Realm>,
     name: &String,
     key_type: KeyType,
@@ -12,20 +13,27 @@ pub fn create(
     let rng = ring::rand::SystemRandom::new();
     let client_secret: [u8; 32] = ring::rand::generate(&rng).unwrap().expose();
 
-    realm.clients.insert(schema::Client {
-        realm: realm.id(),
-        shortname: name.into(),
-        secret: base64::encode(client_secret),
-        access_key_type: key_type.into(),
-        refresh_key_type: KeyType::HMac(HMacType::Sha256).into_serialized(),
-        direct_grant_enabled: false,
-        redirects: Default::default(),
-        scopes: Default::default(),
-    })?;
+    realm.clients.insert(
+        lease,
+        schema::Client {
+            realm: realm.id(),
+            shortname: name.into(),
+            secret: base64::encode(client_secret),
+            access_key_type: key_type.into(),
+            refresh_key_type: KeyType::HMac(HMacType::Sha256).into_serialized(),
+            direct_grant_enabled: false,
+            redirects: Default::default(),
+            scopes: Default::default(),
+        },
+    )?;
     Ok(())
 }
 
-pub fn rotate_secret(realm: &microrm::Stored<schema::Realm>, name: &str) -> Result<(), UIDCError> {
+pub fn rotate_secret(
+    lease: &mut microrm::ConnectionLease,
+    realm: &microrm::Stored<schema::Realm>,
+    name: &str,
+) -> Result<(), UIDCError> {
     let rng = ring::rand::SystemRandom::new();
     let client_secret: [u8; 32] = ring::rand::generate(&rng).unwrap().expose();
 
@@ -33,27 +41,31 @@ pub fn rotate_secret(realm: &microrm::Stored<schema::Realm>, name: &str) -> Resu
         .clients
         .with(schema::Client::Shortname, name)
         .first()
-        .get()?
+        .get(lease)?
         .ok_or(UIDCError::Abort("no such client"))?;
     client.secret = base64::encode(client_secret);
 
-    client.sync()?;
+    client.sync(lease)?;
 
     Ok(())
 }
 
 pub fn add_redirect(
+    lease: &mut microrm::ConnectionLease,
     realm: &microrm::Stored<schema::Realm>,
     name: &str,
     pattern: &str,
 ) -> Result<(), UIDCError> {
-    let Some(client) = realm.clients.keyed((realm.id(), name)).get()? else {
+    let Some(client) = realm.clients.keyed((realm.id(), name)).get(lease)? else {
         return Err(UIDCError::Abort("no such client"));
     };
 
-    client.redirects.insert(schema::ClientRedirect {
-        redirect_pattern: pattern.into(),
-    })?;
+    client.redirects.insert(
+        lease,
+        schema::ClientRedirect {
+            redirect_pattern: pattern.into(),
+        },
+    )?;
 
     Ok(())
 }

+ 15 - 11
src/ext.rs

@@ -19,7 +19,7 @@ pub trait ExternalAuthenticator {
     fn generate_registration_url(&self, realm: &str, redirect: &str) -> String;
     fn extract_login_state(&self, req: UIDCRequest) -> impl Future<Output = tide::Response>;
 
-    fn handle_registration(&self, req: UIDCRequest) -> tide::Response {
+    fn handle_registration(&self, _req: UIDCRequest) -> tide::Response {
         todo!()
     }
 
@@ -30,28 +30,32 @@ pub trait ExternalAuthenticator {
         redirect: &str,
     ) -> tide::Response {
         let sh = SessionHelper::new(&req);
+        let mut lease = req.state().pool.acquire().expect("couldn't get lease");
 
-        let Ok((resp, cookie)) = sh.get_or_build_session(&req) else {
+        let Ok((resp, cookie)) = sh.get_or_build_session(&mut lease, &req) else {
             return tide::Response::builder(500)
                 .body("error while building session")
                 .build();
         };
 
-        let realm_id = sh.get_realm().unwrap().id();
+        let realm_id = sh.get_realm(&mut lease).unwrap().id();
 
         // remove any existing authentication for this realm just in case
         resp.auth
             .with(schema::SessionAuth::Realm, realm_id)
             .first()
-            .delete()
+            .delete(&mut lease)
             .expect("couldn't remove existing authentication");
         resp.auth
-            .insert(schema::SessionAuth {
-                realm: realm_id,
-                user: Some(user),
-                pending_user: None,
-                pending_challenges: vec![].into_serialized(),
-            })
+            .insert(
+                &mut lease,
+                schema::SessionAuth {
+                    realm: realm_id,
+                    user: Some(user),
+                    pending_user: None,
+                    pending_challenges: vec![].into_serialized(),
+                },
+            )
             .expect("couldn't insert new authentication");
 
         let mut resp: tide::Response = tide::Redirect::see_other(redirect).into();
@@ -70,7 +74,7 @@ pub trait ExternalAuthenticator {
             tide::Response::new(200),
             redirect,
             None,
-            Some("Github user not associated with any local user.".to_string()),
+            Some("External user not associated with any local user.".to_string()),
         );
     }
 }

+ 5 - 2
src/ext/github.rs

@@ -175,7 +175,9 @@ impl super::ExternalAuthenticator for GithubAuthenticator {
 
             let user_id = resp.id.to_string();
 
-            let Some(realm) = state.db.realms.keyed(realm).get().ok().flatten() else {
+            let mut lease = state.pool.acquire().expect("could not acquire lease");
+
+            let Some(realm) = state.db.realms.keyed(realm).get(&mut lease).ok().flatten() else {
                 return tide::Response::builder(404).body("no such realm").build();
             };
 
@@ -185,10 +187,11 @@ impl super::ExternalAuthenticator for GithubAuthenticator {
                     &user_id,
                     schema::ExternalAuthProvider::Github.into_serialized(),
                 ))
-                .get()
+                .get(&mut lease)
                 .ok()
                 .flatten();
 
+            drop(lease);
             match (query.mode, external_auth_map) {
                 (CallbackRequestType::Login, Some(map)) => {
                     self.handle_matching_login(req, map.internal_user_id, query.redirect.as_str())

+ 55 - 33
src/key.rs

@@ -122,7 +122,12 @@ fn pubkey_id(data: &[u8]) -> String {
     key_id
 }
 
-fn generate_rsa(realm: &schema::Realm, kty: KeyType, bits: usize) -> Result<String, UIDCError> {
+fn generate_rsa(
+    lease: &mut microrm::ConnectionLease,
+    realm: &schema::Realm,
+    kty: KeyType,
+    bits: usize,
+) -> Result<String, UIDCError> {
     // ring does not generate RSA keypairs, so we need to shell out to openssl for this.
     let openssl_output = std::process::Command::new("sh")
         .arg("-c")
@@ -150,19 +155,26 @@ fn generate_rsa(realm: &schema::Realm, kty: KeyType, bits: usize) -> Result<Stri
     })
     .unwrap();
 
-    realm.keys.insert(schema::Key {
-        key_id: key_id.clone(),
-        key_type: kty.into(),
-        key_state: schema::KeyState::Active.into(),
-        public_data,
-        secret_data: secret,
-        expiry,
-    })?;
+    realm.keys.insert(
+        lease,
+        schema::Key {
+            key_id: key_id.clone(),
+            key_type: kty.into(),
+            key_state: schema::KeyState::Active.into(),
+            public_data,
+            secret_data: secret,
+            expiry,
+        },
+    )?;
 
     Ok(key_id)
 }
 
-pub fn generate_in(realm: &schema::Realm, kty: KeyType) -> Result<String, UIDCError> {
+pub fn generate_in(
+    lease: &mut microrm::ConnectionLease,
+    realm: &schema::Realm,
+    kty: KeyType,
+) -> Result<String, UIDCError> {
     let rng = ring::rand::SystemRandom::new();
     match kty {
         KeyType::HMac(hmty) => {
@@ -177,19 +189,22 @@ pub fn generate_in(realm: &schema::Realm, kty: KeyType) -> Result<String, UIDCEr
                 .map_err(|_| UIDCError::Abort("couldn't generate random values"))?;
 
             let expiry = time::OffsetDateTime::now_utc() + time::Duration::days(730);
-            realm.keys.insert(schema::Key {
-                key_id: key_id.clone(),
-                key_type: kty.into(),
-                key_state: schema::KeyState::Active.into(),
-                // no separate public data for HMAC keys
-                public_data: vec![],
-                secret_data: keydata.clone(),
-                expiry,
-            })?;
+            realm.keys.insert(
+                lease,
+                schema::Key {
+                    key_id: key_id.clone(),
+                    key_type: kty.into(),
+                    key_state: schema::KeyState::Active.into(),
+                    // no separate public data for HMAC keys
+                    public_data: vec![],
+                    secret_data: keydata.clone(),
+                    expiry,
+                },
+            )?;
             Ok(key_id)
         }
-        KeyType::RSA2048 => generate_rsa(realm, KeyType::RSA2048, 2048),
-        KeyType::RSA4096 => generate_rsa(realm, KeyType::RSA4096, 4096),
+        KeyType::RSA2048 => generate_rsa(lease, realm, KeyType::RSA2048, 2048),
+        KeyType::RSA4096 => generate_rsa(lease, realm, KeyType::RSA4096, 4096),
         KeyType::Ed25519 => {
             let generated_keypair = Ed25519KeyPair::generate_pkcs8(&rng)
                 .map_err(|_| KeyError::Plain("failed to generate key"))?;
@@ -202,22 +217,25 @@ pub fn generate_in(realm: &schema::Realm, kty: KeyType) -> Result<String, UIDCEr
             let key_id = pubkey_id(pubkey.as_ref());
             let expiry = time::OffsetDateTime::now_utc() + time::Duration::days(730);
 
-            realm.keys.insert(schema::Key {
-                key_id: key_id.clone(),
-                key_type: kty.into(),
-                key_state: schema::KeyState::Active.into(),
-                public_data: pubkey.as_ref().into(),
-                secret_data: keydata,
-                expiry,
-            })?;
+            realm.keys.insert(
+                lease,
+                schema::Key {
+                    key_id: key_id.clone(),
+                    key_type: kty.into(),
+                    key_state: schema::KeyState::Active.into(),
+                    public_data: pubkey.as_ref().into(),
+                    secret_data: keydata,
+                    expiry,
+                },
+            )?;
 
             Ok(key_id)
         }
     }
 }
 
-pub fn list(realm: &schema::Realm) -> Result<(), UIDCError> {
-    let keys = realm.keys.get()?;
+pub fn list(lease: &mut microrm::ConnectionLease, realm: &schema::Realm) -> Result<(), UIDCError> {
+    let keys = realm.keys.get(lease)?;
 
     for key in keys {
         println!(
@@ -232,7 +250,11 @@ pub fn list(realm: &schema::Realm) -> Result<(), UIDCError> {
     Ok(())
 }
 
-pub fn remove(realm: &schema::Realm, key_id: &String) -> Result<(), UIDCError> {
-    realm.keys.with(schema::Key::KeyId, key_id).delete()?;
+pub fn remove(
+    lease: &mut microrm::ConnectionLease,
+    realm: &schema::Realm,
+    key_id: &String,
+) -> Result<(), UIDCError> {
+    realm.keys.with(schema::Key::KeyId, key_id).delete(lease)?;
     Ok(())
 }

+ 27 - 15
src/realm.rs

@@ -49,18 +49,22 @@ impl RealmCache {
         }
     }
 
-    pub fn get_helper(&self, id: schema::RealmID) -> Option<Arc<RealmHelper>> {
+    pub fn get_helper(
+        &self,
+        lease: &mut microrm::ConnectionLease,
+        id: schema::RealmID,
+    ) -> Option<Arc<RealmHelper>> {
         if let Some(rh) = self.realms.read().unwrap().get(&id) {
             return Some(rh.clone());
         }
 
-        let realm = self.db.realms.by_id(id).ok().flatten()?;
+        let realm = self.db.realms.by_id(lease, id).ok().flatten()?;
 
         self.realms
             .write()
             .unwrap()
             .insert(id, RealmHelper::new(self.config.clone(), realm).into());
-        self.get_helper(id)
+        self.get_helper(lease, id)
     }
 }
 
@@ -88,6 +92,7 @@ impl RealmHelper {
 
     fn determine_roles<'a>(
         &self,
+        lease: &mut microrm::ConnectionLease,
         client: &microrm::Stored<schema::Client>,
         user: &microrm::Stored<schema::User>,
         scopes: impl Iterator<Item = &'a str>,
@@ -98,17 +103,21 @@ impl RealmHelper {
             if scope_name.is_empty() {
                 continue;
             }
-            let Some(scope) = client.scopes.keyed((self.realm.id(), scope_name)).get()? else {
+            let Some(scope) = client
+                .scopes
+                .keyed((self.realm.id(), scope_name))
+                .get(lease)?
+            else {
                 // requested nonexistent scope
                 return Err(UIDCError::Abort("requested nonexistent scope!"));
             };
-            requested_roles.extend(scope.roles.get_ids()?.into_iter());
+            requested_roles.extend(scope.roles.get_ids(lease)?.into_iter());
         }
 
         let mut available_roles = vec![]; // HashSet::<schema::RoleID>::new();
 
-        for group in user.groups.get()? {
-            available_roles.extend(group.roles.get_ids()?.into_iter());
+        for group in user.groups.get(lease)? {
+            available_roles.extend(group.roles.get_ids(lease)?.into_iter());
         }
 
         requested_roles.sort();
@@ -119,7 +128,7 @@ impl RealmHelper {
         let roles = requested_roles
             .into_iter()
             .filter(|x| available_roles.contains(x))
-            .flat_map(|v| self.realm.roles.with_id(v).first().get())
+            .flat_map(|v| self.realm.roles.with_id(v).first().get(lease))
             .flatten()
             .map(|v| v.wrapped().shortname)
             .collect();
@@ -183,6 +192,7 @@ impl RealmHelper {
 
     pub fn generate_access_token<'a>(
         &self,
+        lease: &mut microrm::ConnectionLease,
         client: &microrm::Stored<schema::Client>,
         user: &microrm::Stored<schema::User>,
         scopes: impl Iterator<Item = &'a str> + Clone,
@@ -190,7 +200,7 @@ impl RealmHelper {
         let iat = std::time::SystemTime::now();
         let exp = iat + std::time::Duration::from_secs(self.config.auth_token_expiry);
 
-        let resulting_roles = self.determine_roles(client, user, scopes.clone())?;
+        let resulting_roles = self.determine_roles(lease, client, user, scopes.clone())?;
 
         let atclaims = AccessTokenClaims {
             sub: user.username.as_str(),
@@ -212,7 +222,7 @@ impl RealmHelper {
             )
             .with(schema::Key::KeyType, &client.access_key_type)
             .first()
-            .get()?
+            .get(lease)?
         else {
             return Err(UIDCError::Abort("no matching signing key"));
         };
@@ -237,6 +247,7 @@ impl RealmHelper {
 
     pub fn generate_refresh_token<'a>(
         &self,
+        lease: &mut microrm::ConnectionLease,
         client: &microrm::Stored<schema::Client>,
         user: &microrm::Stored<schema::User>,
         scopes: impl Iterator<Item = &'a str> + Clone,
@@ -264,7 +275,7 @@ impl RealmHelper {
             )
             .with(schema::Key::KeyType, &client.refresh_key_type)
             .first()
-            .get()?
+            .get(lease)?
         else {
             return Err(UIDCError::Abort("no matching signing key"));
         };
@@ -286,6 +297,7 @@ impl RealmHelper {
 
     pub fn trade_refresh_token(
         &self,
+        lease: &mut microrm::ConnectionLease,
         client: &microrm::Stored<schema::Client>,
         rtoken: &str,
     ) -> Result<(String, String), UIDCError> {
@@ -300,7 +312,7 @@ impl RealmHelper {
             .keys
             .with(schema::Key::KeyId, kid)
             .first()
-            .get()?
+            .get(lease)?
         else {
             return Err(UIDCError::Abort("no matching key"));
         };
@@ -326,14 +338,14 @@ impl RealmHelper {
             .realm
             .users
             .keyed((self.realm.id(), rt.claims.sub.as_str()))
-            .get()?
+            .get(lease)?
         else {
             return Err(UIDCError::Abort("user no longer exists or was renamed"));
         };
 
         let scopes = rt.claims.scopes.iter().map(String::as_str);
-        let access_token = self.generate_access_token(client, &user, scopes.clone())?;
-        let refresh_token = self.generate_refresh_token(client, &user, scopes)?;
+        let access_token = self.generate_access_token(lease, client, &user, scopes.clone())?;
+        let refresh_token = self.generate_refresh_token(lease, client, &user, scopes)?;
 
         Ok((access_token, refresh_token))
     }

+ 2 - 2
src/schema.rs

@@ -1,4 +1,4 @@
-pub use microrm::prelude::{Database, Entity};
+pub use microrm::prelude::*;
 use serde::{Deserialize, Serialize};
 use strum::EnumString;
 
@@ -223,7 +223,7 @@ pub struct Realm {
     pub single_use_auth: microrm::RelationMap<SingleUseAuth>,
 }
 
-#[derive(Clone, Database)]
+#[derive(Schema)]
 pub struct UIDCDatabase {
     pub realms: microrm::IDMap<Realm>,
 

+ 10 - 3
src/server.rs

@@ -1,3 +1,5 @@
+use microrm::schema::Schema;
+
 use crate::{
     config,
     ext::{ExternalAuthenticator, GithubAuthenticator},
@@ -12,6 +14,7 @@ pub use session::SessionHelper;
 
 pub struct ServerState {
     pub config: config::Config,
+    pub pool: microrm::ConnectionPool,
     pub db: schema::UIDCDatabase,
     pub templates: handlebars::Handlebars<'static>,
     pub realms: realm::RealmCache,
@@ -36,11 +39,13 @@ pub type UIDCRequest = tide::Request<ServerStateWrapper>;
 async fn index(req: UIDCRequest) -> tide::Result<tide::Response> {
     let shelper = session::SessionHelper::new(&req);
 
-    let realm = shelper.get_realm()?;
-    let session = shelper.get_session(&req);
+    let mut lease = req.state().pool.acquire()?;
+
+    let realm = shelper.get_realm(&mut lease)?;
+    let session = shelper.get_session(&mut lease, &req);
     let auth = session
         .as_ref()
-        .and_then(|session| shelper.get_auth_for_session(realm.id(), session));
+        .and_then(|session| shelper.get_auth_for_session(&mut lease, realm.id(), session));
 
     let response = tide::Response::builder(200)
         .content_type(tide::http::mime::HTML)
@@ -67,6 +72,7 @@ async fn index(req: UIDCRequest) -> tide::Result<tide::Response> {
 }
 
 pub async fn run_server(
+    pool: microrm::ConnectionPool,
     db: schema::UIDCDatabase,
     config: config::Config,
     bind: &str,
@@ -77,6 +83,7 @@ pub async fn run_server(
         realms: realm::RealmCache::new(config.clone(), db.clone()),
         config,
         db,
+        pool,
         templates: handlebars::Handlebars::new(),
         client: surf::client(),
     }));

+ 5 - 3
src/server/oidc.rs

@@ -106,12 +106,13 @@ async fn token(request: Request) -> tide::Result<tide::Response> {
 
 async fn jwks(request: Request) -> tide::Result<tide::Response> {
     let shelper = SessionHelper::new(&request);
-    let realm = shelper.get_realm()?;
+    let mut lease = request.state().pool.acquire()?;
+    let realm = shelper.get_realm(&mut lease)?;
 
     // build JWK set
     let mut jwkset = jsonwebtoken::jwk::JwkSet { keys: vec![] };
 
-    for key in realm.keys.get()?.into_iter() {
+    for key in realm.keys.get(&mut lease)?.into_iter() {
         if *key.key_state.as_ref() == schema::KeyState::Retired {
             continue;
         }
@@ -135,6 +136,7 @@ async fn discovery_config(request: Request) -> tide::Result<tide::Response> {
     let server_config = &request.state().core.config;
     let realm_name = request.param("realm").unwrap();
     let base_url = format!("{}/{}", server_config.base_url, realm_name);
+    let mut lease = request.state().pool.acquire()?;
 
     let Some(_realm) = &request
         .state()
@@ -143,7 +145,7 @@ async fn discovery_config(request: Request) -> tide::Result<tide::Response> {
         .realms
         .keyed(realm_name)
         .first()
-        .get()?
+        .get(&mut lease)?
     else {
         return Ok(tide::Response::builder(404)
             .header(tide::http::headers::ACCESS_CONTROL_ALLOW_ORIGIN, "*")

+ 31 - 20
src/server/oidc/authorize.rs

@@ -3,6 +3,7 @@ use crate::{client::ClientExt, config, schema, server::session::SessionHelper};
 use microrm::prelude::*;
 
 fn do_code_authorize<'l, 's>(
+    lease: &mut microrm::ConnectionLease,
     config: &config::Config,
     realm: &microrm::Stored<schema::Realm>,
     client: &microrm::Stored<schema::Client>,
@@ -26,18 +27,21 @@ fn do_code_authorize<'l, 's>(
         .expose();
     let encoded_auth_code = base64::encode_config(raw_auth_code, base64::URL_SAFE_NO_PAD);
 
-    realm.auth_codes.insert(schema::AuthCode {
-        realm: realm.id(),
-        client: client.id(),
-        user: user.id(),
-        scopes: scopes
-            .map(|x| x.to_owned())
-            .collect::<Vec<_>>()
-            .into_serialized(),
-        expiry: expiry.into(),
-        redirect_uri: redirect_uri.clone(),
-        code: encoded_auth_code.clone(),
-    })?;
+    realm.auth_codes.insert(
+        lease,
+        schema::AuthCode {
+            realm: realm.id(),
+            client: client.id(),
+            user: user.id(),
+            scopes: scopes
+                .map(|x| x.to_owned())
+                .collect::<Vec<_>>()
+                .into_serialized(),
+            expiry: expiry.into(),
+            redirect_uri: redirect_uri.clone(),
+            code: encoded_auth_code.clone(),
+        },
+    )?;
 
     let new_params = [
         ("response_type", "code"),
@@ -63,7 +67,8 @@ pub(super) fn do_authorize(
     state: Option<&str>,
 ) -> Result<tide::Response, OIDCError> {
     let shelper = SessionHelper::new(&request);
-    let realm = shelper.get_realm().map_err(|_| {
+    let mut lease = request.state().pool.acquire()?;
+    let realm = shelper.get_realm(&mut lease).map_err(|_| {
         OIDCError(
             OIDCErrorType::InvalidRequest,
             "No such realm!".into(),
@@ -87,15 +92,15 @@ pub(super) fn do_authorize(
     // collect session authentication info
 
     let potential_sauth = shelper
-        .get_session(&request)
-        .and_then(|session| shelper.get_auth_for_session(realm.id(), &session));
+        .get_session(&mut lease, &request)
+        .and_then(|session| shelper.get_auth_for_session(&mut lease, realm.id(), &session));
 
     let Some(user_id) = potential_sauth.and_then(|v| v.user) else {
         // if we don't have any relevant auth info, redirect to login
         return make_redirect();
     };
 
-    let Ok(Some(user)) = realm.users.with_id(user_id).first().get() else {
+    let Ok(Some(user)) = realm.users.with_id(user_id).first().get(&mut lease) else {
         return Err(OIDCError(
             OIDCErrorType::ServerError,
             "Internal state error!".into(),
@@ -108,7 +113,7 @@ pub(super) fn do_authorize(
         .clients
         .with(schema::Client::Shortname, &qp.client_id)
         .first()
-        .get()
+        .get(&mut lease)
         .ok()
         .flatten()
         .ok_or_else(|| {
@@ -122,7 +127,7 @@ pub(super) fn do_authorize(
     let scopes = qp.scope.as_deref().unwrap_or("").split_whitespace();
 
     // check that redirect URI matches
-    match client.check_redirect(qp.redirect_uri.as_str()) {
+    match client.check_redirect(&mut lease, qp.redirect_uri.as_str()) {
         Ok(true) => (),
         Ok(false) => {
             return Err(OIDCError(
@@ -142,6 +147,7 @@ pub(super) fn do_authorize(
 
     if qp.response_type == "code" {
         do_code_authorize(
+            &mut lease,
             &request.state().core.config,
             &realm,
             &client,
@@ -151,10 +157,15 @@ pub(super) fn do_authorize(
             state,
         )
     } else if qp.response_type == "token" {
-        let rhelper = request.state().core.realms.get_helper(realm.id()).unwrap();
+        let rhelper = request
+            .state()
+            .core
+            .realms
+            .get_helper(&mut lease, realm.id())
+            .unwrap();
 
         let token = rhelper
-            .generate_access_token(&client, &user, scopes)
+            .generate_access_token(&mut lease, &client, &user, scopes)
             .map_err(|e| {
                 OIDCError(
                     OIDCErrorType::ServerError,

+ 34 - 18
src/server/oidc/token.rs

@@ -9,6 +9,7 @@ use crate::{
 use microrm::prelude::*;
 
 fn do_authorization_code<'l>(
+    lease: &mut microrm::ConnectionLease,
     realm: &microrm::Stored<schema::Realm>,
     rhelper: &RealmHelper,
     client: &microrm::Stored<schema::Client>,
@@ -24,7 +25,7 @@ fn do_authorization_code<'l>(
     let code = realm
         .auth_codes
         .keyed((realm.id(), client.id(), code))
-        .get()?
+        .get(lease)?
         .ok_or(OIDCError(
             OIDCErrorType::AccessDenied,
             "invalid authorization code".into(),
@@ -44,7 +45,7 @@ fn do_authorization_code<'l>(
         .users
         .with_id(code.user)
         .first()
-        .get()?
+        .get(lease)?
         .ok_or(OIDCError(
             OIDCErrorType::ServerError,
             "could not find user".into(),
@@ -53,6 +54,7 @@ fn do_authorization_code<'l>(
 
     let access_token = rhelper
         .generate_access_token(
+            lease,
             client,
             &user,
             code.scopes.as_ref().iter().map(String::as_str),
@@ -80,6 +82,7 @@ fn do_authorization_code<'l>(
 }
 
 fn do_refresh_token<'l>(
+    lease: &mut microrm::ConnectionLease,
     _realm: &microrm::Stored<schema::Realm>,
     rhelper: &RealmHelper,
     client: &microrm::Stored<schema::Client>,
@@ -93,7 +96,7 @@ fn do_refresh_token<'l>(
         ));
     };
 
-    let (access, refresh) = match rhelper.trade_refresh_token(client, rtoken.as_str()) {
+    let (access, refresh) = match rhelper.trade_refresh_token(lease, client, rtoken.as_str()) {
         Ok((a, r)) => (a, r),
         Err(e) => {
             return Err(OIDCError(
@@ -118,6 +121,7 @@ fn do_refresh_token<'l>(
 }
 
 fn do_direct_grant<'l>(
+    lease: &mut microrm::ConnectionLease,
     realm: &microrm::Stored<schema::Realm>,
     rhelper: &RealmHelper,
     client: &microrm::Stored<schema::Client>,
@@ -151,7 +155,7 @@ fn do_direct_grant<'l>(
         .users
         .with(schema::User::Username, username)
         .first()
-        .get()?
+        .get(lease)?
     else {
         return Err(OIDCError(
             OIDCErrorType::AccessDenied,
@@ -164,7 +168,7 @@ fn do_direct_grant<'l>(
     match user
         .auth
         .with(schema::AuthChallenge::Enabled, true)
-        .count()?
+        .count(lease)?
     {
         0 => {
             return Err(OIDCError(
@@ -183,7 +187,11 @@ fn do_direct_grant<'l>(
         }
     }
 
-    match user.verify_challenge_by_type(schema::AuthChallengeType::Password, password.as_bytes()) {
+    match user.verify_challenge_by_type(
+        lease,
+        schema::AuthChallengeType::Password,
+        password.as_bytes(),
+    ) {
         Err(UIDCError::UserError(UserError::NoSuchChallenge)) => Err(OIDCError(
             OIDCErrorType::AccessDenied,
             "user has no associated password".into(),
@@ -202,7 +210,8 @@ fn do_direct_grant<'l>(
         )),
         Ok(true) => {
             let scopes = treq.scope.as_deref().unwrap_or("").split_whitespace();
-            let Ok(access_token) = rhelper.generate_access_token(client, &user, scopes.clone())
+            let Ok(access_token) =
+                rhelper.generate_access_token(lease, client, &user, scopes.clone())
             else {
                 return Err(OIDCError(
                     OIDCErrorType::ServerError,
@@ -210,7 +219,8 @@ fn do_direct_grant<'l>(
                     None,
                 ));
             };
-            let Ok(refresh_token) = rhelper.generate_refresh_token(client, &user, scopes) else {
+            let Ok(refresh_token) = rhelper.generate_refresh_token(lease, client, &user, scopes)
+            else {
                 return Err(OIDCError(
                     OIDCErrorType::ServerError,
                     "could not generate access token".into(),
@@ -235,11 +245,6 @@ fn do_direct_grant<'l>(
 }
 
 pub(super) async fn do_token<'l>(mut request: Request) -> Result<tide::Response, OIDCError<'l>> {
-    let shelper = SessionHelper::new(&request);
-    let realm = shelper
-        .get_realm()
-        .map_err(|_| OIDCError(OIDCErrorType::InvalidRequest, "no such realm".into(), None))?;
-
     let treq: api::TokenRequestBody = request.body_form().await.map_err(|e| {
         OIDCError(
             OIDCErrorType::InvalidRequest,
@@ -248,6 +253,12 @@ pub(super) async fn do_token<'l>(mut request: Request) -> Result<tide::Response,
         )
     })?;
 
+    let shelper = SessionHelper::new(&request);
+    let mut lease = request.state().pool.acquire()?;
+    let realm = shelper
+        .get_realm(&mut lease)
+        .map_err(|_| OIDCError(OIDCErrorType::InvalidRequest, "no such realm".into(), None))?;
+
     // TODO: support HTTP basic auth for client authentication instead of treq.client_id
     let client_name = treq.client_id.as_ref().ok_or(OIDCError(
         OIDCErrorType::InvalidRequest,
@@ -257,21 +268,26 @@ pub(super) async fn do_token<'l>(mut request: Request) -> Result<tide::Response,
     let client = realm
         .clients
         .keyed((realm.id(), client_name))
-        .get()?
+        .get(&mut lease)?
         .ok_or(OIDCError(
             OIDCErrorType::InvalidRequest,
             format!("unknown client name {client_name}").into(),
             None,
         ))?;
 
-    let rhelper = request.state().core.realms.get_helper(realm.id()).unwrap();
+    let rhelper = request
+        .state()
+        .core
+        .realms
+        .get_helper(&mut lease, realm.id())
+        .unwrap();
 
     if treq.grant_type == "authorization_code" {
-        do_authorization_code(&realm, rhelper.as_ref(), &client, &treq)
+        do_authorization_code(&mut lease, &realm, rhelper.as_ref(), &client, &treq)
     } else if treq.grant_type == "refresh_token" {
-        do_refresh_token(&realm, rhelper.as_ref(), &client, &treq)
+        do_refresh_token(&mut lease, &realm, rhelper.as_ref(), &client, &treq)
     } else if treq.grant_type == "password" {
-        do_direct_grant(&realm, rhelper.as_ref(), &client, &treq)
+        do_direct_grant(&mut lease, &realm, rhelper.as_ref(), &client, &treq)
     } else {
         Err(OIDCError(
             OIDCErrorType::InvalidRequest,

+ 89 - 59
src/server/session.rs

@@ -1,5 +1,5 @@
 use crate::{ext::ExternalAuthenticator, schema, user::UserExt, UIDCError};
-use microrm::{prelude::*, schema::Stored};
+use microrm::{prelude::*, schema::Stored, ConnectionLease};
 use serde::Deserialize;
 use tide::http::Cookie;
 
@@ -22,17 +22,18 @@ impl<'l> SessionHelper<'l> {
         }
     }
 
-    pub fn get_realm(&self) -> tide::Result<Stored<schema::Realm>> {
+    pub fn get_realm(&self, lease: &mut ConnectionLease) -> tide::Result<Stored<schema::Realm>> {
         self.state
             .db
             .realms
             .keyed(self.realm_str)
-            .get()?
+            .get(lease)?
             .ok_or(tide::Error::from_str(404, "No such realm"))
     }
 
     fn build_session(
         &self,
+        lease: &mut ConnectionLease,
     ) -> tide::Result<(schema::Session, Option<tide::http::Cookie<'static>>)> {
         let rng = ring::rand::SystemRandom::new();
         let session_id: [u8; 32] = ring::rand::generate(&rng)
@@ -40,34 +41,45 @@ impl<'l> SessionHelper<'l> {
             .expose();
         let session_id = base64::encode_config(session_id, base64::URL_SAFE_NO_PAD);
 
-        let session = self.state.db.sessions.insert_and_return(schema::Session {
-            session_id: session_id.clone(),
-            auth: Default::default(),
-            expiry: time::OffsetDateTime::now_utc() + time::Duration::minutes(10),
-        })?;
+        let session = self.state.db.sessions.insert_and_return(
+            lease,
+            schema::Session {
+                session_id: session_id.clone(),
+                auth: Default::default(),
+                expiry: time::OffsetDateTime::now_utc() + time::Duration::minutes(10),
+            },
+        )?;
         let session_cookie = Cookie::build(SESSION_COOKIE_NAME, session_id)
             .path("/")
             .finish();
         Ok((session.wrapped(), Some(session_cookie)))
     }
 
-    pub fn verify_session(&self, req: &Request) -> Option<(Stored<schema::Realm>, schema::UserID)> {
-        self.get_or_build_session(req)
+    pub fn verify_session(
+        &self,
+        lease: &mut ConnectionLease,
+        req: &Request,
+    ) -> Option<(Stored<schema::Realm>, schema::UserID)> {
+        self.get_or_build_session(lease, req)
             .ok()
-            .zip(self.get_realm().ok())
+            .zip(self.get_realm(lease).ok())
             .and_then(|((sid, _cookie), realm)| {
-                self.get_auth_for_session(realm.id(), &sid)
+                self.get_auth_for_session(lease, realm.id(), &sid)
                     .and_then(|auth| auth.user.map(|user| (realm, user)))
             })
     }
 
-    pub fn get_session(&self, req: &Request) -> Option<schema::Session> {
+    pub fn get_session(
+        &self,
+        lease: &mut ConnectionLease,
+        req: &Request,
+    ) -> Option<schema::Session> {
         req.cookie(SESSION_COOKIE_NAME).and_then(|sid| {
             self.state
                 .db
                 .sessions
                 .keyed(sid.value())
-                .get()
+                .get(lease)
                 .ok()
                 .flatten()
                 .map(|v| v.wrapped())
@@ -76,16 +88,18 @@ impl<'l> SessionHelper<'l> {
 
     pub fn get_or_build_session(
         &self,
+        lease: &mut ConnectionLease,
         req: &Request,
     ) -> tide::Result<(schema::Session, Option<tide::http::Cookie<'static>>)> {
-        match self.get_session(req) {
+        match self.get_session(lease, req) {
             Some(s) => Ok((s, None)),
-            None => self.build_session(),
+            None => self.build_session(lease),
         }
     }
 
     pub fn get_auth_for_session(
         &self,
+        lease: &mut ConnectionLease,
         realm: schema::RealmID,
         session: &schema::Session,
     ) -> Option<Stored<schema::SessionAuth>> {
@@ -93,12 +107,13 @@ impl<'l> SessionHelper<'l> {
             .auth
             .with(schema::SessionAuth::Realm, realm)
             .first()
-            .get()
+            .get(lease)
             .ok()?
     }
 
     pub fn destroy_auth(
         &self,
+        lease: &mut ConnectionLease,
         realm: schema::RealmID,
         session: &schema::Session,
     ) -> Result<(), UIDCError> {
@@ -106,7 +121,7 @@ impl<'l> SessionHelper<'l> {
             .auth
             .with(schema::SessionAuth::Realm, realm)
             .first()
-            .delete()?;
+            .delete(lease)?;
         Ok(())
     }
 }
@@ -199,8 +214,9 @@ async fn v1_onetime(req: Request) -> tide::Result<tide::Response> {
 
     let shelper = SessionHelper::new(&req);
 
-    let realm = shelper.get_realm()?;
-    let (session, cookie) = shelper.get_or_build_session(&req)?;
+    let mut lease = req.state().pool.acquire()?;
+    let realm = shelper.get_realm(&mut lease)?;
+    let (session, cookie) = shelper.get_or_build_session(&mut lease, &req)?;
     if let Some(c) = cookie {
         response.insert_cookie(c)
     }
@@ -218,7 +234,12 @@ async fn v1_onetime(req: Request) -> tide::Result<tide::Response> {
 
     println!("looking for single_use_auth with code {}", otq.code);
 
-    let Some(authinfo) = realm.single_use_auth.keyed(otq.code).first().remove()? else {
+    let Some(authinfo) = realm
+        .single_use_auth
+        .keyed(otq.code)
+        .first()
+        .remove(&mut lease)?
+    else {
         return Ok(shelper.render_login_from_auth(
             response,
             otq.redirect,
@@ -241,20 +262,23 @@ async fn v1_onetime(req: Request) -> tide::Result<tide::Response> {
         ));
     }
 
-    match shelper.get_auth_for_session(realm.id(), &session) {
+    match shelper.get_auth_for_session(&mut lease, realm.id(), &session) {
         Some(mut sauth) => {
             sauth.user = Some(authinfo.user);
             sauth.pending_user = None;
             sauth.pending_challenges = vec![].into_serialized();
-            sauth.sync()?;
+            sauth.sync(&mut lease)?;
         }
         None => {
-            session.auth.insert(schema::SessionAuth {
-                realm: realm.id(),
-                user: Some(authinfo.user),
-                pending_user: None,
-                pending_challenges: vec![].into_serialized(),
-            })?;
+            session.auth.insert(
+                &mut lease,
+                schema::SessionAuth {
+                    realm: realm.id(),
+                    user: Some(authinfo.user),
+                    pending_user: None,
+                    pending_challenges: vec![].into_serialized(),
+                },
+            )?;
         }
     }
 
@@ -269,14 +293,15 @@ async fn v1_login(req: Request) -> tide::Result<tide::Response> {
     let mut response = tide::Response::builder(200).build();
 
     let shelper = SessionHelper::new(&req);
+    let mut lease = req.state().pool.acquire()?;
 
-    let realm = shelper.get_realm()?;
-    let (session, cookie) = shelper.get_or_build_session(&req)?;
+    let realm = shelper.get_realm(&mut lease)?;
+    let (session, cookie) = shelper.get_or_build_session(&mut lease, &req)?;
     if let Some(c) = cookie {
         response.insert_cookie(c)
     }
 
-    let auth = shelper.get_auth_for_session(realm.id(), &session);
+    let auth = shelper.get_auth_for_session(&mut lease, realm.id(), &session);
 
     #[derive(serde::Deserialize)]
     struct LoginQuery {
@@ -307,18 +332,19 @@ async fn v1_login_post(mut req: Request) -> tide::Result<tide::Response> {
     let body: ResponseBody = req.body_form().await?;
 
     let shelper = SessionHelper::new(&req);
+    let mut lease = req.state().pool.acquire()?;
 
-    let realm = shelper.get_realm()?;
-    let (session, cookie) = shelper.get_or_build_session(&req)?;
+    let realm = shelper.get_realm(&mut lease)?;
+    let (session, cookie) = shelper.get_or_build_session(&mut lease, &req)?;
     if let Some(c) = cookie {
         response.insert_cookie(c)
     }
 
-    let mut auth = shelper.get_auth_for_session(realm.id(), &session);
+    let mut auth = shelper.get_auth_for_session(&mut lease, realm.id(), &session);
 
     // check if a login reset was requested; if so, we start again from the top
     if body.reset.is_some() {
-        shelper.destroy_auth(realm.id(), &session)?;
+        shelper.destroy_auth(&mut lease, realm.id(), &session)?;
         // TODO: include original redirect URL here
         return Ok(tide::Redirect::new("login").into());
     }
@@ -347,13 +373,13 @@ async fn v1_login_post(mut req: Request) -> tide::Result<tide::Response> {
     match challenge {
         // handle the username challenge specially because this sets up the pending challenges.
         ChallengeType::Username => {
-            shelper.destroy_auth(realm.id(), &session)?;
+            shelper.destroy_auth(&mut lease, realm.id(), &session)?;
 
             let user = realm
                 .users
                 .with(schema::User::Username, &body.challenge)
                 .first()
-                .get()?;
+                .get(&mut lease)?;
             if user.is_none() {
                 error = Some(format!("No such user {}", body.challenge));
             } else {
@@ -365,25 +391,28 @@ async fn v1_login_post(mut req: Request) -> tide::Result<tide::Response> {
                         schema::AuthChallenge::ChallengeType,
                         microrm::schema::Serialized::from(schema::AuthChallengeType::Totp),
                     )
-                    .count()?
+                    .count(&mut lease)?
                     > 0;
 
                 // TODO: support more flows than just username,password[,totp]
                 auth = Some(
-                    session.auth.insert_and_return(schema::SessionAuth {
-                        realm: realm.id(),
-                        user: None,
-                        pending_user: Some(user.id()),
-                        pending_challenges: if has_totp {
-                            vec![
-                                schema::AuthChallengeType::Password,
-                                schema::AuthChallengeType::Totp,
-                            ]
-                        } else {
-                            vec![schema::AuthChallengeType::Password]
-                        }
-                        .into(),
-                    })?,
+                    session.auth.insert_and_return(
+                        &mut lease,
+                        schema::SessionAuth {
+                            realm: realm.id(),
+                            user: None,
+                            pending_user: Some(user.id()),
+                            pending_challenges: if has_totp {
+                                vec![
+                                    schema::AuthChallengeType::Password,
+                                    schema::AuthChallengeType::Totp,
+                                ]
+                            } else {
+                                vec![schema::AuthChallengeType::Password]
+                            }
+                            .into(),
+                        },
+                    )?,
                 );
             }
         }
@@ -393,11 +422,11 @@ async fn v1_login_post(mut req: Request) -> tide::Result<tide::Response> {
                     let user = realm
                         .users
                         .with_id(user_id)
-                        .get()?
+                        .get(&mut lease)?
                         .ok_or(UIDCError::Abort("session auth refers to nonexistent user"))?;
 
                     let verification =
-                        user.verify_challenge_by_type(ctype, body.challenge.as_bytes());
+                        user.verify_challenge_by_type(&mut lease, ctype, body.challenge.as_bytes());
 
                     match verification {
                         Ok(true) => {
@@ -408,7 +437,7 @@ async fn v1_login_post(mut req: Request) -> tide::Result<tide::Response> {
                                 auth.user = auth.pending_user.take();
                             }
 
-                            auth.sync()?;
+                            auth.sync(&mut lease)?;
                         }
                         Ok(false) => {
                             error = Some("Incorrect response. Please try again".into());
@@ -431,6 +460,7 @@ async fn v1_login_post(mut req: Request) -> tide::Result<tide::Response> {
 
 async fn v1_logout(req: Request) -> tide::Result<tide::Response> {
     let shelper = SessionHelper::new(&req);
+    let mut lease = req.state().pool.acquire()?;
 
     #[derive(serde::Deserialize)]
     struct LogoutQuery {
@@ -439,10 +469,10 @@ async fn v1_logout(req: Request) -> tide::Result<tide::Response> {
 
     let query: LogoutQuery = req.query().unwrap();
 
-    let realm = shelper.get_realm()?;
+    let realm = shelper.get_realm(&mut lease)?;
     shelper
-        .get_session(&req)
-        .map(|sid| shelper.destroy_auth(realm.id(), &sid));
+        .get_session(&mut lease, &req)
+        .map(|sid| shelper.destroy_auth(&mut lease, realm.id(), &sid));
     Ok(tide::Redirect::new(query.redirect.unwrap_or_else(|| "../..".into())).into())
 }
 

+ 23 - 9
src/server/um.rs

@@ -6,6 +6,7 @@ use crate::{schema, user::UserExt, UIDCError};
 type Request = tide::Request<super::ServerStateWrapper>;
 
 fn generate_template_data(
+    lease: &mut microrm::ConnectionLease,
     realm: &microrm::Stored<schema::Realm>,
     user: &microrm::Stored<schema::User>,
 ) -> Result<serde_json::Value, UIDCError> {
@@ -15,7 +16,7 @@ fn generate_template_data(
             schema::AuthChallenge::ChallengeType,
             schema::AuthChallengeType::Totp.into_serialized(),
         )
-        .count()?
+        .count(lease)?
         > 0;
 
     let template_data = serde_json::json!({
@@ -33,8 +34,9 @@ fn generate_template_data(
 
 async fn um_index(req: Request) -> tide::Result<tide::Response> {
     let shelper = super::session::SessionHelper::new(&req);
+    let mut lease = req.state().pool.acquire()?;
 
-    let (realm, user_id) = match shelper.verify_session(&req) {
+    let (realm, user_id) = match shelper.verify_session(&mut lease, &req) {
         Some(v) => v,
         None => {
             return Ok(tide::Redirect::temporary(format!(
@@ -45,9 +47,14 @@ async fn um_index(req: Request) -> tide::Result<tide::Response> {
         }
     };
 
-    let user = realm.users.with_id(user_id).first().get()?.unwrap();
+    let user = realm
+        .users
+        .with_id(user_id)
+        .first()
+        .get(&mut lease)?
+        .unwrap();
 
-    let template_data = generate_template_data(&realm, &user)?;
+    let template_data = generate_template_data(&mut lease, &realm, &user)?;
 
     Ok(tide::Response::builder(200)
         .content_type(mime::HTML)
@@ -73,15 +80,21 @@ async fn um_update(mut req: Request) -> tide::Result<tide::Response> {
     let update_form: UpdateForm = req.body_form().await?;
 
     let shelper = super::session::SessionHelper::new(&req);
+    let mut lease = req.state().pool.acquire()?;
 
-    let (realm, user_id) = match shelper.verify_session(&req) {
+    let (realm, user_id) = match shelper.verify_session(&mut lease, &req) {
         Some(v) => v,
         None => {
             return Ok(tide::Redirect::temporary("../v1/session/login?redirect=../../um/").into())
         }
     };
 
-    let user = realm.users.with_id(user_id).first().get()?.unwrap();
+    let user = realm
+        .users
+        .with_id(user_id)
+        .first()
+        .get(&mut lease)?
+        .unwrap();
 
     log::info!("processing update request...");
 
@@ -89,6 +102,7 @@ async fn um_update(mut req: Request) -> tide::Result<tide::Response> {
         let mut info_msgs = vec![];
 
         let challenge = user.verify_challenge_by_type(
+            &mut lease,
             schema::AuthChallengeType::Password,
             update_form.current_password.as_bytes(),
         )?;
@@ -105,7 +119,7 @@ async fn um_update(mut req: Request) -> tide::Result<tide::Response> {
                 Err(UIDCError::Abort("entered passwords do not match"))?
             }
             if !new_pass.is_empty() {
-                user.set_new_password(new_pass.as_bytes())?;
+                user.set_new_password(&mut lease, new_pass.as_bytes())?;
                 info_msgs.push("Updated password!".into());
             }
         } else if update_form.new_password.is_some() || update_form.new_password_repeated.is_some()
@@ -115,7 +129,7 @@ async fn um_update(mut req: Request) -> tide::Result<tide::Response> {
 
         if let Some(totp) = update_form.totp_control.as_ref() {
             if totp == "remove" {
-                user.clear_totp()?;
+                user.clear_totp(&mut lease)?;
                 info_msgs.push("Cleared TOTP setup".into());
             } else if totp == "reset" {
                 let (_secret, _uri) = user.generate_totp_with_uri()?;
@@ -128,7 +142,7 @@ async fn um_update(mut req: Request) -> tide::Result<tide::Response> {
         Ok(info_msgs)
     })();
 
-    let mut template_data = generate_template_data(&realm, &user)?;
+    let mut template_data = generate_template_data(&mut lease, &realm, &user)?;
 
     match progress {
         Ok(info_msgs) => {

+ 36 - 21
src/user.rs

@@ -41,6 +41,7 @@ pub trait UserExt {
     /// UserError::NoSuchChallenge if challenge not found
     fn verify_challenge_by_type(
         &self,
+        lease: &mut microrm::ConnectionLease,
         challenge_type: schema::AuthChallengeType,
         response: &[u8],
     ) -> Result<bool, UIDCError> {
@@ -52,7 +53,7 @@ pub trait UserExt {
                 challenge_type.clone().into_serialized(),
             )
             .first()
-            .get()?
+            .get(lease)?
             .ok_or(UserError::NoSuchChallenge)?;
 
         match challenge_type {
@@ -62,14 +63,18 @@ pub trait UserExt {
         }
     }
 
-    fn set_new_password(&self, password: &[u8]) -> Result<(), UIDCError> {
+    fn set_new_password(
+        &self,
+        lease: &mut microrm::ConnectionLease,
+        password: &[u8],
+    ) -> Result<(), UIDCError> {
         self.stored_user()
             .auth
             .with(
                 schema::AuthChallenge::ChallengeType,
                 &schema::AuthChallengeType::Password.into(),
             )
-            .delete()?;
+            .delete(lease)?;
 
         let rng = ring::rand::SystemRandom::new();
         let salt: [u8; 16] = ring::rand::generate(&rng)
@@ -86,13 +91,16 @@ pub trait UserExt {
             &mut generated,
         );
 
-        self.stored_user().auth.insert(schema::AuthChallenge {
-            user_id: self.stored_user().id(),
-            challenge_type: schema::AuthChallengeType::Password.into(),
-            public: salt.into(),
-            secret: generated.into(),
-            enabled: true,
-        })?;
+        self.stored_user().auth.insert(
+            lease,
+            schema::AuthChallenge {
+                user_id: self.stored_user().id(),
+                challenge_type: schema::AuthChallengeType::Password.into(),
+                public: salt.into(),
+                secret: generated.into(),
+                enabled: true,
+            },
+        )?;
 
         Ok(())
     }
@@ -116,27 +124,34 @@ pub trait UserExt {
         Ok((secret.into(), uri))
     }
 
-    fn set_new_totp(&self, secret: &[u8]) -> Result<(), UIDCError> {
-        self.clear_totp()?;
-        self.stored_user().auth.insert(schema::AuthChallenge {
-            user_id: self.stored_user().id(),
-            challenge_type: schema::AuthChallengeType::Totp.into(),
-            public: vec![],
-            secret: secret.into(),
-            enabled: true,
-        })?;
+    fn set_new_totp(
+        &self,
+        lease: &mut microrm::ConnectionLease,
+        secret: &[u8],
+    ) -> Result<(), UIDCError> {
+        self.clear_totp(lease)?;
+        self.stored_user().auth.insert(
+            lease,
+            schema::AuthChallenge {
+                user_id: self.stored_user().id(),
+                challenge_type: schema::AuthChallengeType::Totp.into(),
+                public: vec![],
+                secret: secret.into(),
+                enabled: true,
+            },
+        )?;
 
         Ok(())
     }
 
-    fn clear_totp(&self) -> Result<(), UIDCError> {
+    fn clear_totp(&self, lease: &mut microrm::ConnectionLease) -> Result<(), UIDCError> {
         self.stored_user()
             .auth
             .with(
                 schema::AuthChallenge::ChallengeType,
                 &schema::AuthChallengeType::Totp.into(),
             )
-            .delete()?;
+            .delete(lease)?;
         Ok(())
     }
 }