mirror of
				https://github.com/dani-garcia/vaultwarden.git
				synced 2025-10-31 22:01:15 +00:00 
			
		
		
		
	Merge branch 'BlackDex-emergency-access-cleanup'
This commit is contained in:
		
				commit
				
					
						6bbb3d53ae
					
				
			
		
					 12 changed files with 173 additions and 153 deletions
				
			
		|  | @ -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. | ||||
|  |  | |||
|  | @ -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 | ||||
|         } | ||||
|     } | ||||
|  |  | |||
|  | @ -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"); | ||||
|  |  | |||
|  | @ -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?; | ||||
|     } | ||||
| 
 | ||||
|  |  | |||
|  | @ -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.
 | ||||
|  |  | |||
							
								
								
									
										12
									
								
								src/auth.rs
									
										
									
									
									
								
							
							
						
						
									
										12
									
								
								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()); | ||||
|  |  | |||
|  | @ -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(); | ||||
|  |  | |||
|  | @ -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)?; | ||||
|  |  | |||
|  | @ -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() | ||||
|         }} | ||||
|     } | ||||
|  |  | |||
|  | @ -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, | ||||
|  |  | |||
|  | @ -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) | ||||
| } | ||||
|  |  | |||
							
								
								
									
										18
									
								
								src/mail.rs
									
										
									
									
									
								
							
							
						
						
									
										18
									
								
								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", | ||||
|  |  | |||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue