Browse Source
Signed-off-by: thelittlefireman <thelittlefireman@users.noreply.github.com>pull/1377/head
thelittlefireman
4 years ago
37 changed files with 1619 additions and 13 deletions
@ -0,0 +1 @@ |
|||
DROP TABLE emergency_access; |
@ -0,0 +1,14 @@ |
|||
CREATE TABLE emergency_access ( |
|||
uuid CHAR(36) NOT NULL PRIMARY KEY, |
|||
grantor_uuid CHAR(36) REFERENCES users (uuid), |
|||
grantee_uuid CHAR(36) REFERENCES users (uuid), |
|||
email VARCHAR(255), |
|||
key_encrypted TEXT, |
|||
atype INTEGER NOT NULL, |
|||
status INTEGER NOT NULL, |
|||
wait_time_days INTEGER NOT NULL, |
|||
recovery_initiated_at DATETIME, |
|||
last_notification_at DATETIME, |
|||
updated_at DATETIME NOT NULL, |
|||
created_at DATETIME NOT NULL |
|||
); |
@ -0,0 +1 @@ |
|||
DROP TABLE emergency_access; |
@ -0,0 +1,14 @@ |
|||
CREATE TABLE emergency_access ( |
|||
uuid CHAR(36) NOT NULL PRIMARY KEY, |
|||
grantor_uuid CHAR(36) REFERENCES users (uuid), |
|||
grantee_uuid CHAR(36) REFERENCES users (uuid), |
|||
email VARCHAR(255), |
|||
key_encrypted TEXT, |
|||
atype INTEGER NOT NULL, |
|||
status INTEGER NOT NULL, |
|||
wait_time_days INTEGER NOT NULL, |
|||
recovery_initiated_at TIMESTAMP, |
|||
last_notification_at TIMESTAMP, |
|||
updated_at TIMESTAMP NOT NULL, |
|||
created_at TIMESTAMP NOT NULL |
|||
); |
@ -0,0 +1 @@ |
|||
DROP TABLE emergency_access; |
@ -0,0 +1,14 @@ |
|||
CREATE TABLE emergency_access ( |
|||
uuid TEXT NOT NULL PRIMARY KEY, |
|||
grantor_uuid TEXT REFERENCES users (uuid), |
|||
grantee_uuid TEXT REFERENCES users (uuid), |
|||
email TEXT, |
|||
key_encrypted TEXT, |
|||
atype INTEGER NOT NULL, |
|||
status INTEGER NOT NULL, |
|||
wait_time_days INTEGER NOT NULL, |
|||
recovery_initiated_at DATETIME, |
|||
last_notification_at DATETIME, |
|||
updated_at DATETIME NOT NULL, |
|||
created_at DATETIME NOT NULL |
|||
); |
@ -1,24 +1,841 @@ |
|||
use chrono::{Duration, Utc}; |
|||
use rocket::Route; |
|||
use rocket_contrib::json::Json; |
|||
use serde_json::Value; |
|||
use std::borrow::Borrow; |
|||
|
|||
use crate::{api::JsonResult, auth::Headers, db::DbConn}; |
|||
use crate::{ |
|||
api::{EmptyResult, JsonResult, JsonUpcase, NumberOrString}, |
|||
auth::{decode_emergency_access_invite, Headers}, |
|||
db::{models::*, DbConn, DbPool}, |
|||
mail, CONFIG, |
|||
}; |
|||
|
|||
pub fn routes() -> Vec<Route> { |
|||
routes![get_contacts,] |
|||
routes![ |
|||
get_contacts, |
|||
get_grantees, |
|||
get_emergency_access, |
|||
put_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, |
|||
policies_emergency_access, |
|||
] |
|||
} |
|||
|
|||
/// This endpoint is expected to return at least something.
|
|||
/// If we return an error message that will trigger error toasts for the user.
|
|||
/// To prevent this we just return an empty json result with no Data.
|
|||
/// When this feature is going to be implemented it also needs to return this empty Data
|
|||
/// instead of throwing an error/4XX unless it really is an error.
|
|||
// region get
|
|||
|
|||
#[get("/emergency-access/trusted")] |
|||
fn get_contacts(_headers: Headers, _conn: DbConn) -> JsonResult { |
|||
debug!("Emergency access is not supported."); |
|||
fn get_contacts(headers: Headers, conn: DbConn) -> JsonResult { |
|||
check_emergency_access_allowed()?; |
|||
|
|||
let emergency_access_list = EmergencyAccess::find_all_by_grantor_uuid(&headers.user.uuid, &conn); |
|||
|
|||
let emergency_access_list_json: Vec<Value> = |
|||
emergency_access_list.iter().map(|e| e.to_json_grantee_details(&conn)).collect(); |
|||
|
|||
Ok(Json(json!({ |
|||
"Data": emergency_access_list_json, |
|||
"Object": "list", |
|||
"ContinuationToken": null |
|||
}))) |
|||
} |
|||
|
|||
#[get("/emergency-access/granted")] |
|||
fn get_grantees(headers: Headers, conn: DbConn) -> JsonResult { |
|||
check_emergency_access_allowed()?; |
|||
|
|||
let emergency_access_list = EmergencyAccess::find_all_by_grantee_uuid(&headers.user.uuid, &conn); |
|||
|
|||
let emergency_access_list_json: Vec<Value> = |
|||
emergency_access_list.iter().map(|e| e.to_json_grantor_details(&conn)).collect(); |
|||
|
|||
Ok(Json(json!({ |
|||
"Data": [], |
|||
"Data": emergency_access_list_json, |
|||
"Object": "list", |
|||
"ContinuationToken": null |
|||
}))) |
|||
} |
|||
|
|||
#[get("/emergency-access/<emer_id>")] |
|||
fn get_emergency_access(emer_id: String, conn: DbConn) -> JsonResult { |
|||
check_emergency_access_allowed()?; |
|||
|
|||
match EmergencyAccess::find_by_uuid(&emer_id, &conn) { |
|||
Some(emergency_access) => Ok(Json(emergency_access.to_json_grantee_details(&conn))), |
|||
None => err!("Emergency access not valid."), |
|||
} |
|||
} |
|||
|
|||
// endregion
|
|||
|
|||
// region put/post
|
|||
|
|||
#[derive(Deserialize, Debug)] |
|||
#[allow(non_snake_case)] |
|||
struct EmergencyAccessUpdateData { |
|||
Type: NumberOrString, |
|||
WaitTimeDays: i32, |
|||
KeyEncrypted: Option<String>, |
|||
} |
|||
|
|||
#[put("/emergency-access/<emer_id>", data = "<data>")] |
|||
fn put_emergency_access(emer_id: String, data: JsonUpcase<EmergencyAccessUpdateData>, conn: DbConn) -> JsonResult { |
|||
post_emergency_access(emer_id, data, conn) |
|||
} |
|||
|
|||
#[post("/emergency-access/<emer_id>", data = "<data>")] |
|||
fn post_emergency_access(emer_id: String, data: JsonUpcase<EmergencyAccessUpdateData>, conn: DbConn) -> JsonResult { |
|||
check_emergency_access_allowed()?; |
|||
|
|||
let data: EmergencyAccessUpdateData = data.into_inner().data; |
|||
|
|||
let mut emergency_access = match EmergencyAccess::find_by_uuid(&emer_id, &conn) { |
|||
Some(emergency_access) => emergency_access, |
|||
None => err!("Emergency access not valid."), |
|||
}; |
|||
|
|||
let new_type = match EmergencyAccessType::from_str(&data.Type.into_string()) { |
|||
Some(new_type) => new_type as i32, |
|||
None => err!("Invalid emergency access type."), |
|||
}; |
|||
|
|||
emergency_access.atype = new_type; |
|||
emergency_access.wait_time_days = data.WaitTimeDays; |
|||
emergency_access.key_encrypted = data.KeyEncrypted; |
|||
|
|||
emergency_access.save(&conn)?; |
|||
Ok(Json(emergency_access.to_json())) |
|||
} |
|||
|
|||
// endregion
|
|||
|
|||
// region delete
|
|||
|
|||
#[delete("/emergency-access/<emer_id>")] |
|||
fn delete_emergency_access(emer_id: String, headers: Headers, conn: DbConn) -> EmptyResult { |
|||
check_emergency_access_allowed()?; |
|||
|
|||
let grantor_user = headers.user; |
|||
|
|||
let emergency_access = match EmergencyAccess::find_by_uuid(&emer_id, &conn) { |
|||
Some(emer) => { |
|||
if emer.grantor_uuid != grantor_user.uuid && emer.grantee_uuid != Some(grantor_user.uuid) { |
|||
err!("Emergency access not valid.") |
|||
} |
|||
emer |
|||
} |
|||
None => err!("Emergency access not valid."), |
|||
}; |
|||
emergency_access.delete(&conn)?; |
|||
Ok(()) |
|||
} |
|||
|
|||
#[post("/emergency-access/<emer_id>/delete")] |
|||
fn post_delete_emergency_access(emer_id: String, headers: Headers, conn: DbConn) -> EmptyResult { |
|||
delete_emergency_access(emer_id, headers, conn) |
|||
} |
|||
|
|||
// endregion
|
|||
|
|||
// region invite
|
|||
|
|||
#[derive(Deserialize, Debug)] |
|||
#[allow(non_snake_case)] |
|||
struct EmergencyAccessInviteData { |
|||
Email: String, |
|||
Type: NumberOrString, |
|||
WaitTimeDays: i32, |
|||
} |
|||
|
|||
#[post("/emergency-access/invite", data = "<data>")] |
|||
fn send_invite(data: JsonUpcase<EmergencyAccessInviteData>, headers: Headers, conn: DbConn) -> EmptyResult { |
|||
check_emergency_access_allowed()?; |
|||
|
|||
let data: EmergencyAccessInviteData = data.into_inner().data; |
|||
let email = data.Email.to_lowercase(); |
|||
let wait_time_days = data.WaitTimeDays; |
|||
|
|||
let emergency_access_status = EmergencyAccessStatus::Invited as i32; |
|||
|
|||
let new_type = match EmergencyAccessType::from_str(&data.Type.into_string()) { |
|||
Some(new_type) => new_type as i32, |
|||
None => 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 = match User::find_by_mail(&email, &conn) { |
|||
None => { |
|||
if !CONFIG.signups_allowed() { |
|||
err!(format!("Grantee user does not exist: {}", email)) |
|||
} |
|||
|
|||
if !CONFIG.is_email_domain_allowed(&email) { |
|||
err!("Email domain not eligible for invitations") |
|||
} |
|||
|
|||
if !CONFIG.mail_enabled() { |
|||
let invitation = Invitation::new(email.clone()); |
|||
invitation.save(&conn)?; |
|||
} |
|||
|
|||
let mut user = User::new(email.clone()); |
|||
user.save(&conn)?; |
|||
user |
|||
} |
|||
Some(user) => user, |
|||
}; |
|||
|
|||
if EmergencyAccess::find_by_grantor_uuid_and_grantee_uuid_or_email( |
|||
&grantor_user.uuid, |
|||
&grantee_user.uuid, |
|||
&grantee_user.email, |
|||
&conn, |
|||
) |
|||
.is_some() |
|||
{ |
|||
err!(format!("Grantee user already invited: {}", 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, |
|||
); |
|||
new_emergency_access.save(&conn)?; |
|||
|
|||
if CONFIG.mail_enabled() { |
|||
mail::send_emergency_access_invite( |
|||
&grantee_user.email, |
|||
&grantee_user.uuid, |
|||
Some(new_emergency_access.uuid), |
|||
Some(grantor_user.name.clone()), |
|||
Some(grantor_user.email), |
|||
)?; |
|||
} else { |
|||
// Automatically mark user as accepted if no email invites
|
|||
match User::find_by_mail(&email, &conn) { |
|||
Some(user) => { |
|||
match accept_invite_process(user.uuid, new_emergency_access.uuid, Some(email), conn.borrow()) { |
|||
Ok(v) => (v), |
|||
Err(e) => err!(e.to_string()), |
|||
} |
|||
} |
|||
None => err!("Grantee user not found."), |
|||
} |
|||
} |
|||
|
|||
Ok(()) |
|||
} |
|||
|
|||
#[post("/emergency-access/<emer_id>/reinvite")] |
|||
fn resend_invite(emer_id: String, headers: Headers, conn: DbConn) -> EmptyResult { |
|||
check_emergency_access_allowed()?; |
|||
|
|||
let emergency_access = match EmergencyAccess::find_by_uuid(&emer_id, &conn) { |
|||
Some(emer) => emer, |
|||
None => err!("Emergency access not valid."), |
|||
}; |
|||
|
|||
if emergency_access.grantor_uuid != headers.user.uuid { |
|||
err!("Emergency access not valid."); |
|||
} |
|||
|
|||
if emergency_access.status != EmergencyAccessStatus::Invited as i32 { |
|||
err!("The grantee user is already accepted or confirmed to the organization"); |
|||
} |
|||
|
|||
let email = match emergency_access.email.clone() { |
|||
Some(email) => email, |
|||
None => err!("Email not valid."), |
|||
}; |
|||
|
|||
if !CONFIG.is_email_domain_allowed(&email) { |
|||
err!("Email domain not eligible for invitations.") |
|||
} |
|||
|
|||
let grantee_user = match User::find_by_mail(&email, &conn) { |
|||
None => err!("Grantee user not found."), |
|||
Some(user) => user, |
|||
}; |
|||
|
|||
let grantor_user = headers.user; |
|||
|
|||
if CONFIG.mail_enabled() { |
|||
mail::send_emergency_access_invite( |
|||
&email, |
|||
&grantor_user.uuid, |
|||
Some(emergency_access.uuid), |
|||
Some(grantor_user.name.clone()), |
|||
Some(grantor_user.email), |
|||
)?; |
|||
} else { |
|||
if Invitation::find_by_mail(&email, &conn).is_none() { |
|||
let invitation = Invitation::new(email); |
|||
invitation.save(&conn)?; |
|||
} |
|||
|
|||
// Automatically mark user as accepted if no email invites
|
|||
match accept_invite_process(grantee_user.uuid, emergency_access.uuid, emergency_access.email, conn.borrow()) { |
|||
Ok(v) => (v), |
|||
Err(e) => err!(e.to_string()), |
|||
} |
|||
} |
|||
|
|||
Ok(()) |
|||
} |
|||
|
|||
#[derive(Deserialize)] |
|||
#[allow(non_snake_case)] |
|||
struct AcceptData { |
|||
Token: String, |
|||
} |
|||
|
|||
#[post("/emergency-access/<emer_id>/accept", data = "<data>")] |
|||
fn accept_invite(emer_id: String, data: JsonUpcase<AcceptData>, 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)?; |
|||
|
|||
let grantee_user = match User::find_by_mail(&claims.email, &conn) { |
|||
Some(user) => { |
|||
Invitation::take(&claims.email, &conn); |
|||
user |
|||
} |
|||
None => err!("Invited user not found"), |
|||
}; |
|||
|
|||
let emergency_access = match EmergencyAccess::find_by_uuid(&emer_id, &conn) { |
|||
Some(emer) => emer, |
|||
None => err!("Emergency access not valid."), |
|||
}; |
|||
|
|||
// get grantor user to send Accepted email
|
|||
let grantor_user = match User::find_by_uuid(&emergency_access.grantor_uuid, &conn) { |
|||
Some(user) => user, |
|||
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()) |
|||
{ |
|||
match accept_invite_process(grantee_user.uuid.clone(), emer_id, Some(grantee_user.email.clone()), &conn) { |
|||
Ok(v) => (v), |
|||
Err(e) => err!(e.to_string()), |
|||
} |
|||
|
|||
if CONFIG.mail_enabled() { |
|||
if !CONFIG.is_email_domain_allowed(&grantor_user.email) { |
|||
err!("Email domain not valid.") |
|||
} |
|||
|
|||
mail::send_emergency_access_invite_accepted(&grantor_user.email, &grantee_user.email)?; |
|||
} |
|||
|
|||
Ok(()) |
|||
} else { |
|||
err!("Emergency access invitation error.") |
|||
} |
|||
} |
|||
|
|||
fn accept_invite_process(grantee_uuid: String, emer_id: String, email: Option<String>, conn: &DbConn) -> EmptyResult { |
|||
let mut emergency_access = match EmergencyAccess::find_by_uuid(&emer_id, conn) { |
|||
Some(emer) => emer, |
|||
None => err!("Emergency access not valid."), |
|||
}; |
|||
|
|||
let emer_email = emergency_access.email; |
|||
if emer_email.is_none() || emer_email != email { |
|||
err!("User email does not match invite."); |
|||
} |
|||
|
|||
if emergency_access.status == EmergencyAccessStatus::Accepted as i32 { |
|||
err!("Emergency contact already accepted."); |
|||
} |
|||
|
|||
emergency_access.status = EmergencyAccessStatus::Accepted as i32; |
|||
emergency_access.grantee_uuid = Some(grantee_uuid); |
|||
emergency_access.email = None; |
|||
emergency_access.save(conn) |
|||
} |
|||
|
|||
#[derive(Deserialize)] |
|||
#[allow(non_snake_case)] |
|||
struct ConfirmData { |
|||
Key: String, |
|||
} |
|||
|
|||
#[post("/emergency-access/<emer_id>/confirm", data = "<data>")] |
|||
fn confirm_emergency_access( |
|||
emer_id: String, |
|||
data: JsonUpcase<ConfirmData>, |
|||
headers: Headers, |
|||
conn: DbConn, |
|||
) -> JsonResult { |
|||
check_emergency_access_allowed()?; |
|||
|
|||
let confirming_user = headers.user; |
|||
let data: ConfirmData = data.into_inner().data; |
|||
let key = data.Key; |
|||
|
|||
let mut emergency_access = match EmergencyAccess::find_by_uuid(&emer_id, &conn) { |
|||
Some(emer) => emer, |
|||
None => 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 grantor_user = match User::find_by_uuid(&confirming_user.uuid, &conn) { |
|||
Some(user) => user, |
|||
None => err!("Grantor user not found."), |
|||
}; |
|||
|
|||
if let Some(grantee_uuid) = emergency_access.grantee_uuid.as_ref() { |
|||
let grantee_user = match User::find_by_uuid(grantee_uuid, &conn) { |
|||
Some(user) => user, |
|||
None => 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)?; |
|||
|
|||
if CONFIG.mail_enabled() { |
|||
if !CONFIG.is_email_domain_allowed(&grantee_user.email) { |
|||
err!("Email domain not valid.") |
|||
} |
|||
|
|||
mail::send_emergency_access_invite_confirmed(&grantee_user.email, &grantor_user.name)?; |
|||
} |
|||
Ok(Json(emergency_access.to_json())) |
|||
} else { |
|||
err!("Grantee user not found.") |
|||
} |
|||
} |
|||
|
|||
// endregion
|
|||
|
|||
// region access emergency access
|
|||
|
|||
#[post("/emergency-access/<emer_id>/initiate")] |
|||
fn initiate_emergency_access(emer_id: String, headers: Headers, conn: DbConn) -> JsonResult { |
|||
check_emergency_access_allowed()?; |
|||
|
|||
let initiating_user = headers.user; |
|||
let mut emergency_access = match EmergencyAccess::find_by_uuid(&emer_id, &conn) { |
|||
Some(emer) => emer, |
|||
None => err!("Emergency access not valid."), |
|||
}; |
|||
|
|||
if emergency_access.status != EmergencyAccessStatus::Confirmed as i32 |
|||
|| emergency_access.grantee_uuid != Some(initiating_user.uuid.clone()) |
|||
{ |
|||
err!("Emergency access not valid.") |
|||
} |
|||
|
|||
let grantor_user = match User::find_by_uuid(&emergency_access.grantor_uuid, &conn) { |
|||
Some(user) => user, |
|||
None => 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)?; |
|||
|
|||
if CONFIG.mail_enabled() { |
|||
if !CONFIG.is_email_domain_allowed(&grantor_user.email) { |
|||
err!("Email domain not valid.") |
|||
} |
|||
|
|||
mail::send_emergency_access_recovery_initiated( |
|||
&grantor_user.email, |
|||
&initiating_user.name, |
|||
emergency_access.get_atype_as_str(), |
|||
&emergency_access.wait_time_days.clone().to_string(), |
|||
)?; |
|||
} |
|||
Ok(Json(emergency_access.to_json())) |
|||
} |
|||
|
|||
#[post("/emergency-access/<emer_id>/approve")] |
|||
fn approve_emergency_access(emer_id: String, headers: Headers, conn: DbConn) -> JsonResult { |
|||
check_emergency_access_allowed()?; |
|||
|
|||
let approving_user = headers.user; |
|||
let mut emergency_access = match EmergencyAccess::find_by_uuid(&emer_id, &conn) { |
|||
Some(emer) => emer, |
|||
None => err!("Emergency access not valid."), |
|||
}; |
|||
|
|||
if emergency_access.status != EmergencyAccessStatus::RecoveryInitiated as i32 |
|||
|| emergency_access.grantor_uuid != approving_user.uuid |
|||
{ |
|||
err!("Emergency access not valid.") |
|||
} |
|||
|
|||
let grantor_user = match User::find_by_uuid(&approving_user.uuid, &conn) { |
|||
Some(user) => user, |
|||
None => err!("Grantor user not found."), |
|||
}; |
|||
|
|||
if let Some(grantee_uuid) = emergency_access.grantee_uuid.as_ref() { |
|||
let grantee_user = match User::find_by_uuid(grantee_uuid, &conn) { |
|||
Some(user) => user, |
|||
None => err!("Grantee user not found."), |
|||
}; |
|||
|
|||
emergency_access.status = EmergencyAccessStatus::RecoveryApproved as i32; |
|||
emergency_access.save(&conn)?; |
|||
|
|||
if CONFIG.mail_enabled() { |
|||
if !CONFIG.is_email_domain_allowed(&grantee_user.email) { |
|||
err!("Email domain not valid.") |
|||
} |
|||
|
|||
mail::send_emergency_access_recovery_approved(&grantee_user.email, &grantor_user.name)?; |
|||
} |
|||
Ok(Json(emergency_access.to_json())) |
|||
} else { |
|||
err!("Grantee user not found.") |
|||
} |
|||
} |
|||
|
|||
#[post("/emergency-access/<emer_id>/reject")] |
|||
fn reject_emergency_access(emer_id: String, headers: Headers, conn: DbConn) -> JsonResult { |
|||
check_emergency_access_allowed()?; |
|||
|
|||
let rejecting_user = headers.user; |
|||
let mut emergency_access = match EmergencyAccess::find_by_uuid(&emer_id, &conn) { |
|||
Some(emer) => emer, |
|||
None => err!("Emergency access not valid."), |
|||
}; |
|||
|
|||
if (emergency_access.status != EmergencyAccessStatus::RecoveryInitiated as i32 |
|||
&& emergency_access.status != EmergencyAccessStatus::RecoveryApproved as i32) |
|||
|| emergency_access.grantor_uuid != rejecting_user.uuid |
|||
{ |
|||
err!("Emergency access not valid.") |
|||
} |
|||
|
|||
let grantor_user = match User::find_by_uuid(&rejecting_user.uuid, &conn) { |
|||
Some(user) => user, |
|||
None => err!("Grantor user not found."), |
|||
}; |
|||
|
|||
if let Some(grantee_uuid) = emergency_access.grantee_uuid.as_ref() { |
|||
let grantee_user = match User::find_by_uuid(grantee_uuid, &conn) { |
|||
Some(user) => user, |
|||
None => err!("Grantee user not found."), |
|||
}; |
|||
|
|||
emergency_access.status = EmergencyAccessStatus::Confirmed as i32; |
|||
emergency_access.key_encrypted = None; |
|||
emergency_access.save(&conn)?; |
|||
|
|||
if CONFIG.mail_enabled() { |
|||
if !CONFIG.is_email_domain_allowed(&grantee_user.email) { |
|||
err!("Email domain not valid.") |
|||
} |
|||
|
|||
mail::send_emergency_access_recovery_rejected(&grantee_user.email, &grantor_user.name)?; |
|||
} |
|||
Ok(Json(emergency_access.to_json())) |
|||
} else { |
|||
err!("Grantee user not found.") |
|||
} |
|||
} |
|||
|
|||
// endregion
|
|||
|
|||
// region action
|
|||
|
|||
#[post("/emergency-access/<emer_id>/view")] |
|||
fn view_emergency_access(emer_id: String, headers: Headers, 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, &conn) { |
|||
Some(emer) => emer, |
|||
None => err!("Emergency access not valid."), |
|||
}; |
|||
|
|||
if !is_valid_request(&emergency_access, requesting_user.uuid, EmergencyAccessType::View) { |
|||
err!("Emergency access not valid.") |
|||
} |
|||
|
|||
let ciphers = Cipher::find_owned_by_user(&emergency_access.grantor_uuid, &conn); |
|||
|
|||
let ciphers_json: Vec<Value> = |
|||
ciphers.iter().map(|c| c.to_json(&host, &emergency_access.grantor_uuid, &conn)).collect(); |
|||
|
|||
Ok(Json(json!({ |
|||
"Ciphers": ciphers_json, |
|||
"KeyEncrypted": &emergency_access.key_encrypted, |
|||
"Object": "emergencyAccessView", |
|||
}))) |
|||
} |
|||
|
|||
#[post("/emergency-access/<emer_id>/takeover")] |
|||
fn takeover_emergency_access(emer_id: String, headers: Headers, conn: DbConn) -> JsonResult { |
|||
check_emergency_access_allowed()?; |
|||
|
|||
let requesting_user = headers.user; |
|||
let emergency_access = match EmergencyAccess::find_by_uuid(&emer_id, &conn) { |
|||
Some(emer) => emer, |
|||
None => err!("Emergency access not valid."), |
|||
}; |
|||
|
|||
if !is_valid_request(&emergency_access, requesting_user.uuid, EmergencyAccessType::Takeover) { |
|||
err!("Emergency access not valid.") |
|||
} |
|||
|
|||
let grantor_user = match User::find_by_uuid(&emergency_access.grantor_uuid, &conn) { |
|||
Some(user) => user, |
|||
None => err!("Grantor user not found."), |
|||
}; |
|||
|
|||
Ok(Json(json!({ |
|||
"Kdf": grantor_user.client_kdf_type, |
|||
"KdfIterations": grantor_user.client_kdf_iter, |
|||
"KeyEncrypted": &emergency_access.key_encrypted, |
|||
"Object": "emergencyAccessTakeover", |
|||
}))) |
|||
} |
|||
|
|||
#[derive(Deserialize, Debug)] |
|||
#[allow(non_snake_case)] |
|||
struct EmergencyAccessPasswordData { |
|||
NewMasterPasswordHash: String, |
|||
Key: String, |
|||
} |
|||
|
|||
#[post("/emergency-access/<emer_id>/password", data = "<data>")] |
|||
fn password_emergency_access( |
|||
emer_id: String, |
|||
data: JsonUpcase<EmergencyAccessPasswordData>, |
|||
headers: Headers, |
|||
conn: DbConn, |
|||
) -> EmptyResult { |
|||
check_emergency_access_allowed()?; |
|||
|
|||
let data: EmergencyAccessPasswordData = data.into_inner().data; |
|||
let new_master_password_hash = &data.NewMasterPasswordHash; |
|||
let key = data.Key; |
|||
|
|||
let requesting_user = headers.user; |
|||
let emergency_access = match EmergencyAccess::find_by_uuid(&emer_id, &conn) { |
|||
Some(emer) => emer, |
|||
None => err!("Emergency access not valid."), |
|||
}; |
|||
|
|||
if !is_valid_request(&emergency_access, requesting_user.uuid, EmergencyAccessType::Takeover) { |
|||
err!("Emergency access not valid.") |
|||
} |
|||
|
|||
let mut grantor_user = match User::find_by_uuid(&emergency_access.grantor_uuid, &conn) { |
|||
Some(user) => user, |
|||
None => err!("Grantor user not found."), |
|||
}; |
|||
|
|||
// change grantor_user password
|
|||
grantor_user.set_password(new_master_password_hash, None); |
|||
grantor_user.akey = key; |
|||
grantor_user.save(&conn)?; |
|||
|
|||
// Disable TwoFactor providers since they will otherwise block logins
|
|||
TwoFactor::delete_all_by_user(&grantor_user.uuid, &conn)?; |
|||
|
|||
// Removing owner, check that there are at least another owner
|
|||
let user_org_grantor = UserOrganization::find_any_state_by_user(&grantor_user.uuid, &conn); |
|||
|
|||
// Remove grantor from all organisations unless Owner
|
|||
for user_org in user_org_grantor { |
|||
if user_org.atype != UserOrgType::Owner as i32 { |
|||
user_org.delete(&conn)?; |
|||
} |
|||
} |
|||
Ok(()) |
|||
} |
|||
|
|||
// endregion
|
|||
|
|||
#[get("/emergency-access/<emer_id>/policies")] |
|||
fn policies_emergency_access(emer_id: String, headers: Headers, conn: DbConn) -> JsonResult { |
|||
let requesting_user = headers.user; |
|||
let emergency_access = match EmergencyAccess::find_by_uuid(&emer_id, &conn) { |
|||
Some(emer) => emer, |
|||
None => err!("Emergency access not valid."), |
|||
}; |
|||
|
|||
if !is_valid_request(&emergency_access, requesting_user.uuid, EmergencyAccessType::Takeover) { |
|||
err!("Emergency access not valid.") |
|||
} |
|||
|
|||
let grantor_user = match User::find_by_uuid(&emergency_access.grantor_uuid, &conn) { |
|||
Some(user) => user, |
|||
None => err!("Grantor user not found."), |
|||
}; |
|||
|
|||
let policies = OrgPolicy::find_by_user(&grantor_user.uuid, &conn); |
|||
let policies_json: Vec<Value> = policies.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_uuid: String, |
|||
requested_access_type: EmergencyAccessType, |
|||
) -> bool { |
|||
emergency_access.grantee_uuid == Some(requesting_user_uuid) |
|||
&& emergency_access.status == EmergencyAccessStatus::RecoveryApproved as i32 |
|||
&& emergency_access.atype == requested_access_type as i32 |
|||
} |
|||
|
|||
fn check_emergency_access_allowed() -> EmptyResult { |
|||
if !CONFIG.emergency_access_allowed() { |
|||
err!("Emergency access is not allowed.") |
|||
} |
|||
Ok(()) |
|||
} |
|||
|
|||
pub fn emergency_request_timeout_job(pool: DbPool) { |
|||
debug!("Start emergency_request_timeout_job"); |
|||
if !CONFIG.emergency_access_allowed() { |
|||
return; |
|||
} |
|||
|
|||
if let Ok(conn) = pool.get() { |
|||
let emergency_access_list = EmergencyAccess::find_all_recoveries(&conn); |
|||
|
|||
if emergency_access_list.is_empty() { |
|||
debug!("No emergency request timeout to approve"); |
|||
} |
|||
|
|||
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(emer.wait_time_days as i64) |
|||
{ |
|||
emer.status = EmergencyAccessStatus::RecoveryApproved as i32; |
|||
emer.save(&conn).expect("Cannot save emergency access on job"); |
|||
|
|||
if CONFIG.mail_enabled() { |
|||
// get grantor user to send Accepted email
|
|||
let grantor_user = User::find_by_uuid(&emer.grantor_uuid, &conn).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."), &conn) |
|||
.expect("Grantee user not found."); |
|||
|
|||
if !CONFIG.is_email_domain_allowed(&grantor_user.email) { |
|||
error!("Email domain not valid.") |
|||
} |
|||
|
|||
mail::send_emergency_access_recovery_timed_out( |
|||
&grantor_user.email, |
|||
&grantee_user.name.clone(), |
|||
emer.get_atype_as_str(), |
|||
) |
|||
.expect("Error on sending email"); |
|||
|
|||
if !CONFIG.is_email_domain_allowed(&grantee_user.email) { |
|||
error!("Email not valid.") |
|||
} |
|||
|
|||
mail::send_emergency_access_recovery_approved(&grantee_user.email, &grantor_user.name.clone()) |
|||
.expect("Error on sending email"); |
|||
} |
|||
} |
|||
} |
|||
} else { |
|||
error!("Failed to get DB connection while searching emergency request timed out") |
|||
} |
|||
} |
|||
|
|||
pub fn emergency_notification_reminder_job(pool: DbPool) { |
|||
debug!("Start emergency_notification_reminder_job"); |
|||
if !CONFIG.emergency_access_allowed() { |
|||
return; |
|||
} |
|||
|
|||
if let Ok(conn) = pool.get() { |
|||
let emergency_access_list = EmergencyAccess::find_all_recoveries(&conn); |
|||
|
|||
if emergency_access_list.is_empty() { |
|||
debug!("No emergency request reminder notification to send"); |
|||
} |
|||
|
|||
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((emer.wait_time_days as i64) - 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(&conn).expect("Cannot save emergency access on job"); |
|||
|
|||
if CONFIG.mail_enabled() { |
|||
// get grantor user to send Accepted email
|
|||
let grantor_user = User::find_by_uuid(&emer.grantor_uuid, &conn).expect("Grantor user not found."); |
|||
|
|||
if !CONFIG.is_email_domain_allowed(&grantor_user.email) { |
|||
error!("Email not valid.") |
|||
} |
|||
|
|||
// get grantee user to send Accepted email
|
|||
let grantee_user = |
|||
User::find_by_uuid(&emer.grantee_uuid.clone().expect("Grantee user invalid."), &conn) |
|||
.expect("Grantee user not found."); |
|||
|
|||
mail::send_emergency_access_recovery_reminder( |
|||
&grantor_user.email, |
|||
&grantee_user.name.clone(), |
|||
emer.get_atype_as_str(), |
|||
&emer.wait_time_days.to_string(), |
|||
) |
|||
.expect("Error on sending email"); |
|||
} |
|||
} |
|||
} |
|||
} else { |
|||
error!("Failed to get DB connection while searching emergency notification reminder") |
|||
} |
|||
} |
|||
|
@ -0,0 +1,288 @@ |
|||
use chrono::{NaiveDateTime, Utc}; |
|||
use serde_json::Value; |
|||
|
|||
use super::User; |
|||
|
|||
db_object! { |
|||
#[derive(Debug, Identifiable, Queryable, Insertable, Associations, AsChangeset)] |
|||
#[table_name = "emergency_access"] |
|||
#[changeset_options(treat_none_as_null="true")] |
|||
#[belongs_to(User, foreign_key = "grantor_uuid")] |
|||
#[primary_key(uuid)] |
|||
pub struct EmergencyAccess { |
|||
pub uuid: String, |
|||
pub grantor_uuid: String, |
|||
pub grantee_uuid: Option<String>, |
|||
pub email: Option<String>, |
|||
pub key_encrypted: Option<String>, |
|||
pub atype: i32, //EmergencyAccessType
|
|||
pub status: i32, //EmergencyAccessStatus
|
|||
pub wait_time_days: i32, |
|||
pub recovery_initiated_at: Option<NaiveDateTime>, |
|||
pub last_notification_at: Option<NaiveDateTime>, |
|||
pub updated_at: NaiveDateTime, |
|||
pub created_at: NaiveDateTime, |
|||
} |
|||
} |
|||
|
|||
/// Local methods
|
|||
|
|||
impl EmergencyAccess { |
|||
pub fn new(grantor_uuid: String, email: Option<String>, status: i32, atype: i32, wait_time_days: i32) -> Self { |
|||
Self { |
|||
uuid: crate::util::get_uuid(), |
|||
grantor_uuid, |
|||
grantee_uuid: None, |
|||
email, |
|||
status, |
|||
atype, |
|||
wait_time_days, |
|||
recovery_initiated_at: None, |
|||
created_at: Utc::now().naive_utc(), |
|||
updated_at: Utc::now().naive_utc(), |
|||
key_encrypted: None, |
|||
last_notification_at: None, |
|||
} |
|||
} |
|||
|
|||
pub fn get_atype_as_str(&self) -> &'static str { |
|||
if self.atype == EmergencyAccessType::View as i32 { |
|||
"View" |
|||
} else { |
|||
"Takeover" |
|||
} |
|||
} |
|||
|
|||
pub fn to_json(&self) -> Value { |
|||
json!({ |
|||
"Id": self.uuid, |
|||
"Status": self.status, |
|||
"Type": self.atype, |
|||
"WaitTimeDays": self.wait_time_days, |
|||
"Object": "emergencyAccess", |
|||
}) |
|||
} |
|||
|
|||
pub fn to_json_grantor_details(&self, conn: &DbConn) -> Value { |
|||
// find grantor
|
|||
let grantor_user = User::find_by_uuid(&self.grantor_uuid, conn).unwrap(); |
|||
json!({ |
|||
"Id": self.uuid, |
|||
"Status": self.status, |
|||
"Type": self.atype, |
|||
"WaitTimeDays": self.wait_time_days, |
|||
"GrantorId": grantor_user.uuid, |
|||
"Email": grantor_user.email, |
|||
"Name": grantor_user.name, |
|||
"Object": "emergencyAccessGrantorDetails",}) |
|||
} |
|||
|
|||
pub fn to_json_grantee_details(&self, conn: &DbConn) -> Value { |
|||
if self.grantee_uuid.is_some() { |
|||
let grantee_user = |
|||
User::find_by_uuid(&self.grantee_uuid.clone().unwrap(), conn).expect("Grantee user not found."); |
|||
|
|||
json!({ |
|||
"Id": self.uuid, |
|||
"Status": self.status, |
|||
"Type": self.atype, |
|||
"WaitTimeDays": self.wait_time_days, |
|||
"GranteeId": grantee_user.uuid, |
|||
"Email": grantee_user.email, |
|||
"Name": grantee_user.name, |
|||
"Object": "emergencyAccessGranteeDetails",}) |
|||
} else if self.email.is_some() { |
|||
let grantee_user = User::find_by_mail(&self.email.clone().unwrap(), conn).expect("Grantee user not found."); |
|||
json!({ |
|||
"Id": self.uuid, |
|||
"Status": self.status, |
|||
"Type": self.atype, |
|||
"WaitTimeDays": self.wait_time_days, |
|||
"GranteeId": grantee_user.uuid, |
|||
"Email": grantee_user.email, |
|||
"Name": grantee_user.name, |
|||
"Object": "emergencyAccessGranteeDetails",}) |
|||
} else { |
|||
json!({ |
|||
"Id": self.uuid, |
|||
"Status": self.status, |
|||
"Type": self.atype, |
|||
"WaitTimeDays": self.wait_time_days, |
|||
"GranteeId": "", |
|||
"Email": "", |
|||
"Name": "", |
|||
"Object": "emergencyAccessGranteeDetails",}) |
|||
} |
|||
} |
|||
} |
|||
|
|||
#[derive(Copy, Clone, PartialEq, Eq, num_derive::FromPrimitive)] |
|||
pub enum EmergencyAccessType { |
|||
View = 0, |
|||
Takeover = 1, |
|||
} |
|||
|
|||
impl EmergencyAccessType { |
|||
pub fn from_str(s: &str) -> Option<Self> { |
|||
match s { |
|||
"0" | "View" => Some(EmergencyAccessType::View), |
|||
"1" | "Takeover" => Some(EmergencyAccessType::Takeover), |
|||
_ => None, |
|||
} |
|||
} |
|||
} |
|||
|
|||
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, |
|||
Confirmed = 2, |
|||
RecoveryInitiated = 3, |
|||
RecoveryApproved = 4, |
|||
} |
|||
|
|||
// region Database methods
|
|||
|
|||
use crate::db::DbConn; |
|||
|
|||
use crate::api::EmptyResult; |
|||
use crate::error::MapResult; |
|||
|
|||
impl EmergencyAccess { |
|||
pub fn save(&mut self, conn: &DbConn) -> EmptyResult { |
|||
User::update_uuid_revision(&self.grantor_uuid, conn); |
|||
self.updated_at = Utc::now().naive_utc(); |
|||
|
|||
db_run! { conn: |
|||
sqlite, mysql { |
|||
match diesel::replace_into(emergency_access::table) |
|||
.values(EmergencyAccessDb::to_db(self)) |
|||
.execute(conn) |
|||
{ |
|||
Ok(_) => Ok(()), |
|||
// Record already exists and causes a Foreign Key Violation because replace_into() wants to delete the record first.
|
|||
Err(diesel::result::Error::DatabaseError(diesel::result::DatabaseErrorKind::ForeignKeyViolation, _)) => { |
|||
diesel::update(emergency_access::table) |
|||
.filter(emergency_access::uuid.eq(&self.uuid)) |
|||
.set(EmergencyAccessDb::to_db(self)) |
|||
.execute(conn) |
|||
.map_res("Error updating emergency access") |
|||
} |
|||
Err(e) => Err(e.into()), |
|||
}.map_res("Error saving emergency access") |
|||
} |
|||
postgresql { |
|||
let value = EmergencyAccessDb::to_db(self); |
|||
diesel::insert_into(emergency_access::table) |
|||
.values(&value) |
|||
.on_conflict(emergency_access::uuid) |
|||
.do_update() |
|||
.set(&value) |
|||
.execute(conn) |
|||
.map_res("Error saving emergency access") |
|||
} |
|||
} |
|||
} |
|||
|
|||
pub fn delete_all_by_user(user_uuid: &str, conn: &DbConn) -> EmptyResult { |
|||
for user_org in Self::find_all_by_grantor_uuid(user_uuid, conn) { |
|||
user_org.delete(conn)?; |
|||
} |
|||
for user_org in Self::find_all_by_grantee_uuid(user_uuid, conn) { |
|||
user_org.delete(conn)?; |
|||
} |
|||
Ok(()) |
|||
} |
|||
|
|||
pub fn delete(self, conn: &DbConn) -> EmptyResult { |
|||
User::update_uuid_revision(&self.grantor_uuid, conn); |
|||
|
|||
db_run! { conn: { |
|||
diesel::delete(emergency_access::table.filter(emergency_access::uuid.eq(self.uuid))) |
|||
.execute(conn) |
|||
.map_res("Error removing user from organization") |
|||
}} |
|||
} |
|||
|
|||
pub fn find_by_uuid(uuid: &str, conn: &DbConn) -> Option<Self> { |
|||
db_run! { conn: { |
|||
emergency_access::table |
|||
.filter(emergency_access::uuid.eq(uuid)) |
|||
.first::<EmergencyAccessDb>(conn) |
|||
.ok().from_db() |
|||
}} |
|||
} |
|||
|
|||
pub fn find_by_grantor_uuid_and_grantee_uuid_or_email( |
|||
grantor_uuid: &str, |
|||
grantee_uuid: &str, |
|||
email: &str, |
|||
conn: &DbConn, |
|||
) -> Option<Self> { |
|||
db_run! { conn: { |
|||
emergency_access::table |
|||
.filter(emergency_access::grantor_uuid.eq(grantor_uuid)) |
|||
.filter(emergency_access::grantee_uuid.eq(grantee_uuid).or(emergency_access::email.eq(email))) |
|||
.first::<EmergencyAccessDb>(conn) |
|||
.ok().from_db() |
|||
}} |
|||
} |
|||
|
|||
pub fn find_all_recoveries(conn: &DbConn) -> Vec<Self> { |
|||
db_run! { conn: { |
|||
emergency_access::table |
|||
.filter(emergency_access::status.eq(EmergencyAccessStatus::RecoveryInitiated as i32)) |
|||
.load::<EmergencyAccessDb>(conn).expect("Error loading emergency_access").from_db() |
|||
|
|||
}} |
|||
} |
|||
|
|||
pub fn find_by_uuid_and_grantor_uuid(uuid: &str, grantor_uuid: &str, conn: &DbConn) -> Option<Self> { |
|||
db_run! { conn: { |
|||
emergency_access::table |
|||
.filter(emergency_access::uuid.eq(uuid)) |
|||
.filter(emergency_access::grantor_uuid.eq(grantor_uuid)) |
|||
.first::<EmergencyAccessDb>(conn) |
|||
.ok().from_db() |
|||
}} |
|||
} |
|||
|
|||
pub fn find_all_by_grantee_uuid(grantee_uuid: &str, conn: &DbConn) -> Vec<Self> { |
|||
db_run! { conn: { |
|||
emergency_access::table |
|||
.filter(emergency_access::grantee_uuid.eq(grantee_uuid)) |
|||
.load::<EmergencyAccessDb>(conn).expect("Error loading emergency_access").from_db() |
|||
}} |
|||
} |
|||
|
|||
pub fn find_invited_by_grantee_email(grantee_email: &str, conn: &DbConn) -> Option<Self> { |
|||
db_run! { conn: { |
|||
emergency_access::table |
|||
.filter(emergency_access::email.eq(grantee_email)) |
|||
.filter(emergency_access::status.eq(EmergencyAccessStatus::Invited as i32)) |
|||
.first::<EmergencyAccessDb>(conn) |
|||
.ok().from_db() |
|||
}} |
|||
} |
|||
|
|||
pub fn find_all_by_grantor_uuid(grantor_uuid: &str, conn: &DbConn) -> Vec<Self> { |
|||
db_run! { conn: { |
|||
emergency_access::table |
|||
.filter(emergency_access::grantor_uuid.eq(grantor_uuid)) |
|||
.load::<EmergencyAccessDb>(conn).expect("Error loading emergency_access").from_db() |
|||
}} |
|||
} |
|||
} |
|||
|
|||
// endregion
|
@ -0,0 +1,8 @@ |
|||
Emergency contact {{{grantee_email}}} accepted |
|||
<!----------------> |
|||
This email is to notify you that {{grantee_email}} has accepted your invitation to become an emergency access contact. |
|||
|
|||
To confirm this user, Log into {{url}} the Bitwarden web vault, go to settings and confirm the user. |
|||
|
|||
If you do not wish to confirm this user, you can also remove them on the same page. |
|||
{{> email/email_footer_text }} |
@ -0,0 +1,21 @@ |
|||
Emergency contact {{{grantee_email}}} accepted |
|||
<!----------------> |
|||
{{> email/email_header }} |
|||
<table width="100%" cellpadding="0" cellspacing="0" style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;"> |
|||
<tr style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;"> |
|||
<td class="content-block" style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 0 0 10px; -webkit-text-size-adjust: none;" valign="top"> |
|||
This email is to notify you that {{grantee_email}} has accepted your invitation to become an emergency access contact. |
|||
</td> |
|||
</tr> |
|||
<tr style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;"> |
|||
<td class="content-block" style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 0 0 10px; -webkit-text-size-adjust: none;" valign="top"> |
|||
To confirm this user, <a href="{{url}}/">log into</a> the vaultwarden web vault, go to settings and confirm the user. |
|||
</td> |
|||
</tr> |
|||
<tr style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;"> |
|||
<td class="content-block last" style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 0; -webkit-text-size-adjust: none;" valign="top"> |
|||
If you do not wish to confirm this user, you can also remove them on the same page. |
|||
</td> |
|||
</tr> |
|||
</table> |
|||
{{> email/email_footer }} |
@ -0,0 +1,6 @@ |
|||
Emergency contact for {{{grantor_name}}} confirmed |
|||
<!----------------> |
|||
This email is to notify you that you have been confirmed as an emergency access contact for *{{grantor_name}}* was confirmed. |
|||
|
|||
You can now initiate emergency access requests from the web vault. Log in {{url}}. |
|||
{{> email/email_footer_text }} |
@ -0,0 +1,17 @@ |
|||
Emergency contact for {{{grantor_name}}} confirmed |
|||
<!----------------> |
|||
{{> email/email_header }} |
|||
<table width="100%" cellpadding="0" cellspacing="0" style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;"> |
|||
<tr style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;"> |
|||
<td class="content-block" style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 0 0 10px; -webkit-text-size-adjust: none;" valign="top"> |
|||
This email is to notify you that you have been confirmed as an emergency access contact for <b style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;">{{grantor_name}}</b> was confirmed. |
|||
</td> |
|||
</tr> |
|||
<tr style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;"> |
|||
<td class="content-block last" style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 0; -webkit-text-size-adjust: none;" valign="top"> |
|||
You can now initiate emergency access requests from the web vault. <br> |
|||
<a href="{{url}}/">Log in</a> |
|||
</td> |
|||
</tr> |
|||
</table> |
|||
{{> email/email_footer }} |
@ -0,0 +1,4 @@ |
|||
Emergency contact request for {{{grantor_name}}} approved |
|||
<!----------------> |
|||
{{grantor_name}} has approved your emergency request. You may now login {{url}} on the web vault and access their account. |
|||
{{> email/email_footer_text }} |
@ -0,0 +1,11 @@ |
|||
Emergency contact for {{{grantor_name}}} approved |
|||
<!----------------> |
|||
{{> email/email_header }} |
|||
<table width="100%" cellpadding="0" cellspacing="0" style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;"> |
|||
<tr style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;"> |
|||
<td class="content-block" style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 0 0 10px; -webkit-text-size-adjust: none;" valign="top"> |
|||
<b style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;">{{grantor_name}}</b> has approved your emergency request. You may now <a href="{{url}}/">login</a> on the web vault and access their account. |
|||
</td> |
|||
</tr> |
|||
</table> |
|||
{{> email/email_footer }} |
@ -0,0 +1,6 @@ |
|||
Emergency access request by {{{grantee_name}}} initiated |
|||
<!----------------> |
|||
{{grantee_name}} has initiated an emergency request to *{{atype}}* your account. You may login on the web vault and manually approve or reject this request. |
|||
|
|||
If you do nothing, the request will automatically be approved after {{wait_time_days}} day(s). |
|||
{{> email/email_footer_text }} |
@ -0,0 +1,16 @@ |
|||
Emergency access request by {{{grantee_name}}} initiated |
|||
<!----------------> |
|||
{{> email/email_header }} |
|||
<table width="100%" cellpadding="0" cellspacing="0" style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;"> |
|||
<tr style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;"> |
|||
<td class="content-block" style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 0 0 10px; -webkit-text-size-adjust: none;" valign="top"> |
|||
<b style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;">{{grantee_name}}</b> has initiated an emergency request to <b style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;">{{atype}}</b> your account. You may login on the web vault and manually approve or reject this request. |
|||
</td> |
|||
</tr> |
|||
<tr style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;"> |
|||
<td class="content-block last" style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 0; -webkit-text-size-adjust: none;" valign="top"> |
|||
If you do nothing, the request will automatically be approved after <b style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;">{{wait_time_days}}</b> day(s). |
|||
</td> |
|||
</tr> |
|||
</table> |
|||
{{> email/email_footer }} |
@ -0,0 +1,4 @@ |
|||
Emergency access request to {{{grantor_name}}} rejected |
|||
<!----------------> |
|||
{{grantor_name}} has rejected your emergency request. |
|||
{{> email/email_footer_text }} |
@ -0,0 +1,11 @@ |
|||
Emergency access request to {{{grantor_name}}} rejected |
|||
<!----------------> |
|||
{{> email/email_header }} |
|||
<table width="100%" cellpadding="0" cellspacing="0" style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;"> |
|||
<tr style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;"> |
|||
<td class="content-block" style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 0 0 10px; -webkit-text-size-adjust: none;" valign="top"> |
|||
<b style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;">{{grantor_name}}</b> has rejected your emergency request. |
|||
</td> |
|||
</tr> |
|||
</table> |
|||
{{> email/email_footer }} |
@ -0,0 +1,6 @@ |
|||
Emergency access request by {{{grantee_name}}} is pending |
|||
<!----------------> |
|||
{{grantee_name}} has a pending emergency request to *{{atype}}* your account. You may login on the web vault and manually approve or reject this request. |
|||
|
|||
If you do nothing, the request will automatically be approved after {{wait_time_days}} day(s). |
|||
{{> email/email_footer_text }} |
@ -0,0 +1,16 @@ |
|||
Emergency access request by {{{grantee_name}}} is pending |
|||
<!----------------> |
|||
{{> email/email_header }} |
|||
<table width="100%" cellpadding="0" cellspacing="0" style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;"> |
|||
<tr style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;"> |
|||
<td class="content-block" style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 0 0 10px; -webkit-text-size-adjust: none;" valign="top"> |
|||
<b style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;">{{grantee_name}}</b> has a pending emergency request to <b style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;">{{atype}}</b> your account. You may login on the web vault and manually approve or reject this request. |
|||
</td> |
|||
</tr> |
|||
<tr style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;"> |
|||
<td class="content-block last" style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 0; -webkit-text-size-adjust: none;" valign="top"> |
|||
If you do nothing, the request will automatically be approved after <b style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;">{{wait_time_days}}</b> day(s). |
|||
</td> |
|||
</tr> |
|||
</table> |
|||
{{> email/email_footer }} |
@ -0,0 +1,4 @@ |
|||
Emergency access request by {{{grantee_name}}} granted |
|||
<!----------------> |
|||
{{grantee_name}} has been granted emergency request to *{{atype}}* your account. You may login on the web vault and manually revoke this request. |
|||
{{> email/email_footer_text }} |
@ -0,0 +1,11 @@ |
|||
Emergency access request by {{{grantee_name}}} granted |
|||
<!----------------> |
|||
{{> email/email_header }} |
|||
<table width="100%" cellpadding="0" cellspacing="0" style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;"> |
|||
<tr style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;"> |
|||
<td class="content-block" style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 0 0 10px; -webkit-text-size-adjust: none;" valign="top"> |
|||
<b style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;">{{grantee_name}}</b> has been granted emergency request to <b style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;">{{atype}}</b> your account. You may login on the web vault and manually revoke this request. |
|||
</td> |
|||
</tr> |
|||
</table> |
|||
{{> email/email_footer }} |
@ -0,0 +1,8 @@ |
|||
Emergency access for {{{grantor_name}}} |
|||
<!----------------> |
|||
You have been invited to become an emergency contact for {{grantor_name}}. To accept this invite, click the following link: |
|||
|
|||
Click here to join: {{url}}/#/accept-emergency/?id={{emer_id}}&name={{grantor_name}}&email={{email}}&token={{token}} |
|||
|
|||
If you do not wish to become an emergency contact for {{grantor_name}}, you can safely ignore this email. |
|||
{{> email/email_footer_text }} |
@ -0,0 +1,24 @@ |
|||
Emergency access for {{{grantor_name}}} |
|||
<!----------------> |
|||
{{> email/email_header }} |
|||
<table width="100%" cellpadding="0" cellspacing="0" style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;"> |
|||
<tr style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;"> |
|||
<td class="content-block" style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 0 0 10px; -webkit-text-size-adjust: none; text-align: center;" valign="top" align="center"> |
|||
You have been invited to become an emergency contact for <b style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;">{{grantor_name}}</b>. |
|||
</td> |
|||
</tr> |
|||
<tr style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;"> |
|||
<td class="content-block" style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 0 0 10px; -webkit-text-size-adjust: none; text-align: center;" valign="top" align="center"> |
|||
<a href="{{url}}/#/accept-emergency/?id={{emer_id}}&name={{grantor_name}}&email={{email}}&token={{token}}" |
|||
clicktracking=off target="_blank" style="color: #ffffff; text-decoration: none; text-align: center; cursor: pointer; display: inline-block; border-radius: 5px; background-color: #3c8dbc; border-color: #3c8dbc; border-style: solid; border-width: 10px 20px; margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;"> |
|||
Become emergency contact |
|||
</a> |
|||
</td> |
|||
</tr> |
|||
<tr style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;"> |
|||
<td class="content-block last" style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 0; -webkit-text-size-adjust: none; text-align: center;" valign="top" align="center"> |
|||
If you do not wish to become an emergency contact for {{grantor_name}}, you can safely ignore this email. |
|||
</td> |
|||
</tr> |
|||
</table> |
|||
{{> email/email_footer }} |
Loading…
Reference in new issue