From c0e3c2c5e14bf28252c5c0ae6f1552561ac4b863 Mon Sep 17 00:00:00 2001 From: BlackDex Date: Sat, 26 Nov 2022 19:07:28 +0100 Subject: [PATCH] Cleanups and Fixes for Emergency Access - Several cleanups and code optimizations for Emergency Access - Fixed a race-condition regarding jobs for Emergency Access - Some other small changes like `allow(clippy::)` removals Fixes #2925 --- .env.template | 8 +- src/api/admin.rs | 2 +- src/api/core/emergency_access.rs | 191 ++++++++++++++++-------------- src/api/core/organizations.rs | 4 +- src/api/icons.rs | 1 - src/auth.rs | 12 +- src/config.rs | 8 +- src/db/mod.rs | 1 - src/db/models/emergency_access.rs | 78 +++++++----- src/db/models/user.rs | 2 +- src/error.rs | 1 - src/mail.rs | 18 +-- 12 files changed, 173 insertions(+), 153 deletions(-) diff --git a/.env.template b/.env.template index 736b6463..22877f15 100644 --- a/.env.template +++ b/.env.template @@ -119,12 +119,12 @@ # INCOMPLETE_2FA_SCHEDULE="30 * * * * *" ## ## Cron schedule of the job that sends expiration reminders to emergency access grantors. -## Defaults to hourly (5 minutes after the hour). Set blank to disable this job. -# EMERGENCY_NOTIFICATION_REMINDER_SCHEDULE="0 5 * * * *" +## Defaults to hourly (3 minutes after the hour). Set blank to disable this job. +# EMERGENCY_NOTIFICATION_REMINDER_SCHEDULE="0 3 * * * *" ## ## Cron schedule of the job that grants emergency access requests that have met the required wait time. -## Defaults to hourly (5 minutes after the hour). Set blank to disable this job. -# EMERGENCY_REQUEST_TIMEOUT_SCHEDULE="0 5 * * * *" +## Defaults to hourly (7 minutes after the hour). Set blank to disable this job. +# EMERGENCY_REQUEST_TIMEOUT_SCHEDULE="0 7 * * * *" ## ## Cron schedule of the job that cleans old events from the event table. ## Defaults to daily. Set blank to disable this job. Also without EVENTS_DAYS_RETAIN set, this job will not start. diff --git a/src/api/admin.rs b/src/api/admin.rs index 656f63bf..6c908bfc 100644 --- a/src/api/admin.rs +++ b/src/api/admin.rs @@ -284,7 +284,7 @@ async fn invite_user(data: Json, _token: AdminToken, mut conn: DbCon if CONFIG.mail_enabled() { mail::send_invite(&user.email, &user.uuid, None, None, &CONFIG.invitation_org_name(), None).await } else { - let invitation = Invitation::new(user.email.clone()); + let invitation = Invitation::new(&user.email); invitation.save(conn).await } } diff --git a/src/api/core/emergency_access.rs b/src/api/core/emergency_access.rs index 7c3b09c5..7a683ea4 100644 --- a/src/api/core/emergency_access.rs +++ b/src/api/core/emergency_access.rs @@ -1,6 +1,5 @@ use chrono::{Duration, Utc}; -use rocket::serde::json::Json; -use rocket::Route; +use rocket::{serde::json::Json, Route}; use serde_json::Value; use crate::{ @@ -41,9 +40,10 @@ pub fn routes() -> Vec { async fn get_contacts(headers: Headers, mut conn: DbConn) -> JsonResult { check_emergency_access_allowed()?; - let mut emergency_access_list_json = Vec::new(); - for e in EmergencyAccess::find_all_by_grantor_uuid(&headers.user.uuid, &mut conn).await { - emergency_access_list_json.push(e.to_json_grantee_details(&mut conn).await); + let emergency_access_list = EmergencyAccess::find_all_by_grantor_uuid(&headers.user.uuid, &mut conn).await; + let mut emergency_access_list_json = Vec::with_capacity(emergency_access_list.len()); + for ea in emergency_access_list { + emergency_access_list_json.push(ea.to_json_grantee_details(&mut conn).await); } Ok(Json(json!({ @@ -57,9 +57,10 @@ async fn get_contacts(headers: Headers, mut conn: DbConn) -> JsonResult { async fn get_grantees(headers: Headers, mut conn: DbConn) -> JsonResult { check_emergency_access_allowed()?; - let mut emergency_access_list_json = Vec::new(); - for e in EmergencyAccess::find_all_by_grantee_uuid(&headers.user.uuid, &mut conn).await { - emergency_access_list_json.push(e.to_json_grantor_details(&mut conn).await); + let emergency_access_list = EmergencyAccess::find_all_by_grantee_uuid(&headers.user.uuid, &mut conn).await; + let mut emergency_access_list_json = Vec::with_capacity(emergency_access_list.len()); + for ea in emergency_access_list { + emergency_access_list_json.push(ea.to_json_grantor_details(&mut conn).await); } Ok(Json(json!({ @@ -83,7 +84,7 @@ async fn get_emergency_access(emer_id: String, mut conn: DbConn) -> JsonResult { // region put/post -#[derive(Deserialize, Debug)] +#[derive(Deserialize)] #[allow(non_snake_case)] struct EmergencyAccessUpdateData { Type: NumberOrString, @@ -160,7 +161,7 @@ async fn post_delete_emergency_access(emer_id: String, headers: Headers, conn: D // region invite -#[derive(Deserialize, Debug)] +#[derive(Deserialize)] #[allow(non_snake_case)] struct EmergencyAccessInviteData { Email: String, @@ -193,7 +194,7 @@ async fn send_invite(data: JsonUpcase, headers: Heade let grantee_user = match User::find_by_mail(&email, &mut conn).await { None => { if !CONFIG.invitations_allowed() { - err!(format!("Grantee user does not exist: {}", email)) + err!(format!("Grantee user does not exist: {}", &email)) } if !CONFIG.is_email_domain_allowed(&email) { @@ -201,7 +202,7 @@ async fn send_invite(data: JsonUpcase, headers: Heade } if !CONFIG.mail_enabled() { - let invitation = Invitation::new(email.clone()); + let invitation = Invitation::new(&email); invitation.save(&mut conn).await?; } @@ -221,36 +222,29 @@ async fn send_invite(data: JsonUpcase, headers: Heade .await .is_some() { - err!(format!("Grantee user already invited: {}", email)) + err!(format!("Grantee user already invited: {}", &grantee_user.email)) } - let mut new_emergency_access = EmergencyAccess::new( - grantor_user.uuid.clone(), - Some(grantee_user.email.clone()), - emergency_access_status, - new_type, - wait_time_days, - ); + let mut new_emergency_access = + EmergencyAccess::new(grantor_user.uuid, grantee_user.email, emergency_access_status, new_type, wait_time_days); new_emergency_access.save(&mut conn).await?; if CONFIG.mail_enabled() { mail::send_emergency_access_invite( - &grantee_user.email, + &new_emergency_access.email.expect("Grantee email does not exists"), &grantee_user.uuid, - Some(new_emergency_access.uuid), - Some(grantor_user.name.clone()), - Some(grantor_user.email), + &new_emergency_access.uuid, + &grantor_user.name, + &grantor_user.email, ) .await?; } else { // Automatically mark user as accepted if no email invites match User::find_by_mail(&email, &mut conn).await { - Some(user) => { - match accept_invite_process(user.uuid, new_emergency_access.uuid, Some(email), &mut conn).await { - Ok(v) => v, - Err(e) => err!(e.to_string()), - } - } + Some(user) => match accept_invite_process(user.uuid, &mut new_emergency_access, &email, &mut conn).await { + Ok(v) => v, + Err(e) => err!(e.to_string()), + }, None => err!("Grantee user not found."), } } @@ -262,7 +256,7 @@ async fn send_invite(data: JsonUpcase, headers: Heade async fn resend_invite(emer_id: String, headers: Headers, mut conn: DbConn) -> EmptyResult { check_emergency_access_allowed()?; - let emergency_access = match EmergencyAccess::find_by_uuid(&emer_id, &mut conn).await { + let mut emergency_access = match EmergencyAccess::find_by_uuid(&emer_id, &mut conn).await { Some(emer) => emer, None => err!("Emergency access not valid."), }; @@ -291,19 +285,19 @@ async fn resend_invite(emer_id: String, headers: Headers, mut conn: DbConn) -> E mail::send_emergency_access_invite( &email, &grantor_user.uuid, - Some(emergency_access.uuid), - Some(grantor_user.name.clone()), - Some(grantor_user.email), + &emergency_access.uuid, + &grantor_user.name, + &grantor_user.email, ) .await?; } else { if Invitation::find_by_mail(&email, &mut conn).await.is_none() { - let invitation = Invitation::new(email); + let invitation = Invitation::new(&email); invitation.save(&mut conn).await?; } // Automatically mark user as accepted if no email invites - match accept_invite_process(grantee_user.uuid, emergency_access.uuid, emergency_access.email, &mut conn).await { + match accept_invite_process(grantee_user.uuid, &mut emergency_access, &email, &mut conn).await { Ok(v) => v, Err(e) => err!(e.to_string()), } @@ -319,13 +313,24 @@ struct AcceptData { } #[post("/emergency-access//accept", data = "")] -async fn accept_invite(emer_id: String, data: JsonUpcase, mut conn: DbConn) -> EmptyResult { +async fn accept_invite( + emer_id: String, + data: JsonUpcase, + headers: Headers, + mut conn: DbConn, +) -> EmptyResult { check_emergency_access_allowed()?; let data: AcceptData = data.into_inner().data; let token = &data.Token; let claims = decode_emergency_access_invite(token)?; + // This can happen if the user who received the invite used a different email to signup. + // Since we do not know if this is intented, we error out here and do nothing with the invite. + if claims.email != headers.user.email { + err!("Claim email does not match current users email") + } + let grantee_user = match User::find_by_mail(&claims.email, &mut conn).await { Some(user) => { Invitation::take(&claims.email, &mut conn).await; @@ -334,7 +339,7 @@ async fn accept_invite(emer_id: String, data: JsonUpcase, mut conn: None => err!("Invited user not found"), }; - let emergency_access = match EmergencyAccess::find_by_uuid(&emer_id, &mut conn).await { + let mut emergency_access = match EmergencyAccess::find_by_uuid(&emer_id, &mut conn).await { Some(emer) => emer, None => err!("Emergency access not valid."), }; @@ -345,13 +350,11 @@ async fn accept_invite(emer_id: String, data: JsonUpcase, mut conn: None => err!("Grantor user not found."), }; - if (claims.emer_id.is_some() && emer_id == claims.emer_id.unwrap()) - && (claims.grantor_name.is_some() && grantor_user.name == claims.grantor_name.unwrap()) - && (claims.grantor_email.is_some() && grantor_user.email == claims.grantor_email.unwrap()) + if emer_id == claims.emer_id + && grantor_user.name == claims.grantor_name + && grantor_user.email == claims.grantor_email { - match accept_invite_process(grantee_user.uuid.clone(), emer_id, Some(grantee_user.email.clone()), &mut conn) - .await - { + match accept_invite_process(grantee_user.uuid, &mut emergency_access, &grantee_user.email, &mut conn).await { Ok(v) => v, Err(e) => err!(e.to_string()), } @@ -368,17 +371,11 @@ async fn accept_invite(emer_id: String, data: JsonUpcase, mut conn: async fn accept_invite_process( grantee_uuid: String, - emer_id: String, - email: Option, + emergency_access: &mut EmergencyAccess, + grantee_email: &str, conn: &mut DbConn, ) -> EmptyResult { - let mut emergency_access = match EmergencyAccess::find_by_uuid(&emer_id, conn).await { - Some(emer) => emer, - None => err!("Emergency access not valid."), - }; - - let emer_email = emergency_access.email; - if emer_email.is_none() || emer_email != email { + if emergency_access.email.is_none() || emergency_access.email.as_ref().unwrap() != grantee_email { err!("User email does not match invite."); } @@ -463,7 +460,7 @@ async fn initiate_emergency_access(emer_id: String, headers: Headers, mut conn: }; if emergency_access.status != EmergencyAccessStatus::Confirmed as i32 - || emergency_access.grantee_uuid != Some(initiating_user.uuid.clone()) + || emergency_access.grantee_uuid != Some(initiating_user.uuid) { err!("Emergency access not valid.") } @@ -485,7 +482,7 @@ async fn initiate_emergency_access(emer_id: String, headers: Headers, mut conn: &grantor_user.email, &initiating_user.name, emergency_access.get_type_as_str(), - &emergency_access.wait_time_days.clone().to_string(), + &emergency_access.wait_time_days, ) .await?; } @@ -496,19 +493,18 @@ async fn initiate_emergency_access(emer_id: String, headers: Headers, mut conn: async fn approve_emergency_access(emer_id: String, headers: Headers, mut conn: DbConn) -> JsonResult { check_emergency_access_allowed()?; - let approving_user = headers.user; let mut emergency_access = match EmergencyAccess::find_by_uuid(&emer_id, &mut conn).await { Some(emer) => emer, None => err!("Emergency access not valid."), }; if emergency_access.status != EmergencyAccessStatus::RecoveryInitiated as i32 - || emergency_access.grantor_uuid != approving_user.uuid + || emergency_access.grantor_uuid != headers.user.uuid { err!("Emergency access not valid.") } - let grantor_user = match User::find_by_uuid(&approving_user.uuid, &mut conn).await { + let grantor_user = match User::find_by_uuid(&headers.user.uuid, &mut conn).await { Some(user) => user, None => err!("Grantor user not found."), }; @@ -535,7 +531,6 @@ async fn approve_emergency_access(emer_id: String, headers: Headers, mut conn: D async fn reject_emergency_access(emer_id: String, headers: Headers, mut conn: DbConn) -> JsonResult { check_emergency_access_allowed()?; - let rejecting_user = headers.user; let mut emergency_access = match EmergencyAccess::find_by_uuid(&emer_id, &mut conn).await { Some(emer) => emer, None => err!("Emergency access not valid."), @@ -543,12 +538,12 @@ async fn reject_emergency_access(emer_id: String, headers: Headers, mut conn: Db if (emergency_access.status != EmergencyAccessStatus::RecoveryInitiated as i32 && emergency_access.status != EmergencyAccessStatus::RecoveryApproved as i32) - || emergency_access.grantor_uuid != rejecting_user.uuid + || emergency_access.grantor_uuid != headers.user.uuid { err!("Emergency access not valid.") } - let grantor_user = match User::find_by_uuid(&rejecting_user.uuid, &mut conn).await { + let grantor_user = match User::find_by_uuid(&headers.user.uuid, &mut conn).await { Some(user) => user, None => err!("Grantor user not found."), }; @@ -579,14 +574,12 @@ async fn reject_emergency_access(emer_id: String, headers: Headers, mut conn: Db async fn view_emergency_access(emer_id: String, headers: Headers, mut conn: DbConn) -> JsonResult { check_emergency_access_allowed()?; - let requesting_user = headers.user; - let host = headers.host; let emergency_access = match EmergencyAccess::find_by_uuid(&emer_id, &mut conn).await { Some(emer) => emer, None => err!("Emergency access not valid."), }; - if !is_valid_request(&emergency_access, requesting_user.uuid, EmergencyAccessType::View) { + if !is_valid_request(&emergency_access, headers.user.uuid, EmergencyAccessType::View) { err!("Emergency access not valid.") } @@ -596,7 +589,8 @@ async fn view_emergency_access(emer_id: String, headers: Headers, mut conn: DbCo let mut ciphers_json = Vec::new(); for c in ciphers { - ciphers_json.push(c.to_json(&host, &emergency_access.grantor_uuid, Some(&cipher_sync_data), &mut conn).await); + ciphers_json + .push(c.to_json(&headers.host, &emergency_access.grantor_uuid, Some(&cipher_sync_data), &mut conn).await); } Ok(Json(json!({ @@ -633,7 +627,7 @@ async fn takeover_emergency_access(emer_id: String, headers: Headers, mut conn: }))) } -#[derive(Deserialize, Debug)] +#[derive(Deserialize)] #[allow(non_snake_case)] struct EmergencyAccessPasswordData { NewMasterPasswordHash: String, @@ -738,40 +732,44 @@ pub async fn emergency_request_timeout_job(pool: DbPool) { } if let Ok(mut conn) = pool.get().await { - let emergency_access_list = EmergencyAccess::find_all_recoveries(&mut conn).await; + let emergency_access_list = EmergencyAccess::find_all_recoveries_initiated(&mut conn).await; if emergency_access_list.is_empty() { debug!("No emergency request timeout to approve"); } + let now = Utc::now().naive_utc(); for mut emer in emergency_access_list { - if emer.recovery_initiated_at.is_some() - && Utc::now().naive_utc() - >= emer.recovery_initiated_at.unwrap() + Duration::days(i64::from(emer.wait_time_days)) - { - emer.status = EmergencyAccessStatus::RecoveryApproved as i32; - emer.save(&mut conn).await.expect("Cannot save emergency access on job"); + // The find_all_recoveries_initiated already checks if the recovery_initiated_at is not null (None) + let recovery_allowed_at = + emer.recovery_initiated_at.unwrap() + Duration::days(i64::from(emer.wait_time_days)); + if recovery_allowed_at.le(&now) { + // Only update the access status + // Updating the whole record could cause issues when the emergency_notification_reminder_job is also active + emer.update_access_status_and_save(EmergencyAccessStatus::RecoveryApproved as i32, &now, &mut conn) + .await + .expect("Unable to update emergency access status"); if CONFIG.mail_enabled() { // get grantor user to send Accepted email let grantor_user = - User::find_by_uuid(&emer.grantor_uuid, &mut conn).await.expect("Grantor user not found."); + User::find_by_uuid(&emer.grantor_uuid, &mut conn).await.expect("Grantor user not found"); // get grantee user to send Accepted email let grantee_user = - User::find_by_uuid(&emer.grantee_uuid.clone().expect("Grantee user invalid."), &mut conn) + User::find_by_uuid(&emer.grantee_uuid.clone().expect("Grantee user invalid"), &mut conn) .await - .expect("Grantee user not found."); + .expect("Grantee user not found"); mail::send_emergency_access_recovery_timed_out( &grantor_user.email, - &grantee_user.name.clone(), + &grantee_user.name, emer.get_type_as_str(), ) .await .expect("Error on sending email"); - mail::send_emergency_access_recovery_approved(&grantee_user.email, &grantor_user.name.clone()) + mail::send_emergency_access_recovery_approved(&grantee_user.email, &grantor_user.name) .await .expect("Error on sending email"); } @@ -789,38 +787,47 @@ pub async fn emergency_notification_reminder_job(pool: DbPool) { } if let Ok(mut conn) = pool.get().await { - let emergency_access_list = EmergencyAccess::find_all_recoveries(&mut conn).await; + let emergency_access_list = EmergencyAccess::find_all_recoveries_initiated(&mut conn).await; if emergency_access_list.is_empty() { debug!("No emergency request reminder notification to send"); } + let now = Utc::now().naive_utc(); for mut emer in emergency_access_list { - if (emer.recovery_initiated_at.is_some() - && Utc::now().naive_utc() - >= emer.recovery_initiated_at.unwrap() + Duration::days((i64::from(emer.wait_time_days)) - 1)) - && (emer.last_notification_at.is_none() - || (emer.last_notification_at.is_some() - && Utc::now().naive_utc() >= emer.last_notification_at.unwrap() + Duration::days(1))) - { - emer.save(&mut conn).await.expect("Cannot save emergency access on job"); + // The find_all_recoveries_initiated already checks if the recovery_initiated_at is not null (None) + // Calculate the day before the recovery will become active + let final_recovery_reminder_at = + emer.recovery_initiated_at.unwrap() + Duration::days(i64::from(emer.wait_time_days - 1)); + // Calculate if a day has passed since the previous notification, else no notification has been sent before + let next_recovery_reminder_at = if let Some(last_notification_at) = emer.last_notification_at { + last_notification_at + Duration::days(1) + } else { + now + }; + if final_recovery_reminder_at.le(&now) && next_recovery_reminder_at.le(&now) { + // Only update the last notification date + // Updating the whole record could cause issues when the emergency_request_timeout_job is also active + emer.update_last_notification_date_and_save(&now, &mut conn) + .await + .expect("Unable to update emergency access notification date"); if CONFIG.mail_enabled() { // get grantor user to send Accepted email let grantor_user = - User::find_by_uuid(&emer.grantor_uuid, &mut conn).await.expect("Grantor user not found."); + User::find_by_uuid(&emer.grantor_uuid, &mut conn).await.expect("Grantor user not found"); // get grantee user to send Accepted email let grantee_user = - User::find_by_uuid(&emer.grantee_uuid.clone().expect("Grantee user invalid."), &mut conn) + User::find_by_uuid(&emer.grantee_uuid.clone().expect("Grantee user invalid"), &mut conn) .await - .expect("Grantee user not found."); + .expect("Grantee user not found"); mail::send_emergency_access_recovery_reminder( &grantor_user.email, - &grantee_user.name.clone(), + &grantee_user.name, emer.get_type_as_str(), - &emer.wait_time_days.to_string(), // TODO(jjlin): This should be the number of days left. + "1", // This notification is only triggered one day before the activation ) .await .expect("Error on sending email"); diff --git a/src/api/core/organizations.rs b/src/api/core/organizations.rs index 57a982f9..b612ccc3 100644 --- a/src/api/core/organizations.rs +++ b/src/api/core/organizations.rs @@ -721,7 +721,7 @@ async fn send_invite( } if !CONFIG.mail_enabled() { - let invitation = Invitation::new(email.clone()); + let invitation = Invitation::new(&email); invitation.save(&mut conn).await?; } @@ -871,7 +871,7 @@ async fn _reinvite_user(org_id: &str, user_org: &str, invited_by_email: &str, co ) .await?; } else { - let invitation = Invitation::new(user.email); + let invitation = Invitation::new(&user.email); invitation.save(conn).await?; } diff --git a/src/api/icons.rs b/src/api/icons.rs index a69b7359..509e88c0 100644 --- a/src/api/icons.rs +++ b/src/api/icons.rs @@ -260,7 +260,6 @@ mod tests { use cached::proc_macro::cached; #[cached(key = "String", convert = r#"{ domain.to_string() }"#, size = 16, time = 60)] -#[allow(clippy::unused_async)] // This is needed because cached causes a false-positive here. async fn is_domain_blacklisted(domain: &str) -> bool { // First check the blacklist regex if there is a match. // This prevents the blocked domain(s) from being leaked via a DNS lookup. diff --git a/src/auth.rs b/src/auth.rs index 0db2d95a..8dea4165 100644 --- a/src/auth.rs +++ b/src/auth.rs @@ -177,17 +177,17 @@ pub struct EmergencyAccessInviteJwtClaims { pub sub: String, pub email: String, - pub emer_id: Option, - pub grantor_name: Option, - pub grantor_email: Option, + pub emer_id: String, + pub grantor_name: String, + pub grantor_email: String, } pub fn generate_emergency_access_invite_claims( uuid: String, email: String, - emer_id: Option, - grantor_name: Option, - grantor_email: Option, + emer_id: String, + grantor_name: String, + grantor_email: String, ) -> EmergencyAccessInviteJwtClaims { let time_now = Utc::now().naive_utc(); let expire_hours = i64::from(CONFIG.invitation_expiration_hours()); diff --git a/src/config.rs b/src/config.rs index fe98d2df..fbf0e412 100644 --- a/src/config.rs +++ b/src/config.rs @@ -366,11 +366,11 @@ make_config! { /// Defaults to once every minute. Set blank to disable this job. incomplete_2fa_schedule: String, false, def, "30 * * * * *".to_string(); /// Emergency notification reminder schedule |> Cron schedule of the job that sends expiration reminders to emergency access grantors. - /// Defaults to hourly. Set blank to disable this job. - emergency_notification_reminder_schedule: String, false, def, "0 5 * * * *".to_string(); + /// Defaults to hourly. (3 minutes after the hour) Set blank to disable this job. + emergency_notification_reminder_schedule: String, false, def, "0 3 * * * *".to_string(); /// Emergency request timeout schedule |> Cron schedule of the job that grants emergency access requests that have met the required wait time. - /// Defaults to hourly. Set blank to disable this job. - emergency_request_timeout_schedule: String, false, def, "0 5 * * * *".to_string(); + /// Defaults to hourly. (7 minutes after the hour) Set blank to disable this job. + emergency_request_timeout_schedule: String, false, def, "0 7 * * * *".to_string(); /// Event cleanup schedule |> Cron schedule of the job that cleans old events from the event table. /// Defaults to daily. Set blank to disable this job. event_cleanup_schedule: String, false, def, "0 10 0 * * *".to_string(); diff --git a/src/db/mod.rs b/src/db/mod.rs index b04e7e7d..ef30ffe7 100644 --- a/src/db/mod.rs +++ b/src/db/mod.rs @@ -125,7 +125,6 @@ macro_rules! generate_connections { impl DbPool { // For the given database URL, guess its type, run migrations, create pool, and return it - #[allow(clippy::diverging_sub_expression)] pub fn from_config() -> Result { let url = CONFIG.database_url(); let conn_type = DbConnType::from_url(&url)?; diff --git a/src/db/models/emergency_access.rs b/src/db/models/emergency_access.rs index 3971fa04..ccb21e5b 100644 --- a/src/db/models/emergency_access.rs +++ b/src/db/models/emergency_access.rs @@ -1,10 +1,12 @@ use chrono::{NaiveDateTime, Utc}; use serde_json::Value; +use crate::{api::EmptyResult, db::DbConn, error::MapResult}; + use super::User; db_object! { - #[derive(Debug, Identifiable, Queryable, Insertable, AsChangeset)] + #[derive(Identifiable, Queryable, Insertable, AsChangeset)] #[diesel(table_name = emergency_access)] #[diesel(treat_none_as_null = true)] #[diesel(primary_key(uuid))] @@ -27,14 +29,14 @@ db_object! { /// Local methods impl EmergencyAccess { - pub fn new(grantor_uuid: String, email: Option, status: i32, atype: i32, wait_time_days: i32) -> Self { + pub fn new(grantor_uuid: String, email: String, status: i32, atype: i32, wait_time_days: i32) -> Self { let now = Utc::now().naive_utc(); Self { uuid: crate::util::get_uuid(), grantor_uuid, grantee_uuid: None, - email, + email: Some(email), status, atype, wait_time_days, @@ -54,14 +56,6 @@ impl EmergencyAccess { } } - pub fn has_type(&self, access_type: EmergencyAccessType) -> bool { - self.atype == access_type as i32 - } - - pub fn has_status(&self, status: EmergencyAccessStatus) -> bool { - self.status == status as i32 - } - pub fn to_json(&self) -> Value { json!({ "Id": self.uuid, @@ -87,7 +81,6 @@ impl EmergencyAccess { }) } - #[allow(clippy::manual_map)] pub async fn to_json_grantee_details(&self, conn: &mut DbConn) -> Value { let grantee_user = if let Some(grantee_uuid) = self.grantee_uuid.as_deref() { Some(User::find_by_uuid(grantee_uuid, conn).await.expect("Grantee user not found.")) @@ -110,7 +103,7 @@ impl EmergencyAccess { } } -#[derive(Copy, Clone, PartialEq, Eq, num_derive::FromPrimitive)] +#[derive(Copy, Clone)] pub enum EmergencyAccessType { View = 0, Takeover = 1, @@ -126,18 +119,6 @@ impl EmergencyAccessType { } } -impl PartialEq for EmergencyAccessType { - fn eq(&self, other: &i32) -> bool { - *other == *self as i32 - } -} - -impl PartialEq for i32 { - fn eq(&self, other: &EmergencyAccessType) -> bool { - *self == *other as i32 - } -} - pub enum EmergencyAccessStatus { Invited = 0, Accepted = 1, @@ -148,11 +129,6 @@ pub enum EmergencyAccessStatus { // region Database methods -use crate::db::DbConn; - -use crate::api::EmptyResult; -use crate::error::MapResult; - impl EmergencyAccess { pub async fn save(&mut self, conn: &mut DbConn) -> EmptyResult { User::update_uuid_revision(&self.grantor_uuid, conn).await; @@ -189,6 +165,45 @@ impl EmergencyAccess { } } + pub async fn update_access_status_and_save( + &mut self, + status: i32, + date: &NaiveDateTime, + conn: &mut DbConn, + ) -> EmptyResult { + // Update the grantee so that it will refresh it's status. + User::update_uuid_revision(self.grantee_uuid.as_ref().expect("Error getting grantee"), conn).await; + self.status = status; + self.updated_at = date.to_owned(); + + db_run! {conn: { + crate::util::retry(|| { + diesel::update(emergency_access::table.filter(emergency_access::uuid.eq(&self.uuid))) + .set((emergency_access::status.eq(status), emergency_access::updated_at.eq(date))) + .execute(conn) + }, 10) + .map_res("Error updating emergency access status") + }} + } + + pub async fn update_last_notification_date_and_save( + &mut self, + date: &NaiveDateTime, + conn: &mut DbConn, + ) -> EmptyResult { + self.last_notification_at = Some(date.to_owned()); + self.updated_at = date.to_owned(); + + db_run! {conn: { + crate::util::retry(|| { + diesel::update(emergency_access::table.filter(emergency_access::uuid.eq(&self.uuid))) + .set((emergency_access::last_notification_at.eq(date), emergency_access::updated_at.eq(date))) + .execute(conn) + }, 10) + .map_res("Error updating emergency access status") + }} + } + pub async fn delete_all_by_user(user_uuid: &str, conn: &mut DbConn) -> EmptyResult { for ea in Self::find_all_by_grantor_uuid(user_uuid, conn).await { ea.delete(conn).await?; @@ -233,10 +248,11 @@ impl EmergencyAccess { }} } - pub async fn find_all_recoveries(conn: &mut DbConn) -> Vec { + pub async fn find_all_recoveries_initiated(conn: &mut DbConn) -> Vec { db_run! { conn: { emergency_access::table .filter(emergency_access::status.eq(EmergencyAccessStatus::RecoveryInitiated as i32)) + .filter(emergency_access::recovery_initiated_at.is_not_null()) .load::(conn).expect("Error loading emergency_access").from_db() }} } diff --git a/src/db/models/user.rs b/src/db/models/user.rs index 68fb96f6..b59f10b8 100644 --- a/src/db/models/user.rs +++ b/src/db/models/user.rs @@ -364,7 +364,7 @@ impl User { } impl Invitation { - pub fn new(email: String) -> Self { + pub fn new(email: &str) -> Self { let email = email.to_lowercase(); Self { email, diff --git a/src/error.rs b/src/error.rs index decae01e..582604fc 100644 --- a/src/error.rs +++ b/src/error.rs @@ -168,7 +168,6 @@ impl MapResult for Option { } } -#[allow(clippy::unnecessary_wraps)] const fn _has_source(e: T) -> Option { Some(e) } diff --git a/src/mail.rs b/src/mail.rs index fce76e17..af0f8c7c 100644 --- a/src/mail.rs +++ b/src/mail.rs @@ -256,16 +256,16 @@ pub async fn send_invite( pub async fn send_emergency_access_invite( address: &str, uuid: &str, - emer_id: Option, - grantor_name: Option, - grantor_email: Option, + emer_id: &str, + grantor_name: &str, + grantor_email: &str, ) -> EmptyResult { let claims = generate_emergency_access_invite_claims( - uuid.to_string(), + String::from(uuid), String::from(address), - emer_id.clone(), - grantor_name.clone(), - grantor_email, + String::from(emer_id), + String::from(grantor_name), + String::from(grantor_email), ); let invite_token = encode_jwt(&claims); @@ -275,7 +275,7 @@ pub async fn send_emergency_access_invite( json!({ "url": CONFIG.domain(), "img_src": CONFIG._smtp_img_src(), - "emer_id": emer_id.unwrap_or_else(|| "_".to_string()), + "emer_id": emer_id, "email": percent_encode(address.as_bytes(), NON_ALPHANUMERIC).to_string(), "grantor_name": grantor_name, "token": invite_token, @@ -328,7 +328,7 @@ pub async fn send_emergency_access_recovery_initiated( address: &str, grantee_name: &str, atype: &str, - wait_time_days: &str, + wait_time_days: &i32, ) -> EmptyResult { let (subject, body_html, body_text) = get_text( "email/emergency_access_recovery_initiated",