浏览代码

WIP refactoring from moving to microrm 0.4.

Kestrel 1 年之前
父节点
当前提交
f1583e6c81
共有 17 个文件被更改,包括 658 次插入758 次删除
  1. 82 41
      Cargo.lock
  2. 2 2
      Cargo.toml
  3. 118 105
      src/cli.rs
  4. 22 7
      src/client_management.rs
  5. 7 10
      src/config.rs
  6. 11 21
      src/config/helper.rs
  7. 6 1
      src/error.rs
  8. 57 94
      src/group_management.rs
  9. 133 40
      src/key.rs
  10. 1 1
      src/main.rs
  11. 97 144
      src/schema.rs
  12. 22 59
      src/scope_management.rs
  13. 0 4
      src/server.rs
  14. 21 54
      src/token.rs
  15. 22 41
      src/token_management.rs
  16. 32 85
      src/user.rs
  17. 25 49
      src/user_management.rs

+ 82 - 41
Cargo.lock

@@ -575,7 +575,7 @@ dependencies = [
  "percent-encoding",
  "rand 0.8.5",
  "sha2 0.9.9",
- "time",
+ "time 0.2.27",
  "version_check",
 ]
 
@@ -673,6 +673,15 @@ dependencies = [
  "cipher",
 ]
 
+[[package]]
+name = "deranged"
+version = "0.3.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b42b6fa04a440b495c8b04d0e71b707c585f83cb9cb28cf8cd0d976c315e31b4"
+dependencies = [
+ "powerfmt",
+]
+
 [[package]]
 name = "digest"
 version = "0.9.0"
@@ -1144,6 +1153,16 @@ version = "0.2.149"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "a08173bc88b7955d1b3145aa561539096c421ac8debde8cbc3612ec635fee29b"
 
+[[package]]
+name = "libsqlite3-sys"
+version = "0.28.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0c10584274047cb335c23d3e61bcef8e323adae7c5c8c760540f73610177fc3f"
+dependencies = [
+ "pkg-config",
+ "vcpkg",
+]
+
 [[package]]
 name = "linux-raw-sys"
 version = "0.3.8"
@@ -1184,26 +1203,23 @@ checksum = "f665ee40bc4a3c5590afb1e9677db74a508659dfd71e126420da8274909a0167"
 
 [[package]]
 name = "microrm"
-version = "0.3.12"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0a480d8dcd5e02d54fd226c23232f42d4a11f80257a2b6cdb7eb971e7a26fb6b"
+version = "0.4.0"
 dependencies = [
  "base64 0.13.1",
  "lazy_static",
+ "libsqlite3-sys",
  "log",
  "microrm-macros",
  "serde",
- "serde_bytes",
  "serde_json",
  "sha2 0.10.8",
- "sqlite",
+ "time 0.3.34",
+ "topological-sort",
 ]
 
 [[package]]
 name = "microrm-macros"
-version = "0.2.6"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a320ac45baf47907282dfc5de25a26b396b69eaeaede3de20fd7ba9b50838917"
+version = "0.4.0"
 dependencies = [
  "convert_case",
  "proc-macro2",
@@ -1223,6 +1239,12 @@ dependencies = [
  "windows-sys",
 ]
 
+[[package]]
+name = "num-conv"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9"
+
 [[package]]
 name = "num-traits"
 version = "0.2.17"
@@ -1412,6 +1434,12 @@ dependencies = [
  "universal-hash",
 ]
 
+[[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.17"
@@ -1888,36 +1916,6 @@ version = "0.5.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "6e63cff320ae2c57904679ba7cb63280a3dc4613885beafb148ee7bf9aa9042d"
 
-[[package]]
-name = "sqlite"
-version = "0.26.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3fb1a534c07ec276fbbe0e55a1c00814d8563da3a2f4d9d9d4c802bd1278db6a"
-dependencies = [
- "libc",
- "sqlite3-sys",
-]
-
-[[package]]
-name = "sqlite3-src"
-version = "0.3.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a260b07ce75a0644c6f5891f34f46db9869e731838e95293469ab17999abcfa3"
-dependencies = [
- "cc",
- "pkg-config",
-]
-
-[[package]]
-name = "sqlite3-sys"
-version = "0.13.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "04d2f028faeb14352df7934b4771806f60d61ce61be1928ec92396d7492e2e54"
-dependencies = [
- "libc",
- "sqlite3-src",
-]
-
 [[package]]
 name = "standback"
 version = "0.2.17"
@@ -2169,11 +2167,32 @@ dependencies = [
  "libc",
  "standback",
  "stdweb",
- "time-macros",
+ "time-macros 0.1.1",
  "version_check",
  "winapi",
 ]
 
+[[package]]
+name = "time"
+version = "0.3.34"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c8248b6521bb14bc45b4067159b9b6ad792e2d6d754d6c41fb50e29fefe38749"
+dependencies = [
+ "deranged",
+ "itoa",
+ "num-conv",
+ "powerfmt",
+ "serde",
+ "time-core",
+ "time-macros 0.2.17",
+]
+
+[[package]]
+name = "time-core"
+version = "0.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3"
+
 [[package]]
 name = "time-macros"
 version = "0.1.1"
@@ -2184,6 +2203,16 @@ dependencies = [
  "time-macros-impl",
 ]
 
+[[package]]
+name = "time-macros"
+version = "0.2.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7ba3a3ef41e6672a2f0f001392bb5dcd3ff0a9992d618ca761a11c3121547774"
+dependencies = [
+ "num-conv",
+ "time-core",
+]
+
 [[package]]
 name = "time-macros-impl"
 version = "0.1.2"
@@ -2246,6 +2275,12 @@ dependencies = [
  "winnow",
 ]
 
+[[package]]
+name = "topological-sort"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ea68304e134ecd095ac6c3574494fc62b909f416c4fca77e440530221e549d3d"
+
 [[package]]
 name = "tracing"
 version = "0.1.39"
@@ -2280,7 +2315,6 @@ version = "0.0.1"
 dependencies = [
  "base32",
  "base64 0.13.1",
- "chrono",
  "clap",
  "handlebars",
  "hmac 0.12.1",
@@ -2298,6 +2332,7 @@ dependencies = [
  "smol",
  "stderrlog",
  "tide",
+ "time 0.3.34",
  "toml",
 ]
 
@@ -2386,6 +2421,12 @@ dependencies = [
  "sval_serde",
 ]
 
+[[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.4"

+ 2 - 2
Cargo.toml

@@ -11,7 +11,7 @@ smol = "1.3"
 log = "0.4"
 serde = { version =  "1.0", features = ["derive"] }
 lazy_static = "1.4.0"
-chrono = "0.4"
+time = { version = "0.3", features = ["std", "formatting"] }
 
 # crypto
 ring = { version = "0.16.20", features = ["std"] }
@@ -25,7 +25,7 @@ hmac = { version = "0.12" }
 toml = "0.8.2"
 
 # Data storage dependencies
-microrm = { version = "0.3.12" }
+microrm = { version = "0.4.0", path="../microrm/microrm/" }
 serde_bytes = { version = "0.11.6" }
 
 # Public API/server dependencies

+ 118 - 105
src/cli.rs

@@ -1,7 +1,8 @@
 use crate::{
-    client_management, config, group_management, key,
-    schema::{self, RealmID},
-    scope_management, server, token, token_management, user_management, UIDCError,
+    schema::{self, UIDCDatabase},
+    config,
+    UIDCError,
+    key, user_management, client_management, scope_management, group_management, token_management,
 };
 use clap::{Parser, Subcommand};
 use microrm::prelude::*;
@@ -33,10 +34,12 @@ enum Command {
     Group(GroupArgs),
     /// key management
     Key(KeyArgs),
-    /// run the actual OIDC server
+    /// scope management
     Scope(ScopeArgs),
+    /*
     /// run the actual OIDC server
     Server(ServerArgs),
+    */
     /// manual token generation and inspection
     Token(TokenArgs),
     /// role management
@@ -46,8 +49,8 @@ enum Command {
 }
 
 struct RunArgs {
-    db: microrm::DB,
-    realm_id: RealmID,
+    db: UIDCDatabase,
+    realm: schema::Realm,
 }
 
 impl RootArgs {
@@ -56,64 +59,68 @@ impl RootArgs {
             return self.init().await;
         }
 
-        let db = microrm::DB::new(schema::schema(), &self.db, microrm::CreateMode::MustExist)
+        let db = UIDCDatabase::open_path(&self.db)
             .map_err(|e| UIDCError::AbortString(format!("Error accessing database: {:?}", e)))?;
 
-        let realm_id = db
-            .query_interface()
-            .get()
-            .by(schema::Realm::Shortname, self.realm.as_str())
-            .one()?
-            .ok_or(UIDCError::Abort("no such realm"))?
-            .id();
+        let realm =
+            db
+            .realms
+            .unique(&self.realm)
+            .get()?
+            .ok_or(UIDCError::Abort("no such realm"))?;
 
-        let ra = RunArgs { db: db, realm_id };
+        let ra = RunArgs { db: db, realm: realm.wrapped() };
 
         match &self.command {
             Command::Init => unreachable!(),
             Command::Config(v) => v.run(ra).await,
-            Command::Client(v) => v.run(ra).await,
-            Command::Group(v) => v.run(ra).await,
             Command::Key(v) => v.run(ra).await,
+            Command::Client(v) => v.run(ra).await,
             Command::Scope(v) => v.run(ra).await,
-            Command::Server(v) => v.run(ra).await,
+            Command::Group(v) => v.run(ra).await,
+            /*Command::Server(v) => v.run(ra).await,*/
             Command::Token(v) => v.run(ra).await,
             Command::Role(v) => v.run(ra).await,
             Command::User(v) => v.run(ra).await,
+            _ => todo!(),
         }
     }
 
     async fn init(&self) -> Result<(), UIDCError> {
         // first check to see if the database is already vaguely set up
-        let maybedb = microrm::DB::new(schema::schema(), &self.db, microrm::CreateMode::MustExist);
-
-        if maybedb.is_ok() {
-            return Err(UIDCError::Abort(
-                "Database already initialized, not overwriting!",
-            ));
-        }
+        let db = UIDCDatabase::open_path(&self.db)
+            .map_err(|e| UIDCError::AbortString(format!("Error accessing database: {:?}", e)))?;
 
         log::info!("Initializing!");
 
-        let db = microrm::DB::new(
-            schema::schema(),
-            &self.db,
-            microrm::CreateMode::AllowNewDatabase,
-        )
-        .expect("Unable to initialize database!");
+        let primary_realm = "primary".to_string();
+
+        if db.realms.unique(&primary_realm).get()?.is_some() {
+            log::warn!("Already initialized with primary realm!");
+            return Ok(())
+        }
 
         // create primary realm
-        db.query_interface().add(&schema::Realm {
-            shortname: "primary".to_string(),
+        db.realms.insert(schema::Realm {
+            shortname: primary_realm,
+            ..Default::default()
         })?;
+
         Ok(())
     }
 }
 
+
 #[derive(Debug, Subcommand)]
 enum KeyCommand {
-    Inspect,
-    Generate,
+    /// Print details of all keys
+    List,
+    /// Generate a new key; see types subcommand for valid options.
+    Generate { keytype: String },
+    /// List what keytypes are supported
+    Types,
+    /// Remove a key from use
+    Remove { key_id: String },
 }
 
 #[derive(Debug, Parser)]
@@ -125,8 +132,25 @@ struct KeyArgs {
 impl KeyArgs {
     async fn run(&self, args: RunArgs) -> Result<(), UIDCError> {
         match &self.command {
-            KeyCommand::Inspect => key::inspect(&args.db, args.realm_id),
-            KeyCommand::Generate => key::generate(&args.db, args.realm_id),
+            KeyCommand::List => key::list(&args.realm),
+            KeyCommand::Generate { keytype } => {
+                for (spec, kty) in key::KEY_TYPE_NAMES {
+                    if keytype == spec {
+                        key::generate_in(&args.realm, *kty)?;
+                        return Ok(())
+                    }
+                }
+                Err(UIDCError::Abort("invalid keytype; try running `uidc key types`"))
+            },
+            KeyCommand::Types => {
+                for (spec, _kty) in key::KEY_TYPE_NAMES {
+                    println!("- {}", spec);
+                }
+                Ok(())
+            }
+            KeyCommand::Remove { key_id } => {
+                key::remove(&args.realm, key_id)
+            },
         }
     }
 }
@@ -148,13 +172,13 @@ impl ClientArgs {
     async fn run(&self, args: RunArgs) -> Result<(), UIDCError> {
         match &self.command {
             ClientCommand::Create { name } => {
-                client_management::create(&args.db, args.realm_id, name)
+                client_management::create(&args.realm, name)
             }
             ClientCommand::List => {
                 todo!()
             }
             ClientCommand::Inspect { name } => {
-                client_management::inspect(&args.db, args.realm_id, name)
+                client_management::inspect(&args.realm, name)
             }
         }
     }
@@ -175,23 +199,20 @@ struct ConfigArgs {
 
 impl ConfigArgs {
     async fn run(&self, args: RunArgs) -> Result<(), UIDCError> {
-        let qi = args.db.query_interface();
-
         match &self.command {
             ConfigCommand::Dump => {
-                let config = config::Config::build_from(&qi, None);
-                println!("config: {:?}", config);
+                let config = config::Config::build_from(&args.db, None);
+                println!("{:?}", config);
             }
             ConfigCommand::Set { key, value } => {
-                qi.delete().by(schema::PersistentConfig::Key, &key).exec()?;
-                qi.insert(&schema::PersistentConfig {
-                    key: key.clone(),
-                    value: value.clone(),
+                args.db.persistent_config.unique(key).delete()?;
+                args.db.persistent_config.insert(schema::PersistentConfig {
+                    key: key.clone(), value: value.clone()
                 })?;
             }
             ConfigCommand::Load { toml_path } => {
-                let config = config::Config::build_from(&qi, Some(toml_path));
-                config.save(&qi);
+                let config = config::Config::build_from(&args.db, Some(toml_path));
+                config.save(&args.db);
             }
         }
         Ok(())
@@ -236,29 +257,27 @@ struct GroupArgs {
 
 impl GroupArgs {
     async fn run(&self, args: RunArgs) -> Result<(), UIDCError> {
-        let qi = args.db.query_interface();
         match &self.command {
             GroupCommand::Create { group_name } => {
-                group_management::create_group(&qi, args.realm_id, group_name)?;
+                group_management::create_group(&args.realm, group_name)?;
             }
             GroupCommand::Members { group_name } => {
-                group_management::list_members(&qi, args.realm_id, group_name.as_str())?;
+                group_management::list_members(&args.realm, group_name)?;
             }
             GroupCommand::Roles { group_name } => {
-                group_management::list_roles(&qi, args.realm_id, group_name.as_str())?;
+                group_management::list_roles(&args.realm, group_name)?;
             }
             GroupCommand::List => {
-                group_management::list_groups(&qi, args.realm_id)?;
+                group_management::list_groups(&args.realm)?;
             }
             GroupCommand::AttachRole {
                 group_name,
                 role_name,
             } => {
                 group_management::attach_role(
-                    &qi,
-                    args.realm_id,
-                    group_name.as_str(),
-                    role_name.as_str(),
+                    &args.realm,
+                    group_name,
+                    role_name,
                 )?;
             }
             GroupCommand::DetachRole {
@@ -266,10 +285,9 @@ impl GroupArgs {
                 role_name,
             } => {
                 group_management::detach_role(
-                    &qi,
-                    args.realm_id,
-                    group_name.as_str(),
-                    role_name.as_str(),
+                    &args.realm,
+                    group_name,
+                    role_name,
                 )?;
             }
             GroupCommand::AttachUser {
@@ -277,10 +295,9 @@ impl GroupArgs {
                 username,
             } => {
                 group_management::attach_user(
-                    &qi,
-                    args.realm_id,
-                    group_name.as_str(),
-                    username.as_str(),
+                    &args.realm,
+                    group_name,
+                    username,
                 )?;
             }
             GroupCommand::DetachUser {
@@ -288,10 +305,9 @@ impl GroupArgs {
                 username,
             } => {
                 group_management::detach_user(
-                    &qi,
-                    args.realm_id,
-                    group_name.as_str(),
-                    username.as_str(),
+                    &args.realm,
+                    group_name,
+                    username,
                 )?;
             }
         }
@@ -326,37 +342,35 @@ struct ScopeArgs {
 
 impl ScopeArgs {
     async fn run(&self, args: RunArgs) -> Result<(), UIDCError> {
-        let qi = args.db.query_interface();
         match &self.command {
             ScopeCommand::AttachRole {
                 scope_name,
                 role_name,
             } => scope_management::attach_role(
-                &qi,
-                args.realm_id,
-                scope_name.as_str(),
-                role_name.as_str(),
+                &args.realm,
+                scope_name,
+                role_name,
             ),
             ScopeCommand::Create { scope_name } => {
-                scope_management::create_scope(&qi, args.realm_id, scope_name.as_str())
+                scope_management::create_scope(&args.realm, scope_name)
             }
             ScopeCommand::DetachRole {
                 scope_name,
                 role_name,
             } => scope_management::detach_role(
-                &qi,
-                args.realm_id,
-                scope_name.as_str(),
-                role_name.as_str(),
+                &args.realm,
+                scope_name,
+                role_name,
             ),
             ScopeCommand::Inspect { scope_name } => {
-                scope_management::inspect_scope(&qi, args.realm_id, scope_name.as_str())
+                scope_management::inspect_scope(&args.realm, scope_name)
             }
-            ScopeCommand::List => scope_management::list_scopes(&qi, args.realm_id),
+            ScopeCommand::List => scope_management::list_scopes(&args.realm),
         }
     }
 }
 
+/*
 #[derive(Debug, Parser)]
 struct ServerArgs {
     #[clap(short, long)]
@@ -369,6 +383,7 @@ impl ServerArgs {
         server::run_server(args.db, config, self.port.unwrap_or(2114)).await
     }
 }
+*/
 
 #[derive(Debug, Subcommand)]
 enum TokenCommand {
@@ -401,8 +416,7 @@ struct TokenArgs {
 
 impl TokenArgs {
     async fn run(&self, args: RunArgs) -> Result<(), UIDCError> {
-        let config = config::Config::build_from(&args.db.query_interface(), None);
-        let qi = args.db.query_interface();
+        let config = config::Config::build_from(&args.db, None);
         match &self.command {
             TokenCommand::GenerateAuth {
                 client,
@@ -410,12 +424,11 @@ impl TokenArgs {
                 scopes,
             } => {
                 let token = token_management::create_auth_token(
-                    &qi,
+                    &args.realm,
                     &config,
-                    args.realm_id,
-                    client.as_str(),
-                    username.as_str(),
-                    scopes.as_str(),
+                    client,
+                    username,
+                    scopes,
                 )?;
                 println!("{}", token);
                 Ok(())
@@ -426,20 +439,18 @@ impl TokenArgs {
                 scopes,
             } => {
                 let token = token_management::create_refresh_token(
-                    &qi,
+                    &args.realm,
                     &config,
-                    args.realm_id,
-                    client.as_str(),
-                    username.as_str(),
-                    scopes.as_str(),
+                    client,
+                    username,
+                    scopes,
                 )?;
                 println!("{}", token);
                 Ok(())
             }
             TokenCommand::Inspect { token } => token_management::inspect_token(
-                &qi,
                 &config,
-                args.realm_id,
+                &args.realm,
                 token.as_ref().map(|s| s.as_str()),
             ),
         }
@@ -461,11 +472,12 @@ struct RoleArgs {
 
 impl RoleArgs {
     async fn run(&self, args: RunArgs) -> Result<(), UIDCError> {
-        let qi = args.db.query_interface();
         match &self.command {
             RoleCommand::List => {
                 todo!()
-            }
+            },
+            _ => todo!(),
+            /*
             RoleCommand::Create { name } => {
                 let add_result = qi.add(&schema::Role {
                     realm: args.realm_id,
@@ -487,6 +499,7 @@ impl RoleArgs {
                     .by(schema::Role::Shortname, name.as_str())
                     .exec()?;
             }
+            */
         }
         Ok(())
     }
@@ -520,25 +533,23 @@ struct UserArgs {
 
 impl UserArgs {
     async fn run(&self, args: RunArgs) -> Result<(), UIDCError> {
-        let qi = args.db.query_interface();
         match &self.command {
-            UserCommand::List => user_management::list(&qi, args.realm_id),
+            UserCommand::List => user_management::list(&args.realm),
             UserCommand::Create { username } => {
-                user_management::create(&qi, args.realm_id, username.as_str())
+                user_management::create(&args.realm, username)
             }
             UserCommand::Auth {
                 username,
                 change_password,
                 change_totp,
             } => user_management::change_auth(
-                &qi,
-                args.realm_id,
-                username.as_str(),
+                &args.realm,
+                username,
                 *change_password > 0,
                 *change_totp > 0,
             ),
             UserCommand::Inspect { username } => {
-                user_management::inspect(&qi, args.realm_id, username.as_str())
+                user_management::inspect(&args.realm, username)
             }
         }
     }
@@ -554,3 +565,5 @@ pub fn invoked() {
         }
     }
 }
+
+

+ 22 - 7
src/client_management.rs

@@ -1,17 +1,32 @@
 use crate::{schema, UIDCError};
 use microrm::prelude::*;
 
-pub fn create(db: &microrm::DB, realm_id: schema::RealmID, name: &str) -> Result<(), UIDCError> {
-    let qi = db.query_interface();
-
-    qi.add(&schema::Client {
-        realm: realm_id,
+pub fn create(realm: &schema::Realm, name: &String) -> Result<(), UIDCError> {
+    realm.clients.insert(schema::Client {
         shortname: name.into(),
         secret: "".into(),
+        redirects: Default::default(),
+        scopes: Default::default(),
     })?;
     Ok(())
 }
 
-pub fn inspect(db: &microrm::DB, realm_id: schema::RealmID, name: &str) -> Result<(), UIDCError> {
-    todo!()
+pub fn inspect(realm: &schema::Realm, name: &String) -> Result<(), UIDCError> {
+    if let Some(client) = realm.clients.unique(name).get()? {
+        println!("Found client {name}");
+        println!("Valid redirect URIs:");
+        for redirect in client.redirects.get()? {
+            println!(" - {}", redirect.redirect);
+        }
+
+        println!("Valid scopes:");
+        for scope in client.scopes.get()? {
+            println!(" - {}", scope.shortname);
+        }
+    }
+    else {
+        println!("No such client {name}");
+    }
+
+    Ok(())
 }

+ 7 - 10
src/config.rs

@@ -17,14 +17,11 @@ pub struct Config {
 }
 
 impl Config {
-    pub fn build_from(qi: &microrm::QueryInterface, cfile: Option<&str>) -> Self {
+    pub fn build_from(db: &schema::UIDCDatabase, cfile: Option<&str>) -> Self {
         let mut config_map = std::collections::HashMap::<String, String>::new();
-        // load config keys from query interface
-        let db_pcs = qi
-            .get::<schema::PersistentConfig>()
-            .all()
-            .expect("couldn't get config keys from database");
-        config_map.extend(db_pcs.into_iter().map(|pc| {
+        // load config keys from database
+        let db_pcs = db.persistent_config.get().expect("could't get config keys from database");
+        config_map.extend(db_pcs.into_iter().map(|pc: microrm::schema::IDWrap<schema::PersistentConfig>| {
             let pc = pc.wrapped();
             (pc.key, pc.value)
         }));
@@ -66,13 +63,13 @@ impl Config {
         config
     }
 
-    pub fn save<'config, 'qi>(&'config self, qi: &'config microrm::QueryInterface<'qi>) {
+    pub fn save<'config, 'qi>(&'config self, db: &'config schema::UIDCDatabase) {
         let ser = helper::ConfigSerializer {
             config: &self,
-            qi: &qi,
+            db,
             prefix: String::new(),
         };
 
-        self.serialize(&ser);
+        let _ = self.serialize(&ser);
     }
 }

+ 11 - 21
src/config/helper.rs

@@ -1,7 +1,5 @@
 #![allow(unused_variables)]
 
-use microrm::prelude::*;
-
 use crate::schema;
 
 use super::Config;
@@ -173,29 +171,21 @@ impl<'l> serde::Serializer for &'l ValueToStringSerializer {
     }
 }
 
-pub struct ConfigSerializer<'r, 's, 'l> {
+pub struct ConfigSerializer<'r, 's> {
     pub config: &'r Config,
-    pub qi: &'s microrm::QueryInterface<'l>,
+    pub db: &'s schema::UIDCDatabase,
     pub prefix: String,
 }
 
-impl<'r, 's, 'l> ConfigSerializer<'r, 's, 'l> {
+impl<'r, 's> ConfigSerializer<'r, 's> {
     fn update(&self, key: &str, value: String) {
-        self.qi
-            .delete()
-            .by(schema::PersistentConfig::Key, key)
-            .exec()
-            .expect("couldn't update config");
-        self.qi
-            .add(&schema::PersistentConfig {
-                key: key.into(),
-                value,
-            })
-            .expect("couldn't update config");
+        // TODO: delete old config value
+        // self.db.persistent_config.delete(schema::PersistentConfig { key: key.into(), value }).expect("couldn't update config");
+        self.db.persistent_config.insert(schema::PersistentConfig { key: key.into(), value }).expect("couldn't update config");
     }
 }
 
-impl<'r, 's, 'l> serde::ser::SerializeStruct for ConfigSerializer<'r, 's, 'l> {
+impl<'r, 's> serde::ser::SerializeStruct for ConfigSerializer<'r, 's> {
     type Ok = ();
     type Error = ConfigError;
 
@@ -223,14 +213,14 @@ impl<'r, 's, 'l> serde::ser::SerializeStruct for ConfigSerializer<'r, 's, 'l> {
     }
 }
 
-impl<'r, 's, 'l> serde::Serializer for &'r ConfigSerializer<'r, 's, 'l> {
+impl<'r, 's> serde::Serializer for &'r ConfigSerializer<'r, 's> {
     type Ok = ();
     type Error = ConfigError;
 
     type SerializeSeq = serde::ser::Impossible<Self::Ok, Self::Error>;
     type SerializeMap = serde::ser::Impossible<Self::Ok, Self::Error>;
     type SerializeTuple = serde::ser::Impossible<Self::Ok, Self::Error>;
-    type SerializeStruct = ConfigSerializer<'r, 's, 'l>;
+    type SerializeStruct = ConfigSerializer<'r, 's>;
     type SerializeTupleStruct = serde::ser::Impossible<Self::Ok, Self::Error>;
     type SerializeTupleVariant = serde::ser::Impossible<Self::Ok, Self::Error>;
     type SerializeStructVariant = serde::ser::Impossible<Self::Ok, Self::Error>;
@@ -332,7 +322,7 @@ impl<'r, 's, 'l> serde::Serializer for &'r ConfigSerializer<'r, 's, 'l> {
 
         let subser = ConfigSerializer {
             config: self.config,
-            qi: self.qi,
+            db: self.db,
             prefix: new_prefix,
         };
 
@@ -557,7 +547,7 @@ impl<'de> serde::Deserializer<'de> for AtomicForwarder<'de> {
     where
         V: serde::de::Visitor<'de>,
     {
-        unreachable!()
+        visitor.visit_unit()
     }
 
     fn deserialize_u64<V>(self, visitor: V) -> Result<V::Value, Self::Error>

+ 6 - 1
src/error.rs

@@ -1,4 +1,5 @@
-use crate::{key::KeyError, token::TokenError, user::UserError};
+// use crate::{key::KeyError, token::TokenError, user::UserError};
+use crate::{key::KeyError,user::UserError};
 
 #[derive(Debug)]
 pub enum UIDCError {
@@ -12,8 +13,10 @@ pub enum UIDCError {
     /// error with key generation or message signing
     KeyError(KeyError),
 
+    /*
     /// error with token generation or verification
     TokenError(TokenError),
+    */
 
     /// error with user operation
     UserError(UserError),
@@ -45,4 +48,6 @@ macro_rules! error_converter {
 
 error_converter!(KeyError);
 error_converter!(UserError);
+/*
 error_converter!(TokenError);
+*/

+ 57 - 94
src/group_management.rs

@@ -2,153 +2,116 @@ use crate::{schema, UIDCError};
 use microrm::prelude::*;
 
 pub fn create_group(
-    qi: &microrm::QueryInterface,
-    realm_id: schema::RealmID,
-    name: &str,
+    realm: &schema::Realm,
+    name: &String,
 ) -> Result<(), UIDCError> {
-    qi.add(&schema::Group {
-        realm: realm_id,
+    realm.groups.insert(schema::Group {
         shortname: name.into(),
+        roles: Default::default(),
+        users: Default::default(),
     })?;
     Ok(())
 }
 
 pub fn list_groups(
-    qi: &microrm::QueryInterface,
-    realm_id: schema::RealmID,
+    realm: &schema::Realm,
 ) -> Result<(), UIDCError> {
-    for group in qi.get().by(schema::Group::Realm, &realm_id).all()? {
+    for group in realm.groups.get()? {
         println!("{}", group.shortname);
     }
     Ok(())
 }
 
 pub fn list_members(
-    qi: &microrm::QueryInterface,
-    realm_id: schema::RealmID,
-    name: &str,
+    realm: &schema::Realm,
+    name: &String,
 ) -> Result<(), UIDCError> {
-    let group_id = qi
-        .get()
-        .only_ids()
-        .by(schema::Group::Realm, &realm_id)
-        .by(schema::Group::Shortname, name)
-        .one_id()?
-        .ok_or(UIDCError::Abort("no such group"))?;
-
-    for member in qi
-        .get()
-        .by(schema::GroupMembership::Group, &group_id)
-        .all()?
-    {
-        let user = qi
-            .get()
-            .by_id(&member.user)
-            .one()?
-            .ok_or(UIDCError::Abort("no user matching GroupMembership"))?;
-        println!("{}", user.username);
+    for member in realm.groups.unique(name).join(schema::Group::Users).get()? {
+        println!("- {}", member.username);
     }
+
     Ok(())
 }
 
 pub fn list_roles(
-    qi: &microrm::QueryInterface,
-    realm_id: schema::RealmID,
-    name: &str,
+    realm: &schema::Realm,
+    name: &String,
 ) -> Result<(), UIDCError> {
-    let group_id = qi
-        .get()
-        .only_ids()
-        .by(schema::Group::Realm, &realm_id)
-        .by(schema::Group::Shortname, name)
-        .one_id()?
-        .ok_or(UIDCError::Abort("no such group"))?;
-
-    for member in qi.get().by(schema::GroupRole::Group, &group_id).all()? {
-        let role = qi
-            .get()
-            .by_id(&member.role)
-            .one()?
-            .ok_or(UIDCError::Abort("no role matching GroupRole"))?;
-        println!("{}", role.shortname);
+    for role in realm.groups.unique(name).join(schema::Group::Roles).get()? {
+        println!("- {}", role.shortname);
     }
+
     Ok(())
 }
 
 pub fn attach_user(
-    qi: &microrm::QueryInterface,
-    realm_id: schema::RealmID,
-    group_name: &str,
-    username: &str,
+    realm: &schema::Realm,
+    group_name: &String,
+    username: &String,
 ) -> Result<(), UIDCError> {
-    let group = qi
-        .get()
-        .by(schema::Group::Realm, &realm_id)
-        .by(schema::Group::Shortname, group_name)
-        .one()?;
-    let user = qi
-        .get()
-        .by(schema::User::Realm, &realm_id)
-        .by(schema::User::Username, username)
-        .one()?;
+    let group = realm.groups.unique(group_name).get()?;
+    let user = realm.users.unique(username).get()?;
 
     match (group, user) {
         (None, _) => Err(UIDCError::Abort("no such group")),
         (_, None) => Err(UIDCError::Abort("no such user")),
         (Some(group), Some(user)) => {
-            qi.add(&schema::GroupMembership {
-                group: group.id(),
-                user: user.id(),
-            })?;
+            group.users.connect_to(user.id())?;
             Ok(())
         }
     }
 }
 
 pub fn detach_user(
-    qi: &microrm::QueryInterface,
-    realm_id: schema::RealmID,
-    group_name: &str,
-    username: &str,
+    realm: &schema::Realm,
+    group_name: &String,
+    username: &String,
 ) -> Result<(), UIDCError> {
-    todo!()
+    let group = realm.groups.unique(group_name).get()?;
+    let user = realm.users.unique(username).get()?;
+
+    match (group, user) {
+        (None, _) => Err(UIDCError::Abort("no such group")),
+        (_, None) => Err(UIDCError::Abort("no such user")),
+        (Some(group), Some(user)) => {
+            group.users.disconnect_from(user.id())?;
+            Ok(())
+        }
+    }
 }
 
 pub fn attach_role(
-    qi: &microrm::QueryInterface,
-    realm_id: schema::RealmID,
-    group_name: &str,
-    role_name: &str,
+    realm: &schema::Realm,
+    group_name: &String,
+    role_name: &String
 ) -> Result<(), UIDCError> {
-    let group = qi
-        .get()
-        .by(schema::Group::Realm, &realm_id)
-        .by(schema::Group::Shortname, group_name)
-        .one()?;
-    let role = qi
-        .get()
-        .by(schema::Role::Realm, &realm_id)
-        .by(schema::Role::Shortname, role_name)
-        .one()?;
+    let group = realm.groups.unique(group_name).get()?;
+    let role = realm.roles.unique(role_name).get()?;
 
     match (group, role) {
         (None, _) => Err(UIDCError::Abort("no such group")),
         (_, None) => Err(UIDCError::Abort("no such role")),
         (Some(group), Some(role)) => {
-            qi.add(&schema::GroupRole {
-                group: group.id(),
-                role: role.id(),
-            })?;
+            group.roles.connect_to(role.id())?;
             Ok(())
         }
     }
 }
 
 pub fn detach_role(
-    qi: &microrm::QueryInterface,
-    realm_id: schema::RealmID,
-    group_name: &str,
-    role: &str,
+    realm: &schema::Realm,
+    group_name: &String,
+    role_name: &String
 ) -> Result<(), UIDCError> {
-    todo!()
+    let group = realm.groups.unique(group_name).get()?;
+    let role = realm.roles.unique(role_name).get()?;
+
+    match (group, role) {
+        (None, _) => Err(UIDCError::Abort("no such group")),
+        (_, None) => Err(UIDCError::Abort("no such role")),
+        (Some(group), Some(role)) => {
+            group.roles.disconnect_from(role.id())?;
+            Ok(())
+        }
+    }
 }

+ 133 - 40
src/key.rs

@@ -1,69 +1,162 @@
 use crate::{schema, UIDCError};
-use microrm::prelude::*;
 use ring::signature::{Ed25519KeyPair, KeyPair};
 use sha2::Digest;
+use microrm::prelude::*;
 
 #[derive(Debug)]
 pub enum KeyError {
     Plain(&'static str),
 }
 
-pub struct KeyStore<'a, 'r> {
-    qi: &'r microrm::QueryInterface<'a>,
+#[non_exhaustive]
+#[derive(Clone, Copy, Debug, serde::Serialize, serde::Deserialize)]
+pub enum KeyType {
+    RSA2048,
+    RSA4096,
+    Ed25519,
+}
+
+pub const KEY_TYPE_NAMES : &'static [(&'static str, KeyType)] = &[
+    ("rsa2048", KeyType::RSA2048),
+    ("rsa4096", KeyType::RSA4096),
+    ("ed25519", KeyType::Ed25519),
+];
+
+pub enum ParsedKey {
+    Ed25519 { key_id: String, keypair: ring::signature::Ed25519KeyPair },
+    RSA { key_id: String, keypair: ring::signature::RsaKeyPair }
 }
 
-impl<'a, 'r> KeyStore<'a, 'r> {
-    pub fn new(qi: &'r microrm::QueryInterface<'a>) -> Self {
-        Self { qi }
+impl ParsedKey {
+    pub fn key_id(&self) -> &str {
+        match self {
+            Self::Ed25519 { key_id, keypair } => key_id.as_str(),
+            Self::RSA { key_id, keypair } => key_id.as_str(),
+        }
+    }
+
+    pub fn generate_signature(&self, data: &[u8]) -> Result<Vec<u8>, UIDCError> {
+        match self {
+            Self::Ed25519 { key_id, keypair } => {
+                Ok(keypair.sign(data).as_ref().into())
+            },
+            Self::RSA { key_id, keypair } => {
+                let rng = ring::rand::SystemRandom::new();
+                let mut signature = vec![];
+                signature.resize(keypair.public_modulus_len(), 0);
+                keypair.sign(&ring::signature::RSA_PKCS1_SHA256, &rng, data, signature.as_mut_slice()).map_err(|_| KeyError::Plain("failed to generate RSA signature!"))?;
+                Ok(signature)
+            }
+        }
+    }
+
+    pub fn verify_signature(&self, data: &[u8], signature: &[u8]) -> Result<bool, UIDCError> {
+        use ring::signature::VerificationAlgorithm;
+        match self {
+            Self::Ed25519 { key_id, keypair } => {
+                Ok(ring::signature::ED25519.verify(keypair.public_key().as_ref().into(), data.into(), signature.into()).is_ok())
+            },
+            Self::RSA { key_id, keypair } => {
+                Ok(ring::signature::RSA_PKCS1_2048_8192_SHA256.verify(keypair.public_key().as_ref().into(), data.into(), signature.into()).is_ok())
+            },
+        }
     }
+}
 
-    pub fn generate_in(&self, realm_id: schema::RealmID) -> Result<String, UIDCError> {
-        let mut rng = ring::rand::SystemRandom::new();
-        let sign_generated = Ed25519KeyPair::generate_pkcs8(&mut rng)
-            .map_err(|_| KeyError::Plain("failed to generate key"))?;
+fn pubkey_id(data: &[u8]) -> String {
+    let mut key_hasher = sha2::Sha256::new();
+    key_hasher.update(data);
+    let mut key_id = base64::encode(key_hasher.finalize());
+    key_id.truncate(16);
+    key_id
+}
 
-        let keydata = sign_generated.as_ref().to_owned();
+fn generate_rsa(realm: &schema::Realm, kty: KeyType, bits: usize) -> Result<ParsedKey, 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")
+        .arg(format!(
+            "openssl genpkey \
+             -algorithm RSA \
+             -pkeyopt rsa_keygen_pubexp:65537 \
+             -pkeyopt rsa_keygen_bits:{bits} \
+             | openssl pkcs8 \
+             -topk8 \
+             -nocrypt \
+             -outform der"))
+        .output()
+        .map_err(|_| UIDCError::Abort("couldn't invoke openssl"))?;
 
-        let loaded_key = Ed25519KeyPair::from_pkcs8(keydata.as_slice())
-            .expect("couldn't load just-generated key");
-        let pubkey = loaded_key.public_key();
+    let secret = openssl_output.stdout;
 
-        let mut key_hasher = sha2::Sha256::new();
-        key_hasher.update(&pubkey.as_ref());
-        let mut key_id = base64::encode(key_hasher.finalize());
-        key_id.truncate(16);
+    let keypair = ring::signature::RsaKeyPair::from_pkcs8(&secret).map_err(|_| UIDCError::Abort("couldn't parse PKCS#8 output from openssl"))?;
+    let public = keypair.public_key().as_ref();
+    let key_id = pubkey_id(public.as_ref());
+    let expiry = time::OffsetDateTime::now_utc() + time::Duration::days(730);
 
-        self.qi.add(&schema::Key {
-            realm: realm_id,
-            key_id: key_id.clone(),
-            keydata,
-        })?;
+    realm.keys.insert(schema::Key {
+        key_id: key_id.clone(),
+        key_type: kty.into(),
+        public_data: public.into(),
+        secret_data: secret.into(),
+        expiry
+    })?;
 
-        Ok(key_id)
-    }
+    Ok(ParsedKey::RSA { key_id, keypair })
 }
 
-pub fn inspect(db: &microrm::DB, realm_id: schema::RealmID) -> Result<(), UIDCError> {
-    let qi = db.query_interface();
-    println!("Keystore loaded.");
+pub fn generate_in(realm: &schema::Realm, kty: KeyType) -> Result<ParsedKey, UIDCError> {
+    let mut rng = ring::rand::SystemRandom::new();
+    match kty {
+        KeyType::RSA2048 => {
+            generate_rsa(realm, KeyType::RSA2048, 2048)
+        },
+        KeyType::RSA4096 => {
+            generate_rsa(realm, KeyType::RSA4096, 4096)
+        },
+        KeyType::Ed25519 => {
+            let generated_keypair = Ed25519KeyPair::generate_pkcs8(&mut rng)
+                .map_err(|_| KeyError::Plain("failed to generate key"))?;
+
+            let keydata = generated_keypair.as_ref().to_owned();
+
+            let loaded_key = Ed25519KeyPair::from_pkcs8(keydata.as_slice())
+                .expect("couldn't load just-generated key");
+            let pubkey = loaded_key.public_key();
+            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(),
+                // no public data for EdDSA keys
+                public_data: vec![],
+                secret_data: keydata,
+                expiry
+            })?;
 
-    println!("Retrieving keys realm...");
-    let keys = qi.get().by(schema::Key::Realm, &realm_id).all()?;
+            Ok(ParsedKey::Ed25519 { key_id, keypair: loaded_key })
+        },
+    }
+}
+
+pub fn list(realm: &schema::Realm) -> Result<(), UIDCError> {
+    let keys = realm.keys.get()?;
 
     for key in keys {
-        println!("- [{:20}]", key.key_id);
+        println!("- [{}] {:?}, expires {}", key.key_id, key.key_type, key.expiry.format(&time::format_description::well_known::Rfc3339).unwrap());
     }
     Ok(())
 }
 
-pub fn generate(db: &microrm::DB, realm_id: schema::RealmID) -> Result<(), UIDCError> {
-    let qi = db.query_interface();
-    let cs = KeyStore::new(&qi);
-    match cs.generate_in(realm_id) {
-        Ok(_) => (),
-        Err(e) => {
-            println!("Failed to generate key: {}", e);
-        }
-    }
+pub fn remove(realm: &schema::Realm, key_id: &String) -> Result<(), UIDCError> {
+    realm.keys.with(schema::Key::KeyId, key_id).delete()?;
+    /*for key in realm.keys.get()? {
+        if key.key_id != key_id { continue }
+        println!("Deleting key...");
+        realm.keys.dissociate_with(key.id())?;
+        break
+    }*/
+
     Ok(())
 }

+ 1 - 1
src/main.rs

@@ -10,7 +10,7 @@ mod key;
 mod role_management;
 mod schema;
 mod scope_management;
-mod server;
+// mod server;
 mod token;
 mod token_management;
 mod user;

+ 97 - 144
src/schema.rs

@@ -1,65 +1,42 @@
-use microrm::make_index;
-pub use microrm::{Entity, Modelable, Schema};
+pub use microrm::prelude::{Entity, Database};
+use microrm::schema::{IDMap, AssocMap, Serialized, Relation, AssocDomain, AssocRange};
 use serde::{Deserialize, Serialize};
 
+use crate::key::KeyType;
+
+// ----------------------------------------------------------------------
+// uidc internal types
+// ----------------------------------------------------------------------
+
 /// Simple key-value store for persistent configuration
-#[derive(Debug, Entity, Serialize, Deserialize)]
+#[derive(Debug, Entity)]
 pub struct PersistentConfig {
-    #[microrm_unique]
+    #[unique]
     pub key: String,
     pub value: String,
 }
 
-#[derive(Debug, Entity, Serialize, Deserialize)]
+// ----------------------------------------------------------------------
+// Session types
+// ----------------------------------------------------------------------
+
+#[derive(Debug, Entity)]
 pub struct Session {
-    #[microrm_unique]
-    pub key: String,
-    // TODO: add expiry here
+    auth: AssocMap<SessionAuth>,
+    // expiry: std::time::SystemTime
 }
 
-/// Authentication state for a session. If no challenges are left, it's considered authorized.
-#[derive(Debug, Entity, Serialize, Deserialize)]
-pub struct SessionAuthentication {
-    #[microrm_foreign]
-    #[microrm_unique]
+#[derive(Debug, Entity)]
+pub struct SessionAuth {
     pub realm: RealmID,
-    #[microrm_foreign]
-    #[microrm_unique]
-    pub session: SessionID,
-    #[microrm_foreign]
-    #[microrm_unique]
-    pub user: UserID,
-
-    pub challenges_left: Vec<AuthChallengeType>,
-}
 
-// **** oauth types ****
-#[derive(Debug, Entity, Serialize, Deserialize)]
-pub struct Realm {
-    #[microrm_unique]
-    pub shortname: String,
-}
+    pub user: Option<UserID>,
 
-#[derive(Debug, Entity, Serialize, Deserialize)]
-pub struct Key {
-    #[microrm_foreign]
-    pub realm: RealmID,
-    pub key_id: String,
-    #[serde(with = "serde_bytes")]
-    pub keydata: Vec<u8>,
+    pub pending_user: Option<UserID>,
+    pub pending_challenges: Serialized<Vec<AuthChallengeType>>,
 }
 
-/// End-user representation object
-#[derive(Debug, Entity, Serialize, Deserialize)]
-pub struct User {
-    #[microrm_foreign]
-    #[microrm_unique]
-    pub realm: RealmID,
-    #[microrm_unique]
-    pub username: String,
-}
-
-#[derive(Clone, Copy, Debug, PartialEq, PartialOrd, Modelable, Serialize, Deserialize)]
+#[derive(Clone, Copy, Debug, PartialEq, PartialOrd, Serialize, Deserialize)]
 pub enum AuthChallengeType {
     Username,
     Password,
@@ -68,130 +45,106 @@ pub enum AuthChallengeType {
     WebAuthn,
 }
 
-#[derive(Debug, Entity, Serialize, Deserialize)]
+#[derive(Debug, Entity)]
 pub struct AuthChallenge {
-    #[microrm_foreign]
-    #[microrm_unique]
-    pub user: UserID,
-    pub challenge_type: AuthChallengeType,
-    #[serde(with = "serde_bytes")]
+    #[unique]
+    pub challenge_type: Serialized<AuthChallengeType>,
     pub public: Vec<u8>,
-    #[serde(with = "serde_bytes")]
     pub secret: Vec<u8>,
     pub enabled: bool,
 }
 
-/// User semantic grouping
-#[derive(Debug, Entity, Serialize, Deserialize)]
+// ----------------------------------------------------------------------
+// OIDC types
+// ----------------------------------------------------------------------
+
+pub struct UserGroupRelation;
+impl Relation for UserGroupRelation {
+    type Domain = User;
+    type Range = Group;
+    const NAME: &'static str = "UserGroup";
+}
+
+pub struct GroupRoleRelation;
+impl Relation for GroupRoleRelation {
+    type Domain = Group;
+    type Range = Role;
+    const NAME: &'static str = "GroupRole";
+}
+
+#[derive(Debug, Default, Entity)]
+pub struct Realm {
+    #[unique]
+    pub shortname: String,
+    pub users: AssocMap<User>,
+    pub groups: AssocMap<Group>,
+    pub roles: AssocMap<Role>,
+    pub clients: AssocMap<Client>,
+    pub keys: AssocMap<Key>,
+    pub scopes: AssocMap<Scope>,
+}
+
+#[derive(Debug, Entity)]
+pub struct Key {
+    #[unique]
+    pub key_id: String,
+    pub key_type: Serialized<KeyType>,
+    pub public_data: Vec<u8>,
+    pub secret_data: Vec<u8>,
+    pub expiry: time::OffsetDateTime,
+}
+
+#[derive(Debug, Entity)]
+pub struct User {
+    #[unique]
+    pub username: String,
+    pub auth: AssocMap<AuthChallenge>,
+    pub groups: AssocDomain<UserGroupRelation>,
+}
+
+#[derive(Debug, Entity)]
 pub struct Group {
-    #[microrm_foreign]
-    #[microrm_unique]
-    pub realm: RealmID,
-    #[microrm_unique]
+    #[unique]
     pub shortname: String,
+    pub users: AssocRange<UserGroupRelation>,
+    pub roles: AssocDomain<GroupRoleRelation>,
 }
 
-/// User membership in group
-#[derive(Debug, Entity, Serialize, Deserialize)]
-pub struct GroupMembership {
-    #[microrm_foreign]
-    #[microrm_unique]
-    pub group: GroupID,
-    #[microrm_foreign]
-    #[microrm_unique]
-    pub user: UserID,
+#[derive(Debug, Entity)]
+pub struct Role {
+    #[unique]
+    pub shortname: String,
+    pub groups: AssocRange<GroupRoleRelation>,
 }
 
 /// OAuth2 client representation
-#[derive(Debug, Entity, Serialize, Deserialize)]
+#[derive(Debug, Entity)]
 pub struct Client {
-    #[microrm_foreign]
-    #[microrm_unique]
-    pub realm: RealmID,
-    #[microrm_unique]
+    #[unique]
     pub shortname: String,
-
     pub secret: String,
+    pub redirects: AssocMap<ClientRedirect>,
+    pub scopes: AssocMap<Scope>,
 }
 
-#[derive(Debug, Entity, Serialize, Deserialize)]
+#[derive(Debug, Entity)]
 pub struct ClientRedirect {
-    #[microrm_foreign]
-    #[microrm_unique]
-    pub client: ClientID,
     pub redirect: String,
 }
 
 /// Requested group of permissions
-#[derive(Debug, Entity, Serialize, Deserialize)]
+#[derive(Debug, Entity)]
 pub struct Scope {
-    #[microrm_foreign]
-    #[microrm_unique]
-    pub realm: RealmID,
-    #[microrm_unique]
+    #[unique]
     pub shortname: String,
+    pub roles: AssocMap<Role>,
 }
 
-/// Specific atomic permission
-#[derive(Debug, Entity, Serialize, Deserialize)]
-pub struct Role {
-    #[microrm_foreign]
-    #[microrm_unique]
-    pub realm: RealmID,
-    #[microrm_unique]
-    pub shortname: String,
-}
+#[derive(Database)]
+pub struct UIDCDatabase {
+    pub persistent_config: IDMap<PersistentConfig>,
+
+    pub realms: IDMap<Realm>,
 
-/// Role membership in scope
-#[derive(Debug, Entity, Serialize, Deserialize)]
-pub struct ScopeRole {
-    #[microrm_foreign]
-    #[microrm_unique]
-    pub scope: ScopeID,
-    #[microrm_foreign]
-    #[microrm_unique]
-    pub role: RoleID,
-}
-
-/// Assigned permissions in group
-#[derive(Debug, Entity, Serialize, Deserialize)]
-pub struct GroupRole {
-    #[microrm_foreign]
-    #[microrm_unique]
-    pub group: GroupID,
-    #[microrm_foreign]
-    #[microrm_unique]
-    pub role: RoleID,
-}
-
-#[derive(Debug, Entity, Serialize, Deserialize)]
-pub struct RevokedToken {
-    #[microrm_foreign]
-    #[microrm_unique]
-    pub user: UserID,
-    #[microrm_unique]
-    pub nonce: String,
-}
-
-pub fn schema() -> Schema {
-    Schema::new()
-        // global config types
-        .entity::<PersistentConfig>()
-        // session types
-        .entity::<Session>()
-        .entity::<SessionAuthentication>()
-        // oauth types
-        .entity::<Realm>()
-        .entity::<Key>()
-        .entity::<User>()
-        .entity::<AuthChallenge>()
-        .entity::<Group>()
-        .entity::<GroupMembership>()
-        .entity::<Client>()
-        .entity::<ClientRedirect>()
-        .entity::<Scope>()
-        .entity::<Role>()
-        .entity::<ScopeRole>()
-        .entity::<GroupRole>()
-        .entity::<RevokedToken>()
+    pub sessions: IDMap<Session>,
 }

+ 22 - 59
src/scope_management.rs

@@ -2,48 +2,35 @@ use crate::{schema, UIDCError};
 use microrm::prelude::*;
 
 pub fn create_scope(
-    qi: &microrm::QueryInterface,
-    realm_id: schema::RealmID,
-    name: &str,
+    realm: &schema::Realm,
+    name: &String,
 ) -> Result<(), UIDCError> {
-    qi.add(&schema::Scope {
-        realm: realm_id,
+    realm.scopes.insert(schema::Scope {
         shortname: name.into(),
+        roles: Default::default(),
     })?;
     Ok(())
 }
 
 pub fn list_scopes(
-    qi: &microrm::QueryInterface,
-    realm_id: schema::RealmID,
+    realm: &schema::Realm,
 ) -> Result<(), UIDCError> {
-    for scope in qi.get().by(schema::Scope::Realm, &realm_id).all()? {
+    for scope in realm.scopes.get()? {
         println!("{}", scope.shortname);
     }
     Ok(())
 }
 
 pub fn inspect_scope(
-    qi: &microrm::QueryInterface,
-    realm_id: schema::RealmID,
-    scope_name: &str,
+    realm: &schema::Realm,
+    scope_name: &String,
 ) -> Result<(), UIDCError> {
-    let scope = qi
-        .get()
-        .by(schema::Scope::Realm, &realm_id)
-        .by(schema::Scope::Shortname, scope_name)
-        .one()?
-        .ok_or(UIDCError::Abort("no such scope"))?;
+    let scope = realm.scopes.unique(scope_name).get()?.ok_or(UIDCError::Abort("no such scope"))?;
 
     println!("scope name: {}", scope.shortname);
 
     println!("attached roles:");
-    for scope_role in qi.get().by(schema::ScopeRole::Scope, &scope.id()).all()? {
-        let role = qi
-            .get()
-            .by_id(&scope_role.role)
-            .one()?
-            .ok_or(UIDCError::Abort("role referenced that no longer exists?"))?;
+    for role in scope.roles.get()? {
         println!(" - {}", role.shortname);
     }
 
@@ -51,57 +38,33 @@ pub fn inspect_scope(
 }
 
 pub fn attach_role(
-    qi: &microrm::QueryInterface,
-    realm_id: schema::RealmID,
-    scope_name: &str,
-    role_name: &str,
+    realm: &schema::Realm,
+    scope_name: &String,
+    role_name: &String,
 ) -> Result<(), UIDCError> {
-    let scope = qi
-        .get()
-        .by(schema::Scope::Realm, &realm_id)
-        .by(schema::Scope::Shortname, scope_name)
-        .one()?;
-    let role = qi
-        .get()
-        .by(schema::Role::Realm, &realm_id)
-        .by(schema::Role::Shortname, role_name)
-        .one()?;
+    let scope = realm.scopes.unique(scope_name).get()?;
+    let role = realm.roles.unique(role_name).get()?;
 
     match (scope, role) {
         (None, _) => Err(UIDCError::Abort("no such scope")),
         (_, None) => Err(UIDCError::Abort("no such role")),
         (Some(scope), Some(role)) => {
-            qi.add(&schema::ScopeRole {
-                scope: scope.id(),
-                role: role.id(),
-            })?;
+            scope.roles.connect_to(role.id())?;
             Ok(())
         }
     }
 }
 
 pub fn detach_role(
-    qi: &microrm::QueryInterface,
-    realm_id: schema::RealmID,
-    scope_name: &str,
-    role_name: &str,
+    realm: &schema::Realm,
+    scope_name: &String,
+    role_name: &String,
 ) -> Result<(), UIDCError> {
-    let scope = qi
-        .get()
-        .by(schema::Scope::Realm, &realm_id)
-        .by(schema::Scope::Shortname, scope_name)
-        .one()?;
-    let role = qi
-        .get()
-        .by(schema::Role::Realm, &realm_id)
-        .by(schema::Role::Shortname, role_name)
-        .one()?;
+    let scope = realm.scopes.unique(scope_name).get()?;
+    let role = realm.roles.unique(role_name).get()?;
 
     if let Some((scope, role)) = scope.as_ref().zip(role) {
-        qi.delete()
-            .by(schema::ScopeRole::Scope, &scope.id())
-            .by(schema::ScopeRole::Role, &role.id())
-            .exec()?
+        scope.roles.disconnect_from(role.id())?;
     } else if scope.is_none() {
         println!("No such scope!");
     } else {

+ 0 - 4
src/server.rs

@@ -1,5 +1,3 @@
-use tide::prelude::Listener;
-
 use crate::{config, UIDCError};
 
 mod oidc;
@@ -37,8 +35,6 @@ async fn index(req: tide::Request<ServerStateWrapper>) -> tide::Result<tide::Res
     Ok(response)
 }
 
-fn run_cleanup() {}
-
 pub async fn run_server(
     db: microrm::DB,
     config: config::Config,

+ 21 - 54
src/token.rs

@@ -1,5 +1,5 @@
-use crate::{config, jwt, schema, UIDCError};
-use microrm::{entity::EntityID, prelude::*};
+use crate::{config, jwt, schema, UIDCError, key};
+use microrm::prelude::*;
 
 #[derive(Debug)]
 pub enum TokenError {
@@ -22,74 +22,37 @@ impl From<ring::error::KeyRejected> for TokenError {
 
 pub fn generate_auth_token<'a>(
     config: &config::Config,
-    qi: &microrm::QueryInterface,
-    realm: schema::RealmID,
-    client: schema::ClientID,
-    user: schema::UserID,
+    realm: &schema::Realm,
+    client: &schema::Client,
+    user: &schema::User,
     scopes: impl Iterator<Item = &'a str>,
 ) -> Result<String, UIDCError> {
-    let realm = qi
-        .get()
-        .by_id(&realm)
-        .one()?
-        .ok_or(TokenError::RequestError("no such realm"))?;
-    let client = qi
-        .get()
-        .by_id(&client)
-        .one()?
-        .ok_or(TokenError::RequestError("no such client"))?;
-    let user = qi
-        .get()
-        .by_id(&user)
-        .one()?
-        .ok_or(TokenError::RequestError("no such user"))?;
-
     let issuer = format!("{}/{}", config.base_url, realm.shortname,);
 
     let iat = std::time::SystemTime::now();
     let exp = iat + std::time::Duration::from_secs(config.auth_token_expiry);
 
     // find all roles the user can possibly have access to
-    let mut user_roles = vec![];
-    for group in qi
-        .get()
-        .by(schema::GroupMembership::User, &user.id())
-        .all()?
-    {
-        for group_role in qi.get().by(schema::GroupRole::Group, &group.id()).all()? {
-            user_roles.push(group_role.role);
-        }
-    }
+    let mut user_roles = user.groups.join(schema::Group::Roles).get()?;
 
     // find all roles requested by the scopes
     let mut requested_roles = vec![];
-    for scope in scopes {
-        if let Some(scope) = qi.get().by(schema::Scope::Shortname, scope).one()? {
-            for scope_role in qi.get().by(schema::ScopeRole::Scope, &scope.id()).all()? {
-                requested_roles.push(scope_role.role);
-            }
+    for scope_name in scopes {
+        if let Some(scope) = realm.scopes.unique(&scope_name.to_string()).get()? {
+            requested_roles.extend(scope.roles.get()?.into_iter());
         }
     }
 
-    user_roles.sort_by_key(|k| k.raw_id());
+    user_roles.sort_by_key(|k| k.id());
     user_roles.dedup();
-    requested_roles.sort_by_key(|k| k.raw_id());
+    requested_roles.sort_by_key(|k| k.id());
     requested_roles.dedup();
 
     // find the intersection between requested roles and the ones the user actually has
     let resulting_roles = requested_roles
-        .iter()
+        .into_iter()
         .filter(|req| user_roles.contains(req))
-        .map(|role_id| {
-            Ok(serde_json::Value::String(
-                qi.get()
-                    .by_id(role_id)
-                    .one()?
-                    .ok_or(UIDCError::Abort("inconsistent role state"))?
-                    .wrapped()
-                    .shortname,
-            ))
-        });
+        .map(|role| Ok(serde_json::Value::String(role.wrapped().shortname)));
 
     let token = jwt::JWTData {
         sub: user.username.as_str(),
@@ -104,6 +67,9 @@ pub fn generate_auth_token<'a>(
         .into(),
     };
 
+    /*
+    let key = key::get_signing_key_in(realm)?;
+
     let key = qi
         .get()
         .by(schema::Key::Realm, &realm.id())
@@ -113,14 +79,15 @@ pub fn generate_auth_token<'a>(
         .map_err(Into::<TokenError>::into)?;
 
     Ok(jwt::JWT::sign(&kpair, token).into_string())
+    */
+    todo!()
 }
 
 pub fn generate_refresh_token<'a>(
     config: &config::Config,
-    qi: &microrm::QueryInterface,
-    realm: schema::RealmID,
-    client: schema::ClientID,
-    user: schema::UserID,
+    realm: &schema::Realm,
+    client: &schema::Client,
+    user: &schema::User,
     scopes: impl Iterator<Item = &'a str>,
 ) -> Result<String, UIDCError> {
     todo!()

+ 22 - 41
src/token_management.rs

@@ -3,67 +3,48 @@ use microrm::prelude::*;
 use ring::signature::KeyPair;
 
 pub fn create_auth_token(
-    qi: &microrm::QueryInterface,
+    realm: &schema::Realm,
     config: &Config,
-    realm_id: schema::RealmID,
-    client: &str,
-    username: &str,
-    scopes: &str,
+    client: &String,
+    username: &String,
+    scopes: &String,
 ) -> Result<String, UIDCError> {
     token::generate_auth_token(
         config,
-        qi,
-        realm_id,
-        qi.get()
-            .only_ids()
-            .by(schema::Client::Realm, &realm_id)
-            .by(schema::Client::Shortname, client)
-            .one_id()?
-            .ok_or(UIDCError::Abort("no such client"))?,
-        qi.get()
-            .only_ids()
-            .by(schema::User::Realm, &realm_id)
-            .by(schema::User::Username, username)
-            .one_id()?
-            .ok_or(UIDCError::Abort("no such user"))?,
+        realm,
+        &realm.clients.unique(client).get()?
+            .ok_or(UIDCError::Abort("no such client"))?.wrapped(),
+        &realm.users.unique(username).get()?
+            .ok_or(UIDCError::Abort("no such user"))?.wrapped(),
         scopes.split_whitespace(),
     )
 }
 
 pub fn create_refresh_token(
-    qi: &microrm::QueryInterface,
+    realm: &schema::Realm,
     config: &Config,
-    realm_id: schema::RealmID,
-    client: &str,
-    username: &str,
-    scopes: &str,
+    client: &String,
+    username: &String,
+    scopes: &String,
 ) -> Result<String, UIDCError> {
     token::generate_refresh_token(
         config,
-        qi,
-        realm_id,
-        qi.get()
-            .only_ids()
-            .by(schema::Client::Realm, &realm_id)
-            .by(schema::Client::Shortname, client)
-            .one_id()?
-            .ok_or(UIDCError::Abort("no such client"))?,
-        qi.get()
-            .only_ids()
-            .by(schema::User::Realm, &realm_id)
-            .by(schema::User::Username, username)
-            .one_id()?
-            .ok_or(UIDCError::Abort("no such user"))?,
+        realm,
+        &realm.clients.unique(client).get()?
+            .ok_or(UIDCError::Abort("no such client"))?.wrapped(),
+        &realm.users.unique(username).get()?
+            .ok_or(UIDCError::Abort("no such user"))?.wrapped(),
         scopes.split_whitespace(),
     )
 }
 
 pub fn inspect_token(
-    qi: &microrm::QueryInterface,
     config: &Config,
-    realm_id: schema::RealmID,
+    realm: &schema::Realm,
     token: Option<&str>,
 ) -> Result<(), UIDCError> {
+    todo!()
+    /*
     let key = qi
         .get()
         .by(schema::Key::Realm, &realm_id)
@@ -111,5 +92,5 @@ pub fn inspect_token(
     } else {
         println!("Claim parsing failed!");
     }
-    Ok(())
+    Ok(())*/
 }

+ 32 - 85
src/user.rs

@@ -1,4 +1,5 @@
 use crate::{schema, UIDCError};
+use microrm::schema::IDWrap;
 use microrm::prelude::*;
 
 #[derive(Debug)]
@@ -9,9 +10,9 @@ pub enum UserError {
     InvalidInput,
 }
 
-pub struct User {
-    id: schema::UserID,
-    model: Option<schema::User>,
+pub struct User<'a> {
+    realm: &'a schema::Realm,
+    user: IDWrap<schema::User>,
 }
 
 static PBKDF2_ROUNDS: std::num::NonZeroU32 = unsafe { std::num::NonZeroU32::new_unchecked(20000) };
@@ -38,60 +39,36 @@ fn generate_totp_digits(secret: &[u8], time_offset: isize) -> Result<u32, UIDCEr
     Ok(truncation % 1_000_000)
 }
 
-impl User {
-    pub fn from_model(model: microrm::WithID<schema::User>) -> Self {
-        Self {
-            id: model.id(),
-            model: Some(model.wrapped()),
-        }
-    }
-
-    pub fn from_id(id: schema::UserID) -> Self {
-        Self { id, model: None }
-    }
-
-    pub fn id(&self) -> schema::UserID {
-        self.id
+impl<'a> User<'a> {
+    pub fn from_schema(realm: &'a schema::Realm, user: IDWrap<schema::User>) -> Self {
+        Self { realm, user }
     }
 
     pub fn change_username(
-        &self,
-        qi: &microrm::QueryInterface,
-        new_name: &str,
+        &mut self,
+        new_name: &String,
     ) -> Result<(), UIDCError> {
         // check to ensure the new username isn't already in use
-        if qi
-            .get()
-            .by(schema::User::Username, new_name)
-            .one()?
-            .is_some()
-        {
+        if self.realm.users.unique(new_name).get()?.is_some() {
             Err(UIDCError::Abort("username already in use"))
         } else {
-            Ok(qi
-                .update()
-                .update(schema::User::Username, new_name)
-                .by_id(&self.id)
-                .exec()?)
+            self.user.username = new_name.clone();
+            // self.realm.users.update(&self.user);
+            Ok(())
         }
     }
 
     /// returns Ok(true) if challenge passed, Ok(false) if challenge failed, and
     /// UserError::NoSuchChallenge if challenge not found
-    pub fn verify_challenge(
+    pub fn verify_challenge_by_type(
         &self,
-        qi: &microrm::QueryInterface,
-        which: schema::AuthChallengeType,
+        challenge_type: schema::AuthChallengeType,
         response: &[u8],
     ) -> Result<bool, UIDCError> {
-        let challenge = qi
-            .get()
-            .by(schema::AuthChallenge::User, &self.id)
-            .by(schema::AuthChallenge::ChallengeType, &which)
-            .one()?
-            .ok_or(UserError::NoSuchChallenge)?;
-
-        match which {
+        let ct = challenge_type.into();
+        let challenge = self.user.auth.with(schema::AuthChallenge::ChallengeType, &ct).first().get()?.ok_or(UserError::NoSuchChallenge)?;
+
+        match challenge_type {
             schema::AuthChallengeType::Password => challenge.verify_password_challenge(response),
             schema::AuthChallengeType::TOTP => challenge.verify_totp_challenge(response),
             _ => todo!(),
@@ -100,16 +77,9 @@ impl User {
 
     pub fn set_new_password(
         &self,
-        qi: &microrm::QueryInterface,
         password: &[u8],
     ) -> Result<(), UIDCError> {
-        qi.delete()
-            .by(schema::AuthChallenge::User, &self.id)
-            .by(
-                schema::AuthChallenge::ChallengeType,
-                &schema::AuthChallengeType::Password,
-            )
-            .exec()?;
+        self.user.auth.with(schema::AuthChallenge::ChallengeType, &schema::AuthChallengeType::Password.into()).delete()?;
 
         let rng = ring::rand::SystemRandom::new();
         let salt: [u8; 16] = ring::rand::generate(&rng)
@@ -126,19 +96,18 @@ impl User {
             &mut generated,
         );
 
-        qi.add(&schema::AuthChallenge {
-            user: self.id,
-            challenge_type: schema::AuthChallengeType::Password,
+        self.user.auth.insert(schema::AuthChallenge {
+            challenge_type: schema::AuthChallengeType::Password.into(),
             public: salt.into(),
             secret: generated.into(),
             enabled: true,
         })?;
+
         Ok(())
     }
 
     pub fn generate_totp_with_uri(
         &self,
-        qi: &microrm::QueryInterface,
     ) -> Result<(Vec<u8>, String), UIDCError> {
         let rng = ring::rand::SystemRandom::new();
         let secret: [u8; 16] = ring::rand::generate(&rng)
@@ -150,50 +119,28 @@ impl User {
             secret.as_slice(),
         );
 
-        let username = qi
-            .get()
-            .by_id(&self.id)
-            .one()?
-            .ok_or(UIDCError::Abort("no such user"))?
-            .wrapped()
-            .username;
-
-        let uri = format!("otpauth://totp/uidc:{username}@uidc?secret={uri_secret}&issuer=uidc");
+        let uri = format!("otpauth://totp/uidc:{username}@uidc?secret={uri_secret}&issuer=uidc", username = self.user.username);
 
         Ok((secret.into(), uri))
     }
 
     pub fn set_new_totp(
         &self,
-        qi: &microrm::QueryInterface,
         secret: &[u8],
     ) -> Result<(), UIDCError> {
-        qi.delete()
-            .by(schema::AuthChallenge::User, &self.id)
-            .by(
-                schema::AuthChallenge::ChallengeType,
-                &schema::AuthChallengeType::TOTP,
-            )
-            .exec()?;
-
-        qi.add(&schema::AuthChallenge {
-            user: self.id,
-            challenge_type: schema::AuthChallengeType::TOTP,
+        self.clear_totp()?;
+        self.user.auth.insert(schema::AuthChallenge {
+            challenge_type: schema::AuthChallengeType::TOTP.into(),
             public: vec![],
             secret: secret.into(),
             enabled: true,
         })?;
+
         Ok(())
     }
 
-    pub fn clear_totp(&self, qi: &microrm::QueryInterface) -> Result<(), UIDCError> {
-        qi.delete()
-            .by(schema::AuthChallenge::User, &self.id)
-            .by(
-                schema::AuthChallenge::ChallengeType,
-                &schema::AuthChallengeType::TOTP,
-            )
-            .exec()?;
+    pub fn clear_totp(&self) -> Result<(), UIDCError> {
+        self.user.auth.with(schema::AuthChallenge::ChallengeType, &schema::AuthChallengeType::TOTP.into()).delete()?;
         Ok(())
     }
 }
@@ -202,7 +149,7 @@ impl schema::AuthChallenge {
     pub fn verify_password_challenge(&self, response: &[u8]) -> Result<bool, UIDCError> {
         use ring::pbkdf2;
 
-        if self.challenge_type != schema::AuthChallengeType::Password {
+        if *self.challenge_type.as_ref() != schema::AuthChallengeType::Password {
             return Err(UIDCError::Abort(
                 "verifying password challenge on non-password challenge",
             ));
@@ -224,7 +171,7 @@ impl schema::AuthChallenge {
             .parse::<u32>()
             .map_err(|_| UserError::InvalidInput)?;
 
-        if self.challenge_type != schema::AuthChallengeType::TOTP {
+        if *self.challenge_type.as_ref() != schema::AuthChallengeType::TOTP {
             return Err(UIDCError::Abort(
                 "verifying TOTP challenge on non-TOTP challenge",
             ));

+ 25 - 49
src/user_management.rs

@@ -1,14 +1,14 @@
-use crate::{schema, user, UIDCError};
+use crate::{schema, UIDCError};
 use microrm::prelude::*;
 
-pub fn list(qi: &microrm::QueryInterface, realm_id: schema::RealmID) -> Result<(), UIDCError> {
-    let users = qi.get().by(schema::User::Realm, &realm_id).all()?;
+pub fn list(realm: &schema::Realm) -> Result<(), UIDCError> {
+    let users = realm.users.get()?;
 
     println!("User list ({} users):", users.len());
 
     for user in &users {
         println!("- {:20}", user.username);
-        let auth_challenges = qi.get().by(schema::AuthChallenge::User, &user.id()).all()?;
+        let auth_challenges = user.auth.get()?;
         for ch in &auth_challenges {
             println!("    - Has {:?} authentication challenge", ch.challenge_type);
         }
@@ -18,16 +18,11 @@ pub fn list(qi: &microrm::QueryInterface, realm_id: schema::RealmID) -> Result<(
 }
 
 pub fn create(
-    qi: &microrm::QueryInterface,
-    realm_id: schema::RealmID,
-    username: &str,
+    realm: &schema::Realm,
+    username: &String,
 ) -> Result<(), UIDCError> {
     // check that the user doesn't exist already
-    let existing_user = qi
-        .get()
-        .by(schema::User::Realm, &realm_id)
-        .by(schema::User::Username, &username)
-        .one()?;
+    let existing_user = realm.users.unique(&username).get()?;
 
     if existing_user.is_some() {
         log::error!(
@@ -37,42 +32,37 @@ pub fn create(
         return Ok(());
     }
 
-    qi.add(&schema::User {
-        realm: realm_id,
-        username: username.to_owned(),
+    realm.users.insert(schema::User {
+        username: username.into(),
+        auth: Default::default(),
+        groups: Default::default(),
     })?;
+
     Ok(())
 }
 
 pub fn change_auth(
-    qi: &microrm::QueryInterface,
-    realm_id: schema::RealmID,
-    username: &str,
+    realm: &schema::Realm,
+    username: &String,
     change_password: bool,
     change_totp: bool,
 ) -> Result<(), UIDCError> {
     // check that the user exists
-    let existing_user = qi
-        .get()
-        .by(schema::User::Realm, &realm_id)
-        .by(schema::User::Username, &username)
-        .one()?
-        .ok_or(UIDCError::Abort("no such user"))?;
+    let user = realm.users.unique(username).get()?.ok_or(UIDCError::Abort("no such user"))?;
 
-    let user = user::User::from_model(existing_user);
+    let user = crate::user::User::from_schema(realm, user);
 
     if change_password {
         let raw_pass = rpassword::prompt_password("Enter new user password: ").unwrap();
-        user.set_new_password(qi, raw_pass.as_bytes())?;
+        user.set_new_password(raw_pass.as_bytes())?;
     }
     if change_totp {
-        let (new_secret, new_uri) = user.generate_totp_with_uri(qi)?;
+        let (new_secret, new_uri) = user.generate_totp_with_uri()?;
         println!("Please confirm you can generate tokens with the new secret:");
         qr2term::print_qr(new_uri.as_str())
             .map_err(|_| UIDCError::Abort("could not display QR code"))?;
         let new_challenge = schema::AuthChallenge {
-            user: user.id(),
-            challenge_type: schema::AuthChallengeType::TOTP,
+            challenge_type: schema::AuthChallengeType::TOTP.into(),
             public: vec![],
             secret: new_secret.clone(),
             enabled: true,
@@ -84,41 +74,27 @@ pub fn change_auth(
                 break;
             }
         }
-        user.set_new_totp(qi, new_secret.as_slice())?;
+        user.set_new_totp(new_secret.as_slice())?;
     }
     Ok(())
 }
 
 pub fn inspect(
-    qi: &microrm::QueryInterface,
-    realm_id: schema::RealmID,
-    username: &str,
+    realm: &schema::Realm,
+    username: &String,
 ) -> Result<(), UIDCError> {
-    let user = qi
-        .get()
-        .by(schema::User::Realm, &realm_id)
-        .by(schema::User::Username, username)
-        .one()?;
+    let user = realm.users.unique(username).get()?;
 
     if let Some(user) = user {
         println!("User found: {}", username);
         println!("Groups:");
-        for group_membership in qi
-            .get()
-            .by(schema::GroupMembership::User, &user.id())
-            .all()?
-        {
-            let group = qi
-                .get()
-                .by_id(&group_membership.group)
-                .one()?
-                .ok_or(UIDCError::Abort("reference to nonexistent group"))?;
+        for group in user.groups.get()? {
             println!(" - {}", group.shortname);
         }
 
         println!("Authentication methods:");
 
-        for challenge in qi.get().by(schema::AuthChallenge::User, &user.id()).all()? {
+        for challenge in user.auth.get()? {
             println!(" - {:?}", challenge.challenge_type);
         }
     } else {