Преглед на файлове

Refactoring user management code out of user_management.rs.

Kestrel преди 2 години
родител
ревизия
8eec16eb2a
променени са 10 файла, в които са добавени 129 реда и са изтрити 52 реда
  1. 17 0
      Cargo.lock
  2. 3 3
      src/cert.rs
  3. 23 0
      src/config.rs
  4. 2 0
      src/main.rs
  5. 6 6
      src/schema.rs
  6. 4 6
      src/server.rs
  7. 3 3
      src/server/oidc.rs
  8. 13 12
      src/server/session.rs
  9. 49 0
      src/user.rs
  10. 9 22
      src/user_management.rs

+ 17 - 0
Cargo.lock

@@ -1078,11 +1078,28 @@ name = "microrm-macros"
 version = "0.2.1"
 dependencies = [
  "convert_case",
+ "nom",
  "proc-macro2",
  "quote",
  "syn",
 ]
 
+[[package]]
+name = "minimal-lexical"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a"
+
+[[package]]
+name = "nom"
+version = "7.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a8903e5a29a317527874d0402f867152a3d21c908bb0b933e416c65e301d4c36"
+dependencies = [
+ "memchr",
+ "minimal-lexical",
+]
+
 [[package]]
 name = "num-integer"
 version = "0.1.45"

+ 3 - 3
src/cert.rs

