Kestrel před 1 rokem
rodič
revize
6268e6a9df
11 změnil soubory, kde provedl 363 přidání a 154 odebrání
  1. 1 1
      .gitignore
  2. 15 4
      src/cert.rs
  3. 28 32
      src/cli.rs
  4. 1 2
      src/config.rs
  5. 1 2
      src/main.rs
  6. 7 10
      src/schema.rs
  7. 13 4
      src/server.rs
  8. 12 6
      src/server/oidc.rs
  9. 162 69
      src/server/session.rs
  10. 69 14
      src/user.rs
  11. 54 10
      src/user_management.rs

+ 1 - 1
.gitignore

@@ -1,3 +1,3 @@
 /target
-/vogt.db
+/uauth.db
 .*.sw?

+ 15 - 4
src/cert.rs

@@ -1,8 +1,8 @@
 use crate::schema;
+use microrm::prelude::*;
 use ring::signature::Ed25519KeyPair;
 use sha2::Digest;
 use std::collections::HashMap;
-use microrm::prelude::*;
 
 pub struct CertStore<'a> {
     db: &'a microrm::DB,
@@ -20,7 +20,11 @@ impl<'a> CertStore<'a> {
     }
 
     fn realm_id(&self, realm_name: &str) -> Option<schema::RealmID> {
-        self.qi.get().by(schema::Realm::Shortname, realm_name).one().expect("couldn't query db")
+        self.qi
+            .get()
+            .by(schema::Realm::Shortname, realm_name)
+            .one()
+            .expect("couldn't query db")
             .map(|x| x.id())
     }
 
