Files
vaultwarden/src/api/core/emergency_access.rs
T
Daniel García f357032ee3 List attachments in sync with a stable URL
Sync and the cipher responses list each attachment with an unsigned URL of
upstream's form, which no longer changes on every sync nor costs a presign per
attachment on S3, and which isn't served without a token. The attachment routes
sign it: the cipher route and its admin variant, and the emergency access view's
route the web vault downloads through, which is added.
2026-11-14 14:24:22 +01:00

971 lines
33 KiB
Rust

use chrono::Utc;
use rocket::{Route, serde::json::Json};
use serde_json::Value;
use crate::{
CONFIG,
api::{
EmptyResult, JsonResult,
core::{
CipherSyncData, CipherSyncType,
accounts::{AuthenticationData, UnlockData},
},
},
auth::{Headers, decode_emergency_access_invite},
db::{
DbConn, DbPool,
models::{
Attachment, AttachmentId, Cipher, CipherId, EMERGENCY_ACCESS_TIMEOUT_RANGE, EmergencyAccess,
EmergencyAccessId, EmergencyAccessStatus, EmergencyAccessType, Invitation, Membership, MembershipType,
OrgPolicy, TwoFactor, User, UserId,
},
},
mail,
util::NumberOrString,
};
pub fn routes() -> Vec<Route> {
routes![
get_contacts,
get_grantees,
get_emergency_access,
put_emergency_access,
post_emergency_access,
delete_emergency_access,
post_delete_emergency_access,
send_invite,
resend_invite,
accept_invite,
confirm_emergency_access,
initiate_emergency_access,
approve_emergency_access,
reject_emergency_access,
takeover_emergency_access,
password_emergency_access,
view_emergency_access,
get_emergency_access_attachment,
policies_emergency_access,
]
}
// region get
#[get("/emergency-access/trusted")]
async fn get_contacts(headers: Headers, conn: DbConn) -> Json<Value> {
let emergency_access_list = if CONFIG.emergency_access_allowed() {
EmergencyAccess::find_all_by_grantor_uuid(&headers.user.uuid, &conn).await
} else {
Vec::new()
};
let mut emergency_access_list_json = Vec::with_capacity(emergency_access_list.len());
for ea in emergency_access_list {
if let Some(grantee) = ea.to_json_grantee_details(&conn).await {
emergency_access_list_json.push(grantee);
}
}
Json(json!({
"data": emergency_access_list_json,
"object": "list",
"continuationToken": null
}))
}
#[get("/emergency-access/granted")]
async fn get_grantees(headers: Headers, conn: DbConn) -> Json<Value> {
let emergency_access_list = if CONFIG.emergency_access_allowed() {
EmergencyAccess::find_all_by_grantee_uuid(&headers.user.uuid, &conn).await
} else {
Vec::new()
};
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(&conn).await);
}
Json(json!({
"data": emergency_access_list_json,
"object": "list",
"continuationToken": null
}))
}
#[get("/emergency-access/<emer_id>")]
async fn get_emergency_access(emer_id: EmergencyAccessId, headers: Headers, conn: DbConn) -> JsonResult {
check_emergency_access_enabled()?;
if let Some(emergency_access) =
EmergencyAccess::find_by_uuid_and_grantor_uuid(&emer_id, &headers.user.uuid, &conn).await
{
Ok(Json(
emergency_access.to_json_grantee_details(&conn).await.expect("Grantee user should exist but does not!"),
))
} else {
err!("Emergency access not valid.")
}
}
// endregion
// region put/post
#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
struct EmergencyAccessUpdateData {
r#type: NumberOrString,
wait_time_days: i32,
key_encrypted: Option<String>,
}
#[put("/emergency-access/<emer_id>", data = "<data>")]
async fn put_emergency_access(
emer_id: EmergencyAccessId,
data: Json<EmergencyAccessUpdateData>,
headers: Headers,
conn: DbConn,
) -> JsonResult {
post_emergency_access(emer_id, data, headers, conn).await
}
#[post("/emergency-access/<emer_id>", data = "<data>")]
async fn post_emergency_access(
emer_id: EmergencyAccessId,
data: Json<EmergencyAccessUpdateData>,
headers: Headers,
conn: DbConn,
) -> JsonResult {
check_emergency_access_enabled()?;
let data: EmergencyAccessUpdateData = data.into_inner();
let Some(new_type) = EmergencyAccessType::from_str(&data.r#type.into_string()) else {
err!("Invalid emergency access type.")
};
if !(EMERGENCY_ACCESS_TIMEOUT_RANGE).contains(&data.wait_time_days) {
err!("Emergency access wait time is out-of-bounds!")
}
let Some(mut emergency_access) =
EmergencyAccess::find_by_uuid_and_grantor_uuid(&emer_id, &headers.user.uuid, &conn).await
else {
err!("Emergency access not valid.")
};
emergency_access.atype = new_type as i32;
emergency_access.wait_time_days = data.wait_time_days;
if data.key_encrypted.is_some() {
emergency_access.key_encrypted = data.key_encrypted;
}
emergency_access.save(&conn).await?;
Ok(Json(emergency_access.to_json()))
}
// endregion
// region delete
#[delete("/emergency-access/<emer_id>")]
async fn delete_emergency_access(emer_id: EmergencyAccessId, headers: Headers, conn: DbConn) -> EmptyResult {
check_emergency_access_enabled()?;
let emergency_access = match (
EmergencyAccess::find_by_uuid_and_grantor_uuid(&emer_id, &headers.user.uuid, &conn).await,
EmergencyAccess::find_by_uuid_and_grantee_uuid(&emer_id, &headers.user.uuid, &conn).await,
) {
(Some(grantor_emer), None) => {
info!("Grantor deleted emergency access {emer_id}");
grantor_emer
}
(None, Some(grantee_emer)) => {
info!("Grantee deleted emergency access {emer_id}");
grantee_emer
}
_ => err!("Emergency access not valid."),
};
emergency_access.delete(&conn).await?;
Ok(())
}
#[post("/emergency-access/<emer_id>/delete")]
async fn post_delete_emergency_access(emer_id: EmergencyAccessId, headers: Headers, conn: DbConn) -> EmptyResult {
delete_emergency_access(emer_id, headers, conn).await
}
// endregion
// region invite
#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
struct EmergencyAccessInviteData {
email: String,
r#type: NumberOrString,
wait_time_days: i32,
}
#[post("/emergency-access/invite", data = "<data>")]
async fn send_invite(data: Json<EmergencyAccessInviteData>, headers: Headers, conn: DbConn) -> EmptyResult {
check_emergency_access_enabled()?;
let data: EmergencyAccessInviteData = data.into_inner();
let email = data.email.to_lowercase();
let wait_time_days = data.wait_time_days;
if !(EMERGENCY_ACCESS_TIMEOUT_RANGE).contains(&wait_time_days) {
err!("Emergency access wait time is out-of-bounds!")
}
let Some(new_type) = EmergencyAccessType::from_str(&data.r#type.into_string()) else {
err!("Invalid emergency access type.")
};
let grantor_user = headers.user;
// avoid setting yourself as emergency contact
if email == grantor_user.email {
err!("You can not set yourself as an emergency contact.")
}
let (grantee_user, new_user) = match User::find_by_mail(&email, &conn).await {
None => {
if !CONFIG.invitations_allowed() {
err!(format!("Grantee user does not exist: {}", email.escape_debug()))
}
if !CONFIG.is_email_domain_allowed(&email) {
err!("Email domain not eligible for invitations")
}
if !CONFIG.mail_enabled() {
let invitation = Invitation::new(&email);
invitation.save(&conn).await?;
}
let mut user = User::new(&email, None);
user.save(&conn).await?;
(user, true)
}
Some(user) if user.password_hash.is_empty() => (user, true),
Some(user) => (user, false),
};
if EmergencyAccess::find_by_grantor_uuid_and_grantee_uuid_or_email(
&grantor_user.uuid,
&grantee_user.uuid,
&grantee_user.email,
&conn,
)
.await
.is_some()
{
err!(format!("Grantee user already invited: {}", &grantee_user.email))
}
let new_type = new_type as i32;
let emergency_access_status = EmergencyAccessStatus::Invited as i32;
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(&conn).await?;
if CONFIG.mail_enabled() {
mail::send_emergency_access_invite(
&new_emergency_access.email.expect("Grantee email does not exists"),
grantee_user.uuid,
new_emergency_access.uuid,
&grantor_user.name,
&grantor_user.email,
)
.await?;
} else if !new_user {
// if mail is not enabled immediately accept the invitation for existing users
new_emergency_access.accept_invite(&grantee_user.uuid, &email, &conn).await?;
}
Ok(())
}
#[post("/emergency-access/<emer_id>/reinvite")]
async fn resend_invite(emer_id: EmergencyAccessId, headers: Headers, conn: DbConn) -> EmptyResult {
check_emergency_access_enabled()?;
let Some(mut emergency_access) =
EmergencyAccess::find_by_uuid_and_grantor_uuid(&emer_id, &headers.user.uuid, &conn).await
else {
err!("Emergency access not valid.")
};
if emergency_access.status != EmergencyAccessStatus::Invited as i32 {
err!("The grantee user is already accepted or confirmed for emergency access");
}
let Some(email) = emergency_access.email.clone() else {
err!("Email not valid.")
};
let Some(grantee_user) = User::find_by_mail(&email, &conn).await else {
err!("Grantee user not found.")
};
let grantor_user = headers.user;
if CONFIG.mail_enabled() {
mail::send_emergency_access_invite(
&email,
grantor_user.uuid,
emergency_access.uuid,
&grantor_user.name,
&grantor_user.email,
)
.await?;
} else if !grantee_user.password_hash.is_empty() {
// accept the invitation for existing user
emergency_access.accept_invite(&grantee_user.uuid, &email, &conn).await?;
} else if CONFIG.invitations_allowed() && Invitation::find_by_mail(&email, &conn).await.is_none() {
let invitation = Invitation::new(&email);
invitation.save(&conn).await?;
}
Ok(())
}
#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
struct AcceptData {
token: String,
}
#[post("/emergency-access/<emer_id>/accept", data = "<data>")]
async fn accept_invite(
emer_id: EmergencyAccessId,
data: Json<AcceptData>,
headers: Headers,
conn: DbConn,
) -> EmptyResult {
check_emergency_access_enabled()?;
let data: AcceptData = data.into_inner();
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 intended, 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 = if let Some(user) = User::find_by_mail(&claims.email, &conn).await {
Invitation::take(&claims.email, &conn).await;
user
} else {
err!("Invited user not found")
};
// We need to search for the uuid in combination with the email, since we do not yet store the uuid of the grantee in the database.
// The uuid of the grantee gets stored once accepted.
let Some(mut emergency_access) =
EmergencyAccess::find_by_uuid_and_grantee_email(&emer_id, &headers.user.email, &conn).await
else {
err!("Emergency access not valid.")
};
// get grantor user to send Accepted email
let Some(grantor_user) = User::find_by_uuid(&emergency_access.grantor_uuid, &conn).await else {
err!("Grantor user not found.")
};
if emer_id == claims.emer_id
&& grantor_user.name == claims.grantor_name
&& grantor_user.email == claims.grantor_email
{
emergency_access.accept_invite(&grantee_user.uuid, &grantee_user.email, &conn).await?;
if CONFIG.mail_enabled() {
mail::send_emergency_access_invite_accepted(&grantor_user.email, &grantee_user.email).await?;
}
Ok(())
} else {
err!("Emergency access invitation error.")
}
}
#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
struct ConfirmData {
key: String,
}
#[post("/emergency-access/<emer_id>/confirm", data = "<data>")]
async fn confirm_emergency_access(
emer_id: EmergencyAccessId,
data: Json<ConfirmData>,
headers: Headers,
conn: DbConn,
) -> JsonResult {
check_emergency_access_enabled()?;
let confirming_user = headers.user;
let data: ConfirmData = data.into_inner();
let key = data.key;
let Some(mut emergency_access) =
EmergencyAccess::find_by_uuid_and_grantor_uuid(&emer_id, &confirming_user.uuid, &conn).await
else {
err!("Emergency access not valid.")
};
if emergency_access.status != EmergencyAccessStatus::Accepted as i32
|| emergency_access.grantor_uuid != confirming_user.uuid
{
err!("Emergency access not valid.")
}
let Some(grantor_user) = User::find_by_uuid(&confirming_user.uuid, &conn).await else {
err!("Grantor user not found.")
};
if let Some(grantee_uuid) = emergency_access.grantee_uuid.as_ref() {
let Some(grantee_user) = User::find_by_uuid(grantee_uuid, &conn).await else {
err!("Grantee user not found.")
};
emergency_access.status = EmergencyAccessStatus::Confirmed as i32;
emergency_access.key_encrypted = Some(key);
emergency_access.email = None;
emergency_access.save(&conn).await?;
if CONFIG.mail_enabled() {
mail::send_emergency_access_invite_confirmed(&grantee_user.email, &grantor_user.name).await?;
}
Ok(Json(emergency_access.to_json()))
} else {
err!("Grantee user not found.")
}
}
// endregion
// region access emergency access
#[post("/emergency-access/<emer_id>/initiate")]
async fn initiate_emergency_access(emer_id: EmergencyAccessId, headers: Headers, conn: DbConn) -> JsonResult {
check_emergency_access_enabled()?;
let initiating_user = headers.user;
let Some(mut emergency_access) =
EmergencyAccess::find_by_uuid_and_grantee_uuid(&emer_id, &initiating_user.uuid, &conn).await
else {
err!(
"Emergency access not valid.",
format!("Emergency access '{emer_id}', not found for user '{}'.", initiating_user.email)
)
};
if emergency_access.status != EmergencyAccessStatus::Confirmed as i32 {
err!(
"Emergency access not valid.",
format!("Emergency access '{emer_id}', not confirmed for user '{}'.", initiating_user.email)
)
}
if !EMERGENCY_ACCESS_TIMEOUT_RANGE.contains(&emergency_access.wait_time_days) {
err!(
"Emergency access not valid.",
format!("Emergency access '{emer_id}' timeout range '{}' is invalid", emergency_access.wait_time_days)
)
}
let Some(grantor_user) = User::find_by_uuid(&emergency_access.grantor_uuid, &conn).await else {
err!("Grantor user not found.")
};
let now = Utc::now().naive_utc();
emergency_access.status = EmergencyAccessStatus::RecoveryInitiated as i32;
emergency_access.updated_at = now;
emergency_access.recovery_initiated_at = Some(now);
emergency_access.last_notification_at = Some(now);
emergency_access.save(&conn).await?;
if CONFIG.mail_enabled() {
mail::send_emergency_access_recovery_initiated(
&grantor_user.email,
&initiating_user.name,
emergency_access.get_type_as_str(),
&emergency_access.wait_time_days,
)
.await?;
}
Ok(Json(emergency_access.to_json()))
}
#[post("/emergency-access/<emer_id>/approve")]
async fn approve_emergency_access(emer_id: EmergencyAccessId, headers: Headers, conn: DbConn) -> JsonResult {
check_emergency_access_enabled()?;
let Some(mut emergency_access) =
EmergencyAccess::find_by_uuid_and_grantor_uuid(&emer_id, &headers.user.uuid, &conn).await
else {
err!(
"Emergency access not valid.",
format!("Emergency access '{emer_id}', not found for user '{}'.", headers.user.email)
)
};
if emergency_access.status != EmergencyAccessStatus::RecoveryInitiated as i32 {
err!(
"Emergency access not valid.",
format!("Emergency access '{emer_id}', not initiated for user '{}'.", headers.user.email)
)
}
let Some(grantor_user) = User::find_by_uuid(&headers.user.uuid, &conn).await else {
err!("Grantor user not found.")
};
if let Some(grantee_uuid) = emergency_access.grantee_uuid.as_ref() {
let Some(grantee_user) = User::find_by_uuid(grantee_uuid, &conn).await else {
err!("Grantee user not found.")
};
emergency_access.status = EmergencyAccessStatus::RecoveryApproved as i32;
emergency_access.save(&conn).await?;
if CONFIG.mail_enabled() {
mail::send_emergency_access_recovery_approved(&grantee_user.email, &grantor_user.name).await?;
}
Ok(Json(emergency_access.to_json()))
} else {
err!("Grantee user not found.")
}
}
#[post("/emergency-access/<emer_id>/reject")]
async fn reject_emergency_access(emer_id: EmergencyAccessId, headers: Headers, conn: DbConn) -> JsonResult {
check_emergency_access_enabled()?;
let Some(mut emergency_access) =
EmergencyAccess::find_by_uuid_and_grantor_uuid(&emer_id, &headers.user.uuid, &conn).await
else {
err!("Emergency access not valid.")
};
if emergency_access.status != EmergencyAccessStatus::RecoveryInitiated as i32
&& emergency_access.status != EmergencyAccessStatus::RecoveryApproved as i32
{
err!("Emergency access not valid.")
}
if let Some(grantee_uuid) = emergency_access.grantee_uuid.as_ref() {
let Some(grantee_user) = User::find_by_uuid(grantee_uuid, &conn).await else {
err!("Grantee user not found.")
};
emergency_access.status = EmergencyAccessStatus::Confirmed as i32;
emergency_access.save(&conn).await?;
if CONFIG.mail_enabled() {
mail::send_emergency_access_recovery_rejected(&grantee_user.email, &headers.user.name).await?;
}
Ok(Json(emergency_access.to_json()))
} else {
err!("Grantee user not found.")
}
}
// endregion
// region action
#[post("/emergency-access/<emer_id>/view")]
async fn view_emergency_access(emer_id: EmergencyAccessId, headers: Headers, conn: DbConn) -> JsonResult {
check_emergency_access_enabled()?;
let Some(emergency_access) =
EmergencyAccess::find_by_uuid_and_grantee_uuid(&emer_id, &headers.user.uuid, &conn).await
else {
err!("Emergency access not valid.")
};
if !is_valid_request(&emergency_access, &headers.user.uuid, EmergencyAccessType::View) {
err!("Emergency access not valid.")
}
let ciphers = Cipher::find_owned_by_user(&emergency_access.grantor_uuid, &conn).await;
let cipher_sync_data = CipherSyncData::new(&emergency_access.grantor_uuid, CipherSyncType::User, &conn).await;
let mut ciphers_json = Vec::with_capacity(ciphers.len());
for c in ciphers {
ciphers_json.push(
c.to_json(
&headers.host,
&emergency_access.grantor_uuid,
Some(&cipher_sync_data),
CipherSyncType::User,
&conn,
)
.await?,
);
}
Ok(Json(json!({
"ciphers": ciphers_json,
"keyEncrypted": &emergency_access.key_encrypted,
"object": "emergencyAccessView",
})))
}
/// An attachment of the grantor's for a grantee with approved view access, with a `url` to download it from: the view
/// lists the attachments with one that only names the file. Only the grantor's own ciphers, the ones the view lists.
///
/// Ref: <https://github.com/bitwarden/server/blob/341acc9d3d8988cec8fe05debfa6d557cf5ab749/src/Api/Auth/Controllers/EmergencyAccessController.cs#L219-L226>
#[get("/emergency-access/<emer_id>/<cipher_id>/attachment/<attachment_id>")]
async fn get_emergency_access_attachment(
emer_id: EmergencyAccessId,
cipher_id: CipherId,
attachment_id: AttachmentId,
headers: Headers,
conn: DbConn,
) -> JsonResult {
check_emergency_access_enabled()?;
let Some(emergency_access) =
EmergencyAccess::find_by_uuid_and_grantee_uuid(&emer_id, &headers.user.uuid, &conn).await
else {
err!("Emergency access not valid.")
};
if !is_valid_request(&emergency_access, &headers.user.uuid, EmergencyAccessType::View) {
err!("Emergency access not valid.")
}
let Some(cipher) = Cipher::find_by_uuid(&cipher_id, &conn).await else {
err!("Cipher doesn't exist")
};
if cipher.user_uuid.as_ref() != Some(&emergency_access.grantor_uuid) || cipher.organization_uuid.is_some() {
err!("Cipher is not accessible")
}
match Attachment::find_by_id(&attachment_id, &conn).await {
Some(attachment) if attachment.cipher_uuid == cipher_id => {
Ok(Json(attachment.to_json_for_download(&headers.host).await?))
}
Some(_) => err!("Attachment doesn't belong to cipher"),
None => err!("Attachment doesn't exist"),
}
}
#[post("/emergency-access/<emer_id>/takeover")]
async fn takeover_emergency_access(emer_id: EmergencyAccessId, headers: Headers, conn: DbConn) -> JsonResult {
check_emergency_access_enabled()?;
let requesting_user = headers.user;
let Some(emergency_access) =
EmergencyAccess::find_by_uuid_and_grantee_uuid(&emer_id, &requesting_user.uuid, &conn).await
else {
err!("Emergency access not valid.")
};
if !is_valid_request(&emergency_access, &requesting_user.uuid, EmergencyAccessType::Takeover) {
err!("Emergency access not valid.")
}
let Some(grantor_user) = User::find_by_uuid(&emergency_access.grantor_uuid, &conn).await else {
err!("Grantor user not found.")
};
let result = json!({
"kdf": grantor_user.client_kdf_type,
"kdfIterations": grantor_user.client_kdf_iter,
"kdfMemory": grantor_user.client_kdf_memory,
"kdfParallelism": grantor_user.client_kdf_parallelism,
"keyEncrypted": &emergency_access.key_encrypted,
"salt": grantor_user.master_password_salt(),
"object": "emergencyAccessTakeover",
});
Ok(Json(result))
}
#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
struct EmergencyAccessPasswordData {
// Legacy payload
new_master_password_hash: Option<String>,
key: Option<String>,
// Current payload
authentication_data: Option<AuthenticationData>,
unlock_data: Option<UnlockData>,
}
#[post("/emergency-access/<emer_id>/password", data = "<data>")]
async fn password_emergency_access(
emer_id: EmergencyAccessId,
data: Json<EmergencyAccessPasswordData>,
headers: Headers,
conn: DbConn,
) -> EmptyResult {
check_emergency_access_enabled()?;
let data: EmergencyAccessPasswordData = data.into_inner();
let requesting_user = headers.user;
let Some(emergency_access) =
EmergencyAccess::find_by_uuid_and_grantee_uuid(&emer_id, &requesting_user.uuid, &conn).await
else {
err!("Emergency access not valid.")
};
if !is_valid_request(&emergency_access, &requesting_user.uuid, EmergencyAccessType::Takeover) {
err!("Emergency access not valid.")
}
let Some(mut grantor_user) = User::find_by_uuid(&emergency_access.grantor_uuid, &conn).await else {
err!("Grantor user not found.")
};
let (new_master_password_hash, new_key) =
if let (Some(authentication_data), Some(unlock_data)) = (data.authentication_data, data.unlock_data) {
authentication_data.check(&grantor_user, &unlock_data)?;
if !authentication_data.kdf.matches_user(&grantor_user) {
err!("KDF settings do not match the grantor account")
}
(authentication_data.master_password_authentication_hash, unlock_data.master_key_wrapped_user_key)
} else if let (Some(new_master_password_hash), Some(new_key)) = (data.new_master_password_hash, data.key) {
(new_master_password_hash, new_key)
} else {
err!("Invalid request!")
};
// change grantor_user password
grantor_user.set_password(&new_master_password_hash, Some(new_key), true, None, &conn).await?;
grantor_user.save(&conn).await?;
// Disable TwoFactor providers since they will otherwise block logins
TwoFactor::delete_all_by_user(&grantor_user.uuid, &conn).await?;
// Remove grantor from all organisations unless Owner
for member in Membership::find_any_state_by_user(&grantor_user.uuid, &conn).await {
if member.atype != MembershipType::Owner as i32 {
member.delete(&conn).await?;
}
}
Ok(())
}
// endregion
#[get("/emergency-access/<emer_id>/policies")]
async fn policies_emergency_access(emer_id: EmergencyAccessId, headers: Headers, conn: DbConn) -> JsonResult {
let requesting_user = headers.user;
let Some(emergency_access) =
EmergencyAccess::find_by_uuid_and_grantee_uuid(&emer_id, &requesting_user.uuid, &conn).await
else {
err!("Emergency access not valid.")
};
if !is_valid_request(&emergency_access, &requesting_user.uuid, EmergencyAccessType::Takeover) {
err!("Emergency access not valid.")
}
let Some(grantor_user) = User::find_by_uuid(&emergency_access.grantor_uuid, &conn).await else {
err!("Grantor user not found.")
};
let policies = OrgPolicy::find_confirmed_by_user(&grantor_user.uuid, &conn);
let policies_json: Vec<Value> = policies.await.iter().map(OrgPolicy::to_json).collect();
Ok(Json(json!({
"data": policies_json,
"object": "list",
"continuationToken": null
})))
}
fn is_valid_request(
emergency_access: &EmergencyAccess,
requesting_user_id: &UserId,
requested_access_type: EmergencyAccessType,
) -> bool {
emergency_access.grantee_uuid.is_some()
&& emergency_access.grantee_uuid.as_ref().unwrap() == requesting_user_id
&& emergency_access.status == EmergencyAccessStatus::RecoveryApproved as i32
&& emergency_access.atype == requested_access_type as i32
}
fn check_emergency_access_enabled() -> EmptyResult {
if !CONFIG.emergency_access_allowed() {
err!("Emergency access is not enabled.")
}
Ok(())
}
pub async fn emergency_request_timeout_job(pool: DbPool) {
debug!("Start emergency_request_timeout_job");
if !CONFIG.emergency_access_allowed() {
return;
}
let Ok(conn) = pool.get().await else {
error!("Failed to get DB connection for emergency time-outs!");
return;
};
let emergency_access_list = EmergencyAccess::find_all_recoveries_initiated(&conn).await;
if emergency_access_list.is_empty() {
debug!("No emergency request timeout to approve");
return;
}
let now = Utc::now().naive_utc();
for mut emer in emergency_access_list {
match emer.is_recovery_allowed(&now) {
Ok(true) => {} // Recovery is allowed
Ok(false) => continue,
Err(e) => {
error!("Failed to check if recovery is allowed for '{}': {e}", emer.uuid);
continue;
}
}
// Get grantor user to send Accepted email
let Some(grantor_user) = User::find_by_uuid(&emer.grantor_uuid, &conn).await else {
error!("Grantor user '{}' not found for emergency access '{}'", emer.grantor_uuid, emer.uuid);
continue;
};
let Some(grantee_uuid) = emer.grantee_uuid.as_ref() else {
error!("Missing grantee for emergency access '{}'", emer.uuid);
continue;
};
// Get grantee user to send Accepted email
let Some(grantee_user) = User::find_by_uuid(grantee_uuid, &conn).await else {
error!("Grantee user '{grantee_uuid}' not found for emergency access '{}'", emer.uuid);
continue;
};
// Only update the access status
// Updating the whole record could cause issues when the emergency_notification_reminder_job is also active
let status_initiated = EmergencyAccessStatus::RecoveryInitiated as i32;
let status_approved = EmergencyAccessStatus::RecoveryApproved as i32;
if let Err(e) = emer.update_access_status_and_save(status_initiated, status_approved, &now, &conn).await {
error!("Unable to update emergency access status for '{}': {e}", emer.uuid);
continue;
}
if CONFIG.mail_enabled() {
if let Err(e) = mail::send_emergency_access_recovery_timed_out(
&grantor_user.email,
&grantee_user.name,
emer.get_type_as_str(),
)
.await
{
error!("Failed to send emergency access recovery time-out email for '{}': {e}", emer.uuid);
}
if let Err(e) = mail::send_emergency_access_recovery_approved(&grantee_user.email, &grantor_user.name).await
{
error!("Failed to send emergency access recovery approved email for '{}': {e}", emer.uuid);
}
}
}
}
pub async fn emergency_notification_reminder_job(pool: DbPool) {
debug!("Start emergency_notification_reminder_job");
if !CONFIG.emergency_access_allowed() {
return;
}
let Ok(conn) = pool.get().await else {
error!("Failed to get DB connection for emergency reminders!");
return;
};
let emergency_access_list = EmergencyAccess::find_all_recoveries_initiated(&conn).await;
if emergency_access_list.is_empty() {
debug!("No emergency request reminder notification to send");
return;
}
let now = Utc::now().naive_utc();
for mut emer in emergency_access_list {
let Ok(deadline_at) = emer.deadline_at() else {
error!("Invalid deadline for emergency access '{}'", emer.uuid);
continue;
};
let Some(final_reminder_at) = deadline_at.checked_sub_days(chrono::Days::new(1)) else {
error!("Cannot calculate reminder date for emergency access '{}'", emer.uuid);
continue;
};
// Only send reminders during the last 24 hours, else continue
if now < final_reminder_at || now >= deadline_at {
continue;
}
// Check if we did not sent a recovery mail in the past 24h already, else skip it.
// In case something goes wrong with the date calculation or `last_notification_at` is None we use now.
// This is to prevent possible missed reminders, this might cause an extra reminder,
// but that is better then no reminder at all.
let next_reminder_at = if let Some(last_notification_at) = emer.last_notification_at {
last_notification_at.checked_add_days(chrono::Days::new(1)).unwrap_or(now)
} else {
now
};
// Only send a reminder if the `next_reminder_at` is less then or equal to `now`
// Else we can skip the current emergency access this run.
if next_reminder_at.gt(&now) {
continue;
}
// Get grantor user to send Reminder email
let Some(grantor_user) = User::find_by_uuid(&emer.grantor_uuid, &conn).await else {
error!("Grantor user '{}' not found for emergency access '{}'", emer.grantor_uuid, emer.uuid);
continue;
};
let Some(grantee_uuid) = emer.grantee_uuid.as_ref() else {
error!("Missing grantee for emergency access '{}'", emer.uuid);
continue;
};
// Get grantee user to send Reminder email
let Some(grantee_user) = User::find_by_uuid(grantee_uuid, &conn).await else {
error!("Grantee user '{grantee_uuid}' not found for emergency access '{}'", emer.uuid);
continue;
};
// Only update the last notification date
// Updating the whole record could cause issues when the emergency_request_timeout_job is also active
if let Err(e) = emer.update_last_notification_date_and_save(&now, &conn).await {
error!("Unable to update emergency access reminder date for '{}': {e}", emer.uuid);
continue;
}
if CONFIG.mail_enabled()
&& let Err(e) = mail::send_emergency_access_recovery_reminder(
&grantor_user.email,
&grantee_user.name,
emer.get_type_as_str(),
"1", // This notification is only triggered one day before the activation
)
.await
{
error!("Failed to send emergency access reminder email for '{}': {e}", emer.uuid);
}
}
}