Browse Source

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
BlackDex 2 năm trước cách đây
mục cha
commit
dbcad65b68

+ 4 - 4
.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.

+ 1 - 1
src/api/admin.rs

@@ -284,7 +284,7 @@ async fn invite_user(data: Json<InviteData>, _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
         }
     }

+ 99 - 92
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<Route> {
 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<EmergencyAccessInviteData>, 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<EmergencyAccessInviteData>, 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<EmergencyAccessInviteData>, 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<EmergencyAccessInviteData>, 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/<emer_id>/accept", data = "<data>")]
-async fn accept_invite(emer_id: String, data: JsonUpcase<AcceptData>, mut conn: DbConn) -> EmptyResult {
+async fn accept_invite(
+    emer_id: String,
+    data: JsonUpcase<AcceptData>,
+    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<AcceptData>, 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<AcceptData>, 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<AcceptData>, mut conn:
 
 async fn accept_invite_process(
     grantee_uuid: String,
-    emer_id: String,
-    email: Option<String>,
+    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");

+ 2 - 2
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?;
     }
 

+ 0 - 1
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.

+ 6 - 6
src/auth.rs

@@ -177,17 +177,17 @@ pub struct EmergencyAccessInviteJwtClaims {
     pub sub: String,
 
     pub email: String,
-    pub emer_id: Option<String>,
-    pub grantor_name: Option<String>,
-    pub grantor_email: Option<String>,
+    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<String>,
-    grantor_name: Option<String>,
-    grantor_email: Option<String>,
+    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());

+ 4 - 4
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();

+ 0 - 1
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<Self, Error> {
                 let url = CONFIG.database_url();
                 let conn_type = DbConnType::from_url(&url)?;

+ 47 - 31
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<String>, 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<i32> for EmergencyAccessType {
-    fn eq(&self, other: &i32) -> bool {
-        *other == *self as i32
-    }
-}
-
-impl PartialEq<EmergencyAccessType> 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<Self> {
+    pub async fn find_all_recoveries_initiated(conn: &mut DbConn) -> Vec<Self> {
         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::<EmergencyAccessDb>(conn).expect("Error loading emergency_access").from_db()
         }}
     }

+ 1 - 1
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,

+ 0 - 1
src/error.rs

@@ -168,7 +168,6 @@ impl<S> MapResult<S> for Option<S> {
     }
 }
 
-#[allow(clippy::unnecessary_wraps)]
 const fn _has_source<T>(e: T) -> Option<T> {
     Some(e)
 }

+ 9 - 9
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<String>,
-    grantor_name: Option<String>,
-    grantor_email: Option<String>,
+    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",