From 0b9e3bafd3692c5c247d2fb24083fb9ae3b22b96 Mon Sep 17 00:00:00 2001 From: Stefan Melmuk Date: Fri, 20 Dec 2024 21:04:58 +0100 Subject: [PATCH 01/25] rename membership rename UserOrganization to Membership to clarify the relation and prevent confusion whether something refers to a member(ship) or user --- src/api/admin.rs | 43 ++- src/api/core/accounts.rs | 30 +- src/api/core/ciphers.rs | 33 +- src/api/core/emergency_access.rs | 6 +- src/api/core/events.rs | 12 +- src/api/core/organizations.rs | 593 +++++++++++++++---------------- src/api/core/public.rs | 65 ++-- src/api/core/two_factor/mod.rs | 11 +- src/api/identity.rs | 6 +- src/auth.rs | 46 +-- src/db/models/cipher.rs | 35 +- src/db/models/collection.rs | 50 +-- src/db/models/device.rs | 10 +- src/db/models/event.rs | 6 +- src/db/models/group.rs | 26 +- src/db/models/mod.rs | 2 +- src/db/models/org_policy.rs | 24 +- src/db/models/organization.rs | 166 ++++----- src/db/models/user.rs | 14 +- src/mail.rs | 6 +- 20 files changed, 568 insertions(+), 616 deletions(-) diff --git a/src/api/admin.rs b/src/api/admin.rs index f49f6ed34b..ddb21f796c 100644 --- a/src/api/admin.rs +++ b/src/api/admin.rs @@ -50,7 +50,7 @@ pub fn routes() -> Vec { disable_user, enable_user, remove_2fa, - update_user_org_type, + update_membership_type, update_revision_users, post_config, delete_config, @@ -394,15 +394,15 @@ async fn get_user_json(uuid: &str, _token: AdminToken, mut conn: DbConn) -> Json async fn delete_user(uuid: &str, token: AdminToken, mut conn: DbConn) -> EmptyResult { let user = get_user_or_404(uuid, &mut conn).await?; - // Get the user_org records before deleting the actual user - let user_orgs = UserOrganization::find_any_state_by_user(uuid, &mut conn).await; + // Get the membership records before deleting the actual user + let memberships = Membership::find_any_state_by_user(uuid, &mut conn).await; let res = user.delete(&mut conn).await; - for user_org in user_orgs { + for membership in memberships { log_event( EventType::OrganizationUserRemoved as i32, - &user_org.uuid, - &user_org.org_uuid, + &membership.uuid, + &membership.org_uuid, ACTING_ADMIN_USER, 14, // Use UnknownBrowser type &token.ip.ip, @@ -485,42 +485,41 @@ async fn resend_user_invite(uuid: &str, _token: AdminToken, mut conn: DbConn) -> } #[derive(Debug, Deserialize)] -struct UserOrgTypeData { +struct MembershipTypeData { user_type: NumberOrString, user_uuid: String, org_uuid: String, } #[post("/users/org_type", data = "")] -async fn update_user_org_type(data: Json, token: AdminToken, mut conn: DbConn) -> EmptyResult { - let data: UserOrgTypeData = data.into_inner(); +async fn update_membership_type(data: Json, token: AdminToken, mut conn: DbConn) -> EmptyResult { + let data: MembershipTypeData = data.into_inner(); - let Some(mut user_to_edit) = - UserOrganization::find_by_user_and_org(&data.user_uuid, &data.org_uuid, &mut conn).await + let Some(mut member_to_edit) = Membership::find_by_user_and_org(&data.user_uuid, &data.org_uuid, &mut conn).await else { err!("The specified user isn't member of the organization") }; - let new_type = match UserOrgType::from_str(&data.user_type.into_string()) { + let new_type = match MembershipType::from_str(&data.user_type.into_string()) { Some(new_type) => new_type as i32, None => err!("Invalid type"), }; - if user_to_edit.atype == UserOrgType::Owner && new_type != UserOrgType::Owner { + if member_to_edit.atype == MembershipType::Owner && new_type != MembershipType::Owner { // Removing owner permission, check that there is at least one other confirmed owner - if UserOrganization::count_confirmed_by_org_and_type(&data.org_uuid, UserOrgType::Owner, &mut conn).await <= 1 { + if Membership::count_confirmed_by_org_and_type(&data.org_uuid, MembershipType::Owner, &mut conn).await <= 1 { err!("Can't change the type of the last owner") } } - // This check is also done at api::organizations::{accept_invite(), _confirm_invite, _activate_user(), edit_user()}, update_user_org_type + // This check is also done at api::organizations::{accept_invite(), _confirm_invite, _activate_user(), edit_user()}, update_membership_type // It returns different error messages per function. - if new_type < UserOrgType::Admin { - match OrgPolicy::is_user_allowed(&user_to_edit.user_uuid, &user_to_edit.org_uuid, true, &mut conn).await { + if new_type < MembershipType::Admin { + match OrgPolicy::is_user_allowed(&member_to_edit.user_uuid, &member_to_edit.org_uuid, true, &mut conn).await { Ok(_) => {} Err(OrgPolicyErr::TwoFactorMissing) => { if CONFIG.email_2fa_auto_fallback() { - two_factor::email::find_and_activate_email_2fa(&user_to_edit.user_uuid, &mut conn).await?; + two_factor::email::find_and_activate_email_2fa(&member_to_edit.user_uuid, &mut conn).await?; } else { err!("You cannot modify this user to this type because they have not setup 2FA"); } @@ -533,7 +532,7 @@ async fn update_user_org_type(data: Json, token: AdminToken, mu log_event( EventType::OrganizationUserUpdated as i32, - &user_to_edit.uuid, + &member_to_edit.uuid, &data.org_uuid, ACTING_ADMIN_USER, 14, // Use UnknownBrowser type @@ -542,8 +541,8 @@ async fn update_user_org_type(data: Json, token: AdminToken, mu ) .await; - user_to_edit.atype = new_type; - user_to_edit.save(&mut conn).await + member_to_edit.atype = new_type; + member_to_edit.save(&mut conn).await } #[post("/users/update_revision")] @@ -557,7 +556,7 @@ async fn organizations_overview(_token: AdminToken, mut conn: DbConn) -> ApiResu let mut organizations_json = Vec::with_capacity(organizations.len()); for o in organizations { let mut org = o.to_json(); - org["user_count"] = json!(UserOrganization::count_by_org(&o.uuid, &mut conn).await); + org["user_count"] = json!(Membership::count_by_org(&o.uuid, &mut conn).await); org["cipher_count"] = json!(Cipher::count_by_org(&o.uuid, &mut conn).await); org["collection_count"] = json!(Collection::count_by_org(&o.uuid, &mut conn).await); org["group_count"] = json!(Group::count_by_org(&o.uuid, &mut conn).await); diff --git a/src/api/core/accounts.rs b/src/api/core/accounts.rs index d95a50b892..92d72d5749 100644 --- a/src/api/core/accounts.rs +++ b/src/api/core/accounts.rs @@ -106,15 +106,15 @@ fn enforce_password_hint_setting(password_hint: &Option) -> EmptyResult } Ok(()) } -async fn is_email_2fa_required(org_user_uuid: Option, conn: &mut DbConn) -> bool { +async fn is_email_2fa_required(member_uuid: Option, conn: &mut DbConn) -> bool { if !CONFIG._enable_email_2fa() { return false; } if CONFIG.email_2fa_enforce_on_verified_invite() { return true; } - if org_user_uuid.is_some() { - return OrgPolicy::is_enabled_for_member(&org_user_uuid.unwrap(), OrgPolicyType::TwoFactorAuthentication, conn) + if member_uuid.is_some() { + return OrgPolicy::is_enabled_for_member(&member_uuid.unwrap(), OrgPolicyType::TwoFactorAuthentication, conn) .await; } false @@ -161,9 +161,9 @@ pub async fn _register(data: Json, mut conn: DbConn) -> JsonResult err!("Registration email does not match invite email") } } else if Invitation::take(&email, &mut conn).await { - for user_org in UserOrganization::find_invited_by_user(&user.uuid, &mut conn).await.iter_mut() { - user_org.status = UserOrgStatus::Accepted as i32; - user_org.save(&mut conn).await?; + for membership in Membership::find_invited_by_user(&user.uuid, &mut conn).await.iter_mut() { + membership.status = MembershipStatus::Accepted as i32; + membership.save(&mut conn).await?; } user } else if CONFIG.is_signup_allowed(&email) @@ -484,7 +484,7 @@ fn validate_keydata( existing_ciphers: &[Cipher], existing_folders: &[Folder], existing_emergency_access: &[EmergencyAccess], - existing_user_orgs: &[UserOrganization], + existing_memberships: &[Membership], existing_sends: &[Send], ) -> EmptyResult { // Check that we're correctly rotating all the user's ciphers @@ -516,7 +516,7 @@ fn validate_keydata( } // Check that we're correctly rotating all the user's reset password keys - let existing_reset_password_ids = existing_user_orgs.iter().map(|uo| uo.org_uuid.as_str()).collect::>(); + let existing_reset_password_ids = existing_memberships.iter().map(|m| m.org_uuid.as_str()).collect::>(); let provided_reset_password_ids = data.reset_password_keys.iter().map(|rp| rp.organization_id.as_str()).collect::>(); if !provided_reset_password_ids.is_superset(&existing_reset_password_ids) { @@ -555,9 +555,9 @@ async fn post_rotatekey(data: Json, headers: Headers, mut conn: DbConn, let mut existing_ciphers = Cipher::find_owned_by_user(user_uuid, &mut conn).await; let mut existing_folders = Folder::find_by_user(user_uuid, &mut conn).await; let mut existing_emergency_access = EmergencyAccess::find_all_by_grantor_uuid(user_uuid, &mut conn).await; - let mut existing_user_orgs = UserOrganization::find_by_user(user_uuid, &mut conn).await; + let mut existing_memberships = Membership::find_by_user(user_uuid, &mut conn).await; // We only rotate the reset password key if it is set. - existing_user_orgs.retain(|uo| uo.reset_password_key.is_some()); + existing_memberships.retain(|m| m.reset_password_key.is_some()); let mut existing_sends = Send::find_by_user(user_uuid, &mut conn).await; validate_keydata( @@ -565,7 +565,7 @@ async fn post_rotatekey(data: Json, headers: Headers, mut conn: DbConn, &existing_ciphers, &existing_folders, &existing_emergency_access, - &existing_user_orgs, + &existing_memberships, &existing_sends, )?; @@ -597,14 +597,14 @@ async fn post_rotatekey(data: Json, headers: Headers, mut conn: DbConn, // Update reset password data for reset_password_data in data.reset_password_keys { - let Some(user_org) = - existing_user_orgs.iter_mut().find(|uo| uo.org_uuid == reset_password_data.organization_id) + let Some(membership) = + existing_memberships.iter_mut().find(|m| m.org_uuid == reset_password_data.organization_id) else { err!("Reset password doesn't exist") }; - user_org.reset_password_key = Some(reset_password_data.reset_password_key); - user_org.save(&mut conn).await? + membership.reset_password_key = Some(reset_password_data.reset_password_key); + membership.save(&mut conn).await? } // Update send data diff --git a/src/api/core/ciphers.rs b/src/api/core/ciphers.rs index 136b890a7e..8e1c4f0e13 100644 --- a/src/api/core/ciphers.rs +++ b/src/api/core/ciphers.rs @@ -405,11 +405,11 @@ pub async fn update_cipher_from_data( let transfer_cipher = cipher.organization_uuid.is_none() && data.organization_id.is_some(); if let Some(org_id) = data.organization_id { - match UserOrganization::find_by_user_and_org(&headers.user.uuid, &org_id, conn).await { + match Membership::find_by_user_and_org(&headers.user.uuid, &org_id, conn).await { None => err!("You don't have permission to add item to organization"), - Some(org_user) => { + Some(member) => { if shared_to_collections.is_some() - || org_user.has_full_access() + || member.has_full_access() || cipher.is_write_accessible_to_user(&headers.user.uuid, conn).await { cipher.organization_uuid = Some(org_id); @@ -1593,10 +1593,10 @@ async fn delete_all( match organization { Some(org_data) => { // Organization ID in query params, purging organization vault - match UserOrganization::find_by_user_and_org(&user.uuid, &org_data.org_id, &mut conn).await { + match Membership::find_by_user_and_org(&user.uuid, &org_data.org_id, &mut conn).await { None => err!("You don't have permission to purge the organization vault"), - Some(user_org) => { - if user_org.atype == UserOrgType::Owner { + Some(member) => { + if member.atype == MembershipType::Owner { Cipher::delete_all_by_organization(&org_data.org_id, &mut conn).await?; nt.send_user_update(UpdateType::SyncVault, &user).await; @@ -1835,7 +1835,7 @@ pub struct CipherSyncData { pub cipher_folders: HashMap, pub cipher_favorites: HashSet, pub cipher_collections: HashMap>, - pub user_organizations: HashMap, + pub members: HashMap, pub user_collections: HashMap, pub user_collections_groups: HashMap, pub user_group_full_access_for_organizations: HashSet, @@ -1869,8 +1869,8 @@ impl CipherSyncData { } // Generate a list of Cipher UUID's containing a Vec with one or more Attachment records - let user_org_uuids = UserOrganization::get_org_uuid_by_user(user_uuid, conn).await; - let attachments = Attachment::find_all_by_user_and_orgs(user_uuid, &user_org_uuids, conn).await; + let orgs = Membership::get_orgs_by_user(user_uuid, conn).await; + let attachments = Attachment::find_all_by_user_and_orgs(user_uuid, &orgs, conn).await; let mut cipher_attachments: HashMap> = HashMap::with_capacity(attachments.len()); for attachment in attachments { cipher_attachments.entry(attachment.cipher_uuid.clone()).or_default().push(attachment); @@ -1884,12 +1884,9 @@ impl CipherSyncData { cipher_collections.entry(cipher).or_default().push(collection); } - // Generate a HashMap with the Organization UUID as key and the UserOrganization record - let user_organizations: HashMap = UserOrganization::find_by_user(user_uuid, conn) - .await - .into_iter() - .map(|uo| (uo.org_uuid.clone(), uo)) - .collect(); + // Generate a HashMap with the Organization UUID as key and the Membership record + let members: HashMap = + Membership::find_by_user(user_uuid, conn).await.into_iter().map(|m| (m.org_uuid.clone(), m)).collect(); // Generate a HashMap with the User_Collections UUID as key and the CollectionUser record let user_collections: HashMap = CollectionUser::find_by_user(user_uuid, conn) @@ -1909,9 +1906,9 @@ impl CipherSyncData { HashMap::new() }; - // Get all organizations that the user has full access to via group assignment + // Get all organizations that the given user has full access to via group assignment let user_group_full_access_for_organizations: HashSet = if CONFIG.org_groups_enabled() { - Group::gather_user_organizations_full_access(user_uuid, conn).await.into_iter().collect() + Group::get_orgs_by_user_with_full_access(user_uuid, conn).await.into_iter().collect() } else { HashSet::new() }; @@ -1921,7 +1918,7 @@ impl CipherSyncData { cipher_folders, cipher_favorites, cipher_collections, - user_organizations, + members, user_collections, user_collections_groups, user_group_full_access_for_organizations, diff --git a/src/api/core/emergency_access.rs b/src/api/core/emergency_access.rs index 8ed9e87a7b..6d3d881dd4 100644 --- a/src/api/core/emergency_access.rs +++ b/src/api/core/emergency_access.rs @@ -662,9 +662,9 @@ async fn password_emergency_access( TwoFactor::delete_all_by_user(&grantor_user.uuid, &mut conn).await?; // Remove grantor from all organisations unless Owner - for user_org in UserOrganization::find_any_state_by_user(&grantor_user.uuid, &mut conn).await { - if user_org.atype != UserOrgType::Owner as i32 { - user_org.delete(&mut conn).await?; + for member in Membership::find_any_state_by_user(&grantor_user.uuid, &mut conn).await { + if member.atype != MembershipType::Owner as i32 { + member.delete(&mut conn).await?; } } Ok(()) diff --git a/src/api/core/events.rs b/src/api/core/events.rs index 484094f52e..49ff083f41 100644 --- a/src/api/core/events.rs +++ b/src/api/core/events.rs @@ -8,7 +8,7 @@ use crate::{ api::{EmptyResult, JsonResult}, auth::{AdminHeaders, Headers}, db::{ - models::{Cipher, Event, UserOrganization}, + models::{Cipher, Event, Membership}, DbConn, DbPool, }, util::parse_date, @@ -66,7 +66,7 @@ async fn get_cipher_events(cipher_id: &str, data: EventRange, headers: Headers, Vec::with_capacity(0) } else { let mut events_json = Vec::with_capacity(0); - if UserOrganization::user_has_ge_admin_access_to_cipher(&headers.user.uuid, cipher_id, &mut conn).await { + if Membership::user_has_ge_admin_access_to_cipher(&headers.user.uuid, cipher_id, &mut conn).await { let start_date = parse_date(&data.start); let end_date = if let Some(before_date) = &data.continuation_token { parse_date(before_date) @@ -90,10 +90,10 @@ async fn get_cipher_events(cipher_id: &str, data: EventRange, headers: Headers, }))) } -#[get("/organizations//users//events?")] +#[get("/organizations//users//events?")] async fn get_user_events( org_id: &str, - user_org_id: &str, + member_id: &str, data: EventRange, _headers: AdminHeaders, mut conn: DbConn, @@ -110,7 +110,7 @@ async fn get_user_events( parse_date(&data.end) }; - Event::find_by_org_and_user_org(org_id, user_org_id, &start_date, &end_date, &mut conn) + Event::find_by_org_and_member(org_id, member_id, &start_date, &end_date, &mut conn) .await .iter() .map(|e| e.to_json()) @@ -233,7 +233,7 @@ async fn _log_user_event( ip: &IpAddr, conn: &mut DbConn, ) { - let orgs = UserOrganization::get_org_uuid_by_user(user_uuid, conn).await; + let orgs = Membership::get_orgs_by_user(user_uuid, conn).await; let mut events: Vec = Vec::with_capacity(orgs.len() + 1); // We need an event per org and one without an org // Upstream saves the event also without any org_uuid. diff --git a/src/api/core/organizations.rs b/src/api/core/organizations.rs index f315853699..b5ffe3eda4 100644 --- a/src/api/core/organizations.rs +++ b/src/api/core/organizations.rs @@ -41,7 +41,7 @@ pub fn routes() -> Vec { bulk_delete_organization_collections, post_bulk_collections, get_org_details, - get_org_users, + get_members, send_invite, reinvite_user, bulk_reinvite_user, @@ -50,7 +50,7 @@ pub fn routes() -> Vec { accept_invite, get_user, edit_user, - put_organization_user, + put_membership, delete_user, bulk_delete_user, post_delete_user, @@ -68,14 +68,14 @@ pub fn routes() -> Vec { get_organization_keys, get_organization_public_key, bulk_public_keys, - deactivate_organization_user, - bulk_deactivate_organization_user, - revoke_organization_user, - bulk_revoke_organization_user, - activate_organization_user, - bulk_activate_organization_user, - restore_organization_user, - bulk_restore_organization_user, + deactivate_membership, + bulk_deactivate_membership, + revoke_membership, + bulk_revoke_membership, + activate_membership, + bulk_activate_membership, + restore_membership, + bulk_restore_membership, get_groups, post_groups, get_group, @@ -171,16 +171,16 @@ async fn create_organization(headers: Headers, data: Json, mut conn: Db }; let org = Organization::new(data.name, data.billing_email, private_key, public_key); - let mut user_org = UserOrganization::new(headers.user.uuid, org.uuid.clone()); + let mut member = Membership::new(headers.user.uuid, org.uuid.clone()); let collection = Collection::new(org.uuid.clone(), data.collection_name, None); - user_org.akey = data.key; - user_org.access_all = true; - user_org.atype = UserOrgType::Owner as i32; - user_org.status = UserOrgStatus::Confirmed as i32; + member.akey = data.key; + member.access_all = true; + member.atype = MembershipType::Owner as i32; + member.status = MembershipStatus::Confirmed as i32; org.save(&mut conn).await?; - user_org.save(&mut conn).await?; + member.save(&mut conn).await?; collection.save(&mut conn).await?; Ok(Json(org.to_json())) @@ -215,18 +215,18 @@ async fn post_delete_organization( #[post("/organizations//leave")] async fn leave_organization(org_id: &str, headers: Headers, mut conn: DbConn) -> EmptyResult { - match UserOrganization::find_by_user_and_org(&headers.user.uuid, org_id, &mut conn).await { + match Membership::find_by_user_and_org(&headers.user.uuid, org_id, &mut conn).await { None => err!("User not part of organization"), - Some(user_org) => { - if user_org.atype == UserOrgType::Owner - && UserOrganization::count_confirmed_by_org_and_type(org_id, UserOrgType::Owner, &mut conn).await <= 1 + Some(member) => { + if member.atype == MembershipType::Owner + && Membership::count_confirmed_by_org_and_type(org_id, MembershipType::Owner, &mut conn).await <= 1 { err!("The last owner can't leave") } log_event( EventType::OrganizationUserRemoved as i32, - &user_org.uuid, + &member.uuid, org_id, &headers.user.uuid, headers.device.atype, @@ -235,7 +235,7 @@ async fn leave_organization(org_id: &str, headers: Headers, mut conn: DbConn) -> ) .await; - user_org.delete(&mut conn).await + member.delete(&mut conn).await } } } @@ -317,7 +317,7 @@ async fn get_org_collections(org_id: &str, _headers: ManagerHeadersLoose, mut co async fn get_org_collections_details(org_id: &str, headers: ManagerHeadersLoose, mut conn: DbConn) -> JsonResult { let mut data = Vec::new(); - let Some(user_org) = UserOrganization::find_by_user_and_org(&headers.user.uuid, org_id, &mut conn).await else { + let Some(member) = Membership::find_by_user_and_org(&headers.user.uuid, org_id, &mut conn).await else { err!("User is not part of organization") }; @@ -325,16 +325,15 @@ async fn get_org_collections_details(org_id: &str, headers: ManagerHeadersLoose, let coll_users = CollectionUser::find_by_organization(org_id, &mut conn).await; // check if current user has full access to the organization (either directly or via any group) - let has_full_access_to_org = user_org.access_all - || (CONFIG.org_groups_enabled() - && GroupUser::has_full_access_by_member(org_id, &user_org.uuid, &mut conn).await); + let has_full_access_to_org = member.access_all + || (CONFIG.org_groups_enabled() && GroupUser::has_full_access_by_member(org_id, &member.uuid, &mut conn).await); for col in Collection::find_by_organization(org_id, &mut conn).await { // check whether the current user has access to the given collection let assigned = has_full_access_to_org - || CollectionUser::has_access_to_collection_by_user(&col.uuid, &user_org.user_uuid, &mut conn).await + || CollectionUser::has_access_to_collection_by_user(&col.uuid, &member.user_uuid, &mut conn).await || (CONFIG.org_groups_enabled() - && GroupUser::has_access_to_collection_by_member(&col.uuid, &user_org.uuid, &mut conn).await); + && GroupUser::has_access_to_collection_by_member(&col.uuid, &member.uuid, &mut conn).await); // get the users assigned directly to the given collection let users: Vec = coll_users @@ -410,20 +409,20 @@ async fn post_organization_collections( } for user in data.users { - let Some(org_user) = UserOrganization::find_by_uuid_and_org(&user.id, org_id, &mut conn).await else { + let Some(member) = Membership::find_by_uuid_and_org(&user.id, org_id, &mut conn).await else { err!("User is not part of organization") }; - if org_user.access_all { + if member.access_all { continue; } - CollectionUser::save(&org_user.user_uuid, &collection.uuid, user.read_only, user.hide_passwords, &mut conn) + CollectionUser::save(&member.user_uuid, &collection.uuid, user.read_only, user.hide_passwords, &mut conn) .await?; } - if headers.org_user.atype == UserOrgType::Manager && !headers.org_user.access_all { - CollectionUser::save(&headers.org_user.user_uuid, &collection.uuid, false, false, &mut conn).await?; + if headers.membership.atype == MembershipType::Manager && !headers.membership.access_all { + CollectionUser::save(&headers.membership.user_uuid, &collection.uuid, false, false, &mut conn).await?; } Ok(Json(collection.to_json())) @@ -488,25 +487,25 @@ async fn post_organization_collection_update( CollectionUser::delete_all_by_collection(col_id, &mut conn).await?; for user in data.users { - let Some(org_user) = UserOrganization::find_by_uuid_and_org(&user.id, org_id, &mut conn).await else { + let Some(member) = Membership::find_by_uuid_and_org(&user.id, org_id, &mut conn).await else { err!("User is not part of organization") }; - if org_user.access_all { + if member.access_all { continue; } - CollectionUser::save(&org_user.user_uuid, col_id, user.read_only, user.hide_passwords, &mut conn).await?; + CollectionUser::save(&member.user_uuid, col_id, user.read_only, user.hide_passwords, &mut conn).await?; } Ok(Json(collection.to_json_details(&headers.user.uuid, None, &mut conn).await)) } -#[delete("/organizations//collections//user/")] +#[delete("/organizations//collections//user/")] async fn delete_organization_collection_user( org_id: &str, col_id: &str, - org_user_id: &str, + member_id: &str, _headers: AdminHeaders, mut conn: DbConn, ) -> EmptyResult { @@ -514,10 +513,10 @@ async fn delete_organization_collection_user( err!("Collection not found", "Collection does not exist or does not belong to this organization") }; - match UserOrganization::find_by_uuid_and_org(org_user_id, org_id, &mut conn).await { + match Membership::find_by_uuid_and_org(member_id, org_id, &mut conn).await { None => err!("User not found in organization"), - Some(user_org) => { - match CollectionUser::find_by_collection_and_user(&collection.uuid, &user_org.user_uuid, &mut conn).await { + Some(member) => { + match CollectionUser::find_by_collection_and_user(&collection.uuid, &member.user_uuid, &mut conn).await { None => err!("User not assigned to collection"), Some(col_user) => col_user.delete(&mut conn).await, } @@ -525,15 +524,15 @@ async fn delete_organization_collection_user( } } -#[post("/organizations//collections//delete-user/")] +#[post("/organizations//collections//delete-user/")] async fn post_organization_collection_delete_user( org_id: &str, col_id: &str, - org_user_id: &str, + member_id: &str, headers: AdminHeaders, conn: DbConn, ) -> EmptyResult { - delete_organization_collection_user(org_id, col_id, org_user_id, headers, conn).await + delete_organization_collection_user(org_id, col_id, member_id, headers, conn).await } async fn _delete_organization_collection( @@ -626,8 +625,7 @@ async fn get_org_collection_detail( err!("Collection is not owned by organization") } - let Some(user_org) = UserOrganization::find_by_user_and_org(&headers.user.uuid, org_id, &mut conn).await - else { + let Some(member) = Membership::find_by_user_and_org(&headers.user.uuid, org_id, &mut conn).await else { err!("User is not part of organization") }; @@ -646,7 +644,7 @@ async fn get_org_collection_detail( }; let users: Vec = - CollectionUser::find_by_collection_swap_user_uuid_with_org_user_uuid(&collection.uuid, &mut conn) + CollectionUser::find_by_collection_swap_user_uuid_with_member_uuid(&collection.uuid, &mut conn) .await .iter() .map(|collection_user| { @@ -654,7 +652,7 @@ async fn get_org_collection_detail( }) .collect(); - let assigned = Collection::can_access_collection(&user_org, &collection.uuid, &mut conn).await; + let assigned = Collection::can_access_collection(&member, &collection.uuid, &mut conn).await; let mut json_object = collection.to_json_details(&headers.user.uuid, None, &mut conn).await; json_object["assigned"] = json!(assigned); @@ -677,7 +675,7 @@ async fn get_collection_users(org_id: &str, coll_id: &str, _headers: ManagerHead let mut user_list = Vec::new(); for col_user in CollectionUser::find_by_collection(&collection.uuid, &mut conn).await { user_list.push( - UserOrganization::find_by_user_and_org(&col_user.user_uuid, org_id, &mut conn) + Membership::find_by_user_and_org(&col_user.user_uuid, org_id, &mut conn) .await .unwrap() .to_json_user_access_restrictions(&col_user), @@ -705,7 +703,7 @@ async fn put_collection_users( // And then add all the received ones (except if the user has access_all) for d in data.iter() { - let Some(user) = UserOrganization::find_by_uuid_and_org(&d.id, org_id, &mut conn).await else { + let Some(user) = Membership::find_by_uuid_and_org(&d.id, org_id, &mut conn).await else { err!("User is not part of organization") }; @@ -727,9 +725,7 @@ struct OrgIdData { #[get("/ciphers/organization-details?")] async fn get_org_details(data: OrgIdData, headers: Headers, mut conn: DbConn) -> JsonResult { - if UserOrganization::find_confirmed_by_user_and_org(&headers.user.uuid, &data.organization_id, &mut conn) - .await - .is_none() + if Membership::find_confirmed_by_user_and_org(&headers.user.uuid, &data.organization_id, &mut conn).await.is_none() { err_code!("Resource not found.", rocket::http::Status::NotFound.code); } @@ -762,14 +758,14 @@ struct GetOrgUserData { } #[get("/organizations//users?")] -async fn get_org_users( +async fn get_members( data: GetOrgUserData, org_id: &str, _headers: ManagerHeadersLoose, mut conn: DbConn, ) -> Json { let mut users_json = Vec::new(); - for u in UserOrganization::find_by_org(org_id, &mut conn).await { + for u in Membership::find_by_org(org_id, &mut conn).await { users_json.push( u.to_json_user_details( data.include_collections.unwrap_or(false), @@ -836,17 +832,17 @@ struct InviteData { async fn send_invite(org_id: &str, data: Json, headers: AdminHeaders, mut conn: DbConn) -> EmptyResult { let data: InviteData = data.into_inner(); - let new_type = match UserOrgType::from_str(&data.r#type.into_string()) { + let new_type = match MembershipType::from_str(&data.r#type.into_string()) { Some(new_type) => new_type as i32, None => err!("Invalid type"), }; - if new_type != UserOrgType::User && headers.org_user_type != UserOrgType::Owner { + if new_type != MembershipType::User && headers.membership_type != MembershipType::Owner { err!("Only Owners can invite Managers, Admins or Owners") } for email in data.emails.iter() { - let mut user_org_status = UserOrgStatus::Invited as i32; + let mut member_status = MembershipStatus::Invited as i32; let user = match User::find_by_mail(email, &mut conn).await { None => { if !CONFIG.invitations_allowed() { @@ -867,23 +863,23 @@ async fn send_invite(org_id: &str, data: Json, headers: AdminHeaders user } Some(user) => { - if UserOrganization::find_by_user_and_org(&user.uuid, org_id, &mut conn).await.is_some() { + if Membership::find_by_user_and_org(&user.uuid, org_id, &mut conn).await.is_some() { err!(format!("User already in organization: {email}")) } else { // automatically accept existing users if mail is disabled if !CONFIG.mail_enabled() && !user.password_hash.is_empty() { - user_org_status = UserOrgStatus::Accepted as i32; + member_status = MembershipStatus::Accepted as i32; } user } } }; - let mut new_user = UserOrganization::new(user.uuid.clone(), String::from(org_id)); + let mut new_user = Membership::new(user.uuid.clone(), String::from(org_id)); let access_all = data.access_all; new_user.access_all = access_all; new_user.atype = new_type; - new_user.status = user_org_status; + new_user.status = member_status; // If no accessAll, add the collections received if !access_all { @@ -952,8 +948,8 @@ async fn bulk_reinvite_user( let data: OrgBulkIds = data.into_inner(); let mut bulk_response = Vec::new(); - for org_user_id in data.ids { - let err_msg = match _reinvite_user(org_id, &org_user_id, &headers.user.email, &mut conn).await { + for member_id in data.ids { + let err_msg = match _reinvite_user(org_id, &member_id, &headers.user.email, &mut conn).await { Ok(_) => String::new(), Err(e) => format!("{e:?}"), }; @@ -961,7 +957,7 @@ async fn bulk_reinvite_user( bulk_response.push(json!( { "object": "OrganizationBulkConfirmResponseModel", - "id": org_user_id, + "id": member_id, "error": err_msg } )) @@ -974,21 +970,21 @@ async fn bulk_reinvite_user( })) } -#[post("/organizations//users//reinvite")] -async fn reinvite_user(org_id: &str, user_org: &str, headers: AdminHeaders, mut conn: DbConn) -> EmptyResult { - _reinvite_user(org_id, user_org, &headers.user.email, &mut conn).await +#[post("/organizations//users//reinvite")] +async fn reinvite_user(org_id: &str, member: &str, headers: AdminHeaders, mut conn: DbConn) -> EmptyResult { + _reinvite_user(org_id, member, &headers.user.email, &mut conn).await } -async fn _reinvite_user(org_id: &str, user_org: &str, invited_by_email: &str, conn: &mut DbConn) -> EmptyResult { - let Some(user_org) = UserOrganization::find_by_uuid_and_org(user_org, org_id, conn).await else { +async fn _reinvite_user(org_id: &str, member: &str, invited_by_email: &str, conn: &mut DbConn) -> EmptyResult { + let Some(member) = Membership::find_by_uuid_and_org(member, org_id, conn).await else { err!("The user hasn't been invited to the organization.") }; - if user_org.status != UserOrgStatus::Invited as i32 { + if member.status != MembershipStatus::Invited as i32 { err!("The user is already accepted or confirmed to the organization") } - let Some(user) = User::find_by_uuid(&user_org.user_uuid, conn).await else { + let Some(user) = User::find_by_uuid(&member.user_uuid, conn).await else { err!("User not found.") }; @@ -1005,7 +1001,7 @@ async fn _reinvite_user(org_id: &str, user_org: &str, invited_by_email: &str, co mail::send_invite( &user, Some(org_id.to_string()), - Some(user_org.uuid), + Some(member.uuid), &org_name, Some(invited_by_email.to_string()), ) @@ -1015,9 +1011,9 @@ async fn _reinvite_user(org_id: &str, user_org: &str, invited_by_email: &str, co invitation.save(conn).await?; } else { let _ = Invitation::take(&user.email, conn).await; - let mut user_org = user_org; - user_org.status = UserOrgStatus::Accepted as i32; - user_org.save(conn).await?; + let mut member = member; + member.status = MembershipStatus::Accepted as i32; + member.save(conn).await?; } Ok(()) @@ -1030,28 +1026,28 @@ struct AcceptData { reset_password_key: Option, } -#[post("/organizations//users//accept", data = "")] -async fn accept_invite(org_id: &str, org_user_id: &str, data: Json, mut conn: DbConn) -> EmptyResult { - // The web-vault passes org_id and org_user_id in the URL, but we are just reading them from the JWT instead +#[post("/organizations//users//accept", data = "")] +async fn accept_invite(org_id: &str, member_id: &str, data: Json, mut conn: DbConn) -> EmptyResult { + // The web-vault passes org_id and member_id in the URL, but we are just reading them from the JWT instead let data: AcceptData = data.into_inner(); let claims = decode_invite(&data.token)?; - // If a claim does not have a user_org_id or it does not match the one in from the URI, something is wrong. - match &claims.user_org_id { - Some(ou_id) if ou_id.eq(org_user_id) => {} - _ => err!("Error accepting the invitation", "Claim does not match the org_user_id"), + // If a claim does not have a member_id or it does not match the one in from the URI, something is wrong. + match &claims.member_id { + Some(ou_id) if ou_id.eq(member_id) => {} + _ => err!("Error accepting the invitation", "Claim does not match the member_id"), } match User::find_by_mail(&claims.email, &mut conn).await { Some(user) => { Invitation::take(&claims.email, &mut conn).await; - if let (Some(user_org), Some(org)) = (&claims.user_org_id, &claims.org_id) { - let Some(mut user_org) = UserOrganization::find_by_uuid_and_org(user_org, org, &mut conn).await else { + if let (Some(member), Some(org)) = (&claims.member_id, &claims.org_id) { + let Some(mut member) = Membership::find_by_uuid_and_org(member, org, &mut conn).await else { err!("Error accepting the invitation") }; - if user_org.status != UserOrgStatus::Invited as i32 { + if member.status != MembershipStatus::Invited as i32 { err!("User already accepted the invitation") } @@ -1060,10 +1056,10 @@ async fn accept_invite(org_id: &str, org_user_id: &str, data: Json, err!("Reset password key is required, but not provided."); } - // This check is also done at accept_invite(), _confirm_invite, _activate_user(), edit_user(), admin::update_user_org_type + // This check is also done at accept_invite(), _confirm_invite, _activate_user(), edit_user(), admin::update_membership_type // It returns different error messages per function. - if user_org.atype < UserOrgType::Admin { - match OrgPolicy::is_user_allowed(&user_org.user_uuid, org_id, false, &mut conn).await { + if member.atype < MembershipType::Admin { + match OrgPolicy::is_user_allowed(&member.user_uuid, org_id, false, &mut conn).await { Ok(_) => {} Err(OrgPolicyErr::TwoFactorMissing) => { if CONFIG.email_2fa_auto_fallback() { @@ -1078,13 +1074,13 @@ async fn accept_invite(org_id: &str, org_user_id: &str, data: Json, } } - user_org.status = UserOrgStatus::Accepted as i32; + member.status = MembershipStatus::Accepted as i32; if master_password_required { - user_org.reset_password_key = data.reset_password_key; + member.reset_password_key = data.reset_password_key; } - user_org.save(&mut conn).await?; + member.save(&mut conn).await?; } } None => err!("Invited user not found"), @@ -1137,9 +1133,9 @@ async fn bulk_confirm_invite( match data.keys { Some(keys) => { for invite in keys { - let org_user_id = invite.id.unwrap_or_default(); + let member_id = invite.id.unwrap_or_default(); let user_key = invite.key.unwrap_or_default(); - let err_msg = match _confirm_invite(org_id, &org_user_id, &user_key, &headers, &mut conn, &nt).await { + let err_msg = match _confirm_invite(org_id, &member_id, &user_key, &headers, &mut conn, &nt).await { Ok(_) => String::new(), Err(e) => format!("{e:?}"), }; @@ -1147,7 +1143,7 @@ async fn bulk_confirm_invite( bulk_response.push(json!( { "object": "OrganizationBulkConfirmResponseModel", - "id": org_user_id, + "id": member_id, "error": err_msg } )); @@ -1163,10 +1159,10 @@ async fn bulk_confirm_invite( })) } -#[post("/organizations//users//confirm", data = "")] +#[post("/organizations//users//confirm", data = "")] async fn confirm_invite( org_id: &str, - org_user_id: &str, + member_id: &str, data: Json, headers: AdminHeaders, mut conn: DbConn, @@ -1174,41 +1170,41 @@ async fn confirm_invite( ) -> EmptyResult { let data = data.into_inner(); let user_key = data.key.unwrap_or_default(); - _confirm_invite(org_id, org_user_id, &user_key, &headers, &mut conn, &nt).await + _confirm_invite(org_id, member_id, &user_key, &headers, &mut conn, &nt).await } async fn _confirm_invite( org_id: &str, - org_user_id: &str, + member_id: &str, key: &str, headers: &AdminHeaders, conn: &mut DbConn, nt: &Notify<'_>, ) -> EmptyResult { - if key.is_empty() || org_user_id.is_empty() { + if key.is_empty() || member_id.is_empty() { err!("Key or UserId is not set, unable to process request"); } - let Some(mut user_to_confirm) = UserOrganization::find_by_uuid_and_org(org_user_id, org_id, conn).await else { + let Some(mut member_to_confirm) = Membership::find_by_uuid_and_org(member_id, org_id, conn).await else { err!("The specified user isn't a member of the organization") }; - if user_to_confirm.atype != UserOrgType::User && headers.org_user_type != UserOrgType::Owner { + if member_to_confirm.atype != MembershipType::User && headers.membership_type != MembershipType::Owner { err!("Only Owners can confirm Managers, Admins or Owners") } - if user_to_confirm.status != UserOrgStatus::Accepted as i32 { + if member_to_confirm.status != MembershipStatus::Accepted as i32 { err!("User in invalid state") } - // This check is also done at accept_invite(), _confirm_invite, _activate_user(), edit_user(), admin::update_user_org_type + // This check is also done at accept_invite(), _confirm_invite, _activate_user(), edit_user(), admin::update_membership_type // It returns different error messages per function. - if user_to_confirm.atype < UserOrgType::Admin { - match OrgPolicy::is_user_allowed(&user_to_confirm.user_uuid, org_id, true, conn).await { + if member_to_confirm.atype < MembershipType::Admin { + match OrgPolicy::is_user_allowed(&member_to_confirm.user_uuid, org_id, true, conn).await { Ok(_) => {} Err(OrgPolicyErr::TwoFactorMissing) => { if CONFIG.email_2fa_auto_fallback() { - two_factor::email::find_and_activate_email_2fa(&user_to_confirm.user_uuid, conn).await?; + two_factor::email::find_and_activate_email_2fa(&member_to_confirm.user_uuid, conn).await?; } else { err!("You cannot confirm this user because they have not setup 2FA"); } @@ -1219,12 +1215,12 @@ async fn _confirm_invite( } } - user_to_confirm.status = UserOrgStatus::Confirmed as i32; - user_to_confirm.akey = key.to_string(); + member_to_confirm.status = MembershipStatus::Confirmed as i32; + member_to_confirm.akey = key.to_string(); log_event( EventType::OrganizationUserConfirmed as i32, - &user_to_confirm.uuid, + &member_to_confirm.uuid, org_id, &headers.user.uuid, headers.device.atype, @@ -1238,31 +1234,31 @@ async fn _confirm_invite( Some(org) => org.name, None => err!("Error looking up organization."), }; - let address = match User::find_by_uuid(&user_to_confirm.user_uuid, conn).await { + let address = match User::find_by_uuid(&member_to_confirm.user_uuid, conn).await { Some(user) => user.email, None => err!("Error looking up user."), }; mail::send_invite_confirmed(&address, &org_name).await?; } - let save_result = user_to_confirm.save(conn).await; + let save_result = member_to_confirm.save(conn).await; - if let Some(user) = User::find_by_uuid(&user_to_confirm.user_uuid, conn).await { + if let Some(user) = User::find_by_uuid(&member_to_confirm.user_uuid, conn).await { nt.send_user_update(UpdateType::SyncOrgKeys, &user).await; } save_result } -#[get("/organizations//users/?")] +#[get("/organizations//users/?")] async fn get_user( org_id: &str, - org_user_id: &str, + member_id: &str, data: GetOrgUserData, _headers: AdminHeaders, mut conn: DbConn, ) -> JsonResult { - let Some(user) = UserOrganization::find_by_uuid_and_org(org_user_id, org_id, &mut conn).await else { + let Some(user) = Membership::find_by_uuid_and_org(member_id, org_id, &mut conn).await else { err!("The specified user isn't a member of the organization") }; @@ -1284,64 +1280,64 @@ struct EditUserData { access_all: bool, } -#[put("/organizations//users/", data = "", rank = 1)] -async fn put_organization_user( +#[put("/organizations//users/", data = "", rank = 1)] +async fn put_membership( org_id: &str, - org_user_id: &str, + member_id: &str, data: Json, headers: AdminHeaders, conn: DbConn, ) -> EmptyResult { - edit_user(org_id, org_user_id, data, headers, conn).await + edit_user(org_id, member_id, data, headers, conn).await } -#[post("/organizations//users/", data = "", rank = 1)] +#[post("/organizations//users/", data = "", rank = 1)] async fn edit_user( org_id: &str, - org_user_id: &str, + member_id: &str, data: Json, headers: AdminHeaders, mut conn: DbConn, ) -> EmptyResult { let data: EditUserData = data.into_inner(); - let Some(new_type) = UserOrgType::from_str(&data.r#type.into_string()) else { + let Some(new_type) = MembershipType::from_str(&data.r#type.into_string()) else { err!("Invalid type") }; - let Some(mut user_to_edit) = UserOrganization::find_by_uuid_and_org(org_user_id, org_id, &mut conn).await else { + let Some(mut member_to_edit) = Membership::find_by_uuid_and_org(member_id, org_id, &mut conn).await else { err!("The specified user isn't member of the organization") }; - if new_type != user_to_edit.atype - && (user_to_edit.atype >= UserOrgType::Admin || new_type >= UserOrgType::Admin) - && headers.org_user_type != UserOrgType::Owner + if new_type != member_to_edit.atype + && (member_to_edit.atype >= MembershipType::Admin || new_type >= MembershipType::Admin) + && headers.membership_type != MembershipType::Owner { err!("Only Owners can grant and remove Admin or Owner privileges") } - if user_to_edit.atype == UserOrgType::Owner && headers.org_user_type != UserOrgType::Owner { + if member_to_edit.atype == MembershipType::Owner && headers.membership_type != MembershipType::Owner { err!("Only Owners can edit Owner users") } - if user_to_edit.atype == UserOrgType::Owner - && new_type != UserOrgType::Owner - && user_to_edit.status == UserOrgStatus::Confirmed as i32 + if member_to_edit.atype == MembershipType::Owner + && new_type != MembershipType::Owner + && member_to_edit.status == MembershipStatus::Confirmed as i32 { // Removing owner permission, check that there is at least one other confirmed owner - if UserOrganization::count_confirmed_by_org_and_type(org_id, UserOrgType::Owner, &mut conn).await <= 1 { + if Membership::count_confirmed_by_org_and_type(org_id, MembershipType::Owner, &mut conn).await <= 1 { err!("Can't delete the last owner") } } - // This check is also done at accept_invite(), _confirm_invite, _activate_user(), edit_user(), admin::update_user_org_type + // This check is also done at accept_invite(), _confirm_invite, _activate_user(), edit_user(), admin::update_membership_type // It returns different error messages per function. - if new_type < UserOrgType::Admin { - match OrgPolicy::is_user_allowed(&user_to_edit.user_uuid, org_id, true, &mut conn).await { + if new_type < MembershipType::Admin { + match OrgPolicy::is_user_allowed(&member_to_edit.user_uuid, org_id, true, &mut conn).await { Ok(_) => {} Err(OrgPolicyErr::TwoFactorMissing) => { if CONFIG.email_2fa_auto_fallback() { - two_factor::email::find_and_activate_email_2fa(&user_to_edit.user_uuid, &mut conn).await?; + two_factor::email::find_and_activate_email_2fa(&member_to_edit.user_uuid, &mut conn).await?; } else { err!("You cannot modify this user to this type because they have not setup 2FA"); } @@ -1352,11 +1348,11 @@ async fn edit_user( } } - user_to_edit.access_all = data.access_all; - user_to_edit.atype = new_type as i32; + member_to_edit.access_all = data.access_all; + member_to_edit.atype = new_type as i32; // Delete all the odd collections - for c in CollectionUser::find_by_organization_and_user_uuid(org_id, &user_to_edit.user_uuid, &mut conn).await { + for c in CollectionUser::find_by_organization_and_user_uuid(org_id, &member_to_edit.user_uuid, &mut conn).await { c.delete(&mut conn).await?; } @@ -1367,7 +1363,7 @@ async fn edit_user( None => err!("Collection not found in Organization"), Some(collection) => { CollectionUser::save( - &user_to_edit.user_uuid, + &member_to_edit.user_uuid, &collection.uuid, col.read_only, col.hide_passwords, @@ -1379,16 +1375,16 @@ async fn edit_user( } } - GroupUser::delete_all_by_user(&user_to_edit.uuid, &mut conn).await?; + GroupUser::delete_all_by_member(&member_to_edit.uuid, &mut conn).await?; for group in data.groups.iter().flatten() { - let mut group_entry = GroupUser::new(String::from(group), user_to_edit.uuid.clone()); + let mut group_entry = GroupUser::new(String::from(group), member_to_edit.uuid.clone()); group_entry.save(&mut conn).await?; } log_event( EventType::OrganizationUserUpdated as i32, - &user_to_edit.uuid, + &member_to_edit.uuid, org_id, &headers.user.uuid, headers.device.atype, @@ -1397,7 +1393,7 @@ async fn edit_user( ) .await; - user_to_edit.save(&mut conn).await + member_to_edit.save(&mut conn).await } #[delete("/organizations//users", data = "")] @@ -1411,8 +1407,8 @@ async fn bulk_delete_user( let data: OrgBulkIds = data.into_inner(); let mut bulk_response = Vec::new(); - for org_user_id in data.ids { - let err_msg = match _delete_user(org_id, &org_user_id, &headers, &mut conn, &nt).await { + for member_id in data.ids { + let err_msg = match _delete_user(org_id, &member_id, &headers, &mut conn, &nt).await { Ok(_) => String::new(), Err(e) => format!("{e:?}"), }; @@ -1420,7 +1416,7 @@ async fn bulk_delete_user( bulk_response.push(json!( { "object": "OrganizationBulkConfirmResponseModel", - "id": org_user_id, + "id": member_id, "error": err_msg } )) @@ -1433,53 +1429,54 @@ async fn bulk_delete_user( })) } -#[delete("/organizations//users/")] +#[delete("/organizations//users/")] async fn delete_user( org_id: &str, - org_user_id: &str, + member_id: &str, headers: AdminHeaders, mut conn: DbConn, nt: Notify<'_>, ) -> EmptyResult { - _delete_user(org_id, org_user_id, &headers, &mut conn, &nt).await + _delete_user(org_id, member_id, &headers, &mut conn, &nt).await } -#[post("/organizations//users//delete")] +#[post("/organizations//users//delete")] async fn post_delete_user( org_id: &str, - org_user_id: &str, + member_id: &str, headers: AdminHeaders, mut conn: DbConn, nt: Notify<'_>, ) -> EmptyResult { - _delete_user(org_id, org_user_id, &headers, &mut conn, &nt).await + _delete_user(org_id, member_id, &headers, &mut conn, &nt).await } async fn _delete_user( org_id: &str, - org_user_id: &str, + member_id: &str, headers: &AdminHeaders, conn: &mut DbConn, nt: &Notify<'_>, ) -> EmptyResult { - let Some(user_to_delete) = UserOrganization::find_by_uuid_and_org(org_user_id, org_id, conn).await else { + let Some(member_to_delete) = Membership::find_by_uuid_and_org(member_id, org_id, conn).await else { err!("User to delete isn't member of the organization") }; - if user_to_delete.atype != UserOrgType::User && headers.org_user_type != UserOrgType::Owner { + if member_to_delete.atype != MembershipType::User && headers.membership_type != MembershipType::Owner { err!("Only Owners can delete Admins or Owners") } - if user_to_delete.atype == UserOrgType::Owner && user_to_delete.status == UserOrgStatus::Confirmed as i32 { + if member_to_delete.atype == MembershipType::Owner && member_to_delete.status == MembershipStatus::Confirmed as i32 + { // Removing owner, check that there is at least one other confirmed owner - if UserOrganization::count_confirmed_by_org_and_type(org_id, UserOrgType::Owner, conn).await <= 1 { + if Membership::count_confirmed_by_org_and_type(org_id, MembershipType::Owner, conn).await <= 1 { err!("Can't delete the last owner") } } log_event( EventType::OrganizationUserRemoved as i32, - &user_to_delete.uuid, + &member_to_delete.uuid, org_id, &headers.user.uuid, headers.device.atype, @@ -1488,11 +1485,11 @@ async fn _delete_user( ) .await; - if let Some(user) = User::find_by_uuid(&user_to_delete.user_uuid, conn).await { + if let Some(user) = User::find_by_uuid(&member_to_delete.user_uuid, conn).await { nt.send_user_update(UpdateType::SyncOrgKeys, &user).await; } - user_to_delete.delete(conn).await + member_to_delete.delete(conn).await } #[post("/organizations//users/public-keys", data = "")] @@ -1505,23 +1502,23 @@ async fn bulk_public_keys( let data: OrgBulkIds = data.into_inner(); let mut bulk_response = Vec::new(); - // Check all received UserOrg UUID's and find the matching User to retrieve the public-key. - // If the user does not exists, just ignore it, and do not return any information regarding that UserOrg UUID. + // Check all received Membership UUID's and find the matching User to retrieve the public-key. + // If the user does not exists, just ignore it, and do not return any information regarding that Membership UUID. // The web-vault will then ignore that user for the following steps. - for user_org_id in data.ids { - match UserOrganization::find_by_uuid_and_org(&user_org_id, org_id, &mut conn).await { - Some(user_org) => match User::find_by_uuid(&user_org.user_uuid, &mut conn).await { + for member_id in data.ids { + match Membership::find_by_uuid_and_org(&member_id, org_id, &mut conn).await { + Some(member) => match User::find_by_uuid(&member.user_uuid, &mut conn).await { Some(user) => bulk_response.push(json!( { "object": "organizationUserPublicKeyResponseModel", - "id": user_org_id, + "id": member_id, "userId": user.uuid, "key": user.public_key } )), None => debug!("User doesn't exist"), }, - None => debug!("UserOrg doesn't exist"), + None => debug!("Membership doesn't exist"), } } @@ -1797,14 +1794,14 @@ async fn put_policy( // When enabling the SingleOrg policy, remove this org's members that are members of other orgs if pol_type_enum == OrgPolicyType::SingleOrg && data.enabled { - for member in UserOrganization::find_by_org(org_id, &mut conn).await.into_iter() { + for member in Membership::find_by_org(org_id, &mut conn).await.into_iter() { // Policy only applies to non-Owner/non-Admin members who have accepted joining the org // Exclude invited and revoked users when checking for this policy. // Those users will not be allowed to accept or be activated because of the policy checks done there. // We check if the count is larger then 1, because it includes this organization also. - if member.atype < UserOrgType::Admin - && member.status != UserOrgStatus::Invited as i32 - && UserOrganization::count_accepted_and_confirmed_by_user(&member.user_uuid, &mut conn).await > 1 + if member.atype < MembershipType::Admin + && member.status != MembershipStatus::Invited as i32 + && Membership::count_accepted_and_confirmed_by_user(&member.user_uuid, &mut conn).await > 1 { if CONFIG.mail_enabled() { let org = Organization::find_by_uuid(&member.org_uuid, &mut conn).await.unwrap(); @@ -1948,8 +1945,8 @@ async fn import(org_id: &str, data: Json, headers: Headers, mut c // as opposed to upstream which only removes auto-imported users. // User needs to be admin or owner to use the Directory Connector - match UserOrganization::find_by_user_and_org(&headers.user.uuid, org_id, &mut conn).await { - Some(user_org) if user_org.atype >= UserOrgType::Admin => { /* Okay, nothing to do */ } + match Membership::find_by_user_and_org(&headers.user.uuid, org_id, &mut conn).await { + Some(member) if member.atype >= MembershipType::Admin => { /* Okay, nothing to do */ } Some(_) => err!("User has insufficient permissions to use Directory Connector"), None => err!("User not part of organization"), }; @@ -1957,10 +1954,10 @@ async fn import(org_id: &str, data: Json, headers: Headers, mut c for user_data in &data.users { if user_data.deleted { // If user is marked for deletion and it exists, delete it - if let Some(user_org) = UserOrganization::find_by_email_and_org(&user_data.email, org_id, &mut conn).await { + if let Some(member) = Membership::find_by_email_and_org(&user_data.email, org_id, &mut conn).await { log_event( EventType::OrganizationUserRemoved as i32, - &user_org.uuid, + &member.uuid, org_id, &headers.user.uuid, headers.device.atype, @@ -1969,28 +1966,28 @@ async fn import(org_id: &str, data: Json, headers: Headers, mut c ) .await; - user_org.delete(&mut conn).await?; + member.delete(&mut conn).await?; } // If user is not part of the organization, but it exists - } else if UserOrganization::find_by_email_and_org(&user_data.email, org_id, &mut conn).await.is_none() { + } else if Membership::find_by_email_and_org(&user_data.email, org_id, &mut conn).await.is_none() { if let Some(user) = User::find_by_mail(&user_data.email, &mut conn).await { - let user_org_status = if CONFIG.mail_enabled() { - UserOrgStatus::Invited as i32 + let member_status = if CONFIG.mail_enabled() { + MembershipStatus::Invited as i32 } else { - UserOrgStatus::Accepted as i32 // Automatically mark user as accepted if no email invites + MembershipStatus::Accepted as i32 // Automatically mark user as accepted if no email invites }; - let mut new_org_user = UserOrganization::new(user.uuid.clone(), String::from(org_id)); - new_org_user.access_all = false; - new_org_user.atype = UserOrgType::User as i32; - new_org_user.status = user_org_status; + let mut new_member = Membership::new(user.uuid.clone(), String::from(org_id)); + new_member.access_all = false; + new_member.atype = MembershipType::User as i32; + new_member.status = member_status; - new_org_user.save(&mut conn).await?; + new_member.save(&mut conn).await?; log_event( EventType::OrganizationUserInvited as i32, - &new_org_user.uuid, + &new_member.uuid, org_id, &headers.user.uuid, headers.device.atype, @@ -2008,7 +2005,7 @@ async fn import(org_id: &str, data: Json, headers: Headers, mut c mail::send_invite( &user, Some(String::from(org_id)), - Some(new_org_user.uuid), + Some(new_member.uuid), &org_name, Some(headers.user.email.clone()), ) @@ -2020,12 +2017,12 @@ async fn import(org_id: &str, data: Json, headers: Headers, mut c // If this flag is enabled, any user that isn't provided in the Users list will be removed (by default they will be kept unless they have Deleted == true) if data.overwrite_existing { - for user_org in UserOrganization::find_by_org_and_type(org_id, UserOrgType::User, &mut conn).await { - if let Some(user_email) = User::find_by_uuid(&user_org.user_uuid, &mut conn).await.map(|u| u.email) { + for member in Membership::find_by_org_and_type(org_id, MembershipType::User, &mut conn).await { + if let Some(user_email) = User::find_by_uuid(&member.user_uuid, &mut conn).await.map(|u| u.email) { if !data.users.iter().any(|u| u.email == user_email) { log_event( EventType::OrganizationUserRemoved as i32, - &user_org.uuid, + &member.uuid, org_id, &headers.user.uuid, headers.device.atype, @@ -2034,7 +2031,7 @@ async fn import(org_id: &str, data: Json, headers: Headers, mut c ) .await; - user_org.delete(&mut conn).await?; + member.delete(&mut conn).await?; } } } @@ -2044,35 +2041,25 @@ async fn import(org_id: &str, data: Json, headers: Headers, mut c } // Pre web-vault v2022.9.x endpoint -#[put("/organizations//users//deactivate")] -async fn deactivate_organization_user( - org_id: &str, - org_user_id: &str, - headers: AdminHeaders, - mut conn: DbConn, -) -> EmptyResult { - _revoke_organization_user(org_id, org_user_id, &headers, &mut conn).await +#[put("/organizations//users//deactivate")] +async fn deactivate_membership(org_id: &str, member_id: &str, headers: AdminHeaders, mut conn: DbConn) -> EmptyResult { + _revoke_membership(org_id, member_id, &headers, &mut conn).await } // Pre web-vault v2022.9.x endpoint #[put("/organizations//users/deactivate", data = "")] -async fn bulk_deactivate_organization_user( +async fn bulk_deactivate_membership( org_id: &str, data: Json, headers: AdminHeaders, conn: DbConn, ) -> Json { - bulk_revoke_organization_user(org_id, data, headers, conn).await + bulk_revoke_membership(org_id, data, headers, conn).await } -#[put("/organizations//users//revoke")] -async fn revoke_organization_user( - org_id: &str, - org_user_id: &str, - headers: AdminHeaders, - mut conn: DbConn, -) -> EmptyResult { - _revoke_organization_user(org_id, org_user_id, &headers, &mut conn).await +#[put("/organizations//users//revoke")] +async fn revoke_membership(org_id: &str, member_id: &str, headers: AdminHeaders, mut conn: DbConn) -> EmptyResult { + _revoke_membership(org_id, member_id, &headers, &mut conn).await } #[derive(Deserialize, Debug)] @@ -2082,7 +2069,7 @@ struct OrgBulkRevokeData { } #[put("/organizations//users/revoke", data = "")] -async fn bulk_revoke_organization_user( +async fn bulk_revoke_membership( org_id: &str, data: Json, headers: AdminHeaders, @@ -2092,9 +2079,9 @@ async fn bulk_revoke_organization_user( let mut bulk_response = Vec::new(); match data.ids { - Some(org_users) => { - for org_user_id in org_users { - let err_msg = match _revoke_organization_user(org_id, &org_user_id, &headers, &mut conn).await { + Some(members) => { + for member_id in members { + let err_msg = match _revoke_membership(org_id, &member_id, &headers, &mut conn).await { Ok(_) => String::new(), Err(e) => format!("{e:?}"), }; @@ -2102,7 +2089,7 @@ async fn bulk_revoke_organization_user( bulk_response.push(json!( { "object": "OrganizationUserBulkResponseModel", - "id": org_user_id, + "id": member_id, "error": err_msg } )); @@ -2118,32 +2105,27 @@ async fn bulk_revoke_organization_user( })) } -async fn _revoke_organization_user( - org_id: &str, - org_user_id: &str, - headers: &AdminHeaders, - conn: &mut DbConn, -) -> EmptyResult { - match UserOrganization::find_by_uuid_and_org(org_user_id, org_id, conn).await { - Some(mut user_org) if user_org.status > UserOrgStatus::Revoked as i32 => { - if user_org.user_uuid == headers.user.uuid { +async fn _revoke_membership(org_id: &str, member_id: &str, headers: &AdminHeaders, conn: &mut DbConn) -> EmptyResult { + match Membership::find_by_uuid_and_org(member_id, org_id, conn).await { + Some(mut member) if member.status > MembershipStatus::Revoked as i32 => { + if member.user_uuid == headers.user.uuid { err!("You cannot revoke yourself") } - if user_org.atype == UserOrgType::Owner && headers.org_user_type != UserOrgType::Owner { + if member.atype == MembershipType::Owner && headers.membership_type != MembershipType::Owner { err!("Only owners can revoke other owners") } - if user_org.atype == UserOrgType::Owner - && UserOrganization::count_confirmed_by_org_and_type(org_id, UserOrgType::Owner, conn).await <= 1 + if member.atype == MembershipType::Owner + && Membership::count_confirmed_by_org_and_type(org_id, MembershipType::Owner, conn).await <= 1 { err!("Organization must have at least one confirmed owner") } - user_org.revoke(); - user_org.save(conn).await?; + member.revoke(); + member.save(conn).await?; log_event( EventType::OrganizationUserRevoked as i32, - &user_org.uuid, + &member.uuid, org_id, &headers.user.uuid, headers.device.atype, @@ -2159,39 +2141,29 @@ async fn _revoke_organization_user( } // Pre web-vault v2022.9.x endpoint -#[put("/organizations//users//activate")] -async fn activate_organization_user( - org_id: &str, - org_user_id: &str, - headers: AdminHeaders, - mut conn: DbConn, -) -> EmptyResult { - _restore_organization_user(org_id, org_user_id, &headers, &mut conn).await +#[put("/organizations//users//activate")] +async fn activate_membership(org_id: &str, member_id: &str, headers: AdminHeaders, mut conn: DbConn) -> EmptyResult { + _restore_membership(org_id, member_id, &headers, &mut conn).await } // Pre web-vault v2022.9.x endpoint #[put("/organizations//users/activate", data = "")] -async fn bulk_activate_organization_user( +async fn bulk_activate_membership( org_id: &str, data: Json, headers: AdminHeaders, conn: DbConn, ) -> Json { - bulk_restore_organization_user(org_id, data, headers, conn).await + bulk_restore_membership(org_id, data, headers, conn).await } -#[put("/organizations//users//restore")] -async fn restore_organization_user( - org_id: &str, - org_user_id: &str, - headers: AdminHeaders, - mut conn: DbConn, -) -> EmptyResult { - _restore_organization_user(org_id, org_user_id, &headers, &mut conn).await +#[put("/organizations//users//restore")] +async fn restore_membership(org_id: &str, member_id: &str, headers: AdminHeaders, mut conn: DbConn) -> EmptyResult { + _restore_membership(org_id, member_id, &headers, &mut conn).await } #[put("/organizations//users/restore", data = "")] -async fn bulk_restore_organization_user( +async fn bulk_restore_membership( org_id: &str, data: Json, headers: AdminHeaders, @@ -2200,8 +2172,8 @@ async fn bulk_restore_organization_user( let data = data.into_inner(); let mut bulk_response = Vec::new(); - for org_user_id in data.ids { - let err_msg = match _restore_organization_user(org_id, &org_user_id, &headers, &mut conn).await { + for member_id in data.ids { + let err_msg = match _restore_membership(org_id, &member_id, &headers, &mut conn).await { Ok(_) => String::new(), Err(e) => format!("{e:?}"), }; @@ -2209,7 +2181,7 @@ async fn bulk_restore_organization_user( bulk_response.push(json!( { "object": "OrganizationUserBulkResponseModel", - "id": org_user_id, + "id": member_id, "error": err_msg } )); @@ -2222,29 +2194,24 @@ async fn bulk_restore_organization_user( })) } -async fn _restore_organization_user( - org_id: &str, - org_user_id: &str, - headers: &AdminHeaders, - conn: &mut DbConn, -) -> EmptyResult { - match UserOrganization::find_by_uuid_and_org(org_user_id, org_id, conn).await { - Some(mut user_org) if user_org.status < UserOrgStatus::Accepted as i32 => { - if user_org.user_uuid == headers.user.uuid { +async fn _restore_membership(org_id: &str, member_id: &str, headers: &AdminHeaders, conn: &mut DbConn) -> EmptyResult { + match Membership::find_by_uuid_and_org(member_id, org_id, conn).await { + Some(mut member) if member.status < MembershipStatus::Accepted as i32 => { + if member.user_uuid == headers.user.uuid { err!("You cannot restore yourself") } - if user_org.atype == UserOrgType::Owner && headers.org_user_type != UserOrgType::Owner { + if member.atype == MembershipType::Owner && headers.membership_type != MembershipType::Owner { err!("Only owners can restore other owners") } - // This check is also done at accept_invite(), _confirm_invite, _activate_user(), edit_user(), admin::update_user_org_type + // This check is also done at accept_invite(), _confirm_invite, _activate_user(), edit_user(), admin::update_membership_type // It returns different error messages per function. - if user_org.atype < UserOrgType::Admin { - match OrgPolicy::is_user_allowed(&user_org.user_uuid, org_id, false, conn).await { + if member.atype < MembershipType::Admin { + match OrgPolicy::is_user_allowed(&member.user_uuid, org_id, false, conn).await { Ok(_) => {} Err(OrgPolicyErr::TwoFactorMissing) => { if CONFIG.email_2fa_auto_fallback() { - two_factor::email::find_and_activate_email_2fa(&user_org.user_uuid, conn).await?; + two_factor::email::find_and_activate_email_2fa(&member.user_uuid, conn).await?; } else { err!("You cannot restore this user because they have not setup 2FA"); } @@ -2255,12 +2222,12 @@ async fn _restore_organization_user( } } - user_org.restore(); - user_org.save(conn).await?; + member.restore(); + member.save(conn).await?; log_event( EventType::OrganizationUserRestored as i32, - &user_org.uuid, + &member.uuid, org_id, &headers.user.uuid, headers.device.atype, @@ -2610,7 +2577,7 @@ async fn get_user_groups(org_id: &str, user_id: &str, _headers: AdminHeaders, mu err!("Group support is disabled"); } - if UserOrganization::find_by_uuid_and_org(user_id, org_id, &mut conn).await.is_none() { + if Membership::find_by_uuid_and_org(user_id, org_id, &mut conn).await.is_none() { err!("User could not be found!") }; @@ -2626,21 +2593,21 @@ struct OrganizationUserUpdateGroupsRequest { group_ids: Vec, } -#[post("/organizations//users//groups", data = "")] +#[post("/organizations//users//groups", data = "")] async fn post_user_groups( org_id: &str, - org_user_id: &str, + member_id: &str, data: Json, headers: AdminHeaders, conn: DbConn, ) -> EmptyResult { - put_user_groups(org_id, org_user_id, data, headers, conn).await + put_user_groups(org_id, member_id, data, headers, conn).await } -#[put("/organizations//users//groups", data = "")] +#[put("/organizations//users//groups", data = "")] async fn put_user_groups( org_id: &str, - org_user_id: &str, + member_id: &str, data: Json, headers: AdminHeaders, mut conn: DbConn, @@ -2649,21 +2616,21 @@ async fn put_user_groups( err!("Group support is disabled"); } - if UserOrganization::find_by_uuid_and_org(org_user_id, org_id, &mut conn).await.is_none() { + if Membership::find_by_uuid_and_org(member_id, org_id, &mut conn).await.is_none() { err!("User could not be found or does not belong to the organization."); } - GroupUser::delete_all_by_user(org_user_id, &mut conn).await?; + GroupUser::delete_all_by_member(&member_id, &mut conn).await?; let assigned_group_ids = data.into_inner(); for assigned_group_id in assigned_group_ids.group_ids { - let mut group_user = GroupUser::new(assigned_group_id.clone(), String::from(org_user_id)); + let mut group_user = GroupUser::new(assigned_group_id.clone(), String::from(member_id)); group_user.save(&mut conn).await?; } log_event( EventType::OrganizationUserUpdatedGroups as i32, - org_user_id, + member_id, org_id, &headers.user.uuid, headers.device.atype, @@ -2675,22 +2642,22 @@ async fn put_user_groups( Ok(()) } -#[post("/organizations//groups//delete-user/")] +#[post("/organizations//groups//delete-user/")] async fn post_delete_group_user( org_id: &str, group_id: &str, - org_user_id: &str, + member_id: &str, headers: AdminHeaders, conn: DbConn, ) -> EmptyResult { - delete_group_user(org_id, group_id, org_user_id, headers, conn).await + delete_group_user(org_id, group_id, member_id, headers, conn).await } -#[delete("/organizations//groups//users/")] +#[delete("/organizations//groups//users/")] async fn delete_group_user( org_id: &str, group_id: &str, - org_user_id: &str, + member_id: &str, headers: AdminHeaders, mut conn: DbConn, ) -> EmptyResult { @@ -2698,7 +2665,7 @@ async fn delete_group_user( err!("Group support is disabled"); } - if UserOrganization::find_by_uuid_and_org(org_user_id, org_id, &mut conn).await.is_none() { + if Membership::find_by_uuid_and_org(member_id, org_id, &mut conn).await.is_none() { err!("User could not be found or does not belong to the organization."); } @@ -2708,7 +2675,7 @@ async fn delete_group_user( log_event( EventType::OrganizationUserUpdatedGroups as i32, - org_user_id, + member_id, org_id, &headers.user.uuid, headers.device.atype, @@ -2717,7 +2684,7 @@ async fn delete_group_user( ) .await; - GroupUser::delete_by_group_id_and_user_id(group_id, org_user_id, &mut conn).await + GroupUser::delete_by_group_id_and_user_id(group_id, member_id, &mut conn).await } #[derive(Deserialize)] @@ -2757,10 +2724,10 @@ async fn get_organization_keys(org_id: &str, headers: Headers, conn: DbConn) -> get_organization_public_key(org_id, headers, conn).await } -#[put("/organizations//users//reset-password", data = "")] +#[put("/organizations//users//reset-password", data = "")] async fn put_reset_password( org_id: &str, - org_user_id: &str, + member_id: &str, headers: AdminHeaders, data: Json, mut conn: DbConn, @@ -2770,20 +2737,20 @@ async fn put_reset_password( err!("Required organization not found") }; - let Some(org_user) = UserOrganization::find_by_uuid_and_org(org_user_id, &org.uuid, &mut conn).await else { + let Some(member) = Membership::find_by_uuid_and_org(member_id, &org.uuid, &mut conn).await else { err!("User to reset isn't member of required organization") }; - let Some(user) = User::find_by_uuid(&org_user.user_uuid, &mut conn).await else { + let Some(user) = User::find_by_uuid(&member.user_uuid, &mut conn).await else { err!("User not found") }; - check_reset_password_applicable_and_permissions(org_id, org_user_id, &headers, &mut conn).await?; + check_reset_password_applicable_and_permissions(org_id, member_id, &headers, &mut conn).await?; - if org_user.reset_password_key.is_none() { + if member.reset_password_key.is_none() { err!("Password reset not or not correctly enrolled"); } - if org_user.status != (UserOrgStatus::Confirmed as i32) { + if member.status != (MembershipStatus::Confirmed as i32) { err!("Organization user must be confirmed for password reset functionality"); } @@ -2803,7 +2770,7 @@ async fn put_reset_password( log_event( EventType::OrganizationUserAdminResetPassword as i32, - org_user_id, + member_id, org_id, &headers.user.uuid, headers.device.atype, @@ -2815,10 +2782,10 @@ async fn put_reset_password( Ok(()) } -#[get("/organizations//users//reset-password-details")] +#[get("/organizations//users//reset-password-details")] async fn get_reset_password_details( org_id: &str, - org_user_id: &str, + member_id: &str, headers: AdminHeaders, mut conn: DbConn, ) -> JsonResult { @@ -2826,15 +2793,15 @@ async fn get_reset_password_details( err!("Required organization not found") }; - let Some(org_user) = UserOrganization::find_by_uuid_and_org(org_user_id, org_id, &mut conn).await else { + let Some(member) = Membership::find_by_uuid_and_org(member_id, org_id, &mut conn).await else { err!("User to reset isn't member of required organization") }; - let Some(user) = User::find_by_uuid(&org_user.user_uuid, &mut conn).await else { + let Some(user) = User::find_by_uuid(&member.user_uuid, &mut conn).await else { err!("User not found") }; - check_reset_password_applicable_and_permissions(org_id, org_user_id, &headers, &mut conn).await?; + check_reset_password_applicable_and_permissions(org_id, member_id, &headers, &mut conn).await?; // https://github.com/bitwarden/server/blob/3b50ccb9f804efaacdc46bed5b60e5b28eddefcf/src/Api/Models/Response/Organizations/OrganizationUserResponseModel.cs#L111 Ok(Json(json!({ @@ -2843,7 +2810,7 @@ async fn get_reset_password_details( "kdfIterations":user.client_kdf_iter, "kdfMemory":user.client_kdf_memory, "kdfParallelism":user.client_kdf_parallelism, - "resetPasswordKey":org_user.reset_password_key, + "resetPasswordKey":member.reset_password_key, "encryptedPrivateKey":org.private_key, }))) @@ -2851,20 +2818,20 @@ async fn get_reset_password_details( async fn check_reset_password_applicable_and_permissions( org_id: &str, - org_user_id: &str, + member_id: &str, headers: &AdminHeaders, conn: &mut DbConn, ) -> EmptyResult { check_reset_password_applicable(org_id, conn).await?; - let Some(target_user) = UserOrganization::find_by_uuid_and_org(org_user_id, org_id, conn).await else { + let Some(target_user) = Membership::find_by_uuid_and_org(member_id, org_id, conn).await else { err!("Reset target user not found") }; // Resetting user must be higher/equal to user to reset - match headers.org_user_type { - UserOrgType::Owner => Ok(()), - UserOrgType::Admin if target_user.atype <= UserOrgType::Admin => Ok(()), + match headers.membership_type { + MembershipType::Owner => Ok(()), + MembershipType::Admin if target_user.atype <= MembershipType::Admin => Ok(()), _ => err!("No permission to reset this user's password"), } } @@ -2885,15 +2852,15 @@ async fn check_reset_password_applicable(org_id: &str, conn: &mut DbConn) -> Emp Ok(()) } -#[put("/organizations//users//reset-password-enrollment", data = "")] +#[put("/organizations//users//reset-password-enrollment", data = "")] async fn put_reset_password_enrollment( org_id: &str, - org_user_id: &str, + member_id: &str, headers: Headers, data: Json, mut conn: DbConn, ) -> EmptyResult { - let Some(mut org_user) = UserOrganization::find_by_user_and_org(&headers.user.uuid, org_id, &mut conn).await else { + let Some(mut member) = Membership::find_by_user_and_org(&headers.user.uuid, org_id, &mut conn).await else { err!("User to enroll isn't member of required organization") }; @@ -2916,16 +2883,16 @@ async fn put_reset_password_enrollment( .await?; } - org_user.reset_password_key = reset_request.reset_password_key; - org_user.save(&mut conn).await?; + member.reset_password_key = reset_request.reset_password_key; + member.save(&mut conn).await?; - let log_id = if org_user.reset_password_key.is_some() { + let log_id = if member.reset_password_key.is_some() { EventType::OrganizationUserResetPasswordEnroll as i32 } else { EventType::OrganizationUserResetPasswordWithdraw as i32 }; - log_event(log_id, org_user_id, org_id, &headers.user.uuid, headers.device.atype, &headers.ip.ip, &mut conn).await; + log_event(log_id, member_id, org_id, &headers.user.uuid, headers.device.atype, &headers.ip.ip, &mut conn).await; Ok(()) } diff --git a/src/api/core/public.rs b/src/api/core/public.rs index 3b3e74cb94..1480cef098 100644 --- a/src/api/core/public.rs +++ b/src/api/core/public.rs @@ -54,38 +54,33 @@ async fn ldap_import(data: Json, token: PublicToken, mut conn: Db for user_data in &data.members { if user_data.deleted { // If user is marked for deletion and it exists, revoke it - if let Some(mut user_org) = - UserOrganization::find_by_email_and_org(&user_data.email, &org_id, &mut conn).await - { + if let Some(mut member) = Membership::find_by_email_and_org(&user_data.email, &org_id, &mut conn).await { // Only revoke a user if it is not the last confirmed owner - let revoked = if user_org.atype == UserOrgType::Owner - && user_org.status == UserOrgStatus::Confirmed as i32 + let revoked = if member.atype == MembershipType::Owner + && member.status == MembershipStatus::Confirmed as i32 { - if UserOrganization::count_confirmed_by_org_and_type(&org_id, UserOrgType::Owner, &mut conn).await - <= 1 + if Membership::count_confirmed_by_org_and_type(&org_id, MembershipType::Owner, &mut conn).await <= 1 { warn!("Can't revoke the last owner"); false } else { - user_org.revoke() + member.revoke() } } else { - user_org.revoke() + member.revoke() }; - let ext_modified = user_org.set_external_id(Some(user_data.external_id.clone())); + let ext_modified = member.set_external_id(Some(user_data.external_id.clone())); if revoked || ext_modified { - user_org.save(&mut conn).await?; + member.save(&mut conn).await?; } } // If user is part of the organization, restore it - } else if let Some(mut user_org) = - UserOrganization::find_by_email_and_org(&user_data.email, &org_id, &mut conn).await - { - let restored = user_org.restore(); - let ext_modified = user_org.set_external_id(Some(user_data.external_id.clone())); + } else if let Some(mut member) = Membership::find_by_email_and_org(&user_data.email, &org_id, &mut conn).await { + let restored = member.restore(); + let ext_modified = member.set_external_id(Some(user_data.external_id.clone())); if restored || ext_modified { - user_org.save(&mut conn).await?; + member.save(&mut conn).await?; } } else { // If user is not part of the organization @@ -103,19 +98,19 @@ async fn ldap_import(data: Json, token: PublicToken, mut conn: Db new_user } }; - let user_org_status = if CONFIG.mail_enabled() || user.password_hash.is_empty() { - UserOrgStatus::Invited as i32 + let member_status = if CONFIG.mail_enabled() || user.password_hash.is_empty() { + MembershipStatus::Invited as i32 } else { - UserOrgStatus::Accepted as i32 // Automatically mark user as accepted if no email invites + MembershipStatus::Accepted as i32 // Automatically mark user as accepted if no email invites }; - let mut new_org_user = UserOrganization::new(user.uuid.clone(), org_id.clone()); - new_org_user.set_external_id(Some(user_data.external_id.clone())); - new_org_user.access_all = false; - new_org_user.atype = UserOrgType::User as i32; - new_org_user.status = user_org_status; + let mut new_member = Membership::new(user.uuid.clone(), org_id.clone()); + new_member.set_external_id(Some(user_data.external_id.clone())); + new_member.access_all = false; + new_member.atype = MembershipType::User as i32; + new_member.status = member_status; - new_org_user.save(&mut conn).await?; + new_member.save(&mut conn).await?; if CONFIG.mail_enabled() { let (org_name, org_email) = match Organization::find_by_uuid(&org_id, &mut conn).await { @@ -123,7 +118,7 @@ async fn ldap_import(data: Json, token: PublicToken, mut conn: Db None => err!("Error looking up organization"), }; - mail::send_invite(&user, Some(org_id.clone()), Some(new_org_user.uuid), &org_name, Some(org_email)) + mail::send_invite(&user, Some(org_id.clone()), Some(new_member.uuid), &org_name, Some(org_email)) .await?; } } @@ -149,9 +144,8 @@ async fn ldap_import(data: Json, token: PublicToken, mut conn: Db GroupUser::delete_all_by_group(&group_uuid, &mut conn).await?; for ext_id in &group_data.member_external_ids { - if let Some(user_org) = UserOrganization::find_by_external_id_and_org(ext_id, &org_id, &mut conn).await - { - let mut group_user = GroupUser::new(group_uuid.clone(), user_org.uuid.clone()); + if let Some(member) = Membership::find_by_external_id_and_org(ext_id, &org_id, &mut conn).await { + let mut group_user = GroupUser::new(group_uuid.clone(), member.uuid.clone()); group_user.save(&mut conn).await?; } } @@ -164,20 +158,19 @@ async fn ldap_import(data: Json, token: PublicToken, mut conn: Db if data.overwrite_existing { // Generate a HashSet to quickly verify if a member is listed or not. let sync_members: HashSet = data.members.into_iter().map(|m| m.external_id).collect(); - for user_org in UserOrganization::find_by_org(&org_id, &mut conn).await { - if let Some(ref user_external_id) = user_org.external_id { + for member in Membership::find_by_org(&org_id, &mut conn).await { + if let Some(ref user_external_id) = member.external_id { if !sync_members.contains(user_external_id) { - if user_org.atype == UserOrgType::Owner && user_org.status == UserOrgStatus::Confirmed as i32 { + if member.atype == MembershipType::Owner && member.status == MembershipStatus::Confirmed as i32 { // Removing owner, check that there is at least one other confirmed owner - if UserOrganization::count_confirmed_by_org_and_type(&org_id, UserOrgType::Owner, &mut conn) - .await + if Membership::count_confirmed_by_org_and_type(&org_id, MembershipType::Owner, &mut conn).await <= 1 { warn!("Can't delete the last owner"); continue; } } - user_org.delete(&mut conn).await?; + member.delete(&mut conn).await?; } } } diff --git a/src/api/core/two_factor/mod.rs b/src/api/core/two_factor/mod.rs index 486b526a0a..756ae83697 100644 --- a/src/api/core/two_factor/mod.rs +++ b/src/api/core/two_factor/mod.rs @@ -178,12 +178,11 @@ pub async fn enforce_2fa_policy( ip: &std::net::IpAddr, conn: &mut DbConn, ) -> EmptyResult { - for member in UserOrganization::find_by_user_and_policy(&user.uuid, OrgPolicyType::TwoFactorAuthentication, conn) - .await - .into_iter() + for member in + Membership::find_by_user_and_policy(&user.uuid, OrgPolicyType::TwoFactorAuthentication, conn).await.into_iter() { // Policy only applies to non-Owner/non-Admin members who have accepted joining the org - if member.atype < UserOrgType::Admin { + if member.atype < MembershipType::Admin { if CONFIG.mail_enabled() { let org = Organization::find_by_uuid(&member.org_uuid, conn).await.unwrap(); mail::send_2fa_removed_from_org(&user.email, &org.name).await?; @@ -216,9 +215,9 @@ pub async fn enforce_2fa_policy_for_org( conn: &mut DbConn, ) -> EmptyResult { let org = Organization::find_by_uuid(org_uuid, conn).await.unwrap(); - for member in UserOrganization::find_confirmed_by_org(org_uuid, conn).await.into_iter() { + for member in Membership::find_confirmed_by_org(org_uuid, conn).await.into_iter() { // Don't enforce the policy for Admins and Owners. - if member.atype < UserOrgType::Admin && TwoFactor::find_by_user(&member.user_uuid, conn).await.is_empty() { + if member.atype < MembershipType::Admin && TwoFactor::find_by_user(&member.user_uuid, conn).await.is_empty() { if CONFIG.mail_enabled() { let user = User::find_by_uuid(&member.user_uuid, conn).await.unwrap(); mail::send_2fa_removed_from_org(&user.email, &org.name).await?; diff --git a/src/api/identity.rs b/src/api/identity.rs index 13248f0ad7..1d1119875c 100644 --- a/src/api/identity.rs +++ b/src/api/identity.rs @@ -111,7 +111,7 @@ async fn _refresh_login(data: ConnectData, conn: &mut DbConn) -> JsonResult { // Because this might get used in the future, and is add by the Bitwarden Server, lets keep it, but then commented out // See: https://github.com/dani-garcia/vaultwarden/issues/4156 // --- - // let orgs = UserOrganization::find_confirmed_by_user(&user.uuid, conn).await; + // let members = Membership::find_confirmed_by_user(&user.uuid, conn).await; let (access_token, expires_in) = device.refresh_tokens(&user, scope_vec); device.save(conn).await?; @@ -291,7 +291,7 @@ async fn _password_login( // Because this might get used in the future, and is add by the Bitwarden Server, lets keep it, but then commented out // See: https://github.com/dani-garcia/vaultwarden/issues/4156 // --- - // let orgs = UserOrganization::find_confirmed_by_user(&user.uuid, conn).await; + // let members = Membership::find_confirmed_by_user(&user.uuid, conn).await; let (access_token, expires_in) = device.refresh_tokens(&user, scope_vec); device.save(conn).await?; @@ -440,7 +440,7 @@ async fn _user_api_key_login( // Because this might get used in the future, and is add by the Bitwarden Server, lets keep it, but then commented out // See: https://github.com/dani-garcia/vaultwarden/issues/4156 // --- - // let orgs = UserOrganization::find_confirmed_by_user(&user.uuid, conn).await; + // let members = Membership::find_confirmed_by_user(&user.uuid, conn).await; let (access_token, expires_in) = device.refresh_tokens(&user, scope_vec); device.save(conn).await?; diff --git a/src/auth.rs b/src/auth.rs index 15910679a9..fda4cb0224 100644 --- a/src/auth.rs +++ b/src/auth.rs @@ -191,7 +191,7 @@ pub struct InviteJwtClaims { pub email: String, pub org_id: Option, - pub user_org_id: Option, + pub member_id: Option, pub invited_by_email: Option, } @@ -199,7 +199,7 @@ pub fn generate_invite_claims( uuid: String, email: String, org_id: Option, - user_org_id: Option, + member_id: Option, invited_by_email: Option, ) -> InviteJwtClaims { let time_now = Utc::now(); @@ -211,7 +211,7 @@ pub fn generate_invite_claims( sub: uuid, email, org_id, - user_org_id, + member_id, invited_by_email, } } @@ -371,7 +371,7 @@ use rocket::{ }; use crate::db::{ - models::{Collection, Device, User, UserOrgStatus, UserOrgType, UserOrganization, UserStampException}, + models::{Collection, Device, Membership, MembershipStatus, MembershipType, User, UserStampException}, DbConn, }; @@ -534,8 +534,8 @@ pub struct OrgHeaders { pub host: String, pub device: Device, pub user: User, - pub org_user_type: UserOrgType, - pub org_user: UserOrganization, + pub membership_type: MembershipType, + pub membership: Membership, pub ip: ClientIp, } @@ -574,10 +574,10 @@ impl<'r> FromRequest<'r> for OrgHeaders { }; let user = headers.user; - let org_user = match UserOrganization::find_by_user_and_org(&user.uuid, org_id, &mut conn).await { - Some(user) => { - if user.status == UserOrgStatus::Confirmed as i32 { - user + let membership = match Membership::find_by_user_and_org(&user.uuid, org_id, &mut conn).await { + Some(member) => { + if member.status == MembershipStatus::Confirmed as i32 { + member } else { err_handler!("The current user isn't confirmed member of the organization") } @@ -589,15 +589,15 @@ impl<'r> FromRequest<'r> for OrgHeaders { host: headers.host, device: headers.device, user, - org_user_type: { - if let Some(org_usr_type) = UserOrgType::from_i32(org_user.atype) { + membership_type: { + if let Some(org_usr_type) = MembershipType::from_i32(membership.atype) { org_usr_type } else { // This should only happen if the DB is corrupted err_handler!("Unknown user type in the database") } }, - org_user, + membership, ip: headers.ip, }) } @@ -610,7 +610,7 @@ pub struct AdminHeaders { pub host: String, pub device: Device, pub user: User, - pub org_user_type: UserOrgType, + pub membership_type: MembershipType, pub ip: ClientIp, } @@ -620,12 +620,12 @@ impl<'r> FromRequest<'r> for AdminHeaders { async fn from_request(request: &'r Request<'_>) -> Outcome { let headers = try_outcome!(OrgHeaders::from_request(request).await); - if headers.org_user_type >= UserOrgType::Admin { + if headers.membership_type >= MembershipType::Admin { Outcome::Success(Self { host: headers.host, device: headers.device, user: headers.user, - org_user_type: headers.org_user_type, + membership_type: headers.membership_type, ip: headers.ip, }) } else { @@ -680,7 +680,7 @@ impl<'r> FromRequest<'r> for ManagerHeaders { async fn from_request(request: &'r Request<'_>) -> Outcome { let headers = try_outcome!(OrgHeaders::from_request(request).await); - if headers.org_user_type >= UserOrgType::Manager { + if headers.membership_type >= MembershipType::Manager { match get_col_id(request) { Some(col_id) => { let mut conn = match DbConn::from_request(request).await { @@ -688,7 +688,7 @@ impl<'r> FromRequest<'r> for ManagerHeaders { _ => err_handler!("Error getting DB"), }; - if !Collection::can_access_collection(&headers.org_user, &col_id, &mut conn).await { + if !Collection::can_access_collection(&headers.membership, &col_id, &mut conn).await { err_handler!("The current user isn't a manager for this collection") } } @@ -724,7 +724,7 @@ pub struct ManagerHeadersLoose { pub host: String, pub device: Device, pub user: User, - pub org_user: UserOrganization, + pub membership: Membership, pub ip: ClientIp, } @@ -734,12 +734,12 @@ impl<'r> FromRequest<'r> for ManagerHeadersLoose { async fn from_request(request: &'r Request<'_>) -> Outcome { let headers = try_outcome!(OrgHeaders::from_request(request).await); - if headers.org_user_type >= UserOrgType::Manager { + if headers.membership_type >= MembershipType::Manager { Outcome::Success(Self { host: headers.host, device: headers.device, user: headers.user, - org_user: headers.org_user, + membership: headers.membership, ip: headers.ip, }) } else { @@ -769,7 +769,7 @@ impl ManagerHeaders { if uuid::Uuid::parse_str(col_id).is_err() { err!("Collection Id is malformed!"); } - if !Collection::can_access_collection(&h.org_user, col_id, conn).await { + if !Collection::can_access_collection(&h.membership, col_id, conn).await { err!("You don't have access to all collections!"); } } @@ -795,7 +795,7 @@ impl<'r> FromRequest<'r> for OwnerHeaders { async fn from_request(request: &'r Request<'_>) -> Outcome { let headers = try_outcome!(OrgHeaders::from_request(request).await); - if headers.org_user_type == UserOrgType::Owner { + if headers.membership_type == MembershipType::Owner { Outcome::Success(Self { device: headers.device, user: headers.user, diff --git a/src/db/models/cipher.rs b/src/db/models/cipher.rs index 048cb4d13d..c4bd05dbc8 100644 --- a/src/db/models/cipher.rs +++ b/src/db/models/cipher.rs @@ -4,7 +4,7 @@ use chrono::{NaiveDateTime, TimeDelta, Utc}; use serde_json::Value; use super::{ - Attachment, CollectionCipher, Favorite, FolderCipher, Group, User, UserOrgStatus, UserOrgType, UserOrganization, + Attachment, CollectionCipher, Favorite, FolderCipher, Group, Membership, MembershipStatus, MembershipType, User, }; use crate::api::core::{CipherData, CipherSyncData, CipherSyncType}; @@ -367,17 +367,16 @@ impl Cipher { // Belongs to Organization, need to update affected users if let Some(ref org_uuid) = self.organization_uuid { // users having access to the collection - let mut collection_users = - UserOrganization::find_by_cipher_and_org(&self.uuid, org_uuid, conn).await; + let mut collection_users = Membership::find_by_cipher_and_org(&self.uuid, org_uuid, conn).await; if CONFIG.org_groups_enabled() { // members of a group having access to the collection let group_users = - UserOrganization::find_by_cipher_and_org_with_group(&self.uuid, org_uuid, conn).await; + Membership::find_by_cipher_and_org_with_group(&self.uuid, org_uuid, conn).await; collection_users.extend(group_users); } - for user_org in collection_users { - User::update_uuid_revision(&user_org.user_uuid, conn).await; - user_uuids.push(user_org.user_uuid.clone()) + for member in collection_users { + User::update_uuid_revision(&member.user_uuid, conn).await; + user_uuids.push(member.user_uuid.clone()) } } } @@ -502,11 +501,11 @@ impl Cipher { ) -> bool { if let Some(ref org_uuid) = self.organization_uuid { if let Some(cipher_sync_data) = cipher_sync_data { - if let Some(cached_user_org) = cipher_sync_data.user_organizations.get(org_uuid) { - return cached_user_org.has_full_access(); + if let Some(cached_member) = cipher_sync_data.members.get(org_uuid) { + return cached_member.has_full_access(); } - } else if let Some(user_org) = UserOrganization::find_by_user_and_org(user_uuid, org_uuid, conn).await { - return user_org.has_full_access(); + } else if let Some(member) = Membership::find_by_user_and_org(user_uuid, org_uuid, conn).await { + return member.has_full_access(); } } false @@ -721,7 +720,7 @@ impl Cipher { .left_join(users_organizations::table.on( ciphers::organization_uuid.eq(users_organizations::org_uuid.nullable()) .and(users_organizations::user_uuid.eq(user_uuid)) - .and(users_organizations::status.eq(UserOrgStatus::Confirmed as i32)) + .and(users_organizations::status.eq(MembershipStatus::Confirmed as i32)) )) .left_join(users_collections::table.on( ciphers_collections::collection_uuid.eq(users_collections::collection_uuid) @@ -748,7 +747,7 @@ impl Cipher { if !visible_only { query = query.or_filter( - users_organizations::atype.le(UserOrgType::Admin as i32) // Org admin/owner + users_organizations::atype.le(MembershipType::Admin as i32) // Org admin/owner ); } @@ -766,7 +765,7 @@ impl Cipher { .left_join(users_organizations::table.on( ciphers::organization_uuid.eq(users_organizations::org_uuid.nullable()) .and(users_organizations::user_uuid.eq(user_uuid)) - .and(users_organizations::status.eq(UserOrgStatus::Confirmed as i32)) + .and(users_organizations::status.eq(MembershipStatus::Confirmed as i32)) )) .left_join(users_collections::table.on( ciphers_collections::collection_uuid.eq(users_collections::collection_uuid) @@ -780,7 +779,7 @@ impl Cipher { if !visible_only { query = query.or_filter( - users_organizations::atype.le(UserOrgType::Admin as i32) // Org admin/owner + users_organizations::atype.le(MembershipType::Admin as i32) // Org admin/owner ); } @@ -946,7 +945,7 @@ impl Cipher { .or(groups::access_all.eq(true)) // Access via groups .or(collections_groups::collections_uuid.is_not_null() // Access via groups .and(collections_groups::read_only.eq(false))) - .or(users_organizations::atype.le(UserOrgType::Admin as i32)) // User is admin or owner + .or(users_organizations::atype.le(MembershipType::Admin as i32)) // User is admin or owner ) .select(ciphers_collections::collection_uuid) .load::(conn).unwrap_or_default() @@ -969,7 +968,7 @@ impl Cipher { .filter(users_organizations::access_all.eq(true) // User has access all .or(users_collections::user_uuid.eq(user_id) // User has access to collection .and(users_collections::read_only.eq(false))) - .or(users_organizations::atype.le(UserOrgType::Admin as i32)) // User is admin or owner + .or(users_organizations::atype.le(MembershipType::Admin as i32)) // User is admin or owner ) .select(ciphers_collections::collection_uuid) .load::(conn).unwrap_or_default() @@ -1008,7 +1007,7 @@ impl Cipher { )) .or_filter(users_collections::user_uuid.eq(user_id)) // User has access to collection .or_filter(users_organizations::access_all.eq(true)) // User has access all - .or_filter(users_organizations::atype.le(UserOrgType::Admin as i32)) // User is admin or owner + .or_filter(users_organizations::atype.le(MembershipType::Admin as i32)) // User is admin or owner .or_filter(groups::access_all.eq(true)) //Access via group .or_filter(collections_groups::collections_uuid.is_not_null()) //Access via group .select(ciphers_collections::all_columns) diff --git a/src/db/models/collection.rs b/src/db/models/collection.rs index a26f22c72e..d8aceba37b 100644 --- a/src/db/models/collection.rs +++ b/src/db/models/collection.rs @@ -1,6 +1,6 @@ use serde_json::Value; -use super::{CollectionGroup, GroupUser, User, UserOrgStatus, UserOrgType, UserOrganization}; +use super::{CollectionGroup, GroupUser, Membership, MembershipStatus, MembershipType, User}; use crate::CONFIG; db_object! { @@ -79,13 +79,13 @@ impl Collection { conn: &mut DbConn, ) -> Value { let (read_only, hide_passwords, can_manage) = if let Some(cipher_sync_data) = cipher_sync_data { - match cipher_sync_data.user_organizations.get(&self.org_uuid) { + match cipher_sync_data.members.get(&self.org_uuid) { // Only for Manager types Bitwarden returns true for the can_manage option // Owners and Admins always have true - Some(uo) if uo.has_full_access() => (false, false, uo.atype >= UserOrgType::Manager), - Some(uo) => { + Some(m) if m.has_full_access() => (false, false, m.atype >= MembershipType::Manager), + Some(m) => { // Only let a manager manage collections when the have full read/write access - let is_manager = uo.atype == UserOrgType::Manager; + let is_manager = m.atype == MembershipType::Manager; if let Some(uc) = cipher_sync_data.user_collections.get(&self.uuid) { (uc.read_only, uc.hide_passwords, is_manager && !uc.read_only && !uc.hide_passwords) } else if let Some(cg) = cipher_sync_data.user_collections_groups.get(&self.uuid) { @@ -97,10 +97,10 @@ impl Collection { _ => (true, true, false), } } else { - match UserOrganization::find_confirmed_by_user_and_org(user_uuid, &self.org_uuid, conn).await { - Some(ou) if ou.has_full_access() => (false, false, ou.atype >= UserOrgType::Manager), - Some(ou) => { - let is_manager = ou.atype == UserOrgType::Manager; + match Membership::find_confirmed_by_user_and_org(user_uuid, &self.org_uuid, conn).await { + Some(m) if m.has_full_access() => (false, false, m.atype >= MembershipType::Manager), + Some(m) => { + let is_manager = m.atype == MembershipType::Manager; let read_only = !self.is_writable_by_user(user_uuid, conn).await; let hide_passwords = self.hide_passwords_for_user(user_uuid, conn).await; (read_only, hide_passwords, is_manager && !read_only && !hide_passwords) @@ -121,13 +121,13 @@ impl Collection { json_object } - pub async fn can_access_collection(org_user: &UserOrganization, col_id: &str, conn: &mut DbConn) -> bool { - org_user.has_status(UserOrgStatus::Confirmed) - && (org_user.has_full_access() - || CollectionUser::has_access_to_collection_by_user(col_id, &org_user.user_uuid, conn).await + pub async fn can_access_collection(member: &Membership, col_id: &str, conn: &mut DbConn) -> bool { + member.has_status(MembershipStatus::Confirmed) + && (member.has_full_access() + || CollectionUser::has_access_to_collection_by_user(col_id, &member.user_uuid, conn).await || (CONFIG.org_groups_enabled() - && (GroupUser::has_full_access_by_member(&org_user.org_uuid, &org_user.uuid, conn).await - || GroupUser::has_access_to_collection_by_member(col_id, &org_user.uuid, conn).await))) + && (GroupUser::has_full_access_by_member(&member.org_uuid, &member.uuid, conn).await + || GroupUser::has_access_to_collection_by_member(col_id, &member.uuid, conn).await))) } } @@ -193,8 +193,8 @@ impl Collection { } pub async fn update_users_revision(&self, conn: &mut DbConn) { - for user_org in UserOrganization::find_by_collection_and_org(&self.uuid, &self.org_uuid, conn).await.iter() { - User::update_uuid_revision(&user_org.user_uuid, conn).await; + for member in Membership::find_by_collection_and_org(&self.uuid, &self.org_uuid, conn).await.iter() { + User::update_uuid_revision(&member.user_uuid, conn).await; } } @@ -234,7 +234,7 @@ impl Collection { ) )) .filter( - users_organizations::status.eq(UserOrgStatus::Confirmed as i32) + users_organizations::status.eq(MembershipStatus::Confirmed as i32) ) .filter( users_collections::user_uuid.eq(user_uuid).or( // Directly accessed collection @@ -265,7 +265,7 @@ impl Collection { ) )) .filter( - users_organizations::status.eq(UserOrgStatus::Confirmed as i32) + users_organizations::status.eq(MembershipStatus::Confirmed as i32) ) .filter( users_collections::user_uuid.eq(user_uuid).or( // Directly accessed collection @@ -349,7 +349,7 @@ impl Collection { .filter( users_collections::collection_uuid.eq(uuid).or( // Directly accessed collection users_organizations::access_all.eq(true).or( // access_all in Organization - users_organizations::atype.le(UserOrgType::Admin as i32) // Org admin or owner + users_organizations::atype.le(MembershipType::Admin as i32) // Org admin or owner )).or( groups::access_all.eq(true) // access_all in groups ).or( // access via groups @@ -378,7 +378,7 @@ impl Collection { .filter( users_collections::collection_uuid.eq(uuid).or( // Directly accessed collection users_organizations::access_all.eq(true).or( // access_all in Organization - users_organizations::atype.le(UserOrgType::Admin as i32) // Org admin or owner + users_organizations::atype.le(MembershipType::Admin as i32) // Org admin or owner )) ).select(collections::all_columns) .first::(conn).ok() @@ -411,7 +411,7 @@ impl Collection { collections_groups::groups_uuid.eq(groups_users::groups_uuid) .and(collections_groups::collections_uuid.eq(collections::uuid)) )) - .filter(users_organizations::atype.le(UserOrgType::Admin as i32) // Org admin or owner + .filter(users_organizations::atype.le(MembershipType::Admin as i32) // Org admin or owner .or(users_organizations::access_all.eq(true)) // access_all via membership .or(users_collections::collection_uuid.eq(&self.uuid) // write access given to collection .and(users_collections::read_only.eq(false))) @@ -436,7 +436,7 @@ impl Collection { users_collections::collection_uuid.eq(collections::uuid) .and(users_collections::user_uuid.eq(user_uuid)) )) - .filter(users_organizations::atype.le(UserOrgType::Admin as i32) // Org admin or owner + .filter(users_organizations::atype.le(MembershipType::Admin as i32) // Org admin or owner .or(users_organizations::access_all.eq(true)) // access_all via membership .or(users_collections::collection_uuid.eq(&self.uuid) // write access given to collection .and(users_collections::read_only.eq(false))) @@ -478,7 +478,7 @@ impl Collection { .filter( users_collections::collection_uuid.eq(&self.uuid).and(users_collections::hide_passwords.eq(true)).or(// Directly accessed collection users_organizations::access_all.eq(true).or( // access_all in Organization - users_organizations::atype.le(UserOrgType::Admin as i32) // Org admin or owner + users_organizations::atype.le(MembershipType::Admin as i32) // Org admin or owner )).or( groups::access_all.eq(true) // access_all in groups ).or( // access via groups @@ -607,7 +607,7 @@ impl CollectionUser { }} } - pub async fn find_by_collection_swap_user_uuid_with_org_user_uuid( + pub async fn find_by_collection_swap_user_uuid_with_member_uuid( collection_uuid: &str, conn: &mut DbConn, ) -> Vec { diff --git a/src/db/models/device.rs b/src/db/models/device.rs index 8feab49dd4..efb3380d93 100644 --- a/src/db/models/device.rs +++ b/src/db/models/device.rs @@ -75,12 +75,12 @@ impl Device { // Also These key/value pairs are not used anywhere by either Vaultwarden or Bitwarden Clients // Because these might get used in the future, and they are added by the Bitwarden Server, lets keep it, but then commented out // --- - // fn arg: orgs: Vec, + // fn arg: members: Vec, // --- - // let orgowner: Vec<_> = orgs.iter().filter(|o| o.atype == 0).map(|o| o.org_uuid.clone()).collect(); - // let orgadmin: Vec<_> = orgs.iter().filter(|o| o.atype == 1).map(|o| o.org_uuid.clone()).collect(); - // let orguser: Vec<_> = orgs.iter().filter(|o| o.atype == 2).map(|o| o.org_uuid.clone()).collect(); - // let orgmanager: Vec<_> = orgs.iter().filter(|o| o.atype == 3).map(|o| o.org_uuid.clone()).collect(); + // let orgowner: Vec<_> = members.iter().filter(|m| m.atype == 0).map(|o| o.org_uuid.clone()).collect(); + // let orgadmin: Vec<_> = members.iter().filter(|m| m.atype == 1).map(|o| o.org_uuid.clone()).collect(); + // let orguser: Vec<_> = members.iter().filter(|m| m.atype == 2).map(|o| o.org_uuid.clone()).collect(); + // let orgmanager: Vec<_> = members.iter().filter(|m| m.atype == 3).map(|o| o.org_uuid.clone()).collect(); // Create the JWT claims struct, to send to the client use crate::auth::{encode_jwt, LoginJwtClaims, DEFAULT_VALIDITY, JWT_LOGIN_ISSUER}; diff --git a/src/db/models/event.rs b/src/db/models/event.rs index 22d8fb00e8..0f9e0e107f 100644 --- a/src/db/models/event.rs +++ b/src/db/models/event.rs @@ -274,16 +274,16 @@ impl Event { }} } - pub async fn find_by_org_and_user_org( + pub async fn find_by_org_and_member( org_uuid: &str, - user_org_uuid: &str, + member_uuid: &str, start: &NaiveDateTime, end: &NaiveDateTime, conn: &mut DbConn, ) -> Vec { db_run! { conn: { event::table - .inner_join(users_organizations::table.on(users_organizations::uuid.eq(user_org_uuid))) + .inner_join(users_organizations::table.on(users_organizations::uuid.eq(member_uuid))) .filter(event::org_uuid.eq(org_uuid)) .filter(event::event_date.between(start, end)) .filter(event::user_uuid.eq(users_organizations::user_uuid.nullable()).or(event::act_user_uuid.eq(users_organizations::user_uuid.nullable()))) diff --git a/src/db/models/group.rs b/src/db/models/group.rs index d9a08970b6..06ae42469d 100644 --- a/src/db/models/group.rs +++ b/src/db/models/group.rs @@ -1,4 +1,4 @@ -use super::{User, UserOrganization}; +use super::{Membership, User}; use crate::api::EmptyResult; use crate::db::DbConn; use crate::error::MapResult; @@ -213,7 +213,7 @@ impl Group { }} } //Returns all organizations the user has full access to - pub async fn gather_user_organizations_full_access(user_uuid: &str, conn: &mut DbConn) -> Vec { + pub async fn get_orgs_by_user_with_full_access(user_uuid: &str, conn: &mut DbConn) -> Vec { db_run! { conn: { groups_users::table .inner_join(users_organizations::table.on( @@ -520,9 +520,9 @@ impl GroupUser { } pub async fn update_user_revision(&self, conn: &mut DbConn) { - match UserOrganization::find_by_uuid(&self.users_organizations_uuid, conn).await { - Some(user) => User::update_uuid_revision(&user.user_uuid, conn).await, - None => warn!("User could not be found!"), + match Membership::find_by_uuid(&self.users_organizations_uuid, conn).await { + Some(member) => User::update_uuid_revision(&member.user_uuid, conn).await, + None => warn!("Member could not be found!"), } } @@ -531,9 +531,9 @@ impl GroupUser { users_organizations_uuid: &str, conn: &mut DbConn, ) -> EmptyResult { - match UserOrganization::find_by_uuid(users_organizations_uuid, conn).await { - Some(user) => User::update_uuid_revision(&user.user_uuid, conn).await, - None => warn!("User could not be found!"), + match Membership::find_by_uuid(users_organizations_uuid, conn).await { + Some(member) => User::update_uuid_revision(&member.user_uuid, conn).await, + None => warn!("Member could not be found!"), }; db_run! { conn: { @@ -559,15 +559,15 @@ impl GroupUser { }} } - pub async fn delete_all_by_user(users_organizations_uuid: &str, conn: &mut DbConn) -> EmptyResult { - match UserOrganization::find_by_uuid(users_organizations_uuid, conn).await { - Some(user) => User::update_uuid_revision(&user.user_uuid, conn).await, - None => warn!("User could not be found!"), + pub async fn delete_all_by_member(member_uuid: &str, conn: &mut DbConn) -> EmptyResult { + match Membership::find_by_uuid(member_uuid, conn).await { + Some(member) => User::update_uuid_revision(&member.user_uuid, conn).await, + None => warn!("Member could not be found!"), } db_run! { conn: { diesel::delete(groups_users::table) - .filter(groups_users::users_organizations_uuid.eq(users_organizations_uuid)) + .filter(groups_users::users_organizations_uuid.eq(member_uuid)) .execute(conn) .map_res("Error deleting user groups") }} diff --git a/src/db/models/mod.rs b/src/db/models/mod.rs index 43595bee56..b42ed6058c 100644 --- a/src/db/models/mod.rs +++ b/src/db/models/mod.rs @@ -27,7 +27,7 @@ pub use self::favorite::Favorite; pub use self::folder::{Folder, FolderCipher}; pub use self::group::{CollectionGroup, Group, GroupUser}; pub use self::org_policy::{OrgPolicy, OrgPolicyErr, OrgPolicyType}; -pub use self::organization::{Organization, OrganizationApiKey, UserOrgStatus, UserOrgType, UserOrganization}; +pub use self::organization::{Membership, MembershipStatus, MembershipType, Organization, OrganizationApiKey}; pub use self::send::{Send, SendType}; pub use self::two_factor::{TwoFactor, TwoFactorType}; pub use self::two_factor_duo_context::TwoFactorDuoContext; diff --git a/src/db/models/org_policy.rs b/src/db/models/org_policy.rs index 14447c05dd..d5cb372c30 100644 --- a/src/db/models/org_policy.rs +++ b/src/db/models/org_policy.rs @@ -5,7 +5,7 @@ use crate::api::EmptyResult; use crate::db::DbConn; use crate::error::MapResult; -use super::{TwoFactor, UserOrgStatus, UserOrgType, UserOrganization}; +use super::{Membership, MembershipStatus, MembershipType, TwoFactor}; db_object! { #[derive(Identifiable, Queryable, Insertable, AsChangeset)] @@ -161,7 +161,7 @@ impl OrgPolicy { .and(users_organizations::user_uuid.eq(user_uuid))) ) .filter( - users_organizations::status.eq(UserOrgStatus::Confirmed as i32) + users_organizations::status.eq(MembershipStatus::Confirmed as i32) ) .select(org_policies::all_columns) .load::(conn) @@ -202,10 +202,10 @@ impl OrgPolicy { .and(users_organizations::user_uuid.eq(user_uuid))) ) .filter( - users_organizations::status.eq(UserOrgStatus::Accepted as i32) + users_organizations::status.eq(MembershipStatus::Accepted as i32) ) .or_filter( - users_organizations::status.eq(UserOrgStatus::Confirmed as i32) + users_organizations::status.eq(MembershipStatus::Confirmed as i32) ) .filter(org_policies::atype.eq(policy_type as i32)) .filter(org_policies::enabled.eq(true)) @@ -229,7 +229,7 @@ impl OrgPolicy { .and(users_organizations::user_uuid.eq(user_uuid))) ) .filter( - users_organizations::status.eq(UserOrgStatus::Confirmed as i32) + users_organizations::status.eq(MembershipStatus::Confirmed as i32) ) .filter(org_policies::atype.eq(policy_type as i32)) .filter(org_policies::enabled.eq(true)) @@ -257,8 +257,8 @@ impl OrgPolicy { continue; } - if let Some(user) = UserOrganization::find_by_user_and_org(user_uuid, &policy.org_uuid, conn).await { - if user.atype < UserOrgType::Admin { + if let Some(user) = Membership::find_by_user_and_org(user_uuid, &policy.org_uuid, conn).await { + if user.atype < MembershipType::Admin { return true; } } @@ -316,8 +316,8 @@ impl OrgPolicy { for policy in OrgPolicy::find_confirmed_by_user_and_active_policy(user_uuid, OrgPolicyType::SendOptions, conn).await { - if let Some(user) = UserOrganization::find_by_user_and_org(user_uuid, &policy.org_uuid, conn).await { - if user.atype < UserOrgType::Admin { + if let Some(user) = Membership::find_by_user_and_org(user_uuid, &policy.org_uuid, conn).await { + if user.atype < MembershipType::Admin { match serde_json::from_str::(&policy.data) { Ok(opts) => { if opts.disable_hide_email { @@ -332,9 +332,9 @@ impl OrgPolicy { false } - pub async fn is_enabled_for_member(org_user_uuid: &str, policy_type: OrgPolicyType, conn: &mut DbConn) -> bool { - if let Some(membership) = UserOrganization::find_by_uuid(org_user_uuid, conn).await { - if let Some(policy) = OrgPolicy::find_by_org_and_type(&membership.org_uuid, policy_type, conn).await { + pub async fn is_enabled_for_member(member_uuid: &str, policy_type: OrgPolicyType, conn: &mut DbConn) -> bool { + if let Some(member) = Membership::find_by_uuid(member_uuid, conn).await { + if let Some(policy) = OrgPolicy::find_by_org_and_type(&member.org_uuid, policy_type, conn).await { return policy.enabled; } } diff --git a/src/db/models/organization.rs b/src/db/models/organization.rs index 15f009918f..7eacf57386 100644 --- a/src/db/models/organization.rs +++ b/src/db/models/organization.rs @@ -25,7 +25,7 @@ db_object! { #[derive(Identifiable, Queryable, Insertable, AsChangeset)] #[diesel(table_name = users_organizations)] #[diesel(primary_key(uuid))] - pub struct UserOrganization { + pub struct Membership { pub uuid: String, pub user_uuid: String, pub org_uuid: String, @@ -51,7 +51,7 @@ db_object! { } // https://github.com/bitwarden/server/blob/b86a04cef9f1e1b82cf18e49fc94e017c641130c/src/Core/Enums/OrganizationUserStatusType.cs -pub enum UserOrgStatus { +pub enum MembershipStatus { Revoked = -1, Invited = 0, Accepted = 1, @@ -59,27 +59,27 @@ pub enum UserOrgStatus { } #[derive(Copy, Clone, PartialEq, Eq, num_derive::FromPrimitive)] -pub enum UserOrgType { +pub enum MembershipType { Owner = 0, Admin = 1, User = 2, Manager = 3, } -impl UserOrgType { +impl MembershipType { pub fn from_str(s: &str) -> Option { match s { - "0" | "Owner" => Some(UserOrgType::Owner), - "1" | "Admin" => Some(UserOrgType::Admin), - "2" | "User" => Some(UserOrgType::User), - "3" | "Manager" => Some(UserOrgType::Manager), + "0" | "Owner" => Some(MembershipType::Owner), + "1" | "Admin" => Some(MembershipType::Admin), + "2" | "User" => Some(MembershipType::User), + "3" | "Manager" => Some(MembershipType::Manager), _ => None, } } } -impl Ord for UserOrgType { - fn cmp(&self, other: &UserOrgType) -> Ordering { +impl Ord for MembershipType { + fn cmp(&self, other: &MembershipType) -> Ordering { // For easy comparison, map each variant to an access level (where 0 is lowest). static ACCESS_LEVEL: [i32; 4] = [ 3, // Owner @@ -91,19 +91,19 @@ impl Ord for UserOrgType { } } -impl PartialOrd for UserOrgType { - fn partial_cmp(&self, other: &UserOrgType) -> Option { +impl PartialOrd for MembershipType { + fn partial_cmp(&self, other: &MembershipType) -> Option { Some(self.cmp(other)) } } -impl PartialEq for UserOrgType { +impl PartialEq for MembershipType { fn eq(&self, other: &i32) -> bool { *other == *self as i32 } } -impl PartialOrd for UserOrgType { +impl PartialOrd for MembershipType { fn partial_cmp(&self, other: &i32) -> Option { if let Some(other) = Self::from_i32(*other) { return Some(self.cmp(&other)); @@ -120,25 +120,25 @@ impl PartialOrd for UserOrgType { } } -impl PartialEq for i32 { - fn eq(&self, other: &UserOrgType) -> bool { +impl PartialEq for i32 { + fn eq(&self, other: &MembershipType) -> bool { *self == *other as i32 } } -impl PartialOrd for i32 { - fn partial_cmp(&self, other: &UserOrgType) -> Option { - if let Some(self_type) = UserOrgType::from_i32(*self) { +impl PartialOrd for i32 { + fn partial_cmp(&self, other: &MembershipType) -> Option { + if let Some(self_type) = MembershipType::from_i32(*self) { return Some(self_type.cmp(other)); } None } - fn lt(&self, other: &UserOrgType) -> bool { + fn lt(&self, other: &MembershipType) -> bool { matches!(self.partial_cmp(other), Some(Ordering::Less) | None) } - fn le(&self, other: &UserOrgType) -> bool { + fn le(&self, other: &MembershipType) -> bool { matches!(self.partial_cmp(other), Some(Ordering::Less | Ordering::Equal) | None) } } @@ -199,7 +199,7 @@ impl Organization { // It should also provide enough room for 100+ types, which i doubt will ever happen. static ACTIVATE_REVOKE_DIFF: i32 = 128; -impl UserOrganization { +impl Membership { pub fn new(user_uuid: String, org_uuid: String) -> Self { Self { uuid: crate::util::get_uuid(), @@ -209,15 +209,15 @@ impl UserOrganization { access_all: false, akey: String::new(), - status: UserOrgStatus::Accepted as i32, - atype: UserOrgType::User as i32, + status: MembershipStatus::Accepted as i32, + atype: MembershipType::User as i32, reset_password_key: None, external_id: None, } } pub fn restore(&mut self) -> bool { - if self.status < UserOrgStatus::Invited as i32 { + if self.status < MembershipStatus::Invited as i32 { self.status += ACTIVATE_REVOKE_DIFF; return true; } @@ -225,7 +225,7 @@ impl UserOrganization { } pub fn revoke(&mut self) -> bool { - if self.status > UserOrgStatus::Revoked as i32 { + if self.status > MembershipStatus::Revoked as i32 { self.status -= ACTIVATE_REVOKE_DIFF; return true; } @@ -234,7 +234,7 @@ impl UserOrganization { /// Return the status of the user in an unrevoked state pub fn get_unrevoked_status(&self) -> i32 { - if self.status <= UserOrgStatus::Revoked as i32 { + if self.status <= MembershipStatus::Revoked as i32 { return self.status + ACTIVATE_REVOKE_DIFF; } self.status @@ -283,8 +283,8 @@ impl Organization { err!(format!("BillingEmail {} is not a valid email address", self.billing_email.trim())) } - for user_org in UserOrganization::find_by_org(&self.uuid, conn).await.iter() { - User::update_uuid_revision(&user_org.user_uuid, conn).await; + for member in Membership::find_by_org(&self.uuid, conn).await.iter() { + User::update_uuid_revision(&member.user_uuid, conn).await; } db_run! { conn: @@ -324,7 +324,7 @@ impl Organization { Cipher::delete_all_by_organization(&self.uuid, conn).await?; Collection::delete_all_by_organization(&self.uuid, conn).await?; - UserOrganization::delete_all_by_organization(&self.uuid, conn).await?; + Membership::delete_all_by_organization(&self.uuid, conn).await?; OrgPolicy::delete_all_by_organization(&self.uuid, conn).await?; Group::delete_all_by_organization(&self.uuid, conn).await?; OrganizationApiKey::delete_all_by_organization(&self.uuid, conn).await?; @@ -352,7 +352,7 @@ impl Organization { } } -impl UserOrganization { +impl Membership { pub async fn to_json(&self, conn: &mut DbConn) -> Value { let org = Organization::find_by_uuid(&self.org_uuid, conn).await.unwrap(); @@ -446,8 +446,8 @@ impl UserOrganization { // Because BitWarden want the status to be -1 for revoked users we need to catch that here. // We subtract/add a number so we can restore/activate the user to it's previous state again. - let status = if self.status < UserOrgStatus::Revoked as i32 { - UserOrgStatus::Revoked as i32 + let status = if self.status < MembershipStatus::Revoked as i32 { + MembershipStatus::Revoked as i32 } else { self.status }; @@ -489,12 +489,12 @@ impl UserOrganization { .into_iter() .filter_map(|c| { let (read_only, hide_passwords, can_manage) = if self.has_full_access() { - (false, false, self.atype >= UserOrgType::Manager) + (false, false, self.atype >= MembershipType::Manager) } else if let Some(cu) = cu.get(&c.uuid) { ( cu.read_only, cu.hide_passwords, - self.atype == UserOrgType::Manager && !cu.read_only && !cu.hide_passwords, + self.atype == MembershipType::Manager && !cu.read_only && !cu.hide_passwords, ) // If previous checks failed it might be that this user has access via a group, but we should not return those elements here // Those are returned via a special group endpoint @@ -538,7 +538,7 @@ impl UserOrganization { json!({ "id": self.uuid, "userId": self.user_uuid, - "name": if self.get_unrevoked_status() >= UserOrgStatus::Accepted as i32 { Some(user.name) } else { None }, + "name": if self.get_unrevoked_status() >= MembershipStatus::Accepted as i32 { Some(user.name) } else { None }, "email": user.email, "externalId": self.external_id, "avatarColor": user.avatar_color, @@ -590,8 +590,8 @@ impl UserOrganization { // Because BitWarden want the status to be -1 for revoked users we need to catch that here. // We subtract/add a number so we can restore/activate the user to it's previous state again. - let status = if self.status < UserOrgStatus::Revoked as i32 { - UserOrgStatus::Revoked as i32 + let status = if self.status < MembershipStatus::Revoked as i32 { + MembershipStatus::Revoked as i32 } else { self.status }; @@ -614,7 +614,7 @@ impl UserOrganization { db_run! { conn: sqlite, mysql { match diesel::replace_into(users_organizations::table) - .values(UserOrganizationDb::to_db(self)) + .values(MembershipDb::to_db(self)) .execute(conn) { Ok(_) => Ok(()), @@ -622,7 +622,7 @@ impl UserOrganization { Err(diesel::result::Error::DatabaseError(diesel::result::DatabaseErrorKind::ForeignKeyViolation, _)) => { diesel::update(users_organizations::table) .filter(users_organizations::uuid.eq(&self.uuid)) - .set(UserOrganizationDb::to_db(self)) + .set(MembershipDb::to_db(self)) .execute(conn) .map_res("Error adding user to organization") }, @@ -630,7 +630,7 @@ impl UserOrganization { }.map_res("Error adding user to organization") } postgresql { - let value = UserOrganizationDb::to_db(self); + let value = MembershipDb::to_db(self); diesel::insert_into(users_organizations::table) .values(&value) .on_conflict(users_organizations::uuid) @@ -646,7 +646,7 @@ impl UserOrganization { User::update_uuid_revision(&self.user_uuid, conn).await; CollectionUser::delete_all_by_user_and_org(&self.user_uuid, &self.org_uuid, conn).await?; - GroupUser::delete_all_by_user(&self.uuid, conn).await?; + GroupUser::delete_all_by_member(&self.uuid, conn).await?; db_run! { conn: { diesel::delete(users_organizations::table.filter(users_organizations::uuid.eq(self.uuid))) @@ -656,46 +656,46 @@ impl UserOrganization { } pub async fn delete_all_by_organization(org_uuid: &str, conn: &mut DbConn) -> EmptyResult { - for user_org in Self::find_by_org(org_uuid, conn).await { - user_org.delete(conn).await?; + for member in Self::find_by_org(org_uuid, conn).await { + member.delete(conn).await?; } Ok(()) } pub async fn delete_all_by_user(user_uuid: &str, conn: &mut DbConn) -> EmptyResult { - for user_org in Self::find_any_state_by_user(user_uuid, conn).await { - user_org.delete(conn).await?; + for member in Self::find_any_state_by_user(user_uuid, conn).await { + member.delete(conn).await?; } Ok(()) } - pub async fn find_by_email_and_org(email: &str, org_id: &str, conn: &mut DbConn) -> Option { + pub async fn find_by_email_and_org(email: &str, org_id: &str, conn: &mut DbConn) -> Option { if let Some(user) = User::find_by_mail(email, conn).await { - if let Some(user_org) = UserOrganization::find_by_user_and_org(&user.uuid, org_id, conn).await { - return Some(user_org); + if let Some(member) = Membership::find_by_user_and_org(&user.uuid, org_id, conn).await { + return Some(member); } } None } - pub fn has_status(&self, status: UserOrgStatus) -> bool { + pub fn has_status(&self, status: MembershipStatus) -> bool { self.status == status as i32 } - pub fn has_type(&self, user_type: UserOrgType) -> bool { + pub fn has_type(&self, user_type: MembershipType) -> bool { self.atype == user_type as i32 } pub fn has_full_access(&self) -> bool { - (self.access_all || self.atype >= UserOrgType::Admin) && self.has_status(UserOrgStatus::Confirmed) + (self.access_all || self.atype >= MembershipType::Admin) && self.has_status(MembershipStatus::Confirmed) } pub async fn find_by_uuid(uuid: &str, conn: &mut DbConn) -> Option { db_run! { conn: { users_organizations::table .filter(users_organizations::uuid.eq(uuid)) - .first::(conn) + .first::(conn) .ok().from_db() }} } @@ -705,7 +705,7 @@ impl UserOrganization { users_organizations::table .filter(users_organizations::uuid.eq(uuid)) .filter(users_organizations::org_uuid.eq(org_uuid)) - .first::(conn) + .first::(conn) .ok().from_db() }} } @@ -714,8 +714,8 @@ impl UserOrganization { db_run! { conn: { users_organizations::table .filter(users_organizations::user_uuid.eq(user_uuid)) - .filter(users_organizations::status.eq(UserOrgStatus::Confirmed as i32)) - .load::(conn) + .filter(users_organizations::status.eq(MembershipStatus::Confirmed as i32)) + .load::(conn) .unwrap_or_default().from_db() }} } @@ -724,8 +724,8 @@ impl UserOrganization { db_run! { conn: { users_organizations::table .filter(users_organizations::user_uuid.eq(user_uuid)) - .filter(users_organizations::status.eq(UserOrgStatus::Invited as i32)) - .load::(conn) + .filter(users_organizations::status.eq(MembershipStatus::Invited as i32)) + .load::(conn) .unwrap_or_default().from_db() }} } @@ -734,7 +734,7 @@ impl UserOrganization { db_run! { conn: { users_organizations::table .filter(users_organizations::user_uuid.eq(user_uuid)) - .load::(conn) + .load::(conn) .unwrap_or_default().from_db() }} } @@ -743,7 +743,7 @@ impl UserOrganization { db_run! { conn: { users_organizations::table .filter(users_organizations::user_uuid.eq(user_uuid)) - .filter(users_organizations::status.eq(UserOrgStatus::Accepted as i32).or(users_organizations::status.eq(UserOrgStatus::Confirmed as i32))) + .filter(users_organizations::status.eq(MembershipStatus::Accepted as i32).or(users_organizations::status.eq(MembershipStatus::Confirmed as i32))) .count() .first::(conn) .unwrap_or(0) @@ -754,7 +754,7 @@ impl UserOrganization { db_run! { conn: { users_organizations::table .filter(users_organizations::org_uuid.eq(org_uuid)) - .load::(conn) + .load::(conn) .expect("Error loading user organizations").from_db() }} } @@ -763,8 +763,8 @@ impl UserOrganization { db_run! { conn: { users_organizations::table .filter(users_organizations::org_uuid.eq(org_uuid)) - .filter(users_organizations::status.eq(UserOrgStatus::Confirmed as i32)) - .load::(conn) + .filter(users_organizations::status.eq(MembershipStatus::Confirmed as i32)) + .load::(conn) .unwrap_or_default().from_db() }} } @@ -780,22 +780,22 @@ impl UserOrganization { }} } - pub async fn find_by_org_and_type(org_uuid: &str, atype: UserOrgType, conn: &mut DbConn) -> Vec { + pub async fn find_by_org_and_type(org_uuid: &str, atype: MembershipType, conn: &mut DbConn) -> Vec { db_run! { conn: { users_organizations::table .filter(users_organizations::org_uuid.eq(org_uuid)) .filter(users_organizations::atype.eq(atype as i32)) - .load::(conn) + .load::(conn) .expect("Error loading user organizations").from_db() }} } - pub async fn count_confirmed_by_org_and_type(org_uuid: &str, atype: UserOrgType, conn: &mut DbConn) -> i64 { + pub async fn count_confirmed_by_org_and_type(org_uuid: &str, atype: MembershipType, conn: &mut DbConn) -> i64 { db_run! { conn: { users_organizations::table .filter(users_organizations::org_uuid.eq(org_uuid)) .filter(users_organizations::atype.eq(atype as i32)) - .filter(users_organizations::status.eq(UserOrgStatus::Confirmed as i32)) + .filter(users_organizations::status.eq(MembershipStatus::Confirmed as i32)) .count() .first::(conn) .unwrap_or(0) @@ -807,7 +807,7 @@ impl UserOrganization { users_organizations::table .filter(users_organizations::user_uuid.eq(user_uuid)) .filter(users_organizations::org_uuid.eq(org_uuid)) - .first::(conn) + .first::(conn) .ok().from_db() }} } @@ -818,9 +818,9 @@ impl UserOrganization { .filter(users_organizations::user_uuid.eq(user_uuid)) .filter(users_organizations::org_uuid.eq(org_uuid)) .filter( - users_organizations::status.eq(UserOrgStatus::Confirmed as i32) + users_organizations::status.eq(MembershipStatus::Confirmed as i32) ) - .first::(conn) + .first::(conn) .ok().from_db() }} } @@ -829,12 +829,12 @@ impl UserOrganization { db_run! { conn: { users_organizations::table .filter(users_organizations::user_uuid.eq(user_uuid)) - .load::(conn) + .load::(conn) .expect("Error loading user organizations").from_db() }} } - pub async fn get_org_uuid_by_user(user_uuid: &str, conn: &mut DbConn) -> Vec { + pub async fn get_orgs_by_user(user_uuid: &str, conn: &mut DbConn) -> Vec { db_run! { conn: { users_organizations::table .filter(users_organizations::user_uuid.eq(user_uuid)) @@ -855,10 +855,10 @@ impl UserOrganization { .and(org_policies::enabled.eq(true))) ) .filter( - users_organizations::status.eq(UserOrgStatus::Confirmed as i32) + users_organizations::status.eq(MembershipStatus::Confirmed as i32) ) .select(users_organizations::all_columns) - .load::(conn) + .load::(conn) .unwrap_or_default().from_db() }} } @@ -882,7 +882,7 @@ impl UserOrganization { ) .select(users_organizations::all_columns) .distinct() - .load::(conn).expect("Error loading user organizations").from_db() + .load::(conn).expect("Error loading user organizations").from_db() }} } @@ -908,7 +908,7 @@ impl UserOrganization { ) .select(users_organizations::all_columns) .distinct() - .load::(conn).expect("Error loading user organizations with groups").from_db() + .load::(conn).expect("Error loading user organizations with groups").from_db() }} } @@ -917,7 +917,7 @@ impl UserOrganization { users_organizations::table .inner_join(ciphers::table.on(ciphers::uuid.eq(cipher_uuid).and(ciphers::organization_uuid.eq(users_organizations::org_uuid.nullable())))) .filter(users_organizations::user_uuid.eq(user_uuid)) - .filter(users_organizations::atype.eq_any(vec![UserOrgType::Owner as i32, UserOrgType::Admin as i32])) + .filter(users_organizations::atype.eq_any(vec![MembershipType::Owner as i32, MembershipType::Admin as i32])) .count() .first::(conn) .ok().unwrap_or(0) != 0 @@ -937,7 +937,7 @@ impl UserOrganization { ) ) .select(users_organizations::all_columns) - .load::(conn).expect("Error loading user organizations").from_db() + .load::(conn).expect("Error loading user organizations").from_db() }} } @@ -948,7 +948,7 @@ impl UserOrganization { users_organizations::external_id.eq(ext_id) .and(users_organizations::org_uuid.eq(org_uuid)) ) - .first::(conn).ok().from_db() + .first::(conn).ok().from_db() }} } } @@ -1011,9 +1011,9 @@ mod tests { #[test] #[allow(non_snake_case)] - fn partial_cmp_UserOrgType() { - assert!(UserOrgType::Owner > UserOrgType::Admin); - assert!(UserOrgType::Admin > UserOrgType::Manager); - assert!(UserOrgType::Manager > UserOrgType::User); + fn partial_cmp_MembershipType() { + assert!(MembershipType::Owner > MembershipType::Admin); + assert!(MembershipType::Admin > MembershipType::Manager); + assert!(MembershipType::Manager > MembershipType::User); } } diff --git a/src/db/models/user.rs b/src/db/models/user.rs index 94f42c8420..981a46056f 100644 --- a/src/db/models/user.rs +++ b/src/db/models/user.rs @@ -215,8 +215,7 @@ impl User { } use super::{ - Cipher, Device, EmergencyAccess, Favorite, Folder, Send, TwoFactor, TwoFactorIncomplete, UserOrgType, - UserOrganization, + Cipher, Device, EmergencyAccess, Favorite, Folder, Membership, MembershipType, Send, TwoFactor, TwoFactorIncomplete, }; use crate::db::DbConn; @@ -227,7 +226,7 @@ use crate::error::MapResult; impl User { pub async fn to_json(&self, conn: &mut DbConn) -> Value { let mut orgs_json = Vec::new(); - for c in UserOrganization::find_confirmed_by_user(&self.uuid, conn).await { + for c in Membership::find_confirmed_by_user(&self.uuid, conn).await { orgs_json.push(c.to_json(conn).await); } @@ -304,10 +303,9 @@ impl User { } pub async fn delete(self, conn: &mut DbConn) -> EmptyResult { - for user_org in UserOrganization::find_confirmed_by_user(&self.uuid, conn).await { - if user_org.atype == UserOrgType::Owner - && UserOrganization::count_confirmed_by_org_and_type(&user_org.org_uuid, UserOrgType::Owner, conn).await - <= 1 + for member in Membership::find_confirmed_by_user(&self.uuid, conn).await { + if member.atype == MembershipType::Owner + && Membership::count_confirmed_by_org_and_type(&member.org_uuid, MembershipType::Owner, conn).await <= 1 { err!("Can't delete last owner") } @@ -316,7 +314,7 @@ impl User { Send::delete_all_by_user(&self.uuid, conn).await?; EmergencyAccess::delete_all_by_user(&self.uuid, conn).await?; EmergencyAccess::delete_all_by_grantee_email(&self.email, conn).await?; - UserOrganization::delete_all_by_user(&self.uuid, conn).await?; + Membership::delete_all_by_user(&self.uuid, conn).await?; Cipher::delete_all_by_user(&self.uuid, conn).await?; Favorite::delete_all_by_user(&self.uuid, conn).await?; Folder::delete_all_by_user(&self.uuid, conn).await?; diff --git a/src/mail.rs b/src/mail.rs index 1a32ae259c..a8e250c8a4 100644 --- a/src/mail.rs +++ b/src/mail.rs @@ -260,7 +260,7 @@ pub async fn send_single_org_removed_from_org(address: &str, org_name: &str) -> pub async fn send_invite( user: &User, org_id: Option, - org_user_id: Option, + member_id: Option, org_name: &str, invited_by_email: Option, ) -> EmptyResult { @@ -268,7 +268,7 @@ pub async fn send_invite( user.uuid.clone(), user.email.clone(), org_id.clone(), - org_user_id.clone(), + member_id.clone(), invited_by_email, ); let invite_token = encode_jwt(&claims); @@ -279,7 +279,7 @@ pub async fn send_invite( .append_pair("email", &user.email) .append_pair("organizationName", org_name) .append_pair("organizationId", org_id.as_deref().unwrap_or("_")) - .append_pair("organizationUserId", org_user_id.as_deref().unwrap_or("_")) + .append_pair("organizationUserId", member_id.as_deref().unwrap_or("_")) .append_pair("token", &invite_token); if user.private_key.is_some() { query_params.append_pair("orgUserHasExistingUser", "true"); From 40a788084bbe94aa776b0db2b1da699395d573c9 Mon Sep 17 00:00:00 2001 From: Stefan Melmuk Date: Sat, 21 Dec 2024 07:28:00 +0100 Subject: [PATCH 02/25] use newtype pattern for org_id --- Cargo.lock | 12 + Cargo.toml | 1 + src/api/admin.rs | 8 +- src/api/core/accounts.rs | 7 +- src/api/core/ciphers.rs | 16 +- src/api/core/events.rs | 10 +- src/api/core/organizations.rs | 519 +++++++++++++++++++-------------- src/api/core/public.rs | 5 +- src/api/core/two_factor/mod.rs | 2 +- src/api/identity.rs | 3 +- src/api/notifications.rs | 2 +- src/auth.rs | 19 +- src/db/models/attachment.rs | 11 +- src/db/models/auth_request.rs | 3 +- src/db/models/cipher.rs | 5 +- src/db/models/collection.rs | 36 ++- src/db/models/event.rs | 3 +- src/db/models/group.rs | 37 ++- src/db/models/mod.rs | 4 +- src/db/models/org_policy.rs | 24 +- src/db/models/organization.rs | 135 +++++++-- src/db/models/send.rs | 7 +- src/mail.rs | 4 +- src/main.rs | 2 + 24 files changed, 553 insertions(+), 322 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 311fa43b08..c15428a9f3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -778,6 +778,17 @@ dependencies = [ "url", ] +[[package]] +name = "diesel-derive-newtype" +version = "2.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d5adf688c584fe33726ce0e2898f608a2a92578ac94a4a92fcecf73214fe0716" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "diesel_derives" version = "2.2.3" @@ -3960,6 +3971,7 @@ dependencies = [ "data-encoding", "data-url", "diesel", + "diesel-derive-newtype", "diesel_logger", "diesel_migrations", "dotenvy", diff --git a/Cargo.toml b/Cargo.toml index f739145b48..12ab7229e7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -77,6 +77,7 @@ serde_json = "1.0.133" diesel = { version = "2.2.6", features = ["chrono", "r2d2", "numeric"] } diesel_migrations = "2.2.0" diesel_logger = { version = "0.4.0", optional = true } +diesel-derive-newtype = "2.1.2" # Bundled/Static SQLite libsqlite3-sys = { version = "0.30.1", features = ["bundled"], optional = true } diff --git a/src/api/admin.rs b/src/api/admin.rs index ddb21f796c..4cb5ffc089 100644 --- a/src/api/admin.rs +++ b/src/api/admin.rs @@ -488,7 +488,7 @@ async fn resend_user_invite(uuid: &str, _token: AdminToken, mut conn: DbConn) -> struct MembershipTypeData { user_type: NumberOrString, user_uuid: String, - org_uuid: String, + org_uuid: OrganizationId, } #[post("/users/org_type", data = "")] @@ -570,9 +570,9 @@ async fn organizations_overview(_token: AdminToken, mut conn: DbConn) -> ApiResu Ok(Html(text)) } -#[post("/organizations//delete")] -async fn delete_organization(uuid: &str, _token: AdminToken, mut conn: DbConn) -> EmptyResult { - let org = Organization::find_by_uuid(uuid, &mut conn).await.map_res("Organization doesn't exist")?; +#[post("/organizations//delete")] +async fn delete_organization(org_uuid: OrganizationId, _token: AdminToken, mut conn: DbConn) -> EmptyResult { + let org = Organization::find_by_uuid(&org_uuid, &mut conn).await.map_res("Organization doesn't exist")?; org.delete(&mut conn).await } diff --git a/src/api/core/accounts.rs b/src/api/core/accounts.rs index 92d72d5749..39654d795f 100644 --- a/src/api/core/accounts.rs +++ b/src/api/core/accounts.rs @@ -459,7 +459,7 @@ struct UpdateEmergencyAccessData { #[derive(Deserialize)] #[serde(rename_all = "camelCase")] struct UpdateResetPasswordData { - organization_id: String, + organization_id: OrganizationId, reset_password_key: String, } @@ -516,9 +516,10 @@ fn validate_keydata( } // Check that we're correctly rotating all the user's reset password keys - let existing_reset_password_ids = existing_memberships.iter().map(|m| m.org_uuid.as_str()).collect::>(); + let existing_reset_password_ids = + existing_memberships.iter().map(|m| &m.org_uuid).collect::>(); let provided_reset_password_ids = - data.reset_password_keys.iter().map(|rp| rp.organization_id.as_str()).collect::>(); + data.reset_password_keys.iter().map(|rp| &rp.organization_id).collect::>(); if !provided_reset_password_ids.is_superset(&existing_reset_password_ids) { err!("All existing reset password keys must be included in the rotation") } diff --git a/src/api/core/ciphers.rs b/src/api/core/ciphers.rs index 8e1c4f0e13..8558fef72f 100644 --- a/src/api/core/ciphers.rs +++ b/src/api/core/ciphers.rs @@ -224,7 +224,7 @@ pub struct CipherData { pub folder_id: Option, // TODO: Some of these might appear all the time, no need for Option #[serde(alias = "organizationID")] - pub organization_id: Option, + pub organization_id: Option, key: Option, @@ -1572,14 +1572,14 @@ async fn move_cipher_selected_put( } #[derive(FromForm)] -struct OrganizationId { +struct OrganizationIdData { #[field(name = "organizationId")] - org_id: String, + org_id: OrganizationId, } #[post("/ciphers/purge?", data = "")] async fn delete_all( - organization: Option, + organization: Option, data: Json, headers: Headers, mut conn: DbConn, @@ -1835,10 +1835,10 @@ pub struct CipherSyncData { pub cipher_folders: HashMap, pub cipher_favorites: HashSet, pub cipher_collections: HashMap>, - pub members: HashMap, + pub members: HashMap, pub user_collections: HashMap, pub user_collections_groups: HashMap, - pub user_group_full_access_for_organizations: HashSet, + pub user_group_full_access_for_organizations: HashSet, } #[derive(Eq, PartialEq)] @@ -1885,7 +1885,7 @@ impl CipherSyncData { } // Generate a HashMap with the Organization UUID as key and the Membership record - let members: HashMap = + let members: HashMap = Membership::find_by_user(user_uuid, conn).await.into_iter().map(|m| (m.org_uuid.clone(), m)).collect(); // Generate a HashMap with the User_Collections UUID as key and the CollectionUser record @@ -1907,7 +1907,7 @@ impl CipherSyncData { }; // Get all organizations that the given user has full access to via group assignment - let user_group_full_access_for_organizations: HashSet = if CONFIG.org_groups_enabled() { + let user_group_full_access_for_organizations: HashSet = if CONFIG.org_groups_enabled() { Group::get_orgs_by_user_with_full_access(user_uuid, conn).await.into_iter().collect() } else { HashSet::new() diff --git a/src/api/core/events.rs b/src/api/core/events.rs index 49ff083f41..a0172ebbfb 100644 --- a/src/api/core/events.rs +++ b/src/api/core/events.rs @@ -8,7 +8,7 @@ use crate::{ api::{EmptyResult, JsonResult}, auth::{AdminHeaders, Headers}, db::{ - models::{Cipher, Event, Membership}, + models::{Cipher, Event, Membership, OrganizationId}, DbConn, DbPool, }, util::parse_date, @@ -153,7 +153,7 @@ struct EventCollection { // Optional cipher_id: Option, - organization_id: Option, + organization_id: Option, } // Upstream: @@ -261,7 +261,7 @@ async fn _log_user_event( pub async fn log_event( event_type: i32, source_uuid: &str, - org_uuid: &str, + org_uuid: &OrganizationId, act_user_uuid: &str, device_type: i32, ip: &IpAddr, @@ -277,7 +277,7 @@ pub async fn log_event( async fn _log_event( event_type: i32, source_uuid: &str, - org_uuid: &str, + org_uuid: &OrganizationId, act_user_uuid: &str, device_type: i32, event_date: Option, @@ -313,7 +313,7 @@ async fn _log_event( _ => {} } - event.org_uuid = Some(String::from(org_uuid)); + event.org_uuid = Some(org_uuid.clone()); event.act_user_uuid = Some(String::from(act_user_uuid)); event.device_type = Some(device_type); event.ip_address = Some(ip.to_string()); diff --git a/src/api/core/organizations.rs b/src/api/core/organizations.rs index b5ffe3eda4..bcfd29a63e 100644 --- a/src/api/core/organizations.rs +++ b/src/api/core/organizations.rs @@ -188,7 +188,7 @@ async fn create_organization(headers: Headers, data: Json, mut conn: Db #[delete("/organizations/", data = "")] async fn delete_organization( - org_id: &str, + org_id: OrganizationId, data: Json, headers: OwnerHeaders, mut conn: DbConn, @@ -197,7 +197,7 @@ async fn delete_organization( data.validate(&headers.user, true, &mut conn).await?; - match Organization::find_by_uuid(org_id, &mut conn).await { + match Organization::find_by_uuid(&org_id, &mut conn).await { None => err!("Organization not found"), Some(org) => org.delete(&mut conn).await, } @@ -205,7 +205,7 @@ async fn delete_organization( #[post("/organizations//delete", data = "")] async fn post_delete_organization( - org_id: &str, + org_id: OrganizationId, data: Json, headers: OwnerHeaders, conn: DbConn, @@ -214,12 +214,12 @@ async fn post_delete_organization( } #[post("/organizations//leave")] -async fn leave_organization(org_id: &str, headers: Headers, mut conn: DbConn) -> EmptyResult { - match Membership::find_by_user_and_org(&headers.user.uuid, org_id, &mut conn).await { +async fn leave_organization(org_id: OrganizationId, headers: Headers, mut conn: DbConn) -> EmptyResult { + match Membership::find_by_user_and_org(&headers.user.uuid, &org_id, &mut conn).await { None => err!("User not part of organization"), Some(member) => { if member.atype == MembershipType::Owner - && Membership::count_confirmed_by_org_and_type(org_id, MembershipType::Owner, &mut conn).await <= 1 + && Membership::count_confirmed_by_org_and_type(&org_id, MembershipType::Owner, &mut conn).await <= 1 { err!("The last owner can't leave") } @@ -227,7 +227,7 @@ async fn leave_organization(org_id: &str, headers: Headers, mut conn: DbConn) -> log_event( EventType::OrganizationUserRemoved as i32, &member.uuid, - org_id, + &org_id, &headers.user.uuid, headers.device.atype, &headers.ip.ip, @@ -241,8 +241,8 @@ async fn leave_organization(org_id: &str, headers: Headers, mut conn: DbConn) -> } #[get("/organizations/")] -async fn get_organization(org_id: &str, _headers: OwnerHeaders, mut conn: DbConn) -> JsonResult { - match Organization::find_by_uuid(org_id, &mut conn).await { +async fn get_organization(org_id: OrganizationId, _headers: OwnerHeaders, mut conn: DbConn) -> JsonResult { + match Organization::find_by_uuid(&org_id, &mut conn).await { Some(organization) => Ok(Json(organization.to_json())), None => err!("Can't find organization details"), } @@ -250,7 +250,7 @@ async fn get_organization(org_id: &str, _headers: OwnerHeaders, mut conn: DbConn #[put("/organizations/", data = "")] async fn put_organization( - org_id: &str, + org_id: OrganizationId, headers: OwnerHeaders, data: Json, conn: DbConn, @@ -260,14 +260,14 @@ async fn put_organization( #[post("/organizations/", data = "")] async fn post_organization( - org_id: &str, + org_id: OrganizationId, headers: OwnerHeaders, data: Json, mut conn: DbConn, ) -> JsonResult { let data: OrganizationUpdateData = data.into_inner(); - let Some(mut org) = Organization::find_by_uuid(org_id, &mut conn).await else { + let Some(mut org) = Organization::find_by_uuid(&org_id, &mut conn).await else { err!("Can't find organization details") }; @@ -278,8 +278,8 @@ async fn post_organization( log_event( EventType::OrganizationUpdated as i32, - org_id, - org_id, + org_id.as_ref(), + &org_id, &headers.user.uuid, headers.device.atype, &headers.ip.ip, @@ -305,30 +305,35 @@ async fn get_user_collections(headers: Headers, mut conn: DbConn) -> Json } #[get("/organizations//collections")] -async fn get_org_collections(org_id: &str, _headers: ManagerHeadersLoose, mut conn: DbConn) -> Json { +async fn get_org_collections(org_id: OrganizationId, _headers: ManagerHeadersLoose, mut conn: DbConn) -> Json { Json(json!({ - "data": _get_org_collections(org_id, &mut conn).await, + "data": _get_org_collections(&org_id, &mut conn).await, "object": "list", "continuationToken": null, })) } #[get("/organizations//collections/details")] -async fn get_org_collections_details(org_id: &str, headers: ManagerHeadersLoose, mut conn: DbConn) -> JsonResult { +async fn get_org_collections_details( + org_id: OrganizationId, + headers: ManagerHeadersLoose, + mut conn: DbConn, +) -> JsonResult { let mut data = Vec::new(); - let Some(member) = Membership::find_by_user_and_org(&headers.user.uuid, org_id, &mut conn).await else { + let Some(member) = Membership::find_by_user_and_org(&headers.user.uuid, &org_id, &mut conn).await else { err!("User is not part of organization") }; // get all collection memberships for the current organization - let coll_users = CollectionUser::find_by_organization(org_id, &mut conn).await; + let coll_users = CollectionUser::find_by_organization(&org_id, &mut conn).await; // check if current user has full access to the organization (either directly or via any group) let has_full_access_to_org = member.access_all - || (CONFIG.org_groups_enabled() && GroupUser::has_full_access_by_member(org_id, &member.uuid, &mut conn).await); + || (CONFIG.org_groups_enabled() + && GroupUser::has_full_access_by_member(&org_id, &member.uuid, &mut conn).await); - for col in Collection::find_by_organization(org_id, &mut conn).await { + for col in Collection::find_by_organization(&org_id, &mut conn).await { // check whether the current user has access to the given collection let assigned = has_full_access_to_org || CollectionUser::has_access_to_collection_by_user(&col.uuid, &member.user_uuid, &mut conn).await @@ -371,20 +376,20 @@ async fn get_org_collections_details(org_id: &str, headers: ManagerHeadersLoose, }))) } -async fn _get_org_collections(org_id: &str, conn: &mut DbConn) -> Value { +async fn _get_org_collections(org_id: &OrganizationId, conn: &mut DbConn) -> Value { Collection::find_by_organization(org_id, conn).await.iter().map(Collection::to_json).collect::() } #[post("/organizations//collections", data = "")] async fn post_organization_collections( - org_id: &str, + org_id: OrganizationId, headers: ManagerHeadersLoose, data: Json, mut conn: DbConn, ) -> JsonResult { let data: NewCollectionData = data.into_inner(); - let Some(org) = Organization::find_by_uuid(org_id, &mut conn).await else { + let Some(org) = Organization::find_by_uuid(&org_id, &mut conn).await else { err!("Can't find organization details") }; @@ -394,7 +399,7 @@ async fn post_organization_collections( log_event( EventType::CollectionCreated as i32, &collection.uuid, - org_id, + &org_id, &headers.user.uuid, headers.device.atype, &headers.ip.ip, @@ -409,7 +414,7 @@ async fn post_organization_collections( } for user in data.users { - let Some(member) = Membership::find_by_uuid_and_org(&user.id, org_id, &mut conn).await else { + let Some(member) = Membership::find_by_uuid_and_org(&user.id, &org_id, &mut conn).await else { err!("User is not part of organization") }; @@ -430,7 +435,7 @@ async fn post_organization_collections( #[put("/organizations//collections/", data = "")] async fn put_organization_collection_update( - org_id: &str, + org_id: OrganizationId, col_id: &str, headers: ManagerHeaders, data: Json, @@ -441,7 +446,7 @@ async fn put_organization_collection_update( #[post("/organizations//collections/", data = "")] async fn post_organization_collection_update( - org_id: &str, + org_id: OrganizationId, col_id: &str, headers: ManagerHeaders, data: Json, @@ -449,11 +454,11 @@ async fn post_organization_collection_update( ) -> JsonResult { let data: NewCollectionData = data.into_inner(); - if Organization::find_by_uuid(org_id, &mut conn).await.is_none() { + if Organization::find_by_uuid(&org_id, &mut conn).await.is_none() { err!("Can't find organization details") }; - let Some(mut collection) = Collection::find_by_uuid_and_org(col_id, org_id, &mut conn).await else { + let Some(mut collection) = Collection::find_by_uuid_and_org(col_id, &org_id, &mut conn).await else { err!("Collection not found") }; @@ -468,7 +473,7 @@ async fn post_organization_collection_update( log_event( EventType::CollectionUpdated as i32, &collection.uuid, - org_id, + &org_id, &headers.user.uuid, headers.device.atype, &headers.ip.ip, @@ -487,7 +492,7 @@ async fn post_organization_collection_update( CollectionUser::delete_all_by_collection(col_id, &mut conn).await?; for user in data.users { - let Some(member) = Membership::find_by_uuid_and_org(&user.id, org_id, &mut conn).await else { + let Some(member) = Membership::find_by_uuid_and_org(&user.id, &org_id, &mut conn).await else { err!("User is not part of organization") }; @@ -503,17 +508,17 @@ async fn post_organization_collection_update( #[delete("/organizations//collections//user/")] async fn delete_organization_collection_user( - org_id: &str, + org_id: OrganizationId, col_id: &str, member_id: &str, _headers: AdminHeaders, mut conn: DbConn, ) -> EmptyResult { - let Some(collection) = Collection::find_by_uuid_and_org(col_id, org_id, &mut conn).await else { + let Some(collection) = Collection::find_by_uuid_and_org(col_id, &org_id, &mut conn).await else { err!("Collection not found", "Collection does not exist or does not belong to this organization") }; - match Membership::find_by_uuid_and_org(member_id, org_id, &mut conn).await { + match Membership::find_by_uuid_and_org(member_id, &org_id, &mut conn).await { None => err!("User not found in organization"), Some(member) => { match CollectionUser::find_by_collection_and_user(&collection.uuid, &member.user_uuid, &mut conn).await { @@ -526,7 +531,7 @@ async fn delete_organization_collection_user( #[post("/organizations//collections//delete-user/")] async fn post_organization_collection_delete_user( - org_id: &str, + org_id: OrganizationId, col_id: &str, member_id: &str, headers: AdminHeaders, @@ -536,7 +541,7 @@ async fn post_organization_collection_delete_user( } async fn _delete_organization_collection( - org_id: &str, + org_id: &OrganizationId, col_id: &str, headers: &ManagerHeaders, conn: &mut DbConn, @@ -559,12 +564,12 @@ async fn _delete_organization_collection( #[delete("/organizations//collections/")] async fn delete_organization_collection( - org_id: &str, + org_id: OrganizationId, col_id: &str, headers: ManagerHeaders, mut conn: DbConn, ) -> EmptyResult { - _delete_organization_collection(org_id, col_id, &headers, &mut conn).await + _delete_organization_collection(&org_id, col_id, &headers, &mut conn).await } #[derive(Deserialize, Debug)] @@ -573,17 +578,17 @@ struct DeleteCollectionData { #[allow(dead_code)] id: String, #[allow(dead_code)] - org_id: String, + org_id: OrganizationId, } #[post("/organizations//collections//delete")] async fn post_organization_collection_delete( - org_id: &str, + org_id: OrganizationId, col_id: &str, headers: ManagerHeaders, mut conn: DbConn, ) -> EmptyResult { - _delete_organization_collection(org_id, col_id, &headers, &mut conn).await + _delete_organization_collection(&org_id, col_id, &headers, &mut conn).await } #[derive(Deserialize, Debug)] @@ -594,7 +599,7 @@ struct BulkCollectionIds { #[delete("/organizations//collections", data = "")] async fn bulk_delete_organization_collections( - org_id: &str, + org_id: OrganizationId, headers: ManagerHeadersLoose, data: Json, mut conn: DbConn, @@ -606,14 +611,14 @@ async fn bulk_delete_organization_collections( let headers = ManagerHeaders::from_loose(headers, &collections, &mut conn).await?; for col_id in collections { - _delete_organization_collection(org_id, &col_id, &headers, &mut conn).await? + _delete_organization_collection(&org_id, &col_id, &headers, &mut conn).await? } Ok(()) } #[get("/organizations//collections//details")] async fn get_org_collection_detail( - org_id: &str, + org_id: OrganizationId, coll_id: &str, headers: ManagerHeaders, mut conn: DbConn, @@ -625,7 +630,7 @@ async fn get_org_collection_detail( err!("Collection is not owned by organization") } - let Some(member) = Membership::find_by_user_and_org(&headers.user.uuid, org_id, &mut conn).await else { + let Some(member) = Membership::find_by_user_and_org(&headers.user.uuid, &org_id, &mut conn).await else { err!("User is not part of organization") }; @@ -666,16 +671,21 @@ async fn get_org_collection_detail( } #[get("/organizations//collections//users")] -async fn get_collection_users(org_id: &str, coll_id: &str, _headers: ManagerHeaders, mut conn: DbConn) -> JsonResult { +async fn get_collection_users( + org_id: OrganizationId, + coll_id: &str, + _headers: ManagerHeaders, + mut conn: DbConn, +) -> JsonResult { // Get org and collection, check that collection is from org - let Some(collection) = Collection::find_by_uuid_and_org(coll_id, org_id, &mut conn).await else { + let Some(collection) = Collection::find_by_uuid_and_org(coll_id, &org_id, &mut conn).await else { err!("Collection not found in Organization") }; let mut user_list = Vec::new(); for col_user in CollectionUser::find_by_collection(&collection.uuid, &mut conn).await { user_list.push( - Membership::find_by_user_and_org(&col_user.user_uuid, org_id, &mut conn) + Membership::find_by_user_and_org(&col_user.user_uuid, &org_id, &mut conn) .await .unwrap() .to_json_user_access_restrictions(&col_user), @@ -687,14 +697,14 @@ async fn get_collection_users(org_id: &str, coll_id: &str, _headers: ManagerHead #[put("/organizations//collections//users", data = "")] async fn put_collection_users( - org_id: &str, + org_id: OrganizationId, coll_id: &str, data: Json>, _headers: ManagerHeaders, mut conn: DbConn, ) -> EmptyResult { // Get org and collection, check that collection is from org - if Collection::find_by_uuid_and_org(coll_id, org_id, &mut conn).await.is_none() { + if Collection::find_by_uuid_and_org(coll_id, &org_id, &mut conn).await.is_none() { err!("Collection not found in Organization") } @@ -703,7 +713,7 @@ async fn put_collection_users( // And then add all the received ones (except if the user has access_all) for d in data.iter() { - let Some(user) = Membership::find_by_uuid_and_org(&d.id, org_id, &mut conn).await else { + let Some(user) = Membership::find_by_uuid_and_org(&d.id, &org_id, &mut conn).await else { err!("User is not part of organization") }; @@ -720,7 +730,7 @@ async fn put_collection_users( #[derive(FromForm)] struct OrgIdData { #[field(name = "organizationId")] - organization_id: String, + organization_id: OrganizationId, } #[get("/ciphers/organization-details?")] @@ -737,7 +747,7 @@ async fn get_org_details(data: OrgIdData, headers: Headers, mut conn: DbConn) -> }))) } -async fn _get_org_details(org_id: &str, host: &str, user_uuid: &str, conn: &mut DbConn) -> Value { +async fn _get_org_details(org_id: &OrganizationId, host: &str, user_uuid: &str, conn: &mut DbConn) -> Value { let ciphers = Cipher::find_by_org(org_id, conn).await; let cipher_sync_data = CipherSyncData::new(user_uuid, CipherSyncType::Organization, conn).await; @@ -760,12 +770,12 @@ struct GetOrgUserData { #[get("/organizations//users?")] async fn get_members( data: GetOrgUserData, - org_id: &str, + org_id: OrganizationId, _headers: ManagerHeadersLoose, mut conn: DbConn, ) -> Json { let mut users_json = Vec::new(); - for u in Membership::find_by_org(org_id, &mut conn).await { + for u in Membership::find_by_org(&org_id, &mut conn).await { users_json.push( u.to_json_user_details( data.include_collections.unwrap_or(false), @@ -784,10 +794,15 @@ async fn get_members( } #[post("/organizations//keys", data = "")] -async fn post_org_keys(org_id: &str, data: Json, _headers: AdminHeaders, mut conn: DbConn) -> JsonResult { +async fn post_org_keys( + org_id: OrganizationId, + data: Json, + _headers: AdminHeaders, + mut conn: DbConn, +) -> JsonResult { let data: OrgKeyData = data.into_inner(); - let mut org = match Organization::find_by_uuid(org_id, &mut conn).await { + let mut org = match Organization::find_by_uuid(&org_id, &mut conn).await { Some(organization) => { if organization.private_key.is_some() && organization.public_key.is_some() { err!("Organization Keys already exist") @@ -829,7 +844,12 @@ struct InviteData { } #[post("/organizations//users/invite", data = "")] -async fn send_invite(org_id: &str, data: Json, headers: AdminHeaders, mut conn: DbConn) -> EmptyResult { +async fn send_invite( + org_id: OrganizationId, + data: Json, + headers: AdminHeaders, + mut conn: DbConn, +) -> EmptyResult { let data: InviteData = data.into_inner(); let new_type = match MembershipType::from_str(&data.r#type.into_string()) { @@ -863,7 +883,7 @@ async fn send_invite(org_id: &str, data: Json, headers: AdminHeaders user } Some(user) => { - if Membership::find_by_user_and_org(&user.uuid, org_id, &mut conn).await.is_some() { + if Membership::find_by_user_and_org(&user.uuid, &org_id, &mut conn).await.is_some() { err!(format!("User already in organization: {email}")) } else { // automatically accept existing users if mail is disabled @@ -875,7 +895,7 @@ async fn send_invite(org_id: &str, data: Json, headers: AdminHeaders } }; - let mut new_user = Membership::new(user.uuid.clone(), String::from(org_id)); + let mut new_user = Membership::new(user.uuid.clone(), org_id.clone()); let access_all = data.access_all; new_user.access_all = access_all; new_user.atype = new_type; @@ -884,7 +904,7 @@ async fn send_invite(org_id: &str, data: Json, headers: AdminHeaders // If no accessAll, add the collections received if !access_all { for col in data.collections.iter().flatten() { - match Collection::find_by_uuid_and_org(&col.id, org_id, &mut conn).await { + match Collection::find_by_uuid_and_org(&col.id, &org_id, &mut conn).await { None => err!("Collection not found in Organization"), Some(collection) => { CollectionUser::save( @@ -910,7 +930,7 @@ async fn send_invite(org_id: &str, data: Json, headers: AdminHeaders log_event( EventType::OrganizationUserInvited as i32, &new_user.uuid, - org_id, + &org_id, &headers.user.uuid, headers.device.atype, &headers.ip.ip, @@ -919,14 +939,14 @@ async fn send_invite(org_id: &str, data: Json, headers: AdminHeaders .await; if CONFIG.mail_enabled() { - let org_name = match Organization::find_by_uuid(org_id, &mut conn).await { + let org_name = match Organization::find_by_uuid(&org_id, &mut conn).await { Some(org) => org.name, None => err!("Error looking up organization"), }; mail::send_invite( &user, - Some(String::from(org_id)), + Some(org_id.clone()), Some(new_user.uuid), &org_name, Some(headers.user.email.clone()), @@ -940,7 +960,7 @@ async fn send_invite(org_id: &str, data: Json, headers: AdminHeaders #[post("/organizations//users/reinvite", data = "")] async fn bulk_reinvite_user( - org_id: &str, + org_id: OrganizationId, data: Json, headers: AdminHeaders, mut conn: DbConn, @@ -949,7 +969,7 @@ async fn bulk_reinvite_user( let mut bulk_response = Vec::new(); for member_id in data.ids { - let err_msg = match _reinvite_user(org_id, &member_id, &headers.user.email, &mut conn).await { + let err_msg = match _reinvite_user(&org_id, &member_id, &headers.user.email, &mut conn).await { Ok(_) => String::new(), Err(e) => format!("{e:?}"), }; @@ -971,11 +991,16 @@ async fn bulk_reinvite_user( } #[post("/organizations//users//reinvite")] -async fn reinvite_user(org_id: &str, member: &str, headers: AdminHeaders, mut conn: DbConn) -> EmptyResult { - _reinvite_user(org_id, member, &headers.user.email, &mut conn).await +async fn reinvite_user(org_id: OrganizationId, member: &str, headers: AdminHeaders, mut conn: DbConn) -> EmptyResult { + _reinvite_user(&org_id, member, &headers.user.email, &mut conn).await } -async fn _reinvite_user(org_id: &str, member: &str, invited_by_email: &str, conn: &mut DbConn) -> EmptyResult { +async fn _reinvite_user( + org_id: &OrganizationId, + member: &str, + invited_by_email: &str, + conn: &mut DbConn, +) -> EmptyResult { let Some(member) = Membership::find_by_uuid_and_org(member, org_id, conn).await else { err!("The user hasn't been invited to the organization.") }; @@ -1000,7 +1025,7 @@ async fn _reinvite_user(org_id: &str, member: &str, invited_by_email: &str, conn if CONFIG.mail_enabled() { mail::send_invite( &user, - Some(org_id.to_string()), + Some(org_id.clone()), Some(member.uuid), &org_name, Some(invited_by_email.to_string()), @@ -1027,7 +1052,12 @@ struct AcceptData { } #[post("/organizations//users//accept", data = "")] -async fn accept_invite(org_id: &str, member_id: &str, data: Json, mut conn: DbConn) -> EmptyResult { +async fn accept_invite( + org_id: OrganizationId, + member_id: &str, + data: Json, + mut conn: DbConn, +) -> EmptyResult { // The web-vault passes org_id and member_id in the URL, but we are just reading them from the JWT instead let data: AcceptData = data.into_inner(); let claims = decode_invite(&data.token)?; @@ -1059,7 +1089,7 @@ async fn accept_invite(org_id: &str, member_id: &str, data: Json, mu // This check is also done at accept_invite(), _confirm_invite, _activate_user(), edit_user(), admin::update_membership_type // It returns different error messages per function. if member.atype < MembershipType::Admin { - match OrgPolicy::is_user_allowed(&member.user_uuid, org_id, false, &mut conn).await { + match OrgPolicy::is_user_allowed(&member.user_uuid, &org_id, false, &mut conn).await { Ok(_) => {} Err(OrgPolicyErr::TwoFactorMissing) => { if CONFIG.email_2fa_auto_fallback() { @@ -1121,7 +1151,7 @@ struct BulkConfirmData { #[post("/organizations//users/confirm", data = "")] async fn bulk_confirm_invite( - org_id: &str, + org_id: OrganizationId, data: Json, headers: AdminHeaders, mut conn: DbConn, @@ -1135,7 +1165,7 @@ async fn bulk_confirm_invite( for invite in keys { let member_id = invite.id.unwrap_or_default(); let user_key = invite.key.unwrap_or_default(); - let err_msg = match _confirm_invite(org_id, &member_id, &user_key, &headers, &mut conn, &nt).await { + let err_msg = match _confirm_invite(&org_id, &member_id, &user_key, &headers, &mut conn, &nt).await { Ok(_) => String::new(), Err(e) => format!("{e:?}"), }; @@ -1161,7 +1191,7 @@ async fn bulk_confirm_invite( #[post("/organizations//users//confirm", data = "")] async fn confirm_invite( - org_id: &str, + org_id: OrganizationId, member_id: &str, data: Json, headers: AdminHeaders, @@ -1170,11 +1200,11 @@ async fn confirm_invite( ) -> EmptyResult { let data = data.into_inner(); let user_key = data.key.unwrap_or_default(); - _confirm_invite(org_id, member_id, &user_key, &headers, &mut conn, &nt).await + _confirm_invite(&org_id, member_id, &user_key, &headers, &mut conn, &nt).await } async fn _confirm_invite( - org_id: &str, + org_id: &OrganizationId, member_id: &str, key: &str, headers: &AdminHeaders, @@ -1252,13 +1282,13 @@ async fn _confirm_invite( #[get("/organizations//users/?")] async fn get_user( - org_id: &str, + org_id: OrganizationId, member_id: &str, data: GetOrgUserData, _headers: AdminHeaders, mut conn: DbConn, ) -> JsonResult { - let Some(user) = Membership::find_by_uuid_and_org(member_id, org_id, &mut conn).await else { + let Some(user) = Membership::find_by_uuid_and_org(member_id, &org_id, &mut conn).await else { err!("The specified user isn't a member of the organization") }; @@ -1282,7 +1312,7 @@ struct EditUserData { #[put("/organizations//users/", data = "", rank = 1)] async fn put_membership( - org_id: &str, + org_id: OrganizationId, member_id: &str, data: Json, headers: AdminHeaders, @@ -1293,7 +1323,7 @@ async fn put_membership( #[post("/organizations//users/", data = "", rank = 1)] async fn edit_user( - org_id: &str, + org_id: OrganizationId, member_id: &str, data: Json, headers: AdminHeaders, @@ -1305,7 +1335,7 @@ async fn edit_user( err!("Invalid type") }; - let Some(mut member_to_edit) = Membership::find_by_uuid_and_org(member_id, org_id, &mut conn).await else { + let Some(mut member_to_edit) = Membership::find_by_uuid_and_org(member_id, &org_id, &mut conn).await else { err!("The specified user isn't member of the organization") }; @@ -1325,7 +1355,7 @@ async fn edit_user( && member_to_edit.status == MembershipStatus::Confirmed as i32 { // Removing owner permission, check that there is at least one other confirmed owner - if Membership::count_confirmed_by_org_and_type(org_id, MembershipType::Owner, &mut conn).await <= 1 { + if Membership::count_confirmed_by_org_and_type(&org_id, MembershipType::Owner, &mut conn).await <= 1 { err!("Can't delete the last owner") } } @@ -1333,7 +1363,7 @@ async fn edit_user( // This check is also done at accept_invite(), _confirm_invite, _activate_user(), edit_user(), admin::update_membership_type // It returns different error messages per function. if new_type < MembershipType::Admin { - match OrgPolicy::is_user_allowed(&member_to_edit.user_uuid, org_id, true, &mut conn).await { + match OrgPolicy::is_user_allowed(&member_to_edit.user_uuid, &org_id, true, &mut conn).await { Ok(_) => {} Err(OrgPolicyErr::TwoFactorMissing) => { if CONFIG.email_2fa_auto_fallback() { @@ -1352,14 +1382,14 @@ async fn edit_user( member_to_edit.atype = new_type as i32; // Delete all the odd collections - for c in CollectionUser::find_by_organization_and_user_uuid(org_id, &member_to_edit.user_uuid, &mut conn).await { + for c in CollectionUser::find_by_organization_and_user_uuid(&org_id, &member_to_edit.user_uuid, &mut conn).await { c.delete(&mut conn).await?; } // If no accessAll, add the collections received if !data.access_all { for col in data.collections.iter().flatten() { - match Collection::find_by_uuid_and_org(&col.id, org_id, &mut conn).await { + match Collection::find_by_uuid_and_org(&col.id, &org_id, &mut conn).await { None => err!("Collection not found in Organization"), Some(collection) => { CollectionUser::save( @@ -1385,7 +1415,7 @@ async fn edit_user( log_event( EventType::OrganizationUserUpdated as i32, &member_to_edit.uuid, - org_id, + &org_id, &headers.user.uuid, headers.device.atype, &headers.ip.ip, @@ -1398,7 +1428,7 @@ async fn edit_user( #[delete("/organizations//users", data = "")] async fn bulk_delete_user( - org_id: &str, + org_id: OrganizationId, data: Json, headers: AdminHeaders, mut conn: DbConn, @@ -1408,7 +1438,7 @@ async fn bulk_delete_user( let mut bulk_response = Vec::new(); for member_id in data.ids { - let err_msg = match _delete_user(org_id, &member_id, &headers, &mut conn, &nt).await { + let err_msg = match _delete_user(&org_id, &member_id, &headers, &mut conn, &nt).await { Ok(_) => String::new(), Err(e) => format!("{e:?}"), }; @@ -1431,28 +1461,28 @@ async fn bulk_delete_user( #[delete("/organizations//users/")] async fn delete_user( - org_id: &str, + org_id: OrganizationId, member_id: &str, headers: AdminHeaders, mut conn: DbConn, nt: Notify<'_>, ) -> EmptyResult { - _delete_user(org_id, member_id, &headers, &mut conn, &nt).await + _delete_user(&org_id, member_id, &headers, &mut conn, &nt).await } #[post("/organizations//users//delete")] async fn post_delete_user( - org_id: &str, + org_id: OrganizationId, member_id: &str, headers: AdminHeaders, mut conn: DbConn, nt: Notify<'_>, ) -> EmptyResult { - _delete_user(org_id, member_id, &headers, &mut conn, &nt).await + _delete_user(&org_id, member_id, &headers, &mut conn, &nt).await } async fn _delete_user( - org_id: &str, + org_id: &OrganizationId, member_id: &str, headers: &AdminHeaders, conn: &mut DbConn, @@ -1494,7 +1524,7 @@ async fn _delete_user( #[post("/organizations//users/public-keys", data = "")] async fn bulk_public_keys( - org_id: &str, + org_id: OrganizationId, data: Json, _headers: AdminHeaders, mut conn: DbConn, @@ -1506,7 +1536,7 @@ async fn bulk_public_keys( // If the user does not exists, just ignore it, and do not return any information regarding that Membership UUID. // The web-vault will then ignore that user for the following steps. for member_id in data.ids { - match Membership::find_by_uuid_and_org(&member_id, org_id, &mut conn).await { + match Membership::find_by_uuid_and_org(&member_id, &org_id, &mut conn).await { Some(member) => match User::find_by_uuid(&member.user_uuid, &mut conn).await { Some(user) => bulk_response.push(json!( { @@ -1614,7 +1644,7 @@ async fn post_org_import( #[serde(rename_all = "camelCase")] #[allow(dead_code)] struct BulkCollectionsData { - organization_id: String, + organization_id: OrganizationId, cipher_ids: Vec, collection_ids: HashSet, remove_collections: bool, @@ -1671,8 +1701,8 @@ async fn post_bulk_collections(data: Json, headers: Headers } #[get("/organizations//policies")] -async fn list_policies(org_id: &str, _headers: AdminHeaders, mut conn: DbConn) -> Json { - let policies = OrgPolicy::find_by_org(org_id, &mut conn).await; +async fn list_policies(org_id: OrganizationId, _headers: AdminHeaders, mut conn: DbConn) -> Json { + let policies = OrgPolicy::find_by_org(&org_id, &mut conn).await; let policies_json: Vec = policies.iter().map(OrgPolicy::to_json).collect(); Json(json!({ @@ -1683,11 +1713,11 @@ async fn list_policies(org_id: &str, _headers: AdminHeaders, mut conn: DbConn) - } #[get("/organizations//policies/token?")] -async fn list_policies_token(org_id: &str, token: &str, mut conn: DbConn) -> JsonResult { +async fn list_policies_token(org_id: OrganizationId, token: &str, mut conn: DbConn) -> JsonResult { // web-vault 2024.6.2 seems to send these values and cause logs to output errors // Catch this and prevent errors in the logs // TODO: CleanUp after 2024.6.x is not used anymore. - if org_id == "undefined" && token == "undefined" { + if org_id.as_ref() == "undefined" && token == "undefined" { return Ok(Json(json!({}))); } @@ -1702,7 +1732,7 @@ async fn list_policies_token(org_id: &str, token: &str, mut conn: DbConn) -> Jso } // TODO: We receive the invite token as ?token=<>, validate it contains the org id - let policies = OrgPolicy::find_by_org(org_id, &mut conn).await; + let policies = OrgPolicy::find_by_org(&org_id, &mut conn).await; let policies_json: Vec = policies.iter().map(OrgPolicy::to_json).collect(); Ok(Json(json!({ @@ -1713,14 +1743,14 @@ async fn list_policies_token(org_id: &str, token: &str, mut conn: DbConn) -> Jso } #[get("/organizations//policies/")] -async fn get_policy(org_id: &str, pol_type: i32, _headers: AdminHeaders, mut conn: DbConn) -> JsonResult { +async fn get_policy(org_id: OrganizationId, pol_type: i32, _headers: AdminHeaders, mut conn: DbConn) -> JsonResult { let Some(pol_type_enum) = OrgPolicyType::from_i32(pol_type) else { err!("Invalid or unsupported policy type") }; - let policy = match OrgPolicy::find_by_org_and_type(org_id, pol_type_enum, &mut conn).await { + let policy = match OrgPolicy::find_by_org_and_type(&org_id, pol_type_enum, &mut conn).await { Some(p) => p, - None => OrgPolicy::new(String::from(org_id), pol_type_enum, "null".to_string()), + None => OrgPolicy::new(org_id.clone(), pol_type_enum, "null".to_string()), }; Ok(Json(policy.to_json())) @@ -1736,7 +1766,7 @@ struct PolicyData { #[put("/organizations//policies/", data = "")] async fn put_policy( - org_id: &str, + org_id: OrganizationId, pol_type: i32, data: Json, headers: AdminHeaders, @@ -1756,7 +1786,7 @@ async fn put_policy( if CONFIG.enforce_single_org_with_reset_pw_policy() { if pol_type_enum == OrgPolicyType::ResetPassword && data.enabled { let single_org_policy_enabled = - match OrgPolicy::find_by_org_and_type(org_id, OrgPolicyType::SingleOrg, &mut conn).await { + match OrgPolicy::find_by_org_and_type(&org_id, OrgPolicyType::SingleOrg, &mut conn).await { Some(p) => p.enabled, None => false, }; @@ -1769,7 +1799,7 @@ async fn put_policy( // Also prevent the Single Org Policy to be disabled if the Reset Password policy is enabled if pol_type_enum == OrgPolicyType::SingleOrg && !data.enabled { let reset_pw_policy_enabled = - match OrgPolicy::find_by_org_and_type(org_id, OrgPolicyType::ResetPassword, &mut conn).await { + match OrgPolicy::find_by_org_and_type(&org_id, OrgPolicyType::ResetPassword, &mut conn).await { Some(p) => p.enabled, None => false, }; @@ -1783,7 +1813,7 @@ async fn put_policy( // When enabling the TwoFactorAuthentication policy, revoke all members that do not have 2FA if pol_type_enum == OrgPolicyType::TwoFactorAuthentication && data.enabled { two_factor::enforce_2fa_policy_for_org( - org_id, + &org_id, &headers.user.uuid, headers.device.atype, &headers.ip.ip, @@ -1794,7 +1824,7 @@ async fn put_policy( // When enabling the SingleOrg policy, remove this org's members that are members of other orgs if pol_type_enum == OrgPolicyType::SingleOrg && data.enabled { - for member in Membership::find_by_org(org_id, &mut conn).await.into_iter() { + for member in Membership::find_by_org(&org_id, &mut conn).await.into_iter() { // Policy only applies to non-Owner/non-Admin members who have accepted joining the org // Exclude invited and revoked users when checking for this policy. // Those users will not be allowed to accept or be activated because of the policy checks done there. @@ -1813,7 +1843,7 @@ async fn put_policy( log_event( EventType::OrganizationUserRemoved as i32, &member.uuid, - org_id, + &org_id, &headers.user.uuid, headers.device.atype, &headers.ip.ip, @@ -1826,9 +1856,9 @@ async fn put_policy( } } - let mut policy = match OrgPolicy::find_by_org_and_type(org_id, pol_type_enum, &mut conn).await { + let mut policy = match OrgPolicy::find_by_org_and_type(&org_id, pol_type_enum, &mut conn).await { Some(p) => p, - None => OrgPolicy::new(String::from(org_id), pol_type_enum, "{}".to_string()), + None => OrgPolicy::new(org_id.clone(), pol_type_enum, "{}".to_string()), }; policy.enabled = data.enabled; @@ -1838,7 +1868,7 @@ async fn put_policy( log_event( EventType::PolicyUpdated as i32, &policy.uuid, - org_id, + &org_id, &headers.user.uuid, headers.device.atype, &headers.ip.ip, @@ -1851,7 +1881,7 @@ async fn put_policy( #[allow(unused_variables)] #[get("/organizations//tax")] -fn get_organization_tax(org_id: &str, _headers: Headers) -> Json { +fn get_organization_tax(org_id: OrganizationId, _headers: Headers) -> Json { // Prevent a 404 error, which also causes Javascript errors. // Upstream sends "Only allowed when not self hosted." As an error message. // If we do the same it will also output this to the log, which is overkill. @@ -1936,7 +1966,7 @@ struct OrgImportData { } #[post("/organizations//import", data = "")] -async fn import(org_id: &str, data: Json, headers: Headers, mut conn: DbConn) -> EmptyResult { +async fn import(org_id: OrganizationId, data: Json, headers: Headers, mut conn: DbConn) -> EmptyResult { let data = data.into_inner(); // TODO: Currently we aren't storing the externalId's anywhere, so we also don't have a way @@ -1945,7 +1975,7 @@ async fn import(org_id: &str, data: Json, headers: Headers, mut c // as opposed to upstream which only removes auto-imported users. // User needs to be admin or owner to use the Directory Connector - match Membership::find_by_user_and_org(&headers.user.uuid, org_id, &mut conn).await { + match Membership::find_by_user_and_org(&headers.user.uuid, &org_id, &mut conn).await { Some(member) if member.atype >= MembershipType::Admin => { /* Okay, nothing to do */ } Some(_) => err!("User has insufficient permissions to use Directory Connector"), None => err!("User not part of organization"), @@ -1954,11 +1984,11 @@ async fn import(org_id: &str, data: Json, headers: Headers, mut c for user_data in &data.users { if user_data.deleted { // If user is marked for deletion and it exists, delete it - if let Some(member) = Membership::find_by_email_and_org(&user_data.email, org_id, &mut conn).await { + if let Some(member) = Membership::find_by_email_and_org(&user_data.email, &org_id, &mut conn).await { log_event( EventType::OrganizationUserRemoved as i32, &member.uuid, - org_id, + &org_id, &headers.user.uuid, headers.device.atype, &headers.ip.ip, @@ -1970,7 +2000,7 @@ async fn import(org_id: &str, data: Json, headers: Headers, mut c } // If user is not part of the organization, but it exists - } else if Membership::find_by_email_and_org(&user_data.email, org_id, &mut conn).await.is_none() { + } else if Membership::find_by_email_and_org(&user_data.email, &org_id, &mut conn).await.is_none() { if let Some(user) = User::find_by_mail(&user_data.email, &mut conn).await { let member_status = if CONFIG.mail_enabled() { MembershipStatus::Invited as i32 @@ -1978,7 +2008,7 @@ async fn import(org_id: &str, data: Json, headers: Headers, mut c MembershipStatus::Accepted as i32 // Automatically mark user as accepted if no email invites }; - let mut new_member = Membership::new(user.uuid.clone(), String::from(org_id)); + let mut new_member = Membership::new(user.uuid.clone(), org_id.clone()); new_member.access_all = false; new_member.atype = MembershipType::User as i32; new_member.status = member_status; @@ -1988,7 +2018,7 @@ async fn import(org_id: &str, data: Json, headers: Headers, mut c log_event( EventType::OrganizationUserInvited as i32, &new_member.uuid, - org_id, + &org_id, &headers.user.uuid, headers.device.atype, &headers.ip.ip, @@ -1997,14 +2027,14 @@ async fn import(org_id: &str, data: Json, headers: Headers, mut c .await; if CONFIG.mail_enabled() { - let org_name = match Organization::find_by_uuid(org_id, &mut conn).await { + let org_name = match Organization::find_by_uuid(&org_id, &mut conn).await { Some(org) => org.name, None => err!("Error looking up organization"), }; mail::send_invite( &user, - Some(String::from(org_id)), + Some(org_id.clone()), Some(new_member.uuid), &org_name, Some(headers.user.email.clone()), @@ -2017,13 +2047,13 @@ async fn import(org_id: &str, data: Json, headers: Headers, mut c // If this flag is enabled, any user that isn't provided in the Users list will be removed (by default they will be kept unless they have Deleted == true) if data.overwrite_existing { - for member in Membership::find_by_org_and_type(org_id, MembershipType::User, &mut conn).await { + for member in Membership::find_by_org_and_type(&org_id, MembershipType::User, &mut conn).await { if let Some(user_email) = User::find_by_uuid(&member.user_uuid, &mut conn).await.map(|u| u.email) { if !data.users.iter().any(|u| u.email == user_email) { log_event( EventType::OrganizationUserRemoved as i32, &member.uuid, - org_id, + &org_id, &headers.user.uuid, headers.device.atype, &headers.ip.ip, @@ -2042,14 +2072,19 @@ async fn import(org_id: &str, data: Json, headers: Headers, mut c // Pre web-vault v2022.9.x endpoint #[put("/organizations//users//deactivate")] -async fn deactivate_membership(org_id: &str, member_id: &str, headers: AdminHeaders, mut conn: DbConn) -> EmptyResult { - _revoke_membership(org_id, member_id, &headers, &mut conn).await +async fn deactivate_membership( + org_id: OrganizationId, + member_id: &str, + headers: AdminHeaders, + mut conn: DbConn, +) -> EmptyResult { + _revoke_membership(&org_id, member_id, &headers, &mut conn).await } // Pre web-vault v2022.9.x endpoint #[put("/organizations//users/deactivate", data = "")] async fn bulk_deactivate_membership( - org_id: &str, + org_id: OrganizationId, data: Json, headers: AdminHeaders, conn: DbConn, @@ -2058,8 +2093,13 @@ async fn bulk_deactivate_membership( } #[put("/organizations//users//revoke")] -async fn revoke_membership(org_id: &str, member_id: &str, headers: AdminHeaders, mut conn: DbConn) -> EmptyResult { - _revoke_membership(org_id, member_id, &headers, &mut conn).await +async fn revoke_membership( + org_id: OrganizationId, + member_id: &str, + headers: AdminHeaders, + mut conn: DbConn, +) -> EmptyResult { + _revoke_membership(&org_id, member_id, &headers, &mut conn).await } #[derive(Deserialize, Debug)] @@ -2070,7 +2110,7 @@ struct OrgBulkRevokeData { #[put("/organizations//users/revoke", data = "")] async fn bulk_revoke_membership( - org_id: &str, + org_id: OrganizationId, data: Json, headers: AdminHeaders, mut conn: DbConn, @@ -2081,7 +2121,7 @@ async fn bulk_revoke_membership( match data.ids { Some(members) => { for member_id in members { - let err_msg = match _revoke_membership(org_id, &member_id, &headers, &mut conn).await { + let err_msg = match _revoke_membership(&org_id, &member_id, &headers, &mut conn).await { Ok(_) => String::new(), Err(e) => format!("{e:?}"), }; @@ -2105,7 +2145,12 @@ async fn bulk_revoke_membership( })) } -async fn _revoke_membership(org_id: &str, member_id: &str, headers: &AdminHeaders, conn: &mut DbConn) -> EmptyResult { +async fn _revoke_membership( + org_id: &OrganizationId, + member_id: &str, + headers: &AdminHeaders, + conn: &mut DbConn, +) -> EmptyResult { match Membership::find_by_uuid_and_org(member_id, org_id, conn).await { Some(mut member) if member.status > MembershipStatus::Revoked as i32 => { if member.user_uuid == headers.user.uuid { @@ -2142,14 +2187,19 @@ async fn _revoke_membership(org_id: &str, member_id: &str, headers: &AdminHeader // Pre web-vault v2022.9.x endpoint #[put("/organizations//users//activate")] -async fn activate_membership(org_id: &str, member_id: &str, headers: AdminHeaders, mut conn: DbConn) -> EmptyResult { - _restore_membership(org_id, member_id, &headers, &mut conn).await +async fn activate_membership( + org_id: OrganizationId, + member_id: &str, + headers: AdminHeaders, + mut conn: DbConn, +) -> EmptyResult { + _restore_membership(&org_id, member_id, &headers, &mut conn).await } // Pre web-vault v2022.9.x endpoint #[put("/organizations//users/activate", data = "")] async fn bulk_activate_membership( - org_id: &str, + org_id: OrganizationId, data: Json, headers: AdminHeaders, conn: DbConn, @@ -2158,13 +2208,18 @@ async fn bulk_activate_membership( } #[put("/organizations//users//restore")] -async fn restore_membership(org_id: &str, member_id: &str, headers: AdminHeaders, mut conn: DbConn) -> EmptyResult { - _restore_membership(org_id, member_id, &headers, &mut conn).await +async fn restore_membership( + org_id: OrganizationId, + member_id: &str, + headers: AdminHeaders, + mut conn: DbConn, +) -> EmptyResult { + _restore_membership(&org_id, member_id, &headers, &mut conn).await } #[put("/organizations//users/restore", data = "")] async fn bulk_restore_membership( - org_id: &str, + org_id: OrganizationId, data: Json, headers: AdminHeaders, mut conn: DbConn, @@ -2173,7 +2228,7 @@ async fn bulk_restore_membership( let mut bulk_response = Vec::new(); for member_id in data.ids { - let err_msg = match _restore_membership(org_id, &member_id, &headers, &mut conn).await { + let err_msg = match _restore_membership(&org_id, &member_id, &headers, &mut conn).await { Ok(_) => String::new(), Err(e) => format!("{e:?}"), }; @@ -2194,7 +2249,12 @@ async fn bulk_restore_membership( })) } -async fn _restore_membership(org_id: &str, member_id: &str, headers: &AdminHeaders, conn: &mut DbConn) -> EmptyResult { +async fn _restore_membership( + org_id: &OrganizationId, + member_id: &str, + headers: &AdminHeaders, + conn: &mut DbConn, +) -> EmptyResult { match Membership::find_by_uuid_and_org(member_id, org_id, conn).await { Some(mut member) if member.status < MembershipStatus::Accepted as i32 => { if member.user_uuid == headers.user.uuid { @@ -2243,10 +2303,10 @@ async fn _restore_membership(org_id: &str, member_id: &str, headers: &AdminHeade } #[get("/organizations//groups")] -async fn get_groups(org_id: &str, _headers: ManagerHeadersLoose, mut conn: DbConn) -> JsonResult { +async fn get_groups(org_id: OrganizationId, _headers: ManagerHeadersLoose, mut conn: DbConn) -> JsonResult { let groups: Vec = if CONFIG.org_groups_enabled() { // Group::find_by_organization(&org_id, &mut conn).await.iter().map(Group::to_json).collect::() - let groups = Group::find_by_organization(org_id, &mut conn).await; + let groups = Group::find_by_organization(&org_id, &mut conn).await; let mut groups_json = Vec::with_capacity(groups.len()); for g in groups { @@ -2278,8 +2338,8 @@ struct GroupRequest { } impl GroupRequest { - pub fn to_group(&self, organizations_uuid: &str) -> Group { - Group::new(String::from(organizations_uuid), self.name.clone(), self.access_all, self.external_id.clone()) + pub fn to_group(&self, org_uuid: &OrganizationId) -> Group { + Group::new(org_uuid.clone(), self.name.clone(), self.access_all, self.external_id.clone()) } pub fn update_group(&self, mut group: Group) -> Group { @@ -2328,7 +2388,7 @@ impl SelectionReadOnly { #[post("/organizations//groups/", data = "")] async fn post_group( - org_id: &str, + org_id: OrganizationId, group_id: &str, data: Json, headers: AdminHeaders, @@ -2338,18 +2398,23 @@ async fn post_group( } #[post("/organizations//groups", data = "")] -async fn post_groups(org_id: &str, headers: AdminHeaders, data: Json, mut conn: DbConn) -> JsonResult { +async fn post_groups( + org_id: OrganizationId, + headers: AdminHeaders, + data: Json, + mut conn: DbConn, +) -> JsonResult { if !CONFIG.org_groups_enabled() { err!("Group support is disabled"); } let group_request = data.into_inner(); - let group = group_request.to_group(org_id); + let group = group_request.to_group(&org_id); log_event( EventType::GroupCreated as i32, &group.uuid, - org_id, + &org_id, &headers.user.uuid, headers.device.atype, &headers.ip.ip, @@ -2362,7 +2427,7 @@ async fn post_groups(org_id: &str, headers: AdminHeaders, data: Json/groups/", data = "")] async fn put_group( - org_id: &str, + org_id: OrganizationId, group_id: &str, data: Json, headers: AdminHeaders, @@ -2372,7 +2437,7 @@ async fn put_group( err!("Group support is disabled"); } - let Some(group) = Group::find_by_uuid_and_org(group_id, org_id, &mut conn).await else { + let Some(group) = Group::find_by_uuid_and_org(group_id, &org_id, &mut conn).await else { err!("Group not found", "Group uuid is invalid or does not belong to the organization") }; @@ -2385,7 +2450,7 @@ async fn put_group( log_event( EventType::GroupUpdated as i32, &updated_group.uuid, - org_id, + &org_id, &headers.user.uuid, headers.device.atype, &headers.ip.ip, @@ -2400,7 +2465,7 @@ async fn add_update_group( mut group: Group, collections: Vec, users: Vec, - org_id: &str, + org_id: OrganizationId, headers: &AdminHeaders, conn: &mut DbConn, ) -> JsonResult { @@ -2418,7 +2483,7 @@ async fn add_update_group( log_event( EventType::OrganizationUserUpdatedGroups as i32, &assigned_user_id, - org_id, + &org_id, &headers.user.uuid, headers.device.atype, &headers.ip.ip, @@ -2437,12 +2502,17 @@ async fn add_update_group( } #[get("/organizations//groups//details")] -async fn get_group_details(org_id: &str, group_id: &str, _headers: AdminHeaders, mut conn: DbConn) -> JsonResult { +async fn get_group_details( + org_id: OrganizationId, + group_id: &str, + _headers: AdminHeaders, + mut conn: DbConn, +) -> JsonResult { if !CONFIG.org_groups_enabled() { err!("Group support is disabled"); } - let Some(group) = Group::find_by_uuid_and_org(group_id, org_id, &mut conn).await else { + let Some(group) = Group::find_by_uuid_and_org(group_id, &org_id, &mut conn).await else { err!("Group not found", "Group uuid is invalid or does not belong to the organization") }; @@ -2450,16 +2520,26 @@ async fn get_group_details(org_id: &str, group_id: &str, _headers: AdminHeaders, } #[post("/organizations//groups//delete")] -async fn post_delete_group(org_id: &str, group_id: &str, headers: AdminHeaders, mut conn: DbConn) -> EmptyResult { - _delete_group(org_id, group_id, &headers, &mut conn).await +async fn post_delete_group( + org_id: OrganizationId, + group_id: &str, + headers: AdminHeaders, + mut conn: DbConn, +) -> EmptyResult { + _delete_group(&org_id, group_id, &headers, &mut conn).await } #[delete("/organizations//groups/")] -async fn delete_group(org_id: &str, group_id: &str, headers: AdminHeaders, mut conn: DbConn) -> EmptyResult { - _delete_group(org_id, group_id, &headers, &mut conn).await +async fn delete_group(org_id: OrganizationId, group_id: &str, headers: AdminHeaders, mut conn: DbConn) -> EmptyResult { + _delete_group(&org_id, group_id, &headers, &mut conn).await } -async fn _delete_group(org_id: &str, group_id: &str, headers: &AdminHeaders, conn: &mut DbConn) -> EmptyResult { +async fn _delete_group( + org_id: &OrganizationId, + group_id: &str, + headers: &AdminHeaders, + conn: &mut DbConn, +) -> EmptyResult { if !CONFIG.org_groups_enabled() { err!("Group support is disabled"); } @@ -2484,7 +2564,7 @@ async fn _delete_group(org_id: &str, group_id: &str, headers: &AdminHeaders, con #[delete("/organizations//groups", data = "")] async fn bulk_delete_groups( - org_id: &str, + org_id: OrganizationId, data: Json, headers: AdminHeaders, mut conn: DbConn, @@ -2496,18 +2576,18 @@ async fn bulk_delete_groups( let data: OrgBulkIds = data.into_inner(); for group_id in data.ids { - _delete_group(org_id, &group_id, &headers, &mut conn).await? + _delete_group(&org_id, &group_id, &headers, &mut conn).await? } Ok(()) } #[get("/organizations//groups/")] -async fn get_group(org_id: &str, group_id: &str, _headers: AdminHeaders, mut conn: DbConn) -> JsonResult { +async fn get_group(org_id: OrganizationId, group_id: &str, _headers: AdminHeaders, mut conn: DbConn) -> JsonResult { if !CONFIG.org_groups_enabled() { err!("Group support is disabled"); } - let Some(group) = Group::find_by_uuid_and_org(group_id, org_id, &mut conn).await else { + let Some(group) = Group::find_by_uuid_and_org(group_id, &org_id, &mut conn).await else { err!("Group not found", "Group uuid is invalid or does not belong to the organization") }; @@ -2515,12 +2595,17 @@ async fn get_group(org_id: &str, group_id: &str, _headers: AdminHeaders, mut con } #[get("/organizations//groups//users")] -async fn get_group_users(org_id: &str, group_id: &str, _headers: AdminHeaders, mut conn: DbConn) -> JsonResult { +async fn get_group_users( + org_id: OrganizationId, + group_id: &str, + _headers: AdminHeaders, + mut conn: DbConn, +) -> JsonResult { if !CONFIG.org_groups_enabled() { err!("Group support is disabled"); } - if Group::find_by_uuid_and_org(group_id, org_id, &mut conn).await.is_none() { + if Group::find_by_uuid_and_org(group_id, &org_id, &mut conn).await.is_none() { err!("Group could not be found!", "Group uuid is invalid or does not belong to the organization") }; @@ -2535,7 +2620,7 @@ async fn get_group_users(org_id: &str, group_id: &str, _headers: AdminHeaders, m #[put("/organizations//groups//users", data = "")] async fn put_group_users( - org_id: &str, + org_id: OrganizationId, group_id: &str, headers: AdminHeaders, data: Json>, @@ -2545,7 +2630,7 @@ async fn put_group_users( err!("Group support is disabled"); } - if Group::find_by_uuid_and_org(group_id, org_id, &mut conn).await.is_none() { + if Group::find_by_uuid_and_org(group_id, &org_id, &mut conn).await.is_none() { err!("Group could not be found!", "Group uuid is invalid or does not belong to the organization") }; @@ -2559,7 +2644,7 @@ async fn put_group_users( log_event( EventType::OrganizationUserUpdatedGroups as i32, &assigned_user_id, - org_id, + &org_id, &headers.user.uuid, headers.device.atype, &headers.ip.ip, @@ -2572,12 +2657,17 @@ async fn put_group_users( } #[get("/organizations//users//groups")] -async fn get_user_groups(org_id: &str, user_id: &str, _headers: AdminHeaders, mut conn: DbConn) -> JsonResult { +async fn get_user_groups( + org_id: OrganizationId, + user_id: &str, + _headers: AdminHeaders, + mut conn: DbConn, +) -> JsonResult { if !CONFIG.org_groups_enabled() { err!("Group support is disabled"); } - if Membership::find_by_uuid_and_org(user_id, org_id, &mut conn).await.is_none() { + if Membership::find_by_uuid_and_org(user_id, &org_id, &mut conn).await.is_none() { err!("User could not be found!") }; @@ -2595,7 +2685,7 @@ struct OrganizationUserUpdateGroupsRequest { #[post("/organizations//users//groups", data = "")] async fn post_user_groups( - org_id: &str, + org_id: OrganizationId, member_id: &str, data: Json, headers: AdminHeaders, @@ -2606,7 +2696,7 @@ async fn post_user_groups( #[put("/organizations//users//groups", data = "")] async fn put_user_groups( - org_id: &str, + org_id: OrganizationId, member_id: &str, data: Json, headers: AdminHeaders, @@ -2616,7 +2706,7 @@ async fn put_user_groups( err!("Group support is disabled"); } - if Membership::find_by_uuid_and_org(member_id, org_id, &mut conn).await.is_none() { + if Membership::find_by_uuid_and_org(member_id, &org_id, &mut conn).await.is_none() { err!("User could not be found or does not belong to the organization."); } @@ -2631,7 +2721,7 @@ async fn put_user_groups( log_event( EventType::OrganizationUserUpdatedGroups as i32, member_id, - org_id, + &org_id, &headers.user.uuid, headers.device.atype, &headers.ip.ip, @@ -2644,7 +2734,7 @@ async fn put_user_groups( #[post("/organizations//groups//delete-user/")] async fn post_delete_group_user( - org_id: &str, + org_id: OrganizationId, group_id: &str, member_id: &str, headers: AdminHeaders, @@ -2655,7 +2745,7 @@ async fn post_delete_group_user( #[delete("/organizations//groups//users/")] async fn delete_group_user( - org_id: &str, + org_id: OrganizationId, group_id: &str, member_id: &str, headers: AdminHeaders, @@ -2665,18 +2755,18 @@ async fn delete_group_user( err!("Group support is disabled"); } - if Membership::find_by_uuid_and_org(member_id, org_id, &mut conn).await.is_none() { + if Membership::find_by_uuid_and_org(member_id, &org_id, &mut conn).await.is_none() { err!("User could not be found or does not belong to the organization."); } - if Group::find_by_uuid_and_org(group_id, org_id, &mut conn).await.is_none() { + if Group::find_by_uuid_and_org(group_id, &org_id, &mut conn).await.is_none() { err!("Group could not be found or does not belong to the organization."); } log_event( EventType::OrganizationUserUpdatedGroups as i32, member_id, - org_id, + &org_id, &headers.user.uuid, headers.device.atype, &headers.ip.ip, @@ -2706,8 +2796,8 @@ struct OrganizationUserResetPasswordRequest { // But the clients do not seem to use this at all // Just add it here in case they will #[get("/organizations//public-key")] -async fn get_organization_public_key(org_id: &str, _headers: Headers, mut conn: DbConn) -> JsonResult { - let Some(org) = Organization::find_by_uuid(org_id, &mut conn).await else { +async fn get_organization_public_key(org_id: OrganizationId, _headers: Headers, mut conn: DbConn) -> JsonResult { + let Some(org) = Organization::find_by_uuid(&org_id, &mut conn).await else { err!("Organization not found") }; @@ -2720,20 +2810,20 @@ async fn get_organization_public_key(org_id: &str, _headers: Headers, mut conn: // Obsolete - Renamed to public-key (2023.8), left for backwards compatibility with older clients // https://github.com/bitwarden/server/blob/25dc0c9178e3e3584074bbef0d4be827b7c89415/src/Api/AdminConsole/Controllers/OrganizationsController.cs#L463-L468 #[get("/organizations//keys")] -async fn get_organization_keys(org_id: &str, headers: Headers, conn: DbConn) -> JsonResult { +async fn get_organization_keys(org_id: OrganizationId, headers: Headers, conn: DbConn) -> JsonResult { get_organization_public_key(org_id, headers, conn).await } #[put("/organizations//users//reset-password", data = "")] async fn put_reset_password( - org_id: &str, + org_id: OrganizationId, member_id: &str, headers: AdminHeaders, data: Json, mut conn: DbConn, nt: Notify<'_>, ) -> EmptyResult { - let Some(org) = Organization::find_by_uuid(org_id, &mut conn).await else { + let Some(org) = Organization::find_by_uuid(&org_id, &mut conn).await else { err!("Required organization not found") }; @@ -2745,7 +2835,7 @@ async fn put_reset_password( err!("User not found") }; - check_reset_password_applicable_and_permissions(org_id, member_id, &headers, &mut conn).await?; + check_reset_password_applicable_and_permissions(&org_id, member_id, &headers, &mut conn).await?; if member.reset_password_key.is_none() { err!("Password reset not or not correctly enrolled"); @@ -2771,7 +2861,7 @@ async fn put_reset_password( log_event( EventType::OrganizationUserAdminResetPassword as i32, member_id, - org_id, + &org_id, &headers.user.uuid, headers.device.atype, &headers.ip.ip, @@ -2784,16 +2874,16 @@ async fn put_reset_password( #[get("/organizations//users//reset-password-details")] async fn get_reset_password_details( - org_id: &str, + org_id: OrganizationId, member_id: &str, headers: AdminHeaders, mut conn: DbConn, ) -> JsonResult { - let Some(org) = Organization::find_by_uuid(org_id, &mut conn).await else { + let Some(org) = Organization::find_by_uuid(&org_id, &mut conn).await else { err!("Required organization not found") }; - let Some(member) = Membership::find_by_uuid_and_org(member_id, org_id, &mut conn).await else { + let Some(member) = Membership::find_by_uuid_and_org(member_id, &org_id, &mut conn).await else { err!("User to reset isn't member of required organization") }; @@ -2801,7 +2891,7 @@ async fn get_reset_password_details( err!("User not found") }; - check_reset_password_applicable_and_permissions(org_id, member_id, &headers, &mut conn).await?; + check_reset_password_applicable_and_permissions(&org_id, member_id, &headers, &mut conn).await?; // https://github.com/bitwarden/server/blob/3b50ccb9f804efaacdc46bed5b60e5b28eddefcf/src/Api/Models/Response/Organizations/OrganizationUserResponseModel.cs#L111 Ok(Json(json!({ @@ -2817,7 +2907,7 @@ async fn get_reset_password_details( } async fn check_reset_password_applicable_and_permissions( - org_id: &str, + org_id: &OrganizationId, member_id: &str, headers: &AdminHeaders, conn: &mut DbConn, @@ -2836,7 +2926,7 @@ async fn check_reset_password_applicable_and_permissions( } } -async fn check_reset_password_applicable(org_id: &str, conn: &mut DbConn) -> EmptyResult { +async fn check_reset_password_applicable(org_id: &OrganizationId, conn: &mut DbConn) -> EmptyResult { if !CONFIG.mail_enabled() { err!("Password reset is not supported on an email-disabled instance."); } @@ -2854,22 +2944,22 @@ async fn check_reset_password_applicable(org_id: &str, conn: &mut DbConn) -> Emp #[put("/organizations//users//reset-password-enrollment", data = "")] async fn put_reset_password_enrollment( - org_id: &str, + org_id: OrganizationId, member_id: &str, headers: Headers, data: Json, mut conn: DbConn, ) -> EmptyResult { - let Some(mut member) = Membership::find_by_user_and_org(&headers.user.uuid, org_id, &mut conn).await else { + let Some(mut member) = Membership::find_by_user_and_org(&headers.user.uuid, &org_id, &mut conn).await else { err!("User to enroll isn't member of required organization") }; - check_reset_password_applicable(org_id, &mut conn).await?; + check_reset_password_applicable(&org_id, &mut conn).await?; let reset_request = data.into_inner(); if reset_request.reset_password_key.is_none() - && OrgPolicy::org_is_reset_password_auto_enroll(org_id, &mut conn).await + && OrgPolicy::org_is_reset_password_auto_enroll(&org_id, &mut conn).await { err!("Reset password can't be withdrawed due to an enterprise policy"); } @@ -2892,7 +2982,7 @@ async fn put_reset_password_enrollment( EventType::OrganizationUserResetPasswordWithdraw as i32 }; - log_event(log_id, member_id, org_id, &headers.user.uuid, headers.device.atype, &headers.ip.ip, &mut conn).await; + log_event(log_id, member_id, &org_id, &headers.user.uuid, headers.device.atype, &headers.ip.ip, &mut conn).await; Ok(()) } @@ -2906,7 +2996,7 @@ async fn put_reset_password_enrollment( // Else the export will be just an empty JSON file. #[get("/organizations//export")] async fn get_org_export( - org_id: &str, + org_id: OrganizationId, headers: AdminHeaders, client_version: Option, mut conn: DbConn, @@ -2928,12 +3018,12 @@ async fn get_org_export( // Backwards compatible pre v2023.1.0 response Json(json!({ "collections": { - "data": convert_json_key_lcase_first(_get_org_collections(org_id, &mut conn).await), + "data": convert_json_key_lcase_first(_get_org_collections(&org_id, &mut conn).await), "object": "list", "continuationToken": null, }, "ciphers": { - "data": convert_json_key_lcase_first(_get_org_details(org_id, &headers.host, &headers.user.uuid, &mut conn).await), + "data": convert_json_key_lcase_first(_get_org_details(&org_id, &headers.host, &headers.user.uuid, &mut conn).await), "object": "list", "continuationToken": null, } @@ -2941,14 +3031,14 @@ async fn get_org_export( } else { // v2023.1.0 and newer response Json(json!({ - "collections": convert_json_key_lcase_first(_get_org_collections(org_id, &mut conn).await), - "ciphers": convert_json_key_lcase_first(_get_org_details(org_id, &headers.host, &headers.user.uuid, &mut conn).await), + "collections": convert_json_key_lcase_first(_get_org_collections(&org_id, &mut conn).await), + "ciphers": convert_json_key_lcase_first(_get_org_details(&org_id, &headers.host, &headers.user.uuid, &mut conn).await), })) } } async fn _api_key( - org_id: &str, + org_id: &OrganizationId, data: Json, rotate: bool, headers: AdminHeaders, @@ -2971,7 +3061,7 @@ async fn _api_key( } None => { let api_key = crate::crypto::generate_api_key(); - let new_org_api_key = OrganizationApiKey::new(String::from(org_id), api_key); + let new_org_api_key = OrganizationApiKey::new(org_id.clone(), api_key); new_org_api_key.save(&conn).await.expect("Error creating organization API Key"); new_org_api_key } @@ -2985,16 +3075,21 @@ async fn _api_key( } #[post("/organizations//api-key", data = "")] -async fn api_key(org_id: &str, data: Json, headers: AdminHeaders, conn: DbConn) -> JsonResult { - _api_key(org_id, data, false, headers, conn).await +async fn api_key( + org_id: OrganizationId, + data: Json, + headers: AdminHeaders, + conn: DbConn, +) -> JsonResult { + _api_key(&org_id, data, false, headers, conn).await } #[post("/organizations//rotate-api-key", data = "")] async fn rotate_api_key( - org_id: &str, + org_id: OrganizationId, data: Json, headers: AdminHeaders, conn: DbConn, ) -> JsonResult { - _api_key(org_id, data, true, headers, conn).await + _api_key(&org_id, data, true, headers, conn).await } diff --git a/src/api/core/public.rs b/src/api/core/public.rs index 1480cef098..7478833f1e 100644 --- a/src/api/core/public.rs +++ b/src/api/core/public.rs @@ -179,7 +179,7 @@ async fn ldap_import(data: Json, token: PublicToken, mut conn: Db Ok(()) } -pub struct PublicToken(String); +pub struct PublicToken(OrganizationId); #[rocket::async_trait] impl<'r> FromRequest<'r> for PublicToken { @@ -222,7 +222,8 @@ impl<'r> FromRequest<'r> for PublicToken { let Some(org_uuid) = claims.client_id.strip_prefix("organization.") else { err_handler!("Malformed client_id") }; - let Some(org_api_key) = OrganizationApiKey::find_by_org_uuid(org_uuid, &conn).await else { + let org_uuid: OrganizationId = org_uuid.to_string().into(); + let Some(org_api_key) = OrganizationApiKey::find_by_org_uuid(&org_uuid, &conn).await else { err_handler!("Invalid client_id") }; if org_api_key.org_uuid != claims.client_sub { diff --git a/src/api/core/two_factor/mod.rs b/src/api/core/two_factor/mod.rs index 756ae83697..c2facd6229 100644 --- a/src/api/core/two_factor/mod.rs +++ b/src/api/core/two_factor/mod.rs @@ -208,7 +208,7 @@ pub async fn enforce_2fa_policy( } pub async fn enforce_2fa_policy_for_org( - org_uuid: &str, + org_uuid: &OrganizationId, act_uuid: &str, device_type: i32, ip: &std::net::IpAddr, diff --git a/src/api/identity.rs b/src/api/identity.rs index 1d1119875c..a32433f06e 100644 --- a/src/api/identity.rs +++ b/src/api/identity.rs @@ -472,7 +472,8 @@ async fn _organization_api_key_login(data: ConnectData, conn: &mut DbConn, ip: & let Some(org_uuid) = client_id.strip_prefix("organization.") else { err!("Malformed client_id", format!("IP: {}.", ip.ip)) }; - let Some(org_api_key) = OrganizationApiKey::find_by_org_uuid(org_uuid, conn).await else { + let org_uuid: OrganizationId = org_uuid.to_string().into(); + let Some(org_api_key) = OrganizationApiKey::find_by_org_uuid(&org_uuid, conn).await else { err!("Invalid client_id", format!("IP: {}.", ip.ip)) }; diff --git a/src/api/notifications.rs b/src/api/notifications.rs index 8c925e37bb..dc65003906 100644 --- a/src/api/notifications.rs +++ b/src/api/notifications.rs @@ -422,7 +422,7 @@ impl WebSocketUsers { if *NOTIFICATIONS_DISABLED { return; } - let org_uuid = convert_option(cipher.organization_uuid.clone()); + let org_uuid = convert_option(cipher.organization_uuid.as_deref()); // Depending if there are collections provided or not, we need to have different values for the following variables. // The user_uuid should be `null`, and the revision date should be set to now, else the clients won't sync the collection change. let (user_uuid, collection_uuids, revision_date) = if let Some(collection_uuids) = collection_uuids { diff --git a/src/auth.rs b/src/auth.rs index fda4cb0224..a2cbc9eef7 100644 --- a/src/auth.rs +++ b/src/auth.rs @@ -14,6 +14,7 @@ use std::{ net::IpAddr, }; +use crate::db::models::OrganizationId; use crate::{error::Error, CONFIG}; const JWT_ALGORITHM: Algorithm = Algorithm::RS256; @@ -190,7 +191,7 @@ pub struct InviteJwtClaims { pub sub: String, pub email: String, - pub org_id: Option, + pub org_id: Option, pub member_id: Option, pub invited_by_email: Option, } @@ -198,7 +199,7 @@ pub struct InviteJwtClaims { pub fn generate_invite_claims( uuid: String, email: String, - org_id: Option, + org_id: Option, member_id: Option, invited_by_email: Option, ) -> InviteJwtClaims { @@ -266,18 +267,18 @@ pub struct OrgApiKeyLoginJwtClaims { pub sub: String, pub client_id: String, - pub client_sub: String, + pub client_sub: OrganizationId, pub scope: Vec, } -pub fn generate_organization_api_key_login_claims(uuid: String, org_id: String) -> OrgApiKeyLoginJwtClaims { +pub fn generate_organization_api_key_login_claims(uuid: String, org_id: OrganizationId) -> OrgApiKeyLoginJwtClaims { let time_now = Utc::now(); OrgApiKeyLoginJwtClaims { nbf: time_now.timestamp(), exp: (time_now + TimeDelta::try_hours(1).unwrap()).timestamp(), iss: JWT_ORG_API_KEY_ISSUER.to_string(), sub: uuid, - client_id: format!("organization.{org_id}"), + client_id: format!("organization.{}", org_id), client_sub: org_id, scope: vec!["api.organization".into()], } @@ -549,17 +550,17 @@ impl<'r> FromRequest<'r> for OrgHeaders { // org_id is usually the second path param ("/organizations/"), // but there are cases where it is a query value. // First check the path, if this is not a valid uuid, try the query values. - let url_org_id: Option<&str> = { + let url_org_id: Option = { let mut url_org_id = None; if let Some(Ok(org_id)) = request.param::<&str>(1) { if uuid::Uuid::parse_str(org_id).is_ok() { - url_org_id = Some(org_id); + url_org_id = Some(org_id.to_string().into()); } } if let Some(Ok(org_id)) = request.query_value::<&str>("organizationId") { if uuid::Uuid::parse_str(org_id).is_ok() { - url_org_id = Some(org_id); + url_org_id = Some(org_id.to_string().into()); } } @@ -574,7 +575,7 @@ impl<'r> FromRequest<'r> for OrgHeaders { }; let user = headers.user; - let membership = match Membership::find_by_user_and_org(&user.uuid, org_id, &mut conn).await { + let membership = match Membership::find_by_user_and_org(&user.uuid, &org_id, &mut conn).await { Some(member) => { if member.status == MembershipStatus::Confirmed as i32 { member diff --git a/src/db/models/attachment.rs b/src/db/models/attachment.rs index 65855cc0a9..266fd19266 100644 --- a/src/db/models/attachment.rs +++ b/src/db/models/attachment.rs @@ -3,6 +3,7 @@ use std::io::ErrorKind; use bigdecimal::{BigDecimal, ToPrimitive}; use serde_json::Value; +use super::OrganizationId; use crate::CONFIG; db_object! { @@ -172,7 +173,7 @@ impl Attachment { }} } - pub async fn size_by_org(org_uuid: &str, conn: &mut DbConn) -> i64 { + pub async fn size_by_org(org_uuid: &OrganizationId, conn: &mut DbConn) -> i64 { db_run! { conn: { let result: Option = attachments::table .left_join(ciphers::table.on(ciphers::uuid.eq(attachments::cipher_uuid))) @@ -189,7 +190,7 @@ impl Attachment { }} } - pub async fn count_by_org(org_uuid: &str, conn: &mut DbConn) -> i64 { + pub async fn count_by_org(org_uuid: &OrganizationId, conn: &mut DbConn) -> i64 { db_run! { conn: { attachments::table .left_join(ciphers::table.on(ciphers::uuid.eq(attachments::cipher_uuid))) @@ -203,7 +204,11 @@ impl Attachment { // This will return all attachments linked to the user or org // There is no filtering done here if the user actually has access! // It is used to speed up the sync process, and the matching is done in a different part. - pub async fn find_all_by_user_and_orgs(user_uuid: &str, org_uuids: &Vec, conn: &mut DbConn) -> Vec { + pub async fn find_all_by_user_and_orgs( + user_uuid: &str, + org_uuids: &Vec, + conn: &mut DbConn, + ) -> Vec { db_run! { conn: { attachments::table .left_join(ciphers::table.on(ciphers::uuid.eq(attachments::cipher_uuid))) diff --git a/src/db/models/auth_request.rs b/src/db/models/auth_request.rs index 3aca20cbe8..ed931eddc3 100644 --- a/src/db/models/auth_request.rs +++ b/src/db/models/auth_request.rs @@ -1,3 +1,4 @@ +use super::OrganizationId; use crate::crypto::ct_eq; use chrono::{NaiveDateTime, Utc}; @@ -9,7 +10,7 @@ db_object! { pub struct AuthRequest { pub uuid: String, pub user_uuid: String, - pub organization_uuid: Option, + pub organization_uuid: Option, pub request_device_identifier: String, pub device_type: i32, // https://github.com/bitwarden/server/blob/master/src/Core/Enums/DeviceType.cs diff --git a/src/db/models/cipher.rs b/src/db/models/cipher.rs index c4bd05dbc8..76cd0a778e 100644 --- a/src/db/models/cipher.rs +++ b/src/db/models/cipher.rs @@ -4,7 +4,8 @@ use chrono::{NaiveDateTime, TimeDelta, Utc}; use serde_json::Value; use super::{ - Attachment, CollectionCipher, Favorite, FolderCipher, Group, Membership, MembershipStatus, MembershipType, User, + Attachment, CollectionCipher, Favorite, FolderCipher, Group, Membership, MembershipStatus, MembershipType, + OrganizationId, User, }; use crate::api::core::{CipherData, CipherSyncData, CipherSyncType}; @@ -22,7 +23,7 @@ db_object! { pub updated_at: NaiveDateTime, pub user_uuid: Option, - pub organization_uuid: Option, + pub organization_uuid: Option, pub key: Option, diff --git a/src/db/models/collection.rs b/src/db/models/collection.rs index d8aceba37b..055ecb2ce0 100644 --- a/src/db/models/collection.rs +++ b/src/db/models/collection.rs @@ -1,6 +1,6 @@ use serde_json::Value; -use super::{CollectionGroup, GroupUser, Membership, MembershipStatus, MembershipType, User}; +use super::{CollectionGroup, GroupUser, Membership, MembershipStatus, MembershipType, OrganizationId, User}; use crate::CONFIG; db_object! { @@ -9,7 +9,7 @@ db_object! { #[diesel(primary_key(uuid))] pub struct Collection { pub uuid: String, - pub org_uuid: String, + pub org_uuid: OrganizationId, pub name: String, pub external_id: Option, } @@ -35,7 +35,7 @@ db_object! { /// Local methods impl Collection { - pub fn new(org_uuid: String, name: String, external_id: Option) -> Self { + pub fn new(org_uuid: OrganizationId, name: String, external_id: Option) -> Self { let mut new_model = Self { uuid: crate::util::get_uuid(), org_uuid, @@ -185,7 +185,7 @@ impl Collection { }} } - pub async fn delete_all_by_organization(org_uuid: &str, conn: &mut DbConn) -> EmptyResult { + pub async fn delete_all_by_organization(org_uuid: &OrganizationId, conn: &mut DbConn) -> EmptyResult { for collection in Self::find_by_organization(org_uuid, conn).await { collection.delete(conn).await?; } @@ -279,15 +279,19 @@ impl Collection { } } - pub async fn find_by_organization_and_user_uuid(org_uuid: &str, user_uuid: &str, conn: &mut DbConn) -> Vec { + pub async fn find_by_organization_and_user_uuid( + org_uuid: &OrganizationId, + user_uuid: &str, + conn: &mut DbConn, + ) -> Vec { Self::find_by_user_uuid(user_uuid.to_owned(), conn) .await .into_iter() - .filter(|c| c.org_uuid == org_uuid) + .filter(|c| &c.org_uuid == org_uuid) .collect() } - pub async fn find_by_organization(org_uuid: &str, conn: &mut DbConn) -> Vec { + pub async fn find_by_organization(org_uuid: &OrganizationId, conn: &mut DbConn) -> Vec { db_run! { conn: { collections::table .filter(collections::org_uuid.eq(org_uuid)) @@ -297,7 +301,7 @@ impl Collection { }} } - pub async fn count_by_org(org_uuid: &str, conn: &mut DbConn) -> i64 { + pub async fn count_by_org(org_uuid: &OrganizationId, conn: &mut DbConn) -> i64 { db_run! { conn: { collections::table .filter(collections::org_uuid.eq(org_uuid)) @@ -308,7 +312,7 @@ impl Collection { }} } - pub async fn find_by_uuid_and_org(uuid: &str, org_uuid: &str, conn: &mut DbConn) -> Option { + pub async fn find_by_uuid_and_org(uuid: &str, org_uuid: &OrganizationId, conn: &mut DbConn) -> Option { db_run! { conn: { collections::table .filter(collections::uuid.eq(uuid)) @@ -498,7 +502,11 @@ impl Collection { /// Database methods impl CollectionUser { - pub async fn find_by_organization_and_user_uuid(org_uuid: &str, user_uuid: &str, conn: &mut DbConn) -> Vec { + pub async fn find_by_organization_and_user_uuid( + org_uuid: &OrganizationId, + user_uuid: &str, + conn: &mut DbConn, + ) -> Vec { db_run! { conn: { users_collections::table .filter(users_collections::user_uuid.eq(user_uuid)) @@ -511,7 +519,7 @@ impl CollectionUser { }} } - pub async fn find_by_organization(org_uuid: &str, conn: &mut DbConn) -> Vec { + pub async fn find_by_organization(org_uuid: &OrganizationId, conn: &mut DbConn) -> Vec { db_run! { conn: { users_collections::table .inner_join(collections::table.on(collections::uuid.eq(users_collections::collection_uuid))) @@ -661,7 +669,11 @@ impl CollectionUser { }} } - pub async fn delete_all_by_user_and_org(user_uuid: &str, org_uuid: &str, conn: &mut DbConn) -> EmptyResult { + pub async fn delete_all_by_user_and_org( + user_uuid: &str, + org_uuid: &OrganizationId, + conn: &mut DbConn, + ) -> EmptyResult { let collectionusers = Self::find_by_organization_and_user_uuid(org_uuid, user_uuid, conn).await; db_run! { conn: { diff --git a/src/db/models/event.rs b/src/db/models/event.rs index 0f9e0e107f..3a225803a0 100644 --- a/src/db/models/event.rs +++ b/src/db/models/event.rs @@ -1,6 +1,7 @@ use crate::db::DbConn; use serde_json::Value; +use super::OrganizationId; use crate::{api::EmptyResult, error::MapResult, CONFIG}; use chrono::{NaiveDateTime, TimeDelta, Utc}; @@ -18,7 +19,7 @@ db_object! { pub uuid: String, pub event_type: i32, // EventType pub user_uuid: Option, - pub org_uuid: Option, + pub org_uuid: Option, pub cipher_uuid: Option, pub collection_uuid: Option, pub group_uuid: Option, diff --git a/src/db/models/group.rs b/src/db/models/group.rs index 06ae42469d..aeb8502aa3 100644 --- a/src/db/models/group.rs +++ b/src/db/models/group.rs @@ -1,4 +1,4 @@ -use super::{Membership, User}; +use super::{Membership, OrganizationId, User}; use crate::api::EmptyResult; use crate::db::DbConn; use crate::error::MapResult; @@ -11,7 +11,7 @@ db_object! { #[diesel(primary_key(uuid))] pub struct Group { pub uuid: String, - pub organizations_uuid: String, + pub organizations_uuid: OrganizationId, pub name: String, pub access_all: bool, pub external_id: Option, @@ -40,7 +40,12 @@ db_object! { /// Local methods impl Group { - pub fn new(organizations_uuid: String, name: String, access_all: bool, external_id: Option) -> Self { + pub fn new( + organizations_uuid: OrganizationId, + name: String, + access_all: bool, + external_id: Option, + ) -> Self { let now = Utc::now().naive_utc(); let mut new_model = Self { @@ -163,27 +168,27 @@ impl Group { } } - pub async fn delete_all_by_organization(org_uuid: &str, conn: &mut DbConn) -> EmptyResult { + pub async fn delete_all_by_organization(org_uuid: &OrganizationId, conn: &mut DbConn) -> EmptyResult { for group in Self::find_by_organization(org_uuid, conn).await { group.delete(conn).await?; } Ok(()) } - pub async fn find_by_organization(organizations_uuid: &str, conn: &mut DbConn) -> Vec { + pub async fn find_by_organization(org_uuid: &OrganizationId, conn: &mut DbConn) -> Vec { db_run! { conn: { groups::table - .filter(groups::organizations_uuid.eq(organizations_uuid)) + .filter(groups::organizations_uuid.eq(org_uuid)) .load::(conn) .expect("Error loading groups") .from_db() }} } - pub async fn count_by_org(organizations_uuid: &str, conn: &mut DbConn) -> i64 { + pub async fn count_by_org(org_uuid: &OrganizationId, conn: &mut DbConn) -> i64 { db_run! { conn: { groups::table - .filter(groups::organizations_uuid.eq(organizations_uuid)) + .filter(groups::organizations_uuid.eq(org_uuid)) .count() .first::(conn) .ok() @@ -191,7 +196,7 @@ impl Group { }} } - pub async fn find_by_uuid_and_org(uuid: &str, org_uuid: &str, conn: &mut DbConn) -> Option { + pub async fn find_by_uuid_and_org(uuid: &str, org_uuid: &OrganizationId, conn: &mut DbConn) -> Option { db_run! { conn: { groups::table .filter(groups::uuid.eq(uuid)) @@ -202,7 +207,11 @@ impl Group { }} } - pub async fn find_by_external_id_and_org(external_id: &str, org_uuid: &str, conn: &mut DbConn) -> Option { + pub async fn find_by_external_id_and_org( + external_id: &str, + org_uuid: &OrganizationId, + conn: &mut DbConn, + ) -> Option { db_run! { conn: { groups::table .filter(groups::external_id.eq(external_id)) @@ -213,7 +222,7 @@ impl Group { }} } //Returns all organizations the user has full access to - pub async fn get_orgs_by_user_with_full_access(user_uuid: &str, conn: &mut DbConn) -> Vec { + pub async fn get_orgs_by_user_with_full_access(user_uuid: &str, conn: &mut DbConn) -> Vec { db_run! { conn: { groups_users::table .inner_join(users_organizations::table.on( @@ -226,12 +235,12 @@ impl Group { .filter(groups::access_all.eq(true)) .select(groups::organizations_uuid) .distinct() - .load::(conn) + .load::(conn) .expect("Error loading organization group full access information for user") }} } - pub async fn is_in_full_access_group(user_uuid: &str, org_uuid: &str, conn: &mut DbConn) -> bool { + pub async fn is_in_full_access_group(user_uuid: &str, org_uuid: &OrganizationId, conn: &mut DbConn) -> bool { db_run! { conn: { groups::table .inner_join(groups_users::table.on( @@ -504,7 +513,7 @@ impl GroupUser { }} } - pub async fn has_full_access_by_member(org_uuid: &str, member_uuid: &str, conn: &mut DbConn) -> bool { + pub async fn has_full_access_by_member(org_uuid: &OrganizationId, member_uuid: &str, conn: &mut DbConn) -> bool { db_run! { conn: { groups_users::table .inner_join(groups::table.on( diff --git a/src/db/models/mod.rs b/src/db/models/mod.rs index b42ed6058c..69b7460edd 100644 --- a/src/db/models/mod.rs +++ b/src/db/models/mod.rs @@ -27,7 +27,9 @@ pub use self::favorite::Favorite; pub use self::folder::{Folder, FolderCipher}; pub use self::group::{CollectionGroup, Group, GroupUser}; pub use self::org_policy::{OrgPolicy, OrgPolicyErr, OrgPolicyType}; -pub use self::organization::{Membership, MembershipStatus, MembershipType, Organization, OrganizationApiKey}; +pub use self::organization::{ + Membership, MembershipStatus, MembershipType, Organization, OrganizationApiKey, OrganizationId, +}; pub use self::send::{Send, SendType}; pub use self::two_factor::{TwoFactor, TwoFactorType}; pub use self::two_factor_duo_context::TwoFactorDuoContext; diff --git a/src/db/models/org_policy.rs b/src/db/models/org_policy.rs index d5cb372c30..f0a99d2411 100644 --- a/src/db/models/org_policy.rs +++ b/src/db/models/org_policy.rs @@ -5,7 +5,7 @@ use crate::api::EmptyResult; use crate::db::DbConn; use crate::error::MapResult; -use super::{Membership, MembershipStatus, MembershipType, TwoFactor}; +use super::{Membership, MembershipStatus, MembershipType, OrganizationId, TwoFactor}; db_object! { #[derive(Identifiable, Queryable, Insertable, AsChangeset)] @@ -13,7 +13,7 @@ db_object! { #[diesel(primary_key(uuid))] pub struct OrgPolicy { pub uuid: String, - pub org_uuid: String, + pub org_uuid: OrganizationId, pub atype: i32, pub enabled: bool, pub data: String, @@ -62,7 +62,7 @@ pub enum OrgPolicyErr { /// Local methods impl OrgPolicy { - pub fn new(org_uuid: String, atype: OrgPolicyType, data: String) -> Self { + pub fn new(org_uuid: OrganizationId, atype: OrgPolicyType, data: String) -> Self { Self { uuid: crate::util::get_uuid(), org_uuid, @@ -142,7 +142,7 @@ impl OrgPolicy { }} } - pub async fn find_by_org(org_uuid: &str, conn: &mut DbConn) -> Vec { + pub async fn find_by_org(org_uuid: &OrganizationId, conn: &mut DbConn) -> Vec { db_run! { conn: { org_policies::table .filter(org_policies::org_uuid.eq(org_uuid)) @@ -170,7 +170,11 @@ impl OrgPolicy { }} } - pub async fn find_by_org_and_type(org_uuid: &str, policy_type: OrgPolicyType, conn: &mut DbConn) -> Option { + pub async fn find_by_org_and_type( + org_uuid: &OrganizationId, + policy_type: OrgPolicyType, + conn: &mut DbConn, + ) -> Option { db_run! { conn: { org_policies::table .filter(org_policies::org_uuid.eq(org_uuid)) @@ -181,7 +185,7 @@ impl OrgPolicy { }} } - pub async fn delete_all_by_organization(org_uuid: &str, conn: &mut DbConn) -> EmptyResult { + pub async fn delete_all_by_organization(org_uuid: &OrganizationId, conn: &mut DbConn) -> EmptyResult { db_run! { conn: { diesel::delete(org_policies::table.filter(org_policies::org_uuid.eq(org_uuid))) .execute(conn) @@ -246,14 +250,14 @@ impl OrgPolicy { pub async fn is_applicable_to_user( user_uuid: &str, policy_type: OrgPolicyType, - exclude_org_uuid: Option<&str>, + exclude_org_uuid: Option<&OrganizationId>, conn: &mut DbConn, ) -> bool { for policy in OrgPolicy::find_accepted_and_confirmed_by_user_and_active_policy(user_uuid, policy_type, conn).await { // Check if we need to skip this organization. - if exclude_org_uuid.is_some() && exclude_org_uuid.unwrap() == policy.org_uuid { + if exclude_org_uuid.is_some() && *exclude_org_uuid.unwrap() == policy.org_uuid { continue; } @@ -268,7 +272,7 @@ impl OrgPolicy { pub async fn is_user_allowed( user_uuid: &str, - org_uuid: &str, + org_uuid: &OrganizationId, exclude_current_org: bool, conn: &mut DbConn, ) -> OrgPolicyResult { @@ -296,7 +300,7 @@ impl OrgPolicy { Ok(()) } - pub async fn org_is_reset_password_auto_enroll(org_uuid: &str, conn: &mut DbConn) -> bool { + pub async fn org_is_reset_password_auto_enroll(org_uuid: &OrganizationId, conn: &mut DbConn) -> bool { match OrgPolicy::find_by_org_and_type(org_uuid, OrgPolicyType::ResetPassword, conn).await { Some(policy) => match serde_json::from_str::(&policy.data) { Ok(opts) => { diff --git a/src/db/models/organization.rs b/src/db/models/organization.rs index 7eacf57386..73a86b7f93 100644 --- a/src/db/models/organization.rs +++ b/src/db/models/organization.rs @@ -1,9 +1,13 @@ use chrono::{NaiveDateTime, Utc}; use num_traits::FromPrimitive; +use rocket::request::FromParam; use serde_json::Value; use std::{ + borrow::Borrow, cmp::Ordering, collections::{HashMap, HashSet}, + fmt::{Display, Formatter}, + ops::Deref, }; use super::{CollectionUser, Group, GroupUser, OrgPolicy, OrgPolicyType, TwoFactor, User}; @@ -15,7 +19,7 @@ db_object! { #[diesel(table_name = organizations)] #[diesel(primary_key(uuid))] pub struct Organization { - pub uuid: String, + pub uuid: OrganizationId, pub name: String, pub billing_email: String, pub private_key: Option, @@ -28,7 +32,7 @@ db_object! { pub struct Membership { pub uuid: String, pub user_uuid: String, - pub org_uuid: String, + pub org_uuid: OrganizationId, pub access_all: bool, pub akey: String, @@ -43,7 +47,7 @@ db_object! { #[diesel(primary_key(uuid, org_uuid))] pub struct OrganizationApiKey { pub uuid: String, - pub org_uuid: String, + pub org_uuid: OrganizationId, pub atype: i32, pub api_key: String, pub revision_date: NaiveDateTime, @@ -147,7 +151,7 @@ impl PartialOrd for i32 { impl Organization { pub fn new(name: String, billing_email: String, private_key: Option, public_key: Option) -> Self { Self { - uuid: crate::util::get_uuid(), + uuid: OrganizationId(crate::util::get_uuid()), name, billing_email, private_key, @@ -200,7 +204,7 @@ impl Organization { static ACTIVATE_REVOKE_DIFF: i32 = 128; impl Membership { - pub fn new(user_uuid: String, org_uuid: String) -> Self { + pub fn new(user_uuid: String, org_uuid: OrganizationId) -> Self { Self { uuid: crate::util::get_uuid(), @@ -255,7 +259,7 @@ impl Membership { } impl OrganizationApiKey { - pub fn new(org_uuid: String, api_key: String) -> Self { + pub fn new(org_uuid: OrganizationId, api_key: String) -> Self { Self { uuid: crate::util::get_uuid(), @@ -336,7 +340,7 @@ impl Organization { }} } - pub async fn find_by_uuid(uuid: &str, conn: &mut DbConn) -> Option { + pub async fn find_by_uuid(uuid: &OrganizationId, conn: &mut DbConn) -> Option { db_run! { conn: { organizations::table .filter(organizations::uuid.eq(uuid)) @@ -655,7 +659,7 @@ impl Membership { }} } - pub async fn delete_all_by_organization(org_uuid: &str, conn: &mut DbConn) -> EmptyResult { + pub async fn delete_all_by_organization(org_uuid: &OrganizationId, conn: &mut DbConn) -> EmptyResult { for member in Self::find_by_org(org_uuid, conn).await { member.delete(conn).await?; } @@ -669,9 +673,13 @@ impl Membership { Ok(()) } - pub async fn find_by_email_and_org(email: &str, org_id: &str, conn: &mut DbConn) -> Option { + pub async fn find_by_email_and_org( + email: &str, + org_uuid: &OrganizationId, + conn: &mut DbConn, + ) -> Option { if let Some(user) = User::find_by_mail(email, conn).await { - if let Some(member) = Membership::find_by_user_and_org(&user.uuid, org_id, conn).await { + if let Some(member) = Membership::find_by_user_and_org(&user.uuid, org_uuid, conn).await { return Some(member); } } @@ -700,7 +708,7 @@ impl Membership { }} } - pub async fn find_by_uuid_and_org(uuid: &str, org_uuid: &str, conn: &mut DbConn) -> Option { + pub async fn find_by_uuid_and_org(uuid: &str, org_uuid: &OrganizationId, conn: &mut DbConn) -> Option { db_run! { conn: { users_organizations::table .filter(users_organizations::uuid.eq(uuid)) @@ -750,7 +758,7 @@ impl Membership { }} } - pub async fn find_by_org(org_uuid: &str, conn: &mut DbConn) -> Vec { + pub async fn find_by_org(org_uuid: &OrganizationId, conn: &mut DbConn) -> Vec { db_run! { conn: { users_organizations::table .filter(users_organizations::org_uuid.eq(org_uuid)) @@ -759,7 +767,7 @@ impl Membership { }} } - pub async fn find_confirmed_by_org(org_uuid: &str, conn: &mut DbConn) -> Vec { + pub async fn find_confirmed_by_org(org_uuid: &OrganizationId, conn: &mut DbConn) -> Vec { db_run! { conn: { users_organizations::table .filter(users_organizations::org_uuid.eq(org_uuid)) @@ -769,7 +777,7 @@ impl Membership { }} } - pub async fn count_by_org(org_uuid: &str, conn: &mut DbConn) -> i64 { + pub async fn count_by_org(org_uuid: &OrganizationId, conn: &mut DbConn) -> i64 { db_run! { conn: { users_organizations::table .filter(users_organizations::org_uuid.eq(org_uuid)) @@ -780,7 +788,11 @@ impl Membership { }} } - pub async fn find_by_org_and_type(org_uuid: &str, atype: MembershipType, conn: &mut DbConn) -> Vec { + pub async fn find_by_org_and_type( + org_uuid: &OrganizationId, + atype: MembershipType, + conn: &mut DbConn, + ) -> Vec { db_run! { conn: { users_organizations::table .filter(users_organizations::org_uuid.eq(org_uuid)) @@ -790,7 +802,11 @@ impl Membership { }} } - pub async fn count_confirmed_by_org_and_type(org_uuid: &str, atype: MembershipType, conn: &mut DbConn) -> i64 { + pub async fn count_confirmed_by_org_and_type( + org_uuid: &OrganizationId, + atype: MembershipType, + conn: &mut DbConn, + ) -> i64 { db_run! { conn: { users_organizations::table .filter(users_organizations::org_uuid.eq(org_uuid)) @@ -802,7 +818,7 @@ impl Membership { }} } - pub async fn find_by_user_and_org(user_uuid: &str, org_uuid: &str, conn: &mut DbConn) -> Option { + pub async fn find_by_user_and_org(user_uuid: &str, org_uuid: &OrganizationId, conn: &mut DbConn) -> Option { db_run! { conn: { users_organizations::table .filter(users_organizations::user_uuid.eq(user_uuid)) @@ -812,7 +828,11 @@ impl Membership { }} } - pub async fn find_confirmed_by_user_and_org(user_uuid: &str, org_uuid: &str, conn: &mut DbConn) -> Option { + pub async fn find_confirmed_by_user_and_org( + user_uuid: &str, + org_uuid: &OrganizationId, + conn: &mut DbConn, + ) -> Option { db_run! { conn: { users_organizations::table .filter(users_organizations::user_uuid.eq(user_uuid)) @@ -834,12 +854,12 @@ impl Membership { }} } - pub async fn get_orgs_by_user(user_uuid: &str, conn: &mut DbConn) -> Vec { + pub async fn get_orgs_by_user(user_uuid: &str, conn: &mut DbConn) -> Vec { db_run! { conn: { users_organizations::table .filter(users_organizations::user_uuid.eq(user_uuid)) .select(users_organizations::org_uuid) - .load::(conn) + .load::(conn) .unwrap_or_default() }} } @@ -863,7 +883,7 @@ impl Membership { }} } - pub async fn find_by_cipher_and_org(cipher_uuid: &str, org_uuid: &str, conn: &mut DbConn) -> Vec { + pub async fn find_by_cipher_and_org(cipher_uuid: &str, org_uuid: &OrganizationId, conn: &mut DbConn) -> Vec { db_run! { conn: { users_organizations::table .filter(users_organizations::org_uuid.eq(org_uuid)) @@ -886,7 +906,11 @@ impl Membership { }} } - pub async fn find_by_cipher_and_org_with_group(cipher_uuid: &str, org_uuid: &str, conn: &mut DbConn) -> Vec { + pub async fn find_by_cipher_and_org_with_group( + cipher_uuid: &str, + org_uuid: &OrganizationId, + conn: &mut DbConn, + ) -> Vec { db_run! { conn: { users_organizations::table .filter(users_organizations::org_uuid.eq(org_uuid)) @@ -924,7 +948,11 @@ impl Membership { }} } - pub async fn find_by_collection_and_org(collection_uuid: &str, org_uuid: &str, conn: &mut DbConn) -> Vec { + pub async fn find_by_collection_and_org( + collection_uuid: &str, + org_uuid: &OrganizationId, + conn: &mut DbConn, + ) -> Vec { db_run! { conn: { users_organizations::table .filter(users_organizations::org_uuid.eq(org_uuid)) @@ -941,7 +969,11 @@ impl Membership { }} } - pub async fn find_by_external_id_and_org(ext_id: &str, org_uuid: &str, conn: &mut DbConn) -> Option { + pub async fn find_by_external_id_and_org( + ext_id: &str, + org_uuid: &OrganizationId, + conn: &mut DbConn, + ) -> Option { db_run! {conn: { users_organizations::table .filter( @@ -987,7 +1019,7 @@ impl OrganizationApiKey { } } - pub async fn find_by_org_uuid(org_uuid: &str, conn: &DbConn) -> Option { + pub async fn find_by_org_uuid(org_uuid: &OrganizationId, conn: &DbConn) -> Option { db_run! { conn: { organization_api_key::table .filter(organization_api_key::org_uuid.eq(org_uuid)) @@ -996,7 +1028,7 @@ impl OrganizationApiKey { }} } - pub async fn delete_all_by_organization(org_uuid: &str, conn: &mut DbConn) -> EmptyResult { + pub async fn delete_all_by_organization(org_uuid: &OrganizationId, conn: &mut DbConn) -> EmptyResult { db_run! { conn: { diesel::delete(organization_api_key::table.filter(organization_api_key::org_uuid.eq(org_uuid))) .execute(conn) @@ -1005,6 +1037,57 @@ impl OrganizationApiKey { } } +#[derive(DieselNewType, FromForm, Clone, Debug, Hash, PartialEq, Eq, Serialize, Deserialize)] +pub struct OrganizationId(String); + +impl AsRef for OrganizationId { + fn as_ref(&self) -> &str { + &self.0 + } +} + +impl Deref for OrganizationId { + type Target = str; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl Borrow for OrganizationId { + fn borrow(&self) -> &str { + &self.0 + } +} + +impl Display for OrganizationId { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.0) + } +} + +impl From for OrganizationId { + fn from(raw: String) -> Self { + Self(raw) + } +} + +impl<'r> FromParam<'r> for OrganizationId { + 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(OrganizationId(param.to_string())) + } else { + Err(()) + } + } +} + +#[derive(DieselNewType, Clone, Debug, Hash, PartialEq, Eq, Serialize)] +pub struct MembershipId(String); + #[cfg(test)] mod tests { use super::*; diff --git a/src/db/models/send.rs b/src/db/models/send.rs index fb95f86b8f..9e49ed5f42 100644 --- a/src/db/models/send.rs +++ b/src/db/models/send.rs @@ -3,7 +3,7 @@ use serde_json::Value; use crate::util::LowerCase; -use super::User; +use super::{OrganizationId, User}; db_object! { #[derive(Identifiable, Queryable, Insertable, AsChangeset)] @@ -14,8 +14,7 @@ db_object! { pub uuid: String, pub user_uuid: Option, - pub organization_uuid: Option, - + pub organization_uuid: Option, pub name: String, pub notes: Option, @@ -332,7 +331,7 @@ impl Send { Some(total) } - pub async fn find_by_org(org_uuid: &str, conn: &mut DbConn) -> Vec { + pub async fn find_by_org(org_uuid: &OrganizationId, conn: &mut DbConn) -> Vec { db_run! {conn: { sends::table .filter(sends::organization_uuid.eq(org_uuid)) diff --git a/src/mail.rs b/src/mail.rs index a8e250c8a4..e1c8c6daac 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, User}, + db::models::{Device, DeviceType, OrganizationId, User}, error::Error, CONFIG, }; @@ -259,7 +259,7 @@ pub async fn send_single_org_removed_from_org(address: &str, org_name: &str) -> pub async fn send_invite( user: &User, - org_id: Option, + org_id: Option, member_id: Option, org_name: &str, invited_by_email: Option, diff --git a/src/main.rs b/src/main.rs index 3a151cdf7f..530c7b2cd0 100644 --- a/src/main.rs +++ b/src/main.rs @@ -24,6 +24,8 @@ extern crate log; extern crate diesel; #[macro_use] extern crate diesel_migrations; +#[macro_use] +extern crate diesel_derive_newtype; use std::{ collections::HashMap, From 64ae0aa3864a4ff78c7f1413ac6bbab600d05f70 Mon Sep 17 00:00:00 2001 From: Stefan Melmuk Date: Sat, 21 Dec 2024 09:09:09 +0100 Subject: [PATCH 03/25] introduce newtype membership_id --- src/api/core/accounts.rs | 4 +- src/api/core/events.rs | 6 +- src/api/core/organizations.rs | 211 ++++++++++++++++++---------------- src/auth.rs | 6 +- src/db/models/group.rs | 28 +++-- src/db/models/mod.rs | 2 +- src/db/models/org_policy.rs | 8 +- src/db/models/organization.rs | 63 ++++++++-- src/mail.rs | 4 +- 9 files changed, 204 insertions(+), 128 deletions(-) diff --git a/src/api/core/accounts.rs b/src/api/core/accounts.rs index 39654d795f..26986c64b3 100644 --- a/src/api/core/accounts.rs +++ b/src/api/core/accounts.rs @@ -79,7 +79,7 @@ pub struct RegisterData { name: Option, token: Option, #[allow(dead_code)] - organization_user_id: Option, + organization_user_id: Option, } #[derive(Debug, Deserialize)] @@ -106,7 +106,7 @@ fn enforce_password_hint_setting(password_hint: &Option) -> EmptyResult } Ok(()) } -async fn is_email_2fa_required(member_uuid: Option, conn: &mut DbConn) -> bool { +async fn is_email_2fa_required(member_uuid: Option, conn: &mut DbConn) -> bool { if !CONFIG._enable_email_2fa() { return false; } diff --git a/src/api/core/events.rs b/src/api/core/events.rs index a0172ebbfb..9bd1c8068f 100644 --- a/src/api/core/events.rs +++ b/src/api/core/events.rs @@ -8,7 +8,7 @@ use crate::{ api::{EmptyResult, JsonResult}, auth::{AdminHeaders, Headers}, db::{ - models::{Cipher, Event, Membership, OrganizationId}, + models::{Cipher, Event, Membership, MembershipId, OrganizationId}, DbConn, DbPool, }, util::parse_date, @@ -93,7 +93,7 @@ async fn get_cipher_events(cipher_id: &str, data: EventRange, headers: Headers, #[get("/organizations//users//events?")] async fn get_user_events( org_id: &str, - member_id: &str, + member_id: MembershipId, data: EventRange, _headers: AdminHeaders, mut conn: DbConn, @@ -110,7 +110,7 @@ async fn get_user_events( parse_date(&data.end) }; - Event::find_by_org_and_member(org_id, member_id, &start_date, &end_date, &mut conn) + Event::find_by_org_and_member(org_id, &member_id, &start_date, &end_date, &mut conn) .await .iter() .map(|e| e.to_json()) diff --git a/src/api/core/organizations.rs b/src/api/core/organizations.rs index bcfd29a63e..ffefcd7623 100644 --- a/src/api/core/organizations.rs +++ b/src/api/core/organizations.rs @@ -125,7 +125,7 @@ struct OrganizationUpdateData { struct NewCollectionData { name: String, groups: Vec, - users: Vec, + users: Vec, id: Option, external_id: Option, } @@ -138,6 +138,14 @@ struct NewCollectionObjectData { read_only: bool, } +#[derive(Deserialize)] +#[serde(rename_all = "camelCase")] +struct NewCollectionMemberData { + hide_passwords: bool, + id: MembershipId, + read_only: bool, +} + #[derive(Deserialize)] #[serde(rename_all = "camelCase")] struct OrgKeyData { @@ -151,6 +159,12 @@ struct OrgBulkIds { ids: Vec, } +#[derive(Deserialize, Debug)] +#[serde(rename_all = "camelCase")] +struct BulkMembershipIds { + ids: Vec, +} + #[post("/organizations", data = "")] async fn create_organization(headers: Headers, data: Json, mut conn: DbConn) -> JsonResult { if !CONFIG.is_org_creation_allowed(&headers.user.email) { @@ -510,7 +524,7 @@ async fn post_organization_collection_update( async fn delete_organization_collection_user( org_id: OrganizationId, col_id: &str, - member_id: &str, + member_id: MembershipId, _headers: AdminHeaders, mut conn: DbConn, ) -> EmptyResult { @@ -518,7 +532,7 @@ async fn delete_organization_collection_user( err!("Collection not found", "Collection does not exist or does not belong to this organization") }; - match Membership::find_by_uuid_and_org(member_id, &org_id, &mut conn).await { + match Membership::find_by_uuid_and_org(&member_id, &org_id, &mut conn).await { None => err!("User not found in organization"), Some(member) => { match CollectionUser::find_by_collection_and_user(&collection.uuid, &member.user_uuid, &mut conn).await { @@ -533,7 +547,7 @@ async fn delete_organization_collection_user( async fn post_organization_collection_delete_user( org_id: OrganizationId, col_id: &str, - member_id: &str, + member_id: MembershipId, headers: AdminHeaders, conn: DbConn, ) -> EmptyResult { @@ -827,7 +841,7 @@ async fn post_org_keys( #[derive(Deserialize)] #[serde(rename_all = "camelCase")] struct CollectionData { - id: String, + id: MembershipId, read_only: bool, hide_passwords: bool, } @@ -895,11 +909,11 @@ async fn send_invite( } }; - let mut new_user = Membership::new(user.uuid.clone(), org_id.clone()); + let mut new_member = Membership::new(user.uuid.clone(), org_id.clone()); let access_all = data.access_all; - new_user.access_all = access_all; - new_user.atype = new_type; - new_user.status = member_status; + new_member.access_all = access_all; + new_member.atype = new_type; + new_member.status = member_status; // If no accessAll, add the collections received if !access_all { @@ -920,16 +934,16 @@ async fn send_invite( } } - new_user.save(&mut conn).await?; + new_member.save(&mut conn).await?; for group in data.groups.iter() { - let mut group_entry = GroupUser::new(String::from(group), user.uuid.clone()); + let mut group_entry = GroupUser::new(String::from(group), new_member.uuid.clone()); group_entry.save(&mut conn).await?; } log_event( EventType::OrganizationUserInvited as i32, - &new_user.uuid, + &new_member.uuid, &org_id, &headers.user.uuid, headers.device.atype, @@ -947,7 +961,7 @@ async fn send_invite( mail::send_invite( &user, Some(org_id.clone()), - Some(new_user.uuid), + Some(new_member.uuid), &org_name, Some(headers.user.email.clone()), ) @@ -961,11 +975,11 @@ async fn send_invite( #[post("/organizations//users/reinvite", data = "")] async fn bulk_reinvite_user( org_id: OrganizationId, - data: Json, + data: Json, headers: AdminHeaders, mut conn: DbConn, ) -> Json { - let data: OrgBulkIds = data.into_inner(); + let data: BulkMembershipIds = data.into_inner(); let mut bulk_response = Vec::new(); for member_id in data.ids { @@ -990,18 +1004,23 @@ async fn bulk_reinvite_user( })) } -#[post("/organizations//users//reinvite")] -async fn reinvite_user(org_id: OrganizationId, member: &str, headers: AdminHeaders, mut conn: DbConn) -> EmptyResult { - _reinvite_user(&org_id, member, &headers.user.email, &mut conn).await +#[post("/organizations//users//reinvite")] +async fn reinvite_user( + org_id: OrganizationId, + member_id: MembershipId, + headers: AdminHeaders, + mut conn: DbConn, +) -> EmptyResult { + _reinvite_user(&org_id, &member_id, &headers.user.email, &mut conn).await } async fn _reinvite_user( org_id: &OrganizationId, - member: &str, + member_id: &MembershipId, invited_by_email: &str, conn: &mut DbConn, ) -> EmptyResult { - let Some(member) = Membership::find_by_uuid_and_org(member, org_id, conn).await else { + let Some(member) = Membership::find_by_uuid_and_org(member_id, org_id, conn).await else { err!("The user hasn't been invited to the organization.") }; @@ -1054,7 +1073,7 @@ struct AcceptData { #[post("/organizations//users//accept", data = "")] async fn accept_invite( org_id: OrganizationId, - member_id: &str, + member_id: MembershipId, data: Json, mut conn: DbConn, ) -> EmptyResult { @@ -1064,7 +1083,7 @@ async fn accept_invite( // If a claim does not have a member_id or it does not match the one in from the URI, something is wrong. match &claims.member_id { - Some(ou_id) if ou_id.eq(member_id) => {} + Some(ou_id) if ou_id.eq(&member_id) => {} _ => err!("Error accepting the invitation", "Claim does not match the member_id"), } @@ -1139,7 +1158,7 @@ async fn accept_invite( #[derive(Deserialize)] #[serde(rename_all = "camelCase")] struct ConfirmData { - id: Option, + id: Option, key: Option, } @@ -1163,7 +1182,7 @@ async fn bulk_confirm_invite( match data.keys { Some(keys) => { for invite in keys { - let member_id = invite.id.unwrap_or_default(); + let member_id = invite.id.unwrap(); let user_key = invite.key.unwrap_or_default(); let err_msg = match _confirm_invite(&org_id, &member_id, &user_key, &headers, &mut conn, &nt).await { Ok(_) => String::new(), @@ -1192,7 +1211,7 @@ async fn bulk_confirm_invite( #[post("/organizations//users//confirm", data = "")] async fn confirm_invite( org_id: OrganizationId, - member_id: &str, + member_id: MembershipId, data: Json, headers: AdminHeaders, mut conn: DbConn, @@ -1200,12 +1219,12 @@ async fn confirm_invite( ) -> EmptyResult { let data = data.into_inner(); let user_key = data.key.unwrap_or_default(); - _confirm_invite(&org_id, member_id, &user_key, &headers, &mut conn, &nt).await + _confirm_invite(&org_id, &member_id, &user_key, &headers, &mut conn, &nt).await } async fn _confirm_invite( org_id: &OrganizationId, - member_id: &str, + member_id: &MembershipId, key: &str, headers: &AdminHeaders, conn: &mut DbConn, @@ -1283,12 +1302,12 @@ async fn _confirm_invite( #[get("/organizations//users/?")] async fn get_user( org_id: OrganizationId, - member_id: &str, + member_id: MembershipId, data: GetOrgUserData, _headers: AdminHeaders, mut conn: DbConn, ) -> JsonResult { - let Some(user) = Membership::find_by_uuid_and_org(member_id, &org_id, &mut conn).await else { + let Some(user) = Membership::find_by_uuid_and_org(&member_id, &org_id, &mut conn).await else { err!("The specified user isn't a member of the organization") }; @@ -1313,7 +1332,7 @@ struct EditUserData { #[put("/organizations//users/", data = "", rank = 1)] async fn put_membership( org_id: OrganizationId, - member_id: &str, + member_id: MembershipId, data: Json, headers: AdminHeaders, conn: DbConn, @@ -1324,7 +1343,7 @@ async fn put_membership( #[post("/organizations//users/", data = "", rank = 1)] async fn edit_user( org_id: OrganizationId, - member_id: &str, + member_id: MembershipId, data: Json, headers: AdminHeaders, mut conn: DbConn, @@ -1335,7 +1354,7 @@ async fn edit_user( err!("Invalid type") }; - let Some(mut member_to_edit) = Membership::find_by_uuid_and_org(member_id, &org_id, &mut conn).await else { + let Some(mut member_to_edit) = Membership::find_by_uuid_and_org(&member_id, &org_id, &mut conn).await else { err!("The specified user isn't member of the organization") }; @@ -1429,12 +1448,12 @@ async fn edit_user( #[delete("/organizations//users", data = "")] async fn bulk_delete_user( org_id: OrganizationId, - data: Json, + data: Json, headers: AdminHeaders, mut conn: DbConn, nt: Notify<'_>, ) -> Json { - let data: OrgBulkIds = data.into_inner(); + let data: BulkMembershipIds = data.into_inner(); let mut bulk_response = Vec::new(); for member_id in data.ids { @@ -1462,28 +1481,28 @@ async fn bulk_delete_user( #[delete("/organizations//users/")] async fn delete_user( org_id: OrganizationId, - member_id: &str, + member_id: MembershipId, headers: AdminHeaders, mut conn: DbConn, nt: Notify<'_>, ) -> EmptyResult { - _delete_user(&org_id, member_id, &headers, &mut conn, &nt).await + _delete_user(&org_id, &member_id, &headers, &mut conn, &nt).await } #[post("/organizations//users//delete")] async fn post_delete_user( org_id: OrganizationId, - member_id: &str, + member_id: MembershipId, headers: AdminHeaders, mut conn: DbConn, nt: Notify<'_>, ) -> EmptyResult { - _delete_user(&org_id, member_id, &headers, &mut conn, &nt).await + _delete_user(&org_id, &member_id, &headers, &mut conn, &nt).await } async fn _delete_user( org_id: &OrganizationId, - member_id: &str, + member_id: &MembershipId, headers: &AdminHeaders, conn: &mut DbConn, nt: &Notify<'_>, @@ -1525,11 +1544,11 @@ async fn _delete_user( #[post("/organizations//users/public-keys", data = "")] async fn bulk_public_keys( org_id: OrganizationId, - data: Json, + data: Json, _headers: AdminHeaders, mut conn: DbConn, ) -> Json { - let data: OrgBulkIds = data.into_inner(); + let data: BulkMembershipIds = data.into_inner(); let mut bulk_response = Vec::new(); // Check all received Membership UUID's and find the matching User to retrieve the public-key. @@ -2074,18 +2093,24 @@ async fn import(org_id: OrganizationId, data: Json, headers: Head #[put("/organizations//users//deactivate")] async fn deactivate_membership( org_id: OrganizationId, - member_id: &str, + member_id: MembershipId, headers: AdminHeaders, mut conn: DbConn, ) -> EmptyResult { - _revoke_membership(&org_id, member_id, &headers, &mut conn).await + _revoke_membership(&org_id, &member_id, &headers, &mut conn).await +} + +#[derive(Deserialize, Debug)] +#[serde(rename_all = "camelCase")] +struct BulkRevokeMembershipIds { + ids: Option>, } // Pre web-vault v2022.9.x endpoint #[put("/organizations//users/deactivate", data = "")] async fn bulk_deactivate_membership( org_id: OrganizationId, - data: Json, + data: Json, headers: AdminHeaders, conn: DbConn, ) -> Json { @@ -2095,23 +2120,17 @@ async fn bulk_deactivate_membership( #[put("/organizations//users//revoke")] async fn revoke_membership( org_id: OrganizationId, - member_id: &str, + member_id: MembershipId, headers: AdminHeaders, mut conn: DbConn, ) -> EmptyResult { - _revoke_membership(&org_id, member_id, &headers, &mut conn).await -} - -#[derive(Deserialize, Debug)] -#[serde(rename_all = "camelCase")] -struct OrgBulkRevokeData { - ids: Option>, + _revoke_membership(&org_id, &member_id, &headers, &mut conn).await } #[put("/organizations//users/revoke", data = "")] async fn bulk_revoke_membership( org_id: OrganizationId, - data: Json, + data: Json, headers: AdminHeaders, mut conn: DbConn, ) -> Json { @@ -2147,7 +2166,7 @@ async fn bulk_revoke_membership( async fn _revoke_membership( org_id: &OrganizationId, - member_id: &str, + member_id: &MembershipId, headers: &AdminHeaders, conn: &mut DbConn, ) -> EmptyResult { @@ -2189,18 +2208,18 @@ async fn _revoke_membership( #[put("/organizations//users//activate")] async fn activate_membership( org_id: OrganizationId, - member_id: &str, + member_id: MembershipId, headers: AdminHeaders, mut conn: DbConn, ) -> EmptyResult { - _restore_membership(&org_id, member_id, &headers, &mut conn).await + _restore_membership(&org_id, &member_id, &headers, &mut conn).await } // Pre web-vault v2022.9.x endpoint #[put("/organizations//users/activate", data = "")] async fn bulk_activate_membership( org_id: OrganizationId, - data: Json, + data: Json, headers: AdminHeaders, conn: DbConn, ) -> Json { @@ -2210,17 +2229,17 @@ async fn bulk_activate_membership( #[put("/organizations//users//restore")] async fn restore_membership( org_id: OrganizationId, - member_id: &str, + member_id: MembershipId, headers: AdminHeaders, mut conn: DbConn, ) -> EmptyResult { - _restore_membership(&org_id, member_id, &headers, &mut conn).await + _restore_membership(&org_id, &member_id, &headers, &mut conn).await } #[put("/organizations//users/restore", data = "")] async fn bulk_restore_membership( org_id: OrganizationId, - data: Json, + data: Json, headers: AdminHeaders, mut conn: DbConn, ) -> Json { @@ -2251,7 +2270,7 @@ async fn bulk_restore_membership( async fn _restore_membership( org_id: &OrganizationId, - member_id: &str, + member_id: &MembershipId, headers: &AdminHeaders, conn: &mut DbConn, ) -> EmptyResult { @@ -2334,7 +2353,7 @@ struct GroupRequest { access_all: bool, external_id: Option, collections: Vec, - users: Vec, + users: Vec, } impl GroupRequest { @@ -2464,7 +2483,7 @@ async fn put_group( async fn add_update_group( mut group: Group, collections: Vec, - users: Vec, + members: Vec, org_id: OrganizationId, headers: &AdminHeaders, conn: &mut DbConn, @@ -2476,13 +2495,13 @@ async fn add_update_group( collection_group.save(conn).await?; } - for assigned_user_id in users { - let mut user_entry = GroupUser::new(group.uuid.clone(), assigned_user_id.clone()); + for assigned_member in members { + let mut user_entry = GroupUser::new(group.uuid.clone(), assigned_member.clone()); user_entry.save(conn).await?; log_event( EventType::OrganizationUserUpdatedGroups as i32, - &assigned_user_id, + &assigned_member, &org_id, &headers.user.uuid, headers.device.atype, @@ -2609,7 +2628,7 @@ async fn get_group_users( err!("Group could not be found!", "Group uuid is invalid or does not belong to the organization") }; - let group_users: Vec = GroupUser::find_by_group(group_id, &mut conn) + let group_users: Vec = GroupUser::find_by_group(group_id, &mut conn) .await .iter() .map(|entry| entry.users_organizations_uuid.clone()) @@ -2623,7 +2642,7 @@ async fn put_group_users( org_id: OrganizationId, group_id: &str, headers: AdminHeaders, - data: Json>, + data: Json>, mut conn: DbConn, ) -> EmptyResult { if !CONFIG.org_groups_enabled() { @@ -2636,14 +2655,14 @@ async fn put_group_users( GroupUser::delete_all_by_group(group_id, &mut conn).await?; - let assigned_user_ids = data.into_inner(); - for assigned_user_id in assigned_user_ids { - let mut user_entry = GroupUser::new(String::from(group_id), assigned_user_id.clone()); + let assigned_members = data.into_inner(); + for assigned_member in assigned_members { + let mut user_entry = GroupUser::new(String::from(group_id), assigned_member.clone()); user_entry.save(&mut conn).await?; log_event( EventType::OrganizationUserUpdatedGroups as i32, - &assigned_user_id, + &assigned_member, &org_id, &headers.user.uuid, headers.device.atype, @@ -2656,10 +2675,10 @@ async fn put_group_users( Ok(()) } -#[get("/organizations//users//groups")] +#[get("/organizations//users//groups")] async fn get_user_groups( org_id: OrganizationId, - user_id: &str, + member_id: MembershipId, _headers: AdminHeaders, mut conn: DbConn, ) -> JsonResult { @@ -2667,12 +2686,12 @@ async fn get_user_groups( err!("Group support is disabled"); } - if Membership::find_by_uuid_and_org(user_id, &org_id, &mut conn).await.is_none() { + if Membership::find_by_uuid_and_org(&member_id, &org_id, &mut conn).await.is_none() { err!("User could not be found!") }; let user_groups: Vec = - GroupUser::find_by_user(user_id, &mut conn).await.iter().map(|entry| entry.groups_uuid.clone()).collect(); + GroupUser::find_by_member(&member_id, &mut conn).await.iter().map(|entry| entry.groups_uuid.clone()).collect(); Ok(Json(json!(user_groups))) } @@ -2686,7 +2705,7 @@ struct OrganizationUserUpdateGroupsRequest { #[post("/organizations//users//groups", data = "")] async fn post_user_groups( org_id: OrganizationId, - member_id: &str, + member_id: MembershipId, data: Json, headers: AdminHeaders, conn: DbConn, @@ -2697,7 +2716,7 @@ async fn post_user_groups( #[put("/organizations//users//groups", data = "")] async fn put_user_groups( org_id: OrganizationId, - member_id: &str, + member_id: MembershipId, data: Json, headers: AdminHeaders, mut conn: DbConn, @@ -2706,7 +2725,7 @@ async fn put_user_groups( err!("Group support is disabled"); } - if Membership::find_by_uuid_and_org(member_id, &org_id, &mut conn).await.is_none() { + if Membership::find_by_uuid_and_org(&member_id, &org_id, &mut conn).await.is_none() { err!("User could not be found or does not belong to the organization."); } @@ -2714,13 +2733,13 @@ async fn put_user_groups( let assigned_group_ids = data.into_inner(); for assigned_group_id in assigned_group_ids.group_ids { - let mut group_user = GroupUser::new(assigned_group_id.clone(), String::from(member_id)); + let mut group_user = GroupUser::new(assigned_group_id.clone(), member_id.clone()); group_user.save(&mut conn).await?; } log_event( EventType::OrganizationUserUpdatedGroups as i32, - member_id, + &member_id, &org_id, &headers.user.uuid, headers.device.atype, @@ -2736,7 +2755,7 @@ async fn put_user_groups( async fn post_delete_group_user( org_id: OrganizationId, group_id: &str, - member_id: &str, + member_id: MembershipId, headers: AdminHeaders, conn: DbConn, ) -> EmptyResult { @@ -2747,7 +2766,7 @@ async fn post_delete_group_user( async fn delete_group_user( org_id: OrganizationId, group_id: &str, - member_id: &str, + member_id: MembershipId, headers: AdminHeaders, mut conn: DbConn, ) -> EmptyResult { @@ -2755,7 +2774,7 @@ async fn delete_group_user( err!("Group support is disabled"); } - if Membership::find_by_uuid_and_org(member_id, &org_id, &mut conn).await.is_none() { + if Membership::find_by_uuid_and_org(&member_id, &org_id, &mut conn).await.is_none() { err!("User could not be found or does not belong to the organization."); } @@ -2765,7 +2784,7 @@ async fn delete_group_user( log_event( EventType::OrganizationUserUpdatedGroups as i32, - member_id, + &member_id, &org_id, &headers.user.uuid, headers.device.atype, @@ -2774,7 +2793,7 @@ async fn delete_group_user( ) .await; - GroupUser::delete_by_group_id_and_user_id(group_id, member_id, &mut conn).await + GroupUser::delete_by_group_and_member(group_id, &member_id, &mut conn).await } #[derive(Deserialize)] @@ -2817,7 +2836,7 @@ async fn get_organization_keys(org_id: OrganizationId, headers: Headers, conn: D #[put("/organizations//users//reset-password", data = "")] async fn put_reset_password( org_id: OrganizationId, - member_id: &str, + member_id: MembershipId, headers: AdminHeaders, data: Json, mut conn: DbConn, @@ -2827,7 +2846,7 @@ async fn put_reset_password( err!("Required organization not found") }; - let Some(member) = Membership::find_by_uuid_and_org(member_id, &org.uuid, &mut conn).await else { + let Some(member) = Membership::find_by_uuid_and_org(&member_id, &org.uuid, &mut conn).await else { err!("User to reset isn't member of required organization") }; @@ -2835,7 +2854,7 @@ async fn put_reset_password( err!("User not found") }; - check_reset_password_applicable_and_permissions(&org_id, member_id, &headers, &mut conn).await?; + check_reset_password_applicable_and_permissions(&org_id, &member_id, &headers, &mut conn).await?; if member.reset_password_key.is_none() { err!("Password reset not or not correctly enrolled"); @@ -2860,7 +2879,7 @@ async fn put_reset_password( log_event( EventType::OrganizationUserAdminResetPassword as i32, - member_id, + &member_id, &org_id, &headers.user.uuid, headers.device.atype, @@ -2875,7 +2894,7 @@ async fn put_reset_password( #[get("/organizations//users//reset-password-details")] async fn get_reset_password_details( org_id: OrganizationId, - member_id: &str, + member_id: MembershipId, headers: AdminHeaders, mut conn: DbConn, ) -> JsonResult { @@ -2883,7 +2902,7 @@ async fn get_reset_password_details( err!("Required organization not found") }; - let Some(member) = Membership::find_by_uuid_and_org(member_id, &org_id, &mut conn).await else { + let Some(member) = Membership::find_by_uuid_and_org(&member_id, &org_id, &mut conn).await else { err!("User to reset isn't member of required organization") }; @@ -2891,7 +2910,7 @@ async fn get_reset_password_details( err!("User not found") }; - check_reset_password_applicable_and_permissions(&org_id, member_id, &headers, &mut conn).await?; + check_reset_password_applicable_and_permissions(&org_id, &member_id, &headers, &mut conn).await?; // https://github.com/bitwarden/server/blob/3b50ccb9f804efaacdc46bed5b60e5b28eddefcf/src/Api/Models/Response/Organizations/OrganizationUserResponseModel.cs#L111 Ok(Json(json!({ @@ -2908,7 +2927,7 @@ async fn get_reset_password_details( async fn check_reset_password_applicable_and_permissions( org_id: &OrganizationId, - member_id: &str, + member_id: &MembershipId, headers: &AdminHeaders, conn: &mut DbConn, ) -> EmptyResult { @@ -2945,7 +2964,7 @@ async fn check_reset_password_applicable(org_id: &OrganizationId, conn: &mut DbC #[put("/organizations//users//reset-password-enrollment", data = "")] async fn put_reset_password_enrollment( org_id: OrganizationId, - member_id: &str, + member_id: MembershipId, headers: Headers, data: Json, mut conn: DbConn, @@ -2982,7 +3001,7 @@ async fn put_reset_password_enrollment( EventType::OrganizationUserResetPasswordWithdraw as i32 }; - log_event(log_id, member_id, &org_id, &headers.user.uuid, headers.device.atype, &headers.ip.ip, &mut conn).await; + log_event(log_id, &member_id, &org_id, &headers.user.uuid, headers.device.atype, &headers.ip.ip, &mut conn).await; Ok(()) } diff --git a/src/auth.rs b/src/auth.rs index a2cbc9eef7..e09d616168 100644 --- a/src/auth.rs +++ b/src/auth.rs @@ -14,7 +14,7 @@ use std::{ net::IpAddr, }; -use crate::db::models::OrganizationId; +use crate::db::models::{MembershipId, OrganizationId}; use crate::{error::Error, CONFIG}; const JWT_ALGORITHM: Algorithm = Algorithm::RS256; @@ -192,7 +192,7 @@ pub struct InviteJwtClaims { pub email: String, pub org_id: Option, - pub member_id: Option, + pub member_id: Option, pub invited_by_email: Option, } @@ -200,7 +200,7 @@ pub fn generate_invite_claims( uuid: String, email: String, org_id: Option, - member_id: Option, + member_id: Option, invited_by_email: Option, ) -> InviteJwtClaims { let time_now = Utc::now(); diff --git a/src/db/models/group.rs b/src/db/models/group.rs index aeb8502aa3..c0a317616d 100644 --- a/src/db/models/group.rs +++ b/src/db/models/group.rs @@ -1,4 +1,4 @@ -use super::{Membership, OrganizationId, User}; +use super::{Membership, MembershipId, OrganizationId, User}; use crate::api::EmptyResult; use crate::db::DbConn; use crate::error::MapResult; @@ -34,7 +34,7 @@ db_object! { #[diesel(primary_key(groups_uuid, users_organizations_uuid))] pub struct GroupUser { pub groups_uuid: String, - pub users_organizations_uuid: String + pub users_organizations_uuid: MembershipId } } @@ -124,7 +124,7 @@ impl CollectionGroup { } impl GroupUser { - pub fn new(groups_uuid: String, users_organizations_uuid: String) -> Self { + pub fn new(groups_uuid: String, users_organizations_uuid: MembershipId) -> Self { Self { groups_uuid, users_organizations_uuid, @@ -485,10 +485,10 @@ impl GroupUser { }} } - pub async fn find_by_user(users_organizations_uuid: &str, conn: &mut DbConn) -> Vec { + pub async fn find_by_member(member_uuid: &MembershipId, conn: &mut DbConn) -> Vec { db_run! { conn: { groups_users::table - .filter(groups_users::users_organizations_uuid.eq(users_organizations_uuid)) + .filter(groups_users::users_organizations_uuid.eq(member_uuid)) .load::(conn) .expect("Error loading groups for user") .from_db() @@ -497,7 +497,7 @@ impl GroupUser { pub async fn has_access_to_collection_by_member( collection_uuid: &str, - member_uuid: &str, + member_uuid: &MembershipId, conn: &mut DbConn, ) -> bool { db_run! { conn: { @@ -513,7 +513,11 @@ impl GroupUser { }} } - pub async fn has_full_access_by_member(org_uuid: &OrganizationId, member_uuid: &str, conn: &mut DbConn) -> bool { + pub async fn has_full_access_by_member( + org_uuid: &OrganizationId, + member_uuid: &MembershipId, + conn: &mut DbConn, + ) -> bool { db_run! { conn: { groups_users::table .inner_join(groups::table.on( @@ -535,12 +539,12 @@ impl GroupUser { } } - pub async fn delete_by_group_id_and_user_id( + pub async fn delete_by_group_and_member( group_uuid: &str, - users_organizations_uuid: &str, + member_uuid: &MembershipId, conn: &mut DbConn, ) -> EmptyResult { - match Membership::find_by_uuid(users_organizations_uuid, conn).await { + match Membership::find_by_uuid(member_uuid, conn).await { Some(member) => User::update_uuid_revision(&member.user_uuid, conn).await, None => warn!("Member could not be found!"), }; @@ -548,7 +552,7 @@ impl GroupUser { db_run! { conn: { diesel::delete(groups_users::table) .filter(groups_users::groups_uuid.eq(group_uuid)) - .filter(groups_users::users_organizations_uuid.eq(users_organizations_uuid)) + .filter(groups_users::users_organizations_uuid.eq(member_uuid)) .execute(conn) .map_res("Error deleting group users") }} @@ -568,7 +572,7 @@ impl GroupUser { }} } - pub async fn delete_all_by_member(member_uuid: &str, conn: &mut DbConn) -> EmptyResult { + pub async fn delete_all_by_member(member_uuid: &MembershipId, conn: &mut DbConn) -> EmptyResult { match Membership::find_by_uuid(member_uuid, conn).await { Some(member) => User::update_uuid_revision(&member.user_uuid, conn).await, None => warn!("Member could not be found!"), diff --git a/src/db/models/mod.rs b/src/db/models/mod.rs index 69b7460edd..fe61e42fe0 100644 --- a/src/db/models/mod.rs +++ b/src/db/models/mod.rs @@ -28,7 +28,7 @@ pub use self::folder::{Folder, FolderCipher}; pub use self::group::{CollectionGroup, Group, GroupUser}; pub use self::org_policy::{OrgPolicy, OrgPolicyErr, OrgPolicyType}; pub use self::organization::{ - Membership, MembershipStatus, MembershipType, Organization, OrganizationApiKey, OrganizationId, + Membership, MembershipId, MembershipStatus, MembershipType, Organization, OrganizationApiKey, OrganizationId, }; pub use self::send::{Send, SendType}; pub use self::two_factor::{TwoFactor, TwoFactorType}; diff --git a/src/db/models/org_policy.rs b/src/db/models/org_policy.rs index f0a99d2411..c63fa16d52 100644 --- a/src/db/models/org_policy.rs +++ b/src/db/models/org_policy.rs @@ -5,7 +5,7 @@ use crate::api::EmptyResult; use crate::db::DbConn; use crate::error::MapResult; -use super::{Membership, MembershipStatus, MembershipType, OrganizationId, TwoFactor}; +use super::{Membership, MembershipId, MembershipStatus, MembershipType, OrganizationId, TwoFactor}; db_object! { #[derive(Identifiable, Queryable, Insertable, AsChangeset)] @@ -336,7 +336,11 @@ impl OrgPolicy { false } - pub async fn is_enabled_for_member(member_uuid: &str, policy_type: OrgPolicyType, conn: &mut DbConn) -> bool { + pub async fn is_enabled_for_member( + member_uuid: &MembershipId, + policy_type: OrgPolicyType, + conn: &mut DbConn, + ) -> bool { if let Some(member) = Membership::find_by_uuid(member_uuid, conn).await { if let Some(policy) = OrgPolicy::find_by_org_and_type(&member.org_uuid, policy_type, conn).await { return policy.enabled; diff --git a/src/db/models/organization.rs b/src/db/models/organization.rs index 73a86b7f93..7a3657edf7 100644 --- a/src/db/models/organization.rs +++ b/src/db/models/organization.rs @@ -30,7 +30,7 @@ db_object! { #[diesel(table_name = users_organizations)] #[diesel(primary_key(uuid))] pub struct Membership { - pub uuid: String, + pub uuid: MembershipId, pub user_uuid: String, pub org_uuid: OrganizationId, @@ -206,7 +206,7 @@ static ACTIVATE_REVOKE_DIFF: i32 = 128; impl Membership { pub fn new(user_uuid: String, org_uuid: OrganizationId) -> Self { Self { - uuid: crate::util::get_uuid(), + uuid: MembershipId(crate::util::get_uuid()), user_uuid, org_uuid, @@ -459,7 +459,7 @@ impl Membership { let twofactor_enabled = !TwoFactor::find_by_user(&user.uuid, conn).await.is_empty(); let groups: Vec = if include_groups && CONFIG.org_groups_enabled() { - GroupUser::find_by_user(&self.uuid, conn).await.iter().map(|gu| gu.groups_uuid.clone()).collect() + GroupUser::find_by_member(&self.uuid, conn).await.iter().map(|gu| gu.groups_uuid.clone()).collect() } else { // The Bitwarden clients seem to call this API regardless of whether groups are enabled, // so just act as if there are no groups. @@ -699,7 +699,7 @@ impl Membership { (self.access_all || self.atype >= MembershipType::Admin) && self.has_status(MembershipStatus::Confirmed) } - pub async fn find_by_uuid(uuid: &str, conn: &mut DbConn) -> Option { + pub async fn find_by_uuid(uuid: &MembershipId, conn: &mut DbConn) -> Option { db_run! { conn: { users_organizations::table .filter(users_organizations::uuid.eq(uuid)) @@ -708,7 +708,11 @@ impl Membership { }} } - pub async fn find_by_uuid_and_org(uuid: &str, org_uuid: &OrganizationId, conn: &mut DbConn) -> Option { + pub async fn find_by_uuid_and_org( + uuid: &MembershipId, + org_uuid: &OrganizationId, + conn: &mut DbConn, + ) -> Option { db_run! { conn: { users_organizations::table .filter(users_organizations::uuid.eq(uuid)) @@ -1078,16 +1082,61 @@ impl<'r> FromParam<'r> for OrganizationId { #[inline(always)] fn from_param(param: &'r str) -> Result { if param.chars().all(|c| matches!(c, 'a'..='z' | 'A'..='Z' |'0'..='9' | '-')) { - Ok(OrganizationId(param.to_string())) + Ok(Self(param.to_string())) } else { Err(()) } } } -#[derive(DieselNewType, Clone, Debug, Hash, PartialEq, Eq, Serialize)] +#[derive(DieselNewType, FromForm, Clone, Debug, Hash, PartialEq, Eq, Serialize, Deserialize)] pub struct MembershipId(String); +impl AsRef for MembershipId { + fn as_ref(&self) -> &str { + &self.0 + } +} + +impl Deref for MembershipId { + type Target = str; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl Borrow for MembershipId { + fn borrow(&self) -> &str { + &self.0 + } +} + +impl Display for MembershipId { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.0) + } +} + +impl From for MembershipId { + fn from(raw: String) -> Self { + Self(raw) + } +} + +impl<'r> FromParam<'r> for MembershipId { + 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(()) + } + } +} + #[cfg(test)] mod tests { use super::*; diff --git a/src/mail.rs b/src/mail.rs index e1c8c6daac..126adace6b 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, OrganizationId, User}, + db::models::{Device, DeviceType, MembershipId, OrganizationId, User}, error::Error, CONFIG, }; @@ -260,7 +260,7 @@ pub async fn send_single_org_removed_from_org(address: &str, org_name: &str) -> pub async fn send_invite( user: &User, org_id: Option, - member_id: Option, + member_id: Option, org_name: &str, invited_by_email: Option, ) -> EmptyResult { From 16afe108985740cfdf6fa5d82f5fabd61322b082 Mon Sep 17 00:00:00 2001 From: Stefan Melmuk Date: Sat, 21 Dec 2024 11:41:24 +0100 Subject: [PATCH 04/25] introduce user_id newtype --- src/api/admin.rs | 34 ++++---- src/api/core/accounts.rs | 12 +-- src/api/core/ciphers.rs | 6 +- src/api/core/emergency_access.rs | 2 +- src/api/core/events.rs | 14 ++-- src/api/core/organizations.rs | 28 +++++-- src/api/core/sends.rs | 2 +- src/api/core/two_factor/authenticator.rs | 8 +- src/api/core/two_factor/duo.rs | 6 +- src/api/core/two_factor/email.rs | 8 +- src/api/core/two_factor/protected_actions.rs | 4 +- src/api/core/two_factor/webauthn.rs | 14 ++-- src/api/identity.rs | 13 +-- src/api/notifications.rs | 55 ++++++------ src/api/push.rs | 10 +-- src/auth.rs | 4 +- src/db/models/attachment.rs | 8 +- src/db/models/auth_request.rs | 10 +-- src/db/models/cipher.rs | 47 ++++++----- src/db/models/collection.rs | 28 +++---- src/db/models/device.rs | 17 ++-- src/db/models/emergency_access.rs | 33 ++++---- src/db/models/event.rs | 4 +- src/db/models/favorite.rs | 12 +-- src/db/models/folder.rs | 14 ++-- src/db/models/group.rs | 8 +- src/db/models/mod.rs | 2 +- src/db/models/org_policy.rs | 14 ++-- src/db/models/organization.rs | 36 ++++---- src/db/models/send.rs | 14 ++-- src/db/models/two_factor.rs | 11 +-- src/db/models/two_factor_incomplete.rs | 20 +++-- src/db/models/user.rs | 88 ++++++++++++++++---- src/mail.rs | 22 ++--- 34 files changed, 351 insertions(+), 257 deletions(-) diff --git a/src/api/admin.rs b/src/api/admin.rs index 4cb5ffc089..673af54548 100644 --- a/src/api/admin.rs +++ b/src/api/admin.rs @@ -280,7 +280,7 @@ struct InviteData { email: String, } -async fn get_user_or_404(uuid: &str, conn: &mut DbConn) -> ApiResult { +async fn get_user_or_404(uuid: &UserId, conn: &mut DbConn) -> ApiResult { if let Some(user) = User::find_by_uuid(uuid, conn).await { Ok(user) } else { @@ -382,8 +382,8 @@ async fn get_user_by_mail_json(mail: &str, _token: AdminToken, mut conn: DbConn) } #[get("/users/")] -async fn get_user_json(uuid: &str, _token: AdminToken, mut conn: DbConn) -> JsonResult { - let u = get_user_or_404(uuid, &mut conn).await?; +async fn get_user_json(uuid: UserId, _token: AdminToken, mut conn: DbConn) -> JsonResult { + let u = get_user_or_404(&uuid, &mut conn).await?; let mut usr = u.to_json(&mut conn).await; usr["userEnabled"] = json!(u.enabled); usr["createdAt"] = json!(format_naive_datetime_local(&u.created_at, DT_FMT)); @@ -391,11 +391,11 @@ async fn get_user_json(uuid: &str, _token: AdminToken, mut conn: DbConn) -> Json } #[post("/users//delete")] -async fn delete_user(uuid: &str, token: AdminToken, mut conn: DbConn) -> EmptyResult { - let user = get_user_or_404(uuid, &mut conn).await?; +async fn delete_user(uuid: UserId, token: AdminToken, mut conn: DbConn) -> EmptyResult { + let user = get_user_or_404(&uuid, &mut conn).await?; // Get the membership records before deleting the actual user - let memberships = Membership::find_any_state_by_user(uuid, &mut conn).await; + let memberships = Membership::find_any_state_by_user(&uuid, &mut conn).await; let res = user.delete(&mut conn).await; for membership in memberships { @@ -415,8 +415,8 @@ async fn delete_user(uuid: &str, token: AdminToken, mut conn: DbConn) -> EmptyRe } #[post("/users//deauth")] -async fn deauth_user(uuid: &str, _token: AdminToken, mut conn: DbConn, nt: Notify<'_>) -> EmptyResult { - let mut user = get_user_or_404(uuid, &mut conn).await?; +async fn deauth_user(uuid: UserId, _token: AdminToken, mut conn: DbConn, nt: Notify<'_>) -> EmptyResult { + let mut user = get_user_or_404(&uuid, &mut conn).await?; nt.send_logout(&user, None).await; @@ -436,8 +436,8 @@ async fn deauth_user(uuid: &str, _token: AdminToken, mut conn: DbConn, nt: Notif } #[post("/users//disable")] -async fn disable_user(uuid: &str, _token: AdminToken, mut conn: DbConn, nt: Notify<'_>) -> EmptyResult { - let mut user = get_user_or_404(uuid, &mut conn).await?; +async fn disable_user(uuid: UserId, _token: AdminToken, mut conn: DbConn, nt: Notify<'_>) -> EmptyResult { + let mut user = get_user_or_404(&uuid, &mut conn).await?; Device::delete_all_by_user(&user.uuid, &mut conn).await?; user.reset_security_stamp(); user.enabled = false; @@ -450,16 +450,16 @@ async fn disable_user(uuid: &str, _token: AdminToken, mut conn: DbConn, nt: Noti } #[post("/users//enable")] -async fn enable_user(uuid: &str, _token: AdminToken, mut conn: DbConn) -> EmptyResult { - let mut user = get_user_or_404(uuid, &mut conn).await?; +async fn enable_user(uuid: UserId, _token: AdminToken, mut conn: DbConn) -> EmptyResult { + let mut user = get_user_or_404(&uuid, &mut conn).await?; user.enabled = true; user.save(&mut conn).await } #[post("/users//remove-2fa")] -async fn remove_2fa(uuid: &str, token: AdminToken, mut conn: DbConn) -> EmptyResult { - let mut user = get_user_or_404(uuid, &mut conn).await?; +async fn remove_2fa(uuid: UserId, token: AdminToken, mut conn: DbConn) -> EmptyResult { + let mut user = get_user_or_404(&uuid, &mut conn).await?; TwoFactor::delete_all_by_user(&user.uuid, &mut conn).await?; two_factor::enforce_2fa_policy(&user, ACTING_ADMIN_USER, 14, &token.ip.ip, &mut conn).await?; user.totp_recover = None; @@ -467,8 +467,8 @@ async fn remove_2fa(uuid: &str, token: AdminToken, mut conn: DbConn) -> EmptyRes } #[post("/users//invite/resend")] -async fn resend_user_invite(uuid: &str, _token: AdminToken, mut conn: DbConn) -> EmptyResult { - if let Some(user) = User::find_by_uuid(uuid, &mut conn).await { +async fn resend_user_invite(uuid: UserId, _token: AdminToken, mut conn: DbConn) -> EmptyResult { + if let Some(user) = User::find_by_uuid(&uuid, &mut conn).await { //TODO: replace this with user.status check when it will be available (PR#3397) if !user.password_hash.is_empty() { err_code!("User already accepted invitation", Status::BadRequest.code); @@ -487,7 +487,7 @@ async fn resend_user_invite(uuid: &str, _token: AdminToken, mut conn: DbConn) -> #[derive(Debug, Deserialize)] struct MembershipTypeData { user_type: NumberOrString, - user_uuid: String, + user_uuid: UserId, org_uuid: OrganizationId, } diff --git a/src/api/core/accounts.rs b/src/api/core/accounts.rs index 26986c64b3..38dc657bb3 100644 --- a/src/api/core/accounts.rs +++ b/src/api/core/accounts.rs @@ -306,8 +306,8 @@ async fn put_avatar(data: Json, headers: Headers, mut conn: DbConn) } #[get("/users//public-key")] -async fn get_public_keys(uuid: &str, _headers: Headers, mut conn: DbConn) -> JsonResult { - let user = match User::find_by_uuid(uuid, &mut conn).await { +async fn get_public_keys(uuid: UserId, _headers: Headers, mut conn: DbConn) -> JsonResult { + let user = match User::find_by_uuid(&uuid, &mut conn).await { Some(user) if user.public_key.is_some() => user, Some(_) => err_code!("User has no public_key", Status::NotFound.code), None => err_code!("User doesn't exist", Status::NotFound.code), @@ -793,7 +793,7 @@ async fn post_verify_email(headers: Headers) -> EmptyResult { #[derive(Deserialize)] #[serde(rename_all = "camelCase")] struct VerifyEmailTokenData { - user_id: String, + user_id: UserId, token: String, } @@ -808,7 +808,7 @@ async fn post_verify_email_token(data: Json, mut conn: DbC let Ok(claims) = decode_verify_email(&data.token) else { err!("Invalid claim") }; - if claims.sub != user.uuid { + if claims.sub != *user.uuid { err!("Invalid claim"); } user.verified_at = Some(Utc::now().naive_utc()); @@ -850,7 +850,7 @@ async fn post_delete_recover(data: Json, mut conn: DbConn) -> #[derive(Deserialize)] #[serde(rename_all = "camelCase")] struct DeleteRecoverTokenData { - user_id: String, + user_id: UserId, token: String, } @@ -866,7 +866,7 @@ async fn post_delete_recover_token(data: Json, mut conn: err!("User doesn't exist") }; - if claims.sub != user.uuid { + if claims.sub != *user.uuid { err!("Invalid claim"); } user.delete(&mut conn).await diff --git a/src/api/core/ciphers.rs b/src/api/core/ciphers.rs index 8558fef72f..21dbf08cd4 100644 --- a/src/api/core/ciphers.rs +++ b/src/api/core/ciphers.rs @@ -771,7 +771,7 @@ async fn post_collections_update( let posted_collections = HashSet::::from_iter(data.collection_ids); let current_collections = - HashSet::::from_iter(cipher.get_collections(headers.user.uuid.clone(), &mut conn).await); + HashSet::::from_iter(cipher.get_collections(headers.user.uuid.to_string(), &mut conn).await); for collection in posted_collections.symmetric_difference(¤t_collections) { match Collection::find_by_uuid_and_org(collection, cipher.organization_uuid.as_ref().unwrap(), &mut conn).await @@ -848,7 +848,7 @@ async fn post_collections_admin( let posted_collections = HashSet::::from_iter(data.collection_ids); let current_collections = - HashSet::::from_iter(cipher.get_admin_collections(headers.user.uuid.clone(), &mut conn).await); + HashSet::::from_iter(cipher.get_admin_collections(headers.user.uuid.to_string(), &mut conn).await); for collection in posted_collections.symmetric_difference(¤t_collections) { match Collection::find_by_uuid_and_org(collection, cipher.organization_uuid.as_ref().unwrap(), &mut conn).await @@ -1848,7 +1848,7 @@ pub enum CipherSyncType { } impl CipherSyncData { - pub async fn new(user_uuid: &str, sync_type: CipherSyncType, conn: &mut DbConn) -> Self { + pub async fn new(user_uuid: &UserId, sync_type: CipherSyncType, conn: &mut DbConn) -> Self { let cipher_folders: HashMap; let cipher_favorites: HashSet; match sync_type { diff --git a/src/api/core/emergency_access.rs b/src/api/core/emergency_access.rs index 6d3d881dd4..cc93f23c8a 100644 --- a/src/api/core/emergency_access.rs +++ b/src/api/core/emergency_access.rs @@ -701,7 +701,7 @@ async fn policies_emergency_access(emer_id: &str, headers: Headers, mut conn: Db fn is_valid_request( emergency_access: &EmergencyAccess, - requesting_user_uuid: &str, + requesting_user_uuid: &UserId, requested_access_type: EmergencyAccessType, ) -> bool { emergency_access.grantee_uuid.is_some() diff --git a/src/api/core/events.rs b/src/api/core/events.rs index 9bd1c8068f..a49dc78566 100644 --- a/src/api/core/events.rs +++ b/src/api/core/events.rs @@ -8,7 +8,7 @@ use crate::{ api::{EmptyResult, JsonResult}, auth::{AdminHeaders, Headers}, db::{ - models::{Cipher, Event, Membership, MembershipId, OrganizationId}, + models::{Cipher, Event, Membership, MembershipId, OrganizationId, UserId}, DbConn, DbPool, }, util::parse_date, @@ -218,7 +218,7 @@ async fn post_events_collect(data: Json>, headers: Headers, Ok(()) } -pub async fn log_user_event(event_type: i32, user_uuid: &str, device_type: i32, ip: &IpAddr, conn: &mut DbConn) { +pub async fn log_user_event(event_type: i32, user_uuid: &UserId, device_type: i32, ip: &IpAddr, conn: &mut DbConn) { if !CONFIG.org_events_enabled() { return; } @@ -227,7 +227,7 @@ pub async fn log_user_event(event_type: i32, user_uuid: &str, device_type: i32, async fn _log_user_event( event_type: i32, - user_uuid: &str, + user_uuid: &UserId, device_type: i32, event_date: Option, ip: &IpAddr, @@ -238,8 +238,8 @@ async fn _log_user_event( // Upstream saves the event also without any org_uuid. let mut event = Event::new(event_type, event_date); - event.user_uuid = Some(String::from(user_uuid)); - event.act_user_uuid = Some(String::from(user_uuid)); + event.user_uuid = Some(user_uuid.clone()); + event.act_user_uuid = Some(user_uuid.to_string()); event.device_type = Some(device_type); event.ip_address = Some(ip.to_string()); events.push(event); @@ -247,9 +247,9 @@ async fn _log_user_event( // For each org a user is a member of store these events per org for org_uuid in orgs { let mut event = Event::new(event_type, event_date); - event.user_uuid = Some(String::from(user_uuid)); + event.user_uuid = Some(user_uuid.clone()); event.org_uuid = Some(org_uuid); - event.act_user_uuid = Some(String::from(user_uuid)); + event.act_user_uuid = Some(user_uuid.to_string()); event.device_type = Some(device_type); event.ip_address = Some(ip.to_string()); events.push(event); diff --git a/src/api/core/organizations.rs b/src/api/core/organizations.rs index ffefcd7623..d9a8e64485 100644 --- a/src/api/core/organizations.rs +++ b/src/api/core/organizations.rs @@ -358,7 +358,7 @@ async fn get_org_collections_details( let users: Vec = coll_users .iter() .filter(|collection_user| collection_user.collection_uuid == col.uuid) - .map(|collection_user| SelectionReadOnly::to_collection_user_details_read_only(collection_user).to_json()) + .map(|collection_user| UserSelection::to_collection_user_details_read_only(collection_user).to_json()) .collect(); // get the group details for the given collection @@ -667,7 +667,7 @@ async fn get_org_collection_detail( .await .iter() .map(|collection_user| { - SelectionReadOnly::to_collection_user_details_read_only(collection_user).to_json() + UserSelection::to_collection_user_details_read_only(collection_user).to_json() }) .collect(); @@ -761,7 +761,7 @@ async fn get_org_details(data: OrgIdData, headers: Headers, mut conn: DbConn) -> }))) } -async fn _get_org_details(org_id: &OrganizationId, host: &str, user_uuid: &str, conn: &mut DbConn) -> Value { +async fn _get_org_details(org_id: &OrganizationId, host: &str, user_uuid: &UserId, conn: &mut DbConn) -> Value { let ciphers = Cipher::find_by_org(org_id, conn).await; let cipher_sync_data = CipherSyncData::new(user_uuid, CipherSyncType::Organization, conn).await; @@ -2384,16 +2384,30 @@ impl SelectionReadOnly { CollectionGroup::new(self.id.clone(), groups_uuid, self.read_only, self.hide_passwords) } - pub fn to_collection_group_details_read_only(collection_group: &CollectionGroup) -> SelectionReadOnly { - SelectionReadOnly { + pub fn to_collection_group_details_read_only(collection_group: &CollectionGroup) -> Self { + Self { id: collection_group.groups_uuid.clone(), read_only: collection_group.read_only, hide_passwords: collection_group.hide_passwords, } } - pub fn to_collection_user_details_read_only(collection_user: &CollectionUser) -> SelectionReadOnly { - SelectionReadOnly { + pub fn to_json(&self) -> Value { + json!(self) + } +} + +#[derive(Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +struct UserSelection { + id: UserId, + read_only: bool, + hide_passwords: bool, +} + +impl UserSelection { + pub fn to_collection_user_details_read_only(collection_user: &CollectionUser) -> Self { + Self { id: collection_user.user_uuid.clone(), read_only: collection_user.read_only, hide_passwords: collection_user.hide_passwords, diff --git a/src/api/core/sends.rs b/src/api/core/sends.rs index b98ecf70a0..cf217e9fd0 100644 --- a/src/api/core/sends.rs +++ b/src/api/core/sends.rs @@ -106,7 +106,7 @@ async fn enforce_disable_hide_email_policy(data: &SendData, headers: &Headers, c Ok(()) } -fn create_send(data: SendData, user_uuid: String) -> ApiResult { +fn create_send(data: SendData, user_uuid: UserId) -> ApiResult { let data_val = if data.r#type == SendType::Text as i32 { data.text } else if data.r#type == SendType::File as i32 { diff --git a/src/api/core/two_factor/authenticator.rs b/src/api/core/two_factor/authenticator.rs index 64cc6486c2..5861bee284 100644 --- a/src/api/core/two_factor/authenticator.rs +++ b/src/api/core/two_factor/authenticator.rs @@ -7,7 +7,7 @@ use crate::{ auth::{ClientIp, Headers}, crypto, db::{ - models::{EventType, TwoFactor, TwoFactorType}, + models::{EventType, TwoFactor, TwoFactorType, UserId}, DbConn, }, util::NumberOrString, @@ -95,7 +95,7 @@ async fn activate_authenticator_put(data: Json, headers } pub async fn validate_totp_code_str( - user_uuid: &str, + user_uuid: &UserId, totp_code: &str, secret: &str, ip: &ClientIp, @@ -109,7 +109,7 @@ pub async fn validate_totp_code_str( } pub async fn validate_totp_code( - user_uuid: &str, + user_uuid: &UserId, totp_code: &str, secret: &str, ip: &ClientIp, @@ -124,7 +124,7 @@ pub async fn validate_totp_code( let mut twofactor = match TwoFactor::find_by_user_and_type(user_uuid, TwoFactorType::Authenticator as i32, conn).await { Some(tf) => tf, - _ => TwoFactor::new(user_uuid.to_string(), TwoFactorType::Authenticator, secret.to_string()), + _ => TwoFactor::new(user_uuid.clone(), TwoFactorType::Authenticator, secret.to_string()), }; // The amount of steps back and forward in time diff --git a/src/api/core/two_factor/duo.rs b/src/api/core/two_factor/duo.rs index 764210434a..5567f41e24 100644 --- a/src/api/core/two_factor/duo.rs +++ b/src/api/core/two_factor/duo.rs @@ -11,7 +11,7 @@ use crate::{ auth::Headers, crypto, db::{ - models::{EventType, TwoFactor, TwoFactorType, User}, + models::{EventType, TwoFactor, TwoFactorType, User, UserId}, DbConn, }, error::MapResult, @@ -228,11 +228,11 @@ const AUTH_PREFIX: &str = "AUTH"; const DUO_PREFIX: &str = "TX"; const APP_PREFIX: &str = "APP"; -async fn get_user_duo_data(uuid: &str, conn: &mut DbConn) -> DuoStatus { +async fn get_user_duo_data(user_uuid: &UserId, conn: &mut DbConn) -> DuoStatus { let type_ = TwoFactorType::Duo as i32; // If the user doesn't have an entry, disabled - let Some(twofactor) = TwoFactor::find_by_user_and_type(uuid, type_, conn).await else { + let Some(twofactor) = TwoFactor::find_by_user_and_type(user_uuid, type_, conn).await else { return DuoStatus::Disabled(DuoData::global().is_some()); }; diff --git a/src/api/core/two_factor/email.rs b/src/api/core/two_factor/email.rs index 09f2f3b796..eabfbcf5a8 100644 --- a/src/api/core/two_factor/email.rs +++ b/src/api/core/two_factor/email.rs @@ -10,7 +10,7 @@ use crate::{ auth::Headers, crypto, db::{ - models::{EventType, TwoFactor, TwoFactorType, User}, + models::{EventType, TwoFactor, TwoFactorType, User, UserId}, DbConn, }, error::{Error, MapResult}, @@ -59,7 +59,7 @@ async fn send_email_login(data: Json, mut conn: DbConn) -> E } /// Generate the token, save the data for later verification and send email to user -pub async fn send_token(user_uuid: &str, conn: &mut DbConn) -> EmptyResult { +pub async fn send_token(user_uuid: &UserId, conn: &mut DbConn) -> EmptyResult { let type_ = TwoFactorType::Email as i32; let mut twofactor = TwoFactor::find_by_user_and_type(user_uuid, type_, conn).await.map_res("Two factor not found")?; @@ -198,7 +198,7 @@ async fn email(data: Json, headers: Headers, mut conn: DbConn) -> Jso } /// Validate the email code when used as TwoFactor token mechanism -pub async fn validate_email_code_str(user_uuid: &str, token: &str, data: &str, conn: &mut DbConn) -> EmptyResult { +pub async fn validate_email_code_str(user_uuid: &UserId, token: &str, data: &str, conn: &mut DbConn) -> EmptyResult { let mut email_data = EmailTokenData::from_json(data)?; let mut twofactor = TwoFactor::find_by_user_and_type(user_uuid, TwoFactorType::Email as i32, conn) .await @@ -327,7 +327,7 @@ pub fn obscure_email(email: &str) -> String { format!("{}@{}", new_name, &domain) } -pub async fn find_and_activate_email_2fa(user_uuid: &str, conn: &mut DbConn) -> EmptyResult { +pub async fn find_and_activate_email_2fa(user_uuid: &UserId, conn: &mut DbConn) -> EmptyResult { if let Some(user) = User::find_by_uuid(user_uuid, conn).await { activate_email_2fa(&user, conn).await } else { diff --git a/src/api/core/two_factor/protected_actions.rs b/src/api/core/two_factor/protected_actions.rs index 1a1d59c88a..775fef735c 100644 --- a/src/api/core/two_factor/protected_actions.rs +++ b/src/api/core/two_factor/protected_actions.rs @@ -6,7 +6,7 @@ use crate::{ auth::Headers, crypto, db::{ - models::{TwoFactor, TwoFactorType}, + models::{TwoFactor, TwoFactorType, UserId}, DbConn, }, error::{Error, MapResult}, @@ -104,7 +104,7 @@ async fn verify_otp(data: Json, headers: Headers, mut con pub async fn validate_protected_action_otp( otp: &str, - user_uuid: &str, + user_uuid: &UserId, delete_if_valid: bool, conn: &mut DbConn, ) -> EmptyResult { diff --git a/src/api/core/two_factor/webauthn.rs b/src/api/core/two_factor/webauthn.rs index 9ee83d3855..f7a04dd7c1 100644 --- a/src/api/core/two_factor/webauthn.rs +++ b/src/api/core/two_factor/webauthn.rs @@ -11,7 +11,7 @@ use crate::{ }, auth::Headers, db::{ - models::{EventType, TwoFactor, TwoFactorType}, + models::{EventType, TwoFactor, TwoFactorType, UserId}, DbConn, }, error::Error, @@ -148,7 +148,7 @@ async fn generate_webauthn_challenge(data: Json, headers: Hea )?; let type_ = TwoFactorType::WebauthnRegisterChallenge; - TwoFactor::new(user.uuid, type_, serde_json::to_string(&state)?).save(&mut conn).await?; + TwoFactor::new(user.uuid.clone(), type_, serde_json::to_string(&state)?).save(&mut conn).await?; let mut challenge_value = serde_json::to_value(challenge.public_key)?; challenge_value["status"] = "ok".into(); @@ -352,7 +352,7 @@ async fn delete_webauthn(data: Json, headers: Headers, mut conn: } pub async fn get_webauthn_registrations( - user_uuid: &str, + user_uuid: &UserId, conn: &mut DbConn, ) -> Result<(bool, Vec), Error> { let type_ = TwoFactorType::Webauthn as i32; @@ -362,7 +362,7 @@ pub async fn get_webauthn_registrations( } } -pub async fn generate_webauthn_login(user_uuid: &str, conn: &mut DbConn) -> JsonResult { +pub async fn generate_webauthn_login(user_uuid: &UserId, conn: &mut DbConn) -> JsonResult { // Load saved credentials let creds: Vec = get_webauthn_registrations(user_uuid, conn).await?.1.into_iter().map(|r| r.credential).collect(); @@ -376,7 +376,7 @@ pub async fn generate_webauthn_login(user_uuid: &str, conn: &mut DbConn) -> Json let (response, state) = WebauthnConfig::load().generate_challenge_authenticate_options(creds, Some(ext))?; // Save the challenge state for later validation - TwoFactor::new(user_uuid.into(), TwoFactorType::WebauthnLoginChallenge, serde_json::to_string(&state)?) + TwoFactor::new(user_uuid.clone(), TwoFactorType::WebauthnLoginChallenge, serde_json::to_string(&state)?) .save(conn) .await?; @@ -384,7 +384,7 @@ pub async fn generate_webauthn_login(user_uuid: &str, conn: &mut DbConn) -> Json Ok(Json(serde_json::to_value(response.public_key)?)) } -pub async fn validate_webauthn_login(user_uuid: &str, response: &str, conn: &mut DbConn) -> EmptyResult { +pub async fn validate_webauthn_login(user_uuid: &UserId, response: &str, conn: &mut DbConn) -> EmptyResult { let type_ = TwoFactorType::WebauthnLoginChallenge as i32; let state = match TwoFactor::find_by_user_and_type(user_uuid, type_, conn).await { Some(tf) => { @@ -413,7 +413,7 @@ pub async fn validate_webauthn_login(user_uuid: &str, response: &str, conn: &mut if ®.credential.cred_id == cred_id { reg.credential.counter = auth_data.counter; - TwoFactor::new(user_uuid.to_string(), TwoFactorType::Webauthn, serde_json::to_string(®istrations)?) + TwoFactor::new(user_uuid.clone(), TwoFactorType::Webauthn, serde_json::to_string(®istrations)?) .save(conn) .await?; return Ok(()); diff --git a/src/api/identity.rs b/src/api/identity.rs index a32433f06e..c1506adbad 100644 --- a/src/api/identity.rs +++ b/src/api/identity.rs @@ -31,7 +31,7 @@ pub fn routes() -> Vec { async fn login(data: Form, client_header: ClientHeaders, mut conn: DbConn) -> JsonResult { let data: ConnectData = data.into_inner(); - let mut user_uuid: Option = None; + let mut user_uuid: Option = None; let login_result = match data.grant_type.as_ref() { "refresh_token" => { @@ -141,7 +141,7 @@ struct MasterPasswordPolicy { async fn _password_login( data: ConnectData, - user_uuid: &mut Option, + user_uuid: &mut Option, conn: &mut DbConn, ip: &ClientIp, ) -> JsonResult { @@ -359,7 +359,7 @@ async fn _password_login( async fn _api_key_login( data: ConnectData, - user_uuid: &mut Option, + user_uuid: &mut Option, conn: &mut DbConn, ip: &ClientIp, ) -> JsonResult { @@ -376,7 +376,7 @@ async fn _api_key_login( async fn _user_api_key_login( data: ConnectData, - user_uuid: &mut Option, + user_uuid: &mut Option, conn: &mut DbConn, ip: &ClientIp, ) -> JsonResult { @@ -385,7 +385,8 @@ async fn _user_api_key_login( let Some(client_user_uuid) = client_id.strip_prefix("user.") else { err!("Malformed client_id", format!("IP: {}.", ip.ip)) }; - let Some(user) = User::find_by_uuid(client_user_uuid, conn).await else { + let client_user_uuid: UserId = client_user_uuid.to_string().into(); + let Some(user) = User::find_by_uuid(&client_user_uuid, conn).await else { err!("Invalid client_id", format!("IP: {}.", ip.ip)) }; @@ -615,7 +616,7 @@ fn _selected_data(tf: Option) -> ApiResult { async fn _json_err_twofactor( providers: &[i32], - user_uuid: &str, + user_uuid: &UserId, data: &ConnectData, conn: &mut DbConn, ) -> ApiResult { diff --git a/src/api/notifications.rs b/src/api/notifications.rs index dc65003906..1674952120 100644 --- a/src/api/notifications.rs +++ b/src/api/notifications.rs @@ -10,7 +10,7 @@ use rocket_ws::{Message, WebSocket}; use crate::{ auth::{ClientIp, WsAccessTokenHeader}, db::{ - models::{Cipher, Folder, Send as DbSend, User}, + models::{Cipher, Folder, Send as DbSend, User, UserId}, DbConn, }, Error, CONFIG, @@ -53,13 +53,13 @@ struct WsAccessToken { struct WSEntryMapGuard { users: Arc, - user_uuid: String, + user_uuid: UserId, entry_uuid: uuid::Uuid, addr: IpAddr, } impl WSEntryMapGuard { - fn new(users: Arc, user_uuid: String, entry_uuid: uuid::Uuid, addr: IpAddr) -> Self { + fn new(users: Arc, user_uuid: UserId, entry_uuid: uuid::Uuid, addr: IpAddr) -> Self { Self { users, user_uuid, @@ -72,7 +72,7 @@ impl WSEntryMapGuard { impl Drop for WSEntryMapGuard { fn drop(&mut self) { info!("Closing WS connection from {}", self.addr); - if let Some(mut entry) = self.users.map.get_mut(&self.user_uuid) { + if let Some(mut entry) = self.users.map.get_mut(&self.user_uuid.to_string()) { entry.retain(|(uuid, _)| uuid != &self.entry_uuid); } } @@ -129,7 +129,7 @@ fn websockets_hub<'r>( // Add a channel to send messages to this client to the map let entry_uuid = uuid::Uuid::new_v4(); let (tx, rx) = tokio::sync::mpsc::channel::(100); - users.map.entry(claims.sub.clone()).or_default().push((entry_uuid, tx)); + users.map.entry(claims.sub.to_string()).or_default().push((entry_uuid, tx)); // Once the guard goes out of scope, the connection will have been closed and the entry will be deleted from the map (rx, WSEntryMapGuard::new(users, claims.sub, entry_uuid, addr)) @@ -328,8 +328,8 @@ pub struct WebSocketUsers { } impl WebSocketUsers { - async fn send_update(&self, user_uuid: &str, data: &[u8]) { - if let Some(user) = self.map.get(user_uuid).map(|v| v.clone()) { + async fn send_update(&self, user_uuid: &UserId, data: &[u8]) { + if let Some(user) = self.map.get(user_uuid.as_ref()).map(|v| v.clone()) { for (_, sender) in user.iter() { if let Err(e) = sender.send(Message::binary(data)).await { error!("Error sending WS update {e}"); @@ -345,7 +345,7 @@ impl WebSocketUsers { return; } let data = create_update( - vec![("UserId".into(), user.uuid.clone().into()), ("Date".into(), serialize_date(user.updated_at))], + vec![("UserId".into(), user.uuid.to_string().into()), ("Date".into(), serialize_date(user.updated_at))], ut, None, ); @@ -365,7 +365,7 @@ impl WebSocketUsers { return; } let data = create_update( - vec![("UserId".into(), user.uuid.clone().into()), ("Date".into(), serialize_date(user.updated_at))], + vec![("UserId".into(), user.uuid.to_string().into()), ("Date".into(), serialize_date(user.updated_at))], UpdateType::LogOut, acting_device_uuid.clone(), ); @@ -393,7 +393,7 @@ impl WebSocketUsers { let data = create_update( vec![ ("Id".into(), folder.uuid.clone().into()), - ("UserId".into(), folder.user_uuid.clone().into()), + ("UserId".into(), folder.user_uuid.to_string().into()), ("RevisionDate".into(), serialize_date(folder.updated_at)), ], ut, @@ -413,7 +413,7 @@ impl WebSocketUsers { &self, ut: UpdateType, cipher: &Cipher, - user_uuids: &[String], + user_uuids: &[UserId], acting_device_uuid: &String, collection_uuids: Option>, conn: &mut DbConn, @@ -432,7 +432,7 @@ impl WebSocketUsers { serialize_date(Utc::now().naive_utc()), ) } else { - (convert_option(cipher.user_uuid.clone()), Value::Nil, serialize_date(cipher.updated_at)) + (convert_option(cipher.user_uuid.as_deref()), Value::Nil, serialize_date(cipher.updated_at)) }; let data = create_update( @@ -462,7 +462,7 @@ impl WebSocketUsers { &self, ut: UpdateType, send: &DbSend, - user_uuids: &[String], + user_uuids: &[UserId], acting_device_uuid: &String, conn: &mut DbConn, ) { @@ -470,7 +470,7 @@ impl WebSocketUsers { if *NOTIFICATIONS_DISABLED { return; } - let user_uuid = convert_option(send.user_uuid.clone()); + let user_uuid = convert_option(send.user_uuid.as_deref()); let data = create_update( vec![ @@ -494,7 +494,7 @@ impl WebSocketUsers { pub async fn send_auth_request( &self, - user_uuid: &String, + user_uuid: &UserId, auth_request_uuid: &String, acting_device_uuid: &String, conn: &mut DbConn, @@ -504,7 +504,7 @@ impl WebSocketUsers { return; } let data = create_update( - vec![("Id".into(), auth_request_uuid.clone().into()), ("UserId".into(), user_uuid.clone().into())], + vec![("Id".into(), auth_request_uuid.clone().into()), ("UserId".into(), user_uuid.to_string().into())], UpdateType::AuthRequest, Some(acting_device_uuid.to_string()), ); @@ -513,13 +513,13 @@ impl WebSocketUsers { } if CONFIG.push_enabled() { - push_auth_request(user_uuid.to_string(), auth_request_uuid.to_string(), conn).await; + push_auth_request(user_uuid.clone(), auth_request_uuid.to_string(), conn).await; } } pub async fn send_auth_response( &self, - user_uuid: &String, + user_uuid: &UserId, auth_response_uuid: &str, approving_device_uuid: String, conn: &mut DbConn, @@ -529,17 +529,16 @@ impl WebSocketUsers { return; } let data = create_update( - vec![("Id".into(), auth_response_uuid.to_owned().into()), ("UserId".into(), user_uuid.clone().into())], + vec![("Id".into(), auth_response_uuid.to_owned().into()), ("UserId".into(), user_uuid.to_string().into())], UpdateType::AuthRequestResponse, approving_device_uuid.clone().into(), ); if CONFIG.enable_websocket() { - self.send_update(auth_response_uuid, &data).await; + self.send_update(user_uuid, &data).await; } if CONFIG.push_enabled() { - push_auth_response(user_uuid.to_string(), auth_response_uuid.to_string(), approving_device_uuid, conn) - .await; + push_auth_response(user_uuid.clone(), auth_response_uuid.to_string(), approving_device_uuid, conn).await; } } } @@ -558,16 +557,16 @@ impl AnonymousWebSocketSubscriptions { } } - pub async fn send_auth_response(&self, user_uuid: &String, auth_response_uuid: &str) { + pub async fn send_auth_response(&self, user_uuid: &UserId, auth_response_uuid: &str) { if !CONFIG.enable_websocket() { return; } let data = create_anonymous_update( - vec![("Id".into(), auth_response_uuid.to_owned().into()), ("UserId".into(), user_uuid.clone().into())], + vec![("Id".into(), auth_response_uuid.to_owned().into()), ("UserId".into(), user_uuid.to_string().into())], UpdateType::AuthRequestResponse, - user_uuid.to_string(), + user_uuid.clone(), ); - self.send_update(auth_response_uuid, &data).await; + self.send_update(user_uuid, &data).await; } } @@ -604,7 +603,7 @@ fn create_update(payload: Vec<(Value, Value)>, ut: UpdateType, acting_device_uui serialize(value) } -fn create_anonymous_update(payload: Vec<(Value, Value)>, ut: UpdateType, user_id: String) -> Vec { +fn create_anonymous_update(payload: Vec<(Value, Value)>, ut: UpdateType, user_id: UserId) -> Vec { use rmpv::Value as V; let value = V::Array(vec![ @@ -615,7 +614,7 @@ fn create_anonymous_update(payload: Vec<(Value, Value)>, ut: UpdateType, user_id V::Array(vec![V::Map(vec![ ("Type".into(), (ut as i32).into()), ("Payload".into(), payload.into()), - ("UserId".into(), user_id.into()), + ("UserId".into(), user_id.to_string().into()), ])]), ]); diff --git a/src/api/push.rs b/src/api/push.rs index 7396a68da5..bf9601a5de 100644 --- a/src/api/push.rs +++ b/src/api/push.rs @@ -7,7 +7,7 @@ use tokio::sync::RwLock; use crate::{ api::{ApiResult, EmptyResult, UpdateType}, - db::models::{Cipher, Device, Folder, Send, User}, + db::models::{Cipher, Device, Folder, Send, User, UserId}, http_client::make_http_request, util::format_date, CONFIG, @@ -284,8 +284,8 @@ async fn send_to_push_relay(notification_data: Value) { }; } -pub async fn push_auth_request(user_uuid: String, auth_request_uuid: String, conn: &mut crate::db::DbConn) { - if Device::check_user_has_push_device(user_uuid.as_str(), conn).await { +pub async fn push_auth_request(user_uuid: UserId, auth_request_uuid: String, conn: &mut crate::db::DbConn) { + if Device::check_user_has_push_device(&user_uuid, conn).await { tokio::task::spawn(send_to_push_relay(json!({ "userId": user_uuid, "organizationId": (), @@ -301,12 +301,12 @@ pub async fn push_auth_request(user_uuid: String, auth_request_uuid: String, con } pub async fn push_auth_response( - user_uuid: String, + user_uuid: UserId, auth_request_uuid: String, approving_device_uuid: String, conn: &mut crate::db::DbConn, ) { - if Device::check_user_has_push_device(user_uuid.as_str(), conn).await { + if Device::check_user_has_push_device(&user_uuid, conn).await { tokio::task::spawn(send_to_push_relay(json!({ "userId": user_uuid, "organizationId": (), diff --git a/src/auth.rs b/src/auth.rs index e09d616168..3f92b127e1 100644 --- a/src/auth.rs +++ b/src/auth.rs @@ -14,7 +14,7 @@ use std::{ net::IpAddr, }; -use crate::db::models::{MembershipId, OrganizationId}; +use crate::db::models::{MembershipId, OrganizationId, UserId}; use crate::{error::Error, CONFIG}; const JWT_ALGORITHM: Algorithm = Algorithm::RS256; @@ -151,7 +151,7 @@ pub struct LoginJwtClaims { // Issuer pub iss: String, // Subject - pub sub: String, + pub sub: UserId, pub premium: bool, pub name: String, diff --git a/src/db/models/attachment.rs b/src/db/models/attachment.rs index 266fd19266..443704ac3f 100644 --- a/src/db/models/attachment.rs +++ b/src/db/models/attachment.rs @@ -3,7 +3,7 @@ use std::io::ErrorKind; use bigdecimal::{BigDecimal, ToPrimitive}; use serde_json::Value; -use super::OrganizationId; +use super::{OrganizationId, UserId}; use crate::CONFIG; db_object! { @@ -145,7 +145,7 @@ impl Attachment { }} } - pub async fn size_by_user(user_uuid: &str, conn: &mut DbConn) -> i64 { + pub async fn size_by_user(user_uuid: &UserId, conn: &mut DbConn) -> i64 { db_run! { conn: { let result: Option = attachments::table .left_join(ciphers::table.on(ciphers::uuid.eq(attachments::cipher_uuid))) @@ -162,7 +162,7 @@ impl Attachment { }} } - pub async fn count_by_user(user_uuid: &str, conn: &mut DbConn) -> i64 { + pub async fn count_by_user(user_uuid: &UserId, conn: &mut DbConn) -> i64 { db_run! { conn: { attachments::table .left_join(ciphers::table.on(ciphers::uuid.eq(attachments::cipher_uuid))) @@ -205,7 +205,7 @@ impl Attachment { // There is no filtering done here if the user actually has access! // It is used to speed up the sync process, and the matching is done in a different part. pub async fn find_all_by_user_and_orgs( - user_uuid: &str, + user_uuid: &UserId, org_uuids: &Vec, conn: &mut DbConn, ) -> Vec { diff --git a/src/db/models/auth_request.rs b/src/db/models/auth_request.rs index ed931eddc3..3f3a53a91e 100644 --- a/src/db/models/auth_request.rs +++ b/src/db/models/auth_request.rs @@ -1,4 +1,4 @@ -use super::OrganizationId; +use super::{OrganizationId, UserId}; use crate::crypto::ct_eq; use chrono::{NaiveDateTime, Utc}; @@ -9,7 +9,7 @@ db_object! { #[diesel(primary_key(uuid))] pub struct AuthRequest { pub uuid: String, - pub user_uuid: String, + pub user_uuid: UserId, pub organization_uuid: Option, pub request_device_identifier: String, @@ -34,7 +34,7 @@ db_object! { impl AuthRequest { pub fn new( - user_uuid: String, + user_uuid: UserId, request_device_identifier: String, device_type: i32, request_ip: String, @@ -112,7 +112,7 @@ impl AuthRequest { }} } - pub async fn find_by_uuid_and_user(uuid: &str, user_uuid: &str, conn: &mut DbConn) -> Option { + pub async fn find_by_uuid_and_user(uuid: &str, user_uuid: &UserId, conn: &mut DbConn) -> Option { db_run! {conn: { auth_requests::table .filter(auth_requests::uuid.eq(uuid)) @@ -123,7 +123,7 @@ impl AuthRequest { }} } - pub async fn find_by_user(user_uuid: &str, conn: &mut DbConn) -> Vec { + pub async fn find_by_user(user_uuid: &UserId, conn: &mut DbConn) -> Vec { db_run! {conn: { auth_requests::table .filter(auth_requests::user_uuid.eq(user_uuid)) diff --git a/src/db/models/cipher.rs b/src/db/models/cipher.rs index 76cd0a778e..db75d2bf1d 100644 --- a/src/db/models/cipher.rs +++ b/src/db/models/cipher.rs @@ -5,7 +5,7 @@ use serde_json::Value; use super::{ Attachment, CollectionCipher, Favorite, FolderCipher, Group, Membership, MembershipStatus, MembershipType, - OrganizationId, User, + OrganizationId, User, UserId, }; use crate::api::core::{CipherData, CipherSyncData, CipherSyncType}; @@ -22,7 +22,7 @@ db_object! { pub created_at: NaiveDateTime, pub updated_at: NaiveDateTime, - pub user_uuid: Option, + pub user_uuid: Option, pub organization_uuid: Option, pub key: Option, @@ -136,7 +136,7 @@ impl Cipher { pub async fn to_json( &self, host: &str, - user_uuid: &str, + user_uuid: &UserId, cipher_sync_data: Option<&CipherSyncData>, sync_type: CipherSyncType, conn: &mut DbConn, @@ -357,7 +357,7 @@ impl Cipher { json_object } - pub async fn update_users_revision(&self, conn: &mut DbConn) -> Vec { + pub async fn update_users_revision(&self, conn: &mut DbConn) -> Vec { let mut user_uuids = Vec::new(); match self.user_uuid { Some(ref user_uuid) => { @@ -443,7 +443,7 @@ impl Cipher { Ok(()) } - pub async fn delete_all_by_user(user_uuid: &str, conn: &mut DbConn) -> EmptyResult { + pub async fn delete_all_by_user(user_uuid: &UserId, conn: &mut DbConn) -> EmptyResult { for cipher in Self::find_owned_by_user(user_uuid, conn).await { cipher.delete(conn).await?; } @@ -461,7 +461,12 @@ impl Cipher { } } - pub async fn move_to_folder(&self, folder_uuid: Option, user_uuid: &str, conn: &mut DbConn) -> EmptyResult { + pub async fn move_to_folder( + &self, + folder_uuid: Option, + user_uuid: &UserId, + conn: &mut DbConn, + ) -> EmptyResult { User::update_uuid_revision(user_uuid, conn).await; match (self.get_folder_uuid(user_uuid, conn).await, folder_uuid) { @@ -489,14 +494,14 @@ impl Cipher { } /// Returns whether this cipher is directly owned by the user. - pub fn is_owned_by_user(&self, user_uuid: &str) -> bool { + pub fn is_owned_by_user(&self, user_uuid: &UserId) -> bool { self.user_uuid.is_some() && self.user_uuid.as_ref().unwrap() == user_uuid } /// Returns whether this cipher is owned by an org in which the user has full access. async fn is_in_full_access_org( &self, - user_uuid: &str, + user_uuid: &UserId, cipher_sync_data: Option<&CipherSyncData>, conn: &mut DbConn, ) -> bool { @@ -515,7 +520,7 @@ impl Cipher { /// Returns whether this cipher is owned by an group in which the user has full access. async fn is_in_full_access_group( &self, - user_uuid: &str, + user_uuid: &UserId, cipher_sync_data: Option<&CipherSyncData>, conn: &mut DbConn, ) -> bool { @@ -539,7 +544,7 @@ impl Cipher { /// the access restrictions. pub async fn get_access_restrictions( &self, - user_uuid: &str, + user_uuid: &UserId, cipher_sync_data: Option<&CipherSyncData>, conn: &mut DbConn, ) -> Option<(bool, bool)> { @@ -599,7 +604,7 @@ impl Cipher { Some((read_only, hide_passwords)) } - async fn get_user_collections_access_flags(&self, user_uuid: &str, conn: &mut DbConn) -> Vec<(bool, bool)> { + async fn get_user_collections_access_flags(&self, user_uuid: &UserId, conn: &mut DbConn) -> Vec<(bool, bool)> { db_run! {conn: { // Check whether this cipher is in any collections accessible to the // user. If so, retrieve the access flags for each collection. @@ -616,7 +621,7 @@ impl Cipher { }} } - async fn get_group_collections_access_flags(&self, user_uuid: &str, conn: &mut DbConn) -> Vec<(bool, bool)> { + async fn get_group_collections_access_flags(&self, user_uuid: &UserId, conn: &mut DbConn) -> Vec<(bool, bool)> { if !CONFIG.org_groups_enabled() { return Vec::new(); } @@ -642,31 +647,31 @@ impl Cipher { }} } - pub async fn is_write_accessible_to_user(&self, user_uuid: &str, conn: &mut DbConn) -> bool { + pub async fn is_write_accessible_to_user(&self, user_uuid: &UserId, conn: &mut DbConn) -> bool { match self.get_access_restrictions(user_uuid, None, conn).await { Some((read_only, _hide_passwords)) => !read_only, None => false, } } - pub async fn is_accessible_to_user(&self, user_uuid: &str, conn: &mut DbConn) -> bool { + pub async fn is_accessible_to_user(&self, user_uuid: &UserId, conn: &mut DbConn) -> bool { self.get_access_restrictions(user_uuid, None, conn).await.is_some() } // Returns whether this cipher is a favorite of the specified user. - pub async fn is_favorite(&self, user_uuid: &str, conn: &mut DbConn) -> bool { + pub async fn is_favorite(&self, user_uuid: &UserId, conn: &mut DbConn) -> bool { Favorite::is_favorite(&self.uuid, user_uuid, conn).await } // Sets whether this cipher is a favorite of the specified user. - pub async fn set_favorite(&self, favorite: Option, user_uuid: &str, conn: &mut DbConn) -> EmptyResult { + pub async fn set_favorite(&self, favorite: Option, user_uuid: &UserId, conn: &mut DbConn) -> EmptyResult { match favorite { None => Ok(()), // No change requested. Some(status) => Favorite::set_favorite(status, &self.uuid, user_uuid, conn).await, } } - pub async fn get_folder_uuid(&self, user_uuid: &str, conn: &mut DbConn) -> Option { + pub async fn get_folder_uuid(&self, user_uuid: &UserId, conn: &mut DbConn) -> Option { db_run! {conn: { folders_ciphers::table .inner_join(folders::table) @@ -711,7 +716,7 @@ impl Cipher { // true, then the non-interesting ciphers will not be returned. As a // result, those ciphers will not appear in "My Vault" for the org // owner/admin, but they can still be accessed via the org vault view. - pub async fn find_by_user(user_uuid: &str, visible_only: bool, conn: &mut DbConn) -> Vec { + pub async fn find_by_user(user_uuid: &UserId, visible_only: bool, conn: &mut DbConn) -> Vec { if CONFIG.org_groups_enabled() { db_run! {conn: { let mut query = ciphers::table @@ -793,12 +798,12 @@ impl Cipher { } // Find all ciphers visible to the specified user. - pub async fn find_by_user_visible(user_uuid: &str, conn: &mut DbConn) -> Vec { + pub async fn find_by_user_visible(user_uuid: &UserId, conn: &mut DbConn) -> Vec { Self::find_by_user(user_uuid, true, conn).await } // Find all ciphers directly owned by the specified user. - pub async fn find_owned_by_user(user_uuid: &str, conn: &mut DbConn) -> Vec { + pub async fn find_owned_by_user(user_uuid: &UserId, conn: &mut DbConn) -> Vec { db_run! {conn: { ciphers::table .filter( @@ -809,7 +814,7 @@ impl Cipher { }} } - pub async fn count_owned_by_user(user_uuid: &str, conn: &mut DbConn) -> i64 { + pub async fn count_owned_by_user(user_uuid: &UserId, conn: &mut DbConn) -> i64 { db_run! {conn: { ciphers::table .filter(ciphers::user_uuid.eq(user_uuid)) diff --git a/src/db/models/collection.rs b/src/db/models/collection.rs index 055ecb2ce0..9313585de5 100644 --- a/src/db/models/collection.rs +++ b/src/db/models/collection.rs @@ -1,6 +1,6 @@ use serde_json::Value; -use super::{CollectionGroup, GroupUser, Membership, MembershipStatus, MembershipType, OrganizationId, User}; +use super::{CollectionGroup, GroupUser, Membership, MembershipStatus, MembershipType, OrganizationId, User, UserId}; use crate::CONFIG; db_object! { @@ -18,7 +18,7 @@ db_object! { #[diesel(table_name = users_collections)] #[diesel(primary_key(user_uuid, collection_uuid))] pub struct CollectionUser { - pub user_uuid: String, + pub user_uuid: UserId, pub collection_uuid: String, pub read_only: bool, pub hide_passwords: bool, @@ -74,7 +74,7 @@ impl Collection { pub async fn to_json_details( &self, - user_uuid: &str, + user_uuid: &UserId, cipher_sync_data: Option<&crate::api::core::CipherSyncData>, conn: &mut DbConn, ) -> Value { @@ -208,7 +208,7 @@ impl Collection { }} } - pub async fn find_by_user_uuid(user_uuid: String, conn: &mut DbConn) -> Vec { + pub async fn find_by_user_uuid(user_uuid: UserId, conn: &mut DbConn) -> Vec { if CONFIG.org_groups_enabled() { db_run! { conn: { collections::table @@ -281,7 +281,7 @@ impl Collection { pub async fn find_by_organization_and_user_uuid( org_uuid: &OrganizationId, - user_uuid: &str, + user_uuid: &UserId, conn: &mut DbConn, ) -> Vec { Self::find_by_user_uuid(user_uuid.to_owned(), conn) @@ -324,7 +324,7 @@ impl Collection { }} } - pub async fn find_by_uuid_and_user(uuid: &str, user_uuid: String, conn: &mut DbConn) -> Option { + pub async fn find_by_uuid_and_user(uuid: &str, user_uuid: UserId, conn: &mut DbConn) -> Option { if CONFIG.org_groups_enabled() { db_run! { conn: { collections::table @@ -391,7 +391,7 @@ impl Collection { } } - pub async fn is_writable_by_user(&self, user_uuid: &str, conn: &mut DbConn) -> bool { + pub async fn is_writable_by_user(&self, user_uuid: &UserId, conn: &mut DbConn) -> bool { let user_uuid = user_uuid.to_string(); if CONFIG.org_groups_enabled() { db_run! { conn: { @@ -453,7 +453,7 @@ impl Collection { } } - pub async fn hide_passwords_for_user(&self, user_uuid: &str, conn: &mut DbConn) -> bool { + pub async fn hide_passwords_for_user(&self, user_uuid: &UserId, conn: &mut DbConn) -> bool { let user_uuid = user_uuid.to_string(); db_run! { conn: { collections::table @@ -504,7 +504,7 @@ impl Collection { impl CollectionUser { pub async fn find_by_organization_and_user_uuid( org_uuid: &OrganizationId, - user_uuid: &str, + user_uuid: &UserId, conn: &mut DbConn, ) -> Vec { db_run! { conn: { @@ -533,7 +533,7 @@ impl CollectionUser { } pub async fn save( - user_uuid: &str, + user_uuid: &UserId, collection_uuid: &str, read_only: bool, hide_passwords: bool, @@ -632,7 +632,7 @@ impl CollectionUser { pub async fn find_by_collection_and_user( collection_uuid: &str, - user_uuid: &str, + user_uuid: &UserId, conn: &mut DbConn, ) -> Option { db_run! { conn: { @@ -646,7 +646,7 @@ impl CollectionUser { }} } - pub async fn find_by_user(user_uuid: &str, conn: &mut DbConn) -> Vec { + pub async fn find_by_user(user_uuid: &UserId, conn: &mut DbConn) -> Vec { db_run! { conn: { users_collections::table .filter(users_collections::user_uuid.eq(user_uuid)) @@ -670,7 +670,7 @@ impl CollectionUser { } pub async fn delete_all_by_user_and_org( - user_uuid: &str, + user_uuid: &UserId, org_uuid: &OrganizationId, conn: &mut DbConn, ) -> EmptyResult { @@ -689,7 +689,7 @@ impl CollectionUser { }} } - pub async fn has_access_to_collection_by_user(col_id: &str, user_uuid: &str, conn: &mut DbConn) -> bool { + pub async fn has_access_to_collection_by_user(col_id: &str, user_uuid: &UserId, conn: &mut DbConn) -> bool { Self::find_by_collection_and_user(col_id, user_uuid, conn).await.is_some() } } diff --git a/src/db/models/device.rs b/src/db/models/device.rs index efb3380d93..634d7c4f99 100644 --- a/src/db/models/device.rs +++ b/src/db/models/device.rs @@ -1,5 +1,6 @@ use chrono::{NaiveDateTime, Utc}; +use super::UserId; use crate::{crypto, CONFIG}; use core::fmt; @@ -13,7 +14,7 @@ db_object! { pub created_at: NaiveDateTime, pub updated_at: NaiveDateTime, - pub user_uuid: String, + pub user_uuid: UserId, pub name: String, pub atype: i32, // https://github.com/bitwarden/server/blob/dcc199bcce4aa2d5621f6fab80f1b49d8b143418/src/Core/Enums/DeviceType.cs @@ -28,7 +29,7 @@ db_object! { /// Local methods impl Device { - pub fn new(uuid: String, user_uuid: String, name: String, atype: i32) -> Self { + pub fn new(uuid: String, user_uuid: UserId, name: String, atype: i32) -> Self { let now = Utc::now().naive_utc(); Self { @@ -150,7 +151,7 @@ impl Device { } } - pub async fn delete_all_by_user(user_uuid: &str, conn: &mut DbConn) -> EmptyResult { + pub async fn delete_all_by_user(user_uuid: &UserId, conn: &mut DbConn) -> EmptyResult { db_run! { conn: { diesel::delete(devices::table.filter(devices::user_uuid.eq(user_uuid))) .execute(conn) @@ -158,7 +159,7 @@ impl Device { }} } - pub async fn find_by_uuid_and_user(uuid: &str, user_uuid: &str, conn: &mut DbConn) -> Option { + pub async fn find_by_uuid_and_user(uuid: &str, user_uuid: &UserId, conn: &mut DbConn) -> Option { db_run! { conn: { devices::table .filter(devices::uuid.eq(uuid)) @@ -169,7 +170,7 @@ impl Device { }} } - pub async fn find_by_user(user_uuid: &str, conn: &mut DbConn) -> Vec { + pub async fn find_by_user(user_uuid: &UserId, conn: &mut DbConn) -> Vec { db_run! { conn: { devices::table .filter(devices::user_uuid.eq(user_uuid)) @@ -208,7 +209,7 @@ impl Device { }} } - pub async fn find_latest_active_by_user(user_uuid: &str, conn: &mut DbConn) -> Option { + pub async fn find_latest_active_by_user(user_uuid: &UserId, conn: &mut DbConn) -> Option { db_run! { conn: { devices::table .filter(devices::user_uuid.eq(user_uuid)) @@ -219,7 +220,7 @@ impl Device { }} } - pub async fn find_push_devices_by_user(user_uuid: &str, conn: &mut DbConn) -> Vec { + pub async fn find_push_devices_by_user(user_uuid: &UserId, conn: &mut DbConn) -> Vec { db_run! { conn: { devices::table .filter(devices::user_uuid.eq(user_uuid)) @@ -230,7 +231,7 @@ impl Device { }} } - pub async fn check_user_has_push_device(user_uuid: &str, conn: &mut DbConn) -> bool { + pub async fn check_user_has_push_device(user_uuid: &UserId, conn: &mut DbConn) -> bool { db_run! { conn: { devices::table .filter(devices::user_uuid.eq(user_uuid)) diff --git a/src/db/models/emergency_access.rs b/src/db/models/emergency_access.rs index f4f3b9a9c2..429092eaf4 100644 --- a/src/db/models/emergency_access.rs +++ b/src/db/models/emergency_access.rs @@ -3,7 +3,7 @@ use serde_json::Value; use crate::{api::EmptyResult, db::DbConn, error::MapResult}; -use super::User; +use super::{User, UserId}; db_object! { #[derive(Identifiable, Queryable, Insertable, AsChangeset)] @@ -12,8 +12,8 @@ db_object! { #[diesel(primary_key(uuid))] pub struct EmergencyAccess { pub uuid: String, - pub grantor_uuid: String, - pub grantee_uuid: Option, + pub grantor_uuid: UserId, + pub grantee_uuid: Option, pub email: Option, pub key_encrypted: Option, pub atype: i32, //EmergencyAccessType @@ -29,7 +29,7 @@ db_object! { // Local methods impl EmergencyAccess { - pub fn new(grantor_uuid: String, email: String, status: i32, atype: i32, wait_time_days: i32) -> Self { + pub fn new(grantor_uuid: UserId, email: String, status: i32, atype: i32, wait_time_days: i32) -> Self { let now = Utc::now().naive_utc(); Self { @@ -82,7 +82,7 @@ impl EmergencyAccess { } pub async fn to_json_grantee_details(&self, conn: &mut DbConn) -> Option { - let grantee_user = if let Some(grantee_uuid) = self.grantee_uuid.as_deref() { + let grantee_user = if let Some(grantee_uuid) = &self.grantee_uuid { User::find_by_uuid(grantee_uuid, conn).await.expect("Grantee user not found.") } else if let Some(email) = self.email.as_deref() { match User::find_by_mail(email, conn).await { @@ -211,7 +211,7 @@ impl EmergencyAccess { }} } - pub async fn delete_all_by_user(user_uuid: &str, conn: &mut DbConn) -> EmptyResult { + pub async fn delete_all_by_user(user_uuid: &UserId, conn: &mut DbConn) -> EmptyResult { for ea in Self::find_all_by_grantor_uuid(user_uuid, conn).await { ea.delete(conn).await?; } @@ -239,8 +239,8 @@ impl EmergencyAccess { } pub async fn find_by_grantor_uuid_and_grantee_uuid_or_email( - grantor_uuid: &str, - grantee_uuid: &str, + grantor_uuid: &UserId, + grantee_uuid: &UserId, email: &str, conn: &mut DbConn, ) -> Option { @@ -262,7 +262,7 @@ impl EmergencyAccess { }} } - pub async fn find_by_uuid_and_grantor_uuid(uuid: &str, grantor_uuid: &str, conn: &mut DbConn) -> Option { + pub async fn find_by_uuid_and_grantor_uuid(uuid: &str, grantor_uuid: &UserId, conn: &mut DbConn) -> Option { db_run! { conn: { emergency_access::table .filter(emergency_access::uuid.eq(uuid)) @@ -272,7 +272,7 @@ impl EmergencyAccess { }} } - pub async fn find_by_uuid_and_grantee_uuid(uuid: &str, grantee_uuid: &str, conn: &mut DbConn) -> Option { + pub async fn find_by_uuid_and_grantee_uuid(uuid: &str, grantee_uuid: &UserId, conn: &mut DbConn) -> Option { db_run! { conn: { emergency_access::table .filter(emergency_access::uuid.eq(uuid)) @@ -292,7 +292,7 @@ impl EmergencyAccess { }} } - pub async fn find_all_by_grantee_uuid(grantee_uuid: &str, conn: &mut DbConn) -> Vec { + pub async fn find_all_by_grantee_uuid(grantee_uuid: &UserId, conn: &mut DbConn) -> Vec { db_run! { conn: { emergency_access::table .filter(emergency_access::grantee_uuid.eq(grantee_uuid)) @@ -319,7 +319,7 @@ impl EmergencyAccess { }} } - pub async fn find_all_by_grantor_uuid(grantor_uuid: &str, conn: &mut DbConn) -> Vec { + pub async fn find_all_by_grantor_uuid(grantor_uuid: &UserId, conn: &mut DbConn) -> Vec { db_run! { conn: { emergency_access::table .filter(emergency_access::grantor_uuid.eq(grantor_uuid)) @@ -327,7 +327,12 @@ impl EmergencyAccess { }} } - pub async fn accept_invite(&mut self, grantee_uuid: &str, grantee_email: &str, conn: &mut DbConn) -> EmptyResult { + pub async fn accept_invite( + &mut self, + grantee_uuid: &UserId, + grantee_email: &str, + conn: &mut DbConn, + ) -> EmptyResult { if self.email.is_none() || self.email.as_ref().unwrap() != grantee_email { err!("User email does not match invite."); } @@ -337,7 +342,7 @@ impl EmergencyAccess { } self.status = EmergencyAccessStatus::Accepted as i32; - self.grantee_uuid = Some(String::from(grantee_uuid)); + self.grantee_uuid = Some(grantee_uuid.clone()); self.email = None; self.save(conn).await } diff --git a/src/db/models/event.rs b/src/db/models/event.rs index 3a225803a0..f52190ec00 100644 --- a/src/db/models/event.rs +++ b/src/db/models/event.rs @@ -1,7 +1,7 @@ use crate::db::DbConn; use serde_json::Value; -use super::OrganizationId; +use super::{OrganizationId, UserId}; use crate::{api::EmptyResult, error::MapResult, CONFIG}; use chrono::{NaiveDateTime, TimeDelta, Utc}; @@ -18,7 +18,7 @@ db_object! { pub struct Event { pub uuid: String, pub event_type: i32, // EventType - pub user_uuid: Option, + pub user_uuid: Option, pub org_uuid: Option, pub cipher_uuid: Option, pub collection_uuid: Option, diff --git a/src/db/models/favorite.rs b/src/db/models/favorite.rs index a301f5977b..14f89eafac 100644 --- a/src/db/models/favorite.rs +++ b/src/db/models/favorite.rs @@ -1,11 +1,11 @@ -use super::User; +use super::{User, UserId}; db_object! { #[derive(Identifiable, Queryable, Insertable)] #[diesel(table_name = favorites)] #[diesel(primary_key(user_uuid, cipher_uuid))] pub struct Favorite { - pub user_uuid: String, + pub user_uuid: UserId, pub cipher_uuid: String, } } @@ -17,7 +17,7 @@ use crate::error::MapResult; impl Favorite { // Returns whether the specified cipher is a favorite of the specified user. - pub async fn is_favorite(cipher_uuid: &str, user_uuid: &str, conn: &mut DbConn) -> bool { + pub async fn is_favorite(cipher_uuid: &str, user_uuid: &UserId, conn: &mut DbConn) -> bool { db_run! { conn: { let query = favorites::table .filter(favorites::cipher_uuid.eq(cipher_uuid)) @@ -29,7 +29,7 @@ impl Favorite { } // Sets whether the specified cipher is a favorite of the specified user. - pub async fn set_favorite(favorite: bool, cipher_uuid: &str, user_uuid: &str, conn: &mut DbConn) -> EmptyResult { + pub async fn set_favorite(favorite: bool, cipher_uuid: &str, user_uuid: &UserId, conn: &mut DbConn) -> EmptyResult { let (old, new) = (Self::is_favorite(cipher_uuid, user_uuid, conn).await, favorite); match (old, new) { (false, true) => { @@ -71,7 +71,7 @@ impl Favorite { } // Delete all favorite entries associated with the specified user. - pub async fn delete_all_by_user(user_uuid: &str, conn: &mut DbConn) -> EmptyResult { + pub async fn delete_all_by_user(user_uuid: &UserId, conn: &mut DbConn) -> EmptyResult { db_run! { conn: { diesel::delete(favorites::table.filter(favorites::user_uuid.eq(user_uuid))) .execute(conn) @@ -81,7 +81,7 @@ impl Favorite { /// Return a vec with (cipher_uuid) this will only contain favorite flagged ciphers /// This is used during a full sync so we only need one query for all favorite cipher matches. - pub async fn get_all_cipher_uuid_by_user(user_uuid: &str, conn: &mut DbConn) -> Vec { + pub async fn get_all_cipher_uuid_by_user(user_uuid: &UserId, conn: &mut DbConn) -> Vec { db_run! { conn: { favorites::table .filter(favorites::user_uuid.eq(user_uuid)) diff --git a/src/db/models/folder.rs b/src/db/models/folder.rs index 4546072071..9666e15933 100644 --- a/src/db/models/folder.rs +++ b/src/db/models/folder.rs @@ -1,7 +1,7 @@ use chrono::{NaiveDateTime, Utc}; use serde_json::Value; -use super::User; +use super::{User, UserId}; db_object! { #[derive(Identifiable, Queryable, Insertable, AsChangeset)] @@ -11,7 +11,7 @@ db_object! { pub uuid: String, pub created_at: NaiveDateTime, pub updated_at: NaiveDateTime, - pub user_uuid: String, + pub user_uuid: UserId, pub name: String, } @@ -26,7 +26,7 @@ db_object! { /// Local methods impl Folder { - pub fn new(user_uuid: String, name: String) -> Self { + pub fn new(user_uuid: UserId, name: String) -> Self { let now = Utc::now().naive_utc(); Self { @@ -113,14 +113,14 @@ impl Folder { }} } - pub async fn delete_all_by_user(user_uuid: &str, conn: &mut DbConn) -> EmptyResult { + pub async fn delete_all_by_user(user_uuid: &UserId, conn: &mut DbConn) -> EmptyResult { for folder in Self::find_by_user(user_uuid, conn).await { folder.delete(conn).await?; } Ok(()) } - pub async fn find_by_uuid_and_user(uuid: &str, user_uuid: &str, conn: &mut DbConn) -> Option { + pub async fn find_by_uuid_and_user(uuid: &str, user_uuid: &UserId, conn: &mut DbConn) -> Option { db_run! { conn: { folders::table .filter(folders::uuid.eq(uuid)) @@ -131,7 +131,7 @@ impl Folder { }} } - pub async fn find_by_user(user_uuid: &str, conn: &mut DbConn) -> Vec { + pub async fn find_by_user(user_uuid: &UserId, conn: &mut DbConn) -> Vec { db_run! { conn: { folders::table .filter(folders::user_uuid.eq(user_uuid)) @@ -216,7 +216,7 @@ impl FolderCipher { /// Return a vec with (cipher_uuid, folder_uuid) /// This is used during a full sync so we only need one query for all folder matches. - pub async fn find_by_user(user_uuid: &str, conn: &mut DbConn) -> Vec<(String, String)> { + pub async fn find_by_user(user_uuid: &UserId, conn: &mut DbConn) -> Vec<(String, String)> { db_run! { conn: { folders_ciphers::table .inner_join(folders::table) diff --git a/src/db/models/group.rs b/src/db/models/group.rs index c0a317616d..0a6cca10f4 100644 --- a/src/db/models/group.rs +++ b/src/db/models/group.rs @@ -1,4 +1,4 @@ -use super::{Membership, MembershipId, OrganizationId, User}; +use super::{Membership, MembershipId, OrganizationId, User, UserId}; use crate::api::EmptyResult; use crate::db::DbConn; use crate::error::MapResult; @@ -222,7 +222,7 @@ impl Group { }} } //Returns all organizations the user has full access to - pub async fn get_orgs_by_user_with_full_access(user_uuid: &str, conn: &mut DbConn) -> Vec { + pub async fn get_orgs_by_user_with_full_access(user_uuid: &UserId, conn: &mut DbConn) -> Vec { db_run! { conn: { groups_users::table .inner_join(users_organizations::table.on( @@ -240,7 +240,7 @@ impl Group { }} } - pub async fn is_in_full_access_group(user_uuid: &str, org_uuid: &OrganizationId, conn: &mut DbConn) -> bool { + pub async fn is_in_full_access_group(user_uuid: &UserId, org_uuid: &OrganizationId, conn: &mut DbConn) -> bool { db_run! { conn: { groups::table .inner_join(groups_users::table.on( @@ -353,7 +353,7 @@ impl CollectionGroup { }} } - pub async fn find_by_user(user_uuid: &str, conn: &mut DbConn) -> Vec { + pub async fn find_by_user(user_uuid: &UserId, conn: &mut DbConn) -> Vec { db_run! { conn: { collections_groups::table .inner_join(groups_users::table.on( diff --git a/src/db/models/mod.rs b/src/db/models/mod.rs index fe61e42fe0..7042b2941d 100644 --- a/src/db/models/mod.rs +++ b/src/db/models/mod.rs @@ -34,4 +34,4 @@ pub use self::send::{Send, SendType}; pub use self::two_factor::{TwoFactor, TwoFactorType}; pub use self::two_factor_duo_context::TwoFactorDuoContext; pub use self::two_factor_incomplete::TwoFactorIncomplete; -pub use self::user::{Invitation, User, UserKdfType, UserStampException}; +pub use self::user::{Invitation, User, UserId, UserKdfType, UserStampException}; diff --git a/src/db/models/org_policy.rs b/src/db/models/org_policy.rs index c63fa16d52..78cf99731e 100644 --- a/src/db/models/org_policy.rs +++ b/src/db/models/org_policy.rs @@ -5,7 +5,7 @@ use crate::api::EmptyResult; use crate::db::DbConn; use crate::error::MapResult; -use super::{Membership, MembershipId, MembershipStatus, MembershipType, OrganizationId, TwoFactor}; +use super::{Membership, MembershipId, MembershipStatus, MembershipType, OrganizationId, TwoFactor, UserId}; db_object! { #[derive(Identifiable, Queryable, Insertable, AsChangeset)] @@ -152,7 +152,7 @@ impl OrgPolicy { }} } - pub async fn find_confirmed_by_user(user_uuid: &str, conn: &mut DbConn) -> Vec { + pub async fn find_confirmed_by_user(user_uuid: &UserId, conn: &mut DbConn) -> Vec { db_run! { conn: { org_policies::table .inner_join( @@ -194,7 +194,7 @@ impl OrgPolicy { } pub async fn find_accepted_and_confirmed_by_user_and_active_policy( - user_uuid: &str, + user_uuid: &UserId, policy_type: OrgPolicyType, conn: &mut DbConn, ) -> Vec { @@ -221,7 +221,7 @@ impl OrgPolicy { } pub async fn find_confirmed_by_user_and_active_policy( - user_uuid: &str, + user_uuid: &UserId, policy_type: OrgPolicyType, conn: &mut DbConn, ) -> Vec { @@ -248,7 +248,7 @@ impl OrgPolicy { /// and the user is not an owner or admin of that org. This is only useful for checking /// applicability of policy types that have these particular semantics. pub async fn is_applicable_to_user( - user_uuid: &str, + user_uuid: &UserId, policy_type: OrgPolicyType, exclude_org_uuid: Option<&OrganizationId>, conn: &mut DbConn, @@ -271,7 +271,7 @@ impl OrgPolicy { } pub async fn is_user_allowed( - user_uuid: &str, + user_uuid: &UserId, org_uuid: &OrganizationId, exclude_current_org: bool, conn: &mut DbConn, @@ -316,7 +316,7 @@ impl OrgPolicy { /// Returns true if the user belongs to an org that has enabled the `DisableHideEmail` /// option of the `Send Options` policy, and the user is not an owner or admin of that org. - pub async fn is_hide_email_disabled(user_uuid: &str, conn: &mut DbConn) -> bool { + pub async fn is_hide_email_disabled(user_uuid: &UserId, conn: &mut DbConn) -> bool { for policy in OrgPolicy::find_confirmed_by_user_and_active_policy(user_uuid, OrgPolicyType::SendOptions, conn).await { diff --git a/src/db/models/organization.rs b/src/db/models/organization.rs index 7a3657edf7..db991fe119 100644 --- a/src/db/models/organization.rs +++ b/src/db/models/organization.rs @@ -10,7 +10,7 @@ use std::{ ops::Deref, }; -use super::{CollectionUser, Group, GroupUser, OrgPolicy, OrgPolicyType, TwoFactor, User}; +use super::{CollectionUser, Group, GroupUser, OrgPolicy, OrgPolicyType, TwoFactor, User, UserId}; use crate::db::models::{Collection, CollectionGroup}; use crate::CONFIG; @@ -31,7 +31,7 @@ db_object! { #[diesel(primary_key(uuid))] pub struct Membership { pub uuid: MembershipId, - pub user_uuid: String, + pub user_uuid: UserId, pub org_uuid: OrganizationId, pub access_all: bool, @@ -204,7 +204,7 @@ impl Organization { static ACTIVATE_REVOKE_DIFF: i32 = 128; impl Membership { - pub fn new(user_uuid: String, org_uuid: OrganizationId) -> Self { + pub fn new(user_uuid: UserId, org_uuid: OrganizationId) -> Self { Self { uuid: MembershipId(crate::util::get_uuid()), @@ -666,7 +666,7 @@ impl Membership { Ok(()) } - pub async fn delete_all_by_user(user_uuid: &str, conn: &mut DbConn) -> EmptyResult { + pub async fn delete_all_by_user(user_uuid: &UserId, conn: &mut DbConn) -> EmptyResult { for member in Self::find_any_state_by_user(user_uuid, conn).await { member.delete(conn).await?; } @@ -722,7 +722,7 @@ impl Membership { }} } - pub async fn find_confirmed_by_user(user_uuid: &str, conn: &mut DbConn) -> Vec { + pub async fn find_confirmed_by_user(user_uuid: &UserId, conn: &mut DbConn) -> Vec { db_run! { conn: { users_organizations::table .filter(users_organizations::user_uuid.eq(user_uuid)) @@ -732,7 +732,7 @@ impl Membership { }} } - pub async fn find_invited_by_user(user_uuid: &str, conn: &mut DbConn) -> Vec { + pub async fn find_invited_by_user(user_uuid: &UserId, conn: &mut DbConn) -> Vec { db_run! { conn: { users_organizations::table .filter(users_organizations::user_uuid.eq(user_uuid)) @@ -742,7 +742,7 @@ impl Membership { }} } - pub async fn find_any_state_by_user(user_uuid: &str, conn: &mut DbConn) -> Vec { + pub async fn find_any_state_by_user(user_uuid: &UserId, conn: &mut DbConn) -> Vec { db_run! { conn: { users_organizations::table .filter(users_organizations::user_uuid.eq(user_uuid)) @@ -751,7 +751,7 @@ impl Membership { }} } - pub async fn count_accepted_and_confirmed_by_user(user_uuid: &str, conn: &mut DbConn) -> i64 { + pub async fn count_accepted_and_confirmed_by_user(user_uuid: &UserId, conn: &mut DbConn) -> i64 { db_run! { conn: { users_organizations::table .filter(users_organizations::user_uuid.eq(user_uuid)) @@ -822,7 +822,11 @@ impl Membership { }} } - pub async fn find_by_user_and_org(user_uuid: &str, org_uuid: &OrganizationId, conn: &mut DbConn) -> Option { + pub async fn find_by_user_and_org( + user_uuid: &UserId, + org_uuid: &OrganizationId, + conn: &mut DbConn, + ) -> Option { db_run! { conn: { users_organizations::table .filter(users_organizations::user_uuid.eq(user_uuid)) @@ -833,7 +837,7 @@ impl Membership { } pub async fn find_confirmed_by_user_and_org( - user_uuid: &str, + user_uuid: &UserId, org_uuid: &OrganizationId, conn: &mut DbConn, ) -> Option { @@ -849,7 +853,7 @@ impl Membership { }} } - pub async fn find_by_user(user_uuid: &str, conn: &mut DbConn) -> Vec { + pub async fn find_by_user(user_uuid: &UserId, conn: &mut DbConn) -> Vec { db_run! { conn: { users_organizations::table .filter(users_organizations::user_uuid.eq(user_uuid)) @@ -858,7 +862,7 @@ impl Membership { }} } - pub async fn get_orgs_by_user(user_uuid: &str, conn: &mut DbConn) -> Vec { + pub async fn get_orgs_by_user(user_uuid: &UserId, conn: &mut DbConn) -> Vec { db_run! { conn: { users_organizations::table .filter(users_organizations::user_uuid.eq(user_uuid)) @@ -868,7 +872,11 @@ impl Membership { }} } - pub async fn find_by_user_and_policy(user_uuid: &str, policy_type: OrgPolicyType, conn: &mut DbConn) -> Vec { + pub async fn find_by_user_and_policy( + user_uuid: &UserId, + policy_type: OrgPolicyType, + conn: &mut DbConn, + ) -> Vec { db_run! { conn: { users_organizations::table .inner_join( @@ -940,7 +948,7 @@ impl Membership { }} } - pub async fn user_has_ge_admin_access_to_cipher(user_uuid: &str, cipher_uuid: &str, conn: &mut DbConn) -> bool { + pub async fn user_has_ge_admin_access_to_cipher(user_uuid: &UserId, cipher_uuid: &str, conn: &mut DbConn) -> bool { db_run! { conn: { users_organizations::table .inner_join(ciphers::table.on(ciphers::uuid.eq(cipher_uuid).and(ciphers::organization_uuid.eq(users_organizations::org_uuid.nullable())))) diff --git a/src/db/models/send.rs b/src/db/models/send.rs index 9e49ed5f42..8cb27367ae 100644 --- a/src/db/models/send.rs +++ b/src/db/models/send.rs @@ -3,7 +3,7 @@ use serde_json::Value; use crate::util::LowerCase; -use super::{OrganizationId, User}; +use super::{OrganizationId, User, UserId}; db_object! { #[derive(Identifiable, Queryable, Insertable, AsChangeset)] @@ -13,7 +13,7 @@ db_object! { pub struct Send { pub uuid: String, - pub user_uuid: Option, + pub user_uuid: Option, pub organization_uuid: Option, pub name: String, @@ -242,7 +242,7 @@ impl Send { } } - pub async fn update_users_revision(&self, conn: &mut DbConn) -> Vec { + pub async fn update_users_revision(&self, conn: &mut DbConn) -> Vec { let mut user_uuids = Vec::new(); match &self.user_uuid { Some(user_uuid) => { @@ -256,7 +256,7 @@ impl Send { user_uuids } - pub async fn delete_all_by_user(user_uuid: &str, conn: &mut DbConn) -> EmptyResult { + pub async fn delete_all_by_user(user_uuid: &UserId, conn: &mut DbConn) -> EmptyResult { for send in Self::find_by_user(user_uuid, conn).await { send.delete(conn).await?; } @@ -289,7 +289,7 @@ impl Send { }} } - pub async fn find_by_uuid_and_user(uuid: &str, user_uuid: &str, conn: &mut DbConn) -> Option { + pub async fn find_by_uuid_and_user(uuid: &str, user_uuid: &UserId, conn: &mut DbConn) -> Option { db_run! {conn: { sends::table .filter(sends::uuid.eq(uuid)) @@ -300,7 +300,7 @@ impl Send { }} } - pub async fn find_by_user(user_uuid: &str, conn: &mut DbConn) -> Vec { + pub async fn find_by_user(user_uuid: &UserId, conn: &mut DbConn) -> Vec { db_run! {conn: { sends::table .filter(sends::user_uuid.eq(user_uuid)) @@ -308,7 +308,7 @@ impl Send { }} } - pub async fn size_by_user(user_uuid: &str, conn: &mut DbConn) -> Option { + pub async fn size_by_user(user_uuid: &UserId, conn: &mut DbConn) -> Option { let sends = Self::find_by_user(user_uuid, conn).await; #[derive(serde::Deserialize)] diff --git a/src/db/models/two_factor.rs b/src/db/models/two_factor.rs index 9155c5183a..0df69869c5 100644 --- a/src/db/models/two_factor.rs +++ b/src/db/models/two_factor.rs @@ -1,5 +1,6 @@ use serde_json::Value; +use super::UserId; use crate::{api::EmptyResult, db::DbConn, error::MapResult}; db_object! { @@ -8,7 +9,7 @@ db_object! { #[diesel(primary_key(uuid))] pub struct TwoFactor { pub uuid: String, - pub user_uuid: String, + pub user_uuid: UserId, pub atype: i32, pub enabled: bool, pub data: String, @@ -41,7 +42,7 @@ pub enum TwoFactorType { /// Local methods impl TwoFactor { - pub fn new(user_uuid: String, atype: TwoFactorType, data: String) -> Self { + pub fn new(user_uuid: UserId, atype: TwoFactorType, data: String) -> Self { Self { uuid: crate::util::get_uuid(), user_uuid, @@ -118,7 +119,7 @@ impl TwoFactor { }} } - pub async fn find_by_user(user_uuid: &str, conn: &mut DbConn) -> Vec { + pub async fn find_by_user(user_uuid: &UserId, conn: &mut DbConn) -> Vec { db_run! { conn: { twofactor::table .filter(twofactor::user_uuid.eq(user_uuid)) @@ -129,7 +130,7 @@ impl TwoFactor { }} } - pub async fn find_by_user_and_type(user_uuid: &str, atype: i32, conn: &mut DbConn) -> Option { + pub async fn find_by_user_and_type(user_uuid: &UserId, atype: i32, conn: &mut DbConn) -> Option { db_run! { conn: { twofactor::table .filter(twofactor::user_uuid.eq(user_uuid)) @@ -140,7 +141,7 @@ impl TwoFactor { }} } - pub async fn delete_all_by_user(user_uuid: &str, conn: &mut DbConn) -> EmptyResult { + pub async fn delete_all_by_user(user_uuid: &UserId, conn: &mut DbConn) -> EmptyResult { db_run! { conn: { diesel::delete(twofactor::table.filter(twofactor::user_uuid.eq(user_uuid))) .execute(conn) diff --git a/src/db/models/two_factor_incomplete.rs b/src/db/models/two_factor_incomplete.rs index 12813eb5d3..6c6cacb88b 100644 --- a/src/db/models/two_factor_incomplete.rs +++ b/src/db/models/two_factor_incomplete.rs @@ -1,13 +1,19 @@ use chrono::{NaiveDateTime, Utc}; -use crate::{api::EmptyResult, auth::ClientIp, db::DbConn, error::MapResult, CONFIG}; +use crate::{ + api::EmptyResult, + auth::ClientIp, + db::{models::UserId, DbConn}, + error::MapResult, + CONFIG, +}; db_object! { #[derive(Identifiable, Queryable, Insertable, AsChangeset)] #[diesel(table_name = twofactor_incomplete)] #[diesel(primary_key(user_uuid, device_uuid))] pub struct TwoFactorIncomplete { - pub user_uuid: String, + pub user_uuid: UserId, // This device UUID is simply what's claimed by the device. It doesn't // necessarily correspond to any UUID in the devices table, since a device // must complete 2FA login before being added into the devices table. @@ -21,7 +27,7 @@ db_object! { impl TwoFactorIncomplete { pub async fn mark_incomplete( - user_uuid: &str, + user_uuid: &UserId, device_uuid: &str, device_name: &str, device_type: i32, @@ -55,7 +61,7 @@ impl TwoFactorIncomplete { }} } - pub async fn mark_complete(user_uuid: &str, device_uuid: &str, conn: &mut DbConn) -> EmptyResult { + pub async fn mark_complete(user_uuid: &UserId, device_uuid: &str, conn: &mut DbConn) -> EmptyResult { if CONFIG.incomplete_2fa_time_limit() <= 0 || !CONFIG.mail_enabled() { return Ok(()); } @@ -63,7 +69,7 @@ impl TwoFactorIncomplete { Self::delete_by_user_and_device(user_uuid, device_uuid, conn).await } - pub async fn find_by_user_and_device(user_uuid: &str, device_uuid: &str, conn: &mut DbConn) -> Option { + pub async fn find_by_user_and_device(user_uuid: &UserId, device_uuid: &str, conn: &mut DbConn) -> Option { db_run! { conn: { twofactor_incomplete::table .filter(twofactor_incomplete::user_uuid.eq(user_uuid)) @@ -88,7 +94,7 @@ impl TwoFactorIncomplete { Self::delete_by_user_and_device(&self.user_uuid, &self.device_uuid, conn).await } - pub async fn delete_by_user_and_device(user_uuid: &str, device_uuid: &str, conn: &mut DbConn) -> EmptyResult { + pub async fn delete_by_user_and_device(user_uuid: &UserId, device_uuid: &str, conn: &mut DbConn) -> EmptyResult { db_run! { conn: { diesel::delete(twofactor_incomplete::table .filter(twofactor_incomplete::user_uuid.eq(user_uuid)) @@ -98,7 +104,7 @@ impl TwoFactorIncomplete { }} } - pub async fn delete_all_by_user(user_uuid: &str, conn: &mut DbConn) -> EmptyResult { + pub async fn delete_all_by_user(user_uuid: &UserId, conn: &mut DbConn) -> EmptyResult { db_run! { conn: { diesel::delete(twofactor_incomplete::table.filter(twofactor_incomplete::user_uuid.eq(user_uuid))) .execute(conn) diff --git a/src/db/models/user.rs b/src/db/models/user.rs index 981a46056f..99b3895594 100644 --- a/src/db/models/user.rs +++ b/src/db/models/user.rs @@ -1,9 +1,23 @@ -use crate::util::{format_date, get_uuid, retry}; use chrono::{NaiveDateTime, TimeDelta, Utc}; +use rocket::request::FromParam; use serde_json::Value; +use std::{ + borrow::Borrow, + fmt::{Display, Formatter}, + ops::Deref, +}; -use crate::crypto; -use crate::CONFIG; +use super::{ + Cipher, Device, EmergencyAccess, Favorite, Folder, Membership, MembershipType, TwoFactor, TwoFactorIncomplete, +}; +use crate::{ + api::EmptyResult, + crypto, + db::DbConn, + error::MapResult, + util::{format_date, get_uuid, retry}, + CONFIG, +}; db_object! { #[derive(Identifiable, Queryable, Insertable, AsChangeset)] @@ -11,7 +25,7 @@ db_object! { #[diesel(treat_none_as_null = true)] #[diesel(primary_key(uuid))] pub struct User { - pub uuid: String, + pub uuid: UserId, pub enabled: bool, pub created_at: NaiveDateTime, pub updated_at: NaiveDateTime, @@ -91,7 +105,7 @@ impl User { let email = email.to_lowercase(); Self { - uuid: get_uuid(), + uuid: UserId(get_uuid()), enabled: true, created_at: now, updated_at: now, @@ -214,14 +228,6 @@ impl User { } } -use super::{ - Cipher, Device, EmergencyAccess, Favorite, Folder, Membership, MembershipType, Send, TwoFactor, TwoFactorIncomplete, -}; -use crate::db::DbConn; - -use crate::api::EmptyResult; -use crate::error::MapResult; - /// Database methods impl User { pub async fn to_json(&self, conn: &mut DbConn) -> Value { @@ -311,7 +317,7 @@ impl User { } } - Send::delete_all_by_user(&self.uuid, conn).await?; + super::Send::delete_all_by_user(&self.uuid, conn).await?; EmergencyAccess::delete_all_by_user(&self.uuid, conn).await?; EmergencyAccess::delete_all_by_grantee_email(&self.email, conn).await?; Membership::delete_all_by_user(&self.uuid, conn).await?; @@ -330,7 +336,7 @@ impl User { }} } - pub async fn update_uuid_revision(uuid: &str, conn: &mut DbConn) { + pub async fn update_uuid_revision(uuid: &UserId, conn: &mut DbConn) { if let Err(e) = Self::_update_revision(uuid, &Utc::now().naive_utc(), conn).await { warn!("Failed to update revision for {}: {:#?}", uuid, e); } @@ -355,7 +361,7 @@ impl User { Self::_update_revision(&self.uuid, &self.updated_at, conn).await } - async fn _update_revision(uuid: &str, date: &NaiveDateTime, conn: &mut DbConn) -> EmptyResult { + async fn _update_revision(uuid: &UserId, date: &NaiveDateTime, conn: &mut DbConn) -> EmptyResult { db_run! {conn: { retry(|| { diesel::update(users::table.filter(users::uuid.eq(uuid))) @@ -377,7 +383,7 @@ impl User { }} } - pub async fn find_by_uuid(uuid: &str, conn: &mut DbConn) -> Option { + pub async fn find_by_uuid(uuid: &UserId, conn: &mut DbConn) -> Option { db_run! {conn: { users::table.filter(users::uuid.eq(uuid)).first::(conn).ok().from_db() }} @@ -456,3 +462,51 @@ impl Invitation { } } } + +#[derive(DieselNewType, FromForm, Clone, Debug, Hash, PartialEq, Eq, Serialize, Deserialize)] +pub struct UserId(String); + +impl AsRef for UserId { + fn as_ref(&self) -> &str { + &self.0 + } +} + +impl Deref for UserId { + type Target = str; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl Borrow for UserId { + fn borrow(&self) -> &str { + &self.0 + } +} + +impl Display for UserId { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.0) + } +} + +impl From for UserId { + fn from(raw: String) -> Self { + Self(raw) + } +} + +impl<'r> FromParam<'r> for UserId { + 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/mail.rs b/src/mail.rs index 126adace6b..476968fd06 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}, + db::models::{Device, DeviceType, MembershipId, OrganizationId, User, UserId}, error::Error, CONFIG, }; @@ -166,8 +166,8 @@ pub async fn send_password_hint(address: &str, hint: Option) -> EmptyRes send_email(address, &subject, body_html, body_text).await } -pub async fn send_delete_account(address: &str, uuid: &str) -> EmptyResult { - let claims = generate_delete_claims(uuid.to_string()); +pub async fn send_delete_account(address: &str, user_id: &UserId) -> EmptyResult { + let claims = generate_delete_claims(user_id.to_string()); let delete_token = encode_jwt(&claims); let (subject, body_html, body_text) = get_text( @@ -175,7 +175,7 @@ pub async fn send_delete_account(address: &str, uuid: &str) -> EmptyResult { json!({ "url": CONFIG.domain(), "img_src": CONFIG._smtp_img_src(), - "user_id": uuid, + "user_id": user_id, "email": percent_encode(address.as_bytes(), NON_ALPHANUMERIC).to_string(), "token": delete_token, }), @@ -184,8 +184,8 @@ pub async fn send_delete_account(address: &str, uuid: &str) -> EmptyResult { send_email(address, &subject, body_html, body_text).await } -pub async fn send_verify_email(address: &str, uuid: &str) -> EmptyResult { - let claims = generate_verify_email_claims(uuid.to_string()); +pub async fn send_verify_email(address: &str, user_id: &UserId) -> EmptyResult { + let claims = generate_verify_email_claims(user_id.to_string()); let verify_email_token = encode_jwt(&claims); let (subject, body_html, body_text) = get_text( @@ -193,7 +193,7 @@ pub async fn send_verify_email(address: &str, uuid: &str) -> EmptyResult { json!({ "url": CONFIG.domain(), "img_src": CONFIG._smtp_img_src(), - "user_id": uuid, + "user_id": user_id, "email": percent_encode(address.as_bytes(), NON_ALPHANUMERIC).to_string(), "token": verify_email_token, }), @@ -214,8 +214,8 @@ pub async fn send_welcome(address: &str) -> EmptyResult { send_email(address, &subject, body_html, body_text).await } -pub async fn send_welcome_must_verify(address: &str, uuid: &str) -> EmptyResult { - let claims = generate_verify_email_claims(uuid.to_string()); +pub async fn send_welcome_must_verify(address: &str, user_id: &UserId) -> EmptyResult { + let claims = generate_verify_email_claims(user_id.to_string()); let verify_email_token = encode_jwt(&claims); let (subject, body_html, body_text) = get_text( @@ -223,7 +223,7 @@ pub async fn send_welcome_must_verify(address: &str, uuid: &str) -> EmptyResult json!({ "url": CONFIG.domain(), "img_src": CONFIG._smtp_img_src(), - "user_id": uuid, + "user_id": user_id, "token": verify_email_token, }), )?; @@ -265,7 +265,7 @@ pub async fn send_invite( invited_by_email: Option, ) -> EmptyResult { let claims = generate_invite_claims( - user.uuid.clone(), + user.uuid.to_string(), user.email.clone(), org_id.clone(), member_id.clone(), From 7f2db91f2332c0a393e98b7da1ef6a9b930c509d Mon Sep 17 00:00:00 2001 From: Stefan Melmuk Date: Sat, 21 Dec 2024 12:58:57 +0100 Subject: [PATCH 05/25] introduce collection_id newtype --- src/api/core/ciphers.rs | 31 ++++----- src/api/core/organizations.rs | 122 +++++++++++++++++++--------------- src/api/notifications.rs | 6 +- src/auth.rs | 12 ++-- src/db/models/cipher.rs | 23 ++++--- src/db/models/collection.rs | 98 ++++++++++++++++++++++----- src/db/models/group.rs | 12 ++-- src/db/models/mod.rs | 2 +- src/db/models/organization.rs | 12 ++-- 9 files changed, 201 insertions(+), 117 deletions(-) diff --git a/src/api/core/ciphers.rs b/src/api/core/ciphers.rs index 21dbf08cd4..3fbddc452e 100644 --- a/src/api/core/ciphers.rs +++ b/src/api/core/ciphers.rs @@ -368,7 +368,7 @@ pub async fn update_cipher_from_data( cipher: &mut Cipher, data: CipherData, headers: &Headers, - shared_to_collections: Option>, + shared_to_collections: Option>, conn: &mut DbConn, nt: &Notify<'_>, ut: UpdateType, @@ -710,7 +710,7 @@ async fn put_cipher_partial( #[serde(rename_all = "camelCase")] struct CollectionsAdminData { #[serde(alias = "CollectionIds")] - collection_ids: Vec, + collection_ids: Vec, } #[put("/ciphers//collections_v2", data = "")] @@ -769,9 +769,9 @@ async fn post_collections_update( err!("Cipher is not write accessible") } - let posted_collections = HashSet::::from_iter(data.collection_ids); + let posted_collections = HashSet::::from_iter(data.collection_ids); let current_collections = - HashSet::::from_iter(cipher.get_collections(headers.user.uuid.to_string(), &mut conn).await); + HashSet::::from_iter(cipher.get_collections(headers.user.uuid.to_string(), &mut conn).await); for collection in posted_collections.symmetric_difference(¤t_collections) { match Collection::find_by_uuid_and_org(collection, cipher.organization_uuid.as_ref().unwrap(), &mut conn).await @@ -846,9 +846,10 @@ async fn post_collections_admin( err!("Cipher is not write accessible") } - let posted_collections = HashSet::::from_iter(data.collection_ids); - let current_collections = - HashSet::::from_iter(cipher.get_admin_collections(headers.user.uuid.to_string(), &mut conn).await); + let posted_collections = HashSet::::from_iter(data.collection_ids); + let current_collections = HashSet::::from_iter( + cipher.get_admin_collections(headers.user.uuid.to_string(), &mut conn).await, + ); for collection in posted_collections.symmetric_difference(¤t_collections) { match Collection::find_by_uuid_and_org(collection, cipher.organization_uuid.as_ref().unwrap(), &mut conn).await @@ -900,7 +901,7 @@ struct ShareCipherData { #[serde(alias = "Cipher")] cipher: CipherData, #[serde(alias = "CollectionIds")] - collection_ids: Vec, + collection_ids: Vec, } #[post("/ciphers//share", data = "")] @@ -933,7 +934,7 @@ async fn put_cipher_share( #[serde(rename_all = "camelCase")] struct ShareSelectedCipherData { ciphers: Vec, - collection_ids: Vec, + collection_ids: Vec, } #[put("/ciphers/share", data = "")] @@ -1834,10 +1835,10 @@ pub struct CipherSyncData { pub cipher_attachments: HashMap>, pub cipher_folders: HashMap, pub cipher_favorites: HashSet, - pub cipher_collections: HashMap>, + pub cipher_collections: HashMap>, pub members: HashMap, - pub user_collections: HashMap, - pub user_collections_groups: HashMap, + pub user_collections: HashMap, + pub user_collections_groups: HashMap, pub user_group_full_access_for_organizations: HashSet, } @@ -1878,7 +1879,7 @@ impl CipherSyncData { // Generate a HashMap with the Cipher UUID as key and one or more Collection UUID's let user_cipher_collections = Cipher::get_collections_with_cipher_by_user(user_uuid.to_string(), conn).await; - let mut cipher_collections: HashMap> = + let mut cipher_collections: HashMap> = HashMap::with_capacity(user_cipher_collections.len()); for (cipher, collection) in user_cipher_collections { cipher_collections.entry(cipher).or_default().push(collection); @@ -1889,14 +1890,14 @@ impl CipherSyncData { Membership::find_by_user(user_uuid, conn).await.into_iter().map(|m| (m.org_uuid.clone(), m)).collect(); // Generate a HashMap with the User_Collections UUID as key and the CollectionUser record - let user_collections: HashMap = CollectionUser::find_by_user(user_uuid, conn) + let user_collections: HashMap = CollectionUser::find_by_user(user_uuid, conn) .await .into_iter() .map(|uc| (uc.collection_uuid.clone(), uc)) .collect(); // Generate a HashMap with the collections_uuid as key and the CollectionGroup record - let user_collections_groups: HashMap = if CONFIG.org_groups_enabled() { + let user_collections_groups: HashMap = if CONFIG.org_groups_enabled() { CollectionGroup::find_by_user(user_uuid, conn) .await .into_iter() diff --git a/src/api/core/organizations.rs b/src/api/core/organizations.rs index d9a8e64485..b7ac7dcb19 100644 --- a/src/api/core/organizations.rs +++ b/src/api/core/organizations.rs @@ -126,7 +126,7 @@ struct NewCollectionData { name: String, groups: Vec, users: Vec, - id: Option, + id: Option, external_id: Option, } @@ -340,7 +340,7 @@ async fn get_org_collections_details( }; // get all collection memberships for the current organization - let coll_users = CollectionUser::find_by_organization(&org_id, &mut conn).await; + let col_users = CollectionUser::find_by_organization(&org_id, &mut conn).await; // check if current user has full access to the organization (either directly or via any group) let has_full_access_to_org = member.access_all @@ -355,7 +355,7 @@ async fn get_org_collections_details( && GroupUser::has_access_to_collection_by_member(&col.uuid, &member.uuid, &mut conn).await); // get the users assigned directly to the given collection - let users: Vec = coll_users + let users: Vec = col_users .iter() .filter(|collection_user| collection_user.collection_uuid == col.uuid) .map(|collection_user| UserSelection::to_collection_user_details_read_only(collection_user).to_json()) @@ -450,7 +450,7 @@ async fn post_organization_collections( #[put("/organizations//collections/", data = "")] async fn put_organization_collection_update( org_id: OrganizationId, - col_id: &str, + col_id: CollectionId, headers: ManagerHeaders, data: Json, conn: DbConn, @@ -461,7 +461,7 @@ async fn put_organization_collection_update( #[post("/organizations//collections/", data = "")] async fn post_organization_collection_update( org_id: OrganizationId, - col_id: &str, + col_id: CollectionId, headers: ManagerHeaders, data: Json, mut conn: DbConn, @@ -472,7 +472,7 @@ async fn post_organization_collection_update( err!("Can't find organization details") }; - let Some(mut collection) = Collection::find_by_uuid_and_org(col_id, &org_id, &mut conn).await else { + let Some(mut collection) = Collection::find_by_uuid_and_org(&col_id, &org_id, &mut conn).await else { err!("Collection not found") }; @@ -495,15 +495,13 @@ async fn post_organization_collection_update( ) .await; - CollectionGroup::delete_all_by_collection(col_id, &mut conn).await?; + CollectionGroup::delete_all_by_collection(&col_id, &mut conn).await?; for group in data.groups { - CollectionGroup::new(String::from(col_id), group.id, group.read_only, group.hide_passwords) - .save(&mut conn) - .await?; + CollectionGroup::new(col_id.clone(), group.id, group.read_only, group.hide_passwords).save(&mut conn).await?; } - CollectionUser::delete_all_by_collection(col_id, &mut conn).await?; + CollectionUser::delete_all_by_collection(&col_id, &mut conn).await?; for user in data.users { let Some(member) = Membership::find_by_uuid_and_org(&user.id, &org_id, &mut conn).await else { @@ -514,7 +512,7 @@ async fn post_organization_collection_update( continue; } - CollectionUser::save(&member.user_uuid, col_id, user.read_only, user.hide_passwords, &mut conn).await?; + CollectionUser::save(&member.user_uuid, &col_id, user.read_only, user.hide_passwords, &mut conn).await?; } Ok(Json(collection.to_json_details(&headers.user.uuid, None, &mut conn).await)) @@ -523,12 +521,12 @@ async fn post_organization_collection_update( #[delete("/organizations//collections//user/")] async fn delete_organization_collection_user( org_id: OrganizationId, - col_id: &str, + col_id: CollectionId, member_id: MembershipId, _headers: AdminHeaders, mut conn: DbConn, ) -> EmptyResult { - let Some(collection) = Collection::find_by_uuid_and_org(col_id, &org_id, &mut conn).await else { + let Some(collection) = Collection::find_by_uuid_and_org(&col_id, &org_id, &mut conn).await else { err!("Collection not found", "Collection does not exist or does not belong to this organization") }; @@ -546,7 +544,7 @@ async fn delete_organization_collection_user( #[post("/organizations//collections//delete-user/")] async fn post_organization_collection_delete_user( org_id: OrganizationId, - col_id: &str, + col_id: CollectionId, member_id: MembershipId, headers: AdminHeaders, conn: DbConn, @@ -556,7 +554,7 @@ async fn post_organization_collection_delete_user( async fn _delete_organization_collection( org_id: &OrganizationId, - col_id: &str, + col_id: &CollectionId, headers: &ManagerHeaders, conn: &mut DbConn, ) -> EmptyResult { @@ -579,11 +577,11 @@ async fn _delete_organization_collection( #[delete("/organizations//collections/")] async fn delete_organization_collection( org_id: OrganizationId, - col_id: &str, + col_id: CollectionId, headers: ManagerHeaders, mut conn: DbConn, ) -> EmptyResult { - _delete_organization_collection(&org_id, col_id, &headers, &mut conn).await + _delete_organization_collection(&org_id, &col_id, &headers, &mut conn).await } #[derive(Deserialize, Debug)] @@ -598,17 +596,17 @@ struct DeleteCollectionData { #[post("/organizations//collections//delete")] async fn post_organization_collection_delete( org_id: OrganizationId, - col_id: &str, + col_id: CollectionId, headers: ManagerHeaders, mut conn: DbConn, ) -> EmptyResult { - _delete_organization_collection(&org_id, col_id, &headers, &mut conn).await + _delete_organization_collection(&org_id, &col_id, &headers, &mut conn).await } #[derive(Deserialize, Debug)] #[serde(rename_all = "camelCase")] struct BulkCollectionIds { - ids: Vec, + ids: Vec, } #[delete("/organizations//collections", data = "")] @@ -630,14 +628,14 @@ async fn bulk_delete_organization_collections( Ok(()) } -#[get("/organizations//collections//details")] +#[get("/organizations//collections//details")] async fn get_org_collection_detail( org_id: OrganizationId, - coll_id: &str, + col_id: CollectionId, headers: ManagerHeaders, mut conn: DbConn, ) -> JsonResult { - match Collection::find_by_uuid_and_user(coll_id, headers.user.uuid.clone(), &mut conn).await { + match Collection::find_by_uuid_and_user(&col_id, headers.user.uuid.clone(), &mut conn).await { None => err!("Collection not found"), Some(collection) => { if collection.org_uuid != org_id { @@ -684,15 +682,15 @@ async fn get_org_collection_detail( } } -#[get("/organizations//collections//users")] +#[get("/organizations//collections//users")] async fn get_collection_users( org_id: OrganizationId, - coll_id: &str, + col_id: CollectionId, _headers: ManagerHeaders, mut conn: DbConn, ) -> JsonResult { // Get org and collection, check that collection is from org - let Some(collection) = Collection::find_by_uuid_and_org(coll_id, &org_id, &mut conn).await else { + let Some(collection) = Collection::find_by_uuid_and_org(&col_id, &org_id, &mut conn).await else { err!("Collection not found in Organization") }; @@ -709,21 +707,21 @@ async fn get_collection_users( Ok(Json(json!(user_list))) } -#[put("/organizations//collections//users", data = "")] +#[put("/organizations//collections//users", data = "")] async fn put_collection_users( org_id: OrganizationId, - coll_id: &str, - data: Json>, + col_id: CollectionId, + data: Json>, _headers: ManagerHeaders, mut conn: DbConn, ) -> EmptyResult { // Get org and collection, check that collection is from org - if Collection::find_by_uuid_and_org(coll_id, &org_id, &mut conn).await.is_none() { + if Collection::find_by_uuid_and_org(&col_id, &org_id, &mut conn).await.is_none() { err!("Collection not found in Organization") } // Delete all the user-collections - CollectionUser::delete_all_by_collection(coll_id, &mut conn).await?; + CollectionUser::delete_all_by_collection(&col_id, &mut conn).await?; // And then add all the received ones (except if the user has access_all) for d in data.iter() { @@ -735,7 +733,7 @@ async fn put_collection_users( continue; } - CollectionUser::save(&user.user_uuid, coll_id, d.read_only, d.hide_passwords, &mut conn).await?; + CollectionUser::save(&user.user_uuid, &col_id, d.read_only, d.hide_passwords, &mut conn).await?; } Ok(()) @@ -841,6 +839,14 @@ async fn post_org_keys( #[derive(Deserialize)] #[serde(rename_all = "camelCase")] struct CollectionData { + id: CollectionId, + read_only: bool, + hide_passwords: bool, +} + +#[derive(Deserialize)] +#[serde(rename_all = "camelCase")] +struct MembershipData { id: MembershipId, read_only: bool, hide_passwords: bool, @@ -1615,14 +1621,14 @@ async fn post_org_import( // TODO: See if we can optimize the whole cipher adding/importing and prevent duplicate code and checks. Cipher::validate_cipher_data(&data.ciphers)?; - let existing_collections: HashSet> = - Collection::find_by_organization(&org_id, &mut conn).await.into_iter().map(|c| (Some(c.uuid))).collect(); - let mut collections: Vec = Vec::with_capacity(data.collections.len()); - for coll in data.collections { - let collection_uuid = if existing_collections.contains(&coll.id) { - coll.id.unwrap() + let existing_collections: HashSet> = + Collection::find_by_organization(&org_id, &mut conn).await.into_iter().map(|c| Some(c.uuid)).collect(); + let mut collections: Vec = Vec::with_capacity(data.collections.len()); + for col in data.collections { + let collection_uuid = if existing_collections.contains(&col.id) { + col.id.unwrap() } else { - let new_collection = Collection::new(org_id.clone(), coll.name, coll.external_id); + let new_collection = Collection::new(org_id.clone(), col.name, col.external_id); new_collection.save(&mut conn).await?; new_collection.uuid }; @@ -1649,10 +1655,10 @@ async fn post_org_import( } // Assign the collections - for (cipher_index, coll_index) in relations { + for (cipher_index, col_index) in relations { let cipher_id = &ciphers[cipher_index]; - let coll_id = &collections[coll_index]; - CollectionCipher::save(cipher_id, coll_id, &mut conn).await?; + let col_id = &collections[col_index]; + CollectionCipher::save(cipher_id, col_id, &mut conn).await?; } let mut user = headers.user; @@ -1665,7 +1671,7 @@ async fn post_org_import( struct BulkCollectionsData { organization_id: OrganizationId, cipher_ids: Vec, - collection_ids: HashSet, + collection_ids: HashSet, remove_collections: bool, } @@ -1683,7 +1689,7 @@ async fn post_bulk_collections(data: Json, headers: Headers // Get all the collection available to the user in one query // Also filter based upon the provided collections - let user_collections: HashMap = + let user_collections: HashMap = Collection::find_by_organization_and_user_uuid(&data.organization_id, &headers.user.uuid, &mut conn) .await .into_iter() @@ -2352,7 +2358,7 @@ struct GroupRequest { #[serde(default)] access_all: bool, external_id: Option, - collections: Vec, + collections: Vec, users: Vec, } @@ -2380,10 +2386,6 @@ struct SelectionReadOnly { } impl SelectionReadOnly { - pub fn to_collection_group(&self, groups_uuid: String) -> CollectionGroup { - CollectionGroup::new(self.id.clone(), groups_uuid, self.read_only, self.hide_passwords) - } - pub fn to_collection_group_details_read_only(collection_group: &CollectionGroup) -> Self { Self { id: collection_group.groups_uuid.clone(), @@ -2397,6 +2399,20 @@ impl SelectionReadOnly { } } +#[derive(Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +struct CollectionSelection { + id: CollectionId, + read_only: bool, + hide_passwords: bool, +} + +impl CollectionSelection { + pub fn to_collection_group(&self, groups_uuid: String) -> CollectionGroup { + CollectionGroup::new(self.id.clone(), groups_uuid, self.read_only, self.hide_passwords) + } +} + #[derive(Deserialize, Serialize)] #[serde(rename_all = "camelCase")] struct UserSelection { @@ -2496,7 +2512,7 @@ async fn put_group( async fn add_update_group( mut group: Group, - collections: Vec, + collections: Vec, members: Vec, org_id: OrganizationId, headers: &AdminHeaders, @@ -2504,8 +2520,8 @@ async fn add_update_group( ) -> JsonResult { group.save(conn).await?; - for selection_read_only_request in collections { - let mut collection_group = selection_read_only_request.to_collection_group(group.uuid.clone()); + for col_selection in collections { + let mut collection_group = col_selection.to_collection_group(group.uuid.clone()); collection_group.save(conn).await?; } diff --git a/src/api/notifications.rs b/src/api/notifications.rs index 1674952120..832e56683b 100644 --- a/src/api/notifications.rs +++ b/src/api/notifications.rs @@ -10,7 +10,7 @@ use rocket_ws::{Message, WebSocket}; use crate::{ auth::{ClientIp, WsAccessTokenHeader}, db::{ - models::{Cipher, Folder, Send as DbSend, User, UserId}, + models::{Cipher, CollectionId, Folder, Send as DbSend, User, UserId}, DbConn, }, Error, CONFIG, @@ -415,7 +415,7 @@ impl WebSocketUsers { cipher: &Cipher, user_uuids: &[UserId], acting_device_uuid: &String, - collection_uuids: Option>, + collection_uuids: Option>, conn: &mut DbConn, ) { // Skip any processing if both WebSockets and Push are not active @@ -428,7 +428,7 @@ impl WebSocketUsers { let (user_uuid, collection_uuids, revision_date) = if let Some(collection_uuids) = collection_uuids { ( Value::Nil, - Value::Array(collection_uuids.into_iter().map(|v| v.into()).collect::>()), + Value::Array(collection_uuids.into_iter().map(|v| v.to_string().into()).collect::>()), serialize_date(Utc::now().naive_utc()), ) } else { diff --git a/src/auth.rs b/src/auth.rs index 3f92b127e1..2a7c33d686 100644 --- a/src/auth.rs +++ b/src/auth.rs @@ -14,7 +14,7 @@ use std::{ net::IpAddr, }; -use crate::db::models::{MembershipId, OrganizationId, UserId}; +use crate::db::models::{CollectionId, MembershipId, OrganizationId, UserId}; use crate::{error::Error, CONFIG}; const JWT_ALGORITHM: Algorithm = Algorithm::RS256; @@ -649,16 +649,16 @@ impl From for Headers { // col_id is usually the fourth path param ("/organizations//collections/"), // but there could be cases where it is a query value. // First check the path, if this is not a valid uuid, try the query values. -fn get_col_id(request: &Request<'_>) -> Option { +fn get_col_id(request: &Request<'_>) -> Option { if let Some(Ok(col_id)) = request.param::(3) { if uuid::Uuid::parse_str(&col_id).is_ok() { - return Some(col_id); + return Some(col_id.into()); } } if let Some(Ok(col_id)) = request.query_value::("collectionId") { if uuid::Uuid::parse_str(&col_id).is_ok() { - return Some(col_id); + return Some(col_id.into()); } } @@ -763,11 +763,11 @@ impl From for Headers { impl ManagerHeaders { pub async fn from_loose( h: ManagerHeadersLoose, - collections: &Vec, + collections: &Vec, conn: &mut DbConn, ) -> Result { for col_id in collections { - if uuid::Uuid::parse_str(col_id).is_err() { + if uuid::Uuid::parse_str(col_id.as_ref()).is_err() { err!("Collection Id is malformed!"); } if !Collection::can_access_collection(&h.membership, col_id, conn).await { diff --git a/src/db/models/cipher.rs b/src/db/models/cipher.rs index db75d2bf1d..56e272bfba 100644 --- a/src/db/models/cipher.rs +++ b/src/db/models/cipher.rs @@ -4,8 +4,8 @@ use chrono::{NaiveDateTime, TimeDelta, Utc}; use serde_json::Value; use super::{ - Attachment, CollectionCipher, Favorite, FolderCipher, Group, Membership, MembershipStatus, MembershipType, - OrganizationId, User, UserId, + Attachment, CollectionCipher, CollectionId, Favorite, FolderCipher, Group, Membership, MembershipStatus, + MembershipType, OrganizationId, User, UserId, }; use crate::api::core::{CipherData, CipherSyncData, CipherSyncType}; @@ -862,7 +862,7 @@ impl Cipher { }} } - pub async fn get_collections(&self, user_id: String, conn: &mut DbConn) -> Vec { + pub async fn get_collections(&self, user_id: String, conn: &mut DbConn) -> Vec { if CONFIG.org_groups_enabled() { db_run! {conn: { ciphers_collections::table @@ -894,7 +894,7 @@ impl Cipher { .and(collections_groups::read_only.eq(false))) ) .select(ciphers_collections::collection_uuid) - .load::(conn).unwrap_or_default() + .load::(conn).unwrap_or_default() }} } else { db_run! {conn: { @@ -916,12 +916,12 @@ impl Cipher { .and(users_collections::read_only.eq(false))) ) .select(ciphers_collections::collection_uuid) - .load::(conn).unwrap_or_default() + .load::(conn).unwrap_or_default() }} } } - pub async fn get_admin_collections(&self, user_id: String, conn: &mut DbConn) -> Vec { + pub async fn get_admin_collections(&self, user_id: String, conn: &mut DbConn) -> Vec { if CONFIG.org_groups_enabled() { db_run! {conn: { ciphers_collections::table @@ -954,7 +954,7 @@ impl Cipher { .or(users_organizations::atype.le(MembershipType::Admin as i32)) // User is admin or owner ) .select(ciphers_collections::collection_uuid) - .load::(conn).unwrap_or_default() + .load::(conn).unwrap_or_default() }} } else { db_run! {conn: { @@ -977,14 +977,17 @@ impl Cipher { .or(users_organizations::atype.le(MembershipType::Admin as i32)) // User is admin or owner ) .select(ciphers_collections::collection_uuid) - .load::(conn).unwrap_or_default() + .load::(conn).unwrap_or_default() }} } } /// Return a Vec with (cipher_uuid, collection_uuid) /// This is used during a full sync so we only need one query for all collections accessible. - pub async fn get_collections_with_cipher_by_user(user_id: String, conn: &mut DbConn) -> Vec<(String, String)> { + pub async fn get_collections_with_cipher_by_user( + user_id: String, + conn: &mut DbConn, + ) -> Vec<(String, CollectionId)> { db_run! {conn: { ciphers_collections::table .inner_join(collections::table.on( @@ -1018,7 +1021,7 @@ impl Cipher { .or_filter(collections_groups::collections_uuid.is_not_null()) //Access via group .select(ciphers_collections::all_columns) .distinct() - .load::<(String, String)>(conn).unwrap_or_default() + .load::<(String, CollectionId)>(conn).unwrap_or_default() }} } } diff --git a/src/db/models/collection.rs b/src/db/models/collection.rs index 9313585de5..ed3b762b50 100644 --- a/src/db/models/collection.rs +++ b/src/db/models/collection.rs @@ -1,4 +1,10 @@ +use rocket::request::FromParam; use serde_json::Value; +use std::{ + borrow::Borrow, + fmt::{Display, Formatter}, + ops::Deref, +}; use super::{CollectionGroup, GroupUser, Membership, MembershipStatus, MembershipType, OrganizationId, User, UserId}; use crate::CONFIG; @@ -8,7 +14,7 @@ db_object! { #[diesel(table_name = collections)] #[diesel(primary_key(uuid))] pub struct Collection { - pub uuid: String, + pub uuid: CollectionId, pub org_uuid: OrganizationId, pub name: String, pub external_id: Option, @@ -19,7 +25,7 @@ db_object! { #[diesel(primary_key(user_uuid, collection_uuid))] pub struct CollectionUser { pub user_uuid: UserId, - pub collection_uuid: String, + pub collection_uuid: CollectionId, pub read_only: bool, pub hide_passwords: bool, } @@ -29,7 +35,7 @@ db_object! { #[diesel(primary_key(cipher_uuid, collection_uuid))] pub struct CollectionCipher { pub cipher_uuid: String, - pub collection_uuid: String, + pub collection_uuid: CollectionId, } } @@ -37,7 +43,7 @@ db_object! { impl Collection { pub fn new(org_uuid: OrganizationId, name: String, external_id: Option) -> Self { let mut new_model = Self { - uuid: crate::util::get_uuid(), + uuid: CollectionId(crate::util::get_uuid()), org_uuid, name, external_id: None, @@ -121,7 +127,7 @@ impl Collection { json_object } - pub async fn can_access_collection(member: &Membership, col_id: &str, conn: &mut DbConn) -> bool { + pub async fn can_access_collection(member: &Membership, col_id: &CollectionId, conn: &mut DbConn) -> bool { member.has_status(MembershipStatus::Confirmed) && (member.has_full_access() || CollectionUser::has_access_to_collection_by_user(col_id, &member.user_uuid, conn).await @@ -198,7 +204,7 @@ impl Collection { } } - pub async fn find_by_uuid(uuid: &str, conn: &mut DbConn) -> Option { + pub async fn find_by_uuid(uuid: &CollectionId, conn: &mut DbConn) -> Option { db_run! { conn: { collections::table .filter(collections::uuid.eq(uuid)) @@ -312,7 +318,11 @@ impl Collection { }} } - pub async fn find_by_uuid_and_org(uuid: &str, org_uuid: &OrganizationId, conn: &mut DbConn) -> Option { + pub async fn find_by_uuid_and_org( + uuid: &CollectionId, + org_uuid: &OrganizationId, + conn: &mut DbConn, + ) -> Option { db_run! { conn: { collections::table .filter(collections::uuid.eq(uuid)) @@ -324,7 +334,7 @@ impl Collection { }} } - pub async fn find_by_uuid_and_user(uuid: &str, user_uuid: UserId, conn: &mut DbConn) -> Option { + pub async fn find_by_uuid_and_user(uuid: &CollectionId, user_uuid: UserId, conn: &mut DbConn) -> Option { if CONFIG.org_groups_enabled() { db_run! { conn: { collections::table @@ -534,7 +544,7 @@ impl CollectionUser { pub async fn save( user_uuid: &UserId, - collection_uuid: &str, + collection_uuid: &CollectionId, read_only: bool, hide_passwords: bool, conn: &mut DbConn, @@ -604,7 +614,7 @@ impl CollectionUser { }} } - pub async fn find_by_collection(collection_uuid: &str, conn: &mut DbConn) -> Vec { + pub async fn find_by_collection(collection_uuid: &CollectionId, conn: &mut DbConn) -> Vec { db_run! { conn: { users_collections::table .filter(users_collections::collection_uuid.eq(collection_uuid)) @@ -616,7 +626,7 @@ impl CollectionUser { } pub async fn find_by_collection_swap_user_uuid_with_member_uuid( - collection_uuid: &str, + collection_uuid: &CollectionId, conn: &mut DbConn, ) -> Vec { db_run! { conn: { @@ -631,7 +641,7 @@ impl CollectionUser { } pub async fn find_by_collection_and_user( - collection_uuid: &str, + collection_uuid: &CollectionId, user_uuid: &UserId, conn: &mut DbConn, ) -> Option { @@ -657,7 +667,7 @@ impl CollectionUser { }} } - pub async fn delete_all_by_collection(collection_uuid: &str, conn: &mut DbConn) -> EmptyResult { + pub async fn delete_all_by_collection(collection_uuid: &CollectionId, conn: &mut DbConn) -> EmptyResult { for collection in CollectionUser::find_by_collection(collection_uuid, conn).await.iter() { User::update_uuid_revision(&collection.user_uuid, conn).await; } @@ -689,14 +699,18 @@ impl CollectionUser { }} } - pub async fn has_access_to_collection_by_user(col_id: &str, user_uuid: &UserId, conn: &mut DbConn) -> bool { + pub async fn has_access_to_collection_by_user( + col_id: &CollectionId, + user_uuid: &UserId, + conn: &mut DbConn, + ) -> bool { Self::find_by_collection_and_user(col_id, user_uuid, conn).await.is_some() } } /// Database methods impl CollectionCipher { - pub async fn save(cipher_uuid: &str, collection_uuid: &str, conn: &mut DbConn) -> EmptyResult { + pub async fn save(cipher_uuid: &str, collection_uuid: &CollectionId, conn: &mut DbConn) -> EmptyResult { Self::update_users_revision(collection_uuid, conn).await; db_run! { conn: @@ -726,7 +740,7 @@ impl CollectionCipher { } } - pub async fn delete(cipher_uuid: &str, collection_uuid: &str, conn: &mut DbConn) -> EmptyResult { + pub async fn delete(cipher_uuid: &str, collection_uuid: &CollectionId, conn: &mut DbConn) -> EmptyResult { Self::update_users_revision(collection_uuid, conn).await; db_run! { conn: { @@ -748,7 +762,7 @@ impl CollectionCipher { }} } - pub async fn delete_all_by_collection(collection_uuid: &str, conn: &mut DbConn) -> EmptyResult { + pub async fn delete_all_by_collection(collection_uuid: &CollectionId, conn: &mut DbConn) -> EmptyResult { db_run! { conn: { diesel::delete(ciphers_collections::table.filter(ciphers_collections::collection_uuid.eq(collection_uuid))) .execute(conn) @@ -756,9 +770,57 @@ impl CollectionCipher { }} } - pub async fn update_users_revision(collection_uuid: &str, conn: &mut DbConn) { + pub async fn update_users_revision(collection_uuid: &CollectionId, conn: &mut DbConn) { if let Some(collection) = Collection::find_by_uuid(collection_uuid, conn).await { collection.update_users_revision(conn).await; } } } + +#[derive(DieselNewType, FromForm, Clone, Debug, Hash, PartialEq, Eq, Serialize, Deserialize)] +pub struct CollectionId(String); + +impl AsRef for CollectionId { + fn as_ref(&self) -> &str { + &self.0 + } +} + +impl Deref for CollectionId { + type Target = str; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl Borrow for CollectionId { + fn borrow(&self) -> &str { + &self.0 + } +} + +impl Display for CollectionId { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.0) + } +} + +impl From for CollectionId { + fn from(raw: String) -> Self { + Self(raw) + } +} + +impl<'r> FromParam<'r> for CollectionId { + 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/group.rs b/src/db/models/group.rs index 0a6cca10f4..f742b64bb0 100644 --- a/src/db/models/group.rs +++ b/src/db/models/group.rs @@ -1,4 +1,4 @@ -use super::{Membership, MembershipId, OrganizationId, User, UserId}; +use super::{CollectionId, Membership, MembershipId, OrganizationId, User, UserId}; use crate::api::EmptyResult; use crate::db::DbConn; use crate::error::MapResult; @@ -23,7 +23,7 @@ db_object! { #[diesel(table_name = collections_groups)] #[diesel(primary_key(collections_uuid, groups_uuid))] pub struct CollectionGroup { - pub collections_uuid: String, + pub collections_uuid: CollectionId, pub groups_uuid: String, pub read_only: bool, pub hide_passwords: bool, @@ -113,7 +113,7 @@ impl Group { } impl CollectionGroup { - pub fn new(collections_uuid: String, groups_uuid: String, read_only: bool, hide_passwords: bool) -> Self { + pub fn new(collections_uuid: CollectionId, groups_uuid: String, read_only: bool, hide_passwords: bool) -> Self { Self { collections_uuid, groups_uuid, @@ -370,7 +370,7 @@ impl CollectionGroup { }} } - pub async fn find_by_collection(collection_uuid: &str, conn: &mut DbConn) -> Vec { + pub async fn find_by_collection(collection_uuid: &CollectionId, conn: &mut DbConn) -> Vec { db_run! { conn: { collections_groups::table .filter(collections_groups::collections_uuid.eq(collection_uuid)) @@ -410,7 +410,7 @@ impl CollectionGroup { }} } - pub async fn delete_all_by_collection(collection_uuid: &str, conn: &mut DbConn) -> EmptyResult { + pub async fn delete_all_by_collection(collection_uuid: &CollectionId, conn: &mut DbConn) -> EmptyResult { let collection_assigned_to_groups = CollectionGroup::find_by_collection(collection_uuid, conn).await; for collection_assigned_to_group in collection_assigned_to_groups { let group_users = GroupUser::find_by_group(&collection_assigned_to_group.groups_uuid, conn).await; @@ -496,7 +496,7 @@ impl GroupUser { } pub async fn has_access_to_collection_by_member( - collection_uuid: &str, + collection_uuid: &CollectionId, member_uuid: &MembershipId, conn: &mut DbConn, ) -> bool { diff --git a/src/db/models/mod.rs b/src/db/models/mod.rs index 7042b2941d..a96c5bb938 100644 --- a/src/db/models/mod.rs +++ b/src/db/models/mod.rs @@ -19,7 +19,7 @@ mod user; pub use self::attachment::Attachment; pub use self::auth_request::AuthRequest; pub use self::cipher::{Cipher, RepromptType}; -pub use self::collection::{Collection, CollectionCipher, CollectionUser}; +pub use self::collection::{Collection, CollectionCipher, CollectionId, CollectionUser}; pub use self::device::{Device, DeviceType}; pub use self::emergency_access::{EmergencyAccess, EmergencyAccessStatus, EmergencyAccessType}; pub use self::event::{Event, EventType}; diff --git a/src/db/models/organization.rs b/src/db/models/organization.rs index db991fe119..9e39161a87 100644 --- a/src/db/models/organization.rs +++ b/src/db/models/organization.rs @@ -10,8 +10,10 @@ use std::{ ops::Deref, }; -use super::{CollectionUser, Group, GroupUser, OrgPolicy, OrgPolicyType, TwoFactor, User, UserId}; -use crate::db::models::{Collection, CollectionGroup}; +use super::{ + Collection, CollectionGroup, CollectionId, CollectionUser, Group, GroupUser, OrgPolicy, OrgPolicyType, TwoFactor, + User, UserId, +}; use crate::CONFIG; db_object! { @@ -474,7 +476,7 @@ impl Membership { // If collections are to be included, only include them if the user does not have full access via a group or defined to the user it self let collections: Vec = if include_collections && !(full_access_group || self.has_full_access()) { // Get all collections for the user here already to prevent more queries - let cu: HashMap = + let cu: HashMap = CollectionUser::find_by_organization_and_user_uuid(&self.org_uuid, &self.user_uuid, conn) .await .into_iter() @@ -482,7 +484,7 @@ impl Membership { .collect(); // Get all collection groups for this user to prevent there inclusion - let cg: HashSet = CollectionGroup::find_by_user(&self.user_uuid, conn) + let cg: HashSet = CollectionGroup::find_by_user(&self.user_uuid, conn) .await .into_iter() .map(|cg| cg.collections_uuid) @@ -961,7 +963,7 @@ impl Membership { } pub async fn find_by_collection_and_org( - collection_uuid: &str, + collection_uuid: &CollectionId, org_uuid: &OrganizationId, conn: &mut DbConn, ) -> Vec { From 189fd7780619cfe14ce6250c23363cc6f56df7d3 Mon Sep 17 00:00:00 2001 From: Stefan Melmuk Date: Sat, 21 Dec 2024 13:31:47 +0100 Subject: [PATCH 06/25] introduce group_id newtype pattern --- src/api/core/organizations.rs | 97 ++++++++++++++++++----------------- src/db/models/group.rs | 82 ++++++++++++++++++++++++----- src/db/models/mod.rs | 2 +- src/db/models/organization.rs | 6 +-- 4 files changed, 123 insertions(+), 64 deletions(-) diff --git a/src/api/core/organizations.rs b/src/api/core/organizations.rs index b7ac7dcb19..6b5048bc75 100644 --- a/src/api/core/organizations.rs +++ b/src/api/core/organizations.rs @@ -124,7 +124,7 @@ struct OrganizationUpdateData { #[serde(rename_all = "camelCase")] struct NewCollectionData { name: String, - groups: Vec, + groups: Vec, users: Vec, id: Option, external_id: Option, @@ -132,9 +132,9 @@ struct NewCollectionData { #[derive(Deserialize)] #[serde(rename_all = "camelCase")] -struct NewCollectionObjectData { +struct NewCollectionGroupData { hide_passwords: bool, - id: String, + id: GroupId, read_only: bool, } @@ -155,8 +155,8 @@ struct OrgKeyData { #[derive(Deserialize, Debug)] #[serde(rename_all = "camelCase")] -struct OrgBulkIds { - ids: Vec, +struct BulkGroupIds { + ids: Vec, } #[derive(Deserialize, Debug)] @@ -367,7 +367,7 @@ async fn get_org_collections_details( .await .iter() .map(|collection_group| { - SelectionReadOnly::to_collection_group_details_read_only(collection_group).to_json() + GroupSelection::to_collection_group_details_read_only(collection_group).to_json() }) .collect() } else { @@ -651,7 +651,7 @@ async fn get_org_collection_detail( .await .iter() .map(|collection_group| { - SelectionReadOnly::to_collection_group_details_read_only(collection_group).to_json() + GroupSelection::to_collection_group_details_read_only(collection_group).to_json() }) .collect() } else { @@ -856,7 +856,7 @@ struct MembershipData { #[serde(rename_all = "camelCase")] struct InviteData { emails: Vec, - groups: Vec, + groups: Vec, r#type: NumberOrString, collections: Option>, #[serde(default)] @@ -942,8 +942,8 @@ async fn send_invite( new_member.save(&mut conn).await?; - for group in data.groups.iter() { - let mut group_entry = GroupUser::new(String::from(group), new_member.uuid.clone()); + for group_id in data.groups.iter() { + let mut group_entry = GroupUser::new(group_id.clone(), new_member.uuid.clone()); group_entry.save(&mut conn).await?; } @@ -1330,7 +1330,7 @@ async fn get_user( struct EditUserData { r#type: NumberOrString, collections: Option>, - groups: Option>, + groups: Option>, #[serde(default)] access_all: bool, } @@ -1432,8 +1432,8 @@ async fn edit_user( GroupUser::delete_all_by_member(&member_to_edit.uuid, &mut conn).await?; - for group in data.groups.iter().flatten() { - let mut group_entry = GroupUser::new(String::from(group), member_to_edit.uuid.clone()); + for group_id in data.groups.iter().flatten() { + let mut group_entry = GroupUser::new(group_id.clone(), member_to_edit.uuid.clone()); group_entry.save(&mut conn).await?; } @@ -2379,13 +2379,13 @@ impl GroupRequest { #[derive(Deserialize, Serialize)] #[serde(rename_all = "camelCase")] -struct SelectionReadOnly { - id: String, +struct GroupSelection { + id: GroupId, read_only: bool, hide_passwords: bool, } -impl SelectionReadOnly { +impl GroupSelection { pub fn to_collection_group_details_read_only(collection_group: &CollectionGroup) -> Self { Self { id: collection_group.groups_uuid.clone(), @@ -2408,7 +2408,7 @@ struct CollectionSelection { } impl CollectionSelection { - pub fn to_collection_group(&self, groups_uuid: String) -> CollectionGroup { + pub fn to_collection_group(&self, groups_uuid: GroupId) -> CollectionGroup { CollectionGroup::new(self.id.clone(), groups_uuid, self.read_only, self.hide_passwords) } } @@ -2438,7 +2438,7 @@ impl UserSelection { #[post("/organizations//groups/", data = "")] async fn post_group( org_id: OrganizationId, - group_id: &str, + group_id: GroupId, data: Json, headers: AdminHeaders, conn: DbConn, @@ -2477,7 +2477,7 @@ async fn post_groups( #[put("/organizations//groups/", data = "")] async fn put_group( org_id: OrganizationId, - group_id: &str, + group_id: GroupId, data: Json, headers: AdminHeaders, mut conn: DbConn, @@ -2486,15 +2486,15 @@ async fn put_group( err!("Group support is disabled"); } - let Some(group) = Group::find_by_uuid_and_org(group_id, &org_id, &mut conn).await else { + let Some(group) = Group::find_by_uuid_and_org(&group_id, &org_id, &mut conn).await else { err!("Group not found", "Group uuid is invalid or does not belong to the organization") }; let group_request = data.into_inner(); let updated_group = group_request.update_group(group); - CollectionGroup::delete_all_by_group(group_id, &mut conn).await?; - GroupUser::delete_all_by_group(group_id, &mut conn).await?; + CollectionGroup::delete_all_by_group(&group_id, &mut conn).await?; + GroupUser::delete_all_by_group(&group_id, &mut conn).await?; log_event( EventType::GroupUpdated as i32, @@ -2553,7 +2553,7 @@ async fn add_update_group( #[get("/organizations//groups//details")] async fn get_group_details( org_id: OrganizationId, - group_id: &str, + group_id: GroupId, _headers: AdminHeaders, mut conn: DbConn, ) -> JsonResult { @@ -2561,7 +2561,7 @@ async fn get_group_details( err!("Group support is disabled"); } - let Some(group) = Group::find_by_uuid_and_org(group_id, &org_id, &mut conn).await else { + let Some(group) = Group::find_by_uuid_and_org(&group_id, &org_id, &mut conn).await else { err!("Group not found", "Group uuid is invalid or does not belong to the organization") }; @@ -2571,21 +2571,26 @@ async fn get_group_details( #[post("/organizations//groups//delete")] async fn post_delete_group( org_id: OrganizationId, - group_id: &str, + group_id: GroupId, headers: AdminHeaders, mut conn: DbConn, ) -> EmptyResult { - _delete_group(&org_id, group_id, &headers, &mut conn).await + _delete_group(&org_id, &group_id, &headers, &mut conn).await } #[delete("/organizations//groups/")] -async fn delete_group(org_id: OrganizationId, group_id: &str, headers: AdminHeaders, mut conn: DbConn) -> EmptyResult { - _delete_group(&org_id, group_id, &headers, &mut conn).await +async fn delete_group( + org_id: OrganizationId, + group_id: GroupId, + headers: AdminHeaders, + mut conn: DbConn, +) -> EmptyResult { + _delete_group(&org_id, &group_id, &headers, &mut conn).await } async fn _delete_group( org_id: &OrganizationId, - group_id: &str, + group_id: &GroupId, headers: &AdminHeaders, conn: &mut DbConn, ) -> EmptyResult { @@ -2614,7 +2619,7 @@ async fn _delete_group( #[delete("/organizations//groups", data = "")] async fn bulk_delete_groups( org_id: OrganizationId, - data: Json, + data: Json, headers: AdminHeaders, mut conn: DbConn, ) -> EmptyResult { @@ -2622,7 +2627,7 @@ async fn bulk_delete_groups( err!("Group support is disabled"); } - let data: OrgBulkIds = data.into_inner(); + let data: BulkGroupIds = data.into_inner(); for group_id in data.ids { _delete_group(&org_id, &group_id, &headers, &mut conn).await? @@ -2631,12 +2636,12 @@ async fn bulk_delete_groups( } #[get("/organizations//groups/")] -async fn get_group(org_id: OrganizationId, group_id: &str, _headers: AdminHeaders, mut conn: DbConn) -> JsonResult { +async fn get_group(org_id: OrganizationId, group_id: GroupId, _headers: AdminHeaders, mut conn: DbConn) -> JsonResult { if !CONFIG.org_groups_enabled() { err!("Group support is disabled"); } - let Some(group) = Group::find_by_uuid_and_org(group_id, &org_id, &mut conn).await else { + let Some(group) = Group::find_by_uuid_and_org(&group_id, &org_id, &mut conn).await else { err!("Group not found", "Group uuid is invalid or does not belong to the organization") }; @@ -2646,7 +2651,7 @@ async fn get_group(org_id: OrganizationId, group_id: &str, _headers: AdminHeader #[get("/organizations//groups//users")] async fn get_group_users( org_id: OrganizationId, - group_id: &str, + group_id: GroupId, _headers: AdminHeaders, mut conn: DbConn, ) -> JsonResult { @@ -2654,11 +2659,11 @@ async fn get_group_users( err!("Group support is disabled"); } - if Group::find_by_uuid_and_org(group_id, &org_id, &mut conn).await.is_none() { + if Group::find_by_uuid_and_org(&&group_id, &org_id, &mut conn).await.is_none() { err!("Group could not be found!", "Group uuid is invalid or does not belong to the organization") }; - let group_users: Vec = GroupUser::find_by_group(group_id, &mut conn) + let group_users: Vec = GroupUser::find_by_group(&group_id, &mut conn) .await .iter() .map(|entry| entry.users_organizations_uuid.clone()) @@ -2670,7 +2675,7 @@ async fn get_group_users( #[put("/organizations//groups//users", data = "")] async fn put_group_users( org_id: OrganizationId, - group_id: &str, + group_id: GroupId, headers: AdminHeaders, data: Json>, mut conn: DbConn, @@ -2679,15 +2684,15 @@ async fn put_group_users( err!("Group support is disabled"); } - if Group::find_by_uuid_and_org(group_id, &org_id, &mut conn).await.is_none() { + if Group::find_by_uuid_and_org(&group_id, &org_id, &mut conn).await.is_none() { err!("Group could not be found!", "Group uuid is invalid or does not belong to the organization") }; - GroupUser::delete_all_by_group(group_id, &mut conn).await?; + GroupUser::delete_all_by_group(&group_id, &mut conn).await?; let assigned_members = data.into_inner(); for assigned_member in assigned_members { - let mut user_entry = GroupUser::new(String::from(group_id), assigned_member.clone()); + let mut user_entry = GroupUser::new(group_id.clone(), assigned_member.clone()); user_entry.save(&mut conn).await?; log_event( @@ -2720,7 +2725,7 @@ async fn get_user_groups( err!("User could not be found!") }; - let user_groups: Vec = + let user_groups: Vec = GroupUser::find_by_member(&member_id, &mut conn).await.iter().map(|entry| entry.groups_uuid.clone()).collect(); Ok(Json(json!(user_groups))) @@ -2729,7 +2734,7 @@ async fn get_user_groups( #[derive(Deserialize)] #[serde(rename_all = "camelCase")] struct OrganizationUserUpdateGroupsRequest { - group_ids: Vec, + group_ids: Vec, } #[post("/organizations//users//groups", data = "")] @@ -2784,7 +2789,7 @@ async fn put_user_groups( #[post("/organizations//groups//delete-user/")] async fn post_delete_group_user( org_id: OrganizationId, - group_id: &str, + group_id: GroupId, member_id: MembershipId, headers: AdminHeaders, conn: DbConn, @@ -2795,7 +2800,7 @@ async fn post_delete_group_user( #[delete("/organizations//groups//users/")] async fn delete_group_user( org_id: OrganizationId, - group_id: &str, + group_id: GroupId, member_id: MembershipId, headers: AdminHeaders, mut conn: DbConn, @@ -2808,7 +2813,7 @@ async fn delete_group_user( err!("User could not be found or does not belong to the organization."); } - if Group::find_by_uuid_and_org(group_id, &org_id, &mut conn).await.is_none() { + if Group::find_by_uuid_and_org(&group_id, &org_id, &mut conn).await.is_none() { err!("Group could not be found or does not belong to the organization."); } @@ -2823,7 +2828,7 @@ async fn delete_group_user( ) .await; - GroupUser::delete_by_group_and_member(group_id, &member_id, &mut conn).await + GroupUser::delete_by_group_and_member(&group_id, &member_id, &mut conn).await } #[derive(Deserialize)] diff --git a/src/db/models/group.rs b/src/db/models/group.rs index f742b64bb0..1ed23f04e9 100644 --- a/src/db/models/group.rs +++ b/src/db/models/group.rs @@ -3,14 +3,20 @@ use crate::api::EmptyResult; use crate::db::DbConn; use crate::error::MapResult; use chrono::{NaiveDateTime, Utc}; +use rocket::request::FromParam; use serde_json::Value; +use std::{ + borrow::Borrow, + fmt::{Display, Formatter}, + ops::Deref, +}; db_object! { #[derive(Identifiable, Queryable, Insertable, AsChangeset)] #[diesel(table_name = groups)] #[diesel(primary_key(uuid))] pub struct Group { - pub uuid: String, + pub uuid: GroupId, pub organizations_uuid: OrganizationId, pub name: String, pub access_all: bool, @@ -24,7 +30,7 @@ db_object! { #[diesel(primary_key(collections_uuid, groups_uuid))] pub struct CollectionGroup { pub collections_uuid: CollectionId, - pub groups_uuid: String, + pub groups_uuid: GroupId, pub read_only: bool, pub hide_passwords: bool, } @@ -33,7 +39,7 @@ db_object! { #[diesel(table_name = groups_users)] #[diesel(primary_key(groups_uuid, users_organizations_uuid))] pub struct GroupUser { - pub groups_uuid: String, + pub groups_uuid: GroupId, pub users_organizations_uuid: MembershipId } } @@ -49,7 +55,7 @@ impl Group { let now = Utc::now().naive_utc(); let mut new_model = Self { - uuid: crate::util::get_uuid(), + uuid: GroupId(crate::util::get_uuid()), organizations_uuid, name, access_all, @@ -113,7 +119,7 @@ impl Group { } impl CollectionGroup { - pub fn new(collections_uuid: CollectionId, groups_uuid: String, read_only: bool, hide_passwords: bool) -> Self { + pub fn new(collections_uuid: CollectionId, groups_uuid: GroupId, read_only: bool, hide_passwords: bool) -> Self { Self { collections_uuid, groups_uuid, @@ -124,7 +130,7 @@ impl CollectionGroup { } impl GroupUser { - pub fn new(groups_uuid: String, users_organizations_uuid: MembershipId) -> Self { + pub fn new(groups_uuid: GroupId, users_organizations_uuid: MembershipId) -> Self { Self { groups_uuid, users_organizations_uuid, @@ -196,7 +202,7 @@ impl Group { }} } - pub async fn find_by_uuid_and_org(uuid: &str, org_uuid: &OrganizationId, conn: &mut DbConn) -> Option { + pub async fn find_by_uuid_and_org(uuid: &GroupId, org_uuid: &OrganizationId, conn: &mut DbConn) -> Option { db_run! { conn: { groups::table .filter(groups::uuid.eq(uuid)) @@ -269,13 +275,13 @@ impl Group { }} } - pub async fn update_revision(uuid: &str, conn: &mut DbConn) { + pub async fn update_revision(uuid: &GroupId, conn: &mut DbConn) { if let Err(e) = Self::_update_revision(uuid, &Utc::now().naive_utc(), conn).await { warn!("Failed to update revision for {}: {:#?}", uuid, e); } } - async fn _update_revision(uuid: &str, date: &NaiveDateTime, conn: &mut DbConn) -> EmptyResult { + async fn _update_revision(uuid: &GroupId, date: &NaiveDateTime, conn: &mut DbConn) -> EmptyResult { db_run! {conn: { crate::util::retry(|| { diesel::update(groups::table.filter(groups::uuid.eq(uuid))) @@ -343,7 +349,7 @@ impl CollectionGroup { } } - pub async fn find_by_group(group_uuid: &str, conn: &mut DbConn) -> Vec { + pub async fn find_by_group(group_uuid: &GroupId, conn: &mut DbConn) -> Vec { db_run! { conn: { collections_groups::table .filter(collections_groups::groups_uuid.eq(group_uuid)) @@ -396,7 +402,7 @@ impl CollectionGroup { }} } - pub async fn delete_all_by_group(group_uuid: &str, conn: &mut DbConn) -> EmptyResult { + pub async fn delete_all_by_group(group_uuid: &GroupId, conn: &mut DbConn) -> EmptyResult { let group_users = GroupUser::find_by_group(group_uuid, conn).await; for group_user in group_users { group_user.update_user_revision(conn).await; @@ -475,7 +481,7 @@ impl GroupUser { } } - pub async fn find_by_group(group_uuid: &str, conn: &mut DbConn) -> Vec { + pub async fn find_by_group(group_uuid: &GroupId, conn: &mut DbConn) -> Vec { db_run! { conn: { groups_users::table .filter(groups_users::groups_uuid.eq(group_uuid)) @@ -540,7 +546,7 @@ impl GroupUser { } pub async fn delete_by_group_and_member( - group_uuid: &str, + group_uuid: &GroupId, member_uuid: &MembershipId, conn: &mut DbConn, ) -> EmptyResult { @@ -558,7 +564,7 @@ impl GroupUser { }} } - pub async fn delete_all_by_group(group_uuid: &str, conn: &mut DbConn) -> EmptyResult { + pub async fn delete_all_by_group(group_uuid: &GroupId, conn: &mut DbConn) -> EmptyResult { let group_users = GroupUser::find_by_group(group_uuid, conn).await; for group_user in group_users { group_user.update_user_revision(conn).await; @@ -586,3 +592,51 @@ impl GroupUser { }} } } + +#[derive(DieselNewType, FromForm, Clone, Debug, Hash, PartialEq, Eq, Serialize, Deserialize)] +pub struct GroupId(String); + +impl AsRef for GroupId { + fn as_ref(&self) -> &str { + &self.0 + } +} + +impl Deref for GroupId { + type Target = str; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl Borrow for GroupId { + fn borrow(&self) -> &str { + &self.0 + } +} + +impl Display for GroupId { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.0) + } +} + +impl From for GroupId { + fn from(raw: String) -> Self { + Self(raw) + } +} + +impl<'r> FromParam<'r> for GroupId { + 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 a96c5bb938..e10951e58d 100644 --- a/src/db/models/mod.rs +++ b/src/db/models/mod.rs @@ -25,7 +25,7 @@ pub use self::emergency_access::{EmergencyAccess, EmergencyAccessStatus, Emergen pub use self::event::{Event, EventType}; pub use self::favorite::Favorite; pub use self::folder::{Folder, FolderCipher}; -pub use self::group::{CollectionGroup, Group, GroupUser}; +pub use self::group::{CollectionGroup, Group, GroupId, GroupUser}; pub use self::org_policy::{OrgPolicy, OrgPolicyErr, OrgPolicyType}; pub use self::organization::{ Membership, MembershipId, MembershipStatus, MembershipType, Organization, OrganizationApiKey, OrganizationId, diff --git a/src/db/models/organization.rs b/src/db/models/organization.rs index 9e39161a87..016bd3dacc 100644 --- a/src/db/models/organization.rs +++ b/src/db/models/organization.rs @@ -11,8 +11,8 @@ use std::{ }; use super::{ - Collection, CollectionGroup, CollectionId, CollectionUser, Group, GroupUser, OrgPolicy, OrgPolicyType, TwoFactor, - User, UserId, + Collection, CollectionGroup, CollectionId, CollectionUser, Group, GroupId, GroupUser, OrgPolicy, OrgPolicyType, + TwoFactor, User, UserId, }; use crate::CONFIG; @@ -460,7 +460,7 @@ impl Membership { let twofactor_enabled = !TwoFactor::find_by_user(&user.uuid, conn).await.is_empty(); - let groups: Vec = if include_groups && CONFIG.org_groups_enabled() { + let groups: Vec = if include_groups && CONFIG.org_groups_enabled() { GroupUser::find_by_member(&self.uuid, conn).await.iter().map(|gu| gu.groups_uuid.clone()).collect() } else { // The Bitwarden clients seem to call this API regardless of whether groups are enabled, From 8b8507f8cc4626dedac7272fdd89ddd18138cf63 Mon Sep 17 00:00:00 2001 From: Stefan Melmuk Date: Sat, 21 Dec 2024 15:03:30 +0100 Subject: [PATCH 07/25] introduce cipher_id newtype --- src/api/core/accounts.rs | 6 +- src/api/core/ciphers.rs | 172 ++++++++++++++++++---------------- src/api/core/events.rs | 18 ++-- src/api/core/organizations.rs | 6 +- src/api/notifications.rs | 2 +- src/api/web.rs | 7 +- src/auth.rs | 6 +- src/db/models/attachment.rs | 16 +++- src/db/models/cipher.rs | 74 +++++++++++++-- src/db/models/collection.rs | 12 ++- src/db/models/event.rs | 12 +-- src/db/models/favorite.rs | 19 ++-- src/db/models/folder.rs | 20 ++-- src/db/models/mod.rs | 2 +- src/db/models/organization.rs | 18 +++- 15 files changed, 240 insertions(+), 150 deletions(-) diff --git a/src/api/core/accounts.rs b/src/api/core/accounts.rs index 38dc657bb3..03d339a1c9 100644 --- a/src/api/core/accounts.rs +++ b/src/api/core/accounts.rs @@ -488,13 +488,13 @@ fn validate_keydata( existing_sends: &[Send], ) -> EmptyResult { // Check that we're correctly rotating all the user's ciphers - let existing_cipher_ids = existing_ciphers.iter().map(|c| c.uuid.as_str()).collect::>(); + let existing_cipher_ids = existing_ciphers.iter().map(|c| &c.uuid).collect::>(); let provided_cipher_ids = data .ciphers .iter() .filter(|c| c.organization_id.is_none()) - .filter_map(|c| c.id.as_deref()) - .collect::>(); + .filter_map(|c| c.id.as_ref()) + .collect::>(); if !provided_cipher_ids.is_superset(&existing_cipher_ids) { err!("All existing ciphers must be included in the rotation") } diff --git a/src/api/core/ciphers.rs b/src/api/core/ciphers.rs index 3fbddc452e..a391abdbc2 100644 --- a/src/api/core/ciphers.rs +++ b/src/api/core/ciphers.rs @@ -192,8 +192,8 @@ async fn get_ciphers(headers: Headers, mut conn: DbConn) -> Json { } #[get("/ciphers/")] -async fn get_cipher(uuid: &str, headers: Headers, mut conn: DbConn) -> JsonResult { - let Some(cipher) = Cipher::find_by_uuid(uuid, &mut conn).await else { +async fn get_cipher(uuid: CipherId, headers: Headers, mut conn: DbConn) -> JsonResult { + let Some(cipher) = Cipher::find_by_uuid(&uuid, &mut conn).await else { err!("Cipher doesn't exist") }; @@ -205,13 +205,13 @@ async fn get_cipher(uuid: &str, headers: Headers, mut conn: DbConn) -> JsonResul } #[get("/ciphers//admin")] -async fn get_cipher_admin(uuid: &str, headers: Headers, conn: DbConn) -> JsonResult { +async fn get_cipher_admin(uuid: CipherId, headers: Headers, conn: DbConn) -> JsonResult { // TODO: Implement this correctly get_cipher(uuid, headers, conn).await } #[get("/ciphers//details")] -async fn get_cipher_details(uuid: &str, headers: Headers, conn: DbConn) -> JsonResult { +async fn get_cipher_details(uuid: CipherId, headers: Headers, conn: DbConn) -> JsonResult { get_cipher(uuid, headers, conn).await } @@ -219,7 +219,7 @@ async fn get_cipher_details(uuid: &str, headers: Headers, conn: DbConn) -> JsonR #[serde(rename_all = "camelCase")] pub struct CipherData { // Id is optional as it is included only in bulk share - pub id: Option, + pub id: Option, // Folder id is not included in import pub folder_id: Option, // TODO: Some of these might appear all the time, no need for Option @@ -256,7 +256,7 @@ pub struct CipherData { // 'Attachments' is unused, contains map of {id: filename} #[allow(dead_code)] attachments: Option, - attachments2: Option>, + attachments2: Option>, // The revision datetime (in ISO 8601 format) of the client's local copy // of the cipher. This is used to prevent a client from updating a cipher @@ -620,7 +620,7 @@ async fn post_ciphers_import( /// Called when an org admin modifies an existing org cipher. #[put("/ciphers//admin", data = "")] async fn put_cipher_admin( - uuid: &str, + uuid: CipherId, data: Json, headers: Headers, conn: DbConn, @@ -631,7 +631,7 @@ async fn put_cipher_admin( #[post("/ciphers//admin", data = "")] async fn post_cipher_admin( - uuid: &str, + uuid: CipherId, data: Json, headers: Headers, conn: DbConn, @@ -641,13 +641,19 @@ async fn post_cipher_admin( } #[post("/ciphers/", data = "")] -async fn post_cipher(uuid: &str, data: Json, headers: Headers, conn: DbConn, nt: Notify<'_>) -> JsonResult { +async fn post_cipher( + uuid: CipherId, + data: Json, + headers: Headers, + conn: DbConn, + nt: Notify<'_>, +) -> JsonResult { put_cipher(uuid, data, headers, conn, nt).await } #[put("/ciphers/", data = "")] async fn put_cipher( - uuid: &str, + uuid: CipherId, data: Json, headers: Headers, mut conn: DbConn, @@ -655,7 +661,7 @@ async fn put_cipher( ) -> JsonResult { let data: CipherData = data.into_inner(); - let Some(mut cipher) = Cipher::find_by_uuid(uuid, &mut conn).await else { + let Some(mut cipher) = Cipher::find_by_uuid(&uuid, &mut conn).await else { err!("Cipher doesn't exist") }; @@ -674,21 +680,26 @@ async fn put_cipher( } #[post("/ciphers//partial", data = "")] -async fn post_cipher_partial(uuid: &str, data: Json, headers: Headers, conn: DbConn) -> JsonResult { +async fn post_cipher_partial( + uuid: CipherId, + data: Json, + headers: Headers, + conn: DbConn, +) -> JsonResult { put_cipher_partial(uuid, data, headers, conn).await } // Only update the folder and favorite for the user, since this cipher is read-only #[put("/ciphers//partial", data = "")] async fn put_cipher_partial( - uuid: &str, + uuid: CipherId, data: Json, headers: Headers, mut conn: DbConn, ) -> JsonResult { let data: PartialCipherData = data.into_inner(); - let Some(cipher) = Cipher::find_by_uuid(uuid, &mut conn).await else { + let Some(cipher) = Cipher::find_by_uuid(&uuid, &mut conn).await else { err!("Cipher doesn't exist") }; @@ -715,7 +726,7 @@ struct CollectionsAdminData { #[put("/ciphers//collections_v2", data = "")] async fn put_collections2_update( - uuid: &str, + uuid: CipherId, data: Json, headers: Headers, conn: DbConn, @@ -726,7 +737,7 @@ async fn put_collections2_update( #[post("/ciphers//collections_v2", data = "")] async fn post_collections2_update( - uuid: &str, + uuid: CipherId, data: Json, headers: Headers, conn: DbConn, @@ -742,7 +753,7 @@ async fn post_collections2_update( #[put("/ciphers//collections", data = "")] async fn put_collections_update( - uuid: &str, + uuid: CipherId, data: Json, headers: Headers, conn: DbConn, @@ -753,7 +764,7 @@ async fn put_collections_update( #[post("/ciphers//collections", data = "")] async fn post_collections_update( - uuid: &str, + uuid: CipherId, data: Json, headers: Headers, mut conn: DbConn, @@ -761,7 +772,7 @@ async fn post_collections_update( ) -> JsonResult { let data: CollectionsAdminData = data.into_inner(); - let Some(cipher) = Cipher::find_by_uuid(uuid, &mut conn).await else { + let Some(cipher) = Cipher::find_by_uuid(&uuid, &mut conn).await else { err!("Cipher doesn't exist") }; @@ -771,7 +782,7 @@ async fn post_collections_update( let posted_collections = HashSet::::from_iter(data.collection_ids); let current_collections = - HashSet::::from_iter(cipher.get_collections(headers.user.uuid.to_string(), &mut conn).await); + HashSet::::from_iter(cipher.get_collections(headers.user.uuid.clone(), &mut conn).await); for collection in posted_collections.symmetric_difference(¤t_collections) { match Collection::find_by_uuid_and_org(collection, cipher.organization_uuid.as_ref().unwrap(), &mut conn).await @@ -819,7 +830,7 @@ async fn post_collections_update( #[put("/ciphers//collections-admin", data = "")] async fn put_collections_admin( - uuid: &str, + uuid: CipherId, data: Json, headers: Headers, conn: DbConn, @@ -830,7 +841,7 @@ async fn put_collections_admin( #[post("/ciphers//collections-admin", data = "")] async fn post_collections_admin( - uuid: &str, + uuid: CipherId, data: Json, headers: Headers, mut conn: DbConn, @@ -838,7 +849,7 @@ async fn post_collections_admin( ) -> EmptyResult { let data: CollectionsAdminData = data.into_inner(); - let Some(cipher) = Cipher::find_by_uuid(uuid, &mut conn).await else { + let Some(cipher) = Cipher::find_by_uuid(&uuid, &mut conn).await else { err!("Cipher doesn't exist") }; @@ -847,9 +858,8 @@ async fn post_collections_admin( } let posted_collections = HashSet::::from_iter(data.collection_ids); - let current_collections = HashSet::::from_iter( - cipher.get_admin_collections(headers.user.uuid.to_string(), &mut conn).await, - ); + let current_collections = + HashSet::::from_iter(cipher.get_admin_collections(headers.user.uuid.clone(), &mut conn).await); for collection in posted_collections.symmetric_difference(¤t_collections) { match Collection::find_by_uuid_and_org(collection, cipher.organization_uuid.as_ref().unwrap(), &mut conn).await @@ -906,7 +916,7 @@ struct ShareCipherData { #[post("/ciphers//share", data = "")] async fn post_cipher_share( - uuid: &str, + uuid: CipherId, data: Json, headers: Headers, mut conn: DbConn, @@ -914,12 +924,12 @@ async fn post_cipher_share( ) -> JsonResult { let data: ShareCipherData = data.into_inner(); - share_cipher_by_uuid(uuid, data, &headers, &mut conn, &nt).await + share_cipher_by_uuid(&uuid, data, &headers, &mut conn, &nt).await } #[put("/ciphers//share", data = "")] async fn put_cipher_share( - uuid: &str, + uuid: CipherId, data: Json, headers: Headers, mut conn: DbConn, @@ -927,7 +937,7 @@ async fn put_cipher_share( ) -> JsonResult { let data: ShareCipherData = data.into_inner(); - share_cipher_by_uuid(uuid, data, &headers, &mut conn, &nt).await + share_cipher_by_uuid(&uuid, data, &headers, &mut conn, &nt).await } #[derive(Deserialize)] @@ -976,7 +986,7 @@ async fn put_cipher_share_selected( } async fn share_cipher_by_uuid( - uuid: &str, + uuid: &CipherId, data: ShareCipherData, headers: &Headers, conn: &mut DbConn, @@ -1030,8 +1040,8 @@ async fn share_cipher_by_uuid( /// their object storage service. For self-hosted instances, it basically just /// redirects to the same location as before the v2 API. #[get("/ciphers//attachment/")] -async fn get_attachment(uuid: &str, attachment_id: &str, headers: Headers, mut conn: DbConn) -> JsonResult { - let Some(cipher) = Cipher::find_by_uuid(uuid, &mut conn).await else { +async fn get_attachment(uuid: CipherId, attachment_id: &str, headers: Headers, mut conn: DbConn) -> JsonResult { + let Some(cipher) = Cipher::find_by_uuid(&uuid, &mut conn).await else { err!("Cipher doesn't exist") }; @@ -1066,12 +1076,12 @@ enum FileUploadType { /// For self-hosted instances, it's another API on the local instance. #[post("/ciphers//attachment/v2", data = "")] async fn post_attachment_v2( - uuid: &str, + uuid: CipherId, data: Json, headers: Headers, mut conn: DbConn, ) -> JsonResult { - let Some(cipher) = Cipher::find_by_uuid(uuid, &mut conn).await else { + let Some(cipher) = Cipher::find_by_uuid(&uuid, &mut conn).await else { err!("Cipher doesn't exist") }; @@ -1121,7 +1131,7 @@ struct UploadData<'f> { /// database record, which is passed in as `attachment`. async fn save_attachment( mut attachment: Option, - cipher_uuid: &str, + cipher_uuid: CipherId, data: Form>, headers: &Headers, mut conn: DbConn, @@ -1136,7 +1146,7 @@ async fn save_attachment( err!("Attachment size can't be negative") } - let Some(cipher) = Cipher::find_by_uuid(cipher_uuid, &mut conn).await else { + let Some(cipher) = Cipher::find_by_uuid(&cipher_uuid, &mut conn).await else { err!("Cipher doesn't exist") }; @@ -1250,11 +1260,11 @@ async fn save_attachment( err!("No attachment key provided") } let attachment = - Attachment::new(file_id.clone(), String::from(cipher_uuid), encrypted_filename.unwrap(), size, data.key); + Attachment::new(file_id.clone(), cipher_uuid.clone(), encrypted_filename.unwrap(), size, data.key); attachment.save(&mut conn).await.expect("Error saving attachment"); } - let folder_path = tokio::fs::canonicalize(&CONFIG.attachments_folder()).await?.join(cipher_uuid); + let folder_path = tokio::fs::canonicalize(&CONFIG.attachments_folder()).await?.join(cipher_uuid.as_ref()); let file_path = folder_path.join(&file_id); tokio::fs::create_dir_all(&folder_path).await?; @@ -1294,7 +1304,7 @@ async fn save_attachment( /// with this one. #[post("/ciphers//attachment/", format = "multipart/form-data", data = "", rank = 1)] async fn post_attachment_v2_data( - uuid: &str, + uuid: CipherId, attachment_id: &str, data: Form>, headers: Headers, @@ -1315,7 +1325,7 @@ async fn post_attachment_v2_data( /// Legacy API for creating an attachment associated with a cipher. #[post("/ciphers//attachment", format = "multipart/form-data", data = "")] async fn post_attachment( - uuid: &str, + uuid: CipherId, data: Form>, headers: Headers, conn: DbConn, @@ -1332,7 +1342,7 @@ async fn post_attachment( #[post("/ciphers//attachment-admin", format = "multipart/form-data", data = "")] async fn post_attachment_admin( - uuid: &str, + uuid: CipherId, data: Form>, headers: Headers, conn: DbConn, @@ -1343,20 +1353,20 @@ async fn post_attachment_admin( #[post("/ciphers//attachment//share", format = "multipart/form-data", data = "")] async fn post_attachment_share( - uuid: &str, + uuid: CipherId, attachment_id: &str, data: Form>, headers: Headers, mut conn: DbConn, nt: Notify<'_>, ) -> JsonResult { - _delete_cipher_attachment_by_id(uuid, attachment_id, &headers, &mut conn, &nt).await?; + _delete_cipher_attachment_by_id(&uuid, attachment_id, &headers, &mut conn, &nt).await?; post_attachment(uuid, data, headers, conn, nt).await } #[post("/ciphers//attachment//delete-admin")] async fn delete_attachment_post_admin( - uuid: &str, + uuid: CipherId, attachment_id: &str, headers: Headers, conn: DbConn, @@ -1367,7 +1377,7 @@ async fn delete_attachment_post_admin( #[post("/ciphers//attachment//delete")] async fn delete_attachment_post( - uuid: &str, + uuid: CipherId, attachment_id: &str, headers: Headers, conn: DbConn, @@ -1378,58 +1388,58 @@ async fn delete_attachment_post( #[delete("/ciphers//attachment/")] async fn delete_attachment( - uuid: &str, + uuid: CipherId, attachment_id: &str, headers: Headers, mut conn: DbConn, nt: Notify<'_>, ) -> EmptyResult { - _delete_cipher_attachment_by_id(uuid, attachment_id, &headers, &mut conn, &nt).await + _delete_cipher_attachment_by_id(&uuid, attachment_id, &headers, &mut conn, &nt).await } #[delete("/ciphers//attachment//admin")] async fn delete_attachment_admin( - uuid: &str, + uuid: CipherId, attachment_id: &str, headers: Headers, mut conn: DbConn, nt: Notify<'_>, ) -> EmptyResult { - _delete_cipher_attachment_by_id(uuid, attachment_id, &headers, &mut conn, &nt).await + _delete_cipher_attachment_by_id(&uuid, attachment_id, &headers, &mut conn, &nt).await } #[post("/ciphers//delete")] -async fn delete_cipher_post(uuid: &str, headers: Headers, mut conn: DbConn, nt: Notify<'_>) -> EmptyResult { - _delete_cipher_by_uuid(uuid, &headers, &mut conn, false, &nt).await +async fn delete_cipher_post(uuid: CipherId, headers: Headers, mut conn: DbConn, nt: Notify<'_>) -> EmptyResult { + _delete_cipher_by_uuid(&uuid, &headers, &mut conn, false, &nt).await // permanent delete } #[post("/ciphers//delete-admin")] -async fn delete_cipher_post_admin(uuid: &str, headers: Headers, mut conn: DbConn, nt: Notify<'_>) -> EmptyResult { - _delete_cipher_by_uuid(uuid, &headers, &mut conn, false, &nt).await +async fn delete_cipher_post_admin(uuid: CipherId, headers: Headers, mut conn: DbConn, nt: Notify<'_>) -> EmptyResult { + _delete_cipher_by_uuid(&uuid, &headers, &mut conn, false, &nt).await // permanent delete } #[put("/ciphers//delete")] -async fn delete_cipher_put(uuid: &str, headers: Headers, mut conn: DbConn, nt: Notify<'_>) -> EmptyResult { - _delete_cipher_by_uuid(uuid, &headers, &mut conn, true, &nt).await +async fn delete_cipher_put(uuid: CipherId, headers: Headers, mut conn: DbConn, nt: Notify<'_>) -> EmptyResult { + _delete_cipher_by_uuid(&uuid, &headers, &mut conn, true, &nt).await // soft delete } #[put("/ciphers//delete-admin")] -async fn delete_cipher_put_admin(uuid: &str, headers: Headers, mut conn: DbConn, nt: Notify<'_>) -> EmptyResult { - _delete_cipher_by_uuid(uuid, &headers, &mut conn, true, &nt).await +async fn delete_cipher_put_admin(uuid: CipherId, headers: Headers, mut conn: DbConn, nt: Notify<'_>) -> EmptyResult { + _delete_cipher_by_uuid(&uuid, &headers, &mut conn, true, &nt).await } #[delete("/ciphers/")] -async fn delete_cipher(uuid: &str, headers: Headers, mut conn: DbConn, nt: Notify<'_>) -> EmptyResult { - _delete_cipher_by_uuid(uuid, &headers, &mut conn, false, &nt).await +async fn delete_cipher(uuid: CipherId, headers: Headers, mut conn: DbConn, nt: Notify<'_>) -> EmptyResult { + _delete_cipher_by_uuid(&uuid, &headers, &mut conn, false, &nt).await // permanent delete } #[delete("/ciphers//admin")] -async fn delete_cipher_admin(uuid: &str, headers: Headers, mut conn: DbConn, nt: Notify<'_>) -> EmptyResult { - _delete_cipher_by_uuid(uuid, &headers, &mut conn, false, &nt).await +async fn delete_cipher_admin(uuid: CipherId, headers: Headers, mut conn: DbConn, nt: Notify<'_>) -> EmptyResult { + _delete_cipher_by_uuid(&uuid, &headers, &mut conn, false, &nt).await // permanent delete } @@ -1494,13 +1504,13 @@ async fn delete_cipher_selected_put_admin( } #[put("/ciphers//restore")] -async fn restore_cipher_put(uuid: &str, headers: Headers, mut conn: DbConn, nt: Notify<'_>) -> JsonResult { - _restore_cipher_by_uuid(uuid, &headers, &mut conn, &nt).await +async fn restore_cipher_put(uuid: CipherId, headers: Headers, mut conn: DbConn, nt: Notify<'_>) -> JsonResult { + _restore_cipher_by_uuid(&uuid, &headers, &mut conn, &nt).await } #[put("/ciphers//restore-admin")] -async fn restore_cipher_put_admin(uuid: &str, headers: Headers, mut conn: DbConn, nt: Notify<'_>) -> JsonResult { - _restore_cipher_by_uuid(uuid, &headers, &mut conn, &nt).await +async fn restore_cipher_put_admin(uuid: CipherId, headers: Headers, mut conn: DbConn, nt: Notify<'_>) -> JsonResult { + _restore_cipher_by_uuid(&uuid, &headers, &mut conn, &nt).await } #[put("/ciphers/restore", data = "")] @@ -1517,7 +1527,7 @@ async fn restore_cipher_selected( #[serde(rename_all = "camelCase")] struct MoveCipherData { folder_id: Option, - ids: Vec, + ids: Vec, } #[post("/ciphers/move", data = "")] @@ -1640,7 +1650,7 @@ async fn delete_all( } async fn _delete_cipher_by_uuid( - uuid: &str, + uuid: &CipherId, headers: &Headers, conn: &mut DbConn, soft_delete: bool, @@ -1695,7 +1705,7 @@ async fn _delete_cipher_by_uuid( #[derive(Deserialize)] #[serde(rename_all = "camelCase")] struct CipherIdsData { - ids: Vec, + ids: Vec, } async fn _delete_multiple_ciphers( @@ -1716,7 +1726,7 @@ async fn _delete_multiple_ciphers( Ok(()) } -async fn _restore_cipher_by_uuid(uuid: &str, headers: &Headers, conn: &mut DbConn, nt: &Notify<'_>) -> JsonResult { +async fn _restore_cipher_by_uuid(uuid: &CipherId, headers: &Headers, conn: &mut DbConn, nt: &Notify<'_>) -> JsonResult { let Some(mut cipher) = Cipher::find_by_uuid(uuid, conn).await else { err!("Cipher doesn't exist") }; @@ -1778,7 +1788,7 @@ async fn _restore_multiple_ciphers( } async fn _delete_cipher_attachment_by_id( - uuid: &str, + uuid: &CipherId, attachment_id: &str, headers: &Headers, conn: &mut DbConn, @@ -1788,7 +1798,7 @@ async fn _delete_cipher_attachment_by_id( err!("Attachment doesn't exist") }; - if attachment.cipher_uuid != uuid { + if &attachment.cipher_uuid != uuid { err!("Attachment from other cipher") } @@ -1832,10 +1842,10 @@ async fn _delete_cipher_attachment_by_id( /// It will prevent the so called N+1 SQL issue by running just a few queries which will hold all the data needed. /// This will not improve the speed of a single cipher.to_json() call that much, so better not to use it for those calls. pub struct CipherSyncData { - pub cipher_attachments: HashMap>, - pub cipher_folders: HashMap, - pub cipher_favorites: HashSet, - pub cipher_collections: HashMap>, + pub cipher_attachments: HashMap>, + pub cipher_folders: HashMap, + pub cipher_favorites: HashSet, + pub cipher_collections: HashMap>, pub members: HashMap, pub user_collections: HashMap, pub user_collections_groups: HashMap, @@ -1850,8 +1860,8 @@ pub enum CipherSyncType { impl CipherSyncData { pub async fn new(user_uuid: &UserId, sync_type: CipherSyncType, conn: &mut DbConn) -> Self { - let cipher_folders: HashMap; - let cipher_favorites: HashSet; + let cipher_folders: HashMap; + let cipher_favorites: HashSet; match sync_type { // User Sync supports Folders and Favorites CipherSyncType::User => { @@ -1872,14 +1882,14 @@ impl CipherSyncData { // Generate a list of Cipher UUID's containing a Vec with one or more Attachment records let orgs = Membership::get_orgs_by_user(user_uuid, conn).await; let attachments = Attachment::find_all_by_user_and_orgs(user_uuid, &orgs, conn).await; - let mut cipher_attachments: HashMap> = HashMap::with_capacity(attachments.len()); + let mut cipher_attachments: HashMap> = HashMap::with_capacity(attachments.len()); for attachment in attachments { cipher_attachments.entry(attachment.cipher_uuid.clone()).or_default().push(attachment); } // Generate a HashMap with the Cipher UUID as key and one or more Collection UUID's - let user_cipher_collections = Cipher::get_collections_with_cipher_by_user(user_uuid.to_string(), conn).await; - let mut cipher_collections: HashMap> = + let user_cipher_collections = Cipher::get_collections_with_cipher_by_user(user_uuid.clone(), conn).await; + let mut cipher_collections: HashMap> = HashMap::with_capacity(user_cipher_collections.len()); for (cipher, collection) in user_cipher_collections { cipher_collections.entry(cipher).or_default().push(collection); diff --git a/src/api/core/events.rs b/src/api/core/events.rs index a49dc78566..ac53744c68 100644 --- a/src/api/core/events.rs +++ b/src/api/core/events.rs @@ -8,7 +8,7 @@ use crate::{ api::{EmptyResult, JsonResult}, auth::{AdminHeaders, Headers}, db::{ - models::{Cipher, Event, Membership, MembershipId, OrganizationId, UserId}, + models::{Cipher, CipherId, Event, Membership, MembershipId, OrganizationId, UserId}, DbConn, DbPool, }, util::parse_date, @@ -59,14 +59,14 @@ async fn get_org_events(org_id: &str, data: EventRange, _headers: AdminHeaders, } #[get("/ciphers//events?")] -async fn get_cipher_events(cipher_id: &str, data: EventRange, headers: Headers, mut conn: DbConn) -> JsonResult { +async fn get_cipher_events(cipher_id: CipherId, data: EventRange, headers: Headers, mut conn: DbConn) -> JsonResult { // Return an empty vec when we org events are disabled. // This prevents client errors let events_json: Vec = if !CONFIG.org_events_enabled() { Vec::with_capacity(0) } else { let mut events_json = Vec::with_capacity(0); - if Membership::user_has_ge_admin_access_to_cipher(&headers.user.uuid, cipher_id, &mut conn).await { + if Membership::user_has_ge_admin_access_to_cipher(&headers.user.uuid, &cipher_id, &mut conn).await { let start_date = parse_date(&data.start); let end_date = if let Some(before_date) = &data.continuation_token { parse_date(before_date) @@ -74,7 +74,7 @@ async fn get_cipher_events(cipher_id: &str, data: EventRange, headers: Headers, parse_date(&data.end) }; - events_json = Event::find_by_cipher_uuid(cipher_id, &start_date, &end_date, &mut conn) + events_json = Event::find_by_cipher_uuid(&cipher_id, &start_date, &end_date, &mut conn) .await .iter() .map(|e| e.to_json()) @@ -152,7 +152,7 @@ struct EventCollection { date: String, // Optional - cipher_id: Option, + cipher_id: Option, organization_id: Option, } @@ -290,19 +290,19 @@ async fn _log_event( // 1000..=1099 Are user events, they need to be logged via log_user_event() // Cipher Events 1100..=1199 => { - event.cipher_uuid = Some(String::from(source_uuid)); + event.cipher_uuid = Some(source_uuid.to_string().into()); } // Collection Events 1300..=1399 => { - event.collection_uuid = Some(String::from(source_uuid)); + event.collection_uuid = Some(source_uuid.to_string().into()); } // Group Events 1400..=1499 => { - event.group_uuid = Some(String::from(source_uuid)); + event.group_uuid = Some(source_uuid.to_string().into()); } // Org User Events 1500..=1599 => { - event.org_user_uuid = Some(String::from(source_uuid)); + event.org_user_uuid = Some(source_uuid.to_string().into()); } // 1600..=1699 Are organizational events, and they do not need the source_uuid // Policy Events diff --git a/src/api/core/organizations.rs b/src/api/core/organizations.rs index 6b5048bc75..15168d941e 100644 --- a/src/api/core/organizations.rs +++ b/src/api/core/organizations.rs @@ -1645,7 +1645,7 @@ async fn post_org_import( let headers: Headers = headers.into(); - let mut ciphers: Vec = Vec::with_capacity(data.ciphers.len()); + let mut ciphers: Vec = Vec::with_capacity(data.ciphers.len()); for mut cipher_data in data.ciphers { // Always clear folder_id's via an organization import cipher_data.folder_id = None; @@ -1670,7 +1670,7 @@ async fn post_org_import( #[allow(dead_code)] struct BulkCollectionsData { organization_id: OrganizationId, - cipher_ids: Vec, + cipher_ids: Vec, collection_ids: HashSet, remove_collections: bool, } @@ -2659,7 +2659,7 @@ async fn get_group_users( err!("Group support is disabled"); } - if Group::find_by_uuid_and_org(&&group_id, &org_id, &mut conn).await.is_none() { + if Group::find_by_uuid_and_org(&group_id, &org_id, &mut conn).await.is_none() { err!("Group could not be found!", "Group uuid is invalid or does not belong to the organization") }; diff --git a/src/api/notifications.rs b/src/api/notifications.rs index 832e56683b..d9d3a60fd2 100644 --- a/src/api/notifications.rs +++ b/src/api/notifications.rs @@ -437,7 +437,7 @@ impl WebSocketUsers { let data = create_update( vec![ - ("Id".into(), cipher.uuid.clone().into()), + ("Id".into(), cipher.uuid.to_string().into()), ("UserId".into(), user_uuid), ("OrganizationId".into(), org_uuid), ("CollectionIds".into(), collection_uuids), diff --git a/src/api/web.rs b/src/api/web.rs index a96d7e2a16..1439fb1e33 100644 --- a/src/api/web.rs +++ b/src/api/web.rs @@ -13,6 +13,7 @@ use serde_json::Value; use crate::{ api::{core::now, ApiResult, EmptyResult}, auth::decode_file_download, + db::models::CipherId, error::Error, util::{get_web_vault_version, Cached, SafeString}, CONFIG, @@ -196,15 +197,15 @@ async fn web_files(p: PathBuf) -> Cached> { } #[get("/attachments//?")] -async fn attachments(uuid: SafeString, file_id: SafeString, token: String) -> Option { +async fn attachments(uuid: CipherId, file_id: SafeString, token: String) -> Option { let Ok(claims) = decode_file_download(&token) else { return None; }; - if claims.sub != *uuid || claims.file_id != *file_id { + if claims.sub != uuid || claims.file_id != *file_id { return None; } - NamedFile::open(Path::new(&CONFIG.attachments_folder()).join(uuid).join(file_id)).await.ok() + NamedFile::open(Path::new(&CONFIG.attachments_folder()).join(uuid.as_ref()).join(file_id)).await.ok() } // We use DbConn here to let the alive healthcheck also verify the database connection. diff --git a/src/auth.rs b/src/auth.rs index 2a7c33d686..565c363037 100644 --- a/src/auth.rs +++ b/src/auth.rs @@ -14,7 +14,7 @@ use std::{ net::IpAddr, }; -use crate::db::models::{CollectionId, MembershipId, OrganizationId, UserId}; +use crate::db::models::{CipherId, CollectionId, MembershipId, OrganizationId, UserId}; use crate::{error::Error, CONFIG}; const JWT_ALGORITHM: Algorithm = Algorithm::RS256; @@ -293,12 +293,12 @@ pub struct FileDownloadClaims { // Issuer pub iss: String, // Subject - pub sub: String, + pub sub: CipherId, pub file_id: String, } -pub fn generate_file_download_claims(uuid: String, file_id: String) -> FileDownloadClaims { +pub fn generate_file_download_claims(uuid: CipherId, file_id: String) -> FileDownloadClaims { let time_now = Utc::now(); FileDownloadClaims { nbf: time_now.timestamp(), diff --git a/src/db/models/attachment.rs b/src/db/models/attachment.rs index 443704ac3f..156cdadba1 100644 --- a/src/db/models/attachment.rs +++ b/src/db/models/attachment.rs @@ -3,7 +3,7 @@ use std::io::ErrorKind; use bigdecimal::{BigDecimal, ToPrimitive}; use serde_json::Value; -use super::{OrganizationId, UserId}; +use super::{CipherId, OrganizationId, UserId}; use crate::CONFIG; db_object! { @@ -13,7 +13,7 @@ db_object! { #[diesel(primary_key(id))] pub struct Attachment { pub id: String, - pub cipher_uuid: String, + pub cipher_uuid: CipherId, pub file_name: String, // encrypted pub file_size: i64, pub akey: Option, @@ -22,7 +22,13 @@ db_object! { /// Local methods impl Attachment { - pub const fn new(id: String, cipher_uuid: String, file_name: String, file_size: i64, akey: Option) -> Self { + pub const fn new( + id: String, + cipher_uuid: CipherId, + file_name: String, + file_size: i64, + akey: Option, + ) -> Self { Self { id, cipher_uuid, @@ -118,7 +124,7 @@ impl Attachment { }} } - pub async fn delete_all_by_cipher(cipher_uuid: &str, conn: &mut DbConn) -> EmptyResult { + pub async fn delete_all_by_cipher(cipher_uuid: &CipherId, conn: &mut DbConn) -> EmptyResult { for attachment in Attachment::find_by_cipher(cipher_uuid, conn).await { attachment.delete(conn).await?; } @@ -135,7 +141,7 @@ impl Attachment { }} } - pub async fn find_by_cipher(cipher_uuid: &str, conn: &mut DbConn) -> Vec { + pub async fn find_by_cipher(cipher_uuid: &CipherId, conn: &mut DbConn) -> Vec { db_run! { conn: { attachments::table .filter(attachments::cipher_uuid.eq(cipher_uuid)) diff --git a/src/db/models/cipher.rs b/src/db/models/cipher.rs index 56e272bfba..29d5bbeb5e 100644 --- a/src/db/models/cipher.rs +++ b/src/db/models/cipher.rs @@ -1,7 +1,13 @@ use crate::util::LowerCase; use crate::CONFIG; use chrono::{NaiveDateTime, TimeDelta, Utc}; +use rocket::request::FromParam; use serde_json::Value; +use std::{ + borrow::Borrow, + fmt::{Display, Formatter}, + ops::Deref, +}; use super::{ Attachment, CollectionCipher, CollectionId, Favorite, FolderCipher, Group, Membership, MembershipStatus, @@ -18,7 +24,7 @@ db_object! { #[diesel(treat_none_as_null = true)] #[diesel(primary_key(uuid))] pub struct Cipher { - pub uuid: String, + pub uuid: CipherId, pub created_at: NaiveDateTime, pub updated_at: NaiveDateTime, @@ -58,7 +64,7 @@ impl Cipher { let now = Utc::now().naive_utc(); Self { - uuid: crate::util::get_uuid(), + uuid: CipherId(crate::util::get_uuid()), created_at: now, updated_at: now, @@ -279,7 +285,7 @@ impl Cipher { Cow::from(Vec::with_capacity(0)) } } else { - Cow::from(self.get_admin_collections(user_uuid.to_string(), conn).await) + Cow::from(self.get_admin_collections(user_uuid.clone(), conn).await) }; // There are three types of cipher response models in upstream @@ -683,7 +689,7 @@ impl Cipher { }} } - pub async fn find_by_uuid(uuid: &str, conn: &mut DbConn) -> Option { + pub async fn find_by_uuid(uuid: &CipherId, conn: &mut DbConn) -> Option { db_run! {conn: { ciphers::table .filter(ciphers::uuid.eq(uuid)) @@ -693,7 +699,7 @@ impl Cipher { }} } - pub async fn find_by_uuid_and_org(cipher_uuid: &str, org_uuid: &str, conn: &mut DbConn) -> Option { + pub async fn find_by_uuid_and_org(cipher_uuid: &CipherId, org_uuid: &str, conn: &mut DbConn) -> Option { db_run! {conn: { ciphers::table .filter(ciphers::uuid.eq(cipher_uuid)) @@ -862,7 +868,7 @@ impl Cipher { }} } - pub async fn get_collections(&self, user_id: String, conn: &mut DbConn) -> Vec { + pub async fn get_collections(&self, user_id: UserId, conn: &mut DbConn) -> Vec { if CONFIG.org_groups_enabled() { db_run! {conn: { ciphers_collections::table @@ -921,7 +927,7 @@ impl Cipher { } } - pub async fn get_admin_collections(&self, user_id: String, conn: &mut DbConn) -> Vec { + pub async fn get_admin_collections(&self, user_id: UserId, conn: &mut DbConn) -> Vec { if CONFIG.org_groups_enabled() { db_run! {conn: { ciphers_collections::table @@ -985,9 +991,9 @@ impl Cipher { /// Return a Vec with (cipher_uuid, collection_uuid) /// This is used during a full sync so we only need one query for all collections accessible. pub async fn get_collections_with_cipher_by_user( - user_id: String, + user_id: UserId, conn: &mut DbConn, - ) -> Vec<(String, CollectionId)> { + ) -> Vec<(CipherId, CollectionId)> { db_run! {conn: { ciphers_collections::table .inner_join(collections::table.on( @@ -1021,7 +1027,55 @@ impl Cipher { .or_filter(collections_groups::collections_uuid.is_not_null()) //Access via group .select(ciphers_collections::all_columns) .distinct() - .load::<(String, CollectionId)>(conn).unwrap_or_default() + .load::<(CipherId, CollectionId)>(conn).unwrap_or_default() }} } } + +#[derive(DieselNewType, FromForm, Clone, Debug, Hash, PartialEq, Eq, Serialize, Deserialize)] +pub struct CipherId(String); + +impl AsRef for CipherId { + fn as_ref(&self) -> &str { + &self.0 + } +} + +impl Deref for CipherId { + type Target = str; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl Borrow for CipherId { + fn borrow(&self) -> &str { + &self.0 + } +} + +impl Display for CipherId { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.0) + } +} + +impl From for CipherId { + fn from(raw: String) -> Self { + Self(raw) + } +} + +impl<'r> FromParam<'r> for CipherId { + 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/collection.rs b/src/db/models/collection.rs index ed3b762b50..1b8c11bf4f 100644 --- a/src/db/models/collection.rs +++ b/src/db/models/collection.rs @@ -6,7 +6,9 @@ use std::{ ops::Deref, }; -use super::{CollectionGroup, GroupUser, Membership, MembershipStatus, MembershipType, OrganizationId, User, UserId}; +use super::{ + CipherId, CollectionGroup, GroupUser, Membership, MembershipStatus, MembershipType, OrganizationId, User, UserId, +}; use crate::CONFIG; db_object! { @@ -34,7 +36,7 @@ db_object! { #[diesel(table_name = ciphers_collections)] #[diesel(primary_key(cipher_uuid, collection_uuid))] pub struct CollectionCipher { - pub cipher_uuid: String, + pub cipher_uuid: CipherId, pub collection_uuid: CollectionId, } } @@ -710,7 +712,7 @@ impl CollectionUser { /// Database methods impl CollectionCipher { - pub async fn save(cipher_uuid: &str, collection_uuid: &CollectionId, conn: &mut DbConn) -> EmptyResult { + pub async fn save(cipher_uuid: &CipherId, collection_uuid: &CollectionId, conn: &mut DbConn) -> EmptyResult { Self::update_users_revision(collection_uuid, conn).await; db_run! { conn: @@ -740,7 +742,7 @@ impl CollectionCipher { } } - pub async fn delete(cipher_uuid: &str, collection_uuid: &CollectionId, conn: &mut DbConn) -> EmptyResult { + pub async fn delete(cipher_uuid: &CipherId, collection_uuid: &CollectionId, conn: &mut DbConn) -> EmptyResult { Self::update_users_revision(collection_uuid, conn).await; db_run! { conn: { @@ -754,7 +756,7 @@ impl CollectionCipher { }} } - pub async fn delete_all_by_cipher(cipher_uuid: &str, conn: &mut DbConn) -> EmptyResult { + pub async fn delete_all_by_cipher(cipher_uuid: &CipherId, conn: &mut DbConn) -> EmptyResult { db_run! { conn: { diesel::delete(ciphers_collections::table.filter(ciphers_collections::cipher_uuid.eq(cipher_uuid))) .execute(conn) diff --git a/src/db/models/event.rs b/src/db/models/event.rs index f52190ec00..c840cd475a 100644 --- a/src/db/models/event.rs +++ b/src/db/models/event.rs @@ -1,7 +1,7 @@ use crate::db::DbConn; use serde_json::Value; -use super::{OrganizationId, UserId}; +use super::{CipherId, CollectionId, GroupId, MembershipId, OrganizationId, UserId}; use crate::{api::EmptyResult, error::MapResult, CONFIG}; use chrono::{NaiveDateTime, TimeDelta, Utc}; @@ -20,10 +20,10 @@ db_object! { pub event_type: i32, // EventType pub user_uuid: Option, pub org_uuid: Option, - pub cipher_uuid: Option, - pub collection_uuid: Option, - pub group_uuid: Option, - pub org_user_uuid: Option, + pub cipher_uuid: Option, + pub collection_uuid: Option, + pub group_uuid: Option, + pub org_user_uuid: Option, pub act_user_uuid: Option, // Upstream enum: https://github.com/bitwarden/server/blob/8a22c0479e987e756ce7412c48a732f9002f0a2d/src/Core/Enums/DeviceType.cs pub device_type: Option, @@ -298,7 +298,7 @@ impl Event { } pub async fn find_by_cipher_uuid( - cipher_uuid: &str, + cipher_uuid: &CipherId, start: &NaiveDateTime, end: &NaiveDateTime, conn: &mut DbConn, diff --git a/src/db/models/favorite.rs b/src/db/models/favorite.rs index 14f89eafac..de2e0febb0 100644 --- a/src/db/models/favorite.rs +++ b/src/db/models/favorite.rs @@ -1,4 +1,4 @@ -use super::{User, UserId}; +use super::{CipherId, User, UserId}; db_object! { #[derive(Identifiable, Queryable, Insertable)] @@ -6,7 +6,7 @@ db_object! { #[diesel(primary_key(user_uuid, cipher_uuid))] pub struct Favorite { pub user_uuid: UserId, - pub cipher_uuid: String, + pub cipher_uuid: CipherId, } } @@ -17,7 +17,7 @@ use crate::error::MapResult; impl Favorite { // Returns whether the specified cipher is a favorite of the specified user. - pub async fn is_favorite(cipher_uuid: &str, user_uuid: &UserId, conn: &mut DbConn) -> bool { + pub async fn is_favorite(cipher_uuid: &CipherId, user_uuid: &UserId, conn: &mut DbConn) -> bool { db_run! { conn: { let query = favorites::table .filter(favorites::cipher_uuid.eq(cipher_uuid)) @@ -29,7 +29,12 @@ impl Favorite { } // Sets whether the specified cipher is a favorite of the specified user. - pub async fn set_favorite(favorite: bool, cipher_uuid: &str, user_uuid: &UserId, conn: &mut DbConn) -> EmptyResult { + pub async fn set_favorite( + favorite: bool, + cipher_uuid: &CipherId, + user_uuid: &UserId, + conn: &mut DbConn, + ) -> EmptyResult { let (old, new) = (Self::is_favorite(cipher_uuid, user_uuid, conn).await, favorite); match (old, new) { (false, true) => { @@ -62,7 +67,7 @@ impl Favorite { } // Delete all favorite entries associated with the specified cipher. - pub async fn delete_all_by_cipher(cipher_uuid: &str, conn: &mut DbConn) -> EmptyResult { + pub async fn delete_all_by_cipher(cipher_uuid: &CipherId, conn: &mut DbConn) -> EmptyResult { db_run! { conn: { diesel::delete(favorites::table.filter(favorites::cipher_uuid.eq(cipher_uuid))) .execute(conn) @@ -81,12 +86,12 @@ impl Favorite { /// Return a vec with (cipher_uuid) this will only contain favorite flagged ciphers /// This is used during a full sync so we only need one query for all favorite cipher matches. - pub async fn get_all_cipher_uuid_by_user(user_uuid: &UserId, conn: &mut DbConn) -> Vec { + pub async fn get_all_cipher_uuid_by_user(user_uuid: &UserId, conn: &mut DbConn) -> Vec { db_run! { conn: { favorites::table .filter(favorites::user_uuid.eq(user_uuid)) .select(favorites::cipher_uuid) - .load::(conn) + .load::(conn) .unwrap_or_default() }} } diff --git a/src/db/models/folder.rs b/src/db/models/folder.rs index 9666e15933..3e2f3e98a6 100644 --- a/src/db/models/folder.rs +++ b/src/db/models/folder.rs @@ -1,7 +1,7 @@ use chrono::{NaiveDateTime, Utc}; use serde_json::Value; -use super::{User, UserId}; +use super::{CipherId, User, UserId}; db_object! { #[derive(Identifiable, Queryable, Insertable, AsChangeset)] @@ -19,7 +19,7 @@ db_object! { #[diesel(table_name = folders_ciphers)] #[diesel(primary_key(cipher_uuid, folder_uuid))] pub struct FolderCipher { - pub cipher_uuid: String, + pub cipher_uuid: CipherId, pub folder_uuid: String, } } @@ -52,10 +52,10 @@ impl Folder { } impl FolderCipher { - pub fn new(folder_uuid: &str, cipher_uuid: &str) -> Self { + pub fn new(folder_uuid: &str, cipher_uuid: &CipherId) -> Self { Self { folder_uuid: folder_uuid.to_string(), - cipher_uuid: cipher_uuid.to_string(), + cipher_uuid: cipher_uuid.clone(), } } } @@ -177,7 +177,7 @@ impl FolderCipher { }} } - pub async fn delete_all_by_cipher(cipher_uuid: &str, conn: &mut DbConn) -> EmptyResult { + pub async fn delete_all_by_cipher(cipher_uuid: &CipherId, conn: &mut DbConn) -> EmptyResult { db_run! { conn: { diesel::delete(folders_ciphers::table.filter(folders_ciphers::cipher_uuid.eq(cipher_uuid))) .execute(conn) @@ -193,7 +193,11 @@ impl FolderCipher { }} } - pub async fn find_by_folder_and_cipher(folder_uuid: &str, cipher_uuid: &str, conn: &mut DbConn) -> Option { + pub async fn find_by_folder_and_cipher( + folder_uuid: &str, + cipher_uuid: &CipherId, + conn: &mut DbConn, + ) -> Option { db_run! { conn: { folders_ciphers::table .filter(folders_ciphers::folder_uuid.eq(folder_uuid)) @@ -216,13 +220,13 @@ impl FolderCipher { /// Return a vec with (cipher_uuid, folder_uuid) /// This is used during a full sync so we only need one query for all folder matches. - pub async fn find_by_user(user_uuid: &UserId, conn: &mut DbConn) -> Vec<(String, String)> { + pub async fn find_by_user(user_uuid: &UserId, conn: &mut DbConn) -> Vec<(CipherId, String)> { db_run! { conn: { folders_ciphers::table .inner_join(folders::table) .filter(folders::user_uuid.eq(user_uuid)) .select(folders_ciphers::all_columns) - .load::<(String, String)>(conn) + .load::<(CipherId, String)>(conn) .unwrap_or_default() }} } diff --git a/src/db/models/mod.rs b/src/db/models/mod.rs index e10951e58d..4a9ba70a1d 100644 --- a/src/db/models/mod.rs +++ b/src/db/models/mod.rs @@ -18,7 +18,7 @@ mod user; pub use self::attachment::Attachment; pub use self::auth_request::AuthRequest; -pub use self::cipher::{Cipher, RepromptType}; +pub use self::cipher::{Cipher, CipherId, RepromptType}; pub use self::collection::{Collection, CollectionCipher, CollectionId, CollectionUser}; pub use self::device::{Device, DeviceType}; pub use self::emergency_access::{EmergencyAccess, EmergencyAccessStatus, EmergencyAccessType}; diff --git a/src/db/models/organization.rs b/src/db/models/organization.rs index 016bd3dacc..15bd25af36 100644 --- a/src/db/models/organization.rs +++ b/src/db/models/organization.rs @@ -11,8 +11,8 @@ use std::{ }; use super::{ - Collection, CollectionGroup, CollectionId, CollectionUser, Group, GroupId, GroupUser, OrgPolicy, OrgPolicyType, - TwoFactor, User, UserId, + CipherId, Collection, CollectionGroup, CollectionId, CollectionUser, Group, GroupId, GroupUser, OrgPolicy, + OrgPolicyType, TwoFactor, User, UserId, }; use crate::CONFIG; @@ -897,7 +897,11 @@ impl Membership { }} } - pub async fn find_by_cipher_and_org(cipher_uuid: &str, org_uuid: &OrganizationId, conn: &mut DbConn) -> Vec { + pub async fn find_by_cipher_and_org( + cipher_uuid: &CipherId, + org_uuid: &OrganizationId, + conn: &mut DbConn, + ) -> Vec { db_run! { conn: { users_organizations::table .filter(users_organizations::org_uuid.eq(org_uuid)) @@ -921,7 +925,7 @@ impl Membership { } pub async fn find_by_cipher_and_org_with_group( - cipher_uuid: &str, + cipher_uuid: &CipherId, org_uuid: &OrganizationId, conn: &mut DbConn, ) -> Vec { @@ -950,7 +954,11 @@ impl Membership { }} } - pub async fn user_has_ge_admin_access_to_cipher(user_uuid: &UserId, cipher_uuid: &str, conn: &mut DbConn) -> bool { + pub async fn user_has_ge_admin_access_to_cipher( + user_uuid: &UserId, + cipher_uuid: &CipherId, + conn: &mut DbConn, + ) -> bool { db_run! { conn: { users_organizations::table .inner_join(ciphers::table.on(ciphers::uuid.eq(cipher_uuid).and(ciphers::organization_uuid.eq(users_organizations::org_uuid.nullable())))) From 5dc05d6ba454e9a6e88e34e93ee089b629dca240 Mon Sep 17 00:00:00 2001 From: Stefan Melmuk Date: Sat, 21 Dec 2024 15:34:36 +0100 Subject: [PATCH 08/25] introduce folder_id newtype --- src/api/core/accounts.rs | 6 ++-- src/api/core/ciphers.rs | 14 ++++---- src/api/core/folders.rs | 24 ++++++++----- src/api/notifications.rs | 2 +- src/db/models/cipher.rs | 34 +++++++++--------- src/db/models/folder.rs | 78 +++++++++++++++++++++++++++++++++------- src/db/models/mod.rs | 2 +- 7 files changed, 111 insertions(+), 49 deletions(-) diff --git a/src/api/core/accounts.rs b/src/api/core/accounts.rs index 03d339a1c9..c18179e359 100644 --- a/src/api/core/accounts.rs +++ b/src/api/core/accounts.rs @@ -445,7 +445,7 @@ struct UpdateFolderData { // There is a bug in 2024.3.x which adds a `null` item. // To bypass this we allow a Option here, but skip it during the updates // See: https://github.com/bitwarden/clients/issues/8453 - id: Option, + id: Option, name: String, } @@ -500,8 +500,8 @@ fn validate_keydata( } // Check that we're correctly rotating all the user's folders - let existing_folder_ids = existing_folders.iter().map(|f| f.uuid.as_str()).collect::>(); - let provided_folder_ids = data.folders.iter().filter_map(|f| f.id.as_deref()).collect::>(); + let existing_folder_ids = existing_folders.iter().map(|f| &f.uuid).collect::>(); + let provided_folder_ids = data.folders.iter().filter_map(|f| f.id.as_ref()).collect::>(); if !provided_folder_ids.is_superset(&existing_folder_ids) { err!("All existing folders must be included in the rotation") } diff --git a/src/api/core/ciphers.rs b/src/api/core/ciphers.rs index a391abdbc2..758ec3b7d9 100644 --- a/src/api/core/ciphers.rs +++ b/src/api/core/ciphers.rs @@ -221,7 +221,7 @@ pub struct CipherData { // Id is optional as it is included only in bulk share pub id: Option, // Folder id is not included in import - pub folder_id: Option, + pub folder_id: Option, // TODO: Some of these might appear all the time, no need for Option #[serde(alias = "organizationID")] pub organization_id: Option, @@ -270,7 +270,7 @@ pub struct CipherData { #[derive(Debug, Deserialize)] #[serde(rename_all = "camelCase")] pub struct PartialCipherData { - folder_id: Option, + folder_id: Option, favorite: bool, } @@ -579,9 +579,9 @@ async fn post_ciphers_import( Cipher::validate_cipher_data(&data.ciphers)?; // Read and create the folders - let existing_folders: HashSet> = + let existing_folders: HashSet> = Folder::find_by_user(&headers.user.uuid, &mut conn).await.into_iter().map(|f| Some(f.uuid)).collect(); - let mut folders: Vec = Vec::with_capacity(data.folders.len()); + let mut folders: Vec = Vec::with_capacity(data.folders.len()); for folder in data.folders.into_iter() { let folder_uuid = if existing_folders.contains(&folder.id) { folder.id.unwrap() @@ -1526,7 +1526,7 @@ async fn restore_cipher_selected( #[derive(Deserialize)] #[serde(rename_all = "camelCase")] struct MoveCipherData { - folder_id: Option, + folder_id: Option, ids: Vec, } @@ -1843,7 +1843,7 @@ async fn _delete_cipher_attachment_by_id( /// This will not improve the speed of a single cipher.to_json() call that much, so better not to use it for those calls. pub struct CipherSyncData { pub cipher_attachments: HashMap>, - pub cipher_folders: HashMap, + pub cipher_folders: HashMap, pub cipher_favorites: HashSet, pub cipher_collections: HashMap>, pub members: HashMap, @@ -1860,7 +1860,7 @@ pub enum CipherSyncType { impl CipherSyncData { pub async fn new(user_uuid: &UserId, sync_type: CipherSyncType, conn: &mut DbConn) -> Self { - let cipher_folders: HashMap; + let cipher_folders: HashMap; let cipher_favorites: HashSet; match sync_type { // User Sync supports Folders and Favorites diff --git a/src/api/core/folders.rs b/src/api/core/folders.rs index 3daa2a064f..708bedffc1 100644 --- a/src/api/core/folders.rs +++ b/src/api/core/folders.rs @@ -24,8 +24,8 @@ async fn get_folders(headers: Headers, mut conn: DbConn) -> Json { } #[get("/folders/")] -async fn get_folder(uuid: &str, headers: Headers, mut conn: DbConn) -> JsonResult { - match Folder::find_by_uuid_and_user(uuid, &headers.user.uuid, &mut conn).await { +async fn get_folder(uuid: FolderId, headers: Headers, mut conn: DbConn) -> JsonResult { + match Folder::find_by_uuid_and_user(&uuid, &headers.user.uuid, &mut conn).await { Some(folder) => Ok(Json(folder.to_json())), _ => err!("Invalid folder", "Folder does not exist or belongs to another user"), } @@ -35,7 +35,7 @@ async fn get_folder(uuid: &str, headers: Headers, mut conn: DbConn) -> JsonResul #[serde(rename_all = "camelCase")] pub struct FolderData { pub name: String, - pub id: Option, + pub id: Option, } #[post("/folders", data = "")] @@ -51,13 +51,19 @@ async fn post_folders(data: Json, headers: Headers, mut conn: DbConn } #[post("/folders/", data = "")] -async fn post_folder(uuid: &str, data: Json, headers: Headers, conn: DbConn, nt: Notify<'_>) -> JsonResult { +async fn post_folder( + uuid: FolderId, + data: Json, + headers: Headers, + conn: DbConn, + nt: Notify<'_>, +) -> JsonResult { put_folder(uuid, data, headers, conn, nt).await } #[put("/folders/", data = "")] async fn put_folder( - uuid: &str, + uuid: FolderId, data: Json, headers: Headers, mut conn: DbConn, @@ -65,7 +71,7 @@ async fn put_folder( ) -> JsonResult { let data: FolderData = data.into_inner(); - let Some(mut folder) = Folder::find_by_uuid_and_user(uuid, &headers.user.uuid, &mut conn).await else { + let Some(mut folder) = Folder::find_by_uuid_and_user(&uuid, &headers.user.uuid, &mut conn).await else { err!("Invalid folder", "Folder does not exist or belongs to another user") }; @@ -78,13 +84,13 @@ async fn put_folder( } #[post("/folders//delete")] -async fn delete_folder_post(uuid: &str, headers: Headers, conn: DbConn, nt: Notify<'_>) -> EmptyResult { +async fn delete_folder_post(uuid: FolderId, headers: Headers, conn: DbConn, nt: Notify<'_>) -> EmptyResult { delete_folder(uuid, headers, conn, nt).await } #[delete("/folders/")] -async fn delete_folder(uuid: &str, headers: Headers, mut conn: DbConn, nt: Notify<'_>) -> EmptyResult { - let Some(folder) = Folder::find_by_uuid_and_user(uuid, &headers.user.uuid, &mut conn).await else { +async fn delete_folder(uuid: FolderId, headers: Headers, mut conn: DbConn, nt: Notify<'_>) -> EmptyResult { + let Some(folder) = Folder::find_by_uuid_and_user(&uuid, &headers.user.uuid, &mut conn).await else { err!("Invalid folder", "Folder does not exist or belongs to another user") }; diff --git a/src/api/notifications.rs b/src/api/notifications.rs index d9d3a60fd2..6653799a36 100644 --- a/src/api/notifications.rs +++ b/src/api/notifications.rs @@ -392,7 +392,7 @@ impl WebSocketUsers { } let data = create_update( vec![ - ("Id".into(), folder.uuid.clone().into()), + ("Id".into(), folder.uuid.to_string().into()), ("UserId".into(), folder.user_uuid.to_string().into()), ("RevisionDate".into(), serialize_date(folder.updated_at)), ], diff --git a/src/db/models/cipher.rs b/src/db/models/cipher.rs index 29d5bbeb5e..933b50eeed 100644 --- a/src/db/models/cipher.rs +++ b/src/db/models/cipher.rs @@ -10,7 +10,7 @@ use std::{ }; use super::{ - Attachment, CollectionCipher, CollectionId, Favorite, FolderCipher, Group, Membership, MembershipStatus, + Attachment, CollectionCipher, CollectionId, Favorite, FolderCipher, FolderId, Group, Membership, MembershipStatus, MembershipType, OrganizationId, User, UserId, }; @@ -334,7 +334,7 @@ impl Cipher { // Skip adding these fields in that case if sync_type == CipherSyncType::User { json_object["folderId"] = json!(if let Some(cipher_sync_data) = cipher_sync_data { - cipher_sync_data.cipher_folders.get(&self.uuid).map(|c| c.to_string()) + cipher_sync_data.cipher_folders.get(&self.uuid).cloned() } else { self.get_folder_uuid(user_uuid, conn).await }); @@ -469,7 +469,7 @@ impl Cipher { pub async fn move_to_folder( &self, - folder_uuid: Option, + folder_uuid: Option, user_uuid: &UserId, conn: &mut DbConn, ) -> EmptyResult { @@ -478,23 +478,25 @@ impl Cipher { match (self.get_folder_uuid(user_uuid, conn).await, folder_uuid) { // No changes (None, None) => Ok(()), - (Some(ref old), Some(ref new)) if old == new => Ok(()), + (Some(ref old_folder), Some(ref new_folder)) if old_folder == new_folder => Ok(()), // Add to folder - (None, Some(new)) => FolderCipher::new(&new, &self.uuid).save(conn).await, + (None, Some(new_folder)) => FolderCipher::new(new_folder, self.uuid.clone()).save(conn).await, // Remove from folder - (Some(old), None) => match FolderCipher::find_by_folder_and_cipher(&old, &self.uuid, conn).await { - Some(old) => old.delete(conn).await, - None => err!("Couldn't move from previous folder"), - }, + (Some(old_folder), None) => { + match FolderCipher::find_by_folder_and_cipher(&old_folder, &self.uuid, conn).await { + Some(old_folder) => old_folder.delete(conn).await, + None => err!("Couldn't move from previous folder"), + } + } // Move to another folder - (Some(old), Some(new)) => { - if let Some(old) = FolderCipher::find_by_folder_and_cipher(&old, &self.uuid, conn).await { - old.delete(conn).await?; + (Some(old_folder), Some(new_folder)) => { + if let Some(old_folder) = FolderCipher::find_by_folder_and_cipher(&old_folder, &self.uuid, conn).await { + old_folder.delete(conn).await?; } - FolderCipher::new(&new, &self.uuid).save(conn).await + FolderCipher::new(new_folder, self.uuid.clone()).save(conn).await } } } @@ -677,14 +679,14 @@ impl Cipher { } } - pub async fn get_folder_uuid(&self, user_uuid: &UserId, conn: &mut DbConn) -> Option { + pub async fn get_folder_uuid(&self, user_uuid: &UserId, conn: &mut DbConn) -> Option { db_run! {conn: { folders_ciphers::table .inner_join(folders::table) .filter(folders::user_uuid.eq(&user_uuid)) .filter(folders_ciphers::cipher_uuid.eq(&self.uuid)) .select(folders_ciphers::folder_uuid) - .first::(conn) + .first::(conn) .ok() }} } @@ -850,7 +852,7 @@ impl Cipher { }} } - pub async fn find_by_folder(folder_uuid: &str, conn: &mut DbConn) -> Vec { + pub async fn find_by_folder(folder_uuid: &FolderId, conn: &mut DbConn) -> Vec { db_run! {conn: { folders_ciphers::table.inner_join(ciphers::table) .filter(folders_ciphers::folder_uuid.eq(folder_uuid)) diff --git a/src/db/models/folder.rs b/src/db/models/folder.rs index 3e2f3e98a6..6a669ade4e 100644 --- a/src/db/models/folder.rs +++ b/src/db/models/folder.rs @@ -1,5 +1,11 @@ use chrono::{NaiveDateTime, Utc}; +use rocket::request::FromParam; use serde_json::Value; +use std::{ + borrow::Borrow, + fmt::{Display, Formatter}, + ops::Deref, +}; use super::{CipherId, User, UserId}; @@ -8,7 +14,7 @@ db_object! { #[diesel(table_name = folders)] #[diesel(primary_key(uuid))] pub struct Folder { - pub uuid: String, + pub uuid: FolderId, pub created_at: NaiveDateTime, pub updated_at: NaiveDateTime, pub user_uuid: UserId, @@ -20,7 +26,7 @@ db_object! { #[diesel(primary_key(cipher_uuid, folder_uuid))] pub struct FolderCipher { pub cipher_uuid: CipherId, - pub folder_uuid: String, + pub folder_uuid: FolderId, } } @@ -30,7 +36,7 @@ impl Folder { let now = Utc::now().naive_utc(); Self { - uuid: crate::util::get_uuid(), + uuid: FolderId(crate::util::get_uuid()), created_at: now, updated_at: now, @@ -52,10 +58,10 @@ impl Folder { } impl FolderCipher { - pub fn new(folder_uuid: &str, cipher_uuid: &CipherId) -> Self { + pub fn new(folder_uuid: FolderId, cipher_uuid: CipherId) -> Self { Self { - folder_uuid: folder_uuid.to_string(), - cipher_uuid: cipher_uuid.clone(), + folder_uuid, + cipher_uuid, } } } @@ -120,7 +126,7 @@ impl Folder { Ok(()) } - pub async fn find_by_uuid_and_user(uuid: &str, user_uuid: &UserId, conn: &mut DbConn) -> Option { + pub async fn find_by_uuid_and_user(uuid: &FolderId, user_uuid: &UserId, conn: &mut DbConn) -> Option { db_run! { conn: { folders::table .filter(folders::uuid.eq(uuid)) @@ -185,7 +191,7 @@ impl FolderCipher { }} } - pub async fn delete_all_by_folder(folder_uuid: &str, conn: &mut DbConn) -> EmptyResult { + pub async fn delete_all_by_folder(folder_uuid: &FolderId, conn: &mut DbConn) -> EmptyResult { db_run! { conn: { diesel::delete(folders_ciphers::table.filter(folders_ciphers::folder_uuid.eq(folder_uuid))) .execute(conn) @@ -194,7 +200,7 @@ impl FolderCipher { } pub async fn find_by_folder_and_cipher( - folder_uuid: &str, + folder_uuid: &FolderId, cipher_uuid: &CipherId, conn: &mut DbConn, ) -> Option { @@ -208,7 +214,7 @@ impl FolderCipher { }} } - pub async fn find_by_folder(folder_uuid: &str, conn: &mut DbConn) -> Vec { + pub async fn find_by_folder(folder_uuid: &FolderId, conn: &mut DbConn) -> Vec { db_run! { conn: { folders_ciphers::table .filter(folders_ciphers::folder_uuid.eq(folder_uuid)) @@ -220,14 +226,62 @@ impl FolderCipher { /// Return a vec with (cipher_uuid, folder_uuid) /// This is used during a full sync so we only need one query for all folder matches. - pub async fn find_by_user(user_uuid: &UserId, conn: &mut DbConn) -> Vec<(CipherId, String)> { + pub async fn find_by_user(user_uuid: &UserId, conn: &mut DbConn) -> Vec<(CipherId, FolderId)> { db_run! { conn: { folders_ciphers::table .inner_join(folders::table) .filter(folders::user_uuid.eq(user_uuid)) .select(folders_ciphers::all_columns) - .load::<(CipherId, String)>(conn) + .load::<(CipherId, FolderId)>(conn) .unwrap_or_default() }} } } + +#[derive(DieselNewType, FromForm, Clone, Debug, Hash, PartialEq, Eq, Serialize, Deserialize)] +pub struct FolderId(String); + +impl AsRef for FolderId { + fn as_ref(&self) -> &str { + &self.0 + } +} + +impl Deref for FolderId { + type Target = str; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl Borrow for FolderId { + fn borrow(&self) -> &str { + &self.0 + } +} + +impl Display for FolderId { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.0) + } +} + +impl From for FolderId { + fn from(raw: String) -> Self { + Self(raw) + } +} + +impl<'r> FromParam<'r> for FolderId { + 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 4a9ba70a1d..d832c04de1 100644 --- a/src/db/models/mod.rs +++ b/src/db/models/mod.rs @@ -24,7 +24,7 @@ pub use self::device::{Device, DeviceType}; pub use self::emergency_access::{EmergencyAccess, EmergencyAccessStatus, EmergencyAccessType}; pub use self::event::{Event, EventType}; pub use self::favorite::Favorite; -pub use self::folder::{Folder, FolderCipher}; +pub use self::folder::{Folder, FolderCipher, FolderId}; pub use self::group::{CollectionGroup, Group, GroupId, GroupUser}; pub use self::org_policy::{OrgPolicy, OrgPolicyErr, OrgPolicyType}; pub use self::organization::{ From 53b1c36085ce9f49abf22c2ac7a7e62f6e92b24a Mon Sep 17 00:00:00 2001 From: Stefan Melmuk Date: Sat, 21 Dec 2024 16:06:34 +0100 Subject: [PATCH 09/25] introduce attachment id --- src/api/core/ciphers.rs | 30 +++++++++---------- src/api/web.rs | 10 +++---- src/auth.rs | 6 ++-- src/crypto.rs | 5 ++-- src/db/models/attachment.rs | 60 +++++++++++++++++++++++++++++++++++-- src/db/models/mod.rs | 2 +- 6 files changed, 84 insertions(+), 29 deletions(-) diff --git a/src/api/core/ciphers.rs b/src/api/core/ciphers.rs index 758ec3b7d9..6e73e2dc5e 100644 --- a/src/api/core/ciphers.rs +++ b/src/api/core/ciphers.rs @@ -256,7 +256,7 @@ pub struct CipherData { // 'Attachments' is unused, contains map of {id: filename} #[allow(dead_code)] attachments: Option, - attachments2: Option>, + attachments2: Option>, // The revision datetime (in ISO 8601 format) of the client's local copy // of the cipher. This is used to prevent a client from updating a cipher @@ -1040,7 +1040,7 @@ async fn share_cipher_by_uuid( /// their object storage service. For self-hosted instances, it basically just /// redirects to the same location as before the v2 API. #[get("/ciphers//attachment/")] -async fn get_attachment(uuid: CipherId, attachment_id: &str, headers: Headers, mut conn: DbConn) -> JsonResult { +async fn get_attachment(uuid: CipherId, attachment_id: AttachmentId, headers: Headers, mut conn: DbConn) -> JsonResult { let Some(cipher) = Cipher::find_by_uuid(&uuid, &mut conn).await else { err!("Cipher doesn't exist") }; @@ -1049,7 +1049,7 @@ async fn get_attachment(uuid: CipherId, attachment_id: &str, headers: Headers, m err!("Cipher is not accessible") } - match Attachment::find_by_id(attachment_id, &mut conn).await { + match Attachment::find_by_id(&attachment_id, &mut conn).await { Some(attachment) if uuid == attachment.cipher_uuid => Ok(Json(attachment.to_json(&headers.host))), Some(_) => err!("Attachment doesn't belong to cipher"), None => err!("Attachment doesn't exist"), @@ -1265,7 +1265,7 @@ async fn save_attachment( } let folder_path = tokio::fs::canonicalize(&CONFIG.attachments_folder()).await?.join(cipher_uuid.as_ref()); - let file_path = folder_path.join(&file_id); + let file_path = folder_path.join(file_id.as_ref()); tokio::fs::create_dir_all(&folder_path).await?; if let Err(_err) = data.data.persist_to(&file_path).await { @@ -1305,13 +1305,13 @@ async fn save_attachment( #[post("/ciphers//attachment/", format = "multipart/form-data", data = "", rank = 1)] async fn post_attachment_v2_data( uuid: CipherId, - attachment_id: &str, + attachment_id: AttachmentId, data: Form>, headers: Headers, mut conn: DbConn, nt: Notify<'_>, ) -> EmptyResult { - let attachment = match Attachment::find_by_id(attachment_id, &mut conn).await { + let attachment = match Attachment::find_by_id(&attachment_id, &mut conn).await { Some(attachment) if uuid == attachment.cipher_uuid => Some(attachment), Some(_) => err!("Attachment doesn't belong to cipher"), None => err!("Attachment doesn't exist"), @@ -1354,20 +1354,20 @@ async fn post_attachment_admin( #[post("/ciphers//attachment//share", format = "multipart/form-data", data = "")] async fn post_attachment_share( uuid: CipherId, - attachment_id: &str, + attachment_id: AttachmentId, data: Form>, headers: Headers, mut conn: DbConn, nt: Notify<'_>, ) -> JsonResult { - _delete_cipher_attachment_by_id(&uuid, attachment_id, &headers, &mut conn, &nt).await?; + _delete_cipher_attachment_by_id(&uuid, &attachment_id, &headers, &mut conn, &nt).await?; post_attachment(uuid, data, headers, conn, nt).await } #[post("/ciphers//attachment//delete-admin")] async fn delete_attachment_post_admin( uuid: CipherId, - attachment_id: &str, + attachment_id: AttachmentId, headers: Headers, conn: DbConn, nt: Notify<'_>, @@ -1378,7 +1378,7 @@ async fn delete_attachment_post_admin( #[post("/ciphers//attachment//delete")] async fn delete_attachment_post( uuid: CipherId, - attachment_id: &str, + attachment_id: AttachmentId, headers: Headers, conn: DbConn, nt: Notify<'_>, @@ -1389,23 +1389,23 @@ async fn delete_attachment_post( #[delete("/ciphers//attachment/")] async fn delete_attachment( uuid: CipherId, - attachment_id: &str, + attachment_id: AttachmentId, headers: Headers, mut conn: DbConn, nt: Notify<'_>, ) -> EmptyResult { - _delete_cipher_attachment_by_id(&uuid, attachment_id, &headers, &mut conn, &nt).await + _delete_cipher_attachment_by_id(&uuid, &attachment_id, &headers, &mut conn, &nt).await } #[delete("/ciphers//attachment//admin")] async fn delete_attachment_admin( uuid: CipherId, - attachment_id: &str, + attachment_id: AttachmentId, headers: Headers, mut conn: DbConn, nt: Notify<'_>, ) -> EmptyResult { - _delete_cipher_attachment_by_id(&uuid, attachment_id, &headers, &mut conn, &nt).await + _delete_cipher_attachment_by_id(&uuid, &attachment_id, &headers, &mut conn, &nt).await } #[post("/ciphers//delete")] @@ -1789,7 +1789,7 @@ async fn _restore_multiple_ciphers( async fn _delete_cipher_attachment_by_id( uuid: &CipherId, - attachment_id: &str, + attachment_id: &AttachmentId, headers: &Headers, conn: &mut DbConn, nt: &Notify<'_>, diff --git a/src/api/web.rs b/src/api/web.rs index 1439fb1e33..608af26d4c 100644 --- a/src/api/web.rs +++ b/src/api/web.rs @@ -13,9 +13,9 @@ use serde_json::Value; use crate::{ api::{core::now, ApiResult, EmptyResult}, auth::decode_file_download, - db::models::CipherId, + db::models::{AttachmentId, CipherId}, error::Error, - util::{get_web_vault_version, Cached, SafeString}, + util::{get_web_vault_version, Cached}, CONFIG, }; @@ -197,15 +197,15 @@ async fn web_files(p: PathBuf) -> Cached> { } #[get("/attachments//?")] -async fn attachments(uuid: CipherId, file_id: SafeString, token: String) -> Option { +async fn attachments(uuid: CipherId, file_id: AttachmentId, token: String) -> Option { let Ok(claims) = decode_file_download(&token) else { return None; }; - if claims.sub != uuid || claims.file_id != *file_id { + if claims.sub != uuid || claims.file_id != file_id { return None; } - NamedFile::open(Path::new(&CONFIG.attachments_folder()).join(uuid.as_ref()).join(file_id)).await.ok() + NamedFile::open(Path::new(&CONFIG.attachments_folder()).join(uuid.as_ref()).join(file_id.as_ref())).await.ok() } // We use DbConn here to let the alive healthcheck also verify the database connection. diff --git a/src/auth.rs b/src/auth.rs index 565c363037..a7f403fbf3 100644 --- a/src/auth.rs +++ b/src/auth.rs @@ -14,7 +14,7 @@ use std::{ net::IpAddr, }; -use crate::db::models::{CipherId, CollectionId, MembershipId, OrganizationId, UserId}; +use crate::db::models::{AttachmentId, CipherId, CollectionId, MembershipId, OrganizationId, UserId}; use crate::{error::Error, CONFIG}; const JWT_ALGORITHM: Algorithm = Algorithm::RS256; @@ -295,10 +295,10 @@ pub struct FileDownloadClaims { // Subject pub sub: CipherId, - pub file_id: String, + pub file_id: AttachmentId, } -pub fn generate_file_download_claims(uuid: CipherId, file_id: String) -> FileDownloadClaims { +pub fn generate_file_download_claims(uuid: CipherId, file_id: AttachmentId) -> FileDownloadClaims { let time_now = Utc::now(); FileDownloadClaims { nbf: time_now.timestamp(), diff --git a/src/crypto.rs b/src/crypto.rs index 99f0fb91c9..c9db1a4b2c 100644 --- a/src/crypto.rs +++ b/src/crypto.rs @@ -89,9 +89,10 @@ pub fn generate_send_id() -> String { generate_id::<32>() // 256 bits } -pub fn generate_attachment_id() -> String { +use crate::db::models::AttachmentId; +pub fn generate_attachment_id() -> AttachmentId { // Attachment IDs are scoped to a cipher, so they can be smaller. - generate_id::<10>() // 80 bits + AttachmentId(generate_id::<10>()) // 80 bits } /// Generates a numeric token for email-based verifications. diff --git a/src/db/models/attachment.rs b/src/db/models/attachment.rs index 156cdadba1..90d90ad702 100644 --- a/src/db/models/attachment.rs +++ b/src/db/models/attachment.rs @@ -1,7 +1,13 @@ use std::io::ErrorKind; use bigdecimal::{BigDecimal, ToPrimitive}; +use rocket::request::FromParam; use serde_json::Value; +use std::{ + borrow::Borrow, + fmt::{Display, Formatter}, + ops::Deref, +}; use super::{CipherId, OrganizationId, UserId}; use crate::CONFIG; @@ -12,7 +18,7 @@ db_object! { #[diesel(treat_none_as_null = true)] #[diesel(primary_key(id))] pub struct Attachment { - pub id: String, + pub id: AttachmentId, pub cipher_uuid: CipherId, pub file_name: String, // encrypted pub file_size: i64, @@ -23,7 +29,7 @@ db_object! { /// Local methods impl Attachment { pub const fn new( - id: String, + id: AttachmentId, cipher_uuid: CipherId, file_name: String, file_size: i64, @@ -131,7 +137,7 @@ impl Attachment { Ok(()) } - pub async fn find_by_id(id: &str, conn: &mut DbConn) -> Option { + pub async fn find_by_id(id: &AttachmentId, conn: &mut DbConn) -> Option { db_run! { conn: { attachments::table .filter(attachments::id.eq(id.to_lowercase())) @@ -227,3 +233,51 @@ impl Attachment { }} } } + +#[derive(DieselNewType, FromForm, Clone, Debug, Hash, PartialEq, Eq, Serialize, Deserialize)] +pub struct AttachmentId(pub String); + +impl AsRef for AttachmentId { + fn as_ref(&self) -> &str { + &self.0 + } +} + +impl Deref for AttachmentId { + type Target = str; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl Borrow for AttachmentId { + fn borrow(&self) -> &str { + &self.0 + } +} + +impl Display for AttachmentId { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.0) + } +} + +impl From for AttachmentId { + fn from(raw: String) -> Self { + Self(raw) + } +} + +impl<'r> FromParam<'r> for AttachmentId { + 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 d832c04de1..c59c6f1398 100644 --- a/src/db/models/mod.rs +++ b/src/db/models/mod.rs @@ -16,7 +16,7 @@ mod two_factor_duo_context; mod two_factor_incomplete; mod user; -pub use self::attachment::Attachment; +pub use self::attachment::{Attachment, AttachmentId}; pub use self::auth_request::AuthRequest; pub use self::cipher::{Cipher, CipherId, RepromptType}; pub use self::collection::{Collection, CollectionCipher, CollectionId, CollectionUser}; From 423a08e749fd8cf9846867cc771f75789345bb8a Mon Sep 17 00:00:00 2001 From: Stefan Melmuk Date: Sat, 21 Dec 2024 17:46:36 +0100 Subject: [PATCH 10/25] add device_id newtype --- src/api/admin.rs | 4 +- src/api/core/accounts.rs | 26 ++++----- src/api/core/organizations.rs | 2 +- src/api/core/sends.rs | 6 +-- src/api/core/two_factor/duo_oidc.rs | 6 +-- src/api/identity.rs | 2 +- src/api/notifications.rs | 32 +++++------ src/api/push.rs | 19 ++++--- src/auth.rs | 4 +- src/db/models/auth_request.rs | 8 +-- src/db/models/device.rs | 74 +++++++++++++++++++++++--- src/db/models/mod.rs | 2 +- src/db/models/two_factor_incomplete.rs | 23 +++++--- 13 files changed, 142 insertions(+), 66 deletions(-) diff --git a/src/api/admin.rs b/src/api/admin.rs index 673af54548..8c3f178ae5 100644 --- a/src/api/admin.rs +++ b/src/api/admin.rs @@ -418,7 +418,7 @@ async fn delete_user(uuid: UserId, token: AdminToken, mut conn: DbConn) -> Empty async fn deauth_user(uuid: UserId, _token: AdminToken, mut conn: DbConn, nt: Notify<'_>) -> EmptyResult { let mut user = get_user_or_404(&uuid, &mut conn).await?; - nt.send_logout(&user, None).await; + nt.send_logout(&user, &DeviceId::empty()).await; if CONFIG.push_enabled() { for device in Device::find_push_devices_by_user(&user.uuid, &mut conn).await { @@ -444,7 +444,7 @@ async fn disable_user(uuid: UserId, _token: AdminToken, mut conn: DbConn, nt: No let save_result = user.save(&mut conn).await; - nt.send_logout(&user, None).await; + nt.send_logout(&user, &DeviceId::empty()).await; save_result } diff --git a/src/api/core/accounts.rs b/src/api/core/accounts.rs index c18179e359..27c29b53a2 100644 --- a/src/api/core/accounts.rs +++ b/src/api/core/accounts.rs @@ -374,7 +374,7 @@ async fn post_password(data: Json, headers: Headers, mut conn: D // Prevent logging out the client where the user requested this endpoint from. // If you do logout the user it will causes issues at the client side. // Adding the device uuid will prevent this. - nt.send_logout(&user, Some(headers.device.uuid)).await; + nt.send_logout(&user, &headers.device.uuid).await; save_result } @@ -434,7 +434,7 @@ async fn post_kdf(data: Json, headers: Headers, mut conn: DbConn, user.set_password(&data.new_master_password_hash, Some(data.key), true, None); let save_result = user.save(&mut conn).await; - nt.send_logout(&user, Some(headers.device.uuid)).await; + nt.send_logout(&user, &headers.device.uuid).await; save_result } @@ -646,7 +646,7 @@ async fn post_rotatekey(data: Json, headers: Headers, mut conn: DbConn, // Prevent logging out the client where the user requested this endpoint from. // If you do logout the user it will causes issues at the client side. // Adding the device uuid will prevent this. - nt.send_logout(&user, Some(headers.device.uuid)).await; + nt.send_logout(&user, &headers.device.uuid).await; save_result } @@ -662,7 +662,7 @@ async fn post_sstamp(data: Json, headers: Headers, mut conn: user.reset_security_stamp(); let save_result = user.save(&mut conn).await; - nt.send_logout(&user, None).await; + nt.send_logout(&user, &DeviceId::empty()).await; save_result } @@ -770,7 +770,7 @@ async fn post_email(data: Json, headers: Headers, mut conn: DbC let save_result = user.save(&mut conn).await; - nt.send_logout(&user, None).await; + nt.send_logout(&user, &DeviceId::empty()).await; save_result } @@ -1028,7 +1028,7 @@ async fn get_known_device(device: KnownDevice, mut conn: DbConn) -> JsonResult { struct KnownDevice { email: String, - uuid: String, + uuid: DeviceId, } #[rocket::async_trait] @@ -1051,7 +1051,7 @@ impl<'r> FromRequest<'r> for KnownDevice { }; let uuid = if let Some(uuid) = req.headers().get_one("X-Device-Identifier") { - uuid.to_string() + uuid.to_string().into() } else { return Outcome::Error((Status::BadRequest, "X-Device-Identifier value is required")); }; @@ -1108,7 +1108,7 @@ async fn put_device_token(uuid: &str, data: Json, headers: Headers, m } #[put("/devices/identifier//clear-token")] -async fn put_clear_device_token(uuid: &str, mut conn: DbConn) -> EmptyResult { +async fn put_clear_device_token(uuid: DeviceId, mut conn: DbConn) -> EmptyResult { // This only clears push token // https://github.com/bitwarden/core/blob/master/src/Api/Controllers/DevicesController.cs#L109 // https://github.com/bitwarden/core/blob/master/src/Core/Services/Implementations/DeviceService.cs#L37 @@ -1117,8 +1117,8 @@ async fn put_clear_device_token(uuid: &str, mut conn: DbConn) -> EmptyResult { return Ok(()); } - if let Some(device) = Device::find_by_uuid(uuid, &mut conn).await { - Device::clear_push_token_by_uuid(uuid, &mut conn).await?; + if let Some(device) = Device::find_by_uuid(&uuid, &mut conn).await { + Device::clear_push_token_by_uuid(&uuid, &mut conn).await?; unregister_push_device(device.push_uuid).await?; } @@ -1127,7 +1127,7 @@ async fn put_clear_device_token(uuid: &str, mut conn: DbConn) -> EmptyResult { // On upstream server, both PUT and POST are declared. Implementing the POST method in case it would be useful somewhere #[post("/devices/identifier//clear-token")] -async fn post_clear_device_token(uuid: &str, conn: DbConn) -> EmptyResult { +async fn post_clear_device_token(uuid: DeviceId, conn: DbConn) -> EmptyResult { put_clear_device_token(uuid, conn).await } @@ -1135,7 +1135,7 @@ async fn post_clear_device_token(uuid: &str, conn: DbConn) -> EmptyResult { #[serde(rename_all = "camelCase")] struct AuthRequestRequest { access_code: String, - device_identifier: String, + device_identifier: DeviceId, email: String, public_key: String, // Not used for now @@ -1215,7 +1215,7 @@ async fn get_auth_request(uuid: &str, headers: Headers, mut conn: DbConn) -> Jso #[derive(Debug, Deserialize)] #[serde(rename_all = "camelCase")] struct AuthResponseRequest { - device_identifier: String, + device_identifier: DeviceId, key: String, master_password_hash: Option, request_approved: bool, diff --git a/src/api/core/organizations.rs b/src/api/core/organizations.rs index 15168d941e..38206745b3 100644 --- a/src/api/core/organizations.rs +++ b/src/api/core/organizations.rs @@ -2910,7 +2910,7 @@ async fn put_reset_password( user.set_password(reset_request.new_master_password_hash.as_str(), Some(reset_request.key), true, None); user.save(&mut conn).await?; - nt.send_logout(&user, None).await; + nt.send_logout(&user, &DeviceId::empty()).await; log_event( EventType::OrganizationUserAdminResetPassword as i32, diff --git a/src/api/core/sends.rs b/src/api/core/sends.rs index cf217e9fd0..6eb923b663 100644 --- a/src/api/core/sends.rs +++ b/src/api/core/sends.rs @@ -485,7 +485,7 @@ async fn post_access( UpdateType::SyncSendUpdate, &send, &send.update_users_revision(&mut conn).await, - &String::from("00000000-0000-0000-0000-000000000000"), + &DeviceId::empty(), &mut conn, ) .await; @@ -542,7 +542,7 @@ async fn post_access_file( UpdateType::SyncSendUpdate, &send, &send.update_users_revision(&mut conn).await, - &String::from("00000000-0000-0000-0000-000000000000"), + &DeviceId::empty(), &mut conn, ) .await; @@ -635,7 +635,7 @@ pub async fn update_send_from_data( send.save(conn).await?; if ut != UpdateType::None { - nt.send_send_update(ut, send, &send.update_users_revision(conn).await, &headers.device.uuid, conn).await; + nt.send_send_update(ut, send, &send.update_users_revision(conn).await, &headers.device.uuid, conn).await } Ok(()) } diff --git a/src/api/core/two_factor/duo_oidc.rs b/src/api/core/two_factor/duo_oidc.rs index eb7fb3296a..e90d229f82 100644 --- a/src/api/core/two_factor/duo_oidc.rs +++ b/src/api/core/two_factor/duo_oidc.rs @@ -10,7 +10,7 @@ use crate::{ api::{core::two_factor::duo::get_duo_keys_email, EmptyResult}, crypto, db::{ - models::{EventType, TwoFactorDuoContext}, + models::{DeviceId, EventType, TwoFactorDuoContext}, DbConn, DbPool, }, error::Error, @@ -379,7 +379,7 @@ fn make_callback_url(client_name: &str) -> Result { pub async fn get_duo_auth_url( email: &str, client_id: &str, - device_identifier: &String, + device_identifier: &DeviceId, conn: &mut DbConn, ) -> Result { let (ik, sk, _, host) = get_duo_keys_email(email, conn).await?; @@ -417,7 +417,7 @@ pub async fn validate_duo_login( email: &str, two_factor_token: &str, client_id: &str, - device_identifier: &str, + device_identifier: &DeviceId, conn: &mut DbConn, ) -> EmptyResult { // Result supplied to us by clients in the form "|" diff --git a/src/api/identity.rs b/src/api/identity.rs index c1506adbad..5cdef8796f 100644 --- a/src/api/identity.rs +++ b/src/api/identity.rs @@ -747,7 +747,7 @@ struct ConnectData { #[field(name = uncased("device_identifier"))] #[field(name = uncased("deviceidentifier"))] - device_identifier: Option, + device_identifier: Option, #[field(name = uncased("device_name"))] #[field(name = uncased("devicename"))] device_name: Option, diff --git a/src/api/notifications.rs b/src/api/notifications.rs index 6653799a36..6c1e2b49f8 100644 --- a/src/api/notifications.rs +++ b/src/api/notifications.rs @@ -10,7 +10,7 @@ use rocket_ws::{Message, WebSocket}; use crate::{ auth::{ClientIp, WsAccessTokenHeader}, db::{ - models::{Cipher, CollectionId, Folder, Send as DbSend, User, UserId}, + models::{Cipher, CollectionId, DeviceId, Folder, Send as DbSend, User, UserId}, DbConn, }, Error, CONFIG, @@ -347,7 +347,7 @@ impl WebSocketUsers { let data = create_update( vec![("UserId".into(), user.uuid.to_string().into()), ("Date".into(), serialize_date(user.updated_at))], ut, - None, + DeviceId::empty(), ); if CONFIG.enable_websocket() { @@ -359,7 +359,7 @@ impl WebSocketUsers { } } - pub async fn send_logout(&self, user: &User, acting_device_uuid: Option) { + pub async fn send_logout(&self, user: &User, acting_device_uuid: &DeviceId) { // Skip any processing if both WebSockets and Push are not active if *NOTIFICATIONS_DISABLED { return; @@ -375,7 +375,7 @@ impl WebSocketUsers { } if CONFIG.push_enabled() { - push_logout(user, acting_device_uuid); + push_logout(user, acting_device_uuid.clone()); } } @@ -383,7 +383,7 @@ impl WebSocketUsers { &self, ut: UpdateType, folder: &Folder, - acting_device_uuid: &String, + acting_device_uuid: &DeviceId, conn: &mut DbConn, ) { // Skip any processing if both WebSockets and Push are not active @@ -397,7 +397,7 @@ impl WebSocketUsers { ("RevisionDate".into(), serialize_date(folder.updated_at)), ], ut, - Some(acting_device_uuid.into()), + acting_device_uuid.clone(), ); if CONFIG.enable_websocket() { @@ -414,7 +414,7 @@ impl WebSocketUsers { ut: UpdateType, cipher: &Cipher, user_uuids: &[UserId], - acting_device_uuid: &String, + acting_device_uuid: &DeviceId, collection_uuids: Option>, conn: &mut DbConn, ) { @@ -444,7 +444,7 @@ impl WebSocketUsers { ("RevisionDate".into(), revision_date), ], ut, - Some(acting_device_uuid.into()), + acting_device_uuid.clone(), ); if CONFIG.enable_websocket() { @@ -463,7 +463,7 @@ impl WebSocketUsers { ut: UpdateType, send: &DbSend, user_uuids: &[UserId], - acting_device_uuid: &String, + acting_device_uuid: &DeviceId, conn: &mut DbConn, ) { // Skip any processing if both WebSockets and Push are not active @@ -479,7 +479,7 @@ impl WebSocketUsers { ("RevisionDate".into(), serialize_date(send.revision_date)), ], ut, - None, + acting_device_uuid.clone(), ); if CONFIG.enable_websocket() { @@ -496,7 +496,7 @@ impl WebSocketUsers { &self, user_uuid: &UserId, auth_request_uuid: &String, - acting_device_uuid: &String, + acting_device_uuid: &DeviceId, conn: &mut DbConn, ) { // Skip any processing if both WebSockets and Push are not active @@ -506,7 +506,7 @@ impl WebSocketUsers { let data = create_update( vec![("Id".into(), auth_request_uuid.clone().into()), ("UserId".into(), user_uuid.to_string().into())], UpdateType::AuthRequest, - Some(acting_device_uuid.to_string()), + acting_device_uuid.clone(), ); if CONFIG.enable_websocket() { self.send_update(user_uuid, &data).await; @@ -521,7 +521,7 @@ impl WebSocketUsers { &self, user_uuid: &UserId, auth_response_uuid: &str, - approving_device_uuid: String, + approving_device_uuid: DeviceId, conn: &mut DbConn, ) { // Skip any processing if both WebSockets and Push are not active @@ -531,7 +531,7 @@ impl WebSocketUsers { let data = create_update( vec![("Id".into(), auth_response_uuid.to_owned().into()), ("UserId".into(), user_uuid.to_string().into())], UpdateType::AuthRequestResponse, - approving_device_uuid.clone().into(), + approving_device_uuid.clone(), ); if CONFIG.enable_websocket() { self.send_update(user_uuid, &data).await; @@ -585,7 +585,7 @@ impl AnonymousWebSocketSubscriptions { ] ] */ -fn create_update(payload: Vec<(Value, Value)>, ut: UpdateType, acting_device_uuid: Option) -> Vec { +fn create_update(payload: Vec<(Value, Value)>, ut: UpdateType, acting_device_uuid: DeviceId) -> Vec { use rmpv::Value as V; let value = V::Array(vec![ @@ -594,7 +594,7 @@ fn create_update(payload: Vec<(Value, Value)>, ut: UpdateType, acting_device_uui V::Nil, "ReceiveMessage".into(), V::Array(vec![V::Map(vec![ - ("ContextId".into(), acting_device_uuid.map(|v| v.into()).unwrap_or_else(|| V::Nil)), + ("ContextId".into(), acting_device_uuid.to_string().into()), ("Type".into(), (ut as i32).into()), ("Payload".into(), payload.into()), ])]), diff --git a/src/api/push.rs b/src/api/push.rs index bf9601a5de..25fe60fe8c 100644 --- a/src/api/push.rs +++ b/src/api/push.rs @@ -7,7 +7,7 @@ use tokio::sync::RwLock; use crate::{ api::{ApiResult, EmptyResult, UpdateType}, - db::models::{Cipher, Device, Folder, Send, User, UserId}, + db::models::{Cipher, Device, DeviceId, Folder, Send, User, UserId}, http_client::make_http_request, util::format_date, CONFIG, @@ -148,7 +148,7 @@ pub async fn unregister_push_device(push_uuid: Option) -> EmptyResult { pub async fn push_cipher_update( ut: UpdateType, cipher: &Cipher, - acting_device_uuid: &String, + acting_device_uuid: &DeviceId, conn: &mut crate::db::DbConn, ) { // We shouldn't send a push notification on cipher update if the cipher belongs to an organization, this isn't implemented in the upstream server too. @@ -178,8 +178,8 @@ pub async fn push_cipher_update( } } -pub fn push_logout(user: &User, acting_device_uuid: Option) { - let acting_device_uuid: Value = acting_device_uuid.map(|v| v.into()).unwrap_or_else(|| Value::Null); +pub fn push_logout(user: &User, acting_device_uuid: DeviceId) { + let acting_device_uuid: Value = acting_device_uuid.to_string().into(); tokio::task::spawn(send_to_push_relay(json!({ "userId": user.uuid, @@ -211,7 +211,7 @@ pub fn push_user_update(ut: UpdateType, user: &User) { pub async fn push_folder_update( ut: UpdateType, folder: &Folder, - acting_device_uuid: &String, + acting_device_uuid: &DeviceId, conn: &mut crate::db::DbConn, ) { if Device::check_user_has_push_device(&folder.user_uuid, conn).await { @@ -230,7 +230,12 @@ pub async fn push_folder_update( } } -pub async fn push_send_update(ut: UpdateType, send: &Send, acting_device_uuid: &String, conn: &mut crate::db::DbConn) { +pub async fn push_send_update( + ut: UpdateType, + send: &Send, + acting_device_uuid: &DeviceId, + conn: &mut crate::db::DbConn, +) { if let Some(s) = &send.user_uuid { if Device::check_user_has_push_device(s, conn).await { tokio::task::spawn(send_to_push_relay(json!({ @@ -303,7 +308,7 @@ pub async fn push_auth_request(user_uuid: UserId, auth_request_uuid: String, con pub async fn push_auth_response( user_uuid: UserId, auth_request_uuid: String, - approving_device_uuid: String, + approving_device_uuid: DeviceId, conn: &mut crate::db::DbConn, ) { if Device::check_user_has_push_device(&user_uuid, conn).await { diff --git a/src/auth.rs b/src/auth.rs index a7f403fbf3..b86d461ba9 100644 --- a/src/auth.rs +++ b/src/auth.rs @@ -14,7 +14,7 @@ use std::{ net::IpAddr, }; -use crate::db::models::{AttachmentId, CipherId, CollectionId, MembershipId, OrganizationId, UserId}; +use crate::db::models::{AttachmentId, CipherId, CollectionId, DeviceId, MembershipId, OrganizationId, UserId}; use crate::{error::Error, CONFIG}; const JWT_ALGORITHM: Algorithm = Algorithm::RS256; @@ -172,7 +172,7 @@ pub struct LoginJwtClaims { // user security_stamp pub sstamp: String, // device uuid - pub device: String, + pub device: DeviceId, // [ "api", "offline_access" ] pub scope: Vec, // [ "Application" ] diff --git a/src/db/models/auth_request.rs b/src/db/models/auth_request.rs index 3f3a53a91e..eab91d87ab 100644 --- a/src/db/models/auth_request.rs +++ b/src/db/models/auth_request.rs @@ -1,4 +1,4 @@ -use super::{OrganizationId, UserId}; +use super::{DeviceId, OrganizationId, UserId}; use crate::crypto::ct_eq; use chrono::{NaiveDateTime, Utc}; @@ -12,11 +12,11 @@ db_object! { pub user_uuid: UserId, pub organization_uuid: Option, - pub request_device_identifier: String, + pub request_device_identifier: DeviceId, pub device_type: i32, // https://github.com/bitwarden/server/blob/master/src/Core/Enums/DeviceType.cs pub request_ip: String, - pub response_device_id: Option, + pub response_device_id: Option, pub access_code: String, pub public_key: String, @@ -35,7 +35,7 @@ db_object! { impl AuthRequest { pub fn new( user_uuid: UserId, - request_device_identifier: String, + request_device_identifier: DeviceId, device_type: i32, request_ip: String, access_code: String, diff --git a/src/db/models/device.rs b/src/db/models/device.rs index 634d7c4f99..267a3141e0 100644 --- a/src/db/models/device.rs +++ b/src/db/models/device.rs @@ -1,4 +1,10 @@ use chrono::{NaiveDateTime, Utc}; +use rocket::request::FromParam; +use std::{ + borrow::Borrow, + fmt::{Display, Formatter}, + ops::Deref, +}; use super::UserId; use crate::{crypto, CONFIG}; @@ -10,7 +16,7 @@ db_object! { #[diesel(treat_none_as_null = true)] #[diesel(primary_key(uuid, user_uuid))] pub struct Device { - pub uuid: String, + pub uuid: DeviceId, pub created_at: NaiveDateTime, pub updated_at: NaiveDateTime, @@ -29,7 +35,7 @@ db_object! { /// Local methods impl Device { - pub fn new(uuid: String, user_uuid: UserId, name: String, atype: i32) -> Self { + pub fn new(uuid: DeviceId, user_uuid: UserId, name: String, atype: i32) -> Self { let now = Utc::now().naive_utc(); Self { @@ -159,7 +165,7 @@ impl Device { }} } - pub async fn find_by_uuid_and_user(uuid: &str, user_uuid: &UserId, conn: &mut DbConn) -> Option { + pub async fn find_by_uuid_and_user(uuid: &DeviceId, user_uuid: &UserId, conn: &mut DbConn) -> Option { db_run! { conn: { devices::table .filter(devices::uuid.eq(uuid)) @@ -180,7 +186,7 @@ impl Device { }} } - pub async fn find_by_uuid(uuid: &str, conn: &mut DbConn) -> Option { + pub async fn find_by_uuid(uuid: &DeviceId, conn: &mut DbConn) -> Option { db_run! { conn: { devices::table .filter(devices::uuid.eq(uuid)) @@ -190,7 +196,7 @@ impl Device { }} } - pub async fn clear_push_token_by_uuid(uuid: &str, conn: &mut DbConn) -> EmptyResult { + pub async fn clear_push_token_by_uuid(uuid: &DeviceId, conn: &mut DbConn) -> EmptyResult { db_run! { conn: { diesel::update(devices::table) .filter(devices::uuid.eq(uuid)) @@ -273,8 +279,8 @@ pub enum DeviceType { LinuxCLI = 25, } -impl fmt::Display for DeviceType { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { +impl Display for DeviceType { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { match self { DeviceType::Android => write!(f, "Android"), DeviceType::Ios => write!(f, "iOS"), @@ -339,3 +345,57 @@ impl DeviceType { } } } + +#[derive(DieselNewType, FromForm, Clone, Debug, Hash, PartialEq, Eq, Serialize, Deserialize)] +pub struct DeviceId(String); + +impl DeviceId { + pub fn empty() -> Self { + Self(String::from("00000000-0000-0000-0000-000000000000")) + } +} + +impl AsRef for DeviceId { + fn as_ref(&self) -> &str { + &self.0 + } +} + +impl Deref for DeviceId { + type Target = str; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl Borrow for DeviceId { + fn borrow(&self) -> &str { + &self.0 + } +} + +impl Display for DeviceId { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + write!(f, "{}", self.0) + } +} + +impl From for DeviceId { + fn from(raw: String) -> Self { + Self(raw) + } +} + +impl<'r> FromParam<'r> for DeviceId { + 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 c59c6f1398..9b2105a805 100644 --- a/src/db/models/mod.rs +++ b/src/db/models/mod.rs @@ -20,7 +20,7 @@ pub use self::attachment::{Attachment, AttachmentId}; pub use self::auth_request::AuthRequest; pub use self::cipher::{Cipher, CipherId, RepromptType}; pub use self::collection::{Collection, CollectionCipher, CollectionId, CollectionUser}; -pub use self::device::{Device, DeviceType}; +pub use self::device::{Device, DeviceId, DeviceType}; pub use self::emergency_access::{EmergencyAccess, EmergencyAccessStatus, EmergencyAccessType}; pub use self::event::{Event, EventType}; pub use self::favorite::Favorite; diff --git a/src/db/models/two_factor_incomplete.rs b/src/db/models/two_factor_incomplete.rs index 6c6cacb88b..b8dc4ad753 100644 --- a/src/db/models/two_factor_incomplete.rs +++ b/src/db/models/two_factor_incomplete.rs @@ -3,7 +3,10 @@ use chrono::{NaiveDateTime, Utc}; use crate::{ api::EmptyResult, auth::ClientIp, - db::{models::UserId, DbConn}, + db::{ + models::{DeviceId, UserId}, + DbConn, + }, error::MapResult, CONFIG, }; @@ -17,7 +20,7 @@ db_object! { // This device UUID is simply what's claimed by the device. It doesn't // necessarily correspond to any UUID in the devices table, since a device // must complete 2FA login before being added into the devices table. - pub device_uuid: String, + pub device_uuid: DeviceId, pub device_name: String, pub device_type: i32, pub login_time: NaiveDateTime, @@ -28,7 +31,7 @@ db_object! { impl TwoFactorIncomplete { pub async fn mark_incomplete( user_uuid: &UserId, - device_uuid: &str, + device_uuid: &DeviceId, device_name: &str, device_type: i32, ip: &ClientIp, @@ -61,7 +64,7 @@ impl TwoFactorIncomplete { }} } - pub async fn mark_complete(user_uuid: &UserId, device_uuid: &str, conn: &mut DbConn) -> EmptyResult { + pub async fn mark_complete(user_uuid: &UserId, device_uuid: &DeviceId, conn: &mut DbConn) -> EmptyResult { if CONFIG.incomplete_2fa_time_limit() <= 0 || !CONFIG.mail_enabled() { return Ok(()); } @@ -69,7 +72,11 @@ impl TwoFactorIncomplete { Self::delete_by_user_and_device(user_uuid, device_uuid, conn).await } - pub async fn find_by_user_and_device(user_uuid: &UserId, device_uuid: &str, conn: &mut DbConn) -> Option { + pub async fn find_by_user_and_device( + user_uuid: &UserId, + device_uuid: &DeviceId, + conn: &mut DbConn, + ) -> Option { db_run! { conn: { twofactor_incomplete::table .filter(twofactor_incomplete::user_uuid.eq(user_uuid)) @@ -94,7 +101,11 @@ impl TwoFactorIncomplete { Self::delete_by_user_and_device(&self.user_uuid, &self.device_uuid, conn).await } - pub async fn delete_by_user_and_device(user_uuid: &UserId, device_uuid: &str, conn: &mut DbConn) -> EmptyResult { + pub async fn delete_by_user_and_device( + user_uuid: &UserId, + device_uuid: &DeviceId, + conn: &mut DbConn, + ) -> EmptyResult { db_run! { conn: { diesel::delete(twofactor_incomplete::table .filter(twofactor_incomplete::user_uuid.eq(user_uuid)) From 6517bbab9b9e6b512fa7f787477d03e4c210bf75 Mon Sep 17 00:00:00 2001 From: Stefan Melmuk Date: Sat, 21 Dec 2024 22:39:46 +0100 Subject: [PATCH 11/25] add org_api_key_id --- src/auth.rs | 13 ++++++--- src/db/models/mod.rs | 3 +- src/db/models/organization.rs | 52 +++++++++++++++++++++++++++++++++-- 3 files changed, 61 insertions(+), 7 deletions(-) diff --git a/src/auth.rs b/src/auth.rs index b86d461ba9..2fcd974076 100644 --- a/src/auth.rs +++ b/src/auth.rs @@ -14,7 +14,9 @@ use std::{ net::IpAddr, }; -use crate::db::models::{AttachmentId, CipherId, CollectionId, DeviceId, MembershipId, OrganizationId, UserId}; +use crate::db::models::{ + AttachmentId, CipherId, CollectionId, DeviceId, MembershipId, OrgApiKeyId, OrganizationId, UserId, +}; use crate::{error::Error, CONFIG}; const JWT_ALGORITHM: Algorithm = Algorithm::RS256; @@ -264,20 +266,23 @@ pub struct OrgApiKeyLoginJwtClaims { // Issuer pub iss: String, // Subject - pub sub: String, + pub sub: OrgApiKeyId, pub client_id: String, pub client_sub: OrganizationId, pub scope: Vec, } -pub fn generate_organization_api_key_login_claims(uuid: String, org_id: OrganizationId) -> OrgApiKeyLoginJwtClaims { +pub fn generate_organization_api_key_login_claims( + org_api_key_uuid: OrgApiKeyId, + org_id: OrganizationId, +) -> OrgApiKeyLoginJwtClaims { let time_now = Utc::now(); OrgApiKeyLoginJwtClaims { nbf: time_now.timestamp(), exp: (time_now + TimeDelta::try_hours(1).unwrap()).timestamp(), iss: JWT_ORG_API_KEY_ISSUER.to_string(), - sub: uuid, + sub: org_api_key_uuid, client_id: format!("organization.{}", org_id), client_sub: org_id, scope: vec!["api.organization".into()], diff --git a/src/db/models/mod.rs b/src/db/models/mod.rs index 9b2105a805..2817fb5612 100644 --- a/src/db/models/mod.rs +++ b/src/db/models/mod.rs @@ -28,7 +28,8 @@ pub use self::folder::{Folder, FolderCipher, FolderId}; pub use self::group::{CollectionGroup, Group, GroupId, GroupUser}; pub use self::org_policy::{OrgPolicy, OrgPolicyErr, OrgPolicyType}; pub use self::organization::{ - Membership, MembershipId, MembershipStatus, MembershipType, Organization, OrganizationApiKey, OrganizationId, + Membership, MembershipId, MembershipStatus, MembershipType, OrgApiKeyId, Organization, OrganizationApiKey, + OrganizationId, }; pub use self::send::{Send, SendType}; pub use self::two_factor::{TwoFactor, TwoFactorType}; diff --git a/src/db/models/organization.rs b/src/db/models/organization.rs index 15bd25af36..6b37d302b0 100644 --- a/src/db/models/organization.rs +++ b/src/db/models/organization.rs @@ -48,7 +48,7 @@ db_object! { #[diesel(table_name = organization_api_key)] #[diesel(primary_key(uuid, org_uuid))] pub struct OrganizationApiKey { - pub uuid: String, + pub uuid: OrgApiKeyId, pub org_uuid: OrganizationId, pub atype: i32, pub api_key: String, @@ -263,7 +263,7 @@ impl Membership { impl OrganizationApiKey { pub fn new(org_uuid: OrganizationId, api_key: String) -> Self { Self { - uuid: crate::util::get_uuid(), + uuid: OrgApiKeyId(crate::util::get_uuid()), org_uuid, atype: 0, // Type 0 is the default and only type we support currently @@ -1107,6 +1107,54 @@ impl<'r> FromParam<'r> for OrganizationId { } } +#[derive(DieselNewType, FromForm, Clone, Debug, Hash, PartialEq, Eq, Serialize, Deserialize)] +pub struct OrgApiKeyId(String); + +impl AsRef for OrgApiKeyId { + fn as_ref(&self) -> &str { + &self.0 + } +} + +impl Deref for OrgApiKeyId { + type Target = str; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl Borrow for OrgApiKeyId { + fn borrow(&self) -> &str { + &self.0 + } +} + +impl Display for OrgApiKeyId { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.0) + } +} + +impl From for OrgApiKeyId { + fn from(raw: String) -> Self { + Self(raw) + } +} + +impl<'r> FromParam<'r> for OrgApiKeyId { + 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(()) + } + } +} + #[derive(DieselNewType, FromForm, Clone, Debug, Hash, PartialEq, Eq, Serialize, Deserialize)] pub struct MembershipId(String); From 23ef84d5e56f406b921065a9e8a0a2a288de9502 Mon Sep 17 00:00:00 2001 From: Stefan Melmuk Date: Sat, 21 Dec 2024 23:37:43 +0100 Subject: [PATCH 12/25] add to_json_details_for_group --- src/api/core/organizations.rs | 30 ++---------------------------- src/db/models/group.rs | 9 +++++++++ 2 files changed, 11 insertions(+), 28 deletions(-) diff --git a/src/api/core/organizations.rs b/src/api/core/organizations.rs index 38206745b3..974335a28b 100644 --- a/src/api/core/organizations.rs +++ b/src/api/core/organizations.rs @@ -366,9 +366,7 @@ async fn get_org_collections_details( CollectionGroup::find_by_collection(&col.uuid, &mut conn) .await .iter() - .map(|collection_group| { - GroupSelection::to_collection_group_details_read_only(collection_group).to_json() - }) + .map(|collection_group| collection_group.to_json_details_for_group()) .collect() } else { Vec::with_capacity(0) @@ -650,9 +648,7 @@ async fn get_org_collection_detail( CollectionGroup::find_by_collection(&collection.uuid, &mut conn) .await .iter() - .map(|collection_group| { - GroupSelection::to_collection_group_details_read_only(collection_group).to_json() - }) + .map(|collection_group| collection_group.to_json_details_for_group()) .collect() } else { // The Bitwarden clients seem to call this API regardless of whether groups are enabled, @@ -2377,28 +2373,6 @@ impl GroupRequest { } } -#[derive(Deserialize, Serialize)] -#[serde(rename_all = "camelCase")] -struct GroupSelection { - id: GroupId, - read_only: bool, - hide_passwords: bool, -} - -impl GroupSelection { - pub fn to_collection_group_details_read_only(collection_group: &CollectionGroup) -> Self { - Self { - id: collection_group.groups_uuid.clone(), - read_only: collection_group.read_only, - hide_passwords: collection_group.hide_passwords, - } - } - - pub fn to_json(&self) -> Value { - json!(self) - } -} - #[derive(Deserialize, Serialize)] #[serde(rename_all = "camelCase")] struct CollectionSelection { diff --git a/src/db/models/group.rs b/src/db/models/group.rs index 1ed23f04e9..60a2dce7eb 100644 --- a/src/db/models/group.rs +++ b/src/db/models/group.rs @@ -127,6 +127,15 @@ impl CollectionGroup { hide_passwords, } } + + pub fn to_json_details_for_group(&self) -> Value { + json!({ + "id": self.groups_uuid, + "readOnly": self.read_only, + "hidePasswords": self.hide_passwords, + "manage": false + }) + } } impl GroupUser { From 493d62188f4b09a2f1b1732165c6df5a9d7a9bed Mon Sep 17 00:00:00 2001 From: Stefan Melmuk Date: Sat, 21 Dec 2024 23:46:03 +0100 Subject: [PATCH 13/25] add to_json_details_for_user --- src/api/core/organizations.rs | 28 ++-------------------------- src/db/models/collection.rs | 11 +++++++++++ 2 files changed, 13 insertions(+), 26 deletions(-) diff --git a/src/api/core/organizations.rs b/src/api/core/organizations.rs index 974335a28b..455c2c7248 100644 --- a/src/api/core/organizations.rs +++ b/src/api/core/organizations.rs @@ -358,7 +358,7 @@ async fn get_org_collections_details( let users: Vec = col_users .iter() .filter(|collection_user| collection_user.collection_uuid == col.uuid) - .map(|collection_user| UserSelection::to_collection_user_details_read_only(collection_user).to_json()) + .map(|collection_user| collection_user.to_json_details_for_user()) .collect(); // get the group details for the given collection @@ -660,9 +660,7 @@ async fn get_org_collection_detail( CollectionUser::find_by_collection_swap_user_uuid_with_member_uuid(&collection.uuid, &mut conn) .await .iter() - .map(|collection_user| { - UserSelection::to_collection_user_details_read_only(collection_user).to_json() - }) + .map(|collection_user| collection_user.to_json_details_for_user()) .collect(); let assigned = Collection::can_access_collection(&member, &collection.uuid, &mut conn).await; @@ -2387,28 +2385,6 @@ impl CollectionSelection { } } -#[derive(Deserialize, Serialize)] -#[serde(rename_all = "camelCase")] -struct UserSelection { - id: UserId, - read_only: bool, - hide_passwords: bool, -} - -impl UserSelection { - pub fn to_collection_user_details_read_only(collection_user: &CollectionUser) -> Self { - Self { - id: collection_user.user_uuid.clone(), - read_only: collection_user.read_only, - hide_passwords: collection_user.hide_passwords, - } - } - - pub fn to_json(&self) -> Value { - json!(self) - } -} - #[post("/organizations//groups/", data = "")] async fn post_group( org_id: OrganizationId, diff --git a/src/db/models/collection.rs b/src/db/models/collection.rs index 1b8c11bf4f..52d23fa6ef 100644 --- a/src/db/models/collection.rs +++ b/src/db/models/collection.rs @@ -779,6 +779,17 @@ impl CollectionCipher { } } +impl CollectionUser { + pub fn to_json_details_for_user(&self) -> Value { + json!({ + "id": self.user_uuid, + "readOnly": self.read_only, + "hidePasswords": self.hide_passwords, + "manage": false + }) + } +} + #[derive(DieselNewType, FromForm, Clone, Debug, Hash, PartialEq, Eq, Serialize, Deserialize)] pub struct CollectionId(String); From 9e93571ac02227a8f02f270863b2c6851a4cc129 Mon Sep 17 00:00:00 2001 From: Stefan Melmuk Date: Sat, 21 Dec 2024 23:49:14 +0100 Subject: [PATCH 14/25] rename to selected_collections --- src/api/core/organizations.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/api/core/organizations.rs b/src/api/core/organizations.rs index 455c2c7248..1ea82c7acf 100644 --- a/src/api/core/organizations.rs +++ b/src/api/core/organizations.rs @@ -2352,7 +2352,7 @@ struct GroupRequest { #[serde(default)] access_all: bool, external_id: Option, - collections: Vec, + collections: Vec, users: Vec, } @@ -2373,13 +2373,13 @@ impl GroupRequest { #[derive(Deserialize, Serialize)] #[serde(rename_all = "camelCase")] -struct CollectionSelection { +struct SelectedCollection { id: CollectionId, read_only: bool, hide_passwords: bool, } -impl CollectionSelection { +impl SelectedCollection { pub fn to_collection_group(&self, groups_uuid: GroupId) -> CollectionGroup { CollectionGroup::new(self.id.clone(), groups_uuid, self.read_only, self.hide_passwords) } @@ -2462,7 +2462,7 @@ async fn put_group( async fn add_update_group( mut group: Group, - collections: Vec, + collections: Vec, members: Vec, org_id: OrganizationId, headers: &AdminHeaders, From 3ae88b3eca48c2c567945228b678b66f6d46953e Mon Sep 17 00:00:00 2001 From: Stefan Melmuk Date: Sun, 22 Dec 2024 15:56:18 +0100 Subject: [PATCH 15/25] less boilerplate by using derive_more --- Cargo.lock | 22 ++++++ Cargo.toml | 2 + src/api/identity.rs | 2 +- src/api/notifications.rs | 2 +- src/db/models/attachment.rs | 40 +---------- src/db/models/cipher.rs | 42 ++--------- src/db/models/collection.rs | 42 ++--------- src/db/models/device.rs | 101 ++++++++------------------- src/db/models/folder.rs | 42 ++--------- src/db/models/group.rs | 42 ++--------- src/db/models/organization.rs | 127 +++------------------------------- src/db/models/user.rs | 44 ++---------- src/mail.rs | 12 +++- 13 files changed, 99 insertions(+), 421 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index c15428a9f3..bc68767d82 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -721,6 +721,27 @@ dependencies = [ "powerfmt", ] +[[package]] +name = "derive_more" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a9b99b9cbbe49445b21764dc0625032a89b145a2642e67603e1c936f5458d05" +dependencies = [ + "derive_more-impl", +] + +[[package]] +name = "derive_more-impl" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb7330aeadfbe296029522e6c40f315320aba36fc43a5b3632f3795348f3bd22" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "unicode-xid", +] + [[package]] name = "devise" version = "0.4.2" @@ -3970,6 +3991,7 @@ dependencies = [ "dashmap", "data-encoding", "data-url", + "derive_more", "diesel", "diesel-derive-newtype", "diesel_logger", diff --git a/Cargo.toml b/Cargo.toml index 12ab7229e7..79cda3bb73 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -77,6 +77,8 @@ serde_json = "1.0.133" diesel = { version = "2.2.6", features = ["chrono", "r2d2", "numeric"] } diesel_migrations = "2.2.0" diesel_logger = { version = "0.4.0", optional = true } + +derive_more = { version = "1.0.0", features = ["from", "into", "as_ref", "deref", "display"] } diesel-derive-newtype = "2.1.2" # Bundled/Static SQLite diff --git a/src/api/identity.rs b/src/api/identity.rs index 5cdef8796f..27bb8ab42c 100644 --- a/src/api/identity.rs +++ b/src/api/identity.rs @@ -385,7 +385,7 @@ async fn _user_api_key_login( let Some(client_user_uuid) = client_id.strip_prefix("user.") else { err!("Malformed client_id", format!("IP: {}.", ip.ip)) }; - let client_user_uuid: UserId = client_user_uuid.to_string().into(); + let client_user_uuid: UserId = client_user_uuid.into(); let Some(user) = User::find_by_uuid(&client_user_uuid, conn).await else { err!("Invalid client_id", format!("IP: {}.", ip.ip)) }; diff --git a/src/api/notifications.rs b/src/api/notifications.rs index 6c1e2b49f8..30ca79d539 100644 --- a/src/api/notifications.rs +++ b/src/api/notifications.rs @@ -72,7 +72,7 @@ impl WSEntryMapGuard { impl Drop for WSEntryMapGuard { fn drop(&mut self) { info!("Closing WS connection from {}", self.addr); - if let Some(mut entry) = self.users.map.get_mut(&self.user_uuid.to_string()) { + if let Some(mut entry) = self.users.map.get_mut(self.user_uuid.as_ref()) { entry.retain(|(uuid, _)| uuid != &self.entry_uuid); } } diff --git a/src/db/models/attachment.rs b/src/db/models/attachment.rs index 90d90ad702..c86bd636e7 100644 --- a/src/db/models/attachment.rs +++ b/src/db/models/attachment.rs @@ -1,13 +1,9 @@ use std::io::ErrorKind; use bigdecimal::{BigDecimal, ToPrimitive}; +use derive_more::{AsRef, Deref, Display}; use rocket::request::FromParam; use serde_json::Value; -use std::{ - borrow::Borrow, - fmt::{Display, Formatter}, - ops::Deref, -}; use super::{CipherId, OrganizationId, UserId}; use crate::CONFIG; @@ -234,41 +230,9 @@ impl Attachment { } } -#[derive(DieselNewType, FromForm, Clone, Debug, Hash, PartialEq, Eq, Serialize, Deserialize)] +#[derive(Clone, Debug, AsRef, Deref, DieselNewType, Display, FromForm, Hash, PartialEq, Eq, Serialize, Deserialize)] pub struct AttachmentId(pub String); -impl AsRef for AttachmentId { - fn as_ref(&self) -> &str { - &self.0 - } -} - -impl Deref for AttachmentId { - type Target = str; - - fn deref(&self) -> &Self::Target { - &self.0 - } -} - -impl Borrow for AttachmentId { - fn borrow(&self) -> &str { - &self.0 - } -} - -impl Display for AttachmentId { - fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - write!(f, "{}", self.0) - } -} - -impl From for AttachmentId { - fn from(raw: String) -> Self { - Self(raw) - } -} - impl<'r> FromParam<'r> for AttachmentId { type Error = (); diff --git a/src/db/models/cipher.rs b/src/db/models/cipher.rs index 933b50eeed..06728f97a5 100644 --- a/src/db/models/cipher.rs +++ b/src/db/models/cipher.rs @@ -1,13 +1,9 @@ use crate::util::LowerCase; use crate::CONFIG; use chrono::{NaiveDateTime, TimeDelta, Utc}; +use derive_more::{AsRef, Deref, Display, From}; use rocket::request::FromParam; use serde_json::Value; -use std::{ - borrow::Borrow, - fmt::{Display, Formatter}, - ops::Deref, -}; use super::{ Attachment, CollectionCipher, CollectionId, Favorite, FolderCipher, FolderId, Group, Membership, MembershipStatus, @@ -1034,41 +1030,11 @@ impl Cipher { } } -#[derive(DieselNewType, FromForm, Clone, Debug, Hash, PartialEq, Eq, Serialize, Deserialize)] +#[derive( + Clone, Debug, AsRef, Deref, DieselNewType, Display, From, FromForm, Hash, PartialEq, Eq, Serialize, Deserialize, +)] pub struct CipherId(String); -impl AsRef for CipherId { - fn as_ref(&self) -> &str { - &self.0 - } -} - -impl Deref for CipherId { - type Target = str; - - fn deref(&self) -> &Self::Target { - &self.0 - } -} - -impl Borrow for CipherId { - fn borrow(&self) -> &str { - &self.0 - } -} - -impl Display for CipherId { - fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - write!(f, "{}", self.0) - } -} - -impl From for CipherId { - fn from(raw: String) -> Self { - Self(raw) - } -} - impl<'r> FromParam<'r> for CipherId { type Error = (); diff --git a/src/db/models/collection.rs b/src/db/models/collection.rs index 52d23fa6ef..7d94f9d54e 100644 --- a/src/db/models/collection.rs +++ b/src/db/models/collection.rs @@ -1,10 +1,6 @@ +use derive_more::{AsRef, Deref, Display, From}; use rocket::request::FromParam; use serde_json::Value; -use std::{ - borrow::Borrow, - fmt::{Display, Formatter}, - ops::Deref, -}; use super::{ CipherId, CollectionGroup, GroupUser, Membership, MembershipStatus, MembershipType, OrganizationId, User, UserId, @@ -790,41 +786,11 @@ impl CollectionUser { } } -#[derive(DieselNewType, FromForm, Clone, Debug, Hash, PartialEq, Eq, Serialize, Deserialize)] +#[derive( + Clone, Debug, AsRef, Deref, DieselNewType, Display, From, FromForm, Hash, PartialEq, Eq, Serialize, Deserialize, +)] pub struct CollectionId(String); -impl AsRef for CollectionId { - fn as_ref(&self) -> &str { - &self.0 - } -} - -impl Deref for CollectionId { - type Target = str; - - fn deref(&self) -> &Self::Target { - &self.0 - } -} - -impl Borrow for CollectionId { - fn borrow(&self) -> &str { - &self.0 - } -} - -impl Display for CollectionId { - fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - write!(f, "{}", self.0) - } -} - -impl From for CollectionId { - fn from(raw: String) -> Self { - Self(raw) - } -} - impl<'r> FromParam<'r> for CollectionId { type Error = (); diff --git a/src/db/models/device.rs b/src/db/models/device.rs index 267a3141e0..69c96bec0d 100644 --- a/src/db/models/device.rs +++ b/src/db/models/device.rs @@ -1,14 +1,9 @@ use chrono::{NaiveDateTime, Utc}; +use derive_more::{Display, From}; use rocket::request::FromParam; -use std::{ - borrow::Borrow, - fmt::{Display, Formatter}, - ops::Deref, -}; use super::UserId; use crate::{crypto, CONFIG}; -use core::fmt; db_object! { #[derive(Identifiable, Queryable, Insertable, AsChangeset)] @@ -250,68 +245,62 @@ impl Device { } } +#[derive(Display)] pub enum DeviceType { + #[display("Android")] Android = 0, + #[display("iOS")] Ios = 1, + #[display("Chrome Extension")] ChromeExtension = 2, + #[display("Firefox Extension")] FirefoxExtension = 3, + #[display("Opera Extension")] OperaExtension = 4, + #[display("Edge Extension")] EdgeExtension = 5, + #[display("Windows")] WindowsDesktop = 6, + #[display("macOS")] MacOsDesktop = 7, + #[display("Linux")] LinuxDesktop = 8, + #[display("Chrome")] ChromeBrowser = 9, + #[display("Firefox")] FirefoxBrowser = 10, + #[display("Opera")] OperaBrowser = 11, + #[display("Edge")] EdgeBrowser = 12, + #[display("Internet Explorer")] IEBrowser = 13, + #[display("Unknown Browser")] UnknownBrowser = 14, + #[display("Android")] AndroidAmazon = 15, + #[display("UWP")] Uwp = 16, + #[display("Safari")] SafariBrowser = 17, + #[display("Vivaldi")] VivaldiBrowser = 18, + #[display("Vivaldi Extension")] VivaldiExtension = 19, + #[display("Safari Extension")] SafariExtension = 20, + #[display("SDK")] Sdk = 21, + #[display("Server")] Server = 22, + #[display("Windows CLI")] WindowsCLI = 23, + #[display("macOS CLI")] MacOsCLI = 24, + #[display("Linux CLI")] LinuxCLI = 25, } -impl Display for DeviceType { - fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { - match self { - DeviceType::Android => write!(f, "Android"), - DeviceType::Ios => write!(f, "iOS"), - DeviceType::ChromeExtension => write!(f, "Chrome Extension"), - DeviceType::FirefoxExtension => write!(f, "Firefox Extension"), - DeviceType::OperaExtension => write!(f, "Opera Extension"), - DeviceType::EdgeExtension => write!(f, "Edge Extension"), - DeviceType::WindowsDesktop => write!(f, "Windows"), - DeviceType::MacOsDesktop => write!(f, "macOS"), - DeviceType::LinuxDesktop => write!(f, "Linux"), - DeviceType::ChromeBrowser => write!(f, "Chrome"), - DeviceType::FirefoxBrowser => write!(f, "Firefox"), - DeviceType::OperaBrowser => write!(f, "Opera"), - DeviceType::EdgeBrowser => write!(f, "Edge"), - DeviceType::IEBrowser => write!(f, "Internet Explorer"), - DeviceType::UnknownBrowser => write!(f, "Unknown Browser"), - DeviceType::AndroidAmazon => write!(f, "Android"), - DeviceType::Uwp => write!(f, "UWP"), - DeviceType::SafariBrowser => write!(f, "Safari"), - DeviceType::VivaldiBrowser => write!(f, "Vivaldi"), - DeviceType::VivaldiExtension => write!(f, "Vivaldi Extension"), - DeviceType::SafariExtension => write!(f, "Safari Extension"), - DeviceType::Sdk => write!(f, "SDK"), - DeviceType::Server => write!(f, "Server"), - DeviceType::WindowsCLI => write!(f, "Windows CLI"), - DeviceType::MacOsCLI => write!(f, "macOS CLI"), - DeviceType::LinuxCLI => write!(f, "Linux CLI"), - } - } -} - impl DeviceType { pub fn from_i32(value: i32) -> DeviceType { match value { @@ -346,7 +335,7 @@ impl DeviceType { } } -#[derive(DieselNewType, FromForm, Clone, Debug, Hash, PartialEq, Eq, Serialize, Deserialize)] +#[derive(Clone, Debug, DieselNewType, Display, From, FromForm, Hash, PartialEq, Eq, Serialize, Deserialize)] pub struct DeviceId(String); impl DeviceId { @@ -355,38 +344,6 @@ impl DeviceId { } } -impl AsRef for DeviceId { - fn as_ref(&self) -> &str { - &self.0 - } -} - -impl Deref for DeviceId { - type Target = str; - - fn deref(&self) -> &Self::Target { - &self.0 - } -} - -impl Borrow for DeviceId { - fn borrow(&self) -> &str { - &self.0 - } -} - -impl Display for DeviceId { - fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { - write!(f, "{}", self.0) - } -} - -impl From for DeviceId { - fn from(raw: String) -> Self { - Self(raw) - } -} - impl<'r> FromParam<'r> for DeviceId { type Error = (); diff --git a/src/db/models/folder.rs b/src/db/models/folder.rs index 6a669ade4e..ea7208bd99 100644 --- a/src/db/models/folder.rs +++ b/src/db/models/folder.rs @@ -1,11 +1,7 @@ use chrono::{NaiveDateTime, Utc}; +use derive_more::{AsRef, Deref, Display, From}; use rocket::request::FromParam; use serde_json::Value; -use std::{ - borrow::Borrow, - fmt::{Display, Formatter}, - ops::Deref, -}; use super::{CipherId, User, UserId}; @@ -238,41 +234,11 @@ impl FolderCipher { } } -#[derive(DieselNewType, FromForm, Clone, Debug, Hash, PartialEq, Eq, Serialize, Deserialize)] +#[derive( + Clone, Debug, AsRef, Deref, DieselNewType, Display, From, FromForm, Hash, PartialEq, Eq, Serialize, Deserialize, +)] pub struct FolderId(String); -impl AsRef for FolderId { - fn as_ref(&self) -> &str { - &self.0 - } -} - -impl Deref for FolderId { - type Target = str; - - fn deref(&self) -> &Self::Target { - &self.0 - } -} - -impl Borrow for FolderId { - fn borrow(&self) -> &str { - &self.0 - } -} - -impl Display for FolderId { - fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - write!(f, "{}", self.0) - } -} - -impl From for FolderId { - fn from(raw: String) -> Self { - Self(raw) - } -} - impl<'r> FromParam<'r> for FolderId { type Error = (); diff --git a/src/db/models/group.rs b/src/db/models/group.rs index 60a2dce7eb..6bd4b6f90a 100644 --- a/src/db/models/group.rs +++ b/src/db/models/group.rs @@ -3,13 +3,9 @@ use crate::api::EmptyResult; use crate::db::DbConn; use crate::error::MapResult; use chrono::{NaiveDateTime, Utc}; +use derive_more::{AsRef, Deref, Display, From}; use rocket::request::FromParam; use serde_json::Value; -use std::{ - borrow::Borrow, - fmt::{Display, Formatter}, - ops::Deref, -}; db_object! { #[derive(Identifiable, Queryable, Insertable, AsChangeset)] @@ -602,41 +598,11 @@ impl GroupUser { } } -#[derive(DieselNewType, FromForm, Clone, Debug, Hash, PartialEq, Eq, Serialize, Deserialize)] +#[derive( + Clone, Debug, AsRef, Deref, DieselNewType, Display, From, FromForm, Hash, PartialEq, Eq, Serialize, Deserialize, +)] pub struct GroupId(String); -impl AsRef for GroupId { - fn as_ref(&self) -> &str { - &self.0 - } -} - -impl Deref for GroupId { - type Target = str; - - fn deref(&self) -> &Self::Target { - &self.0 - } -} - -impl Borrow for GroupId { - fn borrow(&self) -> &str { - &self.0 - } -} - -impl Display for GroupId { - fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - write!(f, "{}", self.0) - } -} - -impl From for GroupId { - fn from(raw: String) -> Self { - Self(raw) - } -} - impl<'r> FromParam<'r> for GroupId { type Error = (); diff --git a/src/db/models/organization.rs b/src/db/models/organization.rs index 6b37d302b0..c6f5caf0bf 100644 --- a/src/db/models/organization.rs +++ b/src/db/models/organization.rs @@ -1,13 +1,11 @@ use chrono::{NaiveDateTime, Utc}; +use derive_more::{AsRef, Deref, Display, From}; use num_traits::FromPrimitive; use rocket::request::FromParam; use serde_json::Value; use std::{ - borrow::Borrow, cmp::Ordering, collections::{HashMap, HashSet}, - fmt::{Display, Formatter}, - ops::Deref, }; use super::{ @@ -1059,41 +1057,13 @@ impl OrganizationApiKey { } } -#[derive(DieselNewType, FromForm, Clone, Debug, Hash, PartialEq, Eq, Serialize, Deserialize)] +#[derive( + Clone, Debug, AsRef, Deref, DieselNewType, Display, From, FromForm, Hash, PartialEq, Eq, Serialize, Deserialize, +)] +#[deref(forward)] +#[from(forward)] pub struct OrganizationId(String); -impl AsRef for OrganizationId { - fn as_ref(&self) -> &str { - &self.0 - } -} - -impl Deref for OrganizationId { - type Target = str; - - fn deref(&self) -> &Self::Target { - &self.0 - } -} - -impl Borrow for OrganizationId { - fn borrow(&self) -> &str { - &self.0 - } -} - -impl Display for OrganizationId { - fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - write!(f, "{}", self.0) - } -} - -impl From for OrganizationId { - fn from(raw: String) -> Self { - Self(raw) - } -} - impl<'r> FromParam<'r> for OrganizationId { type Error = (); @@ -1107,89 +1077,9 @@ impl<'r> FromParam<'r> for OrganizationId { } } -#[derive(DieselNewType, FromForm, Clone, Debug, Hash, PartialEq, Eq, Serialize, Deserialize)] -pub struct OrgApiKeyId(String); - -impl AsRef for OrgApiKeyId { - fn as_ref(&self) -> &str { - &self.0 - } -} - -impl Deref for OrgApiKeyId { - type Target = str; - - fn deref(&self) -> &Self::Target { - &self.0 - } -} - -impl Borrow for OrgApiKeyId { - fn borrow(&self) -> &str { - &self.0 - } -} - -impl Display for OrgApiKeyId { - fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - write!(f, "{}", self.0) - } -} - -impl From for OrgApiKeyId { - fn from(raw: String) -> Self { - Self(raw) - } -} - -impl<'r> FromParam<'r> for OrgApiKeyId { - 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(()) - } - } -} - -#[derive(DieselNewType, FromForm, Clone, Debug, Hash, PartialEq, Eq, Serialize, Deserialize)] +#[derive(Clone, Debug, Deref, DieselNewType, Display, From, FromForm, Hash, PartialEq, Eq, Serialize, Deserialize)] pub struct MembershipId(String); -impl AsRef for MembershipId { - fn as_ref(&self) -> &str { - &self.0 - } -} - -impl Deref for MembershipId { - type Target = str; - - fn deref(&self) -> &Self::Target { - &self.0 - } -} - -impl Borrow for MembershipId { - fn borrow(&self) -> &str { - &self.0 - } -} - -impl Display for MembershipId { - fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - write!(f, "{}", self.0) - } -} - -impl From for MembershipId { - fn from(raw: String) -> Self { - Self(raw) - } -} - impl<'r> FromParam<'r> for MembershipId { type Error = (); @@ -1203,6 +1093,9 @@ impl<'r> FromParam<'r> for MembershipId { } } +#[derive(Clone, Debug, DieselNewType, Display, FromForm, Hash, PartialEq, Eq, Serialize, Deserialize)] +pub struct OrgApiKeyId(String); + #[cfg(test)] mod tests { use super::*; diff --git a/src/db/models/user.rs b/src/db/models/user.rs index 99b3895594..411b9ac72a 100644 --- a/src/db/models/user.rs +++ b/src/db/models/user.rs @@ -1,11 +1,7 @@ use chrono::{NaiveDateTime, TimeDelta, Utc}; +use derive_more::{AsRef, Deref, Display, From}; use rocket::request::FromParam; use serde_json::Value; -use std::{ - borrow::Borrow, - fmt::{Display, Formatter}, - ops::Deref, -}; use super::{ Cipher, Device, EmergencyAccess, Favorite, Folder, Membership, MembershipType, TwoFactor, TwoFactorIncomplete, @@ -463,41 +459,13 @@ impl Invitation { } } -#[derive(DieselNewType, FromForm, Clone, Debug, Hash, PartialEq, Eq, Serialize, Deserialize)] +#[derive( + Clone, Debug, AsRef, Deref, DieselNewType, Display, From, FromForm, Hash, PartialEq, Eq, Serialize, Deserialize, +)] +#[deref(forward)] +#[from(forward)] pub struct UserId(String); -impl AsRef for UserId { - fn as_ref(&self) -> &str { - &self.0 - } -} - -impl Deref for UserId { - type Target = str; - - fn deref(&self) -> &Self::Target { - &self.0 - } -} - -impl Borrow for UserId { - fn borrow(&self) -> &str { - &self.0 - } -} - -impl Display for UserId { - fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - write!(f, "{}", self.0) - } -} - -impl From for UserId { - fn from(raw: String) -> Self { - Self(raw) - } -} - impl<'r> FromParam<'r> for UserId { type Error = (); diff --git a/src/mail.rs b/src/mail.rs index 476968fd06..29ee709479 100644 --- a/src/mail.rs +++ b/src/mail.rs @@ -272,14 +272,22 @@ pub async fn send_invite( invited_by_email, ); let invite_token = encode_jwt(&claims); + let org_id = match org_id { + Some(ref org_id) => org_id.as_ref(), + None => "_", + }; + let member_id = match member_id { + Some(ref member_id) => member_id.as_ref(), + None => "_", + }; let mut query = url::Url::parse("https://query.builder").unwrap(); { let mut query_params = query.query_pairs_mut(); query_params .append_pair("email", &user.email) .append_pair("organizationName", org_name) - .append_pair("organizationId", org_id.as_deref().unwrap_or("_")) - .append_pair("organizationUserId", member_id.as_deref().unwrap_or("_")) + .append_pair("organizationId", org_id) + .append_pair("organizationUserId", member_id) .append_pair("token", &invite_token); if user.private_key.is_some() { query_params.append_pair("orgUserHasExistingUser", "true"); From e361d2445bbc53da1f200274fb321d4a27c54e65 Mon Sep 17 00:00:00 2001 From: Stefan Melmuk Date: Sun, 22 Dec 2024 22:46:25 +0100 Subject: [PATCH 16/25] revert changes to notification interface --- src/api/admin.rs | 4 ++-- src/api/core/accounts.rs | 10 +++++----- src/api/core/organizations.rs | 2 +- src/api/core/sends.rs | 6 +++--- src/api/notifications.rs | 18 +++++++++--------- src/api/push.rs | 4 ++-- src/db/models/device.rs | 6 ------ 7 files changed, 22 insertions(+), 28 deletions(-) diff --git a/src/api/admin.rs b/src/api/admin.rs index 8c3f178ae5..673af54548 100644 --- a/src/api/admin.rs +++ b/src/api/admin.rs @@ -418,7 +418,7 @@ async fn delete_user(uuid: UserId, token: AdminToken, mut conn: DbConn) -> Empty async fn deauth_user(uuid: UserId, _token: AdminToken, mut conn: DbConn, nt: Notify<'_>) -> EmptyResult { let mut user = get_user_or_404(&uuid, &mut conn).await?; - nt.send_logout(&user, &DeviceId::empty()).await; + nt.send_logout(&user, None).await; if CONFIG.push_enabled() { for device in Device::find_push_devices_by_user(&user.uuid, &mut conn).await { @@ -444,7 +444,7 @@ async fn disable_user(uuid: UserId, _token: AdminToken, mut conn: DbConn, nt: No let save_result = user.save(&mut conn).await; - nt.send_logout(&user, &DeviceId::empty()).await; + nt.send_logout(&user, None).await; save_result } diff --git a/src/api/core/accounts.rs b/src/api/core/accounts.rs index 27c29b53a2..ae199abe37 100644 --- a/src/api/core/accounts.rs +++ b/src/api/core/accounts.rs @@ -374,7 +374,7 @@ async fn post_password(data: Json, headers: Headers, mut conn: D // Prevent logging out the client where the user requested this endpoint from. // If you do logout the user it will causes issues at the client side. // Adding the device uuid will prevent this. - nt.send_logout(&user, &headers.device.uuid).await; + nt.send_logout(&user, Some(headers.device.uuid.to_string())).await; save_result } @@ -434,7 +434,7 @@ async fn post_kdf(data: Json, headers: Headers, mut conn: DbConn, user.set_password(&data.new_master_password_hash, Some(data.key), true, None); let save_result = user.save(&mut conn).await; - nt.send_logout(&user, &headers.device.uuid).await; + nt.send_logout(&user, Some(headers.device.uuid.to_string())).await; save_result } @@ -646,7 +646,7 @@ async fn post_rotatekey(data: Json, headers: Headers, mut conn: DbConn, // Prevent logging out the client where the user requested this endpoint from. // If you do logout the user it will causes issues at the client side. // Adding the device uuid will prevent this. - nt.send_logout(&user, &headers.device.uuid).await; + nt.send_logout(&user, Some(headers.device.uuid.to_string())).await; save_result } @@ -662,7 +662,7 @@ async fn post_sstamp(data: Json, headers: Headers, mut conn: user.reset_security_stamp(); let save_result = user.save(&mut conn).await; - nt.send_logout(&user, &DeviceId::empty()).await; + nt.send_logout(&user, None).await; save_result } @@ -770,7 +770,7 @@ async fn post_email(data: Json, headers: Headers, mut conn: DbC let save_result = user.save(&mut conn).await; - nt.send_logout(&user, &DeviceId::empty()).await; + nt.send_logout(&user, None).await; save_result } diff --git a/src/api/core/organizations.rs b/src/api/core/organizations.rs index 1ea82c7acf..6b36f4efc2 100644 --- a/src/api/core/organizations.rs +++ b/src/api/core/organizations.rs @@ -2860,7 +2860,7 @@ async fn put_reset_password( user.set_password(reset_request.new_master_password_hash.as_str(), Some(reset_request.key), true, None); user.save(&mut conn).await?; - nt.send_logout(&user, &DeviceId::empty()).await; + nt.send_logout(&user, None).await; log_event( EventType::OrganizationUserAdminResetPassword as i32, diff --git a/src/api/core/sends.rs b/src/api/core/sends.rs index 6eb923b663..ee3c4d9ce6 100644 --- a/src/api/core/sends.rs +++ b/src/api/core/sends.rs @@ -485,7 +485,7 @@ async fn post_access( UpdateType::SyncSendUpdate, &send, &send.update_users_revision(&mut conn).await, - &DeviceId::empty(), + &String::from("00000000-0000-0000-0000-000000000000").into(), &mut conn, ) .await; @@ -542,7 +542,7 @@ async fn post_access_file( UpdateType::SyncSendUpdate, &send, &send.update_users_revision(&mut conn).await, - &DeviceId::empty(), + &String::from("00000000-0000-0000-0000-000000000000").into(), &mut conn, ) .await; @@ -635,7 +635,7 @@ pub async fn update_send_from_data( send.save(conn).await?; if ut != UpdateType::None { - nt.send_send_update(ut, send, &send.update_users_revision(conn).await, &headers.device.uuid, conn).await + nt.send_send_update(ut, send, &send.update_users_revision(conn).await, &headers.device.uuid, conn).await; } Ok(()) } diff --git a/src/api/notifications.rs b/src/api/notifications.rs index 30ca79d539..476f534131 100644 --- a/src/api/notifications.rs +++ b/src/api/notifications.rs @@ -347,7 +347,7 @@ impl WebSocketUsers { let data = create_update( vec![("UserId".into(), user.uuid.to_string().into()), ("Date".into(), serialize_date(user.updated_at))], ut, - DeviceId::empty(), + None, ); if CONFIG.enable_websocket() { @@ -359,7 +359,7 @@ impl WebSocketUsers { } } - pub async fn send_logout(&self, user: &User, acting_device_uuid: &DeviceId) { + pub async fn send_logout(&self, user: &User, acting_device_uuid: Option) { // Skip any processing if both WebSockets and Push are not active if *NOTIFICATIONS_DISABLED { return; @@ -397,7 +397,7 @@ impl WebSocketUsers { ("RevisionDate".into(), serialize_date(folder.updated_at)), ], ut, - acting_device_uuid.clone(), + Some(acting_device_uuid.to_string()), ); if CONFIG.enable_websocket() { @@ -444,7 +444,7 @@ impl WebSocketUsers { ("RevisionDate".into(), revision_date), ], ut, - acting_device_uuid.clone(), + Some(acting_device_uuid.to_string()), ); if CONFIG.enable_websocket() { @@ -479,7 +479,7 @@ impl WebSocketUsers { ("RevisionDate".into(), serialize_date(send.revision_date)), ], ut, - acting_device_uuid.clone(), + None, ); if CONFIG.enable_websocket() { @@ -506,7 +506,7 @@ impl WebSocketUsers { let data = create_update( vec![("Id".into(), auth_request_uuid.clone().into()), ("UserId".into(), user_uuid.to_string().into())], UpdateType::AuthRequest, - acting_device_uuid.clone(), + Some(acting_device_uuid.to_string()), ); if CONFIG.enable_websocket() { self.send_update(user_uuid, &data).await; @@ -531,7 +531,7 @@ impl WebSocketUsers { let data = create_update( vec![("Id".into(), auth_response_uuid.to_owned().into()), ("UserId".into(), user_uuid.to_string().into())], UpdateType::AuthRequestResponse, - approving_device_uuid.clone(), + Some(approving_device_uuid.to_string()), ); if CONFIG.enable_websocket() { self.send_update(user_uuid, &data).await; @@ -585,7 +585,7 @@ impl AnonymousWebSocketSubscriptions { ] ] */ -fn create_update(payload: Vec<(Value, Value)>, ut: UpdateType, acting_device_uuid: DeviceId) -> Vec { +fn create_update(payload: Vec<(Value, Value)>, ut: UpdateType, acting_device_uuid: Option) -> Vec { use rmpv::Value as V; let value = V::Array(vec![ @@ -594,7 +594,7 @@ fn create_update(payload: Vec<(Value, Value)>, ut: UpdateType, acting_device_uui V::Nil, "ReceiveMessage".into(), V::Array(vec![V::Map(vec![ - ("ContextId".into(), acting_device_uuid.to_string().into()), + ("ContextId".into(), acting_device_uuid.map(|v| v.into()).unwrap_or_else(|| V::Nil)), ("Type".into(), (ut as i32).into()), ("Payload".into(), payload.into()), ])]), diff --git a/src/api/push.rs b/src/api/push.rs index 25fe60fe8c..a1a3a4ba69 100644 --- a/src/api/push.rs +++ b/src/api/push.rs @@ -178,8 +178,8 @@ pub async fn push_cipher_update( } } -pub fn push_logout(user: &User, acting_device_uuid: DeviceId) { - let acting_device_uuid: Value = acting_device_uuid.to_string().into(); +pub fn push_logout(user: &User, acting_device_uuid: Option) { + let acting_device_uuid: Value = acting_device_uuid.map(|v| v.into()).unwrap_or_else(|| Value::Null); tokio::task::spawn(send_to_push_relay(json!({ "userId": user.uuid, diff --git a/src/db/models/device.rs b/src/db/models/device.rs index 69c96bec0d..6458d0a2d4 100644 --- a/src/db/models/device.rs +++ b/src/db/models/device.rs @@ -338,12 +338,6 @@ impl DeviceType { #[derive(Clone, Debug, DieselNewType, Display, From, FromForm, Hash, PartialEq, Eq, Serialize, Deserialize)] pub struct DeviceId(String); -impl DeviceId { - pub fn empty() -> Self { - Self(String::from("00000000-0000-0000-0000-000000000000")) - } -} - impl<'r> FromParam<'r> for DeviceId { type Error = (); From eff2ea0d3f34928f9c7bf86ced64c4b4c620f51d Mon Sep 17 00:00:00 2001 From: Stefan Melmuk Date: Mon, 23 Dec 2024 08:22:15 +0100 Subject: [PATCH 17/25] add send_id newtype --- src/api/core/accounts.rs | 4 +-- src/api/core/sends.rs | 54 ++++++++++++++++++++++------------------ src/api/notifications.rs | 2 +- src/auth.rs | 6 ++--- src/crypto.rs | 4 +-- src/db/models/mod.rs | 2 +- src/db/models/send.rs | 43 ++++++++++++++++++++++++++++---- 7 files changed, 77 insertions(+), 38 deletions(-) diff --git a/src/api/core/accounts.rs b/src/api/core/accounts.rs index ae199abe37..2c97c6dcfd 100644 --- a/src/api/core/accounts.rs +++ b/src/api/core/accounts.rs @@ -525,8 +525,8 @@ fn validate_keydata( } // Check that we're correctly rotating all the user's sends - let existing_send_ids = existing_sends.iter().map(|s| s.uuid.as_str()).collect::>(); - let provided_send_ids = data.sends.iter().filter_map(|s| s.id.as_deref()).collect::>(); + let existing_send_ids = existing_sends.iter().map(|s| &s.uuid).collect::>(); + let provided_send_ids = data.sends.iter().filter_map(|s| s.id.as_ref()).collect::>(); if !provided_send_ids.is_superset(&existing_send_ids) { err!("All existing sends must be included in the rotation") } diff --git a/src/api/core/sends.rs b/src/api/core/sends.rs index ee3c4d9ce6..9a7700f0af 100644 --- a/src/api/core/sends.rs +++ b/src/api/core/sends.rs @@ -67,7 +67,7 @@ pub struct SendData { file_length: Option, // Used for key rotations - pub id: Option, + pub id: Option, } /// Enforces the `Disable Send` policy. A non-owner/admin user belonging to @@ -158,8 +158,8 @@ async fn get_sends(headers: Headers, mut conn: DbConn) -> Json { } #[get("/sends/")] -async fn get_send(uuid: &str, headers: Headers, mut conn: DbConn) -> JsonResult { - match Send::find_by_uuid_and_user(uuid, &headers.user.uuid, &mut conn).await { +async fn get_send(uuid: SendId, headers: Headers, mut conn: DbConn) -> JsonResult { + match Send::find_by_uuid_and_user(&uuid, &headers.user.uuid, &mut conn).await { Some(send) => Ok(Json(send.to_json())), None => err!("Send not found", "Invalid uuid or does not belong to user"), } @@ -249,7 +249,7 @@ async fn post_send_file(data: Form>, headers: Headers, mut conn: err!("Send content is not a file"); } - let file_id = crate::crypto::generate_send_id(); + let file_id = crate::crypto::generate_send_file_id(); let folder_path = tokio::fs::canonicalize(&CONFIG.sends_folder()).await?.join(&send.uuid); let file_path = folder_path.join(&file_id); tokio::fs::create_dir_all(&folder_path).await?; @@ -324,7 +324,7 @@ async fn post_send_file_v2(data: Json, headers: Headers, mut conn: DbC let mut send = create_send(data, headers.user.uuid)?; - let file_id = crate::crypto::generate_send_id(); + let file_id = crate::crypto::generate_send_file_id(); let mut data_value: Value = serde_json::from_str(&send.data)?; if let Some(o) = data_value.as_object_mut() { @@ -352,9 +352,9 @@ pub struct SendFileData { } // https://github.com/bitwarden/server/blob/66f95d1c443490b653e5a15d32977e2f5a3f9e32/src/Api/Tools/Controllers/SendsController.cs#L250 -#[post("/sends//file/", format = "multipart/form-data", data = "")] +#[post("/sends//file/", format = "multipart/form-data", data = "")] async fn post_send_file_v2_data( - send_uuid: &str, + uuid: SendId, file_id: &str, data: Form>, headers: Headers, @@ -365,7 +365,7 @@ async fn post_send_file_v2_data( let mut data = data.into_inner(); - let Some(send) = Send::find_by_uuid_and_user(send_uuid, &headers.user.uuid, &mut conn).await else { + let Some(send) = Send::find_by_uuid_and_user(&uuid, &headers.user.uuid, &mut conn).await else { err!("Send not found. Unable to save the file.", "Invalid uuid or does not belong to user.") }; @@ -402,7 +402,7 @@ async fn post_send_file_v2_data( err!("Send file size does not match.", format!("Expected a file size of {} got {size}", send_data.size)); } - let folder_path = tokio::fs::canonicalize(&CONFIG.sends_folder()).await?.join(send_uuid); + let folder_path = tokio::fs::canonicalize(&CONFIG.sends_folder()).await?.join(uuid); let file_path = folder_path.join(file_id); // Check if the file already exists, if that is the case do not overwrite it @@ -493,16 +493,16 @@ async fn post_access( Ok(Json(send.to_json_access(&mut conn).await)) } -#[post("/sends//access/file/", data = "")] +#[post("/sends//access/file/", data = "")] async fn post_access_file( - send_id: &str, + uuid: SendId, file_id: &str, data: Json, host: Host, mut conn: DbConn, nt: Notify<'_>, ) -> JsonResult { - let Some(mut send) = Send::find_by_uuid(send_id, &mut conn).await else { + let Some(mut send) = Send::find_by_uuid(&uuid, &mut conn).await else { err_code!(SEND_INACCESSIBLE_MSG, 404) }; @@ -547,33 +547,39 @@ async fn post_access_file( ) .await; - let token_claims = crate::auth::generate_send_claims(send_id, file_id); + let token_claims = crate::auth::generate_send_claims(&uuid, file_id); let token = crate::auth::encode_jwt(&token_claims); Ok(Json(json!({ "object": "send-fileDownload", "id": file_id, - "url": format!("{}/api/sends/{}/{}?t={}", &host.host, send_id, file_id, token) + "url": format!("{}/api/sends/{}/{}?t={}", &host.host, uuid, file_id, token) }))) } -#[get("/sends//?")] -async fn download_send(send_id: SafeString, file_id: SafeString, t: &str) -> Option { +#[get("/sends//?")] +async fn download_send(uuid: SendId, file_id: SafeString, t: &str) -> Option { if let Ok(claims) = crate::auth::decode_send(t) { - if claims.sub == format!("{send_id}/{file_id}") { - return NamedFile::open(Path::new(&CONFIG.sends_folder()).join(send_id).join(file_id)).await.ok(); + if claims.sub == format!("{uuid}/{file_id}") { + return NamedFile::open(Path::new(&CONFIG.sends_folder()).join(uuid).join(file_id)).await.ok(); } } None } #[put("/sends/", data = "")] -async fn put_send(uuid: &str, data: Json, headers: Headers, mut conn: DbConn, nt: Notify<'_>) -> JsonResult { +async fn put_send( + uuid: SendId, + data: Json, + headers: Headers, + mut conn: DbConn, + nt: Notify<'_>, +) -> JsonResult { enforce_disable_send_policy(&headers, &mut conn).await?; let data: SendData = data.into_inner(); enforce_disable_hide_email_policy(&data, &headers, &mut conn).await?; - let Some(mut send) = Send::find_by_uuid_and_user(uuid, &headers.user.uuid, &mut conn).await else { + let Some(mut send) = Send::find_by_uuid_and_user(&uuid, &headers.user.uuid, &mut conn).await else { err!("Send not found", "Send uuid is invalid or does not belong to user") }; @@ -641,8 +647,8 @@ pub async fn update_send_from_data( } #[delete("/sends/")] -async fn delete_send(uuid: &str, headers: Headers, mut conn: DbConn, nt: Notify<'_>) -> EmptyResult { - let Some(send) = Send::find_by_uuid_and_user(uuid, &headers.user.uuid, &mut conn).await else { +async fn delete_send(uuid: SendId, headers: Headers, mut conn: DbConn, nt: Notify<'_>) -> EmptyResult { + let Some(send) = Send::find_by_uuid_and_user(&uuid, &headers.user.uuid, &mut conn).await else { err!("Send not found", "Invalid send uuid, or does not belong to user") }; @@ -660,10 +666,10 @@ async fn delete_send(uuid: &str, headers: Headers, mut conn: DbConn, nt: Notify< } #[put("/sends//remove-password")] -async fn put_remove_password(uuid: &str, headers: Headers, mut conn: DbConn, nt: Notify<'_>) -> JsonResult { +async fn put_remove_password(uuid: SendId, headers: Headers, mut conn: DbConn, nt: Notify<'_>) -> JsonResult { enforce_disable_send_policy(&headers, &mut conn).await?; - let Some(mut send) = Send::find_by_uuid_and_user(uuid, &headers.user.uuid, &mut conn).await else { + let Some(mut send) = Send::find_by_uuid_and_user(&uuid, &headers.user.uuid, &mut conn).await else { err!("Send not found", "Invalid send uuid, or does not belong to user") }; diff --git a/src/api/notifications.rs b/src/api/notifications.rs index 476f534131..f9b45f00fe 100644 --- a/src/api/notifications.rs +++ b/src/api/notifications.rs @@ -474,7 +474,7 @@ impl WebSocketUsers { let data = create_update( vec![ - ("Id".into(), send.uuid.clone().into()), + ("Id".into(), send.uuid.to_string().into()), ("UserId".into(), user_uuid), ("RevisionDate".into(), serialize_date(send.revision_date)), ], diff --git a/src/auth.rs b/src/auth.rs index 2fcd974076..74c87a5424 100644 --- a/src/auth.rs +++ b/src/auth.rs @@ -15,7 +15,7 @@ use std::{ }; use crate::db::models::{ - AttachmentId, CipherId, CollectionId, DeviceId, MembershipId, OrgApiKeyId, OrganizationId, UserId, + AttachmentId, CipherId, CollectionId, DeviceId, MembershipId, OrgApiKeyId, OrganizationId, SendId, UserId, }; use crate::{error::Error, CONFIG}; @@ -358,13 +358,13 @@ pub fn generate_admin_claims() -> BasicJwtClaims { } } -pub fn generate_send_claims(send_id: &str, file_id: &str) -> BasicJwtClaims { +pub fn generate_send_claims(uuid: &SendId, file_id: &str) -> BasicJwtClaims { let time_now = Utc::now(); BasicJwtClaims { nbf: time_now.timestamp(), exp: (time_now + TimeDelta::try_minutes(2).unwrap()).timestamp(), iss: JWT_SEND_ISSUER.to_string(), - sub: format!("{send_id}/{file_id}"), + sub: format!("{uuid}/{file_id}"), } } diff --git a/src/crypto.rs b/src/crypto.rs index c9db1a4b2c..eff1785f3e 100644 --- a/src/crypto.rs +++ b/src/crypto.rs @@ -84,8 +84,8 @@ pub fn generate_id() -> String { encode_random_bytes::(HEXLOWER) } -pub fn generate_send_id() -> String { - // Send IDs are globally scoped, so make them longer to avoid collisions. +pub fn generate_send_file_id() -> String { + // Send File IDs are globally scoped, so make them longer to avoid collisions. generate_id::<32>() // 256 bits } diff --git a/src/db/models/mod.rs b/src/db/models/mod.rs index 2817fb5612..b03b24d67a 100644 --- a/src/db/models/mod.rs +++ b/src/db/models/mod.rs @@ -31,7 +31,7 @@ pub use self::organization::{ Membership, MembershipId, MembershipStatus, MembershipType, OrgApiKeyId, Organization, OrganizationApiKey, OrganizationId, }; -pub use self::send::{Send, SendType}; +pub use self::send::{id::SendId, Send, SendType}; pub use self::two_factor::{TwoFactor, TwoFactorType}; pub use self::two_factor_duo_context::TwoFactorDuoContext; pub use self::two_factor_incomplete::TwoFactorIncomplete; diff --git a/src/db/models/send.rs b/src/db/models/send.rs index 8cb27367ae..3ea8b66018 100644 --- a/src/db/models/send.rs +++ b/src/db/models/send.rs @@ -4,6 +4,7 @@ use serde_json::Value; use crate::util::LowerCase; use super::{OrganizationId, User, UserId}; +use id::SendId; db_object! { #[derive(Identifiable, Queryable, Insertable, AsChangeset)] @@ -11,7 +12,7 @@ db_object! { #[diesel(treat_none_as_null = true)] #[diesel(primary_key(uuid))] pub struct Send { - pub uuid: String, + pub uuid: SendId, pub user_uuid: Option, pub organization_uuid: Option, @@ -50,7 +51,7 @@ impl Send { let now = Utc::now().naive_utc(); Self { - uuid: crate::util::get_uuid(), + uuid: SendId::from(crate::util::get_uuid()), user_uuid: None, organization_uuid: None, @@ -272,14 +273,14 @@ impl Send { }; let uuid = match Uuid::from_slice(&uuid_vec) { - Ok(u) => u.to_string(), + Ok(u) => SendId::from(u.to_string()), Err(_) => return None, }; Self::find_by_uuid(&uuid, conn).await } - pub async fn find_by_uuid(uuid: &str, conn: &mut DbConn) -> Option { + pub async fn find_by_uuid(uuid: &SendId, conn: &mut DbConn) -> Option { db_run! {conn: { sends::table .filter(sends::uuid.eq(uuid)) @@ -289,7 +290,7 @@ impl Send { }} } - pub async fn find_by_uuid_and_user(uuid: &str, user_uuid: &UserId, conn: &mut DbConn) -> Option { + pub async fn find_by_uuid_and_user(uuid: &SendId, user_uuid: &UserId, conn: &mut DbConn) -> Option { db_run! {conn: { sends::table .filter(sends::uuid.eq(uuid)) @@ -348,3 +349,35 @@ impl Send { }} } } + +// separate namespace to avoid name collision with std::marker::Send +pub mod id { + use derive_more::{AsRef, Deref, Display, From}; + use rocket::request::FromParam; + use std::marker::Send; + use std::path::Path; + #[derive( + Clone, Debug, AsRef, Deref, DieselNewType, Display, From, FromForm, Hash, PartialEq, Eq, Serialize, Deserialize, + )] + pub struct SendId(String); + + impl AsRef for SendId { + #[inline] + fn as_ref(&self) -> &Path { + Path::new(&self.0) + } + } + + impl<'r> FromParam<'r> for SendId { + 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(()) + } + } + } +} From b92415216c7fb9b50fdf36a24623c2a6e1e7449e Mon Sep 17 00:00:00 2001 From: Stefan Melmuk Date: Tue, 24 Dec 2024 00:28:01 +0100 Subject: [PATCH 18/25] add send_file_id newtype --- src/api/core/sends.rs | 12 ++++++------ src/auth.rs | 5 +++-- src/db/models/mod.rs | 5 ++++- src/db/models/send.rs | 24 ++++++++++++++++++++++++ 4 files changed, 37 insertions(+), 9 deletions(-) diff --git a/src/api/core/sends.rs b/src/api/core/sends.rs index 9a7700f0af..f6b8b508de 100644 --- a/src/api/core/sends.rs +++ b/src/api/core/sends.rs @@ -12,7 +12,7 @@ use crate::{ api::{ApiResult, EmptyResult, JsonResult, Notify, UpdateType}, auth::{ClientIp, Headers, Host}, db::{models::*, DbConn, DbPool}, - util::{NumberOrString, SafeString}, + util::NumberOrString, CONFIG, }; @@ -346,7 +346,7 @@ async fn post_send_file_v2(data: Json, headers: Headers, mut conn: DbC #[derive(Deserialize)] #[allow(non_snake_case)] pub struct SendFileData { - id: String, + id: SendFileId, size: u64, fileName: String, } @@ -355,7 +355,7 @@ pub struct SendFileData { #[post("/sends//file/", format = "multipart/form-data", data = "")] async fn post_send_file_v2_data( uuid: SendId, - file_id: &str, + file_id: SendFileId, data: Form>, headers: Headers, mut conn: DbConn, @@ -496,7 +496,7 @@ async fn post_access( #[post("/sends//access/file/", data = "")] async fn post_access_file( uuid: SendId, - file_id: &str, + file_id: SendFileId, data: Json, host: Host, mut conn: DbConn, @@ -547,7 +547,7 @@ async fn post_access_file( ) .await; - let token_claims = crate::auth::generate_send_claims(&uuid, file_id); + let token_claims = crate::auth::generate_send_claims(&uuid, &file_id); let token = crate::auth::encode_jwt(&token_claims); Ok(Json(json!({ "object": "send-fileDownload", @@ -557,7 +557,7 @@ async fn post_access_file( } #[get("/sends//?")] -async fn download_send(uuid: SendId, file_id: SafeString, t: &str) -> Option { +async fn download_send(uuid: SendId, file_id: SendFileId, t: &str) -> Option { if let Ok(claims) = crate::auth::decode_send(t) { if claims.sub == format!("{uuid}/{file_id}") { return NamedFile::open(Path::new(&CONFIG.sends_folder()).join(uuid).join(file_id)).await.ok(); diff --git a/src/auth.rs b/src/auth.rs index 74c87a5424..090aebe987 100644 --- a/src/auth.rs +++ b/src/auth.rs @@ -15,7 +15,8 @@ use std::{ }; use crate::db::models::{ - AttachmentId, CipherId, CollectionId, DeviceId, MembershipId, OrgApiKeyId, OrganizationId, SendId, UserId, + AttachmentId, CipherId, CollectionId, DeviceId, MembershipId, OrgApiKeyId, OrganizationId, SendFileId, SendId, + UserId, }; use crate::{error::Error, CONFIG}; @@ -358,7 +359,7 @@ pub fn generate_admin_claims() -> BasicJwtClaims { } } -pub fn generate_send_claims(uuid: &SendId, file_id: &str) -> BasicJwtClaims { +pub fn generate_send_claims(uuid: &SendId, file_id: &SendFileId) -> BasicJwtClaims { let time_now = Utc::now(); BasicJwtClaims { nbf: time_now.timestamp(), diff --git a/src/db/models/mod.rs b/src/db/models/mod.rs index b03b24d67a..b6691e7f08 100644 --- a/src/db/models/mod.rs +++ b/src/db/models/mod.rs @@ -31,7 +31,10 @@ pub use self::organization::{ Membership, MembershipId, MembershipStatus, MembershipType, OrgApiKeyId, Organization, OrganizationApiKey, OrganizationId, }; -pub use self::send::{id::SendId, Send, SendType}; +pub use self::send::{ + id::{SendFileId, SendId}, + Send, SendType, +}; pub use self::two_factor::{TwoFactor, TwoFactorType}; pub use self::two_factor_duo_context::TwoFactorDuoContext; pub use self::two_factor_incomplete::TwoFactorIncomplete; diff --git a/src/db/models/send.rs b/src/db/models/send.rs index 3ea8b66018..f1ba0c0cca 100644 --- a/src/db/models/send.rs +++ b/src/db/models/send.rs @@ -356,6 +356,7 @@ pub mod id { use rocket::request::FromParam; use std::marker::Send; use std::path::Path; + #[derive( Clone, Debug, AsRef, Deref, DieselNewType, Display, From, FromForm, Hash, PartialEq, Eq, Serialize, Deserialize, )] @@ -380,4 +381,27 @@ pub mod id { } } } + + #[derive(Clone, Debug, AsRef, Deref, Display, From, FromForm, Hash, PartialEq, Eq, Serialize, Deserialize)] + pub struct SendFileId(String); + + impl AsRef for SendFileId { + #[inline] + fn as_ref(&self) -> &Path { + Path::new(&self.0) + } + } + + impl<'r> FromParam<'r> for SendFileId { + 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(()) + } + } + } } From 13973926996093880d4b1f8f961f1ed82f083898 Mon Sep 17 00:00:00 2001 From: Stefan Melmuk Date: Tue, 24 Dec 2024 00:38:24 +0100 Subject: [PATCH 19/25] remove obsolete SafeString --- src/util.rs | 39 +-------------------------------------- 1 file changed, 1 insertion(+), 38 deletions(-) diff --git a/src/util.rs b/src/util.rs index 8c4efca807..4a8af5e936 100644 --- a/src/util.rs +++ b/src/util.rs @@ -1,13 +1,12 @@ // // Web Headers and caching // -use std::{collections::HashMap, io::Cursor, ops::Deref, path::Path}; +use std::{collections::HashMap, io::Cursor, path::Path}; use num_traits::ToPrimitive; use rocket::{ fairing::{Fairing, Info, Kind}, http::{ContentType, Header, HeaderMap, Method, Status}, - request::FromParam, response::{self, Responder}, Data, Orbit, Request, Response, Rocket, }; @@ -223,42 +222,6 @@ impl<'r, R: 'r + Responder<'r, 'static> + Send> Responder<'r, 'static> for Cache } } -pub struct SafeString(String); - -impl fmt::Display for SafeString { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - self.0.fmt(f) - } -} - -impl Deref for SafeString { - type Target = String; - - fn deref(&self) -> &Self::Target { - &self.0 - } -} - -impl AsRef for SafeString { - #[inline] - fn as_ref(&self) -> &Path { - Path::new(&self.0) - } -} - -impl<'r> FromParam<'r> for SafeString { - 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(SafeString(param.to_string())) - } else { - Err(()) - } - } -} - // Log all the routes from the main paths list, and the attachments endpoint // Effectively ignores, any static file route, and the alive endpoint const LOGGED_ROUTES: [&str; 7] = ["/api", "/admin", "/identity", "/icons", "/attachments", "/events", "/notifications"]; From 6355985539533ee2016043002f92dc9ea27e9887 Mon Sep 17 00:00:00 2001 From: Stefan Melmuk Date: Tue, 24 Dec 2024 00:55:32 +0100 Subject: [PATCH 20/25] use id names more consistently --- src/api/admin.rs | 54 +-- src/api/core/accounts.rs | 18 +- src/api/core/ciphers.rs | 367 ++++++++++--------- src/api/core/emergency_access.rs | 4 +- src/api/core/events.rs | 46 +-- src/api/core/folders.rs | 30 +- src/api/core/organizations.rs | 7 +- src/api/core/public.rs | 6 +- src/api/core/sends.rs | 68 ++-- src/api/core/two_factor/authenticator.rs | 16 +- src/api/core/two_factor/duo.rs | 4 +- src/api/core/two_factor/email.rs | 13 +- src/api/core/two_factor/mod.rs | 16 +- src/api/core/two_factor/protected_actions.rs | 4 +- src/api/core/two_factor/webauthn.rs | 18 +- src/api/core/two_factor/yubikey.rs | 4 +- src/api/identity.rs | 52 +-- src/api/notifications.rs | 52 +-- src/api/push.rs | 71 ++-- src/api/web.rs | 8 +- src/auth.rs | 8 +- 21 files changed, 442 insertions(+), 424 deletions(-) diff --git a/src/api/admin.rs b/src/api/admin.rs index 673af54548..c9a11180eb 100644 --- a/src/api/admin.rs +++ b/src/api/admin.rs @@ -280,8 +280,8 @@ struct InviteData { email: String, } -async fn get_user_or_404(uuid: &UserId, conn: &mut DbConn) -> ApiResult { - if let Some(user) = User::find_by_uuid(uuid, conn).await { +async fn get_user_or_404(user_id: &UserId, conn: &mut DbConn) -> ApiResult { + if let Some(user) = User::find_by_uuid(user_id, conn).await { Ok(user) } else { err_code!("User doesn't exist", Status::NotFound.code); @@ -381,21 +381,21 @@ async fn get_user_by_mail_json(mail: &str, _token: AdminToken, mut conn: DbConn) } } -#[get("/users/")] -async fn get_user_json(uuid: UserId, _token: AdminToken, mut conn: DbConn) -> JsonResult { - let u = get_user_or_404(&uuid, &mut conn).await?; +#[get("/users/")] +async fn get_user_json(user_id: UserId, _token: AdminToken, mut conn: DbConn) -> JsonResult { + let u = get_user_or_404(&user_id, &mut conn).await?; let mut usr = u.to_json(&mut conn).await; usr["userEnabled"] = json!(u.enabled); usr["createdAt"] = json!(format_naive_datetime_local(&u.created_at, DT_FMT)); Ok(Json(usr)) } -#[post("/users//delete")] -async fn delete_user(uuid: UserId, token: AdminToken, mut conn: DbConn) -> EmptyResult { - let user = get_user_or_404(&uuid, &mut conn).await?; +#[post("/users//delete")] +async fn delete_user(user_id: UserId, token: AdminToken, mut conn: DbConn) -> EmptyResult { + let user = get_user_or_404(&user_id, &mut conn).await?; // Get the membership records before deleting the actual user - let memberships = Membership::find_any_state_by_user(&uuid, &mut conn).await; + let memberships = Membership::find_any_state_by_user(&user_id, &mut conn).await; let res = user.delete(&mut conn).await; for membership in memberships { @@ -414,9 +414,9 @@ async fn delete_user(uuid: UserId, token: AdminToken, mut conn: DbConn) -> Empty res } -#[post("/users//deauth")] -async fn deauth_user(uuid: UserId, _token: AdminToken, mut conn: DbConn, nt: Notify<'_>) -> EmptyResult { - let mut user = get_user_or_404(&uuid, &mut conn).await?; +#[post("/users//deauth")] +async fn deauth_user(user_id: UserId, _token: AdminToken, mut conn: DbConn, nt: Notify<'_>) -> EmptyResult { + let mut user = get_user_or_404(&user_id, &mut conn).await?; nt.send_logout(&user, None).await; @@ -435,9 +435,9 @@ async fn deauth_user(uuid: UserId, _token: AdminToken, mut conn: DbConn, nt: Not user.save(&mut conn).await } -#[post("/users//disable")] -async fn disable_user(uuid: UserId, _token: AdminToken, mut conn: DbConn, nt: Notify<'_>) -> EmptyResult { - let mut user = get_user_or_404(&uuid, &mut conn).await?; +#[post("/users//disable")] +async fn disable_user(user_id: UserId, _token: AdminToken, mut conn: DbConn, nt: Notify<'_>) -> EmptyResult { + let mut user = get_user_or_404(&user_id, &mut conn).await?; Device::delete_all_by_user(&user.uuid, &mut conn).await?; user.reset_security_stamp(); user.enabled = false; @@ -449,26 +449,26 @@ async fn disable_user(uuid: UserId, _token: AdminToken, mut conn: DbConn, nt: No save_result } -#[post("/users//enable")] -async fn enable_user(uuid: UserId, _token: AdminToken, mut conn: DbConn) -> EmptyResult { - let mut user = get_user_or_404(&uuid, &mut conn).await?; +#[post("/users//enable")] +async fn enable_user(user_id: UserId, _token: AdminToken, mut conn: DbConn) -> EmptyResult { + let mut user = get_user_or_404(&user_id, &mut conn).await?; user.enabled = true; user.save(&mut conn).await } -#[post("/users//remove-2fa")] -async fn remove_2fa(uuid: UserId, token: AdminToken, mut conn: DbConn) -> EmptyResult { - let mut user = get_user_or_404(&uuid, &mut conn).await?; +#[post("/users//remove-2fa")] +async fn remove_2fa(user_id: UserId, token: AdminToken, mut conn: DbConn) -> EmptyResult { + let mut user = get_user_or_404(&user_id, &mut conn).await?; TwoFactor::delete_all_by_user(&user.uuid, &mut conn).await?; two_factor::enforce_2fa_policy(&user, ACTING_ADMIN_USER, 14, &token.ip.ip, &mut conn).await?; user.totp_recover = None; user.save(&mut conn).await } -#[post("/users//invite/resend")] -async fn resend_user_invite(uuid: UserId, _token: AdminToken, mut conn: DbConn) -> EmptyResult { - if let Some(user) = User::find_by_uuid(&uuid, &mut conn).await { +#[post("/users//invite/resend")] +async fn resend_user_invite(user_id: UserId, _token: AdminToken, mut conn: DbConn) -> EmptyResult { + if let Some(user) = User::find_by_uuid(&user_id, &mut conn).await { //TODO: replace this with user.status check when it will be available (PR#3397) if !user.password_hash.is_empty() { err_code!("User already accepted invitation", Status::BadRequest.code); @@ -570,9 +570,9 @@ async fn organizations_overview(_token: AdminToken, mut conn: DbConn) -> ApiResu Ok(Html(text)) } -#[post("/organizations//delete")] -async fn delete_organization(org_uuid: OrganizationId, _token: AdminToken, mut conn: DbConn) -> EmptyResult { - let org = Organization::find_by_uuid(&org_uuid, &mut conn).await.map_res("Organization doesn't exist")?; +#[post("/organizations//delete")] +async fn delete_organization(org_id: OrganizationId, _token: AdminToken, mut conn: DbConn) -> EmptyResult { + let org = Organization::find_by_uuid(&org_id, &mut conn).await.map_res("Organization doesn't exist")?; org.delete(&mut conn).await } diff --git a/src/api/core/accounts.rs b/src/api/core/accounts.rs index 2c97c6dcfd..436bbf2d4c 100644 --- a/src/api/core/accounts.rs +++ b/src/api/core/accounts.rs @@ -305,9 +305,9 @@ async fn put_avatar(data: Json, headers: Headers, mut conn: DbConn) Ok(Json(user.to_json(&mut conn).await)) } -#[get("/users//public-key")] -async fn get_public_keys(uuid: UserId, _headers: Headers, mut conn: DbConn) -> JsonResult { - let user = match User::find_by_uuid(&uuid, &mut conn).await { +#[get("/users//public-key")] +async fn get_public_keys(user_id: UserId, _headers: Headers, mut conn: DbConn) -> JsonResult { + let user = match User::find_by_uuid(&user_id, &mut conn).await { Some(user) if user.public_key.is_some() => user, Some(_) => err_code!("User has no public_key", Status::NotFound.code), None => err_code!("User doesn't exist", Status::NotFound.code), @@ -549,17 +549,17 @@ async fn post_rotatekey(data: Json, headers: Headers, mut conn: DbConn, // TODO: See if we can optimize the whole cipher adding/importing and prevent duplicate code and checks. Cipher::validate_cipher_data(&data.ciphers)?; - let user_uuid = &headers.user.uuid; + let user_id = &headers.user.uuid; // TODO: Ideally we'd do everything after this point in a single transaction. - let mut existing_ciphers = Cipher::find_owned_by_user(user_uuid, &mut conn).await; - let mut existing_folders = Folder::find_by_user(user_uuid, &mut conn).await; - let mut existing_emergency_access = EmergencyAccess::find_all_by_grantor_uuid(user_uuid, &mut conn).await; - let mut existing_memberships = Membership::find_by_user(user_uuid, &mut conn).await; + let mut existing_ciphers = Cipher::find_owned_by_user(user_id, &mut conn).await; + let mut existing_folders = Folder::find_by_user(user_id, &mut conn).await; + let mut existing_emergency_access = EmergencyAccess::find_all_by_grantor_uuid(user_id, &mut conn).await; + let mut existing_memberships = Membership::find_by_user(user_id, &mut conn).await; // We only rotate the reset password key if it is set. existing_memberships.retain(|m| m.reset_password_key.is_some()); - let mut existing_sends = Send::find_by_user(user_uuid, &mut conn).await; + let mut existing_sends = Send::find_by_user(user_id, &mut conn).await; validate_keydata( &data, diff --git a/src/api/core/ciphers.rs b/src/api/core/ciphers.rs index 6e73e2dc5e..6c75d246ba 100644 --- a/src/api/core/ciphers.rs +++ b/src/api/core/ciphers.rs @@ -191,9 +191,9 @@ async fn get_ciphers(headers: Headers, mut conn: DbConn) -> Json { })) } -#[get("/ciphers/")] -async fn get_cipher(uuid: CipherId, headers: Headers, mut conn: DbConn) -> JsonResult { - let Some(cipher) = Cipher::find_by_uuid(&uuid, &mut conn).await else { +#[get("/ciphers/")] +async fn get_cipher(cipher_id: CipherId, headers: Headers, mut conn: DbConn) -> JsonResult { + let Some(cipher) = Cipher::find_by_uuid(&cipher_id, &mut conn).await else { err!("Cipher doesn't exist") }; @@ -204,15 +204,15 @@ async fn get_cipher(uuid: CipherId, headers: Headers, mut conn: DbConn) -> JsonR Ok(Json(cipher.to_json(&headers.host, &headers.user.uuid, None, CipherSyncType::User, &mut conn).await)) } -#[get("/ciphers//admin")] -async fn get_cipher_admin(uuid: CipherId, headers: Headers, conn: DbConn) -> JsonResult { +#[get("/ciphers//admin")] +async fn get_cipher_admin(cipher_id: CipherId, headers: Headers, conn: DbConn) -> JsonResult { // TODO: Implement this correctly - get_cipher(uuid, headers, conn).await + get_cipher(cipher_id, headers, conn).await } -#[get("/ciphers//details")] -async fn get_cipher_details(uuid: CipherId, headers: Headers, conn: DbConn) -> JsonResult { - get_cipher(uuid, headers, conn).await +#[get("/ciphers//details")] +async fn get_cipher_details(cipher_id: CipherId, headers: Headers, conn: DbConn) -> JsonResult { + get_cipher(cipher_id, headers, conn).await } #[derive(Debug, Deserialize)] @@ -355,9 +355,9 @@ async fn enforce_personal_ownership_policy( conn: &mut DbConn, ) -> EmptyResult { if data.is_none() || data.unwrap().organization_id.is_none() { - let user_uuid = &headers.user.uuid; + let user_id = &headers.user.uuid; let policy_type = OrgPolicyType::PersonalOwnership; - if OrgPolicy::is_applicable_to_user(user_uuid, policy_type, None, conn).await { + if OrgPolicy::is_applicable_to_user(user_id, policy_type, None, conn).await { err!("Due to an Enterprise Policy, you are restricted from saving items to your personal vault.") } } @@ -428,8 +428,8 @@ pub async fn update_cipher_from_data( cipher.user_uuid = Some(headers.user.uuid.clone()); } - if let Some(ref folder_uuid) = data.folder_id { - if Folder::find_by_uuid_and_user(folder_uuid, &headers.user.uuid, conn).await.is_none() { + if let Some(ref folder_id) = data.folder_id { + if Folder::find_by_uuid_and_user(folder_id, &headers.user.uuid, conn).await.is_none() { err!("Invalid folder", "Folder does not exist or belongs to another user"); } } @@ -513,7 +513,7 @@ pub async fn update_cipher_from_data( if ut != UpdateType::None { // Only log events for organizational ciphers - if let Some(org_uuid) = &cipher.organization_uuid { + if let Some(org_id) = &cipher.organization_uuid { let event_type = match (&ut, transfer_cipher) { (UpdateType::SyncCipherCreate, true) => EventType::CipherCreated, (UpdateType::SyncCipherUpdate, true) => EventType::CipherShared, @@ -523,7 +523,7 @@ pub async fn update_cipher_from_data( log_event( event_type as i32, &cipher.uuid, - org_uuid, + org_id, &headers.user.uuid, headers.device.atype, &headers.ip.ip, @@ -583,7 +583,7 @@ async fn post_ciphers_import( Folder::find_by_user(&headers.user.uuid, &mut conn).await.into_iter().map(|f| Some(f.uuid)).collect(); let mut folders: Vec = Vec::with_capacity(data.folders.len()); for folder in data.folders.into_iter() { - let folder_uuid = if existing_folders.contains(&folder.id) { + let folder_id = if existing_folders.contains(&folder.id) { folder.id.unwrap() } else { let mut new_folder = Folder::new(headers.user.uuid.clone(), folder.name); @@ -591,7 +591,7 @@ async fn post_ciphers_import( new_folder.uuid }; - folders.push(folder_uuid); + folders.push(folder_id); } // Read the relations between folders and ciphers @@ -603,8 +603,8 @@ async fn post_ciphers_import( // Read and create the ciphers for (index, mut cipher_data) in data.ciphers.into_iter().enumerate() { - let folder_uuid = relations_map.get(&index).map(|i| folders[*i].clone()); - cipher_data.folder_id = folder_uuid; + let folder_id = relations_map.get(&index).map(|i| folders[*i].clone()); + cipher_data.folder_id = folder_id; let mut cipher = Cipher::new(cipher_data.r#type, cipher_data.name.clone()); update_cipher_from_data(&mut cipher, cipher_data, &headers, None, &mut conn, &nt, UpdateType::None).await?; @@ -618,42 +618,42 @@ async fn post_ciphers_import( } /// Called when an org admin modifies an existing org cipher. -#[put("/ciphers//admin", data = "")] +#[put("/ciphers//admin", data = "")] async fn put_cipher_admin( - uuid: CipherId, + cipher_id: CipherId, data: Json, headers: Headers, conn: DbConn, nt: Notify<'_>, ) -> JsonResult { - put_cipher(uuid, data, headers, conn, nt).await + put_cipher(cipher_id, data, headers, conn, nt).await } -#[post("/ciphers//admin", data = "")] +#[post("/ciphers//admin", data = "")] async fn post_cipher_admin( - uuid: CipherId, + cipher_id: CipherId, data: Json, headers: Headers, conn: DbConn, nt: Notify<'_>, ) -> JsonResult { - post_cipher(uuid, data, headers, conn, nt).await + post_cipher(cipher_id, data, headers, conn, nt).await } -#[post("/ciphers/", data = "")] +#[post("/ciphers/", data = "")] async fn post_cipher( - uuid: CipherId, + cipher_id: CipherId, data: Json, headers: Headers, conn: DbConn, nt: Notify<'_>, ) -> JsonResult { - put_cipher(uuid, data, headers, conn, nt).await + put_cipher(cipher_id, data, headers, conn, nt).await } -#[put("/ciphers/", data = "")] +#[put("/ciphers/", data = "")] async fn put_cipher( - uuid: CipherId, + cipher_id: CipherId, data: Json, headers: Headers, mut conn: DbConn, @@ -661,7 +661,7 @@ async fn put_cipher( ) -> JsonResult { let data: CipherData = data.into_inner(); - let Some(mut cipher) = Cipher::find_by_uuid(&uuid, &mut conn).await else { + let Some(mut cipher) = Cipher::find_by_uuid(&cipher_id, &mut conn).await else { err!("Cipher doesn't exist") }; @@ -679,32 +679,32 @@ async fn put_cipher( Ok(Json(cipher.to_json(&headers.host, &headers.user.uuid, None, CipherSyncType::User, &mut conn).await)) } -#[post("/ciphers//partial", data = "")] +#[post("/ciphers//partial", data = "")] async fn post_cipher_partial( - uuid: CipherId, + cipher_id: CipherId, data: Json, headers: Headers, conn: DbConn, ) -> JsonResult { - put_cipher_partial(uuid, data, headers, conn).await + put_cipher_partial(cipher_id, data, headers, conn).await } // Only update the folder and favorite for the user, since this cipher is read-only -#[put("/ciphers//partial", data = "")] +#[put("/ciphers//partial", data = "")] async fn put_cipher_partial( - uuid: CipherId, + cipher_id: CipherId, data: Json, headers: Headers, mut conn: DbConn, ) -> JsonResult { let data: PartialCipherData = data.into_inner(); - let Some(cipher) = Cipher::find_by_uuid(&uuid, &mut conn).await else { + let Some(cipher) = Cipher::find_by_uuid(&cipher_id, &mut conn).await else { err!("Cipher doesn't exist") }; - if let Some(ref folder_uuid) = data.folder_id { - if Folder::find_by_uuid_and_user(folder_uuid, &headers.user.uuid, &mut conn).await.is_none() { + if let Some(ref folder_id) = data.folder_id { + if Folder::find_by_uuid_and_user(folder_id, &headers.user.uuid, &mut conn).await.is_none() { err!("Invalid folder", "Folder does not exist or belongs to another user"); } } @@ -724,26 +724,26 @@ struct CollectionsAdminData { collection_ids: Vec, } -#[put("/ciphers//collections_v2", data = "")] +#[put("/ciphers//collections_v2", data = "")] async fn put_collections2_update( - uuid: CipherId, + cipher_id: CipherId, data: Json, headers: Headers, conn: DbConn, nt: Notify<'_>, ) -> JsonResult { - post_collections2_update(uuid, data, headers, conn, nt).await + post_collections2_update(cipher_id, data, headers, conn, nt).await } -#[post("/ciphers//collections_v2", data = "")] +#[post("/ciphers//collections_v2", data = "")] async fn post_collections2_update( - uuid: CipherId, + cipher_id: CipherId, data: Json, headers: Headers, conn: DbConn, nt: Notify<'_>, ) -> JsonResult { - let cipher_details = post_collections_update(uuid, data, headers, conn, nt).await?; + let cipher_details = post_collections_update(cipher_id, data, headers, conn, nt).await?; Ok(Json(json!({ // AttachmentUploadDataResponseModel "object": "optionalCipherDetails", "unavailable": false, @@ -751,20 +751,20 @@ async fn post_collections2_update( }))) } -#[put("/ciphers//collections", data = "")] +#[put("/ciphers//collections", data = "")] async fn put_collections_update( - uuid: CipherId, + cipher_id: CipherId, data: Json, headers: Headers, conn: DbConn, nt: Notify<'_>, ) -> JsonResult { - post_collections_update(uuid, data, headers, conn, nt).await + post_collections_update(cipher_id, data, headers, conn, nt).await } -#[post("/ciphers//collections", data = "")] +#[post("/ciphers//collections", data = "")] async fn post_collections_update( - uuid: CipherId, + cipher_id: CipherId, data: Json, headers: Headers, mut conn: DbConn, @@ -772,7 +772,7 @@ async fn post_collections_update( ) -> JsonResult { let data: CollectionsAdminData = data.into_inner(); - let Some(cipher) = Cipher::find_by_uuid(&uuid, &mut conn).await else { + let Some(cipher) = Cipher::find_by_uuid(&cipher_id, &mut conn).await else { err!("Cipher doesn't exist") }; @@ -828,20 +828,20 @@ async fn post_collections_update( Ok(Json(cipher.to_json(&headers.host, &headers.user.uuid, None, CipherSyncType::User, &mut conn).await)) } -#[put("/ciphers//collections-admin", data = "")] +#[put("/ciphers//collections-admin", data = "")] async fn put_collections_admin( - uuid: CipherId, + cipher_id: CipherId, data: Json, headers: Headers, conn: DbConn, nt: Notify<'_>, ) -> EmptyResult { - post_collections_admin(uuid, data, headers, conn, nt).await + post_collections_admin(cipher_id, data, headers, conn, nt).await } -#[post("/ciphers//collections-admin", data = "")] +#[post("/ciphers//collections-admin", data = "")] async fn post_collections_admin( - uuid: CipherId, + cipher_id: CipherId, data: Json, headers: Headers, mut conn: DbConn, @@ -849,7 +849,7 @@ async fn post_collections_admin( ) -> EmptyResult { let data: CollectionsAdminData = data.into_inner(); - let Some(cipher) = Cipher::find_by_uuid(&uuid, &mut conn).await else { + let Some(cipher) = Cipher::find_by_uuid(&cipher_id, &mut conn).await else { err!("Cipher doesn't exist") }; @@ -914,9 +914,9 @@ struct ShareCipherData { collection_ids: Vec, } -#[post("/ciphers//share", data = "")] +#[post("/ciphers//share", data = "")] async fn post_cipher_share( - uuid: CipherId, + cipher_id: CipherId, data: Json, headers: Headers, mut conn: DbConn, @@ -924,12 +924,12 @@ async fn post_cipher_share( ) -> JsonResult { let data: ShareCipherData = data.into_inner(); - share_cipher_by_uuid(&uuid, data, &headers, &mut conn, &nt).await + share_cipher_by_uuid(&cipher_id, data, &headers, &mut conn, &nt).await } -#[put("/ciphers//share", data = "")] +#[put("/ciphers//share", data = "")] async fn put_cipher_share( - uuid: CipherId, + cipher_id: CipherId, data: Json, headers: Headers, mut conn: DbConn, @@ -937,7 +937,7 @@ async fn put_cipher_share( ) -> JsonResult { let data: ShareCipherData = data.into_inner(); - share_cipher_by_uuid(&uuid, data, &headers, &mut conn, &nt).await + share_cipher_by_uuid(&cipher_id, data, &headers, &mut conn, &nt).await } #[derive(Deserialize)] @@ -986,13 +986,13 @@ async fn put_cipher_share_selected( } async fn share_cipher_by_uuid( - uuid: &CipherId, + cipher_id: &CipherId, data: ShareCipherData, headers: &Headers, conn: &mut DbConn, nt: &Notify<'_>, ) -> JsonResult { - let mut cipher = match Cipher::find_by_uuid(uuid, conn).await { + let mut cipher = match Cipher::find_by_uuid(cipher_id, conn).await { Some(cipher) => { if cipher.is_write_accessible_to_user(&headers.user.uuid, conn).await { cipher @@ -1005,9 +1005,9 @@ async fn share_cipher_by_uuid( let mut shared_to_collections = vec![]; - if let Some(organization_uuid) = &data.cipher.organization_id { - for uuid in &data.collection_ids { - match Collection::find_by_uuid_and_org(uuid, organization_uuid, conn).await { + if let Some(organization_id) = &data.cipher.organization_id { + for col_id in &data.collection_ids { + match Collection::find_by_uuid_and_org(col_id, organization_id, conn).await { None => err!("Invalid collection ID provided"), Some(collection) => { if collection.is_writable_by_user(&headers.user.uuid, conn).await { @@ -1039,9 +1039,14 @@ async fn share_cipher_by_uuid( /// Upstream added this v2 API to support direct download of attachments from /// their object storage service. For self-hosted instances, it basically just /// redirects to the same location as before the v2 API. -#[get("/ciphers//attachment/")] -async fn get_attachment(uuid: CipherId, attachment_id: AttachmentId, headers: Headers, mut conn: DbConn) -> JsonResult { - let Some(cipher) = Cipher::find_by_uuid(&uuid, &mut conn).await else { +#[get("/ciphers//attachment/")] +async fn get_attachment( + cipher_id: CipherId, + attachment_id: AttachmentId, + headers: Headers, + mut conn: DbConn, +) -> JsonResult { + let Some(cipher) = Cipher::find_by_uuid(&cipher_id, &mut conn).await else { err!("Cipher doesn't exist") }; @@ -1050,7 +1055,7 @@ async fn get_attachment(uuid: CipherId, attachment_id: AttachmentId, headers: He } match Attachment::find_by_id(&attachment_id, &mut conn).await { - Some(attachment) if uuid == attachment.cipher_uuid => Ok(Json(attachment.to_json(&headers.host))), + Some(attachment) if cipher_id == attachment.cipher_uuid => Ok(Json(attachment.to_json(&headers.host))), Some(_) => err!("Attachment doesn't belong to cipher"), None => err!("Attachment doesn't exist"), } @@ -1074,14 +1079,14 @@ enum FileUploadType { /// This redirects the client to the API it should use to upload the attachment. /// For upstream's cloud-hosted service, it's an Azure object storage API. /// For self-hosted instances, it's another API on the local instance. -#[post("/ciphers//attachment/v2", data = "")] +#[post("/ciphers//attachment/v2", data = "")] async fn post_attachment_v2( - uuid: CipherId, + cipher_id: CipherId, data: Json, headers: Headers, mut conn: DbConn, ) -> JsonResult { - let Some(cipher) = Cipher::find_by_uuid(&uuid, &mut conn).await else { + let Some(cipher) = Cipher::find_by_uuid(&cipher_id, &mut conn).await else { err!("Cipher doesn't exist") }; @@ -1131,7 +1136,7 @@ struct UploadData<'f> { /// database record, which is passed in as `attachment`. async fn save_attachment( mut attachment: Option, - cipher_uuid: CipherId, + cipher_id: CipherId, data: Form>, headers: &Headers, mut conn: DbConn, @@ -1146,7 +1151,7 @@ async fn save_attachment( err!("Attachment size can't be negative") } - let Some(cipher) = Cipher::find_by_uuid(&cipher_uuid, &mut conn).await else { + let Some(cipher) = Cipher::find_by_uuid(&cipher_id, &mut conn).await else { err!("Cipher doesn't exist") }; @@ -1161,11 +1166,11 @@ async fn save_attachment( Some(a) => a.file_size, // v2 API }; - let size_limit = if let Some(ref user_uuid) = cipher.user_uuid { + let size_limit = if let Some(ref user_id) = cipher.user_uuid { match CONFIG.user_attachment_limit() { Some(0) => err!("Attachments are disabled"), Some(limit_kb) => { - let already_used = Attachment::size_by_user(user_uuid, &mut conn).await; + let already_used = Attachment::size_by_user(user_id, &mut conn).await; let left = limit_kb .checked_mul(1024) .and_then(|l| l.checked_sub(already_used)) @@ -1183,11 +1188,11 @@ async fn save_attachment( } None => None, } - } else if let Some(ref org_uuid) = cipher.organization_uuid { + } else if let Some(ref org_id) = cipher.organization_uuid { match CONFIG.org_attachment_limit() { Some(0) => err!("Attachments are disabled"), Some(limit_kb) => { - let already_used = Attachment::size_by_org(org_uuid, &mut conn).await; + let already_used = Attachment::size_by_org(org_id, &mut conn).await; let left = limit_kb .checked_mul(1024) .and_then(|l| l.checked_sub(already_used)) @@ -1260,11 +1265,11 @@ async fn save_attachment( err!("No attachment key provided") } let attachment = - Attachment::new(file_id.clone(), cipher_uuid.clone(), encrypted_filename.unwrap(), size, data.key); + Attachment::new(file_id.clone(), cipher_id.clone(), encrypted_filename.unwrap(), size, data.key); attachment.save(&mut conn).await.expect("Error saving attachment"); } - let folder_path = tokio::fs::canonicalize(&CONFIG.attachments_folder()).await?.join(cipher_uuid.as_ref()); + let folder_path = tokio::fs::canonicalize(&CONFIG.attachments_folder()).await?.join(cipher_id.as_ref()); let file_path = folder_path.join(file_id.as_ref()); tokio::fs::create_dir_all(&folder_path).await?; @@ -1282,11 +1287,11 @@ async fn save_attachment( ) .await; - if let Some(org_uuid) = &cipher.organization_uuid { + if let Some(org_id) = &cipher.organization_uuid { log_event( EventType::CipherAttachmentCreated as i32, &cipher.uuid, - org_uuid, + org_id, &headers.user.uuid, headers.device.atype, &headers.ip.ip, @@ -1300,11 +1305,11 @@ async fn save_attachment( /// v2 API for uploading the actual data content of an attachment. /// This route needs a rank specified so that Rocket prioritizes the -/// /ciphers//attachment/v2 route, which would otherwise conflict +/// /ciphers//attachment/v2 route, which would otherwise conflict /// with this one. -#[post("/ciphers//attachment/", format = "multipart/form-data", data = "", rank = 1)] +#[post("/ciphers//attachment/", format = "multipart/form-data", data = "", rank = 1)] async fn post_attachment_v2_data( - uuid: CipherId, + cipher_id: CipherId, attachment_id: AttachmentId, data: Form>, headers: Headers, @@ -1312,20 +1317,20 @@ async fn post_attachment_v2_data( nt: Notify<'_>, ) -> EmptyResult { let attachment = match Attachment::find_by_id(&attachment_id, &mut conn).await { - Some(attachment) if uuid == attachment.cipher_uuid => Some(attachment), + Some(attachment) if cipher_id == attachment.cipher_uuid => Some(attachment), Some(_) => err!("Attachment doesn't belong to cipher"), None => err!("Attachment doesn't exist"), }; - save_attachment(attachment, uuid, data, &headers, conn, nt).await?; + save_attachment(attachment, cipher_id, data, &headers, conn, nt).await?; Ok(()) } /// Legacy API for creating an attachment associated with a cipher. -#[post("/ciphers//attachment", format = "multipart/form-data", data = "")] +#[post("/ciphers//attachment", format = "multipart/form-data", data = "")] async fn post_attachment( - uuid: CipherId, + cipher_id: CipherId, data: Form>, headers: Headers, conn: DbConn, @@ -1335,111 +1340,121 @@ async fn post_attachment( // the attachment database record as well as saving the data to disk. let attachment = None; - let (cipher, mut conn) = save_attachment(attachment, uuid, data, &headers, conn, nt).await?; + let (cipher, mut conn) = save_attachment(attachment, cipher_id, data, &headers, conn, nt).await?; Ok(Json(cipher.to_json(&headers.host, &headers.user.uuid, None, CipherSyncType::User, &mut conn).await)) } -#[post("/ciphers//attachment-admin", format = "multipart/form-data", data = "")] +#[post("/ciphers//attachment-admin", format = "multipart/form-data", data = "")] async fn post_attachment_admin( - uuid: CipherId, + cipher_id: CipherId, data: Form>, headers: Headers, conn: DbConn, nt: Notify<'_>, ) -> JsonResult { - post_attachment(uuid, data, headers, conn, nt).await + post_attachment(cipher_id, data, headers, conn, nt).await } -#[post("/ciphers//attachment//share", format = "multipart/form-data", data = "")] +#[post("/ciphers//attachment//share", format = "multipart/form-data", data = "")] async fn post_attachment_share( - uuid: CipherId, + cipher_id: CipherId, attachment_id: AttachmentId, data: Form>, headers: Headers, mut conn: DbConn, nt: Notify<'_>, ) -> JsonResult { - _delete_cipher_attachment_by_id(&uuid, &attachment_id, &headers, &mut conn, &nt).await?; - post_attachment(uuid, data, headers, conn, nt).await + _delete_cipher_attachment_by_id(&cipher_id, &attachment_id, &headers, &mut conn, &nt).await?; + post_attachment(cipher_id, data, headers, conn, nt).await } -#[post("/ciphers//attachment//delete-admin")] +#[post("/ciphers//attachment//delete-admin")] async fn delete_attachment_post_admin( - uuid: CipherId, + cipher_id: CipherId, attachment_id: AttachmentId, headers: Headers, conn: DbConn, nt: Notify<'_>, ) -> EmptyResult { - delete_attachment(uuid, attachment_id, headers, conn, nt).await + delete_attachment(cipher_id, attachment_id, headers, conn, nt).await } -#[post("/ciphers//attachment//delete")] +#[post("/ciphers//attachment//delete")] async fn delete_attachment_post( - uuid: CipherId, + cipher_id: CipherId, attachment_id: AttachmentId, headers: Headers, conn: DbConn, nt: Notify<'_>, ) -> EmptyResult { - delete_attachment(uuid, attachment_id, headers, conn, nt).await + delete_attachment(cipher_id, attachment_id, headers, conn, nt).await } -#[delete("/ciphers//attachment/")] +#[delete("/ciphers//attachment/")] async fn delete_attachment( - uuid: CipherId, + cipher_id: CipherId, attachment_id: AttachmentId, headers: Headers, mut conn: DbConn, nt: Notify<'_>, ) -> EmptyResult { - _delete_cipher_attachment_by_id(&uuid, &attachment_id, &headers, &mut conn, &nt).await + _delete_cipher_attachment_by_id(&cipher_id, &attachment_id, &headers, &mut conn, &nt).await } -#[delete("/ciphers//attachment//admin")] +#[delete("/ciphers//attachment//admin")] async fn delete_attachment_admin( - uuid: CipherId, + cipher_id: CipherId, attachment_id: AttachmentId, headers: Headers, mut conn: DbConn, nt: Notify<'_>, ) -> EmptyResult { - _delete_cipher_attachment_by_id(&uuid, &attachment_id, &headers, &mut conn, &nt).await + _delete_cipher_attachment_by_id(&cipher_id, &attachment_id, &headers, &mut conn, &nt).await } -#[post("/ciphers//delete")] -async fn delete_cipher_post(uuid: CipherId, headers: Headers, mut conn: DbConn, nt: Notify<'_>) -> EmptyResult { - _delete_cipher_by_uuid(&uuid, &headers, &mut conn, false, &nt).await +#[post("/ciphers//delete")] +async fn delete_cipher_post(cipher_id: CipherId, headers: Headers, mut conn: DbConn, nt: Notify<'_>) -> EmptyResult { + _delete_cipher_by_uuid(&cipher_id, &headers, &mut conn, false, &nt).await // permanent delete } -#[post("/ciphers//delete-admin")] -async fn delete_cipher_post_admin(uuid: CipherId, headers: Headers, mut conn: DbConn, nt: Notify<'_>) -> EmptyResult { - _delete_cipher_by_uuid(&uuid, &headers, &mut conn, false, &nt).await +#[post("/ciphers//delete-admin")] +async fn delete_cipher_post_admin( + cipher_id: CipherId, + headers: Headers, + mut conn: DbConn, + nt: Notify<'_>, +) -> EmptyResult { + _delete_cipher_by_uuid(&cipher_id, &headers, &mut conn, false, &nt).await // permanent delete } -#[put("/ciphers//delete")] -async fn delete_cipher_put(uuid: CipherId, headers: Headers, mut conn: DbConn, nt: Notify<'_>) -> EmptyResult { - _delete_cipher_by_uuid(&uuid, &headers, &mut conn, true, &nt).await +#[put("/ciphers//delete")] +async fn delete_cipher_put(cipher_id: CipherId, headers: Headers, mut conn: DbConn, nt: Notify<'_>) -> EmptyResult { + _delete_cipher_by_uuid(&cipher_id, &headers, &mut conn, true, &nt).await // soft delete } -#[put("/ciphers//delete-admin")] -async fn delete_cipher_put_admin(uuid: CipherId, headers: Headers, mut conn: DbConn, nt: Notify<'_>) -> EmptyResult { - _delete_cipher_by_uuid(&uuid, &headers, &mut conn, true, &nt).await +#[put("/ciphers//delete-admin")] +async fn delete_cipher_put_admin( + cipher_id: CipherId, + headers: Headers, + mut conn: DbConn, + nt: Notify<'_>, +) -> EmptyResult { + _delete_cipher_by_uuid(&cipher_id, &headers, &mut conn, true, &nt).await } -#[delete("/ciphers/")] -async fn delete_cipher(uuid: CipherId, headers: Headers, mut conn: DbConn, nt: Notify<'_>) -> EmptyResult { - _delete_cipher_by_uuid(&uuid, &headers, &mut conn, false, &nt).await +#[delete("/ciphers/")] +async fn delete_cipher(cipher_id: CipherId, headers: Headers, mut conn: DbConn, nt: Notify<'_>) -> EmptyResult { + _delete_cipher_by_uuid(&cipher_id, &headers, &mut conn, false, &nt).await // permanent delete } -#[delete("/ciphers//admin")] -async fn delete_cipher_admin(uuid: CipherId, headers: Headers, mut conn: DbConn, nt: Notify<'_>) -> EmptyResult { - _delete_cipher_by_uuid(&uuid, &headers, &mut conn, false, &nt).await +#[delete("/ciphers//admin")] +async fn delete_cipher_admin(cipher_id: CipherId, headers: Headers, mut conn: DbConn, nt: Notify<'_>) -> EmptyResult { + _delete_cipher_by_uuid(&cipher_id, &headers, &mut conn, false, &nt).await // permanent delete } @@ -1503,14 +1518,19 @@ async fn delete_cipher_selected_put_admin( _delete_multiple_ciphers(data, headers, conn, true, nt).await // soft delete } -#[put("/ciphers//restore")] -async fn restore_cipher_put(uuid: CipherId, headers: Headers, mut conn: DbConn, nt: Notify<'_>) -> JsonResult { - _restore_cipher_by_uuid(&uuid, &headers, &mut conn, &nt).await +#[put("/ciphers//restore")] +async fn restore_cipher_put(cipher_id: CipherId, headers: Headers, mut conn: DbConn, nt: Notify<'_>) -> JsonResult { + _restore_cipher_by_uuid(&cipher_id, &headers, &mut conn, &nt).await } -#[put("/ciphers//restore-admin")] -async fn restore_cipher_put_admin(uuid: CipherId, headers: Headers, mut conn: DbConn, nt: Notify<'_>) -> JsonResult { - _restore_cipher_by_uuid(&uuid, &headers, &mut conn, &nt).await +#[put("/ciphers//restore-admin")] +async fn restore_cipher_put_admin( + cipher_id: CipherId, + headers: Headers, + mut conn: DbConn, + nt: Notify<'_>, +) -> JsonResult { + _restore_cipher_by_uuid(&cipher_id, &headers, &mut conn, &nt).await } #[put("/ciphers/restore", data = "")] @@ -1538,30 +1558,30 @@ async fn move_cipher_selected( nt: Notify<'_>, ) -> EmptyResult { let data = data.into_inner(); - let user_uuid = headers.user.uuid; + let user_id = headers.user.uuid; - if let Some(ref folder_uuid) = data.folder_id { - if Folder::find_by_uuid_and_user(folder_uuid, &user_uuid, &mut conn).await.is_none() { + if let Some(ref folder_id) = data.folder_id { + if Folder::find_by_uuid_and_user(folder_id, &user_id, &mut conn).await.is_none() { err!("Invalid folder", "Folder does not exist or belongs to another user"); } } - for uuid in data.ids { - let Some(cipher) = Cipher::find_by_uuid(&uuid, &mut conn).await else { + for cipher_id in data.ids { + let Some(cipher) = Cipher::find_by_uuid(&cipher_id, &mut conn).await else { err!("Cipher doesn't exist") }; - if !cipher.is_accessible_to_user(&user_uuid, &mut conn).await { + if !cipher.is_accessible_to_user(&user_id, &mut conn).await { err!("Cipher is not accessible by user") } // Move cipher - cipher.move_to_folder(data.folder_id.clone(), &user_uuid, &mut conn).await?; + cipher.move_to_folder(data.folder_id.clone(), &user_id, &mut conn).await?; nt.send_cipher_update( UpdateType::SyncCipherUpdate, &cipher, - &[user_uuid.clone()], + &[user_id.clone()], &headers.device.uuid, None, &mut conn, @@ -1650,13 +1670,13 @@ async fn delete_all( } async fn _delete_cipher_by_uuid( - uuid: &CipherId, + cipher_id: &CipherId, headers: &Headers, conn: &mut DbConn, soft_delete: bool, nt: &Notify<'_>, ) -> EmptyResult { - let Some(mut cipher) = Cipher::find_by_uuid(uuid, conn).await else { + let Some(mut cipher) = Cipher::find_by_uuid(cipher_id, conn).await else { err!("Cipher doesn't exist") }; @@ -1689,13 +1709,13 @@ async fn _delete_cipher_by_uuid( .await; } - if let Some(org_uuid) = cipher.organization_uuid { + if let Some(org_id) = cipher.organization_uuid { let event_type = match soft_delete { true => EventType::CipherSoftDeleted as i32, false => EventType::CipherDeleted as i32, }; - log_event(event_type, &cipher.uuid, &org_uuid, &headers.user.uuid, headers.device.atype, &headers.ip.ip, conn) + log_event(event_type, &cipher.uuid, &org_id, &headers.user.uuid, headers.device.atype, &headers.ip.ip, conn) .await; } @@ -1717,8 +1737,8 @@ async fn _delete_multiple_ciphers( ) -> EmptyResult { let data = data.into_inner(); - for uuid in data.ids { - if let error @ Err(_) = _delete_cipher_by_uuid(&uuid, &headers, &mut conn, soft_delete, &nt).await { + for cipher_id in data.ids { + if let error @ Err(_) = _delete_cipher_by_uuid(&cipher_id, &headers, &mut conn, soft_delete, &nt).await { return error; }; } @@ -1726,8 +1746,13 @@ async fn _delete_multiple_ciphers( Ok(()) } -async fn _restore_cipher_by_uuid(uuid: &CipherId, headers: &Headers, conn: &mut DbConn, nt: &Notify<'_>) -> JsonResult { - let Some(mut cipher) = Cipher::find_by_uuid(uuid, conn).await else { +async fn _restore_cipher_by_uuid( + cipher_id: &CipherId, + headers: &Headers, + conn: &mut DbConn, + nt: &Notify<'_>, +) -> JsonResult { + let Some(mut cipher) = Cipher::find_by_uuid(cipher_id, conn).await else { err!("Cipher doesn't exist") }; @@ -1748,11 +1773,11 @@ async fn _restore_cipher_by_uuid(uuid: &CipherId, headers: &Headers, conn: &mut ) .await; - if let Some(org_uuid) = &cipher.organization_uuid { + if let Some(org_id) = &cipher.organization_uuid { log_event( EventType::CipherRestored as i32, &cipher.uuid.clone(), - org_uuid, + org_id, &headers.user.uuid, headers.device.atype, &headers.ip.ip, @@ -1773,8 +1798,8 @@ async fn _restore_multiple_ciphers( let data = data.into_inner(); let mut ciphers: Vec = Vec::new(); - for uuid in data.ids { - match _restore_cipher_by_uuid(&uuid, headers, conn, nt).await { + for cipher_id in data.ids { + match _restore_cipher_by_uuid(&cipher_id, headers, conn, nt).await { Ok(json) => ciphers.push(json.into_inner()), err => return err, } @@ -1788,7 +1813,7 @@ async fn _restore_multiple_ciphers( } async fn _delete_cipher_attachment_by_id( - uuid: &CipherId, + cipher_id: &CipherId, attachment_id: &AttachmentId, headers: &Headers, conn: &mut DbConn, @@ -1798,11 +1823,11 @@ async fn _delete_cipher_attachment_by_id( err!("Attachment doesn't exist") }; - if &attachment.cipher_uuid != uuid { + if &attachment.cipher_uuid != cipher_id { err!("Attachment from other cipher") } - let Some(cipher) = Cipher::find_by_uuid(uuid, conn).await else { + let Some(cipher) = Cipher::find_by_uuid(cipher_id, conn).await else { err!("Cipher doesn't exist") }; @@ -1822,11 +1847,11 @@ async fn _delete_cipher_attachment_by_id( ) .await; - if let Some(org_uuid) = cipher.organization_uuid { + if let Some(org_id) = cipher.organization_uuid { log_event( EventType::CipherAttachmentDeleted as i32, &cipher.uuid, - &org_uuid, + &org_id, &headers.user.uuid, headers.device.atype, &headers.ip.ip, @@ -1859,17 +1884,17 @@ pub enum CipherSyncType { } impl CipherSyncData { - pub async fn new(user_uuid: &UserId, sync_type: CipherSyncType, conn: &mut DbConn) -> Self { + pub async fn new(user_id: &UserId, sync_type: CipherSyncType, conn: &mut DbConn) -> Self { let cipher_folders: HashMap; let cipher_favorites: HashSet; match sync_type { // User Sync supports Folders and Favorites CipherSyncType::User => { // Generate a HashMap with the Cipher UUID as key and the Folder UUID as value - cipher_folders = FolderCipher::find_by_user(user_uuid, conn).await.into_iter().collect(); + cipher_folders = FolderCipher::find_by_user(user_id, conn).await.into_iter().collect(); // Generate a HashSet of all the Cipher UUID's which are marked as favorite - cipher_favorites = Favorite::get_all_cipher_uuid_by_user(user_uuid, conn).await.into_iter().collect(); + cipher_favorites = Favorite::get_all_cipher_uuid_by_user(user_id, conn).await.into_iter().collect(); } // Organization Sync does not support Folders and Favorites. // If these are set, it will cause issues in the web-vault. @@ -1880,15 +1905,15 @@ impl CipherSyncData { } // Generate a list of Cipher UUID's containing a Vec with one or more Attachment records - let orgs = Membership::get_orgs_by_user(user_uuid, conn).await; - let attachments = Attachment::find_all_by_user_and_orgs(user_uuid, &orgs, conn).await; + let orgs = Membership::get_orgs_by_user(user_id, conn).await; + let attachments = Attachment::find_all_by_user_and_orgs(user_id, &orgs, conn).await; let mut cipher_attachments: HashMap> = HashMap::with_capacity(attachments.len()); for attachment in attachments { cipher_attachments.entry(attachment.cipher_uuid.clone()).or_default().push(attachment); } // Generate a HashMap with the Cipher UUID as key and one or more Collection UUID's - let user_cipher_collections = Cipher::get_collections_with_cipher_by_user(user_uuid.clone(), conn).await; + let user_cipher_collections = Cipher::get_collections_with_cipher_by_user(user_id.clone(), conn).await; let mut cipher_collections: HashMap> = HashMap::with_capacity(user_cipher_collections.len()); for (cipher, collection) in user_cipher_collections { @@ -1897,10 +1922,10 @@ impl CipherSyncData { // Generate a HashMap with the Organization UUID as key and the Membership record let members: HashMap = - Membership::find_by_user(user_uuid, conn).await.into_iter().map(|m| (m.org_uuid.clone(), m)).collect(); + Membership::find_by_user(user_id, conn).await.into_iter().map(|m| (m.org_uuid.clone(), m)).collect(); // Generate a HashMap with the User_Collections UUID as key and the CollectionUser record - let user_collections: HashMap = CollectionUser::find_by_user(user_uuid, conn) + let user_collections: HashMap = CollectionUser::find_by_user(user_id, conn) .await .into_iter() .map(|uc| (uc.collection_uuid.clone(), uc)) @@ -1908,7 +1933,7 @@ impl CipherSyncData { // Generate a HashMap with the collections_uuid as key and the CollectionGroup record let user_collections_groups: HashMap = if CONFIG.org_groups_enabled() { - CollectionGroup::find_by_user(user_uuid, conn) + CollectionGroup::find_by_user(user_id, conn) .await .into_iter() .map(|collection_group| (collection_group.collections_uuid.clone(), collection_group)) @@ -1919,7 +1944,7 @@ impl CipherSyncData { // Get all organizations that the given user has full access to via group assignment let user_group_full_access_for_organizations: HashSet = if CONFIG.org_groups_enabled() { - Group::get_orgs_by_user_with_full_access(user_uuid, conn).await.into_iter().collect() + Group::get_orgs_by_user_with_full_access(user_id, conn).await.into_iter().collect() } else { HashSet::new() }; diff --git a/src/api/core/emergency_access.rs b/src/api/core/emergency_access.rs index cc93f23c8a..7a609ee4f7 100644 --- a/src/api/core/emergency_access.rs +++ b/src/api/core/emergency_access.rs @@ -701,11 +701,11 @@ async fn policies_emergency_access(emer_id: &str, headers: Headers, mut conn: Db fn is_valid_request( emergency_access: &EmergencyAccess, - requesting_user_uuid: &UserId, + requesting_user_id: &UserId, requested_access_type: EmergencyAccessType, ) -> bool { emergency_access.grantee_uuid.is_some() - && emergency_access.grantee_uuid.as_ref().unwrap() == requesting_user_uuid + && 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 } diff --git a/src/api/core/events.rs b/src/api/core/events.rs index ac53744c68..4083f4475b 100644 --- a/src/api/core/events.rs +++ b/src/api/core/events.rs @@ -180,11 +180,11 @@ async fn post_events_collect(data: Json>, headers: Headers, .await; } 1600..=1699 => { - if let Some(org_uuid) = &event.organization_id { + if let Some(org_id) = &event.organization_id { _log_event( event.r#type, - org_uuid, - org_uuid, + org_id, + org_id, &headers.user.uuid, headers.device.atype, Some(event_date), @@ -197,11 +197,11 @@ async fn post_events_collect(data: Json>, headers: Headers, _ => { if let Some(cipher_uuid) = &event.cipher_id { if let Some(cipher) = Cipher::find_by_uuid(cipher_uuid, &mut conn).await { - if let Some(org_uuid) = cipher.organization_uuid { + if let Some(org_id) = cipher.organization_uuid { _log_event( event.r#type, cipher_uuid, - &org_uuid, + &org_id, &headers.user.uuid, headers.device.atype, Some(event_date), @@ -218,38 +218,38 @@ async fn post_events_collect(data: Json>, headers: Headers, Ok(()) } -pub async fn log_user_event(event_type: i32, user_uuid: &UserId, device_type: i32, ip: &IpAddr, conn: &mut DbConn) { +pub async fn log_user_event(event_type: i32, user_id: &UserId, device_type: i32, ip: &IpAddr, conn: &mut DbConn) { if !CONFIG.org_events_enabled() { return; } - _log_user_event(event_type, user_uuid, device_type, None, ip, conn).await; + _log_user_event(event_type, user_id, device_type, None, ip, conn).await; } async fn _log_user_event( event_type: i32, - user_uuid: &UserId, + user_id: &UserId, device_type: i32, event_date: Option, ip: &IpAddr, conn: &mut DbConn, ) { - let orgs = Membership::get_orgs_by_user(user_uuid, conn).await; + let orgs = Membership::get_orgs_by_user(user_id, conn).await; let mut events: Vec = Vec::with_capacity(orgs.len() + 1); // We need an event per org and one without an org - // Upstream saves the event also without any org_uuid. + // Upstream saves the event also without any org_id. let mut event = Event::new(event_type, event_date); - event.user_uuid = Some(user_uuid.clone()); - event.act_user_uuid = Some(user_uuid.to_string()); + event.user_uuid = Some(user_id.clone()); + event.act_user_uuid = Some(user_id.to_string()); event.device_type = Some(device_type); event.ip_address = Some(ip.to_string()); events.push(event); // For each org a user is a member of store these events per org - for org_uuid in orgs { + for org_id in orgs { let mut event = Event::new(event_type, event_date); - event.user_uuid = Some(user_uuid.clone()); - event.org_uuid = Some(org_uuid); - event.act_user_uuid = Some(user_uuid.to_string()); + event.user_uuid = Some(user_id.clone()); + event.org_uuid = Some(org_id); + event.act_user_uuid = Some(user_id.to_string()); event.device_type = Some(device_type); event.ip_address = Some(ip.to_string()); events.push(event); @@ -261,8 +261,8 @@ async fn _log_user_event( pub async fn log_event( event_type: i32, source_uuid: &str, - org_uuid: &OrganizationId, - act_user_uuid: &str, + org_id: &OrganizationId, + act_user_id: &str, device_type: i32, ip: &IpAddr, conn: &mut DbConn, @@ -270,15 +270,15 @@ pub async fn log_event( if !CONFIG.org_events_enabled() { return; } - _log_event(event_type, source_uuid, org_uuid, act_user_uuid, device_type, None, ip, conn).await; + _log_event(event_type, source_uuid, org_id, act_user_id, device_type, None, ip, conn).await; } #[allow(clippy::too_many_arguments)] async fn _log_event( event_type: i32, source_uuid: &str, - org_uuid: &OrganizationId, - act_user_uuid: &str, + org_id: &OrganizationId, + act_user_id: &str, device_type: i32, event_date: Option, ip: &IpAddr, @@ -313,8 +313,8 @@ async fn _log_event( _ => {} } - event.org_uuid = Some(org_uuid.clone()); - event.act_user_uuid = Some(String::from(act_user_uuid)); + event.org_uuid = Some(org_id.clone()); + event.act_user_uuid = Some(String::from(act_user_id)); event.device_type = Some(device_type); event.ip_address = Some(ip.to_string()); event.save(conn).await.unwrap_or(()); diff --git a/src/api/core/folders.rs b/src/api/core/folders.rs index 708bedffc1..01dea4bb0c 100644 --- a/src/api/core/folders.rs +++ b/src/api/core/folders.rs @@ -23,9 +23,9 @@ async fn get_folders(headers: Headers, mut conn: DbConn) -> Json { })) } -#[get("/folders/")] -async fn get_folder(uuid: FolderId, headers: Headers, mut conn: DbConn) -> JsonResult { - match Folder::find_by_uuid_and_user(&uuid, &headers.user.uuid, &mut conn).await { +#[get("/folders/")] +async fn get_folder(folder_id: FolderId, headers: Headers, mut conn: DbConn) -> JsonResult { + match Folder::find_by_uuid_and_user(&folder_id, &headers.user.uuid, &mut conn).await { Some(folder) => Ok(Json(folder.to_json())), _ => err!("Invalid folder", "Folder does not exist or belongs to another user"), } @@ -50,20 +50,20 @@ async fn post_folders(data: Json, headers: Headers, mut conn: DbConn Ok(Json(folder.to_json())) } -#[post("/folders/", data = "")] +#[post("/folders/", data = "")] async fn post_folder( - uuid: FolderId, + folder_id: FolderId, data: Json, headers: Headers, conn: DbConn, nt: Notify<'_>, ) -> JsonResult { - put_folder(uuid, data, headers, conn, nt).await + put_folder(folder_id, data, headers, conn, nt).await } -#[put("/folders/", data = "")] +#[put("/folders/", data = "")] async fn put_folder( - uuid: FolderId, + folder_id: FolderId, data: Json, headers: Headers, mut conn: DbConn, @@ -71,7 +71,7 @@ async fn put_folder( ) -> JsonResult { let data: FolderData = data.into_inner(); - let Some(mut folder) = Folder::find_by_uuid_and_user(&uuid, &headers.user.uuid, &mut conn).await else { + let Some(mut folder) = Folder::find_by_uuid_and_user(&folder_id, &headers.user.uuid, &mut conn).await else { err!("Invalid folder", "Folder does not exist or belongs to another user") }; @@ -83,14 +83,14 @@ async fn put_folder( Ok(Json(folder.to_json())) } -#[post("/folders//delete")] -async fn delete_folder_post(uuid: FolderId, headers: Headers, conn: DbConn, nt: Notify<'_>) -> EmptyResult { - delete_folder(uuid, headers, conn, nt).await +#[post("/folders//delete")] +async fn delete_folder_post(folder_id: FolderId, headers: Headers, conn: DbConn, nt: Notify<'_>) -> EmptyResult { + delete_folder(folder_id, headers, conn, nt).await } -#[delete("/folders/")] -async fn delete_folder(uuid: FolderId, headers: Headers, mut conn: DbConn, nt: Notify<'_>) -> EmptyResult { - let Some(folder) = Folder::find_by_uuid_and_user(&uuid, &headers.user.uuid, &mut conn).await else { +#[delete("/folders/")] +async fn delete_folder(folder_id: FolderId, headers: Headers, mut conn: DbConn, nt: Notify<'_>) -> EmptyResult { + let Some(folder) = Folder::find_by_uuid_and_user(&folder_id, &headers.user.uuid, &mut conn).await else { err!("Invalid folder", "Folder does not exist or belongs to another user") }; diff --git a/src/api/core/organizations.rs b/src/api/core/organizations.rs index 6b36f4efc2..35297dcc54 100644 --- a/src/api/core/organizations.rs +++ b/src/api/core/organizations.rs @@ -753,14 +753,13 @@ async fn get_org_details(data: OrgIdData, headers: Headers, mut conn: DbConn) -> }))) } -async fn _get_org_details(org_id: &OrganizationId, host: &str, user_uuid: &UserId, conn: &mut DbConn) -> Value { +async fn _get_org_details(org_id: &OrganizationId, host: &str, user_id: &UserId, conn: &mut DbConn) -> Value { let ciphers = Cipher::find_by_org(org_id, conn).await; - let cipher_sync_data = CipherSyncData::new(user_uuid, CipherSyncType::Organization, conn).await; + let cipher_sync_data = CipherSyncData::new(user_id, CipherSyncType::Organization, conn).await; let mut ciphers_json = Vec::with_capacity(ciphers.len()); for c in ciphers { - ciphers_json - .push(c.to_json(host, user_uuid, Some(&cipher_sync_data), CipherSyncType::Organization, conn).await); + ciphers_json.push(c.to_json(host, user_id, Some(&cipher_sync_data), CipherSyncType::Organization, conn).await); } json!(ciphers_json) } diff --git a/src/api/core/public.rs b/src/api/core/public.rs index 7478833f1e..c3ecd25ed2 100644 --- a/src/api/core/public.rs +++ b/src/api/core/public.rs @@ -219,11 +219,11 @@ impl<'r> FromRequest<'r> for PublicToken { Outcome::Success(conn) => conn, _ => err_handler!("Error getting DB"), }; - let Some(org_uuid) = claims.client_id.strip_prefix("organization.") else { + let Some(org_id) = claims.client_id.strip_prefix("organization.") else { err_handler!("Malformed client_id") }; - let org_uuid: OrganizationId = org_uuid.to_string().into(); - let Some(org_api_key) = OrganizationApiKey::find_by_org_uuid(&org_uuid, &conn).await else { + let org_id: OrganizationId = org_id.to_string().into(); + let Some(org_api_key) = OrganizationApiKey::find_by_org_uuid(&org_id, &conn).await else { err_handler!("Invalid client_id") }; if org_api_key.org_uuid != claims.client_sub { diff --git a/src/api/core/sends.rs b/src/api/core/sends.rs index f6b8b508de..e181d6ab84 100644 --- a/src/api/core/sends.rs +++ b/src/api/core/sends.rs @@ -79,9 +79,9 @@ pub struct SendData { /// There is also a Vaultwarden-specific `sends_allowed` config setting that /// controls this policy globally. async fn enforce_disable_send_policy(headers: &Headers, conn: &mut DbConn) -> EmptyResult { - let user_uuid = &headers.user.uuid; + let user_id = &headers.user.uuid; if !CONFIG.sends_allowed() - || OrgPolicy::is_applicable_to_user(user_uuid, OrgPolicyType::DisableSend, None, conn).await + || OrgPolicy::is_applicable_to_user(user_id, OrgPolicyType::DisableSend, None, conn).await { err!("Due to an Enterprise Policy, you are only able to delete an existing Send.") } @@ -95,9 +95,9 @@ async fn enforce_disable_send_policy(headers: &Headers, conn: &mut DbConn) -> Em /// /// Ref: https://bitwarden.com/help/article/policies/#send-options async fn enforce_disable_hide_email_policy(data: &SendData, headers: &Headers, conn: &mut DbConn) -> EmptyResult { - let user_uuid = &headers.user.uuid; + let user_id = &headers.user.uuid; let hide_email = data.hide_email.unwrap_or(false); - if hide_email && OrgPolicy::is_hide_email_disabled(user_uuid, conn).await { + if hide_email && OrgPolicy::is_hide_email_disabled(user_id, conn).await { err!( "Due to an Enterprise Policy, you are not allowed to hide your email address \ from recipients when creating or editing a Send." @@ -106,7 +106,7 @@ async fn enforce_disable_hide_email_policy(data: &SendData, headers: &Headers, c Ok(()) } -fn create_send(data: SendData, user_uuid: UserId) -> ApiResult { +fn create_send(data: SendData, user_id: UserId) -> ApiResult { let data_val = if data.r#type == SendType::Text as i32 { data.text } else if data.r#type == SendType::File as i32 { @@ -129,7 +129,7 @@ fn create_send(data: SendData, user_uuid: UserId) -> ApiResult { } let mut send = Send::new(data.r#type, data.name, data_str, data.key, data.deletion_date.naive_utc()); - send.user_uuid = Some(user_uuid); + send.user_uuid = Some(user_id); send.notes = data.notes; send.max_access_count = match data.max_access_count { Some(m) => Some(m.into_i32()?), @@ -157,11 +157,11 @@ async fn get_sends(headers: Headers, mut conn: DbConn) -> Json { })) } -#[get("/sends/")] -async fn get_send(uuid: SendId, headers: Headers, mut conn: DbConn) -> JsonResult { - match Send::find_by_uuid_and_user(&uuid, &headers.user.uuid, &mut conn).await { +#[get("/sends/")] +async fn get_send(send_id: SendId, headers: Headers, mut conn: DbConn) -> JsonResult { + match Send::find_by_uuid_and_user(&send_id, &headers.user.uuid, &mut conn).await { Some(send) => Ok(Json(send.to_json())), - None => err!("Send not found", "Invalid uuid or does not belong to user"), + None => err!("Send not found", "Invalid send uuid or does not belong to user"), } } @@ -352,9 +352,9 @@ pub struct SendFileData { } // https://github.com/bitwarden/server/blob/66f95d1c443490b653e5a15d32977e2f5a3f9e32/src/Api/Tools/Controllers/SendsController.cs#L250 -#[post("/sends//file/", format = "multipart/form-data", data = "")] +#[post("/sends//file/", format = "multipart/form-data", data = "")] async fn post_send_file_v2_data( - uuid: SendId, + send_id: SendId, file_id: SendFileId, data: Form>, headers: Headers, @@ -365,8 +365,8 @@ async fn post_send_file_v2_data( let mut data = data.into_inner(); - let Some(send) = Send::find_by_uuid_and_user(&uuid, &headers.user.uuid, &mut conn).await else { - err!("Send not found. Unable to save the file.", "Invalid uuid or does not belong to user.") + let Some(send) = Send::find_by_uuid_and_user(&send_id, &headers.user.uuid, &mut conn).await else { + err!("Send not found. Unable to save the file.", "Invalid send uuid or does not belong to user.") }; if send.atype != SendType::File as i32 { @@ -402,7 +402,7 @@ async fn post_send_file_v2_data( err!("Send file size does not match.", format!("Expected a file size of {} got {size}", send_data.size)); } - let folder_path = tokio::fs::canonicalize(&CONFIG.sends_folder()).await?.join(uuid); + let folder_path = tokio::fs::canonicalize(&CONFIG.sends_folder()).await?.join(send_id); let file_path = folder_path.join(file_id); // Check if the file already exists, if that is the case do not overwrite it @@ -493,16 +493,16 @@ async fn post_access( Ok(Json(send.to_json_access(&mut conn).await)) } -#[post("/sends//access/file/", data = "")] +#[post("/sends//access/file/", data = "")] async fn post_access_file( - uuid: SendId, + send_id: SendId, file_id: SendFileId, data: Json, host: Host, mut conn: DbConn, nt: Notify<'_>, ) -> JsonResult { - let Some(mut send) = Send::find_by_uuid(&uuid, &mut conn).await else { + let Some(mut send) = Send::find_by_uuid(&send_id, &mut conn).await else { err_code!(SEND_INACCESSIBLE_MSG, 404) }; @@ -547,28 +547,28 @@ async fn post_access_file( ) .await; - let token_claims = crate::auth::generate_send_claims(&uuid, &file_id); + let token_claims = crate::auth::generate_send_claims(&send_id, &file_id); let token = crate::auth::encode_jwt(&token_claims); Ok(Json(json!({ "object": "send-fileDownload", "id": file_id, - "url": format!("{}/api/sends/{}/{}?t={}", &host.host, uuid, file_id, token) + "url": format!("{}/api/sends/{}/{}?t={}", &host.host, send_id, file_id, token) }))) } -#[get("/sends//?")] -async fn download_send(uuid: SendId, file_id: SendFileId, t: &str) -> Option { +#[get("/sends//?")] +async fn download_send(send_id: SendId, file_id: SendFileId, t: &str) -> Option { if let Ok(claims) = crate::auth::decode_send(t) { - if claims.sub == format!("{uuid}/{file_id}") { - return NamedFile::open(Path::new(&CONFIG.sends_folder()).join(uuid).join(file_id)).await.ok(); + if claims.sub == format!("{send_id}/{file_id}") { + return NamedFile::open(Path::new(&CONFIG.sends_folder()).join(send_id).join(file_id)).await.ok(); } } None } -#[put("/sends/", data = "")] +#[put("/sends/", data = "")] async fn put_send( - uuid: SendId, + send_id: SendId, data: Json, headers: Headers, mut conn: DbConn, @@ -579,8 +579,8 @@ async fn put_send( let data: SendData = data.into_inner(); enforce_disable_hide_email_policy(&data, &headers, &mut conn).await?; - let Some(mut send) = Send::find_by_uuid_and_user(&uuid, &headers.user.uuid, &mut conn).await else { - err!("Send not found", "Send uuid is invalid or does not belong to user") + let Some(mut send) = Send::find_by_uuid_and_user(&send_id, &headers.user.uuid, &mut conn).await else { + err!("Send not found", "Send send_id is invalid or does not belong to user") }; update_send_from_data(&mut send, data, &headers, &mut conn, &nt, UpdateType::SyncSendUpdate).await?; @@ -646,9 +646,9 @@ pub async fn update_send_from_data( Ok(()) } -#[delete("/sends/")] -async fn delete_send(uuid: SendId, headers: Headers, mut conn: DbConn, nt: Notify<'_>) -> EmptyResult { - let Some(send) = Send::find_by_uuid_and_user(&uuid, &headers.user.uuid, &mut conn).await else { +#[delete("/sends/")] +async fn delete_send(send_id: SendId, headers: Headers, mut conn: DbConn, nt: Notify<'_>) -> EmptyResult { + let Some(send) = Send::find_by_uuid_and_user(&send_id, &headers.user.uuid, &mut conn).await else { err!("Send not found", "Invalid send uuid, or does not belong to user") }; @@ -665,11 +665,11 @@ async fn delete_send(uuid: SendId, headers: Headers, mut conn: DbConn, nt: Notif Ok(()) } -#[put("/sends//remove-password")] -async fn put_remove_password(uuid: SendId, headers: Headers, mut conn: DbConn, nt: Notify<'_>) -> JsonResult { +#[put("/sends//remove-password")] +async fn put_remove_password(send_id: SendId, headers: Headers, mut conn: DbConn, nt: Notify<'_>) -> JsonResult { enforce_disable_send_policy(&headers, &mut conn).await?; - let Some(mut send) = Send::find_by_uuid_and_user(&uuid, &headers.user.uuid, &mut conn).await else { + let Some(mut send) = Send::find_by_uuid_and_user(&send_id, &headers.user.uuid, &mut conn).await else { err!("Send not found", "Invalid send uuid, or does not belong to user") }; diff --git a/src/api/core/two_factor/authenticator.rs b/src/api/core/two_factor/authenticator.rs index 5861bee284..a3077a020d 100644 --- a/src/api/core/two_factor/authenticator.rs +++ b/src/api/core/two_factor/authenticator.rs @@ -95,7 +95,7 @@ async fn activate_authenticator_put(data: Json, headers } pub async fn validate_totp_code_str( - user_uuid: &UserId, + user_id: &UserId, totp_code: &str, secret: &str, ip: &ClientIp, @@ -105,11 +105,11 @@ pub async fn validate_totp_code_str( err!("TOTP code is not a number"); } - validate_totp_code(user_uuid, totp_code, secret, ip, conn).await + validate_totp_code(user_id, totp_code, secret, ip, conn).await } pub async fn validate_totp_code( - user_uuid: &UserId, + user_id: &UserId, totp_code: &str, secret: &str, ip: &ClientIp, @@ -121,11 +121,11 @@ pub async fn validate_totp_code( err!("Invalid TOTP secret") }; - let mut twofactor = - match TwoFactor::find_by_user_and_type(user_uuid, TwoFactorType::Authenticator as i32, conn).await { - Some(tf) => tf, - _ => TwoFactor::new(user_uuid.clone(), TwoFactorType::Authenticator, secret.to_string()), - }; + let mut twofactor = match TwoFactor::find_by_user_and_type(user_id, TwoFactorType::Authenticator as i32, conn).await + { + Some(tf) => tf, + _ => TwoFactor::new(user_id.clone(), TwoFactorType::Authenticator, secret.to_string()), + }; // The amount of steps back and forward in time // Also check if we need to disable time drifted TOTP codes. diff --git a/src/api/core/two_factor/duo.rs b/src/api/core/two_factor/duo.rs index 5567f41e24..289f01b3ba 100644 --- a/src/api/core/two_factor/duo.rs +++ b/src/api/core/two_factor/duo.rs @@ -228,11 +228,11 @@ const AUTH_PREFIX: &str = "AUTH"; const DUO_PREFIX: &str = "TX"; const APP_PREFIX: &str = "APP"; -async fn get_user_duo_data(user_uuid: &UserId, conn: &mut DbConn) -> DuoStatus { +async fn get_user_duo_data(user_id: &UserId, conn: &mut DbConn) -> DuoStatus { let type_ = TwoFactorType::Duo as i32; // If the user doesn't have an entry, disabled - let Some(twofactor) = TwoFactor::find_by_user_and_type(user_uuid, type_, conn).await else { + let Some(twofactor) = TwoFactor::find_by_user_and_type(user_id, type_, conn).await else { return DuoStatus::Disabled(DuoData::global().is_some()); }; diff --git a/src/api/core/two_factor/email.rs b/src/api/core/two_factor/email.rs index eabfbcf5a8..d6470a288b 100644 --- a/src/api/core/two_factor/email.rs +++ b/src/api/core/two_factor/email.rs @@ -59,10 +59,9 @@ async fn send_email_login(data: Json, mut conn: DbConn) -> E } /// Generate the token, save the data for later verification and send email to user -pub async fn send_token(user_uuid: &UserId, conn: &mut DbConn) -> EmptyResult { +pub async fn send_token(user_id: &UserId, conn: &mut DbConn) -> EmptyResult { let type_ = TwoFactorType::Email as i32; - let mut twofactor = - TwoFactor::find_by_user_and_type(user_uuid, type_, conn).await.map_res("Two factor not found")?; + let mut twofactor = TwoFactor::find_by_user_and_type(user_id, type_, conn).await.map_res("Two factor not found")?; let generated_token = crypto::generate_email_token(CONFIG.email_token_size()); @@ -198,9 +197,9 @@ async fn email(data: Json, headers: Headers, mut conn: DbConn) -> Jso } /// Validate the email code when used as TwoFactor token mechanism -pub async fn validate_email_code_str(user_uuid: &UserId, token: &str, data: &str, conn: &mut DbConn) -> EmptyResult { +pub async fn validate_email_code_str(user_id: &UserId, token: &str, data: &str, conn: &mut DbConn) -> EmptyResult { let mut email_data = EmailTokenData::from_json(data)?; - let mut twofactor = TwoFactor::find_by_user_and_type(user_uuid, TwoFactorType::Email as i32, conn) + let mut twofactor = TwoFactor::find_by_user_and_type(user_id, TwoFactorType::Email as i32, conn) .await .map_res("Two factor not found")?; let Some(issued_token) = &email_data.last_token else { @@ -327,8 +326,8 @@ pub fn obscure_email(email: &str) -> String { format!("{}@{}", new_name, &domain) } -pub async fn find_and_activate_email_2fa(user_uuid: &UserId, conn: &mut DbConn) -> EmptyResult { - if let Some(user) = User::find_by_uuid(user_uuid, conn).await { +pub async fn find_and_activate_email_2fa(user_id: &UserId, conn: &mut DbConn) -> EmptyResult { + if let Some(user) = User::find_by_uuid(user_id, conn).await { activate_email_2fa(&user, conn).await } else { err!("User not found!"); diff --git a/src/api/core/two_factor/mod.rs b/src/api/core/two_factor/mod.rs index c2facd6229..db1133569f 100644 --- a/src/api/core/two_factor/mod.rs +++ b/src/api/core/two_factor/mod.rs @@ -173,7 +173,7 @@ async fn disable_twofactor_put(data: Json, headers: Header pub async fn enforce_2fa_policy( user: &User, - act_uuid: &str, + act_user_id: &str, device_type: i32, ip: &std::net::IpAddr, conn: &mut DbConn, @@ -195,7 +195,7 @@ pub async fn enforce_2fa_policy( EventType::OrganizationUserRevoked as i32, &member.uuid, &member.org_uuid, - act_uuid, + act_user_id, device_type, ip, conn, @@ -208,14 +208,14 @@ pub async fn enforce_2fa_policy( } pub async fn enforce_2fa_policy_for_org( - org_uuid: &OrganizationId, - act_uuid: &str, + org_id: &OrganizationId, + act_user_id: &str, device_type: i32, ip: &std::net::IpAddr, conn: &mut DbConn, ) -> EmptyResult { - let org = Organization::find_by_uuid(org_uuid, conn).await.unwrap(); - for member in Membership::find_confirmed_by_org(org_uuid, conn).await.into_iter() { + let org = Organization::find_by_uuid(org_id, conn).await.unwrap(); + for member in Membership::find_confirmed_by_org(org_id, conn).await.into_iter() { // Don't enforce the policy for Admins and Owners. if member.atype < MembershipType::Admin && TwoFactor::find_by_user(&member.user_uuid, conn).await.is_empty() { if CONFIG.mail_enabled() { @@ -229,8 +229,8 @@ pub async fn enforce_2fa_policy_for_org( log_event( EventType::OrganizationUserRevoked as i32, &member.uuid, - org_uuid, - act_uuid, + org_id, + act_user_id, device_type, ip, conn, diff --git a/src/api/core/two_factor/protected_actions.rs b/src/api/core/two_factor/protected_actions.rs index 775fef735c..5e4a65becb 100644 --- a/src/api/core/two_factor/protected_actions.rs +++ b/src/api/core/two_factor/protected_actions.rs @@ -104,11 +104,11 @@ async fn verify_otp(data: Json, headers: Headers, mut con pub async fn validate_protected_action_otp( otp: &str, - user_uuid: &UserId, + user_id: &UserId, delete_if_valid: bool, conn: &mut DbConn, ) -> EmptyResult { - let pa = TwoFactor::find_by_user_and_type(user_uuid, TwoFactorType::ProtectedActions as i32, conn) + let pa = TwoFactor::find_by_user_and_type(user_id, TwoFactorType::ProtectedActions as i32, conn) .await .map_res("Protected action token not found, try sending the code again or restart the process")?; let mut pa_data = ProtectedActionData::from_json(&pa.data)?; diff --git a/src/api/core/two_factor/webauthn.rs b/src/api/core/two_factor/webauthn.rs index f7a04dd7c1..614c5df326 100644 --- a/src/api/core/two_factor/webauthn.rs +++ b/src/api/core/two_factor/webauthn.rs @@ -352,20 +352,20 @@ async fn delete_webauthn(data: Json, headers: Headers, mut conn: } pub async fn get_webauthn_registrations( - user_uuid: &UserId, + user_id: &UserId, conn: &mut DbConn, ) -> Result<(bool, Vec), Error> { let type_ = TwoFactorType::Webauthn as i32; - match TwoFactor::find_by_user_and_type(user_uuid, type_, conn).await { + match TwoFactor::find_by_user_and_type(user_id, type_, conn).await { Some(tf) => Ok((tf.enabled, serde_json::from_str(&tf.data)?)), None => Ok((false, Vec::new())), // If no data, return empty list } } -pub async fn generate_webauthn_login(user_uuid: &UserId, conn: &mut DbConn) -> JsonResult { +pub async fn generate_webauthn_login(user_id: &UserId, conn: &mut DbConn) -> JsonResult { // Load saved credentials let creds: Vec = - get_webauthn_registrations(user_uuid, conn).await?.1.into_iter().map(|r| r.credential).collect(); + get_webauthn_registrations(user_id, conn).await?.1.into_iter().map(|r| r.credential).collect(); if creds.is_empty() { err!("No Webauthn devices registered") @@ -376,7 +376,7 @@ pub async fn generate_webauthn_login(user_uuid: &UserId, conn: &mut DbConn) -> J let (response, state) = WebauthnConfig::load().generate_challenge_authenticate_options(creds, Some(ext))?; // Save the challenge state for later validation - TwoFactor::new(user_uuid.clone(), TwoFactorType::WebauthnLoginChallenge, serde_json::to_string(&state)?) + TwoFactor::new(user_id.clone(), TwoFactorType::WebauthnLoginChallenge, serde_json::to_string(&state)?) .save(conn) .await?; @@ -384,9 +384,9 @@ pub async fn generate_webauthn_login(user_uuid: &UserId, conn: &mut DbConn) -> J Ok(Json(serde_json::to_value(response.public_key)?)) } -pub async fn validate_webauthn_login(user_uuid: &UserId, response: &str, conn: &mut DbConn) -> EmptyResult { +pub async fn validate_webauthn_login(user_id: &UserId, response: &str, conn: &mut DbConn) -> EmptyResult { let type_ = TwoFactorType::WebauthnLoginChallenge as i32; - let state = match TwoFactor::find_by_user_and_type(user_uuid, type_, conn).await { + let state = match TwoFactor::find_by_user_and_type(user_id, type_, conn).await { Some(tf) => { let state: AuthenticationState = serde_json::from_str(&tf.data)?; tf.delete(conn).await?; @@ -403,7 +403,7 @@ pub async fn validate_webauthn_login(user_uuid: &UserId, response: &str, conn: & let rsp: PublicKeyCredentialCopy = serde_json::from_str(response)?; let rsp: PublicKeyCredential = rsp.into(); - let mut registrations = get_webauthn_registrations(user_uuid, conn).await?.1; + let mut registrations = get_webauthn_registrations(user_id, conn).await?.1; // If the credential we received is migrated from U2F, enable the U2F compatibility //let use_u2f = registrations.iter().any(|r| r.migrated && r.credential.cred_id == rsp.raw_id.0); @@ -413,7 +413,7 @@ pub async fn validate_webauthn_login(user_uuid: &UserId, response: &str, conn: & if ®.credential.cred_id == cred_id { reg.credential.counter = auth_data.counter; - TwoFactor::new(user_uuid.clone(), TwoFactorType::Webauthn, serde_json::to_string(®istrations)?) + TwoFactor::new(user_id.clone(), TwoFactorType::Webauthn, serde_json::to_string(®istrations)?) .save(conn) .await?; return Ok(()); diff --git a/src/api/core/two_factor/yubikey.rs b/src/api/core/two_factor/yubikey.rs index b2940353e6..a6d9898d1d 100644 --- a/src/api/core/two_factor/yubikey.rs +++ b/src/api/core/two_factor/yubikey.rs @@ -92,10 +92,10 @@ async fn generate_yubikey(data: Json, headers: Headers, mut c data.validate(&user, false, &mut conn).await?; - let user_uuid = &user.uuid; + let user_id = &user.uuid; let yubikey_type = TwoFactorType::YubiKey as i32; - let r = TwoFactor::find_by_user_and_type(user_uuid, yubikey_type, &mut conn).await; + let r = TwoFactor::find_by_user_and_type(user_id, yubikey_type, &mut conn).await; if let Some(r) = r { let yubikey_metadata: YubikeyMetadata = serde_json::from_str(&r.data)?; diff --git a/src/api/identity.rs b/src/api/identity.rs index 27bb8ab42c..2f02f481b8 100644 --- a/src/api/identity.rs +++ b/src/api/identity.rs @@ -31,7 +31,7 @@ pub fn routes() -> Vec { async fn login(data: Form, client_header: ClientHeaders, mut conn: DbConn) -> JsonResult { let data: ConnectData = data.into_inner(); - let mut user_uuid: Option = None; + let mut user_id: Option = None; let login_result = match data.grant_type.as_ref() { "refresh_token" => { @@ -48,7 +48,7 @@ async fn login(data: Form, client_header: ClientHeaders, mut conn: _check_is_some(&data.device_name, "device_name cannot be blank")?; _check_is_some(&data.device_type, "device_type cannot be blank")?; - _password_login(data, &mut user_uuid, &mut conn, &client_header.ip).await + _password_login(data, &mut user_id, &mut conn, &client_header.ip).await } "client_credentials" => { _check_is_some(&data.client_id, "client_id cannot be blank")?; @@ -59,17 +59,17 @@ async fn login(data: Form, client_header: ClientHeaders, mut conn: _check_is_some(&data.device_name, "device_name cannot be blank")?; _check_is_some(&data.device_type, "device_type cannot be blank")?; - _api_key_login(data, &mut user_uuid, &mut conn, &client_header.ip).await + _api_key_login(data, &mut user_id, &mut conn, &client_header.ip).await } t => err!("Invalid type", t), }; - if let Some(user_uuid) = user_uuid { + if let Some(user_id) = user_id { match &login_result { Ok(_) => { log_user_event( EventType::UserLoggedIn as i32, - &user_uuid, + &user_id, client_header.device_type, &client_header.ip.ip, &mut conn, @@ -80,7 +80,7 @@ async fn login(data: Form, client_header: ClientHeaders, mut conn: if let Some(ev) = e.get_event() { log_user_event( ev.event as i32, - &user_uuid, + &user_id, client_header.device_type, &client_header.ip.ip, &mut conn, @@ -141,7 +141,7 @@ struct MasterPasswordPolicy { async fn _password_login( data: ConnectData, - user_uuid: &mut Option, + user_id: &mut Option, conn: &mut DbConn, ip: &ClientIp, ) -> JsonResult { @@ -161,8 +161,8 @@ async fn _password_login( err!("Username or password is incorrect. Try again", format!("IP: {}. Username: {}.", ip.ip, username)) }; - // Set the user_uuid here to be passed back used for event logging. - *user_uuid = Some(user.uuid.clone()); + // Set the user_id here to be passed back used for event logging. + *user_id = Some(user.uuid.clone()); // Check if the user is disabled if !user.enabled { @@ -359,7 +359,7 @@ async fn _password_login( async fn _api_key_login( data: ConnectData, - user_uuid: &mut Option, + user_id: &mut Option, conn: &mut DbConn, ip: &ClientIp, ) -> JsonResult { @@ -368,7 +368,7 @@ async fn _api_key_login( // Validate scope match data.scope.as_ref().unwrap().as_ref() { - "api" => _user_api_key_login(data, user_uuid, conn, ip).await, + "api" => _user_api_key_login(data, user_id, conn, ip).await, "api.organization" => _organization_api_key_login(data, conn, ip).await, _ => err!("Scope not supported"), } @@ -376,22 +376,22 @@ async fn _api_key_login( async fn _user_api_key_login( data: ConnectData, - user_uuid: &mut Option, + user_id: &mut Option, conn: &mut DbConn, ip: &ClientIp, ) -> JsonResult { // Get the user via the client_id let client_id = data.client_id.as_ref().unwrap(); - let Some(client_user_uuid) = client_id.strip_prefix("user.") else { + let Some(client_user_id) = client_id.strip_prefix("user.") else { err!("Malformed client_id", format!("IP: {}.", ip.ip)) }; - let client_user_uuid: UserId = client_user_uuid.into(); - let Some(user) = User::find_by_uuid(&client_user_uuid, conn).await else { + let client_user_id: UserId = client_user_id.into(); + let Some(user) = User::find_by_uuid(&client_user_id, conn).await else { err!("Invalid client_id", format!("IP: {}.", ip.ip)) }; - // Set the user_uuid here to be passed back used for event logging. - *user_uuid = Some(user.uuid.clone()); + // Set the user_id here to be passed back used for event logging. + *user_id = Some(user.uuid.clone()); // Check if the user is disabled if !user.enabled { @@ -470,11 +470,11 @@ async fn _user_api_key_login( async fn _organization_api_key_login(data: ConnectData, conn: &mut DbConn, ip: &ClientIp) -> JsonResult { // Get the org via the client_id let client_id = data.client_id.as_ref().unwrap(); - let Some(org_uuid) = client_id.strip_prefix("organization.") else { + let Some(org_id) = client_id.strip_prefix("organization.") else { err!("Malformed client_id", format!("IP: {}.", ip.ip)) }; - let org_uuid: OrganizationId = org_uuid.to_string().into(); - let Some(org_api_key) = OrganizationApiKey::find_by_org_uuid(&org_uuid, conn).await else { + let org_id: OrganizationId = org_id.to_string().into(); + let Some(org_api_key) = OrganizationApiKey::find_by_org_uuid(&org_id, conn).await else { err!("Invalid client_id", format!("IP: {}.", ip.ip)) }; @@ -616,7 +616,7 @@ fn _selected_data(tf: Option) -> ApiResult { async fn _json_err_twofactor( providers: &[i32], - user_uuid: &UserId, + user_id: &UserId, data: &ConnectData, conn: &mut DbConn, ) -> ApiResult { @@ -637,12 +637,12 @@ async fn _json_err_twofactor( Some(TwoFactorType::Authenticator) => { /* Nothing to do for TOTP */ } Some(TwoFactorType::Webauthn) if CONFIG.domain_set() => { - let request = webauthn::generate_webauthn_login(user_uuid, conn).await?; + let request = webauthn::generate_webauthn_login(user_id, conn).await?; result["TwoFactorProviders2"][provider.to_string()] = request.0; } Some(TwoFactorType::Duo) => { - let email = match User::find_by_uuid(user_uuid, conn).await { + let email = match User::find_by_uuid(user_id, conn).await { Some(u) => u.email, None => err!("User does not exist"), }; @@ -674,7 +674,7 @@ async fn _json_err_twofactor( } Some(tf_type @ TwoFactorType::YubiKey) => { - let Some(twofactor) = TwoFactor::find_by_user_and_type(user_uuid, tf_type as i32, conn).await else { + let Some(twofactor) = TwoFactor::find_by_user_and_type(user_id, tf_type as i32, conn).await else { err!("No YubiKey devices registered") }; @@ -686,13 +686,13 @@ async fn _json_err_twofactor( } Some(tf_type @ TwoFactorType::Email) => { - let Some(twofactor) = TwoFactor::find_by_user_and_type(user_uuid, tf_type as i32, conn).await else { + let Some(twofactor) = TwoFactor::find_by_user_and_type(user_id, tf_type as i32, conn).await else { err!("No twofactor email registered") }; // Send email immediately if email is the only 2FA option if providers.len() == 1 { - email::send_token(user_uuid, conn).await? + email::send_token(user_id, conn).await? } let email_data = email::EmailTokenData::from_json(&twofactor.data)?; diff --git a/src/api/notifications.rs b/src/api/notifications.rs index f9b45f00fe..3e26c7a87f 100644 --- a/src/api/notifications.rs +++ b/src/api/notifications.rs @@ -328,8 +328,8 @@ pub struct WebSocketUsers { } impl WebSocketUsers { - async fn send_update(&self, user_uuid: &UserId, data: &[u8]) { - if let Some(user) = self.map.get(user_uuid.as_ref()).map(|v| v.clone()) { + async fn send_update(&self, user_id: &UserId, data: &[u8]) { + if let Some(user) = self.map.get(user_id.as_ref()).map(|v| v.clone()) { for (_, sender) in user.iter() { if let Err(e) = sender.send(Message::binary(data)).await { error!("Error sending WS update {e}"); @@ -413,7 +413,7 @@ impl WebSocketUsers { &self, ut: UpdateType, cipher: &Cipher, - user_uuids: &[UserId], + user_ids: &[UserId], acting_device_uuid: &DeviceId, collection_uuids: Option>, conn: &mut DbConn, @@ -422,10 +422,10 @@ impl WebSocketUsers { if *NOTIFICATIONS_DISABLED { return; } - let org_uuid = convert_option(cipher.organization_uuid.as_deref()); + let org_id = convert_option(cipher.organization_uuid.as_deref()); // Depending if there are collections provided or not, we need to have different values for the following variables. // The user_uuid should be `null`, and the revision date should be set to now, else the clients won't sync the collection change. - let (user_uuid, collection_uuids, revision_date) = if let Some(collection_uuids) = collection_uuids { + let (user_id, collection_uuids, revision_date) = if let Some(collection_uuids) = collection_uuids { ( Value::Nil, Value::Array(collection_uuids.into_iter().map(|v| v.to_string().into()).collect::>()), @@ -438,8 +438,8 @@ impl WebSocketUsers { let data = create_update( vec![ ("Id".into(), cipher.uuid.to_string().into()), - ("UserId".into(), user_uuid), - ("OrganizationId".into(), org_uuid), + ("UserId".into(), user_id), + ("OrganizationId".into(), org_id), ("CollectionIds".into(), collection_uuids), ("RevisionDate".into(), revision_date), ], @@ -448,12 +448,12 @@ impl WebSocketUsers { ); if CONFIG.enable_websocket() { - for uuid in user_uuids { + for uuid in user_ids { self.send_update(uuid, &data).await; } } - if CONFIG.push_enabled() && user_uuids.len() == 1 { + if CONFIG.push_enabled() && user_ids.len() == 1 { push_cipher_update(ut, cipher, acting_device_uuid, conn).await; } } @@ -462,7 +462,7 @@ impl WebSocketUsers { &self, ut: UpdateType, send: &DbSend, - user_uuids: &[UserId], + user_ids: &[UserId], acting_device_uuid: &DeviceId, conn: &mut DbConn, ) { @@ -470,12 +470,12 @@ impl WebSocketUsers { if *NOTIFICATIONS_DISABLED { return; } - let user_uuid = convert_option(send.user_uuid.as_deref()); + let user_id = convert_option(send.user_uuid.as_deref()); let data = create_update( vec![ ("Id".into(), send.uuid.to_string().into()), - ("UserId".into(), user_uuid), + ("UserId".into(), user_id), ("RevisionDate".into(), serialize_date(send.revision_date)), ], ut, @@ -483,18 +483,18 @@ impl WebSocketUsers { ); if CONFIG.enable_websocket() { - for uuid in user_uuids { + for uuid in user_ids { self.send_update(uuid, &data).await; } } - if CONFIG.push_enabled() && user_uuids.len() == 1 { + if CONFIG.push_enabled() && user_ids.len() == 1 { push_send_update(ut, send, acting_device_uuid, conn).await; } } pub async fn send_auth_request( &self, - user_uuid: &UserId, + user_id: &UserId, auth_request_uuid: &String, acting_device_uuid: &DeviceId, conn: &mut DbConn, @@ -504,22 +504,22 @@ impl WebSocketUsers { return; } let data = create_update( - vec![("Id".into(), auth_request_uuid.clone().into()), ("UserId".into(), user_uuid.to_string().into())], + vec![("Id".into(), auth_request_uuid.clone().into()), ("UserId".into(), user_id.to_string().into())], UpdateType::AuthRequest, Some(acting_device_uuid.to_string()), ); if CONFIG.enable_websocket() { - self.send_update(user_uuid, &data).await; + self.send_update(user_id, &data).await; } if CONFIG.push_enabled() { - push_auth_request(user_uuid.clone(), auth_request_uuid.to_string(), conn).await; + push_auth_request(user_id.clone(), auth_request_uuid.to_string(), conn).await; } } pub async fn send_auth_response( &self, - user_uuid: &UserId, + user_id: &UserId, auth_response_uuid: &str, approving_device_uuid: DeviceId, conn: &mut DbConn, @@ -529,16 +529,16 @@ impl WebSocketUsers { return; } let data = create_update( - vec![("Id".into(), auth_response_uuid.to_owned().into()), ("UserId".into(), user_uuid.to_string().into())], + vec![("Id".into(), auth_response_uuid.to_owned().into()), ("UserId".into(), user_id.to_string().into())], UpdateType::AuthRequestResponse, Some(approving_device_uuid.to_string()), ); if CONFIG.enable_websocket() { - self.send_update(user_uuid, &data).await; + self.send_update(user_id, &data).await; } if CONFIG.push_enabled() { - push_auth_response(user_uuid.clone(), auth_response_uuid.to_string(), approving_device_uuid, conn).await; + push_auth_response(user_id.clone(), auth_response_uuid.to_string(), approving_device_uuid, conn).await; } } } @@ -557,16 +557,16 @@ impl AnonymousWebSocketSubscriptions { } } - pub async fn send_auth_response(&self, user_uuid: &UserId, auth_response_uuid: &str) { + pub async fn send_auth_response(&self, user_id: &UserId, auth_response_uuid: &str) { if !CONFIG.enable_websocket() { return; } let data = create_anonymous_update( - vec![("Id".into(), auth_response_uuid.to_owned().into()), ("UserId".into(), user_uuid.to_string().into())], + vec![("Id".into(), auth_response_uuid.to_owned().into()), ("UserId".into(), user_id.to_string().into())], UpdateType::AuthRequestResponse, - user_uuid.clone(), + user_id.clone(), ); - self.send_update(user_uuid, &data).await; + self.send_update(user_id, &data).await; } } diff --git a/src/api/push.rs b/src/api/push.rs index a1a3a4ba69..0fda9b3a6c 100644 --- a/src/api/push.rs +++ b/src/api/push.rs @@ -126,15 +126,15 @@ pub async fn register_push_device(device: &mut Device, conn: &mut crate::db::DbC Ok(()) } -pub async fn unregister_push_device(push_uuid: Option) -> EmptyResult { - if !CONFIG.push_enabled() || push_uuid.is_none() { +pub async fn unregister_push_device(push_id: Option) -> EmptyResult { + if !CONFIG.push_enabled() || push_id.is_none() { return Ok(()); } let auth_push_token = get_auth_push_token().await?; let auth_header = format!("Bearer {}", &auth_push_token); - match make_http_request(Method::DELETE, &(CONFIG.push_relay_uri() + "/push/" + &push_uuid.unwrap()))? + match make_http_request(Method::DELETE, &(CONFIG.push_relay_uri() + "/push/" + &push_id.unwrap()))? .header(AUTHORIZATION, auth_header) .send() .await @@ -148,24 +148,24 @@ pub async fn unregister_push_device(push_uuid: Option) -> EmptyResult { pub async fn push_cipher_update( ut: UpdateType, cipher: &Cipher, - acting_device_uuid: &DeviceId, + acting_device_id: &DeviceId, conn: &mut crate::db::DbConn, ) { // We shouldn't send a push notification on cipher update if the cipher belongs to an organization, this isn't implemented in the upstream server too. if cipher.organization_uuid.is_some() { return; }; - let Some(user_uuid) = &cipher.user_uuid else { + let Some(user_id) = &cipher.user_uuid else { debug!("Cipher has no uuid"); return; }; - if Device::check_user_has_push_device(user_uuid, conn).await { + if Device::check_user_has_push_device(user_id, conn).await { send_to_push_relay(json!({ - "userId": user_uuid, + "userId": user_id, "organizationId": (), - "deviceId": acting_device_uuid, - "identifier": acting_device_uuid, + "deviceId": acting_device_id, + "identifier": acting_device_id, "type": ut as i32, "payload": { "Id": cipher.uuid, @@ -178,14 +178,14 @@ pub async fn push_cipher_update( } } -pub fn push_logout(user: &User, acting_device_uuid: Option) { - let acting_device_uuid: Value = acting_device_uuid.map(|v| v.into()).unwrap_or_else(|| Value::Null); +pub fn push_logout(user: &User, acting_device_id: Option) { + let acting_device_id: Value = acting_device_id.map(|v| v.into()).unwrap_or_else(|| Value::Null); tokio::task::spawn(send_to_push_relay(json!({ "userId": user.uuid, "organizationId": (), - "deviceId": acting_device_uuid, - "identifier": acting_device_uuid, + "deviceId": acting_device_id, + "identifier": acting_device_id, "type": UpdateType::LogOut as i32, "payload": { "UserId": user.uuid, @@ -211,15 +211,15 @@ pub fn push_user_update(ut: UpdateType, user: &User) { pub async fn push_folder_update( ut: UpdateType, folder: &Folder, - acting_device_uuid: &DeviceId, + acting_device_id: &DeviceId, conn: &mut crate::db::DbConn, ) { if Device::check_user_has_push_device(&folder.user_uuid, conn).await { tokio::task::spawn(send_to_push_relay(json!({ "userId": folder.user_uuid, "organizationId": (), - "deviceId": acting_device_uuid, - "identifier": acting_device_uuid, + "deviceId": acting_device_id, + "identifier": acting_device_id, "type": ut as i32, "payload": { "Id": folder.uuid, @@ -230,19 +230,14 @@ pub async fn push_folder_update( } } -pub async fn push_send_update( - ut: UpdateType, - send: &Send, - acting_device_uuid: &DeviceId, - conn: &mut crate::db::DbConn, -) { +pub async fn push_send_update(ut: UpdateType, send: &Send, acting_device_id: &DeviceId, conn: &mut crate::db::DbConn) { if let Some(s) = &send.user_uuid { if Device::check_user_has_push_device(s, conn).await { tokio::task::spawn(send_to_push_relay(json!({ "userId": send.user_uuid, "organizationId": (), - "deviceId": acting_device_uuid, - "identifier": acting_device_uuid, + "deviceId": acting_device_id, + "identifier": acting_device_id, "type": ut as i32, "payload": { "Id": send.uuid, @@ -289,38 +284,38 @@ async fn send_to_push_relay(notification_data: Value) { }; } -pub async fn push_auth_request(user_uuid: UserId, auth_request_uuid: String, conn: &mut crate::db::DbConn) { - if Device::check_user_has_push_device(&user_uuid, conn).await { +pub async fn push_auth_request(user_id: UserId, auth_request_id: String, conn: &mut crate::db::DbConn) { + if Device::check_user_has_push_device(&user_id, conn).await { tokio::task::spawn(send_to_push_relay(json!({ - "userId": user_uuid, + "userId": user_id, "organizationId": (), "deviceId": null, "identifier": null, "type": UpdateType::AuthRequest as i32, "payload": { - "Id": auth_request_uuid, - "UserId": user_uuid, + "Id": auth_request_id, + "UserId": user_id, } }))); } } pub async fn push_auth_response( - user_uuid: UserId, - auth_request_uuid: String, - approving_device_uuid: DeviceId, + user_id: UserId, + auth_request_id: String, + approving_device_id: DeviceId, conn: &mut crate::db::DbConn, ) { - if Device::check_user_has_push_device(&user_uuid, conn).await { + if Device::check_user_has_push_device(&user_id, conn).await { tokio::task::spawn(send_to_push_relay(json!({ - "userId": user_uuid, + "userId": user_id, "organizationId": (), - "deviceId": approving_device_uuid, - "identifier": approving_device_uuid, + "deviceId": approving_device_id, + "identifier": approving_device_id, "type": UpdateType::AuthRequestResponse as i32, "payload": { - "Id": auth_request_uuid, - "UserId": user_uuid, + "Id": auth_request_id, + "UserId": user_id, } }))); } diff --git a/src/api/web.rs b/src/api/web.rs index 608af26d4c..b0426bb84a 100644 --- a/src/api/web.rs +++ b/src/api/web.rs @@ -196,16 +196,16 @@ async fn web_files(p: PathBuf) -> Cached> { Cached::long(NamedFile::open(Path::new(&CONFIG.web_vault_folder()).join(p)).await.ok(), true) } -#[get("/attachments//?")] -async fn attachments(uuid: CipherId, file_id: AttachmentId, token: String) -> Option { +#[get("/attachments//?")] +async fn attachments(cipher_id: CipherId, file_id: AttachmentId, token: String) -> Option { let Ok(claims) = decode_file_download(&token) else { return None; }; - if claims.sub != uuid || claims.file_id != file_id { + if claims.sub != cipher_id || claims.file_id != file_id { return None; } - NamedFile::open(Path::new(&CONFIG.attachments_folder()).join(uuid.as_ref()).join(file_id.as_ref())).await.ok() + NamedFile::open(Path::new(&CONFIG.attachments_folder()).join(cipher_id.as_ref()).join(file_id.as_ref())).await.ok() } // We use DbConn here to let the alive healthcheck also verify the database connection. diff --git a/src/auth.rs b/src/auth.rs index 090aebe987..969d5211bb 100644 --- a/src/auth.rs +++ b/src/auth.rs @@ -482,19 +482,19 @@ impl<'r> FromRequest<'r> for Headers { err_handler!("Invalid claim") }; - let device_uuid = claims.device; - let user_uuid = claims.sub; + let device_id = claims.device; + let user_id = claims.sub; let mut conn = match DbConn::from_request(request).await { Outcome::Success(conn) => conn, _ => err_handler!("Error getting DB"), }; - let Some(device) = Device::find_by_uuid_and_user(&device_uuid, &user_uuid, &mut conn).await else { + let Some(device) = Device::find_by_uuid_and_user(&device_id, &user_id, &mut conn).await else { err_handler!("Invalid device id") }; - let Some(user) = User::find_by_uuid(&user_uuid, &mut conn).await else { + let Some(user) = User::find_by_uuid(&user_id, &mut conn).await else { err_handler!("Device has no user associated") }; From 3d42b66a1054c3b5336b92153717d6cce21c4446 Mon Sep 17 00:00:00 2001 From: Stefan Melmuk Date: Wed, 25 Dec 2024 02:53:22 +0100 Subject: [PATCH 21/25] add collection_membership type --- src/api/core/organizations.rs | 37 +++++++++++++++--------------- src/db/models/collection.rs | 43 +++++++++++++++++++++++++++-------- 2 files changed, 53 insertions(+), 27 deletions(-) diff --git a/src/api/core/organizations.rs b/src/api/core/organizations.rs index 35297dcc54..28eef5e035 100644 --- a/src/api/core/organizations.rs +++ b/src/api/core/organizations.rs @@ -43,13 +43,13 @@ pub fn routes() -> Vec { get_org_details, get_members, send_invite, - reinvite_user, - bulk_reinvite_user, + reinvite_member, + bulk_reinvite_member, confirm_invite, bulk_confirm_invite, accept_invite, get_user, - edit_user, + edit_membership, put_membership, delete_user, bulk_delete_user, @@ -340,7 +340,8 @@ async fn get_org_collections_details( }; // get all collection memberships for the current organization - let col_users = CollectionUser::find_by_organization(&org_id, &mut conn).await; + // NOTE: the loaded col_users have a MembershipId + let col_users = CollectionUser::find_by_organization_swap_user_uuid_with_member_uuid(&org_id, &mut conn).await; // check if current user has full access to the organization (either directly or via any group) let has_full_access_to_org = member.access_all @@ -688,9 +689,9 @@ async fn get_collection_users( err!("Collection not found in Organization") }; - let mut user_list = Vec::new(); + let mut member_list = Vec::new(); for col_user in CollectionUser::find_by_collection(&collection.uuid, &mut conn).await { - user_list.push( + member_list.push( Membership::find_by_user_and_org(&col_user.user_uuid, &org_id, &mut conn) .await .unwrap() @@ -698,7 +699,7 @@ async fn get_collection_users( ); } - Ok(Json(json!(user_list))) + Ok(Json(json!(member_list))) } #[put("/organizations//collections//users", data = "")] @@ -972,7 +973,7 @@ async fn send_invite( } #[post("/organizations//users/reinvite", data = "")] -async fn bulk_reinvite_user( +async fn bulk_reinvite_member( org_id: OrganizationId, data: Json, headers: AdminHeaders, @@ -982,7 +983,7 @@ async fn bulk_reinvite_user( let mut bulk_response = Vec::new(); for member_id in data.ids { - let err_msg = match _reinvite_user(&org_id, &member_id, &headers.user.email, &mut conn).await { + let err_msg = match _reinvite_member(&org_id, &member_id, &headers.user.email, &mut conn).await { Ok(_) => String::new(), Err(e) => format!("{e:?}"), }; @@ -1004,16 +1005,16 @@ async fn bulk_reinvite_user( } #[post("/organizations//users//reinvite")] -async fn reinvite_user( +async fn reinvite_member( org_id: OrganizationId, member_id: MembershipId, headers: AdminHeaders, mut conn: DbConn, ) -> EmptyResult { - _reinvite_user(&org_id, &member_id, &headers.user.email, &mut conn).await + _reinvite_member(&org_id, &member_id, &headers.user.email, &mut conn).await } -async fn _reinvite_user( +async fn _reinvite_member( org_id: &OrganizationId, member_id: &MembershipId, invited_by_email: &str, @@ -1104,7 +1105,7 @@ async fn accept_invite( err!("Reset password key is required, but not provided."); } - // This check is also done at accept_invite(), _confirm_invite, _activate_user(), edit_user(), admin::update_membership_type + // This check is also done at accept_invite(), _confirm_invite, _activate_membership(), edit_membership(), admin::update_membership_type // It returns different error messages per function. if member.atype < MembershipType::Admin { match OrgPolicy::is_user_allowed(&member.user_uuid, &org_id, false, &mut conn).await { @@ -1245,7 +1246,7 @@ async fn _confirm_invite( err!("User in invalid state") } - // This check is also done at accept_invite(), _confirm_invite, _activate_user(), edit_user(), admin::update_membership_type + // This check is also done at accept_invite(), _confirm_invite, _activate_membership(), edit_membership(), admin::update_membership_type // It returns different error messages per function. if member_to_confirm.atype < MembershipType::Admin { match OrgPolicy::is_user_allowed(&member_to_confirm.user_uuid, org_id, true, conn).await { @@ -1336,11 +1337,11 @@ async fn put_membership( headers: AdminHeaders, conn: DbConn, ) -> EmptyResult { - edit_user(org_id, member_id, data, headers, conn).await + edit_membership(org_id, member_id, data, headers, conn).await } #[post("/organizations//users/", data = "", rank = 1)] -async fn edit_user( +async fn edit_membership( org_id: OrganizationId, member_id: MembershipId, data: Json, @@ -1378,7 +1379,7 @@ async fn edit_user( } } - // This check is also done at accept_invite(), _confirm_invite, _activate_user(), edit_user(), admin::update_membership_type + // This check is also done at accept_invite(), _confirm_invite, _activate_membership(), edit_membership(), admin::update_membership_type // It returns different error messages per function. if new_type < MembershipType::Admin { match OrgPolicy::is_user_allowed(&member_to_edit.user_uuid, &org_id, true, &mut conn).await { @@ -2282,7 +2283,7 @@ async fn _restore_membership( err!("Only owners can restore other owners") } - // This check is also done at accept_invite(), _confirm_invite, _activate_user(), edit_user(), admin::update_membership_type + // This check is also done at accept_invite(), _confirm_invite, _activate_membership(), edit_membership(), admin::update_membership_type // It returns different error messages per function. if member.atype < MembershipType::Admin { match OrgPolicy::is_user_allowed(&member.user_uuid, org_id, false, conn).await { diff --git a/src/db/models/collection.rs b/src/db/models/collection.rs index 7d94f9d54e..94d30fcc7d 100644 --- a/src/db/models/collection.rs +++ b/src/db/models/collection.rs @@ -3,7 +3,8 @@ use rocket::request::FromParam; use serde_json::Value; use super::{ - CipherId, CollectionGroup, GroupUser, Membership, MembershipStatus, MembershipType, OrganizationId, User, UserId, + CipherId, CollectionGroup, GroupUser, Membership, MembershipId, MembershipStatus, MembershipType, OrganizationId, + User, UserId, }; use crate::CONFIG; @@ -527,8 +528,11 @@ impl CollectionUser { }} } - pub async fn find_by_organization(org_uuid: &OrganizationId, conn: &mut DbConn) -> Vec { - db_run! { conn: { + pub async fn find_by_organization_swap_user_uuid_with_member_uuid( + org_uuid: &OrganizationId, + conn: &mut DbConn, + ) -> Vec { + let col_users = db_run! { conn: { users_collections::table .inner_join(collections::table.on(collections::uuid.eq(users_collections::collection_uuid))) .filter(collections::org_uuid.eq(org_uuid)) @@ -537,7 +541,8 @@ impl CollectionUser { .load::(conn) .expect("Error loading users_collections") .from_db() - }} + }}; + col_users.into_iter().map(|c| c.into()).collect() } pub async fn save( @@ -626,8 +631,8 @@ impl CollectionUser { pub async fn find_by_collection_swap_user_uuid_with_member_uuid( collection_uuid: &CollectionId, conn: &mut DbConn, - ) -> Vec { - db_run! { conn: { + ) -> Vec { + let col_users = db_run! { conn: { users_collections::table .filter(users_collections::collection_uuid.eq(collection_uuid)) .inner_join(users_organizations::table.on(users_organizations::user_uuid.eq(users_collections::user_uuid))) @@ -635,7 +640,8 @@ impl CollectionUser { .load::(conn) .expect("Error loading users_collections") .from_db() - }} + }}; + col_users.into_iter().map(|c| c.into()).collect() } pub async fn find_by_collection_and_user( @@ -775,10 +781,18 @@ impl CollectionCipher { } } -impl CollectionUser { +// Added in case we need the membership_uuid instead of the user_uuid +pub struct CollectionMembership { + pub membership_uuid: MembershipId, + pub collection_uuid: CollectionId, + pub read_only: bool, + pub hide_passwords: bool, +} + +impl CollectionMembership { pub fn to_json_details_for_user(&self) -> Value { json!({ - "id": self.user_uuid, + "id": self.membership_uuid, "readOnly": self.read_only, "hidePasswords": self.hide_passwords, "manage": false @@ -786,6 +800,17 @@ impl CollectionUser { } } +impl From for CollectionMembership { + fn from(c: CollectionUser) -> Self { + Self { + membership_uuid: c.user_uuid.to_string().into(), + collection_uuid: c.collection_uuid, + read_only: c.read_only, + hide_passwords: c.hide_passwords, + } + } +} + #[derive( Clone, Debug, AsRef, Deref, DieselNewType, Display, From, FromForm, Hash, PartialEq, Eq, Serialize, Deserialize, )] From 72b51e00822cb00ff01401269635675655e8a4dd Mon Sep 17 00:00:00 2001 From: Stefan Melmuk Date: Wed, 25 Dec 2024 05:08:42 +0100 Subject: [PATCH 22/25] add auth_request_id newtype --- src/api/core/accounts.rs | 27 +++++++++++++++------------ src/api/identity.rs | 7 +++---- src/db/models/auth_request.rs | 28 ++++++++++++++++++++++++---- src/db/models/mod.rs | 2 +- 4 files changed, 43 insertions(+), 21 deletions(-) diff --git a/src/api/core/accounts.rs b/src/api/core/accounts.rs index 436bbf2d4c..1f114497d2 100644 --- a/src/api/core/accounts.rs +++ b/src/api/core/accounts.rs @@ -1189,16 +1189,17 @@ async fn post_auth_request( }))) } -#[get("/auth-requests/")] -async fn get_auth_request(uuid: &str, headers: Headers, mut conn: DbConn) -> JsonResult { - let Some(auth_request) = AuthRequest::find_by_uuid_and_user(uuid, &headers.user.uuid, &mut conn).await else { +#[get("/auth-requests/")] +async fn get_auth_request(auth_request_id: AuthRequestId, headers: Headers, mut conn: DbConn) -> JsonResult { + let Some(auth_request) = AuthRequest::find_by_uuid_and_user(&auth_request_id, &headers.user.uuid, &mut conn).await + else { err!("AuthRequest doesn't exist", "Record not found or user uuid does not match") }; let response_date_utc = auth_request.response_date.map(|response_date| format_date(&response_date)); Ok(Json(json!({ - "id": uuid, + "id": &auth_request_id, "publicKey": auth_request.public_key, "requestDeviceType": DeviceType::from_i32(auth_request.device_type).to_string(), "requestIpAddress": auth_request.request_ip, @@ -1221,9 +1222,9 @@ struct AuthResponseRequest { request_approved: bool, } -#[put("/auth-requests/", data = "")] +#[put("/auth-requests/", data = "")] async fn put_auth_request( - uuid: &str, + auth_request_id: AuthRequestId, data: Json, headers: Headers, mut conn: DbConn, @@ -1231,7 +1232,9 @@ async fn put_auth_request( nt: Notify<'_>, ) -> JsonResult { let data = data.into_inner(); - let Some(mut auth_request) = AuthRequest::find_by_uuid_and_user(uuid, &headers.user.uuid, &mut conn).await else { + let Some(mut auth_request) = + AuthRequest::find_by_uuid_and_user(&auth_request_id, &headers.user.uuid, &mut conn).await + else { err!("AuthRequest doesn't exist", "Record not found or user uuid does not match") }; @@ -1258,7 +1261,7 @@ async fn put_auth_request( } Ok(Json(json!({ - "id": uuid, + "id": &auth_request_id, "publicKey": auth_request.public_key, "requestDeviceType": DeviceType::from_i32(auth_request.device_type).to_string(), "requestIpAddress": auth_request.request_ip, @@ -1272,14 +1275,14 @@ async fn put_auth_request( }))) } -#[get("/auth-requests//response?")] +#[get("/auth-requests//response?")] async fn get_auth_request_response( - uuid: &str, + auth_request_id: AuthRequestId, code: &str, client_headers: ClientHeaders, mut conn: DbConn, ) -> JsonResult { - let Some(auth_request) = AuthRequest::find_by_uuid(uuid, &mut conn).await else { + let Some(auth_request) = AuthRequest::find_by_uuid(&auth_request_id, &mut conn).await else { err!("AuthRequest doesn't exist", "User not found") }; @@ -1293,7 +1296,7 @@ async fn get_auth_request_response( let response_date_utc = auth_request.response_date.map(|response_date| format_date(&response_date)); Ok(Json(json!({ - "id": uuid, + "id": &auth_request_id, "publicKey": auth_request.public_key, "requestDeviceType": DeviceType::from_i32(auth_request.device_type).to_string(), "requestIpAddress": auth_request.request_ip, diff --git a/src/api/identity.rs b/src/api/identity.rs index 2f02f481b8..38cdfce583 100644 --- a/src/api/identity.rs +++ b/src/api/identity.rs @@ -178,9 +178,8 @@ async fn _password_login( let password = data.password.as_ref().unwrap(); // If we get an auth request, we don't check the user's password, but the access code of the auth request - if let Some(ref auth_request_uuid) = data.auth_request { - let Some(auth_request) = AuthRequest::find_by_uuid_and_user(auth_request_uuid.as_str(), &user.uuid, conn).await - else { + if let Some(ref auth_request_id) = data.auth_request { + let Some(auth_request) = AuthRequest::find_by_uuid_and_user(auth_request_id, &user.uuid, conn).await else { err!( "Auth request not found. Try again.", format!("IP: {}. Username: {}.", ip.ip, username), @@ -770,7 +769,7 @@ struct ConnectData { #[field(name = uncased("twofactorremember"))] two_factor_remember: Option, #[field(name = uncased("authrequest"))] - auth_request: Option, + auth_request: Option, } fn _check_is_some(value: &Option, msg: &str) -> EmptyResult { diff --git a/src/db/models/auth_request.rs b/src/db/models/auth_request.rs index eab91d87ab..3417d07eca 100644 --- a/src/db/models/auth_request.rs +++ b/src/db/models/auth_request.rs @@ -1,6 +1,8 @@ use super::{DeviceId, OrganizationId, UserId}; use crate::crypto::ct_eq; use chrono::{NaiveDateTime, Utc}; +use derive_more::{AsRef, Deref, Display, From}; +use rocket::request::FromParam; db_object! { #[derive(Debug, Identifiable, Queryable, Insertable, AsChangeset, Deserialize, Serialize)] @@ -8,7 +10,7 @@ db_object! { #[diesel(treat_none_as_null = true)] #[diesel(primary_key(uuid))] pub struct AuthRequest { - pub uuid: String, + pub uuid: AuthRequestId, pub user_uuid: UserId, pub organization_uuid: Option, @@ -44,7 +46,7 @@ impl AuthRequest { let now = Utc::now().naive_utc(); Self { - uuid: crate::util::get_uuid(), + uuid: AuthRequestId(crate::util::get_uuid()), user_uuid, organization_uuid: None, @@ -102,7 +104,7 @@ impl AuthRequest { } } - pub async fn find_by_uuid(uuid: &str, conn: &mut DbConn) -> Option { + pub async fn find_by_uuid(uuid: &AuthRequestId, conn: &mut DbConn) -> Option { db_run! {conn: { auth_requests::table .filter(auth_requests::uuid.eq(uuid)) @@ -112,7 +114,7 @@ impl AuthRequest { }} } - pub async fn find_by_uuid_and_user(uuid: &str, user_uuid: &UserId, conn: &mut DbConn) -> Option { + pub async fn find_by_uuid_and_user(uuid: &AuthRequestId, user_uuid: &UserId, conn: &mut DbConn) -> Option { db_run! {conn: { auth_requests::table .filter(auth_requests::uuid.eq(uuid)) @@ -158,3 +160,21 @@ impl AuthRequest { } } } + +#[derive( + Clone, Debug, AsRef, Deref, DieselNewType, Display, From, FromForm, Hash, PartialEq, Eq, Serialize, Deserialize, +)] +pub struct AuthRequestId(String); + +impl<'r> FromParam<'r> for AuthRequestId { + 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 b6691e7f08..d503354c1e 100644 --- a/src/db/models/mod.rs +++ b/src/db/models/mod.rs @@ -17,7 +17,7 @@ mod two_factor_incomplete; mod user; pub use self::attachment::{Attachment, AttachmentId}; -pub use self::auth_request::AuthRequest; +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}; From bc7092767031fb9d3157791a380d1e15d512e04b Mon Sep 17 00:00:00 2001 From: Stefan Melmuk Date: Wed, 25 Dec 2024 05:30:11 +0100 Subject: [PATCH 23/25] use device_id --- src/api/core/accounts.rs | 33 +++++++++++++++++++-------------- 1 file changed, 19 insertions(+), 14 deletions(-) diff --git a/src/api/core/accounts.rs b/src/api/core/accounts.rs index 1f114497d2..bb6e4bbf52 100644 --- a/src/api/core/accounts.rs +++ b/src/api/core/accounts.rs @@ -1069,26 +1069,31 @@ struct PushToken { push_token: String, } -#[post("/devices/identifier//token", data = "")] -async fn post_device_token(uuid: &str, data: Json, headers: Headers, conn: DbConn) -> EmptyResult { - put_device_token(uuid, data, headers, conn).await +#[post("/devices/identifier//token", data = "")] +async fn post_device_token(device_id: DeviceId, data: Json, headers: Headers, conn: DbConn) -> EmptyResult { + put_device_token(device_id, data, headers, conn).await } -#[put("/devices/identifier//token", data = "")] -async fn put_device_token(uuid: &str, data: Json, headers: Headers, mut conn: DbConn) -> EmptyResult { +#[put("/devices/identifier//token", data = "")] +async fn put_device_token( + device_id: DeviceId, + data: Json, + headers: Headers, + mut conn: DbConn, +) -> EmptyResult { let data = data.into_inner(); let token = data.push_token; let Some(mut device) = Device::find_by_uuid_and_user(&headers.device.uuid, &headers.user.uuid, &mut conn).await else { - err!(format!("Error: device {uuid} should be present before a token can be assigned")) + err!(format!("Error: device {device_id} should be present before a token can be assigned")) }; // if the device already has been registered if device.is_registered() { // check if the new token is the same as the registered token if device.push_token.is_some() && device.push_token.unwrap() == token.clone() { - debug!("Device {} is already registered and token is the same", uuid); + debug!("Device {} is already registered and token is the same", device_id); return Ok(()); } else { // Try to unregister already registered device @@ -1107,8 +1112,8 @@ async fn put_device_token(uuid: &str, data: Json, headers: Headers, m Ok(()) } -#[put("/devices/identifier//clear-token")] -async fn put_clear_device_token(uuid: DeviceId, mut conn: DbConn) -> EmptyResult { +#[put("/devices/identifier//clear-token")] +async fn put_clear_device_token(device_id: DeviceId, mut conn: DbConn) -> EmptyResult { // This only clears push token // https://github.com/bitwarden/core/blob/master/src/Api/Controllers/DevicesController.cs#L109 // https://github.com/bitwarden/core/blob/master/src/Core/Services/Implementations/DeviceService.cs#L37 @@ -1117,8 +1122,8 @@ async fn put_clear_device_token(uuid: DeviceId, mut conn: DbConn) -> EmptyResult return Ok(()); } - if let Some(device) = Device::find_by_uuid(&uuid, &mut conn).await { - Device::clear_push_token_by_uuid(&uuid, &mut conn).await?; + if let Some(device) = Device::find_by_uuid(&device_id, &mut conn).await { + Device::clear_push_token_by_uuid(&device_id, &mut conn).await?; unregister_push_device(device.push_uuid).await?; } @@ -1126,9 +1131,9 @@ async fn put_clear_device_token(uuid: DeviceId, mut conn: DbConn) -> EmptyResult } // On upstream server, both PUT and POST are declared. Implementing the POST method in case it would be useful somewhere -#[post("/devices/identifier//clear-token")] -async fn post_clear_device_token(uuid: DeviceId, conn: DbConn) -> EmptyResult { - put_clear_device_token(uuid, conn).await +#[post("/devices/identifier//clear-token")] +async fn post_clear_device_token(device_id: DeviceId, conn: DbConn) -> EmptyResult { + put_clear_device_token(device_id, conn).await } #[derive(Debug, Deserialize)] From c0bf5162a8a1b3f917d7b9c86e03c011d3ed9065 Mon Sep 17 00:00:00 2001 From: Stefan Melmuk Date: Wed, 25 Dec 2024 05:30:35 +0100 Subject: [PATCH 24/25] use org_id in events --- src/api/core/events.rs | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/src/api/core/events.rs b/src/api/core/events.rs index 4083f4475b..2b8acdfde2 100644 --- a/src/api/core/events.rs +++ b/src/api/core/events.rs @@ -31,7 +31,12 @@ struct EventRange { // Upstream: https://github.com/bitwarden/server/blob/9ecf69d9cabce732cf2c57976dd9afa5728578fb/src/Api/Controllers/EventsController.cs#LL84C35-L84C41 #[get("/organizations//events?")] -async fn get_org_events(org_id: &str, data: EventRange, _headers: AdminHeaders, mut conn: DbConn) -> JsonResult { +async fn get_org_events( + org_id: OrganizationId, + data: EventRange, + _headers: AdminHeaders, + mut conn: DbConn, +) -> JsonResult { // Return an empty vec when we org events are disabled. // This prevents client errors let events_json: Vec = if !CONFIG.org_events_enabled() { @@ -44,7 +49,7 @@ async fn get_org_events(org_id: &str, data: EventRange, _headers: AdminHeaders, parse_date(&data.end) }; - Event::find_by_organization_uuid(org_id, &start_date, &end_date, &mut conn) + Event::find_by_organization_uuid(&org_id, &start_date, &end_date, &mut conn) .await .iter() .map(|e| e.to_json()) @@ -92,7 +97,7 @@ async fn get_cipher_events(cipher_id: CipherId, data: EventRange, headers: Heade #[get("/organizations//users//events?")] async fn get_user_events( - org_id: &str, + org_id: OrganizationId, member_id: MembershipId, data: EventRange, _headers: AdminHeaders, @@ -110,7 +115,7 @@ async fn get_user_events( parse_date(&data.end) }; - Event::find_by_org_and_member(org_id, &member_id, &start_date, &end_date, &mut conn) + Event::find_by_org_and_member(&org_id, &member_id, &start_date, &end_date, &mut conn) .await .iter() .map(|e| e.to_json()) From 6d34f0630cfe050e8db67e74eed6dadbbf37234b Mon Sep 17 00:00:00 2001 From: Stefan Melmuk Date: Wed, 25 Dec 2024 05:48:33 +0100 Subject: [PATCH 25/25] add emergency_access_id --- src/api/core/accounts.rs | 6 +++--- src/db/models/emergency_access.rs | 10 ++++++++-- src/db/models/mod.rs | 2 +- 3 files changed, 12 insertions(+), 6 deletions(-) diff --git a/src/api/core/accounts.rs b/src/api/core/accounts.rs index bb6e4bbf52..4b2cc94410 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/db/models/emergency_access.rs b/src/db/models/emergency_access.rs index 429092eaf4..3480d9212b 100644 --- a/src/db/models/emergency_access.rs +++ b/src/db/models/emergency_access.rs @@ -1,4 +1,5 @@ use chrono::{NaiveDateTime, Utc}; +use derive_more::{AsRef, Deref, Display, From}; use serde_json::Value; use crate::{api::EmptyResult, db::DbConn, error::MapResult}; @@ -11,7 +12,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 +34,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), @@ -349,3 +350,8 @@ impl EmergencyAccess { } // endregion + +#[derive( + Clone, Debug, AsRef, Deref, DieselNewType, Display, From, FromForm, Hash, PartialEq, Eq, Serialize, Deserialize, +)] +pub struct EmergencyAccessId(String); diff --git a/src/db/models/mod.rs b/src/db/models/mod.rs index d503354c1e..a78c4a9ab2 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};