@@ -58,7 +62,11 @@ pub fn inspect(db: &microrm::DB, realm_name: &str) {
     let qi = db.query_interface();
     let cs = CertStore::new(db);
     println!("Certstore loaded.");
-    let realm = qi.get().by(schema::Realm::Shortname, realm_name).one().expect("couldn't query db");
+    let realm = qi
+        .get()
+        .by(schema::Realm::Shortname, realm_name)
+        .one()
+        .expect("couldn't query db");
     if realm.is_none() {
         println!("No such realm {}", realm_name);
         return;
@@ -66,7 +74,10 @@ pub fn inspect(db: &microrm::DB, realm_name: &str) {
     let realm = realm.unwrap();
 
     println!("Retrieving keys for {} realm...", realm_name);
-    let keys = qi.get().by(schema::Key::Realm, &realm.id()).all()
+    let keys = qi
+        .get()
+        .by(schema::Key::Realm, &realm.id())
+        .all()
         .expect("Can get keys");
     for key in keys {
         println!("- [{:20}]", key.key_id);

+ 28 - 32
src/cli.rs

@@ -1,5 +1,5 @@
-use clap::{Parser,Subcommand};
-use crate::{schema,cert,user_management,server};
+use crate::{cert, schema, server, user_management};
+use clap::{Parser, Subcommand};
 
 #[derive(Debug, Parser)]
 #[clap(author, version, about, long_about = None)]
@@ -23,7 +23,7 @@ enum Command {
     Group(GroupArgs),
     Server(ServerArgs),
     Token(TokenArgs),
-    User(UserArgs)
+    User(UserArgs),
 }
 
 impl RootArgs {
@@ -32,11 +32,7 @@ impl RootArgs {
             return self.init().await;
         }
 
-        let storage = microrm::DB::new(
-            schema::schema(),
-            &self.db,
-            microrm::CreateMode::MustExist,
-        );
+        let storage = microrm::DB::new(schema::schema(), &self.db, microrm::CreateMode::MustExist);
 
         if let Err(e) = storage {
             println!("Error occured while loading database: {}", e);
@@ -56,11 +52,7 @@ impl RootArgs {
 
     async fn init(&self) {
         // first check to see if the database is already vaguely set up
-        let maybedb = microrm::DB::new(
-            schema::schema(),
-            &self.db,
-            microrm::CreateMode::MustExist,
-        );
+        let maybedb = microrm::DB::new(schema::schema(), &self.db, microrm::CreateMode::MustExist);
 
         if maybedb.is_ok() {
             println!("Database already initialized, not overwriting!");
@@ -77,9 +69,11 @@ impl RootArgs {
         .expect("Unable to initialize database!");
 
         // create primary realm
-        db.query_interface().add(&schema::Realm {
-            shortname: "primary".to_string(),
-        }).expect("couldn't add realm");
+        db.query_interface()
+            .add(&schema::Realm {
+                shortname: "primary".to_string(),
+            })
+            .expect("couldn't add realm");
     }
 }
 
@@ -108,22 +102,19 @@ impl CertArgs {
     }
 }
 
-#[derive(Debug,Subcommand)]
-enum GroupCommand {
-}
+#[derive(Debug, Subcommand)]
+enum GroupCommand {}
 
 #[derive(Debug, Parser)]
 struct GroupArgs {
     #[clap(subcommand)]
-    command: GroupCommand
+    command: GroupCommand,
 }
 
 impl GroupArgs {
-    async fn run(&self, root: &RootArgs, db: microrm::DB) {
-    }
+    async fn run(&self, root: &RootArgs, db: microrm::DB) {}
 }
 
-
 #[derive(Debug, Parser)]
 struct ServerArgs {
     port: Option<u16>,
@@ -149,21 +140,19 @@ struct TokenArgs {
 }
 
 impl TokenArgs {
-    async fn run(&self, root: &RootArgs, _db: microrm::DB) {
-        
-    }
+    async fn run(&self, root: &RootArgs, _db: microrm::DB) {}
 }
 
 #[derive(Debug, Parser)]
 struct CreateUserArgs {
-    username: String
+    username: String,
 }
 
 #[derive(Debug, Parser)]
 struct AuthUserArgs {
     username: String,
 
-    #[clap(short='p', long, parse(from_occurrences))]
+    #[clap(short = 'p', long, parse(from_occurrences))]
     change_password: usize,
 }
 
@@ -177,15 +166,22 @@ enum UserCommand {
 #[derive(Debug, Parser)]
 struct UserArgs {
     #[clap(subcommand)]
-    command: UserCommand
+    command: UserCommand,
 }
 
 impl UserArgs {
     async fn run(&self, root: &RootArgs, db: microrm::DB) {
         match &self.command {
-            UserCommand::List => { user_management::list(&root.realm, db) }
-            UserCommand::Create(args) => { user_management::create(&root.realm, db, args.username.as_str()) }
-            UserCommand::Auth(args) => { user_management::change_auth(&root.realm, db, args.username.as_str(), args.change_password > 0) }
+            UserCommand::List => user_management::list(&root.realm, db),
+            UserCommand::Create(args) => {
+                user_management::create(&root.realm, db, args.username.as_str())
+            }
+            UserCommand::Auth(args) => user_management::change_auth(
+                &root.realm,
+                db,
+                args.username.as_str(),
+                args.change_password > 0,
+            ),
         }
     }
 }

+ 1 - 2
src/config.rs

@@ -6,7 +6,7 @@ use serde::Deserialize;
 pub enum Flow {
     Challenge(crate::schema::AuthChallengeType),
     OneOf(Vec<Flow>),
-    AllOf(Vec<Flow>)
+    AllOf(Vec<Flow>),
 }
 
 #[derive(Deserialize)]
@@ -19,5 +19,4 @@ pub struct AuthConfig {
 #[derive(Deserialize)]
 pub struct GlobalConfig {
     auth: AuthConfig,
-
 }

+ 1 - 2
src/main.rs

@@ -1,12 +1,11 @@
 mod cert;
 mod cli;
+mod config;
 mod login;
 mod schema;
 mod server;
 mod user;
 mod user_management;
-mod config;
-
 
 fn main() {
     stderrlog::new()

+ 7 - 10
src/schema.rs

@@ -1,4 +1,4 @@
-pub use microrm::{Schema, Entity, Modelable};
+pub use microrm::{Entity, Modelable, Schema};
 use serde::{Deserialize, Serialize};
 
 #[derive(Debug, Entity, Serialize, Deserialize)]
@@ -9,6 +9,7 @@ pub struct Session {
 
 microrm::make_index!(!SessionKeyIndex, Session::Key);
 
+/// Authentication state for a session. If no challenges are left, it's considered authorized.
 #[derive(Debug, Entity, Serialize, Deserialize)]
 pub struct SessionAuthentication {
     #[microrm_foreign]
@@ -73,7 +74,7 @@ pub struct Group {
 }
 
 /// User membership in group
-#[derive(Debug, Entity,Serialize,Deserialize)]
+#[derive(Debug, Entity, Serialize, Deserialize)]
 pub struct GroupMembership {
     pub group: GroupID,
     pub user: UserID,
@@ -88,11 +89,7 @@ pub struct Client {
     pub secret: String,
 }
 
-microrm::make_index!(
-    !ClientNameIndex,
-    Client::Realm,
-    Client::Shortname
-);
+microrm::make_index!(!ClientNameIndex, Client::Realm, Client::Shortname);
 
 #[derive(Debug, Entity, Serialize, Deserialize)]
 pub struct ClientRedirect {
@@ -118,20 +115,20 @@ pub struct Role {
 }
 
 /// Role membership in scope
-#[derive(Debug, Entity,Serialize,Deserialize)]
+#[derive(Debug, Entity, Serialize, Deserialize)]
 pub struct ScopeRole {
     pub scope: ScopeID,
     pub role: RoleID,
 }
 
 /// Assigned permissions in group
-#[derive(Debug, Entity,Serialize,Deserialize)]
+#[derive(Debug, Entity, Serialize, Deserialize)]
 pub struct GroupRole {
     pub scope: ScopeID,
     pub role: RoleID,
 }
 
-#[derive(Debug, Entity,Serialize,Deserialize)]
+#[derive(Debug, Entity, Serialize, Deserialize)]
 pub struct RevokedToken {
     pub user: UserID,
     pub nonce: String,

+ 13 - 4
src/server.rs

@@ -1,8 +1,8 @@
 use crate::schema;
 use microrm::prelude::*;
 
-mod session;
 mod oidc;
+mod session;
 
 pub struct ServerCoreState {
     pool: microrm::DBPool<'static>,
@@ -21,7 +21,10 @@ fn is_auth_valid<T>(core: &'static ServerCoreState, of: &tide::Request<T>) -> Op
     Some(
         core.pool
             .query_interface()
-            .get().by(schema::Session::Key, &session_id).one().expect("couldn't query db")
+            .get()
+            .by(schema::Session::Key, &session_id)
+            .one()
+            .expect("couldn't query db")
             .is_some(),
     )
 }
@@ -31,12 +34,18 @@ pub async fn run_server(db: microrm::DB, port: u16) {
     let db: &'static mut microrm::DB = Box::leak(db_box);
     let pool = microrm::DBPool::new(db);
 
-    let core_state = Box::leak(Box::new(ServerCoreState { pool, templates: handlebars::Handlebars::new() }));
+    let core_state = Box::leak(Box::new(ServerCoreState {
+        pool,
+        templates: handlebars::Handlebars::new(),
+    }));
 
     // XXX: for development only
     // core_state.templates.write().unwrap().set_dev_mode(true);
 
-    core_state.templates.register_templates_directory(".tmpl", "tmpl/").expect("Couldn't open templates directory?");
+    core_state
+        .templates
+        .register_templates_directory(".tmpl", "tmpl/")
+        .expect("Couldn't open templates directory?");
     println!("registered templates:");
     for tmpl in core_state.templates.get_templates() {
         println!("- {}", tmpl.0);

+ 12 - 6
src/server/oidc.rs

@@ -1,6 +1,6 @@
 use crate::schema;
-use serde::{Deserialize, Serialize};
 use microrm::prelude::*;
+use serde::{Deserialize, Serialize};
 
 #[derive(Clone)]
 pub struct ServerState {
@@ -76,10 +76,11 @@ fn do_authorize(request: Request, state: Option<&str>) -> Result<tide::Response,
 
     // verify the realm and client_id and redirect_uri
     let qi = request.state().core.pool.query_interface();
-    let realm = qi.get().by(
-        schema::Realm::Shortname,
-        &request.param("realm").unwrap(),
-    ).one().expect("couldn't query db");
+    let realm = qi
+        .get()
+        .by(schema::Realm::Shortname, &request.param("realm").unwrap())
+        .one()
+        .expect("couldn't query db");
     if realm.is_none() {
         return Err(OIDCError(
             OIDCErrorType::InvalidRequest,
@@ -89,7 +90,12 @@ fn do_authorize(request: Request, state: Option<&str>) -> Result<tide::Response,
     }
     let realm = realm.unwrap();
 
-    let client = qi.get().by(schema::Client::Realm, &realm.id()).by(schema::Client::Shortname, &qp.client_id).one().expect("couldn't query db");
+    let client = qi
+        .get()
+        .by(schema::Client::Realm, &realm.id())
+        .by(schema::Client::Shortname, &qp.client_id)
+        .one()
+        .expect("couldn't query db");
     if client.is_none() {
         return Err(OIDCError(
             OIDCErrorType::UnauthorizedClient,

+ 162 - 69
src/server/session.rs

@@ -1,43 +1,52 @@
-use crate::{schema,user};
+use crate::{schema, user};
+use microrm::prelude::*;
 use serde::Deserialize;
 use tide::http::Cookie;
-use microrm::prelude::*;
 
 #[derive(Clone)]
 pub struct ServerState {
     core: &'static super::ServerCoreState,
-    realm_cache: std::sync::Arc<std::sync::RwLock<std::collections::HashMap<String, schema::RealmID>>>
+    realm_cache:
+        std::sync::Arc<std::sync::RwLock<std::collections::HashMap<String, schema::RealmID>>>,
 }
 
 type Request = tide::Request<ServerState>;
 
 impl ServerState {
     pub fn get_realm(&self, req: &Request) -> Option<schema::RealmID> {
-        let realm_str = req.param("realm").expect("get_realm called with no :realm param");
+        let realm_str = req
+            .param("realm")
+            .expect("get_realm called with no :realm param");
         let cache = self.realm_cache.read().unwrap();
         let cache_lookup = cache.get(realm_str);
 
         // expected case
-        if cache_lookup.is_some() { return cache_lookup.map(|x| *x) }
+        if cache_lookup.is_some() {
+            return cache_lookup.map(|x| *x);
+        }
         drop(cache);
 
         // unexpected case, but maybe we haven't filled that cache entry yet
-        
+
         let qi = self.core.pool.query_interface();
-        let realm = qi.get().by(schema::Realm::Shortname, realm_str).one().expect("couldn't query db");
+        let realm = qi
+            .get()
+            .by(schema::Realm::Shortname, realm_str)
+            .one()
+            .expect("couldn't query db");
 
         if let Some(with_id) = realm {
             let mut cache = self.realm_cache.write().unwrap();
             cache.insert(realm_str.to_owned(), with_id.id());
-            return Some(with_id.id())
+            return Some(with_id.id());
         }
 
         // other expected case, is bogus realm
-        return None
+        return None;
     }
 
     fn build_session(
-        qi: &microrm::QueryInterface
+        qi: &microrm::QueryInterface,
     ) -> tide::Result<(schema::SessionID, Option<tide::http::Cookie<'static>>)> {
         let rng = ring::rand::SystemRandom::new();
         let session_id: [u8; 32] = ring::rand::generate(&rng)
@@ -45,89 +54,144 @@ impl ServerState {
             .expose();
         let session_id = base64::encode_config(session_id, base64::URL_SAFE_NO_PAD);
 
-        let maybe_id = qi.add(&schema::Session { key: session_id.clone() });
-        Ok((maybe_id.ok().ok_or(tide::Error::from_str(
-            500,
-            "Failed to store session in database",
-        ))?, Some(Cookie::new("vogt_session", session_id))))
+        let maybe_id = qi.add(&schema::Session {
+            key: session_id.clone(),
+        });
+        Ok((
+            maybe_id.ok().ok_or(tide::Error::from_str(
+                500,
+                "Failed to store session in database",
+            ))?,
+            Some(Cookie::new("vogt_session", session_id)),
+        ))
+    }
+
+    pub fn verify_session(&self, req: &Request) -> Option<(schema::RealmID, schema::UserID)> {
+        self.get_or_build_session(req)
+            .ok()
+            .zip(self.get_realm(req))
+            .and_then(|((sid, _cookie), realm)| {
+                self.get_auth_for_session(realm, sid).and_then(|auth| {
+                    if auth.challenges_left.len() == 0 {
+                        Some((realm, auth.user))
+                    } else {
+                        None
+                    }
+                })
+            })
     }
 
-    pub fn get_or_build_session(&self, req: &Request) -> tide::Result<(schema::SessionID, Option<tide::http::Cookie<'static>>)> {
+    pub fn get_or_build_session(
+        &self,
+        req: &Request,
+    ) -> tide::Result<(schema::SessionID, Option<tide::http::Cookie<'static>>)> {
         let qi = self.core.pool.query_interface();
         if let Some(sid) = req.cookie("vogt_session") {
-            let existing = qi.get().by(schema::Session::Key, sid.value()).one().expect("couldn't query db");
+            let existing = qi
+                .get()
+                .by(schema::Session::Key, sid.value())
+                .one()
+                .expect("couldn't query db");
 
             if existing.is_some() {
-                return Ok((existing.unwrap().id(), None))
+                return Ok((existing.unwrap().id(), None));
             }
         }
         Self::build_session(qi)
     }
 
-    pub fn get_auth_for_session(&self, realm: schema::RealmID, session: schema::SessionID) -> Option<microrm::WithID<schema::SessionAuthentication>> {
+    pub fn get_auth_for_session(
+        &self,
+        realm: schema::RealmID,
+        session: schema::SessionID,
+    ) -> Option<microrm::WithID<schema::SessionAuthentication>> {
         let qi = self.core.pool.query_interface();
 
         use schema::SessionAuthentication as SAC;
-        qi.get().by(SAC::Realm, &realm).by(SAC::Session, &session).one().expect("couldn't query db")
-        // qi.get_one_by_multi(&[&SAC::Realm, &SAC::Session], &microrm::value_list!(realm, session))
+        qi.get()
+            .by(SAC::Realm, &realm)
+            .by(SAC::Session, &session)
+            .one()
+            .expect("couldn't query db")
     }
 
     pub fn destroy_auth(&self, realm: schema::RealmID, session: schema::SessionID) {
         let qi = self.core.pool.query_interface();
-        
+
         use schema::SessionAuthentication as SAC;
-        qi.delete().by(SAC::Realm, &realm).by(SAC::Session, &session).exec().expect("couldn't query db")
+        qi.delete()
+            .by(SAC::Realm, &realm)
+            .by(SAC::Session, &session)
+            .exec()
+            .expect("couldn't query db")
     }
 }
 
 impl ServerState {
-    fn render_login_from_auth(&self, mut response: tide::Response, auth: Option<schema::SessionAuthentication>, error_msg: Option<String>) -> tide::Response {
+    fn render_login_from_auth(
+        &self,
+        mut response: tide::Response,
+        auth: Option<schema::SessionAuthentication>,
+        error_msg: Option<String>,
+    ) -> tide::Response {
         log::info!("rendering login response... auth is {:?}", auth);
 
         let to_present: Option<schema::AuthChallengeType> = match auth {
             None => Some(schema::AuthChallengeType::Username),
-            Some(auth) => auth.challenges_left.first().copied()
+            Some(auth) => auth.challenges_left.first().copied(),
         };
 
         if to_present.is_none() {
             response.set_status(302);
             tide::Redirect::new("/").into()
-        }
-        else {
+        } else {
             self.render_login_page(response, to_present.unwrap(), error_msg)
         }
     }
 
-    fn render_login_page(&self, mut response: tide::Response, to_present: schema::AuthChallengeType, error_msg: Option<String>) -> tide::Response {
+    fn render_login_page(
+        &self,
+        mut response: tide::Response,
+        to_present: schema::AuthChallengeType,
+        error_msg: Option<String>,
+    ) -> tide::Response {
         let tmpl = &self.core.templates;
 
-        let do_challenge = |ty,ch| {
-            tmpl.render("id_v1_login", &serde_json::json!(
-                {
-                    "challenge":
-                        format!(r#"
+        let do_challenge = |ty, ch| {
+            tmpl.render(
+                "id_v1_login",
+                &serde_json::json!(
+                    {
+                        "challenge":
+                            format!(r#"
                             <input type="hidden" name="challenge_type" value="{:?}" />
                             <div class="challenge-type">{}</div>
                             <div class="challenge-content">{}</div>
                             "#,
-                            to_present, ty, ch),
-                    "error_msg": error_msg.iter().collect::<Vec<_>>()
-                }
-            )).unwrap()
+                                to_present, ty, ch),
+                        "error_msg": error_msg.iter().collect::<Vec<_>>()
+                    }
+                ),
+            )
+            .unwrap()
         };
 
         response.set_content_type("text/html");
 
         match to_present {
             schema::AuthChallengeType::Username => {
-                response.set_body(do_challenge("Username",
-                    r#"<input name="challenge" type="text" autofocus />"#));
-            },
+                response.set_body(do_challenge(
+                    "Username",
+                    r#"<input name="challenge" type="text" autofocus />"#,
+                ));
+            }
             schema::AuthChallengeType::Password => {
-                response.set_body(do_challenge("Password",
-                    r#"<input name="challenge" type="password" autofocus />"#));
-            },
-            _ => todo!()
+                response.set_body(do_challenge(
+                    "Password",
+                    r#"<input name="challenge" type="password" autofocus />"#,
+                ));
+            }
+            _ => todo!(),
         }
 
         response
@@ -137,19 +201,27 @@ impl ServerState {
 async fn v1_login(req: tide::Request<ServerState>) -> tide::Result<tide::Response> {
     let mut response = tide::Response::builder(200).build();
 
-    let realm = req.state().get_realm(&req).ok_or(tide::Error::from_str(404, "No such realm"))?;
+    let realm = req
+        .state()
+        .get_realm(&req)
+        .ok_or(tide::Error::from_str(404, "No such realm"))?;
     let (session_id, cookie) = req.state().get_or_build_session(&req)?;
     cookie.map(|c| response.insert_cookie(c));
 
     let auth = req.state().get_auth_for_session(realm, session_id);
 
-    Ok(req.state().render_login_from_auth(response, auth.map(|a| a.wrapped()), None))
+    Ok(req
+        .state()
+        .render_login_from_auth(response, auth.map(|a| a.wrapped()), None))
 }
 
 async fn v1_login_post(mut req: tide::Request<ServerState>) -> tide::Result<tide::Response> {
     let mut response = tide::Response::builder(200).build();
 
-    let realm = req.state().get_realm(&req).ok_or(tide::Error::from_str(404, "No such realm"))?;
+    let realm = req
+        .state()
+        .get_realm(&req)
+        .ok_or(tide::Error::from_str(404, "No such realm"))?;
     let (session_id, cookie) = req.state().get_or_build_session(&req)?;
     cookie.map(|c| response.insert_cookie(c));
 
@@ -162,14 +234,14 @@ async fn v1_login_post(mut req: tide::Request<ServerState>) -> tide::Result<tide
         reset: Option<String>,
     }
 
-    let body : ResponseBody = req.body_form().await?;
+    let body: ResponseBody = req.body_form().await?;
 
     // check if a login reset was requested; if so, we start again from the top
     if body.reset.is_some() {
         if let Some(_) = auth {
             req.state().destroy_auth(realm, session_id);
             response.set_status(302);
-            return Ok(tide::Redirect::new("login").into())
+            return Ok(tide::Redirect::new("login").into());
         }
     }
 
@@ -178,7 +250,7 @@ async fn v1_login_post(mut req: tide::Request<ServerState>) -> tide::Result<tide
     let challenge: schema::AuthChallengeType = match body.challenge_type.as_str() {
         "Username" => ChallengeType::Username,
         "Password" => ChallengeType::Password,
-        _ => Err(tide::Error::from_str(400, "Unknown challenge type"))?
+        _ => Err(tide::Error::from_str(400, "Unknown challenge type"))?,
     };
 
     let mut error = None;
@@ -186,7 +258,7 @@ async fn v1_login_post(mut req: tide::Request<ServerState>) -> tide::Result<tide
     // check that the response matches what we're expecting next
     let to_be_presented: Option<schema::AuthChallengeType> = match &auth {
         None => Some(schema::AuthChallengeType::Username),
-        Some(auth) => auth.challenges_left.first().copied()
+        Some(auth) => auth.challenges_left.first().copied(),
     };
 
     if to_be_presented != Some(challenge) {
@@ -198,19 +270,28 @@ async fn v1_login_post(mut req: tide::Request<ServerState>) -> tide::Result<tide
             let qi = req.state().core.pool.query_interface();
             req.state().destroy_auth(realm, session_id);
 
-            let user = qi.get().by(schema::User::Realm, &realm).by(schema::User::Username, &body.challenge).one().expect("couldn't query db");
+            let user = qi
+                .get()
+                .by(schema::User::Realm, &realm)
+                .by(schema::User::Username, &body.challenge)
+                .one()
+                .expect("couldn't query db");
             if user.is_none() {
                 error = Some(format!("No such user {}", body.challenge));
-            }
-            else {
+            } else {
                 let user = user.unwrap();
 
                 // TODO: set list of challenges to be whatever else this user has set up
-                let sa = schema::SessionAuthentication { session: session_id, realm: realm, user: user.id(), challenges_left: vec![schema::AuthChallengeType::Password] };
+                let sa = schema::SessionAuthentication {
+                    session: session_id,
+                    realm: realm,
+                    user: user.id(),
+                    challenges_left: vec![schema::AuthChallengeType::Password],
+                };
                 let id = qi.add(&sa).unwrap();
                 auth = Some(microrm::WithID::new(sa, id));
             }
-        },
+        }
         ct => {
             if let Some(auth) = &mut auth {
                 let qi = req.state().core.pool.query_interface();
@@ -225,31 +306,40 @@ async fn v1_login_post(mut req: tide::Request<ServerState>) -> tide::Result<tide
                     match verification {
                         Some(true) => {
                             auth.challenges_left.remove(0);
-                            qi.update().to(auth.as_ref()).by_id(&auth.id()).exec().expect("couldn't update auth status?");
-                        },
+                            qi.update()
+                                .to(auth.as_ref())
+                                .by_id(&auth.id())
+                                .exec()
+                                .expect("couldn't update auth status?");
+                        }
                         Some(false) => {
                             error = Some("Incorrect response. Please try again".into());
-                        },
+                        }
                         None => {
-                            error = Some("User no longer exists. Please contact an administrator.".into());
-                        },
+                            error = Some(
+                                "User no longer exists. Please contact an administrator.".into(),
+                            );
+                        }
                     }
-                }
-                else {
+                } else {
                     error = Some(format!("User is not configured correctly: either it was deleted or it lacks a required authentication challenge type. Please contact an administrator."));
                 }
-            }
-            else {
+            } else {
                 error = Some(format!("Please restart login process."));
             }
-        },
+        }
     };
 
-    Ok(req.state().render_login_from_auth(response, auth.map(|a| a.wrapped()), error))
+    Ok(req
+        .state()
+        .render_login_from_auth(response, auth.map(|a| a.wrapped()), error))
 }
 
 async fn v1_logout(req: tide::Request<ServerState>) -> tide::Result<tide::Response> {
-    let realm = req.state().get_realm(&req).ok_or(tide::Error::from_str(404, "No such realm"))?;
+    let realm = req
+        .state()
+        .get_realm(&req)
+        .ok_or(tide::Error::from_str(404, "No such realm"))?;
     let (session_id, _) = req.state().get_or_build_session(&req)?;
 
     req.state().destroy_auth(realm, session_id);
@@ -257,7 +347,10 @@ async fn v1_logout(req: tide::Request<ServerState>) -> tide::Result<tide::Respon
 }
 
 pub fn session_v1_server(core: &'static super::ServerCoreState) -> tide::Server<ServerState> {
-    let mut srv = tide::with_state(ServerState { core, realm_cache: std::sync::Arc::new(std::sync::RwLock::new(std::collections::HashMap::new())) });
+    let mut srv = tide::with_state(ServerState {
+        core,
+        realm_cache: std::sync::Arc::new(std::sync::RwLock::new(std::collections::HashMap::new())),
+    });
 
     srv.with(tide::log::LogMiddleware::new());
 

+ 69 - 14
src/user.rs

@@ -3,48 +3,103 @@ use microrm::prelude::*;
 
 pub struct User {
     id: schema::UserID,
-    model: Option<schema::User>
+    model: Option<schema::User>,
 }
 
 static PBKDF2_ROUNDS: std::num::NonZeroU32 = unsafe { std::num::NonZeroU32::new_unchecked(20000) };
 
 impl User {
     pub fn from_model(model: microrm::WithID<schema::User>) -> Self {
-        Self { id: model.id(), model: Some(model.wrapped()) }
+        Self {
+            id: model.id(),
+            model: Some(model.wrapped()),
+        }
     }
 
     /// returns Some(true) if challenge passed, Some(false) if challenge failed, and None if
     /// challenge not found
-    pub fn verify_challenge(&self, qi: &microrm::QueryInterface, which: schema::AuthChallengeType, response: &[u8]) -> Option<bool> {
-        let challenge = qi.get().by(schema::AuthChallenge::User, &self.id).by(schema::AuthChallenge::ChallengeType, &which).one().expect("couldn't query db")?;
+    pub fn verify_challenge(
+        &self,
+        qi: &microrm::QueryInterface,
+        which: schema::AuthChallengeType,
+        response: &[u8],
+    ) -> Option<bool> {
+        let challenge = qi
+            .get()
+            .by(schema::AuthChallenge::User, &self.id)
+            .by(schema::AuthChallenge::ChallengeType, &which)
+            .one()
+            .expect("couldn't query db")?;
 
         match which {
-            schema::AuthChallengeType::Password => self.verify_password_challenge(challenge.wrapped(), response),
-            schema::AuthChallengeType::TOTP => self.verify_totp_challenge(challenge.wrapped(), response),
-            _ => todo!()
+            schema::AuthChallengeType::Password => {
+                self.verify_password_challenge(challenge.wrapped(), response)
+            }
+            schema::AuthChallengeType::TOTP => {
+                self.verify_totp_challenge(challenge.wrapped(), response)
+            }
+            _ => todo!(),
         }
     }
 
     pub fn set_new_password(&self, qi: &microrm::QueryInterface, password: &[u8]) {
-        qi.delete().by(schema::AuthChallenge::User, &self.id).by(schema::AuthChallenge::ChallengeType, &schema::AuthChallengeType::Password).exec().expect("couldn't query db");
+        qi.delete()
+            .by(schema::AuthChallenge::User, &self.id)
+            .by(
+                schema::AuthChallenge::ChallengeType,
+                &schema::AuthChallengeType::Password,
+            )
+            .exec()
+            .expect("couldn't query db");
 
         let rng = ring::rand::SystemRandom::new();
-        let salt: [u8; 16] = ring::rand::generate(&rng).expect("Couldn't generate random salt?").expose();
+        let salt: [u8; 16] = ring::rand::generate(&rng)
+            .expect("Couldn't generate random salt?")
+            .expose();
 
         let mut generated = [0u8; ring::digest::SHA256_OUTPUT_LEN];
 
-        ring::pbkdf2::derive(ring::pbkdf2::PBKDF2_HMAC_SHA256, PBKDF2_ROUNDS, &salt, password, &mut generated);
+        ring::pbkdf2::derive(
+            ring::pbkdf2::PBKDF2_HMAC_SHA256,
+            PBKDF2_ROUNDS,
+            &salt,
+            password,
+            &mut generated,
+        );
 
-        qi.add(&schema::AuthChallenge { user: self.id, challenge_type: schema::AuthChallengeType::Password, public: salt.into(), secret: generated.into()}).expect("couldn't set password");
+        qi.add(&schema::AuthChallenge {
+            user: self.id,
+            challenge_type: schema::AuthChallengeType::Password,
+            public: salt.into(),
+            secret: generated.into(),
+        })
+        .expect("couldn't set password");
     }
 
-    fn verify_password_challenge(&self, challenge: schema::AuthChallenge, response: &[u8]) -> Option<bool> {
+    fn verify_password_challenge(
+        &self,
+        challenge: schema::AuthChallenge,
+        response: &[u8],
+    ) -> Option<bool> {
         use ring::pbkdf2;
 
-        Some(pbkdf2::verify(pbkdf2::PBKDF2_HMAC_SHA256, PBKDF2_ROUNDS, challenge.public.as_slice(), response, challenge.secret.as_slice()).is_ok())
+        Some(
+            pbkdf2::verify(
+                pbkdf2::PBKDF2_HMAC_SHA256,
+                PBKDF2_ROUNDS,
+                challenge.public.as_slice(),
+                response,
+                challenge.secret.as_slice(),
+            )
+            .is_ok(),
+        )
     }
 
-    fn verify_totp_challenge(&self, challenge: schema::AuthChallenge, response: &[u8]) -> Option<bool> {
+    fn verify_totp_challenge(
+        &self,
+        challenge: schema::AuthChallenge,
+        response: &[u8],
+    ) -> Option<bool> {
         todo!()
     }
 }

+ 54 - 10
src/user_management.rs

@@ -1,19 +1,33 @@
-use crate::{schema,user};
+use crate::{schema, user};
 use microrm::prelude::*;
 
 pub fn list(realm: &str, db: microrm::DB) {
     // get realm ID
     let qi = db.query_interface();
 
-    let realm_id = qi.get().by(schema::Realm::Shortname, realm).one().expect("couldn't query db").expect("No such realm").id();
+    let realm_id = qi
+        .get()
+        .by(schema::Realm::Shortname, realm)
+        .one()
+        .expect("couldn't query db")
+        .expect("No such realm")
+        .id();
 
-    let users = qi.get().by(schema::User::Realm, &realm_id).all().expect("couldn't query db");
+    let users = qi
+        .get()
+        .by(schema::User::Realm, &realm_id)
+        .all()
+        .expect("couldn't query db");
 
     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().expect("Can't get authentication challenges?");
+        let auth_challenges = qi
+            .get()
+            .by(schema::AuthChallenge::User, &user.id())
+            .all()
+            .expect("Can't get authentication challenges?");
         for ch in &auth_challenges {
             println!("    - Has {:?} authentication challenge", ch.challenge_type);
         }
@@ -24,26 +38,56 @@ pub fn create(realm: &str, db: microrm::DB, username: &str) {
     // get realm ID
     let qi = db.query_interface();
 
-    let realm_id = qi.get().by(schema::Realm::Shortname, realm).one().expect("couldn't query db").expect("No such realm").id();
+    let realm_id = qi
+        .get()
+        .by(schema::Realm::Shortname, realm)
+        .one()
+        .expect("couldn't query db")
+        .expect("No such realm")
+        .id();
 
     // 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().expect("couldn't query db");
+    let existing_user = qi
+        .get()
+        .by(schema::User::Realm, &realm_id)
+        .by(schema::User::Username, &username)
+        .one()
+        .expect("couldn't query db");
 
     if existing_user.is_some() {
-        log::error!("Can't create user {} in {} realm as a user with that username already exists", username, realm);
+        log::error!(
+            "Can't create user {} in {} realm as a user with that username already exists",
+            username,
+            realm
+        );
         return;
     }
 
-    qi.add(&schema::User { realm: realm_id, username: username.to_owned() }).expect("couldn't add user");
+    qi.add(&schema::User {
+        realm: realm_id,
+        username: username.to_owned(),
+    })
+    .expect("couldn't add user");
 }
 
 pub fn change_auth(realm: &str, db: microrm::DB, username: &str, change_password: bool) {
     // get realm ID
     let qi = db.query_interface();
 
-    let realm_id = qi.get().by(schema::Realm::Shortname, realm).one().expect("couldn't query db").expect("No such realm").id();
+    let realm_id = qi
+        .get()
+        .by(schema::Realm::Shortname, realm)
+        .one()
+        .expect("couldn't query db")
+        .expect("No such realm")
+        .id();
     // check that the user exists
-    let existing_user = qi.get().by(schema::User::Realm, &realm_id).by(schema::User::Username, &username).one().expect("couldn't query db");
+    let existing_user = qi
+        .get()
+        .by(schema::User::Realm, &realm_id)
+        .by(schema::User::Username, &username)
+        .one()
+        .expect("couldn't query db");
     if existing_user.is_none() {
         log::error!("User {} does not exist in the {} realm!", username, realm);
         return;