@@ -20,7 +20,7 @@ impl<'a> CertStore<'a> {
 
     fn realm_id(&self, realm_name: &str) -> Option<schema::RealmID> {
         self.qi
-            .get_one_by(schema::RealmColumns::Shortname, realm_name)
+            .get_one_by(schema::Realm::Shortname, realm_name)
             .map(|x| x.id())
     }
 
@@ -58,7 +58,7 @@ 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_one_by(schema::RealmColumns::Shortname, realm_name);
+    let realm = qi.get_one_by(schema::Realm::Shortname, realm_name);
     if realm.is_none() {
         println!("No such realm {}", realm_name);
         return;
@@ -67,7 +67,7 @@ pub fn inspect(db: &microrm::DB, realm_name: &str) {
 
     println!("Retrieving keys for {} realm...", realm_name);
     let keys = qi
-        .get_all_by(schema::KeyColumns::Realm, realm.id())
+        .get_all_by(schema::Key::Realm, realm.id())
         .expect("Can get keys");
     for key in keys {
         println!("[{:20}]", key.key_id);

+ 23 - 0
src/config.rs

@@ -0,0 +1,23 @@
+#![allow(dead_code)]
+
+use serde::Deserialize;
+
+#[derive(Deserialize)]
+pub enum Flow {
+    Challenge(crate::schema::AuthChallengeType),
+    OneOf(Vec<Flow>),
+    AllOf(Vec<Flow>)
+}
+
+#[derive(Deserialize)]
+pub struct AuthConfig {
+    pbkdf2_rounds: usize,
+
+    login_flow: Flow,
+}
+
+#[derive(Deserialize)]
+pub struct GlobalConfig {
+    auth: AuthConfig,
+
+}

+ 2 - 0
src/main.rs

@@ -3,7 +3,9 @@ mod cli;
 mod login;
 mod schema;
 mod server;
+mod user;
 mod user_management;
+mod config;
 
 fn main() {
     stderrlog::new()

+ 6 - 6
src/schema.rs

@@ -1,4 +1,4 @@
-pub use microrm::{model::SchemaModel, Entity, Modelable};
+pub use microrm::{schema::Schema, Entity, Modelable};
 use serde::{Deserialize, Serialize};
 
 #[derive(Entity, Serialize, Deserialize)]
@@ -7,7 +7,7 @@ pub struct Session {
     // TODO: add expiry here
 }
 
-microrm::make_index!(!SessionKeyIndex, SessionColumns::Key);
+microrm::make_index!(!SessionKeyIndex, Session::Key);
 
 #[derive(Entity, Serialize, Deserialize)]
 pub struct SessionAuthentication {
@@ -80,8 +80,8 @@ pub struct Client {
 
 microrm::make_index!(
     !ClientNameIndex,
-    ClientColumns::Realm,
-    ClientColumns::Shortname
+    Client::Realm,
+    Client::Shortname
 );
 
 #[derive(Entity, Serialize, Deserialize)]
@@ -107,8 +107,8 @@ pub struct Role {
     pub shortname: String,
 }
 
-pub fn schema() -> SchemaModel {
-    microrm::model::SchemaModel::new()
+pub fn schema() -> Schema {
+    Schema::new()
         .entity::<Session>()
         .index::<SessionKeyIndex>()
         .entity::<SessionAuthentication>()

+ 4 - 6
src/server.rs

@@ -1,6 +1,4 @@
-use crate::schema;
-
-mod identity;
+mod session;
 mod oidc;
 
 pub struct ServerCoreState {
@@ -20,7 +18,7 @@ fn is_auth_valid<T>(core: &'static ServerCoreState, of: &tide::Request<T>) -> Op
     Some(
         core.pool
             .query_interface()
-            .get_one_by(crate::schema::SessionColumns::Key, session_id)
+            .get_one_by(crate::schema::Session::Key, session_id)
             .is_some(),
     )
 }
@@ -52,8 +50,8 @@ pub async fn run_server(db: microrm::DB) {
         .serve_dir("static/")
         .expect("Can't serve static files");
 
-    app.at("/:realm/v1/id/")
-        .nest(identity::id_v1_server(core_state));
+    app.at("/:realm/v1/session/")
+        .nest(session::session_v1_server(core_state));
     app.at("/:realm/v1/oidc")
         .nest(oidc::oidc_v1_server(core_state));
 

+ 3 - 3
src/server/oidc.rs

@@ -76,7 +76,7 @@ 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_one_by(
-        schema::RealmColumns::Shortname,
+        schema::Realm::Shortname,
         request.param("realm").unwrap(),
     );
     if realm.is_none() {
@@ -90,8 +90,8 @@ fn do_authorize(request: Request, state: Option<&str>) -> Result<tide::Response,
 
     let client = qi.get_one_by_multi(
         &[
-            schema::ClientColumns::Realm,
-            schema::ClientColumns::Shortname,
+            &schema::Client::Realm,
+            &schema::Client::Shortname,
         ],
         &microrm::value_list!(&realm.id(), &qp.client_id),
     );

+ 13 - 12
src/server/identity.rs → src/server/session.rs

@@ -23,7 +23,7 @@ impl ServerState {
         // unexpected case, but maybe we haven't filled that cache entry yet
         
         let qi = self.core.pool.query_interface();
-        let realm = qi.get_one_by(schema::RealmColumns::Shortname, realm_str);
+        let realm = qi.get_one_by(schema::Realm::Shortname, realm_str);
 
         if let Some(with_id) = realm {
             let mut cache = self.realm_cache.write().unwrap();
@@ -54,7 +54,7 @@ impl ServerState {
     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_one_by(schema::SessionColumns::Key, sid.value());
+            let existing = qi.get_one_by(schema::Session::Key, sid.value());
 
             if existing.is_some() {
                 return Ok((existing.unwrap().id(), None))
@@ -66,27 +66,28 @@ impl ServerState {
     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::SessionAuthenticationColumns as SAC;
-        qi.get_one_by_multi(&[SAC::Realm, SAC::Session], &microrm::value_list!(&realm, &session))
+        use schema::SessionAuthentication as SAC;
+        qi.get_one_by_multi(&[&SAC::Realm, &SAC::Session], &microrm::value_list!(realm, session))
     }
 
     pub fn destroy_auth(&self, realm: schema::RealmID, session: schema::SessionID) {
         let qi = self.core.pool.query_interface();
         
-        use schema::SessionAuthenticationColumns as SAC;
-        qi.delete_by_multi(&[SAC::Realm, SAC::Session], &microrm::value_list!(&realm, &session));
+        use schema::SessionAuthentication as SAC;
+        qi.delete_by_multi(&[&SAC::Realm, &SAC::Session], &microrm::value_list!(realm, session));
     }
 }
 
 impl ServerState {
-    fn render_login_from_auth(&self, 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 {
         let to_present: Option<schema::AuthChallengeType> = match auth {
             None => Some(schema::AuthChallengeType::Username),
             Some(auth) => auth.challenges_left.first().copied()
         };
 
         if to_present.is_none() {
-            todo!("Already logged in!");
+            response.set_status(302);
+            tide::Redirect::new("/").into()
         }
         else {
             self.render_login_page(response, to_present.unwrap(), error_msg)
@@ -183,7 +184,7 @@ async fn v1_login_response(mut req: tide::Request<ServerState>) -> tide::Result<
             let qi = req.state().core.pool.query_interface();
             req.state().destroy_auth(realm, session_id);
 
-            let user = qi.get_one_by_multi(&[schema::UserColumns::Realm, schema::UserColumns::Username], &microrm::value_list![&realm, &body.challenge]);
+            let user = qi.get_one_by_multi(&[&schema::User::Realm, &schema::User::Username], &microrm::value_list![&realm, &body.challenge]);
             if user.is_none() {
                 error = Some(format!("No such user {}", body.challenge));
             }
@@ -202,8 +203,8 @@ async fn v1_login_response(mut req: tide::Request<ServerState>) -> tide::Result<
             else {
                 let qi = req.state().core.pool.query_interface();
 
-                use schema::AuthChallengeColumns;
-                let challenge = qi.get_one_by_multi(&[AuthChallengeColumns::User, AuthChallengeColumns::ChallengeType], &microrm::value_list![&auth.as_ref().unwrap().user, &schema::AuthChallengeType::Password]);
+                use schema::AuthChallenge;
+                let challenge = qi.get_one_by_multi(&[&AuthChallenge::User, &AuthChallenge::ChallengeType], &microrm::value_list![auth.as_ref().unwrap().user, schema::AuthChallengeType::Password]);
 
                 if challenge.is_none() {
                     error = Some(format!("User lacks a password. Please contact an administrator."));
@@ -227,7 +228,7 @@ async fn v1_login_response(mut req: tide::Request<ServerState>) -> tide::Result<
     Ok(req.state().render_login_from_auth(response, auth.map(|a| a.wrapped()), error))
 }
 
-pub fn id_v1_server(core: &'static super::ServerCoreState) -> tide::Server<ServerState> {
+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())) });
 
     srv.with(tide::log::LogMiddleware::new());

+ 49 - 0
src/user.rs

@@ -0,0 +1,49 @@
+use crate::schema;
+
+pub struct User {
+    id: schema::UserID,
+    model: Option<schema::User>
+}
+
+const PBKDF2_ROUNDS: Option<std::num::NonZeroU32> = std::num::NonZeroU32::new(20000);
+
+impl User {
+    pub fn from_model(model: microrm::WithID<schema::User>) -> Self {
+        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_one_by_multi(&[&schema::AuthChallenge::User, &schema::AuthChallenge::ChallengeType], &microrm::value_list![&self.id, &which])?;
+
+        match which {
+            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_multi(&[&schema::AuthChallenge::User, &schema::AuthChallenge::ChallengeType], &microrm::value_list![self.id, schema::AuthChallengeType::Password]);
+
+        let rng = ring::rand::SystemRandom::new();
+        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.unwrap(), &salt, password, &mut generated);
+
+        qi.add(&schema::AuthChallenge { user: self.id, challenge_type: schema::AuthChallengeType::Password, public: salt.into(), secret: generated.into()});
+    }
+
+    fn verify_password_challenge(&self, challenge: schema::AuthChallenge, response: &[u8]) -> Option<bool> {
+        use ring::pbkdf2;
+
+        Some(pbkdf2::verify(pbkdf2::PBKDF2_HMAC_SHA256, PBKDF2_ROUNDS.unwrap(), challenge.public.as_slice(), response, challenge.secret.as_slice()).is_ok())
+    }
+
+    fn verify_totp_challenge(&self, challenge: schema::AuthChallenge, response: &[u8]) -> Option<bool> {
+        todo!()
+    }
+}

+ 9 - 22
src/user_management.rs

@@ -4,15 +4,15 @@ pub fn list(realm: &str, db: microrm::DB) {
     // get realm ID
     let qi = db.query_interface();
 
-    let realm_id = qi.get_one_by(schema::RealmColumns::Shortname, realm).expect("No such realm").id();
+    let realm_id = qi.get_one_by(schema::Realm::Shortname, realm).expect("No such realm").id();
 
-    let users = qi.get_all_by(schema::UserColumns::Realm, realm_id).expect("Can't get list of users");
+    let users = qi.get_all_by(schema::User::Realm, realm_id).expect("Can't get list of users");
 
     println!("User list ({} users):", users.len());
 
     for user in &users {
         println!("- {:20}", user.username);
-        let auth_challenges = qi.get_all_by(schema::AuthChallengeColumns::User, user.id()).expect("Can't get authentication challenges?");
+        let auth_challenges = qi.get_all_by(schema::AuthChallenge::User, user.id()).expect("Can't get authentication challenges?");
         for ch in &auth_challenges {
             println!("    - Has {:?} authentication challenge", ch.challenge_type);
         }
@@ -23,10 +23,10 @@ pub fn create(realm: &str, db: microrm::DB, username: &str) {
     // get realm ID
     let qi = db.query_interface();
 
-    let realm_id = qi.get_one_by(schema::RealmColumns::Shortname, realm).expect("No such realm").id();
+    let realm_id = qi.get_one_by(schema::Realm::Shortname, realm).expect("No such realm").id();
 
     // check that the user doesn't exist already
-    let existing_user = qi.get_one_by_multi(&[schema::UserColumns::Realm, schema::UserColumns::Username], &microrm::value_list![&realm_id, &username]);
+    let existing_user = qi.get_one_by_multi(&[&schema::User::Realm, &schema::User::Username], &microrm::value_list![&realm_id, &username]);
 
     if existing_user.is_some() {
         log::error!("Can't create user {} in {} realm as a user with that username already exists", username, realm);
@@ -40,31 +40,18 @@ pub fn change_auth(realm: &str, db: microrm::DB, username: &str, change_password
     // get realm ID
     let qi = db.query_interface();
 
-    let realm_id = qi.get_one_by(schema::RealmColumns::Shortname, realm).expect("No such realm").id();
+    let realm_id = qi.get_one_by(schema::Realm::Shortname, realm).expect("No such realm").id();
     // check that the user exists
-    let existing_user = qi.get_one_by_multi(&[schema::UserColumns::Realm, schema::UserColumns::Username], &microrm::value_list![&realm_id, &username]);
+    let existing_user = qi.get_one_by_multi(&[&schema::User::Realm, &schema::User::Username], &microrm::value_list![&realm_id, &username]);
     if existing_user.is_none() {
         log::error!("User {} does not exist in the {} realm!", username, realm);
         return;
     }
 
-    let user = existing_user.unwrap();
+    let user = crate::user::User::from_model(existing_user.unwrap());
 
     if change_password {
-        qi.delete_by_multi(&[schema::AuthChallengeColumns::User, schema::AuthChallengeColumns::ChallengeType], &microrm::value_list![&user.id(), &schema::AuthChallengeType::Password]).expect("Couldn't delete existing password?");
-
         let raw_pass = rpassword::prompt_password("Enter new user password: ").unwrap();
-
-        // store hashed password
-        use ring::pbkdf2;
-
-        let rng = ring::rand::SystemRandom::new();
-        let salt: [u8; 16] = ring::rand::generate(&rng).expect("Couldn't generate random salt?").expose();
-
-        let mut generated = [0u8; ring::digest::SHA256_OUTPUT_LEN];
-
-        pbkdf2::derive(pbkdf2::PBKDF2_HMAC_SHA256, std::num::NonZeroU32::new(20000).unwrap(), &salt, raw_pass.as_bytes(), &mut generated);
-
-        qi.add(&schema::AuthChallenge { user: user.id(), challenge_type: schema::AuthChallengeType::Password, public: salt.into(), secret: generated.into() });
+        user.set_new_password(&qi, raw_pass.as_bytes());
     }
 }