From 930c62cdaedf42d23d160a1e75c1e01852ec105a Mon Sep 17 00:00:00 2001 From: Stefan Melmuk Date: Wed, 25 Dec 2024 05:48:33 +0100 Subject: [PATCH] add emergency_access_id --- src/api/core/accounts.rs | 6 +-- src/api/core/emergency_access.rs | 71 +++++++++++++++++-------------- src/auth.rs | 20 ++++----- src/db/models/emergency_access.rs | 42 +++++++++++++++--- src/db/models/mod.rs | 2 +- src/mail.rs | 14 +++--- 6 files changed, 96 insertions(+), 59 deletions(-) diff --git a/src/api/core/accounts.rs b/src/api/core/accounts.rs index 287c8eb0710..f8e1ff87ab2 100644 --- a/src/api/core/accounts.rs +++ b/src/api/core/accounts.rs @@ -452,7 +452,7 @@ struct UpdateFolderData { #[derive(Deserialize)] #[serde(rename_all = "camelCase")] struct UpdateEmergencyAccessData { - id: String, + id: EmergencyAccessId, key_encrypted: String, } @@ -508,9 +508,9 @@ fn validate_keydata( // Check that we're correctly rotating all the user's emergency access keys let existing_emergency_access_ids = - existing_emergency_access.iter().map(|ea| ea.uuid.as_str()).collect::>(); + existing_emergency_access.iter().map(|ea| &ea.uuid).collect::>(); let provided_emergency_access_ids = - data.emergency_access_keys.iter().map(|ea| ea.id.as_str()).collect::>(); + data.emergency_access_keys.iter().map(|ea| &ea.id).collect::>(); if !provided_emergency_access_ids.is_superset(&existing_emergency_access_ids) { err!("All existing emergency access keys must be included in the rotation") } diff --git a/src/api/core/emergency_access.rs b/src/api/core/emergency_access.rs index 7a609ee4f7f..8c6fcb6538b 100644 --- a/src/api/core/emergency_access.rs +++ b/src/api/core/emergency_access.rs @@ -93,10 +93,10 @@ async fn get_grantees(headers: Headers, mut conn: DbConn) -> Json { } #[get("/emergency-access/")] -async fn get_emergency_access(emer_id: &str, headers: Headers, mut conn: DbConn) -> JsonResult { +async fn get_emergency_access(emer_id: EmergencyAccessId, headers: Headers, mut conn: DbConn) -> JsonResult { check_emergency_access_enabled()?; - match EmergencyAccess::find_by_uuid_and_grantor_uuid(emer_id, &headers.user.uuid, &mut conn).await { + match EmergencyAccess::find_by_uuid_and_grantor_uuid(&emer_id, &headers.user.uuid, &mut conn).await { Some(emergency_access) => Ok(Json( emergency_access.to_json_grantee_details(&mut conn).await.expect("Grantee user should exist but does not!"), )), @@ -118,7 +118,7 @@ struct EmergencyAccessUpdateData { #[put("/emergency-access/", data = "")] async fn put_emergency_access( - emer_id: &str, + emer_id: EmergencyAccessId, data: Json, headers: Headers, conn: DbConn, @@ -128,7 +128,7 @@ async fn put_emergency_access( #[post("/emergency-access/", data = "")] async fn post_emergency_access( - emer_id: &str, + emer_id: EmergencyAccessId, data: Json, headers: Headers, mut conn: DbConn, @@ -138,7 +138,7 @@ async fn post_emergency_access( let data: EmergencyAccessUpdateData = data.into_inner(); let Some(mut emergency_access) = - EmergencyAccess::find_by_uuid_and_grantor_uuid(emer_id, &headers.user.uuid, &mut conn).await + EmergencyAccess::find_by_uuid_and_grantor_uuid(&emer_id, &headers.user.uuid, &mut conn).await else { err!("Emergency access not valid.") }; @@ -163,12 +163,12 @@ async fn post_emergency_access( // region delete #[delete("/emergency-access/")] -async fn delete_emergency_access(emer_id: &str, headers: Headers, mut conn: DbConn) -> EmptyResult { +async fn delete_emergency_access(emer_id: EmergencyAccessId, headers: Headers, mut conn: DbConn) -> EmptyResult { check_emergency_access_enabled()?; let emergency_access = match ( - EmergencyAccess::find_by_uuid_and_grantor_uuid(emer_id, &headers.user.uuid, &mut conn).await, - EmergencyAccess::find_by_uuid_and_grantee_uuid(emer_id, &headers.user.uuid, &mut conn).await, + EmergencyAccess::find_by_uuid_and_grantor_uuid(&emer_id, &headers.user.uuid, &mut conn).await, + EmergencyAccess::find_by_uuid_and_grantee_uuid(&emer_id, &headers.user.uuid, &mut conn).await, ) { (Some(grantor_emer), None) => { info!("Grantor deleted emergency access {emer_id}"); @@ -186,7 +186,7 @@ async fn delete_emergency_access(emer_id: &str, headers: Headers, mut conn: DbCo } #[post("/emergency-access//delete")] -async fn post_delete_emergency_access(emer_id: &str, headers: Headers, conn: DbConn) -> EmptyResult { +async fn post_delete_emergency_access(emer_id: EmergencyAccessId, headers: Headers, conn: DbConn) -> EmptyResult { delete_emergency_access(emer_id, headers, conn).await } @@ -266,8 +266,8 @@ async fn send_invite(data: Json, headers: Headers, mu 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, + grantee_user.uuid, + new_emergency_access.uuid, &grantor_user.name, &grantor_user.email, ) @@ -281,11 +281,11 @@ async fn send_invite(data: Json, headers: Headers, mu } #[post("/emergency-access//reinvite")] -async fn resend_invite(emer_id: &str, headers: Headers, mut conn: DbConn) -> EmptyResult { +async fn resend_invite(emer_id: EmergencyAccessId, headers: Headers, mut conn: DbConn) -> EmptyResult { check_emergency_access_enabled()?; let Some(mut emergency_access) = - EmergencyAccess::find_by_uuid_and_grantor_uuid(emer_id, &headers.user.uuid, &mut conn).await + EmergencyAccess::find_by_uuid_and_grantor_uuid(&emer_id, &headers.user.uuid, &mut conn).await else { err!("Emergency access not valid.") }; @@ -307,8 +307,8 @@ async fn resend_invite(emer_id: &str, headers: Headers, mut conn: DbConn) -> Emp if CONFIG.mail_enabled() { mail::send_emergency_access_invite( &email, - &grantor_user.uuid, - &emergency_access.uuid, + grantor_user.uuid, + emergency_access.uuid, &grantor_user.name, &grantor_user.email, ) @@ -331,7 +331,12 @@ struct AcceptData { } #[post("/emergency-access//accept", data = "")] -async fn accept_invite(emer_id: &str, data: Json, headers: Headers, mut conn: DbConn) -> EmptyResult { +async fn accept_invite( + emer_id: EmergencyAccessId, + data: Json, + headers: Headers, + mut conn: DbConn, +) -> EmptyResult { check_emergency_access_enabled()?; let data: AcceptData = data.into_inner(); @@ -355,7 +360,7 @@ async fn accept_invite(emer_id: &str, data: Json, headers: Headers, // 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, &mut conn).await + EmergencyAccess::find_by_uuid_and_grantee_email(&emer_id, &headers.user.email, &mut conn).await else { err!("Emergency access not valid.") }; @@ -389,7 +394,7 @@ struct ConfirmData { #[post("/emergency-access//confirm", data = "")] async fn confirm_emergency_access( - emer_id: &str, + emer_id: EmergencyAccessId, data: Json, headers: Headers, mut conn: DbConn, @@ -401,7 +406,7 @@ async fn confirm_emergency_access( let key = data.key; let Some(mut emergency_access) = - EmergencyAccess::find_by_uuid_and_grantor_uuid(emer_id, &confirming_user.uuid, &mut conn).await + EmergencyAccess::find_by_uuid_and_grantor_uuid(&emer_id, &confirming_user.uuid, &mut conn).await else { err!("Emergency access not valid.") }; @@ -441,12 +446,12 @@ async fn confirm_emergency_access( // region access emergency access #[post("/emergency-access//initiate")] -async fn initiate_emergency_access(emer_id: &str, headers: Headers, mut conn: DbConn) -> JsonResult { +async fn initiate_emergency_access(emer_id: EmergencyAccessId, headers: Headers, mut 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, &mut conn).await + EmergencyAccess::find_by_uuid_and_grantee_uuid(&emer_id, &initiating_user.uuid, &mut conn).await else { err!("Emergency access not valid.") }; @@ -479,11 +484,11 @@ async fn initiate_emergency_access(emer_id: &str, headers: Headers, mut conn: Db } #[post("/emergency-access//approve")] -async fn approve_emergency_access(emer_id: &str, headers: Headers, mut conn: DbConn) -> JsonResult { +async fn approve_emergency_access(emer_id: EmergencyAccessId, headers: Headers, mut conn: DbConn) -> JsonResult { check_emergency_access_enabled()?; let Some(mut emergency_access) = - EmergencyAccess::find_by_uuid_and_grantor_uuid(emer_id, &headers.user.uuid, &mut conn).await + EmergencyAccess::find_by_uuid_and_grantor_uuid(&emer_id, &headers.user.uuid, &mut conn).await else { err!("Emergency access not valid.") }; @@ -514,11 +519,11 @@ async fn approve_emergency_access(emer_id: &str, headers: Headers, mut conn: DbC } #[post("/emergency-access//reject")] -async fn reject_emergency_access(emer_id: &str, headers: Headers, mut conn: DbConn) -> JsonResult { +async fn reject_emergency_access(emer_id: EmergencyAccessId, headers: Headers, mut conn: DbConn) -> JsonResult { check_emergency_access_enabled()?; let Some(mut emergency_access) = - EmergencyAccess::find_by_uuid_and_grantor_uuid(emer_id, &headers.user.uuid, &mut conn).await + EmergencyAccess::find_by_uuid_and_grantor_uuid(&emer_id, &headers.user.uuid, &mut conn).await else { err!("Emergency access not valid.") }; @@ -551,11 +556,11 @@ async fn reject_emergency_access(emer_id: &str, headers: Headers, mut conn: DbCo // region action #[post("/emergency-access//view")] -async fn view_emergency_access(emer_id: &str, headers: Headers, mut conn: DbConn) -> JsonResult { +async fn view_emergency_access(emer_id: EmergencyAccessId, headers: Headers, mut conn: DbConn) -> JsonResult { check_emergency_access_enabled()?; let Some(emergency_access) = - EmergencyAccess::find_by_uuid_and_grantee_uuid(emer_id, &headers.user.uuid, &mut conn).await + EmergencyAccess::find_by_uuid_and_grantee_uuid(&emer_id, &headers.user.uuid, &mut conn).await else { err!("Emergency access not valid.") }; @@ -589,12 +594,12 @@ async fn view_emergency_access(emer_id: &str, headers: Headers, mut conn: DbConn } #[post("/emergency-access//takeover")] -async fn takeover_emergency_access(emer_id: &str, headers: Headers, mut conn: DbConn) -> JsonResult { +async fn takeover_emergency_access(emer_id: EmergencyAccessId, headers: Headers, mut 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, &mut conn).await + EmergencyAccess::find_by_uuid_and_grantee_uuid(&emer_id, &requesting_user.uuid, &mut conn).await else { err!("Emergency access not valid.") }; @@ -628,7 +633,7 @@ struct EmergencyAccessPasswordData { #[post("/emergency-access//password", data = "")] async fn password_emergency_access( - emer_id: &str, + emer_id: EmergencyAccessId, data: Json, headers: Headers, mut conn: DbConn, @@ -641,7 +646,7 @@ async fn password_emergency_access( let requesting_user = headers.user; let Some(emergency_access) = - EmergencyAccess::find_by_uuid_and_grantee_uuid(emer_id, &requesting_user.uuid, &mut conn).await + EmergencyAccess::find_by_uuid_and_grantee_uuid(&emer_id, &requesting_user.uuid, &mut conn).await else { err!("Emergency access not valid.") }; @@ -673,10 +678,10 @@ async fn password_emergency_access( // endregion #[get("/emergency-access//policies")] -async fn policies_emergency_access(emer_id: &str, headers: Headers, mut conn: DbConn) -> JsonResult { +async fn policies_emergency_access(emer_id: EmergencyAccessId, headers: Headers, mut conn: DbConn) -> JsonResult { let requesting_user = headers.user; let Some(emergency_access) = - EmergencyAccess::find_by_uuid_and_grantee_uuid(emer_id, &requesting_user.uuid, &mut conn).await + EmergencyAccess::find_by_uuid_and_grantee_uuid(&emer_id, &requesting_user.uuid, &mut conn).await else { err!("Emergency access not valid.") }; diff --git a/src/auth.rs b/src/auth.rs index 17060c17de5..2d5f79fc03a 100644 --- a/src/auth.rs +++ b/src/auth.rs @@ -15,8 +15,8 @@ use std::{ }; use crate::db::models::{ - AttachmentId, CipherId, CollectionId, DeviceId, MembershipId, OrgApiKeyId, OrganizationId, SendFileId, SendId, - UserId, + AttachmentId, CipherId, CollectionId, DeviceId, EmergencyAccessId, MembershipId, OrgApiKeyId, OrganizationId, + SendFileId, SendId, UserId, }; use crate::{error::Error, CONFIG}; @@ -191,7 +191,7 @@ pub struct InviteJwtClaims { // Issuer pub iss: String, // Subject - pub sub: String, + pub sub: UserId, pub email: String, pub org_id: Option, @@ -200,7 +200,7 @@ pub struct InviteJwtClaims { } pub fn generate_invite_claims( - uuid: String, + user_id: UserId, email: String, org_id: Option, member_id: Option, @@ -212,7 +212,7 @@ pub fn generate_invite_claims( nbf: time_now.timestamp(), exp: (time_now + TimeDelta::try_hours(expire_hours).unwrap()).timestamp(), iss: JWT_INVITE_ISSUER.to_string(), - sub: uuid, + sub: user_id, email, org_id, member_id, @@ -229,18 +229,18 @@ pub struct EmergencyAccessInviteJwtClaims { // Issuer pub iss: String, // Subject - pub sub: String, + pub sub: UserId, pub email: String, - pub emer_id: String, + pub emer_id: EmergencyAccessId, pub grantor_name: String, pub grantor_email: String, } pub fn generate_emergency_access_invite_claims( - uuid: String, + user_id: UserId, email: String, - emer_id: String, + emer_id: EmergencyAccessId, grantor_name: String, grantor_email: String, ) -> EmergencyAccessInviteJwtClaims { @@ -250,7 +250,7 @@ pub fn generate_emergency_access_invite_claims( nbf: time_now.timestamp(), exp: (time_now + TimeDelta::try_hours(expire_hours).unwrap()).timestamp(), iss: JWT_EMERGENCY_ACCESS_INVITE_ISSUER.to_string(), - sub: uuid, + sub: user_id, email, emer_id, grantor_name, diff --git a/src/db/models/emergency_access.rs b/src/db/models/emergency_access.rs index 429092eaf49..29c688b990c 100644 --- a/src/db/models/emergency_access.rs +++ b/src/db/models/emergency_access.rs @@ -1,4 +1,6 @@ use chrono::{NaiveDateTime, Utc}; +use derive_more::{AsRef, Deref, Display, From}; +use rocket::request::FromParam; use serde_json::Value; use crate::{api::EmptyResult, db::DbConn, error::MapResult}; @@ -11,7 +13,7 @@ db_object! { #[diesel(treat_none_as_null = true)] #[diesel(primary_key(uuid))] pub struct EmergencyAccess { - pub uuid: String, + pub uuid: EmergencyAccessId, pub grantor_uuid: UserId, pub grantee_uuid: Option, pub email: Option, @@ -33,7 +35,7 @@ impl EmergencyAccess { let now = Utc::now().naive_utc(); Self { - uuid: crate::util::get_uuid(), + uuid: EmergencyAccessId(crate::util::get_uuid()), grantor_uuid, grantee_uuid: None, email: Some(email), @@ -262,7 +264,11 @@ impl EmergencyAccess { }} } - pub async fn find_by_uuid_and_grantor_uuid(uuid: &str, grantor_uuid: &UserId, conn: &mut DbConn) -> Option { + pub async fn find_by_uuid_and_grantor_uuid( + uuid: &EmergencyAccessId, + grantor_uuid: &UserId, + conn: &mut DbConn, + ) -> Option { db_run! { conn: { emergency_access::table .filter(emergency_access::uuid.eq(uuid)) @@ -272,7 +278,11 @@ impl EmergencyAccess { }} } - pub async fn find_by_uuid_and_grantee_uuid(uuid: &str, grantee_uuid: &UserId, conn: &mut DbConn) -> Option { + pub async fn find_by_uuid_and_grantee_uuid( + uuid: &EmergencyAccessId, + grantee_uuid: &UserId, + conn: &mut DbConn, + ) -> Option { db_run! { conn: { emergency_access::table .filter(emergency_access::uuid.eq(uuid)) @@ -282,7 +292,11 @@ impl EmergencyAccess { }} } - pub async fn find_by_uuid_and_grantee_email(uuid: &str, grantee_email: &str, conn: &mut DbConn) -> Option { + pub async fn find_by_uuid_and_grantee_email( + uuid: &EmergencyAccessId, + grantee_email: &str, + conn: &mut DbConn, + ) -> Option { db_run! { conn: { emergency_access::table .filter(emergency_access::uuid.eq(uuid)) @@ -349,3 +363,21 @@ impl EmergencyAccess { } // endregion + +#[derive( + Clone, Debug, AsRef, Deref, DieselNewType, Display, From, FromForm, Hash, PartialEq, Eq, Serialize, Deserialize, +)] +pub struct EmergencyAccessId(String); + +impl<'r> FromParam<'r> for EmergencyAccessId { + type Error = (); + + #[inline(always)] + fn from_param(param: &'r str) -> Result { + if param.chars().all(|c| matches!(c, 'a'..='z' | 'A'..='Z' |'0'..='9' | '-')) { + Ok(Self(param.to_string())) + } else { + Err(()) + } + } +} diff --git a/src/db/models/mod.rs b/src/db/models/mod.rs index d503354c1eb..a78c4a9ab2c 100644 --- a/src/db/models/mod.rs +++ b/src/db/models/mod.rs @@ -21,7 +21,7 @@ pub use self::auth_request::{AuthRequest, AuthRequestId}; pub use self::cipher::{Cipher, CipherId, RepromptType}; pub use self::collection::{Collection, CollectionCipher, CollectionId, CollectionUser}; pub use self::device::{Device, DeviceId, DeviceType}; -pub use self::emergency_access::{EmergencyAccess, EmergencyAccessStatus, EmergencyAccessType}; +pub use self::emergency_access::{EmergencyAccess, EmergencyAccessId, EmergencyAccessStatus, EmergencyAccessType}; pub use self::event::{Event, EventType}; pub use self::favorite::Favorite; pub use self::folder::{Folder, FolderCipher, FolderId}; diff --git a/src/mail.rs b/src/mail.rs index 33db47fba2c..68ab7413a28 100644 --- a/src/mail.rs +++ b/src/mail.rs @@ -17,7 +17,7 @@ use crate::{ encode_jwt, generate_delete_claims, generate_emergency_access_invite_claims, generate_invite_claims, generate_verify_email_claims, }, - db::models::{Device, DeviceType, MembershipId, OrganizationId, User, UserId}, + db::models::{Device, DeviceType, EmergencyAccessId, MembershipId, OrganizationId, User, UserId}, error::Error, CONFIG, }; @@ -265,7 +265,7 @@ pub async fn send_invite( invited_by_email: Option, ) -> EmptyResult { let claims = generate_invite_claims( - user.uuid.to_string(), + user.uuid.clone(), user.email.clone(), org_id.clone(), member_id.clone(), @@ -313,15 +313,15 @@ pub async fn send_invite( pub async fn send_emergency_access_invite( address: &str, - uuid: &str, - emer_id: &str, + user_id: UserId, + emer_id: EmergencyAccessId, grantor_name: &str, grantor_email: &str, ) -> EmptyResult { let claims = generate_emergency_access_invite_claims( - String::from(uuid), + user_id, String::from(address), - String::from(emer_id), + emer_id.clone(), String::from(grantor_name), String::from(grantor_email), ); @@ -331,7 +331,7 @@ pub async fn send_emergency_access_invite( { let mut query_params = query.query_pairs_mut(); query_params - .append_pair("id", emer_id) + .append_pair("id", &emer_id.to_string()) .append_pair("name", grantor_name) .append_pair("email", address) .append_pair("token", &encode_jwt(&claims));