mirror of
https://github.com/dani-garcia/vaultwarden.wiki.git
synced 2026-11-14 16:06:06 +02:00
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.
971 lines
33 KiB
Rust
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);
|
|
}
|
|
}
|
|
}
|