From c2a8fbb2aad7435d58dd306c2327254f335e619a Mon Sep 17 00:00:00 2001 From: Stefan Melmuk Date: Sat, 21 Dec 2024 07:28:00 +0100 Subject: [PATCH] use newtype pattern --- Cargo.lock | 34 + Cargo.toml | 3 + src/api/admin.rs | 64 +- src/api/core/accounts.rs | 137 +-- src/api/core/ciphers.rs | 470 +++++----- src/api/core/emergency_access.rs | 75 +- src/api/core/events.rs | 83 +- src/api/core/folders.rs | 38 +- src/api/core/organizations.rs | 898 ++++++++++--------- src/api/core/public.rs | 7 +- src/api/core/sends.rs | 82 +- src/api/core/two_factor/authenticator.rs | 18 +- src/api/core/two_factor/duo.rs | 6 +- src/api/core/two_factor/duo_oidc.rs | 6 +- src/api/core/two_factor/email.rs | 15 +- src/api/core/two_factor/mod.rs | 16 +- src/api/core/two_factor/protected_actions.rs | 6 +- src/api/core/two_factor/webauthn.rs | 22 +- src/api/core/two_factor/yubikey.rs | 4 +- src/api/identity.rs | 59 +- src/api/notifications.rs | 121 ++- src/api/push.rs | 68 +- src/api/web.rs | 11 +- src/auth.rs | 85 +- src/crypto.rs | 9 +- src/db/models/attachment.rs | 51 +- src/db/models/auth_request.rs | 43 +- src/db/models/cipher.rs | 163 ++-- src/db/models/collection.rs | 169 +++- src/db/models/device.rs | 108 ++- src/db/models/emergency_access.rs | 71 +- src/db/models/event.rs | 41 +- src/db/models/favorite.rs | 23 +- src/db/models/folder.rs | 62 +- src/db/models/group.rs | 127 ++- src/db/models/mod.rs | 30 +- src/db/models/org_policy.rs | 50 +- src/db/models/organization.rs | 183 +++- src/db/models/send.rs | 84 +- src/db/models/two_factor.rs | 18 +- src/db/models/two_factor_incomplete.rs | 35 +- src/db/models/user.rs | 56 +- src/mail.rs | 46 +- src/main.rs | 2 + src/util.rs | 39 +- 45 files changed, 2207 insertions(+), 1531 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index d0994e37a9..03512c36a3 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" @@ -778,6 +799,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" @@ -3979,7 +4011,9 @@ dependencies = [ "dashmap", "data-encoding", "data-url", + "derive_more", "diesel", + "diesel-derive-newtype", "diesel_logger", "diesel_migrations", "dotenvy", diff --git a/Cargo.toml b/Cargo.toml index 374f58a0d9..9ab1baad67 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -78,6 +78,9 @@ 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 libsqlite3-sys = { version = "0.30.1", features = ["bundled"], optional = true } diff --git a/src/api/admin.rs b/src/api/admin.rs index c7c11b0cca..c215d98739 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: &str, 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: &str, _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: &str, 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 { @@ -403,7 +403,7 @@ async fn delete_user(uuid: &str, token: AdminToken, mut conn: DbConn) -> EmptyRe EventType::OrganizationUserRemoved as i32, &membership.uuid, &membership.org_uuid, - ACTING_ADMIN_USER, + &ACTING_ADMIN_USER.into(), 14, // Use UnknownBrowser type &token.ip.ip, &mut conn, @@ -414,9 +414,9 @@ async fn delete_user(uuid: &str, token: AdminToken, mut conn: DbConn) -> EmptyRe res } -#[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?; +#[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: &str, _token: AdminToken, mut conn: DbConn, nt: Notif user.save(&mut conn).await } -#[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?; +#[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: &str, _token: AdminToken, mut conn: DbConn, nt: Noti save_result } -#[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?; +#[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: &str, 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?; + two_factor::enforce_2fa_policy(&user, &ACTING_ADMIN_USER.into(), 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: &str, _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); @@ -487,8 +487,8 @@ async fn resend_user_invite(uuid: &str, _token: AdminToken, mut conn: DbConn) -> #[derive(Debug, Deserialize)] struct MembershipTypeData { user_type: NumberOrString, - user_uuid: String, - org_uuid: String, + user_uuid: UserId, + org_uuid: OrganizationId, } #[post("/users/org_type", data = "")] @@ -534,7 +534,7 @@ async fn update_membership_type(data: Json, token: AdminToke EventType::OrganizationUserUpdated as i32, &member_to_edit.uuid, &data.org_uuid, - ACTING_ADMIN_USER, + &ACTING_ADMIN_USER.into(), 14, // Use UnknownBrowser type &token.ip.ip, &mut conn, @@ -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_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 f6c2048e67..f3e73244ab 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,15 +106,15 @@ 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_id: Option, conn: &mut DbConn) -> bool { if !CONFIG._enable_email_2fa() { return false; } if CONFIG.email_2fa_enforce_on_verified_invite() { return true; } - if member_uuid.is_some() { - return OrgPolicy::is_enabled_for_member(&member_uuid.unwrap(), OrgPolicyType::TwoFactorAuthentication, conn) + if member_id.is_some() { + return OrgPolicy::is_enabled_for_member(&member_id.unwrap(), OrgPolicyType::TwoFactorAuthentication, conn) .await; } false @@ -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: &str, _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), @@ -379,7 +379,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, Some(headers.device.uuid.clone())).await; save_result } @@ -439,7 +439,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, Some(headers.device.uuid.clone())).await; save_result } @@ -450,21 +450,21 @@ 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, } #[derive(Deserialize)] #[serde(rename_all = "camelCase")] struct UpdateEmergencyAccessData { - id: String, + id: EmergencyAccessId, key_encrypted: String, } #[derive(Deserialize)] #[serde(rename_all = "camelCase")] struct UpdateResetPasswordData { - organization_id: String, + organization_id: OrganizationId, reset_password_key: String, } @@ -493,44 +493,45 @@ 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") } // 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") } // 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") } // 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") } // 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") } @@ -553,17 +554,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, @@ -650,7 +651,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, Some(headers.device.uuid.clone())).await; save_result } @@ -797,7 +798,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, } @@ -812,7 +813,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()); @@ -854,7 +855,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, } @@ -870,7 +871,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 @@ -1032,7 +1033,7 @@ async fn get_known_device(device: KnownDevice, mut conn: DbConn) -> JsonResult { struct KnownDevice { email: String, - uuid: String, + uuid: DeviceId, } #[rocket::async_trait] @@ -1055,7 +1056,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")); }; @@ -1073,26 +1074,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 @@ -1111,8 +1117,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: &str, 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 @@ -1121,8 +1127,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(&device_id, &mut conn).await { + Device::clear_push_token_by_uuid(&device_id, &mut conn).await?; unregister_push_device(device.push_uuid).await?; } @@ -1130,16 +1136,16 @@ 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 { - 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)] #[serde(rename_all = "camelCase")] struct AuthRequestRequest { access_code: String, - device_identifier: String, + device_identifier: DeviceId, email: String, public_key: String, // Not used for now @@ -1193,16 +1199,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, @@ -1219,15 +1226,15 @@ 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, } -#[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, @@ -1235,7 +1242,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") }; @@ -1262,7 +1271,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, @@ -1276,14 +1285,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") }; @@ -1297,7 +1306,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/core/ciphers.rs b/src/api/core/ciphers.rs index 8e1c4f0e13..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: &str, 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,27 +204,27 @@ async fn get_cipher(uuid: &str, headers: Headers, mut conn: DbConn) -> JsonResul 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: &str, 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: &str, 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)] #[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, + 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, @@ -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 @@ -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, } @@ -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.") } } @@ -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, @@ -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, @@ -579,11 +579,11 @@ 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) { + 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,36 +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: &str, + 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: &str, + 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 = "")] -async fn post_cipher(uuid: &str, data: Json, headers: Headers, conn: DbConn, nt: Notify<'_>) -> JsonResult { - put_cipher(uuid, data, headers, conn, nt).await +#[post("/ciphers/", data = "")] +async fn post_cipher( + cipher_id: CipherId, + data: Json, + headers: Headers, + conn: DbConn, + nt: Notify<'_>, +) -> JsonResult { + put_cipher(cipher_id, data, headers, conn, nt).await } -#[put("/ciphers/", data = "")] +#[put("/ciphers/", data = "")] async fn put_cipher( - uuid: &str, + cipher_id: 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(&cipher_id, &mut conn).await else { err!("Cipher doesn't exist") }; @@ -673,27 +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 = "")] -async fn post_cipher_partial(uuid: &str, data: Json, headers: Headers, conn: DbConn) -> JsonResult { - put_cipher_partial(uuid, data, headers, conn).await +#[post("/ciphers//partial", data = "")] +async fn post_cipher_partial( + cipher_id: CipherId, + data: Json, + headers: Headers, + conn: DbConn, +) -> JsonResult { + 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: &str, + 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"); } } @@ -710,29 +721,29 @@ 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 = "")] +#[put("/ciphers//collections_v2", data = "")] async fn put_collections2_update( - uuid: &str, + 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: &str, + 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, @@ -740,20 +751,20 @@ async fn post_collections2_update( }))) } -#[put("/ciphers//collections", data = "")] +#[put("/ciphers//collections", data = "")] async fn put_collections_update( - uuid: &str, + 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: &str, + cipher_id: 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(&cipher_id, &mut conn).await else { err!("Cipher doesn't exist") }; @@ -769,9 +780,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.clone(), &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 @@ -817,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: &str, + 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: &str, + cipher_id: 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(&cipher_id, &mut conn).await else { err!("Cipher doesn't exist") }; @@ -846,9 +857,9 @@ async fn post_collections_admin( 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_admin_collections(headers.user.uuid.clone(), &mut conn).await); + 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 @@ -900,12 +911,12 @@ struct ShareCipherData { #[serde(alias = "Cipher")] cipher: CipherData, #[serde(alias = "CollectionIds")] - collection_ids: Vec, + collection_ids: Vec, } -#[post("/ciphers//share", data = "")] +#[post("/ciphers//share", data = "")] async fn post_cipher_share( - uuid: &str, + cipher_id: CipherId, data: Json, headers: Headers, mut conn: DbConn, @@ -913,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: &str, + cipher_id: CipherId, data: Json, headers: Headers, mut conn: DbConn, @@ -926,14 +937,14 @@ 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)] #[serde(rename_all = "camelCase")] struct ShareSelectedCipherData { ciphers: Vec, - collection_ids: Vec, + collection_ids: Vec, } #[put("/ciphers/share", data = "")] @@ -975,13 +986,13 @@ async fn put_cipher_share_selected( } async fn share_cipher_by_uuid( - uuid: &str, + 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 @@ -994,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 { @@ -1028,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: &str, attachment_id: &str, 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") }; @@ -1038,8 +1054,8 @@ async fn get_attachment(uuid: &str, attachment_id: &str, headers: Headers, mut c err!("Cipher is not accessible") } - match Attachment::find_by_id(attachment_id, &mut conn).await { - Some(attachment) if uuid == attachment.cipher_uuid => Ok(Json(attachment.to_json(&headers.host))), + match Attachment::find_by_id(&attachment_id, &mut conn).await { + 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"), } @@ -1063,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: &str, + 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") }; @@ -1120,7 +1136,7 @@ struct UploadData<'f> { /// database record, which is passed in as `attachment`. async fn save_attachment( mut attachment: Option, - cipher_uuid: &str, + cipher_id: CipherId, data: Form>, headers: &Headers, mut conn: DbConn, @@ -1135,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") }; @@ -1150,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)) @@ -1172,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)) @@ -1249,12 +1265,12 @@ 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_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); - let file_path = folder_path.join(&file_id); + 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?; if let Err(_err) = data.data.persist_to(&file_path).await { @@ -1271,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, @@ -1289,32 +1305,32 @@ 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: &str, - attachment_id: &str, + cipher_id: CipherId, + 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 { - Some(attachment) if uuid == attachment.cipher_uuid => Some(attachment), + let attachment = match Attachment::find_by_id(&attachment_id, &mut conn).await { + 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: &str, + cipher_id: CipherId, data: Form>, headers: Headers, conn: DbConn, @@ -1324,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: &str, + 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: &str, - attachment_id: &str, + 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: &str, - attachment_id: &str, + 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: &str, - attachment_id: &str, + 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: &str, - attachment_id: &str, + 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: &str, - attachment_id: &str, + 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: &str, 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: &str, 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: &str, 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: &str, 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: &str, 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: &str, 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 } @@ -1492,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: &str, 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: &str, 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 = "")] @@ -1515,8 +1546,8 @@ async fn restore_cipher_selected( #[derive(Deserialize)] #[serde(rename_all = "camelCase")] struct MoveCipherData { - folder_id: Option, - ids: Vec, + folder_id: Option, + ids: Vec, } #[post("/ciphers/move", data = "")] @@ -1527,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, @@ -1572,14 +1603,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, @@ -1639,13 +1670,13 @@ async fn delete_all( } async fn _delete_cipher_by_uuid( - uuid: &str, + 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") }; @@ -1678,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; } @@ -1694,7 +1725,7 @@ async fn _delete_cipher_by_uuid( #[derive(Deserialize)] #[serde(rename_all = "camelCase")] struct CipherIdsData { - ids: Vec, + ids: Vec, } async fn _delete_multiple_ciphers( @@ -1706,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; }; } @@ -1715,8 +1746,13 @@ async fn _delete_multiple_ciphers( Ok(()) } -async fn _restore_cipher_by_uuid(uuid: &str, 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") }; @@ -1737,11 +1773,11 @@ async fn _restore_cipher_by_uuid(uuid: &str, headers: &Headers, conn: &mut DbCon ) .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, @@ -1762,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, } @@ -1777,8 +1813,8 @@ async fn _restore_multiple_ciphers( } async fn _delete_cipher_attachment_by_id( - uuid: &str, - attachment_id: &str, + cipher_id: &CipherId, + attachment_id: &AttachmentId, headers: &Headers, conn: &mut DbConn, nt: &Notify<'_>, @@ -1787,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") }; @@ -1811,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, @@ -1831,14 +1867,14 @@ 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 members: HashMap, - pub user_collections: HashMap, - pub user_collections_groups: HashMap, - pub user_group_full_access_for_organizations: HashSet, + 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, + pub user_group_full_access_for_organizations: HashSet, } #[derive(Eq, PartialEq)] @@ -1848,17 +1884,17 @@ pub enum CipherSyncType { } impl CipherSyncData { - pub async fn new(user_uuid: &str, sync_type: CipherSyncType, conn: &mut DbConn) -> Self { - let cipher_folders: HashMap; - let cipher_favorites: HashSet; + 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. @@ -1869,35 +1905,35 @@ 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 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.to_string(), conn).await; - let mut cipher_collections: HashMap> = + 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 { cipher_collections.entry(cipher).or_default().push(collection); } // 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(); + let members: HashMap = + 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)) .collect(); // 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) + let user_collections_groups: HashMap = if CONFIG.org_groups_enabled() { + CollectionGroup::find_by_user(user_id, conn) .await .into_iter() .map(|collection_group| (collection_group.collections_uuid.clone(), collection_group)) @@ -1907,8 +1943,8 @@ 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() + let user_group_full_access_for_organizations: HashSet = if CONFIG.org_groups_enabled() { + 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 6d3d881dd4..8c6fcb6538 100644 --- a/src/api/core/emergency_access.rs +++ b/src/api/core/emergency_access.rs @@ -93,10 +93,10 @@ async fn get_grantees(headers: Headers, mut conn: DbConn) -> Json { } #[get("/emergency-access/")] -async fn get_emergency_access(emer_id: &str, headers: Headers, mut conn: DbConn) -> JsonResult { +async fn get_emergency_access(emer_id: EmergencyAccessId, headers: Headers, mut conn: DbConn) -> JsonResult { check_emergency_access_enabled()?; - match EmergencyAccess::find_by_uuid_and_grantor_uuid(emer_id, &headers.user.uuid, &mut conn).await { + match EmergencyAccess::find_by_uuid_and_grantor_uuid(&emer_id, &headers.user.uuid, &mut conn).await { Some(emergency_access) => Ok(Json( emergency_access.to_json_grantee_details(&mut conn).await.expect("Grantee user should exist but does not!"), )), @@ -118,7 +118,7 @@ struct EmergencyAccessUpdateData { #[put("/emergency-access/", data = "")] async fn put_emergency_access( - emer_id: &str, + emer_id: EmergencyAccessId, data: Json, headers: Headers, conn: DbConn, @@ -128,7 +128,7 @@ async fn put_emergency_access( #[post("/emergency-access/", data = "")] async fn post_emergency_access( - emer_id: &str, + emer_id: EmergencyAccessId, data: Json, headers: Headers, mut conn: DbConn, @@ -138,7 +138,7 @@ async fn post_emergency_access( let data: EmergencyAccessUpdateData = data.into_inner(); let Some(mut emergency_access) = - EmergencyAccess::find_by_uuid_and_grantor_uuid(emer_id, &headers.user.uuid, &mut conn).await + EmergencyAccess::find_by_uuid_and_grantor_uuid(&emer_id, &headers.user.uuid, &mut conn).await else { err!("Emergency access not valid.") }; @@ -163,12 +163,12 @@ async fn post_emergency_access( // region delete #[delete("/emergency-access/")] -async fn delete_emergency_access(emer_id: &str, headers: Headers, mut conn: DbConn) -> EmptyResult { +async fn delete_emergency_access(emer_id: EmergencyAccessId, headers: Headers, mut conn: DbConn) -> EmptyResult { check_emergency_access_enabled()?; let emergency_access = match ( - EmergencyAccess::find_by_uuid_and_grantor_uuid(emer_id, &headers.user.uuid, &mut conn).await, - EmergencyAccess::find_by_uuid_and_grantee_uuid(emer_id, &headers.user.uuid, &mut conn).await, + EmergencyAccess::find_by_uuid_and_grantor_uuid(&emer_id, &headers.user.uuid, &mut conn).await, + EmergencyAccess::find_by_uuid_and_grantee_uuid(&emer_id, &headers.user.uuid, &mut conn).await, ) { (Some(grantor_emer), None) => { info!("Grantor deleted emergency access {emer_id}"); @@ -186,7 +186,7 @@ async fn delete_emergency_access(emer_id: &str, headers: Headers, mut conn: DbCo } #[post("/emergency-access//delete")] -async fn post_delete_emergency_access(emer_id: &str, headers: Headers, conn: DbConn) -> EmptyResult { +async fn post_delete_emergency_access(emer_id: EmergencyAccessId, headers: Headers, conn: DbConn) -> EmptyResult { delete_emergency_access(emer_id, headers, conn).await } @@ -266,8 +266,8 @@ async fn send_invite(data: Json, headers: Headers, mu if CONFIG.mail_enabled() { mail::send_emergency_access_invite( &new_emergency_access.email.expect("Grantee email does not exists"), - &grantee_user.uuid, - &new_emergency_access.uuid, + grantee_user.uuid, + new_emergency_access.uuid, &grantor_user.name, &grantor_user.email, ) @@ -281,11 +281,11 @@ async fn send_invite(data: Json, headers: Headers, mu } #[post("/emergency-access//reinvite")] -async fn resend_invite(emer_id: &str, headers: Headers, mut conn: DbConn) -> EmptyResult { +async fn resend_invite(emer_id: EmergencyAccessId, headers: Headers, mut conn: DbConn) -> EmptyResult { check_emergency_access_enabled()?; let Some(mut emergency_access) = - EmergencyAccess::find_by_uuid_and_grantor_uuid(emer_id, &headers.user.uuid, &mut conn).await + EmergencyAccess::find_by_uuid_and_grantor_uuid(&emer_id, &headers.user.uuid, &mut conn).await else { err!("Emergency access not valid.") }; @@ -307,8 +307,8 @@ async fn resend_invite(emer_id: &str, headers: Headers, mut conn: DbConn) -> Emp if CONFIG.mail_enabled() { mail::send_emergency_access_invite( &email, - &grantor_user.uuid, - &emergency_access.uuid, + grantor_user.uuid, + emergency_access.uuid, &grantor_user.name, &grantor_user.email, ) @@ -331,7 +331,12 @@ struct AcceptData { } #[post("/emergency-access//accept", data = "")] -async fn accept_invite(emer_id: &str, data: Json, headers: Headers, mut conn: DbConn) -> EmptyResult { +async fn accept_invite( + emer_id: EmergencyAccessId, + data: Json, + headers: Headers, + mut conn: DbConn, +) -> EmptyResult { check_emergency_access_enabled()?; let data: AcceptData = data.into_inner(); @@ -355,7 +360,7 @@ async fn accept_invite(emer_id: &str, data: Json, headers: Headers, // We need to search for the uuid in combination with the email, since we do not yet store the uuid of the grantee in the database. // The uuid of the grantee gets stored once accepted. let Some(mut emergency_access) = - EmergencyAccess::find_by_uuid_and_grantee_email(emer_id, &headers.user.email, &mut conn).await + EmergencyAccess::find_by_uuid_and_grantee_email(&emer_id, &headers.user.email, &mut conn).await else { err!("Emergency access not valid.") }; @@ -389,7 +394,7 @@ struct ConfirmData { #[post("/emergency-access//confirm", data = "")] async fn confirm_emergency_access( - emer_id: &str, + emer_id: EmergencyAccessId, data: Json, headers: Headers, mut conn: DbConn, @@ -401,7 +406,7 @@ async fn confirm_emergency_access( let key = data.key; let Some(mut emergency_access) = - EmergencyAccess::find_by_uuid_and_grantor_uuid(emer_id, &confirming_user.uuid, &mut conn).await + EmergencyAccess::find_by_uuid_and_grantor_uuid(&emer_id, &confirming_user.uuid, &mut conn).await else { err!("Emergency access not valid.") }; @@ -441,12 +446,12 @@ async fn confirm_emergency_access( // region access emergency access #[post("/emergency-access//initiate")] -async fn initiate_emergency_access(emer_id: &str, headers: Headers, mut conn: DbConn) -> JsonResult { +async fn initiate_emergency_access(emer_id: EmergencyAccessId, headers: Headers, mut conn: DbConn) -> JsonResult { check_emergency_access_enabled()?; let initiating_user = headers.user; let Some(mut emergency_access) = - EmergencyAccess::find_by_uuid_and_grantee_uuid(emer_id, &initiating_user.uuid, &mut conn).await + EmergencyAccess::find_by_uuid_and_grantee_uuid(&emer_id, &initiating_user.uuid, &mut conn).await else { err!("Emergency access not valid.") }; @@ -479,11 +484,11 @@ async fn initiate_emergency_access(emer_id: &str, headers: Headers, mut conn: Db } #[post("/emergency-access//approve")] -async fn approve_emergency_access(emer_id: &str, headers: Headers, mut conn: DbConn) -> JsonResult { +async fn approve_emergency_access(emer_id: EmergencyAccessId, headers: Headers, mut conn: DbConn) -> JsonResult { check_emergency_access_enabled()?; let Some(mut emergency_access) = - EmergencyAccess::find_by_uuid_and_grantor_uuid(emer_id, &headers.user.uuid, &mut conn).await + EmergencyAccess::find_by_uuid_and_grantor_uuid(&emer_id, &headers.user.uuid, &mut conn).await else { err!("Emergency access not valid.") }; @@ -514,11 +519,11 @@ async fn approve_emergency_access(emer_id: &str, headers: Headers, mut conn: DbC } #[post("/emergency-access//reject")] -async fn reject_emergency_access(emer_id: &str, headers: Headers, mut conn: DbConn) -> JsonResult { +async fn reject_emergency_access(emer_id: EmergencyAccessId, headers: Headers, mut conn: DbConn) -> JsonResult { check_emergency_access_enabled()?; let Some(mut emergency_access) = - EmergencyAccess::find_by_uuid_and_grantor_uuid(emer_id, &headers.user.uuid, &mut conn).await + EmergencyAccess::find_by_uuid_and_grantor_uuid(&emer_id, &headers.user.uuid, &mut conn).await else { err!("Emergency access not valid.") }; @@ -551,11 +556,11 @@ async fn reject_emergency_access(emer_id: &str, headers: Headers, mut conn: DbCo // region action #[post("/emergency-access//view")] -async fn view_emergency_access(emer_id: &str, headers: Headers, mut conn: DbConn) -> JsonResult { +async fn view_emergency_access(emer_id: EmergencyAccessId, headers: Headers, mut conn: DbConn) -> JsonResult { check_emergency_access_enabled()?; let Some(emergency_access) = - EmergencyAccess::find_by_uuid_and_grantee_uuid(emer_id, &headers.user.uuid, &mut conn).await + EmergencyAccess::find_by_uuid_and_grantee_uuid(&emer_id, &headers.user.uuid, &mut conn).await else { err!("Emergency access not valid.") }; @@ -589,12 +594,12 @@ async fn view_emergency_access(emer_id: &str, headers: Headers, mut conn: DbConn } #[post("/emergency-access//takeover")] -async fn takeover_emergency_access(emer_id: &str, headers: Headers, mut conn: DbConn) -> JsonResult { +async fn takeover_emergency_access(emer_id: EmergencyAccessId, headers: Headers, mut conn: DbConn) -> JsonResult { check_emergency_access_enabled()?; let requesting_user = headers.user; let Some(emergency_access) = - EmergencyAccess::find_by_uuid_and_grantee_uuid(emer_id, &requesting_user.uuid, &mut conn).await + EmergencyAccess::find_by_uuid_and_grantee_uuid(&emer_id, &requesting_user.uuid, &mut conn).await else { err!("Emergency access not valid.") }; @@ -628,7 +633,7 @@ struct EmergencyAccessPasswordData { #[post("/emergency-access//password", data = "")] async fn password_emergency_access( - emer_id: &str, + emer_id: EmergencyAccessId, data: Json, headers: Headers, mut conn: DbConn, @@ -641,7 +646,7 @@ async fn password_emergency_access( let requesting_user = headers.user; let Some(emergency_access) = - EmergencyAccess::find_by_uuid_and_grantee_uuid(emer_id, &requesting_user.uuid, &mut conn).await + EmergencyAccess::find_by_uuid_and_grantee_uuid(&emer_id, &requesting_user.uuid, &mut conn).await else { err!("Emergency access not valid.") }; @@ -673,10 +678,10 @@ async fn password_emergency_access( // endregion #[get("/emergency-access//policies")] -async fn policies_emergency_access(emer_id: &str, headers: Headers, mut conn: DbConn) -> JsonResult { +async fn policies_emergency_access(emer_id: EmergencyAccessId, headers: Headers, mut conn: DbConn) -> JsonResult { let requesting_user = headers.user; let Some(emergency_access) = - EmergencyAccess::find_by_uuid_and_grantee_uuid(emer_id, &requesting_user.uuid, &mut conn).await + EmergencyAccess::find_by_uuid_and_grantee_uuid(&emer_id, &requesting_user.uuid, &mut conn).await else { err!("Emergency access not valid.") }; @@ -701,11 +706,11 @@ 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_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 49ff083f41..11ae66c7eb 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, CipherId, Event, Membership, MembershipId, OrganizationId, UserId}, DbConn, DbPool, }, util::parse_date, @@ -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()) @@ -59,14 +64,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 +79,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()) @@ -92,8 +97,8 @@ 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, + org_id: OrganizationId, + member_id: MembershipId, data: EventRange, _headers: AdminHeaders, mut conn: DbConn, @@ -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()) @@ -152,8 +157,8 @@ struct EventCollection { date: String, // Optional - cipher_id: Option, - organization_id: Option, + cipher_id: Option, + organization_id: Option, } // Upstream: @@ -180,11 +185,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 +202,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 +223,38 @@ 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_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: &str, + 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(String::from(user_uuid)); - event.act_user_uuid = Some(String::from(user_uuid)); + event.user_uuid = Some(user_id.clone()); + event.act_user_uuid = Some(user_id.clone()); 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(String::from(user_uuid)); - event.org_uuid = Some(org_uuid); - event.act_user_uuid = Some(String::from(user_uuid)); + event.user_uuid = Some(user_id.clone()); + event.org_uuid = Some(org_id); + event.act_user_uuid = Some(user_id.clone()); event.device_type = Some(device_type); event.ip_address = Some(ip.to_string()); events.push(event); @@ -261,8 +266,8 @@ async fn _log_user_event( pub async fn log_event( event_type: i32, source_uuid: &str, - org_uuid: &str, - act_user_uuid: &str, + org_id: &OrganizationId, + act_user_id: &UserId, device_type: i32, ip: &IpAddr, conn: &mut DbConn, @@ -270,15 +275,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: &str, - act_user_uuid: &str, + org_id: &OrganizationId, + act_user_id: &UserId, device_type: i32, event_date: Option, ip: &IpAddr, @@ -290,31 +295,31 @@ 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 1700..=1799 => { - event.policy_uuid = Some(String::from(source_uuid)); + event.policy_uuid = Some(source_uuid.to_string().into()); } // Ignore others _ => {} } - event.org_uuid = Some(String::from(org_uuid)); - event.act_user_uuid = Some(String::from(act_user_uuid)); + event.org_uuid = Some(org_id.clone()); + event.act_user_uuid = Some(act_user_id.clone()); 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 3daa2a064f..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: &str, 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"), } @@ -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 = "")] @@ -50,14 +50,20 @@ async fn post_folders(data: Json, headers: Headers, mut conn: DbConn Ok(Json(folder.to_json())) } -#[post("/folders/", data = "")] -async fn post_folder(uuid: &str, data: Json, headers: Headers, conn: DbConn, nt: Notify<'_>) -> JsonResult { - put_folder(uuid, data, headers, conn, nt).await +#[post("/folders/", data = "")] +async fn post_folder( + folder_id: FolderId, + data: Json, + headers: Headers, + conn: DbConn, + nt: Notify<'_>, +) -> JsonResult { + put_folder(folder_id, data, headers, conn, nt).await } -#[put("/folders/", data = "")] +#[put("/folders/", data = "")] async fn put_folder( - uuid: &str, + folder_id: 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(&folder_id, &headers.user.uuid, &mut conn).await else { err!("Invalid folder", "Folder does not exist or belongs to another user") }; @@ -77,14 +83,14 @@ async fn put_folder( Ok(Json(folder.to_json())) } -#[post("/folders//delete")] -async fn delete_folder_post(uuid: &str, 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: &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 { +#[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 b21ae6b99d..99a3765dd0 100644 --- a/src/api/core/organizations.rs +++ b/src/api/core/organizations.rs @@ -127,17 +127,25 @@ struct OrganizationUpdateData { #[serde(rename_all = "camelCase")] struct NewCollectionData { name: String, - groups: Vec, - users: Vec, - id: Option, + groups: Vec, + users: Vec, + id: Option, external_id: Option, } #[derive(Deserialize)] #[serde(rename_all = "camelCase")] -struct NewCollectionObjectData { +struct NewCollectionGroupData { hide_passwords: bool, - id: String, + id: GroupId, + read_only: bool, +} + +#[derive(Deserialize)] +#[serde(rename_all = "camelCase")] +struct NewCollectionMemberData { + hide_passwords: bool, + id: MembershipId, read_only: bool, } @@ -150,8 +158,14 @@ struct OrgKeyData { #[derive(Deserialize, Debug)] #[serde(rename_all = "camelCase")] -struct OrgBulkIds { - ids: Vec, +struct BulkGroupIds { + ids: Vec, +} + +#[derive(Deserialize, Debug)] +#[serde(rename_all = "camelCase")] +struct BulkMembershipIds { + ids: Vec, } #[post("/organizations", data = "")] @@ -191,7 +205,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, @@ -200,7 +214,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, } @@ -208,7 +222,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, @@ -217,12 +231,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") } @@ -230,7 +244,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, @@ -244,8 +258,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"), } @@ -253,7 +267,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, @@ -263,14 +277,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") }; @@ -281,8 +295,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, @@ -308,34 +322,39 @@ 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_swap_user_uuid_with_member_uuid(org_id, &mut conn).await; + let col_users = CollectionUser::find_by_organization_swap_user_uuid_with_member_uuid(&org_id, &mut conn).await; // Generate a HashMap to get the correct MembershipType per user to determine the manage permission // We use the uuid instead of the user_uuid here, since that is what is used in CollectionUser - let membership_type: HashMap = - Membership::find_confirmed_by_org(org_id, &mut conn).await.into_iter().map(|m| (m.uuid, m.atype)).collect(); + let membership_type: HashMap = + Membership::find_confirmed_by_org(&org_id, &mut conn).await.into_iter().map(|m| (m.uuid, m.atype)).collect(); // 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 @@ -348,15 +367,13 @@ async fn get_org_collections_details(org_id: &str, headers: ManagerHeadersLoose, } // 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| { - SelectionReadOnly::to_collection_user_details_read_only( - collection_user, - *membership_type.get(&collection_user.user_uuid).unwrap_or(&(MembershipType::User as i32)), + collection_user.to_json_details_for_user( + *membership_type.get(&collection_user.membership_uuid).unwrap_or(&(MembershipType::User as i32)), ) - .to_json() }) .collect(); @@ -365,9 +382,7 @@ async fn get_org_collections_details(org_id: &str, headers: ManagerHeadersLoose, CollectionGroup::find_by_collection(&col.uuid, &mut conn) .await .iter() - .map(|collection_group| { - SelectionReadOnly::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) @@ -389,20 +404,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") }; @@ -412,7 +427,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, @@ -427,7 +442,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") }; @@ -448,8 +463,8 @@ async fn post_organization_collections( #[put("/organizations//collections/", data = "")] async fn put_organization_collection_update( - org_id: &str, - col_id: &str, + org_id: OrganizationId, + col_id: CollectionId, headers: ManagerHeaders, data: Json, conn: DbConn, @@ -459,19 +474,19 @@ async fn put_organization_collection_update( #[post("/organizations//collections/", data = "")] async fn post_organization_collection_update( - org_id: &str, - col_id: &str, + org_id: OrganizationId, + col_id: CollectionId, headers: ManagerHeaders, data: Json, mut conn: DbConn, ) -> 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") }; @@ -486,7 +501,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, @@ -494,18 +509,16 @@ 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 { + let Some(member) = Membership::find_by_uuid_and_org(&user.id, &org_id, &mut conn).await else { err!("User is not part of organization") }; @@ -513,7 +526,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)) @@ -521,17 +534,17 @@ async fn post_organization_collection_update( #[delete("/organizations//collections//user/")] async fn delete_organization_collection_member( - org_id: &str, - col_id: &str, - member_id: &str, + org_id: OrganizationId, + 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") }; - 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 { @@ -544,9 +557,9 @@ async fn delete_organization_collection_member( #[post("/organizations//collections//delete-user/")] async fn post_organization_collection_delete_member( - org_id: &str, - col_id: &str, - member_id: &str, + org_id: OrganizationId, + col_id: CollectionId, + member_id: MembershipId, headers: AdminHeaders, conn: DbConn, ) -> EmptyResult { @@ -554,8 +567,8 @@ async fn post_organization_collection_delete_member( } async fn _delete_organization_collection( - org_id: &str, - col_id: &str, + org_id: &OrganizationId, + col_id: &CollectionId, headers: &ManagerHeaders, conn: &mut DbConn, ) -> EmptyResult { @@ -577,12 +590,12 @@ async fn _delete_organization_collection( #[delete("/organizations//collections/")] async fn delete_organization_collection( - org_id: &str, - col_id: &str, + org_id: OrganizationId, + 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)] @@ -591,28 +604,28 @@ 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, - col_id: &str, + org_id: OrganizationId, + 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 = "")] async fn bulk_delete_organization_collections( - org_id: &str, + org_id: OrganizationId, headers: ManagerHeadersLoose, data: Json, mut conn: DbConn, @@ -624,26 +637,26 @@ 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")] +#[get("/organizations//collections//details")] async fn get_org_collection_detail( - org_id: &str, - coll_id: &str, + org_id: OrganizationId, + 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 { 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") }; @@ -651,9 +664,7 @@ async fn get_org_collection_detail( CollectionGroup::find_by_collection(&collection.uuid, &mut conn) .await .iter() - .map(|collection_group| { - SelectionReadOnly::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, @@ -663,7 +674,7 @@ async fn get_org_collection_detail( // Generate a HashMap to get the correct MembershipType per user to determine the manage permission // We use the uuid instead of the user_uuid here, since that is what is used in CollectionUser - let membership_type: HashMap = Membership::find_confirmed_by_org(org_id, &mut conn) + let membership_type: HashMap = Membership::find_confirmed_by_org(&org_id, &mut conn) .await .into_iter() .map(|m| (m.uuid, m.atype)) @@ -674,11 +685,11 @@ async fn get_org_collection_detail( .await .iter() .map(|collection_user| { - SelectionReadOnly::to_collection_user_details_read_only( - collection_user, - *membership_type.get(&collection_user.user_uuid).unwrap_or(&(MembershipType::User as i32)), + collection_user.to_json_details_for_user( + *membership_type + .get(&collection_user.membership_uuid) + .unwrap_or(&(MembershipType::User as i32)), ) - .to_json() }) .collect(); @@ -695,45 +706,50 @@ 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 { +#[get("/organizations//collections//users")] +async fn get_collection_users( + org_id: OrganizationId, + 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") }; - 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( - Membership::find_by_user_and_org(&col_user.user_uuid, org_id, &mut conn) + member_list.push( + Membership::find_by_user_and_org(&col_user.user_uuid, &org_id, &mut conn) .await .unwrap() .to_json_user_access_restrictions(&col_user), ); } - Ok(Json(json!(user_list))) + Ok(Json(json!(member_list))) } -#[put("/organizations//collections//users", data = "")] +#[put("/organizations//collections//users", data = "")] async fn put_collection_users( - org_id: &str, - coll_id: &str, - data: Json>, + org_id: OrganizationId, + 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() { - 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") }; @@ -741,7 +757,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(()) @@ -750,7 +766,7 @@ async fn put_collection_users( #[derive(FromForm)] struct OrgIdData { #[field(name = "organizationId")] - organization_id: String, + organization_id: OrganizationId, } #[get("/ciphers/organization-details?")] @@ -767,14 +783,13 @@ 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_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) } @@ -790,12 +805,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), @@ -814,10 +829,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") @@ -842,7 +862,15 @@ async fn post_org_keys(org_id: &str, data: Json, _headers: AdminHead #[derive(Deserialize)] #[serde(rename_all = "camelCase")] struct CollectionData { - id: String, + id: CollectionId, + read_only: bool, + hide_passwords: bool, +} + +#[derive(Deserialize)] +#[serde(rename_all = "camelCase")] +struct MembershipData { + id: MembershipId, read_only: bool, hide_passwords: bool, } @@ -851,7 +879,7 @@ struct CollectionData { #[serde(rename_all = "camelCase")] struct InviteData { emails: Vec, - groups: Vec, + groups: Vec, r#type: NumberOrString, collections: Option>, #[serde(default)] @@ -861,7 +889,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 mut data: InviteData = data.into_inner(); // HACK: We need the raw user-type to be sure custom role is selected to determine the access_all permission @@ -910,7 +943,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 @@ -922,7 +955,7 @@ async fn send_invite(org_id: &str, data: Json, headers: AdminHeaders } }; - 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()); let access_all = data.access_all; new_member.access_all = access_all; new_member.atype = new_type; @@ -931,7 +964,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( @@ -949,15 +982,15 @@ async fn send_invite(org_id: &str, data: Json, headers: AdminHeaders 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?; } log_event( EventType::OrganizationUserInvited as i32, &new_member.uuid, - org_id, + &org_id, &headers.user.uuid, headers.device.atype, &headers.ip.ip, @@ -966,14 +999,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_member.uuid), &org_name, Some(headers.user.email.clone()), @@ -987,16 +1020,16 @@ async fn send_invite(org_id: &str, data: Json, headers: AdminHeaders #[post("/organizations//users/reinvite", data = "")] async fn bulk_reinvite_members( - org_id: &str, - data: Json, + org_id: OrganizationId, + 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 { - let err_msg = match _reinvite_member(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:?}"), }; @@ -1017,13 +1050,23 @@ async fn bulk_reinvite_members( })) } -#[post("/organizations//users//reinvite")] -async fn reinvite_member(org_id: &str, member: &str, headers: AdminHeaders, mut conn: DbConn) -> EmptyResult { - _reinvite_member(org_id, member, &headers.user.email, &mut conn).await +#[post("/organizations//users//reinvite")] +async fn reinvite_member( + org_id: OrganizationId, + member_id: MembershipId, + headers: AdminHeaders, + mut conn: DbConn, +) -> EmptyResult { + _reinvite_member(&org_id, &member_id, &headers.user.email, &mut conn).await } -async fn _reinvite_member(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 { +async fn _reinvite_member( + org_id: &OrganizationId, + member_id: &MembershipId, + invited_by_email: &str, + conn: &mut DbConn, +) -> EmptyResult { + 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.") }; @@ -1047,7 +1090,7 @@ async fn _reinvite_member(org_id: &str, member: &str, invited_by_email: &str, co 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()), @@ -1074,14 +1117,19 @@ 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: MembershipId, + 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 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"), } @@ -1106,7 +1154,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_member, edit_member, 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() { @@ -1156,7 +1204,7 @@ async fn accept_invite(org_id: &str, member_id: &str, data: Json, mu #[derive(Deserialize)] #[serde(rename_all = "camelCase")] struct ConfirmData { - id: Option, + id: Option, key: Option, } @@ -1168,7 +1216,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, @@ -1180,9 +1228,9 @@ 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 { + let err_msg = match _confirm_invite(&org_id, &member_id, &user_key, &headers, &mut conn, &nt).await { Ok(_) => String::new(), Err(e) => format!("{e:?}"), }; @@ -1208,8 +1256,8 @@ async fn bulk_confirm_invite( #[post("/organizations//users//confirm", data = "")] async fn confirm_invite( - org_id: &str, - member_id: &str, + org_id: OrganizationId, + member_id: MembershipId, data: Json, headers: AdminHeaders, mut conn: DbConn, @@ -1217,12 +1265,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: &str, - member_id: &str, + org_id: &OrganizationId, + member_id: &MembershipId, key: &str, headers: &AdminHeaders, conn: &mut DbConn, @@ -1298,9 +1346,13 @@ async fn _confirm_invite( } #[get("/organizations//users/mini-details", rank = 1)] -async fn get_org_user_mini_details(org_id: &str, _headers: ManagerHeadersLoose, mut conn: DbConn) -> Json { +async fn get_org_user_mini_details( + org_id: OrganizationId, + _headers: ManagerHeadersLoose, + mut conn: DbConn, +) -> Json { let mut members_json = Vec::new(); - for m in Membership::find_by_org(org_id, &mut conn).await { + for m in Membership::find_by_org(&org_id, &mut conn).await { members_json.push(m.to_json_mini_details(&mut conn).await); } @@ -1313,13 +1365,13 @@ async fn get_org_user_mini_details(org_id: &str, _headers: ManagerHeadersLoose, #[get("/organizations//users/?", rank = 2)] async fn get_user( - org_id: &str, - member_id: &str, + org_id: OrganizationId, + 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") }; @@ -1336,7 +1388,7 @@ async fn get_user( struct EditUserData { r#type: NumberOrString, collections: Option>, - groups: Option>, + groups: Option>, #[serde(default)] access_all: bool, #[serde(default)] @@ -1345,8 +1397,8 @@ struct EditUserData { #[put("/organizations//users/", data = "", rank = 1)] async fn put_member( - org_id: &str, - member_id: &str, + org_id: OrganizationId, + member_id: MembershipId, data: Json, headers: AdminHeaders, conn: DbConn, @@ -1356,8 +1408,8 @@ async fn put_member( #[post("/organizations//users/", data = "", rank = 1)] async fn edit_member( - org_id: &str, - member_id: &str, + org_id: OrganizationId, + member_id: MembershipId, data: Json, headers: AdminHeaders, mut conn: DbConn, @@ -1383,7 +1435,7 @@ async fn edit_member( data.access_all = true; } - let mut member_to_edit = match Membership::find_by_uuid_and_org(member_id, org_id, &mut conn).await { + let mut member_to_edit = match Membership::find_by_uuid_and_org(&member_id, &org_id, &mut conn).await { Some(member) => member, None => err!("The specified user isn't member of the organization"), }; @@ -1404,7 +1456,7 @@ async fn edit_member( && 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") } } @@ -1412,7 +1464,7 @@ async fn edit_member( // This check is also done at accept_invite, _confirm_invite, _activate_member, edit_member, 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() { @@ -1431,14 +1483,14 @@ async fn edit_member( 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( @@ -1456,15 +1508,15 @@ async fn edit_member( 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?; } log_event( EventType::OrganizationUserUpdated as i32, &member_to_edit.uuid, - org_id, + &org_id, &headers.user.uuid, headers.device.atype, &headers.ip.ip, @@ -1477,17 +1529,17 @@ async fn edit_member( #[delete("/organizations//users", data = "")] async fn bulk_delete_member( - org_id: &str, - data: Json, + org_id: OrganizationId, + 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 { - let err_msg = match _delete_member(org_id, &member_id, &headers, &mut conn, &nt).await { + let err_msg = match _delete_member(&org_id, &member_id, &headers, &mut conn, &nt).await { Ok(_) => String::new(), Err(e) => format!("{e:?}"), }; @@ -1510,29 +1562,29 @@ async fn bulk_delete_member( #[delete("/organizations//users/")] async fn delete_member( - org_id: &str, - member_id: &str, + org_id: OrganizationId, + member_id: MembershipId, headers: AdminHeaders, mut conn: DbConn, nt: Notify<'_>, ) -> EmptyResult { - _delete_member(org_id, member_id, &headers, &mut conn, &nt).await + _delete_member(&org_id, &member_id, &headers, &mut conn, &nt).await } #[post("/organizations//users//delete")] async fn post_delete_member( - org_id: &str, - member_id: &str, + org_id: OrganizationId, + member_id: MembershipId, headers: AdminHeaders, mut conn: DbConn, nt: Notify<'_>, ) -> EmptyResult { - _delete_member(org_id, member_id, &headers, &mut conn, &nt).await + _delete_member(&org_id, &member_id, &headers, &mut conn, &nt).await } async fn _delete_member( - org_id: &str, - member_id: &str, + org_id: &OrganizationId, + member_id: &MembershipId, headers: &AdminHeaders, conn: &mut DbConn, nt: &Notify<'_>, @@ -1573,19 +1625,19 @@ async fn _delete_member( #[post("/organizations//users/public-keys", data = "")] async fn bulk_public_keys( - org_id: &str, - data: Json, + org_id: OrganizationId, + 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. // 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!( { @@ -1645,14 +1697,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 }; @@ -1669,7 +1721,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; @@ -1679,10 +1731,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; @@ -1693,9 +1745,9 @@ async fn post_org_import( #[serde(rename_all = "camelCase")] #[allow(dead_code)] struct BulkCollectionsData { - organization_id: String, - cipher_ids: Vec, - collection_ids: HashSet, + organization_id: OrganizationId, + cipher_ids: Vec, + collection_ids: HashSet, remove_collections: bool, } @@ -1713,7 +1765,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() @@ -1750,8 +1802,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!({ @@ -1762,11 +1814,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!({}))); } @@ -1781,7 +1833,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!({ @@ -1792,14 +1844,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())) @@ -1815,7 +1867,7 @@ struct PolicyData { #[put("/organizations//policies/", data = "")] async fn put_policy( - org_id: &str, + org_id: OrganizationId, pol_type: i32, data: Json, headers: AdminHeaders, @@ -1835,7 +1887,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, }; @@ -1848,7 +1900,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, }; @@ -1862,7 +1914,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, @@ -1873,7 +1925,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. @@ -1892,7 +1944,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, @@ -1905,9 +1957,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; @@ -1916,8 +1968,8 @@ async fn put_policy( log_event( EventType::PolicyUpdated as i32, - &policy.uuid, - org_id, + policy.uuid.as_ref(), + &org_id, &headers.user.uuid, headers.device.atype, &headers.ip.ip, @@ -1930,7 +1982,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. @@ -1978,7 +2030,7 @@ fn get_plans_tax_rates(_headers: Headers) -> Json { } #[get("/organizations/<_org_id>/billing/metadata")] -fn get_billing_metadata(_org_id: &str, _headers: Headers) -> Json { +fn get_billing_metadata(_org_id: OrganizationId, _headers: Headers) -> Json { // Prevent a 404 error, which also causes Javascript errors. Json(_empty_data_json()) } @@ -2021,7 +2073,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 @@ -2030,7 +2082,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"), @@ -2039,11 +2091,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, @@ -2055,7 +2107,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 @@ -2063,7 +2115,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; @@ -2073,7 +2125,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, @@ -2082,14 +2134,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()), @@ -2102,13 +2154,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, @@ -2127,15 +2179,26 @@ 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_member(org_id: &str, member_id: &str, headers: AdminHeaders, mut conn: DbConn) -> EmptyResult { - _revoke_member(org_id, member_id, &headers, &mut conn).await +async fn deactivate_member( + org_id: OrganizationId, + member_id: MembershipId, + headers: AdminHeaders, + mut conn: DbConn, +) -> EmptyResult { + _revoke_member(&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_members( - org_id: &str, - data: Json, + org_id: OrganizationId, + data: Json, headers: AdminHeaders, conn: DbConn, ) -> Json { @@ -2143,20 +2206,19 @@ async fn bulk_deactivate_members( } #[put("/organizations//users//revoke")] -async fn revoke_member(org_id: &str, member_id: &str, headers: AdminHeaders, mut conn: DbConn) -> EmptyResult { - _revoke_member(org_id, member_id, &headers, &mut conn).await -} - -#[derive(Deserialize, Debug)] -#[serde(rename_all = "camelCase")] -struct OrgBulkRevokeData { - ids: Option>, +async fn revoke_member( + org_id: OrganizationId, + member_id: MembershipId, + headers: AdminHeaders, + mut conn: DbConn, +) -> EmptyResult { + _revoke_member(&org_id, &member_id, &headers, &mut conn).await } #[put("/organizations//users/revoke", data = "")] async fn bulk_revoke_members( - org_id: &str, - data: Json, + org_id: OrganizationId, + data: Json, headers: AdminHeaders, mut conn: DbConn, ) -> Json { @@ -2166,7 +2228,7 @@ async fn bulk_revoke_members( match data.ids { Some(members) => { for member_id in members { - let err_msg = match _revoke_member(org_id, &member_id, &headers, &mut conn).await { + let err_msg = match _revoke_member(&org_id, &member_id, &headers, &mut conn).await { Ok(_) => String::new(), Err(e) => format!("{e:?}"), }; @@ -2190,7 +2252,12 @@ async fn bulk_revoke_members( })) } -async fn _revoke_member(org_id: &str, member_id: &str, headers: &AdminHeaders, conn: &mut DbConn) -> EmptyResult { +async fn _revoke_member( + org_id: &OrganizationId, + member_id: &MembershipId, + 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 { @@ -2227,15 +2294,20 @@ async fn _revoke_member(org_id: &str, member_id: &str, headers: &AdminHeaders, c // Pre web-vault v2022.9.x endpoint #[put("/organizations//users//activate")] -async fn activate_member(org_id: &str, member_id: &str, headers: AdminHeaders, mut conn: DbConn) -> EmptyResult { - _restore_member(org_id, member_id, &headers, &mut conn).await +async fn activate_member( + org_id: OrganizationId, + member_id: MembershipId, + headers: AdminHeaders, + mut conn: DbConn, +) -> EmptyResult { + _restore_member(&org_id, &member_id, &headers, &mut conn).await } // Pre web-vault v2022.9.x endpoint #[put("/organizations//users/activate", data = "")] async fn bulk_activate_members( - org_id: &str, - data: Json, + org_id: OrganizationId, + data: Json, headers: AdminHeaders, conn: DbConn, ) -> Json { @@ -2243,14 +2315,19 @@ async fn bulk_activate_members( } #[put("/organizations//users//restore")] -async fn restore_member(org_id: &str, member_id: &str, headers: AdminHeaders, mut conn: DbConn) -> EmptyResult { - _restore_member(org_id, member_id, &headers, &mut conn).await +async fn restore_member( + org_id: OrganizationId, + member_id: MembershipId, + headers: AdminHeaders, + mut conn: DbConn, +) -> EmptyResult { + _restore_member(&org_id, &member_id, &headers, &mut conn).await } #[put("/organizations//users/restore", data = "")] async fn bulk_restore_members( - org_id: &str, - data: Json, + org_id: OrganizationId, + data: Json, headers: AdminHeaders, mut conn: DbConn, ) -> Json { @@ -2258,7 +2335,7 @@ async fn bulk_restore_members( let mut bulk_response = Vec::new(); for member_id in data.ids { - let err_msg = match _restore_member(org_id, &member_id, &headers, &mut conn).await { + let err_msg = match _restore_member(&org_id, &member_id, &headers, &mut conn).await { Ok(_) => String::new(), Err(e) => format!("{e:?}"), }; @@ -2279,7 +2356,12 @@ async fn bulk_restore_members( })) } -async fn _restore_member(org_id: &str, member_id: &str, headers: &AdminHeaders, conn: &mut DbConn) -> EmptyResult { +async fn _restore_member( + org_id: &OrganizationId, + member_id: &MembershipId, + 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 { @@ -2328,10 +2410,10 @@ async fn _restore_member(org_id: &str, member_id: &str, headers: &AdminHeaders, } #[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 { @@ -2352,7 +2434,7 @@ async fn get_groups(org_id: &str, _headers: ManagerHeadersLoose, mut conn: DbCon } #[get("/organizations//groups/details", rank = 1)] -async fn get_groups_details(org_id: &str, headers: ManagerHeadersLoose, conn: DbConn) -> JsonResult { +async fn get_groups_details(org_id: OrganizationId, headers: ManagerHeadersLoose, conn: DbConn) -> JsonResult { get_groups(org_id, headers, conn).await } @@ -2363,13 +2445,13 @@ struct GroupRequest { #[serde(default)] access_all: bool, external_id: Option, - collections: Vec, - users: Vec, + collections: Vec, + users: Vec, } 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 { @@ -2384,56 +2466,22 @@ impl GroupRequest { #[derive(Deserialize, Serialize)] #[serde(rename_all = "camelCase")] -struct SelectionReadOnly { - id: String, +struct SelectedCollection { + id: CollectionId, read_only: bool, hide_passwords: bool, - manage: bool, } -impl SelectionReadOnly { - pub fn to_collection_group(&self, groups_uuid: String) -> CollectionGroup { +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) } - - pub fn to_collection_group_details_read_only(collection_group: &CollectionGroup) -> SelectionReadOnly { - // If both read_only and hide_passwords are false, then manage should be true - // You can't have an entry with read_only and manage, or hide_passwords and manage - // Or an entry with everything to false - SelectionReadOnly { - id: collection_group.groups_uuid.clone(), - read_only: collection_group.read_only, - hide_passwords: collection_group.hide_passwords, - manage: !collection_group.read_only && !collection_group.hide_passwords, - } - } - - pub fn to_collection_user_details_read_only( - collection_user: &CollectionUser, - membership_type: i32, - ) -> SelectionReadOnly { - // Vaultwarden allows manage access for Admins and Owners by default - // For managers (Or custom role) it depends if they have read_ony or hide_passwords set to true or not - SelectionReadOnly { - id: collection_user.user_uuid.clone(), - read_only: collection_user.read_only, - hide_passwords: collection_user.hide_passwords, - manage: membership_type >= MembershipType::Admin - || (membership_type == MembershipType::Manager - && !collection_user.read_only - && !collection_user.hide_passwords), - } - } - - pub fn to_json(&self) -> Value { - json!(self) - } } #[post("/organizations//groups/", data = "")] async fn post_group( - org_id: &str, - group_id: &str, + org_id: OrganizationId, + group_id: GroupId, data: Json, headers: AdminHeaders, conn: DbConn, @@ -2442,18 +2490,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, @@ -2466,8 +2519,8 @@ async fn post_groups(org_id: &str, headers: AdminHeaders, data: Json/groups/", data = "")] async fn put_group( - org_id: &str, - group_id: &str, + org_id: OrganizationId, + group_id: GroupId, data: Json, headers: AdminHeaders, mut conn: DbConn, @@ -2476,20 +2529,20 @@ 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, &updated_group.uuid, - org_id, + &org_id, &headers.user.uuid, headers.device.atype, &headers.ip.ip, @@ -2502,27 +2555,27 @@ async fn put_group( async fn add_update_group( mut group: Group, - collections: Vec, - users: Vec, - org_id: &str, + collections: Vec, + members: Vec, + org_id: OrganizationId, headers: &AdminHeaders, conn: &mut DbConn, ) -> 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?; } - 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, - org_id, + &assigned_member, + &org_id, &headers.user.uuid, headers.device.atype, &headers.ip.ip, @@ -2541,12 +2594,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: 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") }; @@ -2554,16 +2612,31 @@ 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: GroupId, + 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: GroupId, + 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: &GroupId, + headers: &AdminHeaders, + conn: &mut DbConn, +) -> EmptyResult { if !CONFIG.org_groups_enabled() { err!("Group support is disabled"); } @@ -2588,8 +2661,8 @@ 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, - data: Json, + org_id: OrganizationId, + data: Json, headers: AdminHeaders, mut conn: DbConn, ) -> EmptyResult { @@ -2597,21 +2670,21 @@ 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? + _delete_group(&org_id, &group_id, &headers, &mut conn).await? } Ok(()) } #[get("/organizations//groups/", rank = 2)] -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: 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") }; @@ -2619,16 +2692,21 @@ async fn get_group(org_id: &str, group_id: &str, _headers: AdminHeaders, mut con } #[get("/organizations//groups//users")] -async fn get_group_members(org_id: &str, group_id: &str, _headers: AdminHeaders, mut conn: DbConn) -> JsonResult { +async fn get_group_members( + org_id: OrganizationId, + group_id: GroupId, + _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") }; - let group_members: Vec = GroupUser::find_by_group(group_id, &mut conn) + let group_members: Vec = GroupUser::find_by_group(&group_id, &mut conn) .await .iter() .map(|entry| entry.users_organizations_uuid.clone()) @@ -2639,31 +2717,31 @@ async fn get_group_members(org_id: &str, group_id: &str, _headers: AdminHeaders, #[put("/organizations//groups//users", data = "")] async fn put_group_members( - org_id: &str, - group_id: &str, + org_id: OrganizationId, + group_id: GroupId, headers: AdminHeaders, - data: Json>, + data: Json>, mut conn: DbConn, ) -> EmptyResult { 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") }; - GroupUser::delete_all_by_group(group_id, &mut conn).await?; + 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(group_id.clone(), assigned_member.clone()); user_entry.save(&mut conn).await?; log_event( EventType::OrganizationUserUpdatedGroups as i32, - &assigned_user_id, - org_id, + &assigned_member, + &org_id, &headers.user.uuid, headers.device.atype, &headers.ip.ip, @@ -2675,18 +2753,23 @@ async fn put_group_members( Ok(()) } -#[get("/organizations//users//groups")] -async fn get_user_groups(org_id: &str, user_id: &str, _headers: AdminHeaders, mut conn: DbConn) -> JsonResult { +#[get("/organizations//users//groups")] +async fn get_user_groups( + org_id: OrganizationId, + member_id: MembershipId, + _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(&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(); + 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))) } @@ -2694,13 +2777,13 @@ async fn get_user_groups(org_id: &str, user_id: &str, _headers: AdminHeaders, mu #[derive(Deserialize)] #[serde(rename_all = "camelCase")] struct OrganizationUserUpdateGroupsRequest { - group_ids: Vec, + group_ids: Vec, } #[post("/organizations//users//groups", data = "")] async fn post_user_groups( - org_id: &str, - member_id: &str, + org_id: OrganizationId, + member_id: MembershipId, data: Json, headers: AdminHeaders, conn: DbConn, @@ -2710,8 +2793,8 @@ async fn post_user_groups( #[put("/organizations//users//groups", data = "")] async fn put_user_groups( - org_id: &str, - member_id: &str, + org_id: OrganizationId, + member_id: MembershipId, data: Json, headers: AdminHeaders, mut conn: DbConn, @@ -2720,22 +2803,22 @@ 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."); } - GroupUser::delete_all_by_member(member_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(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, - org_id, + &member_id, + &org_id, &headers.user.uuid, headers.device.atype, &headers.ip.ip, @@ -2748,9 +2831,9 @@ async fn put_user_groups( #[post("/organizations//groups//delete-user/")] async fn post_delete_group_member( - org_id: &str, - group_id: &str, - member_id: &str, + org_id: OrganizationId, + group_id: GroupId, + member_id: MembershipId, headers: AdminHeaders, conn: DbConn, ) -> EmptyResult { @@ -2759,9 +2842,9 @@ async fn post_delete_group_member( #[delete("/organizations//groups//users/")] async fn delete_group_member( - org_id: &str, - group_id: &str, - member_id: &str, + org_id: OrganizationId, + group_id: GroupId, + member_id: MembershipId, headers: AdminHeaders, mut conn: DbConn, ) -> EmptyResult { @@ -2769,18 +2852,18 @@ async fn delete_group_member( 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, + &member_id, + &org_id, &headers.user.uuid, headers.device.atype, &headers.ip.ip, @@ -2788,7 +2871,7 @@ async fn delete_group_member( ) .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)] @@ -2810,8 +2893,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") }; @@ -2824,24 +2907,24 @@ 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, - member_id: &str, + org_id: OrganizationId, + member_id: MembershipId, 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") }; - 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") }; @@ -2849,7 +2932,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"); @@ -2874,8 +2957,8 @@ async fn put_reset_password( log_event( EventType::OrganizationUserAdminResetPassword as i32, - member_id, - org_id, + &member_id, + &org_id, &headers.user.uuid, headers.device.atype, &headers.ip.ip, @@ -2888,16 +2971,16 @@ async fn put_reset_password( #[get("/organizations//users//reset-password-details")] async fn get_reset_password_details( - org_id: &str, - member_id: &str, + org_id: OrganizationId, + member_id: MembershipId, 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") }; @@ -2905,7 +2988,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!({ @@ -2921,8 +3004,8 @@ async fn get_reset_password_details( } async fn check_reset_password_applicable_and_permissions( - org_id: &str, - member_id: &str, + org_id: &OrganizationId, + member_id: &MembershipId, headers: &AdminHeaders, conn: &mut DbConn, ) -> EmptyResult { @@ -2940,7 +3023,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."); } @@ -2958,22 +3041,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, - member_id: &str, + org_id: OrganizationId, + member_id: MembershipId, 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 withdrawn due to an enterprise policy"); } @@ -2996,7 +3079,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(()) } @@ -3010,7 +3093,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, @@ -3032,12 +3115,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, } @@ -3045,14 +3128,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, @@ -3075,7 +3158,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 } @@ -3089,16 +3172,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..c3ecd25ed2 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 { @@ -219,10 +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 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 b98ecf70a0..e181d6ab84 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, }; @@ -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 @@ -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: String) -> 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: String) -> 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: &str, 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"), } } @@ -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() { @@ -346,16 +346,16 @@ 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, } // 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, - file_id: &str, + send_id: SendId, + file_id: SendFileId, data: Form>, headers: Headers, mut conn: DbConn, @@ -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(send_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(send_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 @@ -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"), + &String::from("00000000-0000-0000-0000-000000000000").into(), &mut conn, ) .await; @@ -495,14 +495,14 @@ async fn post_access( #[post("/sends//access/file/", data = "")] async fn post_access_file( - send_id: &str, - file_id: &str, + send_id: SendId, + file_id: SendFileId, 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(&send_id, &mut conn).await else { err_code!(SEND_INACCESSIBLE_MSG, 404) }; @@ -542,12 +542,12 @@ async fn post_access_file( UpdateType::SyncSendUpdate, &send, &send.update_users_revision(&mut conn).await, - &String::from("00000000-0000-0000-0000-000000000000"), + &String::from("00000000-0000-0000-0000-000000000000").into(), &mut conn, ) .await; - let token_claims = crate::auth::generate_send_claims(send_id, 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", @@ -557,7 +557,7 @@ async fn post_access_file( } #[get("/sends//?")] -async fn download_send(send_id: SafeString, file_id: SafeString, t: &str) -> Option { +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!("{send_id}/{file_id}") { return NamedFile::open(Path::new(&CONFIG.sends_folder()).join(send_id).join(file_id)).await.ok(); @@ -566,15 +566,21 @@ async fn download_send(send_id: SafeString, file_id: SafeString, t: &str) -> Opt None } -#[put("/sends/", data = "")] -async fn put_send(uuid: &str, data: Json, headers: Headers, mut conn: DbConn, nt: Notify<'_>) -> JsonResult { +#[put("/sends/", data = "")] +async fn put_send( + send_id: 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 { - 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?; @@ -640,9 +646,9 @@ pub async fn update_send_from_data( Ok(()) } -#[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 { +#[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") }; @@ -659,11 +665,11 @@ async fn delete_send(uuid: &str, headers: Headers, mut conn: DbConn, nt: Notify< Ok(()) } -#[put("/sends//remove-password")] -async fn put_remove_password(uuid: &str, 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 57d7792692..3386f7d79a 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_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: &str, + 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.to_string(), 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 764210434a..289f01b3ba 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_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(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/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/core/two_factor/email.rs b/src/api/core/two_factor/email.rs index 09f2f3b796..d6470a288b 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,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: &str, 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: &str, 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: &str, 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 756ae83697..cfe0be86a6 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: &UserId, 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: &str, - act_uuid: &str, + org_id: &OrganizationId, + act_user_id: &UserId, 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 1a1d59c88a..5e4a65becb 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,11 +104,11 @@ async fn verify_otp(data: Json, headers: Headers, mut con pub async fn validate_protected_action_otp( otp: &str, - user_uuid: &str, + 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 9ee83d3855..614c5df326 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,20 +352,20 @@ async fn delete_webauthn(data: Json, headers: Headers, mut conn: } pub async fn get_webauthn_registrations( - user_uuid: &str, + 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: &str, 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: &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_id.clone(), TwoFactorType::WebauthnLoginChallenge, serde_json::to_string(&state)?) .save(conn) .await?; @@ -384,9 +384,9 @@ 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_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: &str, response: &str, conn: &mut 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: &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_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 1d1119875c..38cdfce583 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 { @@ -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), @@ -359,7 +358,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 +367,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,21 +375,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 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 { @@ -469,10 +469,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 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)) }; @@ -614,7 +615,7 @@ fn _selected_data(tf: Option) -> ApiResult { async fn _json_err_twofactor( providers: &[i32], - user_uuid: &str, + user_id: &UserId, data: &ConnectData, conn: &mut DbConn, ) -> ApiResult { @@ -635,12 +636,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"), }; @@ -672,7 +673,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") }; @@ -684,13 +685,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)?; @@ -745,7 +746,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, @@ -768,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/api/notifications.rs b/src/api/notifications.rs index 8c925e37bb..3509e04bac 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, CollectionId, DeviceId, 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.as_ref()) { 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_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}"); @@ -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, ); @@ -359,15 +359,15 @@ impl WebSocketUsers { } } - pub async fn send_logout(&self, user: &User, acting_device_uuid: Option) { + pub async fn send_logout(&self, user: &User, acting_device_id: Option) { // Skip any processing if both WebSockets and Push are not active if *NOTIFICATIONS_DISABLED { 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(), + acting_device_id.clone(), ); if CONFIG.enable_websocket() { @@ -375,7 +375,7 @@ impl WebSocketUsers { } if CONFIG.push_enabled() { - push_logout(user, acting_device_uuid); + push_logout(user, acting_device_id.clone()); } } @@ -383,7 +383,7 @@ impl WebSocketUsers { &self, ut: UpdateType, folder: &Folder, - acting_device_uuid: &String, + acting_device_id: &DeviceId, conn: &mut DbConn, ) { // Skip any processing if both WebSockets and Push are not active @@ -392,12 +392,12 @@ impl WebSocketUsers { } let data = create_update( vec![ - ("Id".into(), folder.uuid.clone().into()), - ("UserId".into(), folder.user_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)), ], ut, - Some(acting_device_uuid.into()), + Some(acting_device_id.clone()), ); if CONFIG.enable_websocket() { @@ -405,7 +405,7 @@ impl WebSocketUsers { } if CONFIG.push_enabled() { - push_folder_update(ut, folder, acting_device_uuid, conn).await; + push_folder_update(ut, folder, acting_device_id, conn).await; } } @@ -413,48 +413,48 @@ impl WebSocketUsers { &self, ut: UpdateType, cipher: &Cipher, - user_uuids: &[String], - acting_device_uuid: &String, - collection_uuids: Option>, + user_ids: &[UserId], + acting_device_id: &DeviceId, + collection_uuids: Option>, conn: &mut DbConn, ) { // Skip any processing if both WebSockets and Push are not active if *NOTIFICATIONS_DISABLED { return; } - let org_uuid = convert_option(cipher.organization_uuid.clone()); + 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.into()).collect::>()), + Value::Array(collection_uuids.into_iter().map(|v| v.to_string().into()).collect::>()), 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( vec![ - ("Id".into(), cipher.uuid.clone().into()), - ("UserId".into(), user_uuid), - ("OrganizationId".into(), org_uuid), + ("Id".into(), cipher.uuid.to_string().into()), + ("UserId".into(), user_id), + ("OrganizationId".into(), org_id), ("CollectionIds".into(), collection_uuids), ("RevisionDate".into(), revision_date), ], ut, - Some(acting_device_uuid.into()), + Some(acting_device_id.clone()), ); 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 { - push_cipher_update(ut, cipher, acting_device_uuid, conn).await; + if CONFIG.push_enabled() && user_ids.len() == 1 { + push_cipher_update(ut, cipher, acting_device_id, conn).await; } } @@ -462,20 +462,20 @@ impl WebSocketUsers { &self, ut: UpdateType, send: &DbSend, - user_uuids: &[String], - acting_device_uuid: &String, + user_ids: &[UserId], + acting_device_id: &DeviceId, conn: &mut DbConn, ) { // Skip any processing if both WebSockets and Push are not active if *NOTIFICATIONS_DISABLED { return; } - let user_uuid = convert_option(send.user_uuid.clone()); + let user_id = convert_option(send.user_uuid.as_deref()); let data = create_update( vec![ - ("Id".into(), send.uuid.clone().into()), - ("UserId".into(), user_uuid), + ("Id".into(), send.uuid.to_string().into()), + ("UserId".into(), user_id), ("RevisionDate".into(), serialize_date(send.revision_date)), ], ut, @@ -483,20 +483,20 @@ 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 { - push_send_update(ut, send, acting_device_uuid, conn).await; + if CONFIG.push_enabled() && user_ids.len() == 1 { + push_send_update(ut, send, acting_device_id, conn).await; } } pub async fn send_auth_request( &self, - user_uuid: &String, + user_id: &UserId, auth_request_uuid: &String, - acting_device_uuid: &String, + acting_device_id: &DeviceId, conn: &mut DbConn, ) { // Skip any processing if both WebSockets and Push are not active @@ -504,24 +504,24 @@ 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_id.to_string().into())], UpdateType::AuthRequest, - Some(acting_device_uuid.to_string()), + Some(acting_device_id.clone()), ); 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.to_string(), 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: &String, + user_id: &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 @@ -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_id.to_string().into())], UpdateType::AuthRequestResponse, - approving_device_uuid.clone().into(), + Some(approving_device_uuid.clone()), ); if CONFIG.enable_websocket() { - self.send_update(auth_response_uuid, &data).await; + self.send_update(user_id, &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_id.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_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.clone().into())], + vec![("Id".into(), auth_response_uuid.to_owned().into()), ("UserId".into(), user_id.to_string().into())], UpdateType::AuthRequestResponse, - user_uuid.to_string(), + user_id.clone(), ); - self.send_update(auth_response_uuid, &data).await; + self.send_update(user_id, &data).await; } } @@ -579,14 +578,14 @@ impl AnonymousWebSocketSubscriptions { "ReceiveMessage", // Target [ // Arguments { - "ContextId": acting_device_uuid || Nil, + "ContextId": acting_device_id || Nil, "Type": ut as i32, "Payload": {} } ] ] */ -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_id: Option) -> Vec { use rmpv::Value as V; let value = V::Array(vec![ @@ -595,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_id.map(|v| v.to_string().into()).unwrap_or_else(|| V::Nil)), ("Type".into(), (ut as i32).into()), ("Payload".into(), payload.into()), ])]), @@ -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..95a536f1fb 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, DeviceId, Folder, Send, User, UserId}, http_client::make_http_request, util::format_date, CONFIG, @@ -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: &String, + 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.to_string().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: &String, + 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,14 +230,14 @@ 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_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, @@ -284,38 +284,38 @@ 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_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: String, - auth_request_uuid: String, - approving_device_uuid: String, + 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.as_str(), 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 edbffbbddb..ebb0b0e0c6 100644 --- a/src/api/web.rs +++ b/src/api/web.rs @@ -12,8 +12,9 @@ use serde_json::Value; use crate::{ api::{core::now, ApiResult, EmptyResult}, auth::decode_file_download, + db::models::{AttachmentId, CipherId}, error::Error, - util::{Cached, SafeString}, + util::Cached, CONFIG, }; @@ -158,16 +159,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: SafeString, file_id: SafeString, 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).join(file_id)).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 fda4cb0224..2d5f79fc03 100644 --- a/src/auth.rs +++ b/src/auth.rs @@ -14,6 +14,10 @@ use std::{ net::IpAddr, }; +use crate::db::models::{ + AttachmentId, CipherId, CollectionId, DeviceId, EmergencyAccessId, MembershipId, OrgApiKeyId, OrganizationId, + SendFileId, SendId, UserId, +}; use crate::{error::Error, CONFIG}; const JWT_ALGORITHM: Algorithm = Algorithm::RS256; @@ -150,7 +154,7 @@ pub struct LoginJwtClaims { // Issuer pub iss: String, // Subject - pub sub: String, + pub sub: UserId, pub premium: bool, pub name: String, @@ -171,7 +175,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" ] @@ -187,19 +191,19 @@ pub struct InviteJwtClaims { // Issuer pub iss: String, // Subject - pub sub: String, + pub sub: UserId, pub email: String, - pub org_id: Option, - pub member_id: Option, + pub org_id: Option, + pub member_id: Option, pub invited_by_email: Option, } pub fn generate_invite_claims( - uuid: String, + user_id: UserId, email: String, - org_id: Option, - member_id: Option, + org_id: Option, + member_id: Option, invited_by_email: Option, ) -> InviteJwtClaims { let time_now = Utc::now(); @@ -208,7 +212,7 @@ pub fn generate_invite_claims( nbf: time_now.timestamp(), exp: (time_now + TimeDelta::try_hours(expire_hours).unwrap()).timestamp(), iss: JWT_INVITE_ISSUER.to_string(), - sub: uuid, + sub: user_id, email, org_id, member_id, @@ -225,18 +229,18 @@ pub struct EmergencyAccessInviteJwtClaims { // Issuer pub iss: String, // Subject - pub sub: String, + pub sub: UserId, pub email: String, - pub emer_id: String, + pub emer_id: EmergencyAccessId, pub grantor_name: String, pub grantor_email: String, } pub fn generate_emergency_access_invite_claims( - uuid: String, + user_id: UserId, email: String, - emer_id: String, + emer_id: EmergencyAccessId, grantor_name: String, grantor_email: String, ) -> EmergencyAccessInviteJwtClaims { @@ -246,7 +250,7 @@ pub fn generate_emergency_access_invite_claims( nbf: time_now.timestamp(), exp: (time_now + TimeDelta::try_hours(expire_hours).unwrap()).timestamp(), iss: JWT_EMERGENCY_ACCESS_INVITE_ISSUER.to_string(), - sub: uuid, + sub: user_id, email, emer_id, grantor_name, @@ -263,21 +267,24 @@ pub struct OrgApiKeyLoginJwtClaims { // Issuer pub iss: String, // Subject - pub sub: String, + pub sub: OrgApiKeyId, 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( + 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, - client_id: format!("organization.{org_id}"), + sub: org_api_key_uuid, + client_id: format!("organization.{}", org_id), client_sub: org_id, scope: vec!["api.organization".into()], } @@ -292,18 +299,18 @@ pub struct FileDownloadClaims { // Issuer pub iss: String, // Subject - pub sub: String, + pub sub: CipherId, - pub file_id: String, + pub file_id: AttachmentId, } -pub fn generate_file_download_claims(uuid: String, file_id: String) -> FileDownloadClaims { +pub fn generate_file_download_claims(cipher_id: CipherId, file_id: AttachmentId) -> FileDownloadClaims { let time_now = Utc::now(); FileDownloadClaims { nbf: time_now.timestamp(), exp: (time_now + TimeDelta::try_minutes(5).unwrap()).timestamp(), iss: JWT_FILE_DOWNLOAD_ISSUER.to_string(), - sub: uuid, + sub: cipher_id, file_id, } } @@ -331,14 +338,14 @@ pub fn generate_delete_claims(uuid: String) -> BasicJwtClaims { } } -pub fn generate_verify_email_claims(uuid: String) -> BasicJwtClaims { +pub fn generate_verify_email_claims(user_id: UserId) -> BasicJwtClaims { let time_now = Utc::now(); let expire_hours = i64::from(CONFIG.invitation_expiration_hours()); BasicJwtClaims { nbf: time_now.timestamp(), exp: (time_now + TimeDelta::try_hours(expire_hours).unwrap()).timestamp(), iss: JWT_VERIFYEMAIL_ISSUER.to_string(), - sub: uuid, + sub: user_id.to_string(), } } @@ -352,7 +359,7 @@ pub fn generate_admin_claims() -> BasicJwtClaims { } } -pub fn generate_send_claims(send_id: &str, file_id: &str) -> BasicJwtClaims { +pub fn generate_send_claims(send_id: &SendId, file_id: &SendFileId) -> BasicJwtClaims { let time_now = Utc::now(); BasicJwtClaims { nbf: time_now.timestamp(), @@ -475,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") }; @@ -549,17 +556,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 +581,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 @@ -648,16 +655,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()); } } @@ -762,11 +769,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/crypto.rs b/src/crypto.rs index 99f0fb91c9..eff1785f3e 100644 --- a/src/crypto.rs +++ b/src/crypto.rs @@ -84,14 +84,15 @@ 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 } -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 65855cc0a9..c86bd636e7 100644 --- a/src/db/models/attachment.rs +++ b/src/db/models/attachment.rs @@ -1,8 +1,11 @@ use std::io::ErrorKind; use bigdecimal::{BigDecimal, ToPrimitive}; +use derive_more::{AsRef, Deref, Display}; +use rocket::request::FromParam; use serde_json::Value; +use super::{CipherId, OrganizationId, UserId}; use crate::CONFIG; db_object! { @@ -11,8 +14,8 @@ db_object! { #[diesel(treat_none_as_null = true)] #[diesel(primary_key(id))] pub struct Attachment { - pub id: String, - pub cipher_uuid: String, + pub id: AttachmentId, + pub cipher_uuid: CipherId, pub file_name: String, // encrypted pub file_size: i64, pub akey: Option, @@ -21,7 +24,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: AttachmentId, + cipher_uuid: CipherId, + file_name: String, + file_size: i64, + akey: Option, + ) -> Self { Self { id, cipher_uuid, @@ -117,14 +126,14 @@ 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?; } 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())) @@ -134,7 +143,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)) @@ -144,7 +153,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))) @@ -161,7 +170,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))) @@ -172,7 +181,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 +198,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 +212,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: &UserId, + org_uuids: &Vec, + conn: &mut DbConn, + ) -> Vec { db_run! { conn: { attachments::table .left_join(ciphers::table.on(ciphers::uuid.eq(attachments::cipher_uuid))) @@ -216,3 +229,19 @@ impl Attachment { }} } } + +#[derive(Clone, Debug, AsRef, Deref, DieselNewType, Display, FromForm, Hash, PartialEq, Eq, Serialize, Deserialize)] +pub struct AttachmentId(pub String); + +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/auth_request.rs b/src/db/models/auth_request.rs index 3aca20cbe8..3417d07eca 100644 --- a/src/db/models/auth_request.rs +++ b/src/db/models/auth_request.rs @@ -1,5 +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)] @@ -7,15 +10,15 @@ db_object! { #[diesel(treat_none_as_null = true)] #[diesel(primary_key(uuid))] pub struct AuthRequest { - pub uuid: String, - pub user_uuid: String, - pub organization_uuid: Option, + pub uuid: AuthRequestId, + 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, @@ -33,8 +36,8 @@ db_object! { impl AuthRequest { pub fn new( - user_uuid: String, - request_device_identifier: String, + user_uuid: UserId, + request_device_identifier: DeviceId, device_type: i32, request_ip: String, access_code: String, @@ -43,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, @@ -101,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)) @@ -111,7 +114,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: &AuthRequestId, user_uuid: &UserId, conn: &mut DbConn) -> Option { db_run! {conn: { auth_requests::table .filter(auth_requests::uuid.eq(uuid)) @@ -122,7 +125,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)) @@ -157,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/cipher.rs b/src/db/models/cipher.rs index 04d3b4048c..447ca5b699 100644 --- a/src/db/models/cipher.rs +++ b/src/db/models/cipher.rs @@ -1,10 +1,13 @@ 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 super::{ - Attachment, CollectionCipher, Favorite, FolderCipher, Group, Membership, MembershipStatus, MembershipType, User, + Attachment, CollectionCipher, CollectionId, Favorite, FolderCipher, FolderId, Group, Membership, MembershipStatus, + MembershipType, OrganizationId, User, UserId, }; use crate::api::core::{CipherData, CipherSyncData, CipherSyncType}; @@ -17,12 +20,12 @@ 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, - pub user_uuid: Option, - pub organization_uuid: Option, + pub user_uuid: Option, + pub organization_uuid: Option, pub key: Option, @@ -57,7 +60,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, @@ -135,7 +138,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, @@ -302,7 +305,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 @@ -351,7 +354,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 }); @@ -380,7 +383,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) => { @@ -458,7 +461,7 @@ impl Cipher { }} } - 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 { // TODO: Optimize this by executing a DELETE directly on the database, instead of first fetching. for cipher in Self::find_by_org(org_uuid, conn).await { cipher.delete(conn).await?; @@ -466,7 +469,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?; } @@ -484,42 +487,49 @@ 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) { // 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 } } } /// 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 { @@ -538,7 +548,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 { @@ -562,7 +572,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)> { @@ -622,7 +632,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. @@ -639,7 +649,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(); } @@ -665,43 +675,43 @@ 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) .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() }} } - 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)) @@ -711,7 +721,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: &OrganizationId, conn: &mut DbConn) -> Option { db_run! {conn: { ciphers::table .filter(ciphers::uuid.eq(cipher_uuid)) @@ -734,7 +744,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 @@ -816,12 +826,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( @@ -832,7 +842,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)) @@ -843,7 +853,7 @@ impl Cipher { }} } - 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: { ciphers::table .filter(ciphers::organization_uuid.eq(org_uuid)) @@ -851,7 +861,7 @@ impl Cipher { }} } - 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: { ciphers::table .filter(ciphers::organization_uuid.eq(org_uuid)) @@ -862,7 +872,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)) @@ -880,7 +890,7 @@ impl Cipher { }} } - pub async fn get_collections(&self, user_id: String, conn: &mut DbConn) -> Vec { + pub async fn get_collections(&self, user_uuid: UserId, conn: &mut DbConn) -> Vec { if CONFIG.org_groups_enabled() { db_run! {conn: { ciphers_collections::table @@ -890,11 +900,11 @@ impl Cipher { )) .left_join(users_organizations::table.on( users_organizations::org_uuid.eq(collections::org_uuid) - .and(users_organizations::user_uuid.eq(user_id.clone())) + .and(users_organizations::user_uuid.eq(user_uuid.clone())) )) .left_join(users_collections::table.on( users_collections::collection_uuid.eq(ciphers_collections::collection_uuid) - .and(users_collections::user_uuid.eq(user_id.clone())) + .and(users_collections::user_uuid.eq(user_uuid.clone())) )) .left_join(groups_users::table.on( groups_users::users_organizations_uuid.eq(users_organizations::uuid) @@ -905,14 +915,14 @@ impl Cipher { .and(collections_groups::groups_uuid.eq(groups::uuid)) )) .filter(users_organizations::access_all.eq(true) // User has access all - .or(users_collections::user_uuid.eq(user_id) // User has access to collection + .or(users_collections::user_uuid.eq(user_uuid) // User has access to collection .and(users_collections::read_only.eq(false))) .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))) ) .select(ciphers_collections::collection_uuid) - .load::(conn).unwrap_or_default() + .load::(conn).unwrap_or_default() }} } else { db_run! {conn: { @@ -923,23 +933,23 @@ impl Cipher { )) .inner_join(users_organizations::table.on( users_organizations::org_uuid.eq(collections::org_uuid) - .and(users_organizations::user_uuid.eq(user_id.clone())) + .and(users_organizations::user_uuid.eq(user_uuid.clone())) )) .left_join(users_collections::table.on( users_collections::collection_uuid.eq(ciphers_collections::collection_uuid) - .and(users_collections::user_uuid.eq(user_id.clone())) + .and(users_collections::user_uuid.eq(user_uuid.clone())) )) .filter(users_organizations::access_all.eq(true) // User has access all - .or(users_collections::user_uuid.eq(user_id) // User has access to collection + .or(users_collections::user_uuid.eq(user_uuid) // User has access to collection .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_uuid: UserId, conn: &mut DbConn) -> Vec { if CONFIG.org_groups_enabled() { db_run! {conn: { ciphers_collections::table @@ -949,11 +959,11 @@ impl Cipher { )) .left_join(users_organizations::table.on( users_organizations::org_uuid.eq(collections::org_uuid) - .and(users_organizations::user_uuid.eq(user_id.clone())) + .and(users_organizations::user_uuid.eq(user_uuid.clone())) )) .left_join(users_collections::table.on( users_collections::collection_uuid.eq(ciphers_collections::collection_uuid) - .and(users_collections::user_uuid.eq(user_id.clone())) + .and(users_collections::user_uuid.eq(user_uuid.clone())) )) .left_join(groups_users::table.on( groups_users::users_organizations_uuid.eq(users_organizations::uuid) @@ -964,7 +974,7 @@ impl Cipher { .and(collections_groups::groups_uuid.eq(groups::uuid)) )) .filter(users_organizations::access_all.eq(true) // User has access all - .or(users_collections::user_uuid.eq(user_id) // User has access to collection + .or(users_collections::user_uuid.eq(user_uuid) // User has access to collection .and(users_collections::read_only.eq(false))) .or(groups::access_all.eq(true)) // Access via groups .or(collections_groups::collections_uuid.is_not_null() // Access via groups @@ -972,7 +982,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: { @@ -983,26 +993,29 @@ impl Cipher { )) .inner_join(users_organizations::table.on( users_organizations::org_uuid.eq(collections::org_uuid) - .and(users_organizations::user_uuid.eq(user_id.clone())) + .and(users_organizations::user_uuid.eq(user_uuid.clone())) )) .left_join(users_collections::table.on( users_collections::collection_uuid.eq(ciphers_collections::collection_uuid) - .and(users_collections::user_uuid.eq(user_id.clone())) + .and(users_collections::user_uuid.eq(user_uuid.clone())) )) .filter(users_organizations::access_all.eq(true) // User has access all - .or(users_collections::user_uuid.eq(user_id) // User has access to collection + .or(users_collections::user_uuid.eq(user_uuid) // User has access to collection .and(users_collections::read_only.eq(false))) .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_uuid: UserId, + conn: &mut DbConn, + ) -> Vec<(CipherId, CollectionId)> { db_run! {conn: { ciphers_collections::table .inner_join(collections::table.on( @@ -1010,12 +1023,12 @@ impl Cipher { )) .inner_join(users_organizations::table.on( users_organizations::org_uuid.eq(collections::org_uuid).and( - users_organizations::user_uuid.eq(user_id.clone()) + users_organizations::user_uuid.eq(user_uuid.clone()) ) )) .left_join(users_collections::table.on( users_collections::collection_uuid.eq(ciphers_collections::collection_uuid).and( - users_collections::user_uuid.eq(user_id.clone()) + users_collections::user_uuid.eq(user_uuid.clone()) ) )) .left_join(groups_users::table.on( @@ -1029,14 +1042,32 @@ impl Cipher { collections_groups::groups_uuid.eq(groups::uuid) ) )) - .or_filter(users_collections::user_uuid.eq(user_id)) // User has access to collection + .or_filter(users_collections::user_uuid.eq(user_uuid)) // User has access to collection .or_filter(users_organizations::access_all.eq(true)) // User has access all .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) .distinct() - .load::<(String, String)>(conn).unwrap_or_default() + .load::<(CipherId, CollectionId)>(conn).unwrap_or_default() }} } } + +#[derive( + Clone, Debug, AsRef, Deref, DieselNewType, Display, From, FromForm, Hash, PartialEq, Eq, Serialize, Deserialize, +)] +pub struct CipherId(String); + +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 59967c9688..59d29eb267 100644 --- a/src/db/models/collection.rs +++ b/src/db/models/collection.rs @@ -1,6 +1,11 @@ +use derive_more::{AsRef, Deref, Display, From}; +use rocket::request::FromParam; use serde_json::Value; -use super::{CollectionGroup, GroupUser, Membership, MembershipStatus, MembershipType, User}; +use super::{ + CipherId, CollectionGroup, GroupUser, Membership, MembershipId, MembershipStatus, MembershipType, OrganizationId, + User, UserId, +}; use crate::CONFIG; db_object! { @@ -8,8 +13,8 @@ db_object! { #[diesel(table_name = collections)] #[diesel(primary_key(uuid))] pub struct Collection { - pub uuid: String, - pub org_uuid: String, + pub uuid: CollectionId, + pub org_uuid: OrganizationId, pub name: String, pub external_id: Option, } @@ -18,8 +23,8 @@ db_object! { #[diesel(table_name = users_collections)] #[diesel(primary_key(user_uuid, collection_uuid))] pub struct CollectionUser { - pub user_uuid: String, - pub collection_uuid: String, + pub user_uuid: UserId, + pub collection_uuid: CollectionId, pub read_only: bool, pub hide_passwords: bool, } @@ -28,16 +33,16 @@ db_object! { #[diesel(table_name = ciphers_collections)] #[diesel(primary_key(cipher_uuid, collection_uuid))] pub struct CollectionCipher { - pub cipher_uuid: String, - pub collection_uuid: String, + pub cipher_uuid: CipherId, + pub collection_uuid: CollectionId, } } /// 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(), + uuid: CollectionId(crate::util::get_uuid()), org_uuid, name, external_id: None, @@ -74,7 +79,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 { @@ -121,7 +126,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 @@ -185,7 +190,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?; } @@ -198,7 +203,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)) @@ -208,7 +213,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 @@ -279,15 +284,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: &UserId, + 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 +306,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 +317,11 @@ 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: &CollectionId, + org_uuid: &OrganizationId, + conn: &mut DbConn, + ) -> Option { db_run! { conn: { collections::table .filter(collections::uuid.eq(uuid)) @@ -320,7 +333,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: &CollectionId, user_uuid: UserId, conn: &mut DbConn) -> Option { if CONFIG.org_groups_enabled() { db_run! { conn: { collections::table @@ -387,7 +400,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: { @@ -449,7 +462,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 @@ -498,7 +511,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: &UserId, + conn: &mut DbConn, + ) -> Vec { db_run! { conn: { users_collections::table .filter(users_collections::user_uuid.eq(user_uuid)) @@ -511,8 +528,11 @@ impl CollectionUser { }} } - pub async fn find_by_organization_swap_user_uuid_with_member_uuid(org_uuid: &str, 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)) @@ -521,12 +541,13 @@ impl CollectionUser { .load::(conn) .expect("Error loading users_collections") .from_db() - }} + }}; + col_users.into_iter().map(|c| c.into()).collect() } pub async fn save( - user_uuid: &str, - collection_uuid: &str, + user_uuid: &UserId, + collection_uuid: &CollectionId, read_only: bool, hide_passwords: bool, conn: &mut DbConn, @@ -596,7 +617,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)) @@ -608,10 +629,10 @@ 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: { + ) -> 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))) @@ -619,12 +640,13 @@ 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( - collection_uuid: &str, - user_uuid: &str, + collection_uuid: &CollectionId, + user_uuid: &UserId, conn: &mut DbConn, ) -> Option { db_run! { conn: { @@ -638,7 +660,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)) @@ -649,7 +671,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; } @@ -661,7 +683,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: &UserId, + 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: { @@ -677,14 +703,18 @@ 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: &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: &CipherId, collection_uuid: &CollectionId, conn: &mut DbConn) -> EmptyResult { Self::update_users_revision(collection_uuid, conn).await; db_run! { conn: @@ -714,7 +744,7 @@ impl CollectionCipher { } } - pub async fn delete(cipher_uuid: &str, collection_uuid: &str, 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: { @@ -728,7 +758,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) @@ -736,7 +766,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) @@ -744,9 +774,60 @@ 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; } } } + +// 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, membership_type: i32) -> Value { + json!({ + "id": self.membership_uuid, + "readOnly": self.read_only, + "hidePasswords": self.hide_passwords, + "manage": membership_type >= MembershipType::Admin + || (membership_type == MembershipType::Manager + && !self.read_only + && !self.hide_passwords), + }) + } +} + +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, +)] +pub struct CollectionId(String); + +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/device.rs b/src/db/models/device.rs index efb3380d93..69c96bec0d 100644 --- a/src/db/models/device.rs +++ b/src/db/models/device.rs @@ -1,7 +1,9 @@ use chrono::{NaiveDateTime, Utc}; +use derive_more::{Display, From}; +use rocket::request::FromParam; +use super::UserId; use crate::{crypto, CONFIG}; -use core::fmt; db_object! { #[derive(Identifiable, Queryable, Insertable, AsChangeset)] @@ -9,11 +11,11 @@ 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, - 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 +30,7 @@ db_object! { /// Local methods impl Device { - pub fn new(uuid: String, user_uuid: String, 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 { @@ -150,7 +152,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 +160,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: &DeviceId, user_uuid: &UserId, conn: &mut DbConn) -> Option { db_run! { conn: { devices::table .filter(devices::uuid.eq(uuid)) @@ -169,7 +171,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)) @@ -179,7 +181,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)) @@ -189,7 +191,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)) @@ -208,7 +210,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 +221,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 +232,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)) @@ -243,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 fmt::Display for DeviceType { - fn fmt(&self, f: &mut fmt::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 { @@ -338,3 +334,25 @@ 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 = (); + + #[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/emergency_access.rs b/src/db/models/emergency_access.rs index f4f3b9a9c2..29c688b990 100644 --- a/src/db/models/emergency_access.rs +++ b/src/db/models/emergency_access.rs @@ -1,9 +1,11 @@ use chrono::{NaiveDateTime, Utc}; +use derive_more::{AsRef, Deref, Display, From}; +use rocket::request::FromParam; use serde_json::Value; use crate::{api::EmptyResult, db::DbConn, error::MapResult}; -use super::User; +use super::{User, UserId}; db_object! { #[derive(Identifiable, Queryable, Insertable, AsChangeset)] @@ -11,9 +13,9 @@ db_object! { #[diesel(treat_none_as_null = true)] #[diesel(primary_key(uuid))] pub struct EmergencyAccess { - pub uuid: String, - pub grantor_uuid: String, - pub grantee_uuid: Option, + pub uuid: EmergencyAccessId, + pub grantor_uuid: UserId, + pub grantee_uuid: Option, pub email: Option, pub key_encrypted: Option, pub atype: i32, //EmergencyAccessType @@ -29,11 +31,11 @@ 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 { - uuid: crate::util::get_uuid(), + uuid: EmergencyAccessId(crate::util::get_uuid()), grantor_uuid, grantee_uuid: None, email: Some(email), @@ -82,7 +84,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 +213,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 +241,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 +264,11 @@ 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: &EmergencyAccessId, + grantor_uuid: &UserId, + conn: &mut DbConn, + ) -> Option { db_run! { conn: { emergency_access::table .filter(emergency_access::uuid.eq(uuid)) @@ -272,7 +278,11 @@ impl EmergencyAccess { }} } - pub async fn find_by_uuid_and_grantee_uuid(uuid: &str, grantee_uuid: &str, conn: &mut DbConn) -> Option { + pub async fn find_by_uuid_and_grantee_uuid( + uuid: &EmergencyAccessId, + grantee_uuid: &UserId, + conn: &mut DbConn, + ) -> Option { db_run! { conn: { emergency_access::table .filter(emergency_access::uuid.eq(uuid)) @@ -282,7 +292,11 @@ impl EmergencyAccess { }} } - pub async fn find_by_uuid_and_grantee_email(uuid: &str, grantee_email: &str, conn: &mut DbConn) -> Option { + pub async fn find_by_uuid_and_grantee_email( + uuid: &EmergencyAccessId, + grantee_email: &str, + conn: &mut DbConn, + ) -> Option { db_run! { conn: { emergency_access::table .filter(emergency_access::uuid.eq(uuid)) @@ -292,7 +306,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 +333,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 +341,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,10 +356,28 @@ 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 } } // endregion + +#[derive( + Clone, Debug, AsRef, Deref, DieselNewType, Display, From, FromForm, Hash, PartialEq, Eq, Serialize, Deserialize, +)] +pub struct EmergencyAccessId(String); + +impl<'r> FromParam<'r> for EmergencyAccessId { + type Error = (); + + #[inline(always)] + fn from_param(param: &'r str) -> Result { + if param.chars().all(|c| matches!(c, 'a'..='z' | 'A'..='Z' |'0'..='9' | '-')) { + Ok(Self(param.to_string())) + } else { + Err(()) + } + } +} diff --git a/src/db/models/event.rs b/src/db/models/event.rs index 0f9e0e107f..5fea7160bd 100644 --- a/src/db/models/event.rs +++ b/src/db/models/event.rs @@ -1,9 +1,9 @@ -use crate::db::DbConn; +use chrono::{NaiveDateTime, TimeDelta, Utc}; +//use derive_more::{AsRef, Deref, Display, From}; use serde_json::Value; -use crate::{api::EmptyResult, error::MapResult, CONFIG}; - -use chrono::{NaiveDateTime, TimeDelta, Utc}; +use super::{CipherId, CollectionId, GroupId, MembershipId, OrgPolicyId, OrganizationId, UserId}; +use crate::{api::EmptyResult, db::DbConn, error::MapResult, CONFIG}; // https://bitwarden.com/help/event-logs/ @@ -15,20 +15,20 @@ db_object! { #[diesel(table_name = event)] #[diesel(primary_key(uuid))] pub struct Event { - pub uuid: String, + pub uuid: EventId, 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 act_user_uuid: Option, + 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 act_user_uuid: Option, // Upstream enum: https://github.com/bitwarden/server/blob/8a22c0479e987e756ce7412c48a732f9002f0a2d/src/Core/Enums/DeviceType.cs pub device_type: Option, pub ip_address: Option, pub event_date: NaiveDateTime, - pub policy_uuid: Option, + pub policy_uuid: Option, pub provider_uuid: Option, pub provider_user_uuid: Option, pub provider_org_uuid: Option, @@ -128,7 +128,7 @@ impl Event { }; Self { - uuid: crate::util::get_uuid(), + uuid: EventId(crate::util::get_uuid()), event_type, user_uuid: None, org_uuid: None, @@ -246,7 +246,7 @@ impl Event { /// ############## /// Custom Queries pub async fn find_by_organization_uuid( - org_uuid: &str, + org_uuid: &OrganizationId, start: &NaiveDateTime, end: &NaiveDateTime, conn: &mut DbConn, @@ -263,7 +263,7 @@ impl Event { }} } - 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: { event::table .filter(event::org_uuid.eq(org_uuid)) @@ -275,8 +275,8 @@ impl Event { } pub async fn find_by_org_and_member( - org_uuid: &str, - member_uuid: &str, + org_uuid: &OrganizationId, + member_uuid: &MembershipId, start: &NaiveDateTime, end: &NaiveDateTime, conn: &mut DbConn, @@ -297,7 +297,7 @@ impl Event { } pub async fn find_by_cipher_uuid( - cipher_uuid: &str, + cipher_uuid: &CipherId, start: &NaiveDateTime, end: &NaiveDateTime, conn: &mut DbConn, @@ -327,3 +327,6 @@ impl Event { } } } + +#[derive(Clone, Debug, DieselNewType, FromForm, Hash, PartialEq, Eq, Serialize, Deserialize)] +pub struct EventId(String); diff --git a/src/db/models/favorite.rs b/src/db/models/favorite.rs index a301f5977b..de2e0febb0 100644 --- a/src/db/models/favorite.rs +++ b/src/db/models/favorite.rs @@ -1,12 +1,12 @@ -use super::User; +use super::{CipherId, 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 cipher_uuid: String, + pub user_uuid: UserId, + 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: &str, 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: &str, 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) @@ -71,7 +76,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,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: &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)) .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 4546072071..ea7208bd99 100644 --- a/src/db/models/folder.rs +++ b/src/db/models/folder.rs @@ -1,17 +1,19 @@ use chrono::{NaiveDateTime, Utc}; +use derive_more::{AsRef, Deref, Display, From}; +use rocket::request::FromParam; use serde_json::Value; -use super::User; +use super::{CipherId, User, UserId}; db_object! { #[derive(Identifiable, Queryable, Insertable, AsChangeset)] #[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: String, + pub user_uuid: UserId, pub name: String, } @@ -19,18 +21,18 @@ db_object! { #[diesel(table_name = folders_ciphers)] #[diesel(primary_key(cipher_uuid, folder_uuid))] pub struct FolderCipher { - pub cipher_uuid: String, - pub folder_uuid: String, + pub cipher_uuid: CipherId, + pub folder_uuid: FolderId, } } /// 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 { - uuid: crate::util::get_uuid(), + uuid: FolderId(crate::util::get_uuid()), created_at: now, updated_at: now, @@ -52,10 +54,10 @@ impl Folder { } impl FolderCipher { - pub fn new(folder_uuid: &str, cipher_uuid: &str) -> Self { + pub fn new(folder_uuid: FolderId, cipher_uuid: CipherId) -> Self { Self { - folder_uuid: folder_uuid.to_string(), - cipher_uuid: cipher_uuid.to_string(), + folder_uuid, + cipher_uuid, } } } @@ -113,14 +115,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: &FolderId, user_uuid: &UserId, conn: &mut DbConn) -> Option { db_run! { conn: { folders::table .filter(folders::uuid.eq(uuid)) @@ -131,7 +133,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)) @@ -177,7 +179,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) @@ -185,7 +187,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) @@ -193,7 +195,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: &FolderId, + cipher_uuid: &CipherId, + conn: &mut DbConn, + ) -> Option { db_run! { conn: { folders_ciphers::table .filter(folders_ciphers::folder_uuid.eq(folder_uuid)) @@ -204,7 +210,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)) @@ -216,14 +222,32 @@ 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<(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::<(String, String)>(conn) + .load::<(CipherId, FolderId)>(conn) .unwrap_or_default() }} } } + +#[derive( + Clone, Debug, AsRef, Deref, DieselNewType, Display, From, FromForm, Hash, PartialEq, Eq, Serialize, Deserialize, +)] +pub struct FolderId(String); + +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/group.rs b/src/db/models/group.rs index 526ceb6ec8..6720e9b7ce 100644 --- a/src/db/models/group.rs +++ b/src/db/models/group.rs @@ -1,8 +1,10 @@ -use super::{Membership, User}; +use super::{CollectionId, Membership, MembershipId, OrganizationId, User, UserId}; 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; db_object! { @@ -10,8 +12,8 @@ db_object! { #[diesel(table_name = groups)] #[diesel(primary_key(uuid))] pub struct Group { - pub uuid: String, - pub organizations_uuid: String, + pub uuid: GroupId, + pub organizations_uuid: OrganizationId, pub name: String, pub access_all: bool, pub external_id: Option, @@ -23,8 +25,8 @@ db_object! { #[diesel(table_name = collections_groups)] #[diesel(primary_key(collections_uuid, groups_uuid))] pub struct CollectionGroup { - pub collections_uuid: String, - pub groups_uuid: String, + pub collections_uuid: CollectionId, + pub groups_uuid: GroupId, pub read_only: bool, pub hide_passwords: bool, } @@ -33,18 +35,23 @@ db_object! { #[diesel(table_name = groups_users)] #[diesel(primary_key(groups_uuid, users_organizations_uuid))] pub struct GroupUser { - pub groups_uuid: String, - pub users_organizations_uuid: String + pub groups_uuid: GroupId, + pub users_organizations_uuid: MembershipId } } /// 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 { - uuid: crate::util::get_uuid(), + uuid: GroupId(crate::util::get_uuid()), organizations_uuid, name, access_all, @@ -111,7 +118,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: GroupId, read_only: bool, hide_passwords: bool) -> Self { Self { collections_uuid, groups_uuid, @@ -119,10 +126,22 @@ impl CollectionGroup { hide_passwords, } } + + pub fn to_json_details_for_group(&self) -> Value { + // If both read_only and hide_passwords are false, then manage should be true + // You can't have an entry with read_only and manage, or hide_passwords and manage + // Or an entry with everything to false + json!({ + "id": self.groups_uuid, + "readOnly": self.read_only, + "hidePasswords": self.hide_passwords, + "manage": !self.read_only && !self.hide_passwords, + }) + } } impl GroupUser { - pub fn new(groups_uuid: String, users_organizations_uuid: String) -> Self { + pub fn new(groups_uuid: GroupId, users_organizations_uuid: MembershipId) -> Self { Self { groups_uuid, users_organizations_uuid, @@ -166,27 +185,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() @@ -194,7 +213,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: &GroupId, org_uuid: &OrganizationId, conn: &mut DbConn) -> Option { db_run! { conn: { groups::table .filter(groups::uuid.eq(uuid)) @@ -205,7 +224,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)) @@ -216,7 +239,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( @@ -229,12 +252,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: &UserId, org_uuid: &OrganizationId, conn: &mut DbConn) -> bool { db_run! { conn: { groups::table .inner_join(groups_users::table.on( @@ -263,13 +286,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))) @@ -337,7 +360,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)) @@ -347,7 +370,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( @@ -364,7 +387,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)) @@ -390,7 +413,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; @@ -404,7 +427,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; @@ -469,7 +492,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)) @@ -479,10 +502,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() @@ -490,8 +513,8 @@ impl GroupUser { } pub async fn has_access_to_collection_by_member( - collection_uuid: &str, - member_uuid: &str, + collection_uuid: &CollectionId, + member_uuid: &MembershipId, conn: &mut DbConn, ) -> bool { db_run! { conn: { @@ -507,7 +530,11 @@ 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: &MembershipId, + conn: &mut DbConn, + ) -> bool { db_run! { conn: { groups_users::table .inner_join(groups::table.on( @@ -529,12 +556,12 @@ impl GroupUser { } } - pub async fn delete_by_group_id_and_user_id( - group_uuid: &str, - users_organizations_uuid: &str, + pub async fn delete_by_group_and_member( + group_uuid: &GroupId, + 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!"), }; @@ -542,13 +569,13 @@ 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") }} } - 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; @@ -562,7 +589,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!"), @@ -576,3 +603,21 @@ impl GroupUser { }} } } + +#[derive( + Clone, Debug, AsRef, Deref, DieselNewType, Display, From, FromForm, Hash, PartialEq, Eq, Serialize, Deserialize, +)] +pub struct GroupId(String); + +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 b42ed6058c..90d17313b7 100644 --- a/src/db/models/mod.rs +++ b/src/db/models/mod.rs @@ -16,20 +16,26 @@ mod two_factor_duo_context; mod two_factor_incomplete; 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::device::{Device, DeviceType}; -pub use self::emergency_access::{EmergencyAccess, EmergencyAccessStatus, EmergencyAccessType}; +pub use self::attachment::{Attachment, AttachmentId}; +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, EmergencyAccessId, EmergencyAccessStatus, EmergencyAccessType}; 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::org_policy::{OrgPolicy, OrgPolicyErr, OrgPolicyType}; -pub use self::organization::{Membership, MembershipStatus, MembershipType, Organization, OrganizationApiKey}; -pub use self::send::{Send, SendType}; +pub use self::folder::{Folder, FolderCipher, FolderId}; +pub use self::group::{CollectionGroup, Group, GroupId, GroupUser}; +pub use self::org_policy::{OrgPolicy, OrgPolicyErr, OrgPolicyId, OrgPolicyType}; +pub use self::organization::{ + Membership, MembershipId, MembershipStatus, MembershipType, OrgApiKeyId, Organization, OrganizationApiKey, + OrganizationId, +}; +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; -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 d5cb372c30..304b37422a 100644 --- a/src/db/models/org_policy.rs +++ b/src/db/models/org_policy.rs @@ -1,3 +1,4 @@ +use derive_more::{AsRef, From}; use serde::Deserialize; use serde_json::Value; @@ -5,15 +6,15 @@ use crate::api::EmptyResult; use crate::db::DbConn; use crate::error::MapResult; -use super::{Membership, MembershipStatus, MembershipType, TwoFactor}; +use super::{Membership, MembershipId, MembershipStatus, MembershipType, OrganizationId, TwoFactor, UserId}; db_object! { #[derive(Identifiable, Queryable, Insertable, AsChangeset)] #[diesel(table_name = org_policies)] #[diesel(primary_key(uuid))] pub struct OrgPolicy { - pub uuid: String, - pub org_uuid: String, + pub uuid: OrgPolicyId, + pub org_uuid: OrganizationId, pub atype: i32, pub enabled: bool, pub data: String, @@ -62,9 +63,9 @@ 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(), + uuid: OrgPolicyId(crate::util::get_uuid()), org_uuid, atype: atype as i32, enabled: false, @@ -142,7 +143,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)) @@ -152,7 +153,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( @@ -170,7 +171,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 +186,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) @@ -190,7 +195,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 { @@ -217,7 +222,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 { @@ -244,16 +249,16 @@ 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<&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; } @@ -267,8 +272,8 @@ impl OrgPolicy { } pub async fn is_user_allowed( - user_uuid: &str, - org_uuid: &str, + user_uuid: &UserId, + org_uuid: &OrganizationId, exclude_current_org: bool, conn: &mut DbConn, ) -> OrgPolicyResult { @@ -296,7 +301,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) => { @@ -312,7 +317,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 { @@ -332,7 +337,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; @@ -341,3 +350,6 @@ impl OrgPolicy { false } } + +#[derive(Clone, Debug, AsRef, DieselNewType, From, FromForm, PartialEq, Eq, Hash, Serialize, Deserialize)] +pub struct OrgPolicyId(String); diff --git a/src/db/models/organization.rs b/src/db/models/organization.rs index 80e0d674ea..7ad9b970cb 100644 --- a/src/db/models/organization.rs +++ b/src/db/models/organization.rs @@ -1,13 +1,17 @@ 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::{ cmp::Ordering, collections::{HashMap, HashSet}, }; -use super::{CollectionUser, Group, GroupUser, OrgPolicy, OrgPolicyType, TwoFactor, User}; -use crate::db::models::{Collection, CollectionGroup}; +use super::{ + CipherId, Collection, CollectionGroup, CollectionId, CollectionUser, Group, GroupId, GroupUser, OrgPolicy, + OrgPolicyType, TwoFactor, User, UserId, +}; use crate::CONFIG; db_object! { @@ -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, @@ -26,9 +30,9 @@ db_object! { #[diesel(table_name = users_organizations)] #[diesel(primary_key(uuid))] pub struct Membership { - pub uuid: String, - pub user_uuid: String, - pub org_uuid: String, + pub uuid: MembershipId, + pub user_uuid: UserId, + pub org_uuid: OrganizationId, pub access_all: bool, pub akey: String, @@ -42,8 +46,8 @@ db_object! { #[diesel(table_name = organization_api_key)] #[diesel(primary_key(uuid, org_uuid))] pub struct OrganizationApiKey { - pub uuid: String, - pub org_uuid: String, + pub uuid: OrgApiKeyId, + pub org_uuid: OrganizationId, pub atype: i32, pub api_key: String, pub revision_date: NaiveDateTime, @@ -149,7 +153,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, @@ -215,9 +219,9 @@ 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: UserId, org_uuid: OrganizationId) -> Self { Self { - uuid: crate::util::get_uuid(), + uuid: MembershipId(crate::util::get_uuid()), user_uuid, org_uuid, @@ -279,9 +283,9 @@ 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(), + uuid: OrgApiKeyId(crate::util::get_uuid()), org_uuid, atype: 0, // Type 0 is the default and only type we support currently @@ -360,7 +364,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)) @@ -484,8 +488,8 @@ 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() + 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, // so just act as if there are no groups. @@ -500,7 +504,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() @@ -508,7 +512,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) @@ -718,23 +722,27 @@ 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?; } 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?; } 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); } } @@ -754,7 +762,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)) @@ -763,7 +771,11 @@ 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: &MembershipId, + org_uuid: &OrganizationId, + conn: &mut DbConn, + ) -> Option { db_run! { conn: { users_organizations::table .filter(users_organizations::uuid.eq(uuid)) @@ -773,7 +785,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)) @@ -783,7 +795,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)) @@ -793,7 +805,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)) @@ -802,7 +814,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)) @@ -813,7 +825,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)) @@ -822,7 +834,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)) @@ -832,7 +844,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)) @@ -843,7 +855,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)) @@ -853,7 +869,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)) @@ -865,7 +885,11 @@ 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: &UserId, + org_uuid: &OrganizationId, + conn: &mut DbConn, + ) -> Option { db_run! { conn: { users_organizations::table .filter(users_organizations::user_uuid.eq(user_uuid)) @@ -875,7 +899,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: &UserId, + org_uuid: &OrganizationId, + conn: &mut DbConn, + ) -> Option { db_run! { conn: { users_organizations::table .filter(users_organizations::user_uuid.eq(user_uuid)) @@ -888,7 +916,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)) @@ -897,17 +925,21 @@ 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)) .select(users_organizations::org_uuid) - .load::(conn) + .load::(conn) .unwrap_or_default() }} } - 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( @@ -926,7 +958,11 @@ 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: &CipherId, + org_uuid: &OrganizationId, + conn: &mut DbConn, + ) -> Vec { db_run! { conn: { users_organizations::table .filter(users_organizations::org_uuid.eq(org_uuid)) @@ -949,7 +985,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: &CipherId, + org_uuid: &OrganizationId, + conn: &mut DbConn, + ) -> Vec { db_run! { conn: { users_organizations::table .filter(users_organizations::org_uuid.eq(org_uuid)) @@ -975,7 +1015,11 @@ 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: &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())))) @@ -987,7 +1031,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: &CollectionId, + org_uuid: &OrganizationId, + conn: &mut DbConn, + ) -> Vec { db_run! { conn: { users_organizations::table .filter(users_organizations::org_uuid.eq(org_uuid)) @@ -1004,7 +1052,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( @@ -1050,7 +1102,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)) @@ -1059,7 +1111,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) @@ -1068,6 +1120,45 @@ impl OrganizationApiKey { } } +#[derive( + Clone, Debug, AsRef, Deref, DieselNewType, Display, From, FromForm, Hash, PartialEq, Eq, Serialize, Deserialize, +)] +#[deref(forward)] +#[from(forward)] +pub struct OrganizationId(String); + +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(Self(param.to_string())) + } else { + Err(()) + } + } +} + +#[derive(Clone, Debug, Deref, DieselNewType, Display, From, FromForm, Hash, PartialEq, Eq, Serialize, Deserialize)] +pub struct MembershipId(String); + +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(()) + } + } +} + +#[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/send.rs b/src/db/models/send.rs index fb95f86b8f..f1ba0c0cca 100644 --- a/src/db/models/send.rs +++ b/src/db/models/send.rs @@ -3,7 +3,8 @@ use serde_json::Value; use crate::util::LowerCase; -use super::User; +use super::{OrganizationId, User, UserId}; +use id::SendId; db_object! { #[derive(Identifiable, Queryable, Insertable, AsChangeset)] @@ -11,11 +12,10 @@ db_object! { #[diesel(treat_none_as_null = true)] #[diesel(primary_key(uuid))] pub struct Send { - pub uuid: String, - - pub user_uuid: Option, - pub organization_uuid: Option, + pub uuid: SendId, + pub user_uuid: Option, + pub organization_uuid: Option, pub name: String, pub notes: Option, @@ -51,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, @@ -243,7 +243,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) => { @@ -257,7 +257,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?; } @@ -273,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)) @@ -290,7 +290,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: &SendId, user_uuid: &UserId, conn: &mut DbConn) -> Option { db_run! {conn: { sends::table .filter(sends::uuid.eq(uuid)) @@ -301,7 +301,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)) @@ -309,7 +309,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)] @@ -332,7 +332,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)) @@ -349,3 +349,59 @@ 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(()) + } + } + } + + #[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(()) + } + } + } +} diff --git a/src/db/models/two_factor.rs b/src/db/models/two_factor.rs index 9155c5183a..0f5a5de590 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! { @@ -7,8 +8,8 @@ db_object! { #[diesel(table_name = twofactor)] #[diesel(primary_key(uuid))] pub struct TwoFactor { - pub uuid: String, - pub user_uuid: String, + pub uuid: TwoFactorId, + pub user_uuid: UserId, pub atype: i32, pub enabled: bool, pub data: String, @@ -41,9 +42,9 @@ 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(), + uuid: TwoFactorId(crate::util::get_uuid()), user_uuid, atype: atype as i32, enabled: true, @@ -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) @@ -217,3 +218,6 @@ impl TwoFactor { Ok(()) } } + +#[derive(Clone, Debug, DieselNewType, FromForm, PartialEq, Eq, Hash, Serialize, Deserialize)] +pub struct TwoFactorId(String); diff --git a/src/db/models/two_factor_incomplete.rs b/src/db/models/two_factor_incomplete.rs index 12813eb5d3..b8dc4ad753 100644 --- a/src/db/models/two_factor_incomplete.rs +++ b/src/db/models/two_factor_incomplete.rs @@ -1,17 +1,26 @@ use chrono::{NaiveDateTime, Utc}; -use crate::{api::EmptyResult, auth::ClientIp, db::DbConn, error::MapResult, CONFIG}; +use crate::{ + api::EmptyResult, + auth::ClientIp, + db::{ + models::{DeviceId, 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. - pub device_uuid: String, + pub device_uuid: DeviceId, pub device_name: String, pub device_type: i32, pub login_time: NaiveDateTime, @@ -21,8 +30,8 @@ db_object! { impl TwoFactorIncomplete { pub async fn mark_incomplete( - user_uuid: &str, - device_uuid: &str, + user_uuid: &UserId, + device_uuid: &DeviceId, device_name: &str, device_type: i32, ip: &ClientIp, @@ -55,7 +64,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: &DeviceId, conn: &mut DbConn) -> EmptyResult { if CONFIG.incomplete_2fa_time_limit() <= 0 || !CONFIG.mail_enabled() { return Ok(()); } @@ -63,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: &str, 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)) @@ -88,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: &str, 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)) @@ -98,7 +115,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..411b9ac72a 100644 --- a/src/db/models/user.rs +++ b/src/db/models/user.rs @@ -1,9 +1,19 @@ -use crate::util::{format_date, get_uuid, retry}; use chrono::{NaiveDateTime, TimeDelta, Utc}; +use derive_more::{AsRef, Deref, Display, From}; +use rocket::request::FromParam; use serde_json::Value; -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 +21,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 +101,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 +224,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 +313,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 +332,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 +357,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 +379,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 +458,23 @@ impl Invitation { } } } + +#[derive( + Clone, Debug, AsRef, Deref, DieselNewType, Display, From, FromForm, Hash, PartialEq, Eq, Serialize, Deserialize, +)] +#[deref(forward)] +#[from(forward)] +pub struct UserId(String); + +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 a8e250c8a4..68ab7413a2 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, EmergencyAccessId, 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.clone()); 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.clone()); 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, }), )?; @@ -259,8 +259,8 @@ 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, + org_id: Option, + member_id: Option, org_name: &str, invited_by_email: Option, ) -> EmptyResult { @@ -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"); @@ -305,15 +313,15 @@ pub async fn send_invite( pub async fn send_emergency_access_invite( address: &str, - uuid: &str, - emer_id: &str, + user_id: UserId, + emer_id: EmergencyAccessId, grantor_name: &str, grantor_email: &str, ) -> EmptyResult { let claims = generate_emergency_access_invite_claims( - String::from(uuid), + user_id, String::from(address), - String::from(emer_id), + emer_id.clone(), String::from(grantor_name), String::from(grantor_email), ); @@ -323,7 +331,7 @@ pub async fn send_emergency_access_invite( { let mut query_params = query.query_pairs_mut(); query_params - .append_pair("id", emer_id) + .append_pair("id", &emer_id.to_string()) .append_pair("name", grantor_name) .append_pair("email", address) .append_pair("token", &encode_jwt(&claims)); 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, 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"];