From 45187147d80e01d05e420fc476d83eef2ab04e66 Mon Sep 17 00:00:00 2001 From: Sebastian Pravda Date: Thu, 17 Nov 2022 19:28:04 +0100 Subject: [PATCH 1/6] feat: password reset --- api/src/lib.rs | 1 + api/src/routes/admin.rs | 15 +++++++++ core/src/database/mutation/candidate.rs | 19 +++++++++-- core/src/services/candidate_service.rs | 43 ++++++++++++++++++++++++- 4 files changed, 75 insertions(+), 3 deletions(-) diff --git a/api/src/lib.rs b/api/src/lib.rs index 15d65ca..8a8e476 100644 --- a/api/src/lib.rs +++ b/api/src/lib.rs @@ -69,6 +69,7 @@ async fn start() -> Result<(), rocket::Error> { routes::admin::hello, routes::admin::create_candidate, routes::admin::get_candidate, + routes::admin::reset_candidate_password, ], ) .mount( diff --git a/api/src/routes/admin.rs b/api/src/routes/admin.rs index 5c90006..3d1a437 100644 --- a/api/src/routes/admin.rs +++ b/api/src/routes/admin.rs @@ -121,4 +121,19 @@ pub async fn get_candidate( .map_err(|e| Custom(Status::from_code(e.code()).unwrap(), e.to_string()))?; Ok(Json(details)) +} + +#[post("/candidate//reset_password")] +pub async fn reset_candidate_password( + conn: Connection<'_, Db>, + _session: AdminAuth, + id: i32, +) -> Result> { + let db = conn.into_inner(); + + let new_password = CandidateService::reset_password(db, id) + .await + .map_err(|e| Custom(Status::from_code(e.code()).unwrap(), e.to_string()))?; + + Ok(new_password) } \ No newline at end of file diff --git a/core/src/database/mutation/candidate.rs b/core/src/database/mutation/candidate.rs index 058a2ec..9a25e3d 100644 --- a/core/src/database/mutation/candidate.rs +++ b/core/src/database/mutation/candidate.rs @@ -22,8 +22,23 @@ impl Mutation { updated_at: Set(chrono::offset::Local::now().naive_local()), ..Default::default() } - .insert(db) - .await + .insert(db) + .await + } + + pub async fn change_candidate_password( + db: &DbConn, + candidate: candidate::Model, + new_password_hash: String, + pub_key: String, + priv_key_enc: String, + ) -> Result { + let mut candidate: candidate::ActiveModel = candidate.into(); + candidate.code = Set(new_password_hash); + candidate.public_key = Set(pub_key); + candidate.private_key = Set(priv_key_enc); + + candidate.update(db).await } pub async fn add_candidate_details( diff --git a/core/src/services/candidate_service.rs b/core/src/services/candidate_service.rs index 9deacb1..a330444 100644 --- a/core/src/services/candidate_service.rs +++ b/core/src/services/candidate_service.rs @@ -94,12 +94,32 @@ impl CandidateService { hashed_password, hashed_personal_id_number, pubkey, - encrypted_priv_key, + encrypted_priv_key, ) .await?; Ok(candidate) } + pub async fn reset_password( + db: &DbConn, + id: i32, + ) -> Result { + let candidate = Query::find_candidate_by_id(db, id).await? + .ok_or(ServiceError::CandidateNotFound)?; + + let new_password_plain = crypto::random_8_char_string(); + let new_password_hash = crypto::hash_password(new_password_plain.clone()).await?; + + let (pubkey, priv_key_plain_text) = crypto::create_identity(); + let encrypted_priv_key = crypto::encrypt_password(priv_key_plain_text, + new_password_plain.to_string() + ).await?; + + Mutation::change_candidate_password(db, candidate, new_password_hash, pubkey, encrypted_priv_key).await?; + + Ok(new_password_plain) + } + pub(in crate::services) async fn add_candidate_details( db: &DbConn, candidate: candidate::Model, @@ -423,6 +443,27 @@ mod tests { assert!(!CandidateService::is_application_id_valid(101)); } + #[tokio::test] + async fn test_password_reset() { + let db = get_memory_sqlite_connection().await; + let (candidate, _parent) = put_user_data(&db).await; + + assert!( + CandidateService::login(&db, candidate.application, "test".to_string(), "127.0.0.1".to_string()).await.is_ok() + ); + + let new_password = CandidateService::reset_password(&db, candidate.application).await.unwrap(); + + assert!( + CandidateService::login(&db, candidate.application, "test".to_string(), "127.0.0.1".to_string()).await.is_err() + ); + + assert!( + CandidateService::login(&db, candidate.application, new_password, "127.0.0.1".to_string()).await.is_ok() + ); + + } + // TODO /* #[tokio::test] async fn test_list_candidates() { From 2ccbba4e118ef1bf52ec3c35a76aff7e2a5d2c48 Mon Sep 17 00:00:00 2001 From: Sebastian Pravda Date: Thu, 17 Nov 2022 19:55:04 +0100 Subject: [PATCH 2/6] feat: reencrypt candidate details on password reset --- api/src/routes/admin.rs | 5 +++-- core/src/database/mutation/candidate.rs | 2 +- core/src/services/candidate_service.rs | 23 +++++++++++++++++------ 3 files changed, 21 insertions(+), 9 deletions(-) diff --git a/api/src/routes/admin.rs b/api/src/routes/admin.rs index 3d1a437..b0f1edc 100644 --- a/api/src/routes/admin.rs +++ b/api/src/routes/admin.rs @@ -126,12 +126,13 @@ pub async fn get_candidate( #[post("/candidate//reset_password")] pub async fn reset_candidate_password( conn: Connection<'_, Db>, - _session: AdminAuth, + session: AdminAuth, id: i32, ) -> Result> { let db = conn.into_inner(); + let private_key = session.get_private_key(); - let new_password = CandidateService::reset_password(db, id) + let new_password = CandidateService::reset_password(private_key, db, id) .await .map_err(|e| Custom(Status::from_code(e.code()).unwrap(), e.to_string()))?; diff --git a/core/src/database/mutation/candidate.rs b/core/src/database/mutation/candidate.rs index 9a25e3d..3fbc113 100644 --- a/core/src/database/mutation/candidate.rs +++ b/core/src/database/mutation/candidate.rs @@ -26,7 +26,7 @@ impl Mutation { .await } - pub async fn change_candidate_password( + pub async fn update_candidate_password_with_keys( db: &DbConn, candidate: candidate::Model, new_password_hash: String, diff --git a/core/src/services/candidate_service.rs b/core/src/services/candidate_service.rs index a330444..f84e993 100644 --- a/core/src/services/candidate_service.rs +++ b/core/src/services/candidate_service.rs @@ -11,7 +11,7 @@ use crate::{ Mutation, Query, responses::CandidateResponse, }; -use super::session_service::{AdminUser, SessionService}; +use super::{session_service::{AdminUser, SessionService}, application_service::ApplicationService}; // TODO @@ -101,21 +101,31 @@ impl CandidateService { } pub async fn reset_password( + admin_private_key: String, db: &DbConn, id: i32, ) -> Result { let candidate = Query::find_candidate_by_id(db, id).await? .ok_or(ServiceError::CandidateNotFound)?; + let parent = Query::find_parent_by_id(db, id).await? + .ok_or(ServiceError::CandidateNotFound)?; + - let new_password_plain = crypto::random_8_char_string(); + let new_password_plain = crypto::random_8_char_string(); let new_password_hash = crypto::hash_password(new_password_plain.clone()).await?; let (pubkey, priv_key_plain_text) = crypto::create_identity(); let encrypted_priv_key = crypto::encrypt_password(priv_key_plain_text, new_password_plain.to_string() ).await?; - - Mutation::change_candidate_password(db, candidate, new_password_hash, pubkey, encrypted_priv_key).await?; + + Mutation::update_candidate_password_with_keys(db, candidate.clone(), new_password_hash, pubkey, encrypted_priv_key).await?; + + let enc_details_opt = EncryptedApplicationDetails::try_from((candidate, parent)); + if let Ok(enc_details) = enc_details_opt { + let application_details = enc_details.decrypt(admin_private_key).await?; + ApplicationService::add_all_details(db, id, application_details).await?; + } Ok(new_password_plain) } @@ -443,7 +453,8 @@ mod tests { assert!(!CandidateService::is_application_id_valid(101)); } - #[tokio::test] + // TODO + /* #[tokio::test] async fn test_password_reset() { let db = get_memory_sqlite_connection().await; let (candidate, _parent) = put_user_data(&db).await; @@ -462,7 +473,7 @@ mod tests { CandidateService::login(&db, candidate.application, new_password, "127.0.0.1".to_string()).await.is_ok() ); - } + } */ // TODO /* #[tokio::test] From 315966acba46d16d7334e8f326696982f4c00f98 Mon Sep 17 00:00:00 2001 From: Sebastian Pravda Date: Thu, 17 Nov 2022 20:00:40 +0100 Subject: [PATCH 3/6] feat: revoke all candidate sessions on password reset --- core/src/services/candidate_service.rs | 4 +++- core/src/services/session_service.rs | 12 ++++++------ 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/core/src/services/candidate_service.rs b/core/src/services/candidate_service.rs index f84e993..4903f45 100644 --- a/core/src/services/candidate_service.rs +++ b/core/src/services/candidate_service.rs @@ -118,7 +118,9 @@ impl CandidateService { let encrypted_priv_key = crypto::encrypt_password(priv_key_plain_text, new_password_plain.to_string() ).await?; - + + + SessionService::revoke_all_sessions(db, Some(id), None).await?; Mutation::update_candidate_password_with_keys(db, candidate.clone(), new_password_hash, pubkey, encrypted_priv_key).await?; let enc_details_opt = EncryptedApplicationDetails::try_from((candidate, parent)); diff --git a/core/src/services/session_service.rs b/core/src/services/session_service.rs index 4960fc4..08c9bac 100644 --- a/core/src/services/session_service.rs +++ b/core/src/services/session_service.rs @@ -1,7 +1,7 @@ use std::cmp::min; use entity::{admin, candidate}; -use sea_orm::{prelude::Uuid, DatabaseConnection, ModelTrait}; +use sea_orm::{prelude::Uuid, DatabaseConnection, ModelTrait, DbConn}; use crate::{ crypto::{self}, @@ -114,9 +114,12 @@ impl SessionService { Ok(session.id.to_string()) } + pub async fn revoke_all_sessions(db: &DbConn, user_id: Option, admin_id: Option) -> Result<(), ServiceError> { + Self::delete_old_sessions(db, user_id, admin_id, 0).await + } + /// Authenticate user by session id /// Return user model if session is valid - pub async fn auth_user_session( db: &DatabaseConnection, uuid: Uuid, @@ -162,11 +165,8 @@ impl SessionService { #[cfg(test)] mod tests { - use entity::{admin, candidate, session, parent}; - use sea_orm::{ - prelude::Uuid, sea_query::TableCreateStatement, ConnectionTrait, Database, DbBackend, - DbConn, Schema, + prelude::Uuid, }; use crate::{ From d8a269edd379bf9bc6cca94e099f587b0c10b822 Mon Sep 17 00:00:00 2001 From: Sebastian Pravda Date: Thu, 17 Nov 2022 20:41:03 +0100 Subject: [PATCH 4/6] feat: do not encrypt field of study; list candidates filtering --- api/src/routes/admin.rs | 6 ++++++ core/src/candidate_details.rs | 27 +++++++++++++------------- core/src/database/query/candidate.rs | 9 +++++++-- core/src/responses.rs | 5 ++--- core/src/services/candidate_service.rs | 2 +- 5 files changed, 29 insertions(+), 20 deletions(-) diff --git a/api/src/routes/admin.rs b/api/src/routes/admin.rs index b0f1edc..5b47fbc 100644 --- a/api/src/routes/admin.rs +++ b/api/src/routes/admin.rs @@ -95,6 +95,12 @@ pub async fn list_candidates( ) -> Result>, Custom> { let db = conn.into_inner(); let private_key = session.get_private_key(); + if let Some(field) = field.clone() { + if !(field == "KB".to_string() || field == "IT".to_string() || field == "G") { + return Err(Custom(Status::BadRequest, "Invalid field of study".to_string())); + } + + } let candidates = CandidateService::list_candidates(private_key, db, field) .await diff --git a/core/src/candidate_details.rs b/core/src/candidate_details.rs index ae3cb4f..47c483e 100644 --- a/core/src/candidate_details.rs +++ b/core/src/candidate_details.rs @@ -70,7 +70,7 @@ pub struct EncryptedApplicationDetails { pub citizenship: EncryptedString, pub email: EncryptedString, pub sex: EncryptedString, - pub study: EncryptedString, + pub study: String, // Parent pub parent_name: EncryptedString, @@ -95,7 +95,7 @@ impl EncryptedApplicationDetails { EncryptedString::new(&form.citizenship, &recipients), EncryptedString::new(&form.email, &recipients), EncryptedString::new(&form.sex, &recipients), - EncryptedString::new(&form.study, &recipients), + EncryptedString::new(&form.parent_name, &recipients), EncryptedString::new(&form.parent_surname, &recipients), EncryptedString::new(&form.parent_telephone, &recipients), @@ -112,12 +112,12 @@ impl EncryptedApplicationDetails { citizenship: d.6, email: d.7, sex: d.8, - study: d.9, + study: form.study, - parent_name: d.10, - parent_surname: d.11, - parent_telephone: d.12, - parent_email: d.13, + parent_name: d.9, + parent_surname: d.10, + parent_telephone: d.11, + parent_email: d.12, }) } @@ -132,7 +132,6 @@ impl EncryptedApplicationDetails { self.citizenship.decrypt(&priv_key), // 6 self.email.decrypt(&priv_key), // 7 self.sex.decrypt(&priv_key), // 8 - self.study.decrypt(&priv_key), // 9 self.parent_name.decrypt(&priv_key), self.parent_surname.decrypt(&priv_key), self.parent_telephone.decrypt(&priv_key), @@ -149,12 +148,12 @@ impl EncryptedApplicationDetails { citizenship: d.6, email: d.7, sex: d.8, - study: d.9, + study: self.study, - parent_name: d.10, - parent_surname: d.11, - parent_telephone: d.12, - parent_email: d.13, + parent_name: d.9, + parent_surname: d.10, + parent_telephone: d.11, + parent_email: d.12, }) } } @@ -175,7 +174,7 @@ impl TryFrom<(candidate::Model, parent::Model)> for EncryptedApplicationDetails citizenship: EncryptedString::try_from(candidate.citizenship)?, email: EncryptedString::try_from(candidate.email)?, sex: EncryptedString::try_from(candidate.sex)?, - study: EncryptedString::try_from(candidate.study)?, + study: candidate.study.ok_or(ServiceError::CandidateDetailsNotSet)?, parent_name: EncryptedString::try_from(parent.name)?, parent_surname: EncryptedString::try_from(parent.surname)?, diff --git a/core/src/database/query/candidate.rs b/core/src/database/query/candidate.rs index 37478b9..b317e48 100644 --- a/core/src/database/query/candidate.rs +++ b/core/src/database/query/candidate.rs @@ -29,9 +29,14 @@ impl Query { pub async fn list_candidates( db: &DbConn, - field_of_study: Option, + field_of_study_opt: Option, ) -> Result, DbErr> { - Candidate::find() + let select = Candidate::find(); + if let Some(study) = field_of_study_opt { + select.filter(candidate::Column::Study.eq(study)) + } else { + select + } .join(JoinType::InnerJoin, candidate::Relation::Parent.def()) .column_as(parent::Column::Name, "parent_name") .column_as(parent::Column::Surname, "parent_surname") diff --git a/core/src/responses.rs b/core/src/responses.rs index c2cbdd2..c0b7e5b 100644 --- a/core/src/responses.rs +++ b/core/src/responses.rs @@ -22,13 +22,12 @@ impl CandidateResponse { ) -> Result { let name = decrypt_if_exists(private_key, name_opt).await?; let surname = decrypt_if_exists(private_key, surname_opt).await?; - let study = decrypt_if_exists(private_key, study_opt).await?; Ok( Self { - application_id, name, + application_id, surname, - study, + study: study_opt.unwrap_or("".to_string()), submitted, } ) diff --git a/core/src/services/candidate_service.rs b/core/src/services/candidate_service.rs index 4903f45..c9f3a8c 100644 --- a/core/src/services/candidate_service.rs +++ b/core/src/services/candidate_service.rs @@ -147,7 +147,7 @@ impl CandidateService { field_of_study: Option, ) -> Result, ServiceError> { - let candidates = Query::list_candidates(db, None).await?; + let candidates = Query::list_candidates(db, field_of_study).await?; let mut result: Vec = vec![]; for candidate in candidates { From 139248dc4cfd8c1cea70c8de878939f928e21d5c Mon Sep 17 00:00:00 2001 From: Sebastian Pravda Date: Thu, 17 Nov 2022 20:47:11 +0100 Subject: [PATCH 5/6] refactor: struct name --- core/src/database/query/candidate.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/core/src/database/query/candidate.rs b/core/src/database/query/candidate.rs index b317e48..b57ce51 100644 --- a/core/src/database/query/candidate.rs +++ b/core/src/database/query/candidate.rs @@ -8,7 +8,7 @@ use serde::Serialize; pub const PAGE_SIZE: u64 = 20; #[derive(FromQueryResult, Serialize)] -pub struct ApplicationResult { +pub struct CandidateParentResult { pub application: i32, pub name: Option, pub surname: Option, @@ -30,7 +30,7 @@ impl Query { pub async fn list_candidates( db: &DbConn, field_of_study_opt: Option, - ) -> Result, DbErr> { + ) -> Result, DbErr> { let select = Candidate::find(); if let Some(study) = field_of_study_opt { select.filter(candidate::Column::Study.eq(study)) @@ -40,7 +40,7 @@ impl Query { .join(JoinType::InnerJoin, candidate::Relation::Parent.def()) .column_as(parent::Column::Name, "parent_name") .column_as(parent::Column::Surname, "parent_surname") - .into_model::() + .into_model::() .paginate(db, PAGE_SIZE) .fetch() .await From 7a8affb9b1f3e5cdecb4e8553c4ba7b5d28088f8 Mon Sep 17 00:00:00 2001 From: Sebastian Pravda Date: Thu, 17 Nov 2022 23:47:33 +0100 Subject: [PATCH 6/6] feat: candidate details tests --- core/src/services/candidate_service.rs | 92 ++++++++++++++++++-------- 1 file changed, 66 insertions(+), 26 deletions(-) diff --git a/core/src/services/candidate_service.rs b/core/src/services/candidate_service.rs index c9f3a8c..396bea1 100644 --- a/core/src/services/candidate_service.rs +++ b/core/src/services/candidate_service.rs @@ -435,9 +435,9 @@ mod tests { use super::EncryptedApplicationDetails; use chrono::NaiveDate; - use entity::{candidate, parent}; + use entity::{candidate, parent, admin}; - use crate::candidate_details::ApplicationDetails; + use crate::candidate_details::{ApplicationDetails}; use crate::services::application_service::ApplicationService; use std::path::{PathBuf}; @@ -455,17 +455,19 @@ mod tests { assert!(!CandidateService::is_application_id_valid(101)); } - // TODO - /* #[tokio::test] + #[tokio::test] async fn test_password_reset() { let db = get_memory_sqlite_connection().await; + let admin = create_admin(&db).await; let (candidate, _parent) = put_user_data(&db).await; + let private_key = crypto::decrypt_password(admin.private_key, "admin".to_string()).await.unwrap(); + assert!( CandidateService::login(&db, candidate.application, "test".to_string(), "127.0.0.1".to_string()).await.is_ok() ); - let new_password = CandidateService::reset_password(&db, candidate.application).await.unwrap(); + let new_password = CandidateService::reset_password(private_key, &db, candidate.application).await.unwrap(); assert!( CandidateService::login(&db, candidate.application, "test".to_string(), "127.0.0.1".to_string()).await.is_err() @@ -475,20 +477,21 @@ mod tests { CandidateService::login(&db, candidate.application, new_password, "127.0.0.1".to_string()).await.is_ok() ); - } */ + } - // TODO - /* #[tokio::test] + #[tokio::test] async fn test_list_candidates() { let db = get_memory_sqlite_connection().await; - let candidates = CandidateService::list_candidates(&db, None).await.unwrap(); + let admin = create_admin(&db).await; + let private_key = crypto::decrypt_password(admin.private_key, "admin".to_string()).await.unwrap(); + let candidates = CandidateService::list_candidates(private_key.clone(), &db, None).await.unwrap(); assert_eq!(candidates.len(), 0); put_user_data(&db).await; - let candidates = CandidateService::list_candidates(&db, None).await.unwrap(); + let candidates = CandidateService::list_candidates(private_key.clone(), &db, None).await.unwrap(); assert_eq!(candidates.len(), 1); - } */ + } #[tokio::test] async fn test_encrypt_decrypt_private_key_with_passphrase() { @@ -523,6 +526,31 @@ mod tests { assert_eq!(secret_message, decrypted_message); } + #[cfg(test)] + async fn create_admin(db: &DbConn) -> admin::Model { + use chrono::Utc; + use sea_orm::{Set, ActiveModelTrait}; + + let password = "admin".to_string(); + let (pubkey, priv_key) = crypto::create_identity(); + let enc_priv_key = crypto::encrypt_password(priv_key, password).await.unwrap(); + + let admin = admin::ActiveModel { + name: Set("admin".to_string()), + public_key: Set(pubkey), + private_key: Set(enc_priv_key), + password: Set("admin".to_string()), + created_at: Set(Utc::now().naive_utc()), + updated_at: Set(Utc::now().naive_utc()), + ..Default::default() + } + .insert(db) + .await + .unwrap(); + + admin + } + #[cfg(test)] async fn put_user_data(db: &DbConn) -> (candidate::Model, parent::Model) { let plain_text_password = "test".to_string(); @@ -537,20 +565,20 @@ mod tests { .unwrap(); let form = ApplicationDetails { - name: "test".to_string(), - surname: "aaa".to_string(), - birthplace: "b".to_string(), - birthdate: NaiveDate::from_ymd(1999, 1, 1), - address: "test".to_string(), - telephone: "test".to_string(), - citizenship: "test".to_string(), - email: "test".to_string(), - sex: "test".to_string(), + name: "name".to_string(), + surname: "surname".to_string(), + birthplace: "birthplace".to_string(), + birthdate: NaiveDate::from_ymd(2000, 1, 1), + address: "address".to_string(), + telephone: "telephone".to_string(), + citizenship: "citizenship".to_string(), + email: "email".to_string(), + sex: "sex".to_string(), study: "KB".to_string(), - parent_name: "test".to_string(), - parent_surname: "test".to_string(), - parent_telephone: "test".to_string(), - parent_email: "test".to_string(), + parent_name: "parent_name".to_string(), + parent_surname: "parent_surname".to_string(), + parent_telephone: "parent_telephone".to_string(), + parent_email: "parent_email".to_string(), }; ApplicationService::add_all_details(&db, candidate.application, form) @@ -580,8 +608,20 @@ mod tests { .unwrap(); let dec_details = enc_details.decrypt(dec_priv_key).await.ok().unwrap(); - assert_eq!(dec_details.name, "test"); // TODO: test every element - assert_eq!(dec_details.parent_surname, "test"); + assert_eq!(dec_details.name, "name"); + assert_eq!(dec_details.surname, "surname"); + assert_eq!(dec_details.birthplace, "birthplace"); + assert_eq!(dec_details.birthdate, NaiveDate::from_ymd(2000, 1, 1)); + assert_eq!(dec_details.address, "address"); + assert_eq!(dec_details.telephone, "telephone"); + assert_eq!(dec_details.citizenship, "citizenship"); + assert_eq!(dec_details.email, "email"); + assert_eq!(dec_details.sex, "sex"); + assert_eq!(dec_details.study, "KB"); + assert_eq!(dec_details.parent_name, "parent_name"); + assert_eq!(dec_details.parent_surname, "parent_surname"); + assert_eq!(dec_details.parent_telephone, "parent_telephone"); + assert_eq!(dec_details.parent_email, "parent_email"); } #[cfg(test)]