浏览代码

rename membership and adopt newtype pattern (#5320)

* rename membership

rename UserOrganization to Membership to clarify the relation
and prevent confusion whether something refers to a member(ship) or user

* use newtype pattern

* implement custom derive macro IdFromParam

* add UuidFromParam macro for UUIDs

* add macros to Docker build

Co-authored-by: dfunkt <[email protected]>

---------

Co-authored-by: dfunkt <[email protected]>
Stefan Melmuk 9 月之前
父节点
当前提交
871a3f214a
共有 51 个文件被更改,包括 2225 次插入1562 次删除
  1. 1 0
      .dockerignore
  2. 43 0
      Cargo.lock
  3. 7 0
      Cargo.toml
  4. 1 0
      docker/Dockerfile.alpine
  5. 1 0
      docker/Dockerfile.debian
  6. 1 0
      docker/Dockerfile.j2
  7. 13 0
      macros/Cargo.toml
  8. 58 0
      macros/src/lib.rs
  9. 52 53
      src/api/admin.rs
  10. 83 74
      src/api/core/accounts.rs
  11. 226 190
      src/api/core/ciphers.rs
  12. 43 38
      src/api/core/emergency_access.rs
  13. 45 40
      src/api/core/events.rs
  14. 22 16
      src/api/core/folders.rs
  15. 226 200
      src/api/core/organizations.rs
  16. 29 35
      src/api/core/public.rs
  17. 44 38
      src/api/core/sends.rs
  18. 9 9
      src/api/core/two_factor/authenticator.rs
  19. 3 3
      src/api/core/two_factor/duo.rs
  20. 3 3
      src/api/core/two_factor/duo_oidc.rs
  21. 7 8
      src/api/core/two_factor/email.rs
  22. 12 13
      src/api/core/two_factor/mod.rs
  23. 3 3
      src/api/core/two_factor/protected_actions.rs
  24. 11 11
      src/api/core/two_factor/webauthn.rs
  25. 2 2
      src/api/core/two_factor/yubikey.rs
  26. 33 32
      src/api/identity.rs
  27. 60 61
      src/api/notifications.rs
  28. 34 34
      src/api/push.rs
  29. 6 5
      src/api/web.rs
  30. 66 59
      src/auth.rs
  31. 5 4
      src/crypto.rs
  32. 41 11
      src/db/models/attachment.rs
  33. 32 11
      src/db/models/auth_request.rs
  34. 117 84
      src/db/models/cipher.rs
  35. 147 69
      src/db/models/collection.rs
  36. 51 50
      src/db/models/device.rs
  37. 54 18
      src/db/models/emergency_access.rs
  38. 24 21
      src/db/models/event.rs
  39. 14 9
      src/db/models/favorite.rs
  40. 43 19
      src/db/models/folder.rs
  41. 95 50
      src/db/models/group.rs
  42. 18 12
      src/db/models/mod.rs
  43. 41 29
      src/db/models/org_policy.rs
  44. 228 136
      src/db/models/organization.rs
  45. 59 14
      src/db/models/send.rs
  46. 11 7
      src/db/models/two_factor.rs
  47. 26 9
      src/db/models/two_factor_incomplete.rs
  48. 44 24
      src/db/models/user.rs
  49. 28 20
      src/mail.rs
  50. 2 0
      src/main.rs
  51. 1 38
      src/util.rs

+ 1 - 0
.dockerignore

@@ -5,6 +5,7 @@
 !.git
 !docker/healthcheck.sh
 !docker/start.sh
+!macros
 !migrations
 !src
 

+ 43 - 0
Cargo.lock

@@ -752,6 +752,27 @@ dependencies = [
  "syn",
 ]
 
+[[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"
@@ -809,6 +830,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"
@@ -2013,6 +2045,14 @@ dependencies = [
  "linked-hash-map",
 ]
 
+[[package]]
+name = "macros"
+version = "0.1.0"
+dependencies = [
+ "quote",
+ "syn",
+]
+
 [[package]]
 name = "match_cfg"
 version = "0.1.0"
@@ -4011,7 +4051,9 @@ dependencies = [
  "dashmap",
  "data-encoding",
  "data-url",
+ "derive_more",
  "diesel",
+ "diesel-derive-newtype",
  "diesel_logger",
  "diesel_migrations",
  "dotenvy",
@@ -4028,6 +4070,7 @@ dependencies = [
  "lettre",
  "libsqlite3-sys",
  "log",
+ "macros",
  "mimalloc",
  "num-derive",
  "num-traits",

+ 7 - 0
Cargo.toml

@@ -1,3 +1,5 @@
+workspace = { members = ["macros"] }
+
 [package]
 name = "vaultwarden"
 version = "1.0.0"
@@ -39,6 +41,8 @@ unstable = []
 syslog = "7.0.0"
 
 [dependencies]
+macros = { path = "./macros" }
+
 # Logging
 log = "0.4.22"
 fern = { version = "0.7.1", features = ["syslog-7", "reopen-1"] }
@@ -78,6 +82,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 }
 

+ 1 - 0
docker/Dockerfile.alpine

@@ -76,6 +76,7 @@ RUN source /env-cargo && \
 
 # Copies over *only* your manifests and build files
 COPY ./Cargo.* ./rust-toolchain.toml ./build.rs ./
+COPY ./macros ./macros
 
 ARG CARGO_PROFILE=release
 

+ 1 - 0
docker/Dockerfile.debian

@@ -116,6 +116,7 @@ RUN source /env-cargo && \
 
 # Copies over *only* your manifests and build files
 COPY ./Cargo.* ./rust-toolchain.toml ./build.rs ./
+COPY ./macros ./macros
 
 ARG CARGO_PROFILE=release
 

+ 1 - 0
docker/Dockerfile.j2

@@ -143,6 +143,7 @@ RUN source /env-cargo && \
 
 # Copies over *only* your manifests and build files
 COPY ./Cargo.* ./rust-toolchain.toml ./build.rs ./
+COPY ./macros ./macros
 
 ARG CARGO_PROFILE=release
 

+ 13 - 0
macros/Cargo.toml

@@ -0,0 +1,13 @@
+[package]
+name = "macros"
+version = "0.1.0"
+edition = "2021"
+
+[lib]
+name = "macros"
+path = "src/lib.rs"
+proc-macro = true
+
+[dependencies]
+quote = "1.0.38"
+syn = "2.0.94"

+ 58 - 0
macros/src/lib.rs

@@ -0,0 +1,58 @@
+extern crate proc_macro;
+
+use proc_macro::TokenStream;
+use quote::quote;
+
+#[proc_macro_derive(UuidFromParam)]
+pub fn derive_uuid_from_param(input: TokenStream) -> TokenStream {
+    let ast = syn::parse(input).unwrap();
+
+    impl_derive_uuid_macro(&ast)
+}
+
+fn impl_derive_uuid_macro(ast: &syn::DeriveInput) -> TokenStream {
+    let name = &ast.ident;
+    let gen = quote! {
+        #[automatically_derived]
+        impl<'r> rocket::request::FromParam<'r> for #name {
+            type Error = ();
+
+            #[inline(always)]
+            fn from_param(param: &'r str) -> Result<Self, Self::Error> {
+                if uuid::Uuid::parse_str(param).is_ok() {
+                    Ok(Self(param.to_string()))
+                } else {
+                    Err(())
+                }
+            }
+        }
+    };
+    gen.into()
+}
+
+#[proc_macro_derive(IdFromParam)]
+pub fn derive_id_from_param(input: TokenStream) -> TokenStream {
+    let ast = syn::parse(input).unwrap();
+
+    impl_derive_safestring_macro(&ast)
+}
+
+fn impl_derive_safestring_macro(ast: &syn::DeriveInput) -> TokenStream {
+    let name = &ast.ident;
+    let gen = quote! {
+        #[automatically_derived]
+        impl<'r> rocket::request::FromParam<'r> for #name {
+            type Error = ();
+
+            #[inline(always)]
+            fn from_param(param: &'r str) -> Result<Self, Self::Error> {
+                if param.chars().all(|c| matches!(c, 'a'..='z' | 'A'..='Z' |'0'..='9' | '-')) {
+                    Ok(Self(param.to_string()))
+                } else {
+                    Err(())
+                }
+            }
+        }
+    };
+    gen.into()
+}

+ 52 - 53
src/api/admin.rs

@@ -50,7 +50,7 @@ pub fn routes() -> Vec<Route> {
         disable_user,
         enable_user,
         remove_2fa,
-        update_user_org_type,
+        update_membership_type,
         update_revision_users,
         post_config,
         delete_config,
@@ -280,8 +280,8 @@ struct InviteData {
     email: String,
 }
 
-async fn get_user_or_404(uuid: &str, conn: &mut DbConn) -> ApiResult<User> {
-    if let Some(user) = User::find_by_uuid(uuid, conn).await {
+async fn get_user_or_404(user_id: &UserId, conn: &mut DbConn) -> ApiResult<User> {
+    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,29 +381,29 @@ async fn get_user_by_mail_json(mail: &str, _token: AdminToken, mut conn: DbConn)
     }
 }
 
-#[get("/users/<uuid>")]
-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/<user_id>")]
+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/<uuid>/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/<user_id>/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 user_org records before deleting the actual user
-    let user_orgs = UserOrganization::find_any_state_by_user(uuid, &mut conn).await;
+    // Get the membership records before deleting the actual user
+    let memberships = Membership::find_any_state_by_user(&user_id, &mut conn).await;
     let res = user.delete(&mut conn).await;
 
-    for user_org in user_orgs {
+    for membership in memberships {
         log_event(
             EventType::OrganizationUserRemoved as i32,
-            &user_org.uuid,
-            &user_org.org_uuid,
-            ACTING_ADMIN_USER,
+            &membership.uuid,
+            &membership.org_uuid,
+            &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/<uuid>/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/<user_id>/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/<uuid>/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/<user_id>/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/<uuid>/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/<user_id>/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/<uuid>/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/<user_id>/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/<uuid>/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/<user_id>/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);
@@ -485,42 +485,41 @@ async fn resend_user_invite(uuid: &str, _token: AdminToken, mut conn: DbConn) ->
 }
 
 #[derive(Debug, Deserialize)]
-struct UserOrgTypeData {
+struct MembershipTypeData {
     user_type: NumberOrString,
-    user_uuid: String,
-    org_uuid: String,
+    user_uuid: UserId,
+    org_uuid: OrganizationId,
 }
 
 #[post("/users/org_type", data = "<data>")]
-async fn update_user_org_type(data: Json<UserOrgTypeData>, token: AdminToken, mut conn: DbConn) -> EmptyResult {
-    let data: UserOrgTypeData = data.into_inner();
+async fn update_membership_type(data: Json<MembershipTypeData>, token: AdminToken, mut conn: DbConn) -> EmptyResult {
+    let data: MembershipTypeData = data.into_inner();
 
-    let Some(mut user_to_edit) =
-        UserOrganization::find_by_user_and_org(&data.user_uuid, &data.org_uuid, &mut conn).await
+    let Some(mut member_to_edit) = Membership::find_by_user_and_org(&data.user_uuid, &data.org_uuid, &mut conn).await
     else {
         err!("The specified user isn't member of the organization")
     };
 
-    let new_type = match UserOrgType::from_str(&data.user_type.into_string()) {
+    let new_type = match MembershipType::from_str(&data.user_type.into_string()) {
         Some(new_type) => new_type as i32,
         None => err!("Invalid type"),
     };
 
-    if user_to_edit.atype == UserOrgType::Owner && new_type != UserOrgType::Owner {
+    if member_to_edit.atype == MembershipType::Owner && new_type != MembershipType::Owner {
         // Removing owner permission, check that there is at least one other confirmed owner
-        if UserOrganization::count_confirmed_by_org_and_type(&data.org_uuid, UserOrgType::Owner, &mut conn).await <= 1 {
+        if Membership::count_confirmed_by_org_and_type(&data.org_uuid, MembershipType::Owner, &mut conn).await <= 1 {
             err!("Can't change the type of the last owner")
         }
     }
 
-    // This check is also done at api::organizations::{accept_invite(), _confirm_invite, _activate_user(), edit_user()}, update_user_org_type
+    // This check is also done at api::organizations::{accept_invite, _confirm_invite, _activate_member, edit_member}, update_membership_type
     // It returns different error messages per function.
-    if new_type < UserOrgType::Admin {
-        match OrgPolicy::is_user_allowed(&user_to_edit.user_uuid, &user_to_edit.org_uuid, true, &mut conn).await {
+    if new_type < MembershipType::Admin {
+        match OrgPolicy::is_user_allowed(&member_to_edit.user_uuid, &member_to_edit.org_uuid, true, &mut conn).await {
             Ok(_) => {}
             Err(OrgPolicyErr::TwoFactorMissing) => {
                 if CONFIG.email_2fa_auto_fallback() {
-                    two_factor::email::find_and_activate_email_2fa(&user_to_edit.user_uuid, &mut conn).await?;
+                    two_factor::email::find_and_activate_email_2fa(&member_to_edit.user_uuid, &mut conn).await?;
                 } else {
                     err!("You cannot modify this user to this type because they have not setup 2FA");
                 }
@@ -533,17 +532,17 @@ async fn update_user_org_type(data: Json<UserOrgTypeData>, token: AdminToken, mu
 
     log_event(
         EventType::OrganizationUserUpdated as i32,
-        &user_to_edit.uuid,
+        &member_to_edit.uuid,
         &data.org_uuid,
-        ACTING_ADMIN_USER,
+        &ACTING_ADMIN_USER.into(),
         14, // Use UnknownBrowser type
         &token.ip.ip,
         &mut conn,
     )
     .await;
 
-    user_to_edit.atype = new_type;
-    user_to_edit.save(&mut conn).await
+    member_to_edit.atype = new_type;
+    member_to_edit.save(&mut conn).await
 }
 
 #[post("/users/update_revision")]
@@ -557,7 +556,7 @@ async fn organizations_overview(_token: AdminToken, mut conn: DbConn) -> ApiResu
     let mut organizations_json = Vec::with_capacity(organizations.len());
     for o in organizations {
         let mut org = o.to_json();
-        org["user_count"] = json!(UserOrganization::count_by_org(&o.uuid, &mut conn).await);
+        org["user_count"] = json!(Membership::count_by_org(&o.uuid, &mut conn).await);
         org["cipher_count"] = json!(Cipher::count_by_org(&o.uuid, &mut conn).await);
         org["collection_count"] = json!(Collection::count_by_org(&o.uuid, &mut conn).await);
         org["group_count"] = json!(Group::count_by_org(&o.uuid, &mut conn).await);
@@ -571,9 +570,9 @@ async fn organizations_overview(_token: AdminToken, mut conn: DbConn) -> ApiResu
     Ok(Html(text))
 }
 
-#[post("/organizations/<uuid>/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/<org_id>/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
 }
 

+ 83 - 74
src/api/core/accounts.rs

@@ -79,7 +79,7 @@ pub struct RegisterData {
     name: Option<String>,
     token: Option<String>,
     #[allow(dead_code)]
-    organization_user_id: Option<String>,
+    organization_user_id: Option<MembershipId>,
 }
 
 #[derive(Debug, Deserialize)]
@@ -106,15 +106,15 @@ fn enforce_password_hint_setting(password_hint: &Option<String>) -> EmptyResult
     }
     Ok(())
 }
-async fn is_email_2fa_required(org_user_uuid: Option<String>, conn: &mut DbConn) -> bool {
+async fn is_email_2fa_required(member_id: Option<MembershipId>, conn: &mut DbConn) -> bool {
     if !CONFIG._enable_email_2fa() {
         return false;
     }
     if CONFIG.email_2fa_enforce_on_verified_invite() {
         return true;
     }
-    if org_user_uuid.is_some() {
-        return OrgPolicy::is_enabled_for_member(&org_user_uuid.unwrap(), OrgPolicyType::TwoFactorAuthentication, conn)
+    if member_id.is_some() {
+        return OrgPolicy::is_enabled_for_member(&member_id.unwrap(), OrgPolicyType::TwoFactorAuthentication, conn)
             .await;
     }
     false
@@ -161,9 +161,9 @@ pub async fn _register(data: Json<RegisterData>, mut conn: DbConn) -> JsonResult
                     err!("Registration email does not match invite email")
                 }
             } else if Invitation::take(&email, &mut conn).await {
-                for user_org in UserOrganization::find_invited_by_user(&user.uuid, &mut conn).await.iter_mut() {
-                    user_org.status = UserOrgStatus::Accepted as i32;
-                    user_org.save(&mut conn).await?;
+                for membership in Membership::find_invited_by_user(&user.uuid, &mut conn).await.iter_mut() {
+                    membership.status = MembershipStatus::Accepted as i32;
+                    membership.save(&mut conn).await?;
                 }
                 user
             } else if CONFIG.is_signup_allowed(&email)
@@ -305,9 +305,9 @@ async fn put_avatar(data: Json<AvatarData>, headers: Headers, mut conn: DbConn)
     Ok(Json(user.to_json(&mut conn).await))
 }
 
-#[get("/users/<uuid>/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/<user_id>/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<ChangePassData>, 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<ChangeKdfData>, 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<String>,
+    id: Option<FolderId>,
     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,
 }
 
@@ -489,48 +489,49 @@ fn validate_keydata(
     existing_ciphers: &[Cipher],
     existing_folders: &[Folder],
     existing_emergency_access: &[EmergencyAccess],
-    existing_user_orgs: &[UserOrganization],
+    existing_memberships: &[Membership],
     existing_sends: &[Send],
 ) -> EmptyResult {
     // Check that we're correctly rotating all the user's ciphers
-    let existing_cipher_ids = existing_ciphers.iter().map(|c| c.uuid.as_str()).collect::<HashSet<_>>();
+    let existing_cipher_ids = existing_ciphers.iter().map(|c| &c.uuid).collect::<HashSet<&CipherId>>();
     let provided_cipher_ids = data
         .ciphers
         .iter()
         .filter(|c| c.organization_id.is_none())
-        .filter_map(|c| c.id.as_deref())
-        .collect::<HashSet<_>>();
+        .filter_map(|c| c.id.as_ref())
+        .collect::<HashSet<&CipherId>>();
     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::<HashSet<_>>();
-    let provided_folder_ids = data.folders.iter().filter_map(|f| f.id.as_deref()).collect::<HashSet<_>>();
+    let existing_folder_ids = existing_folders.iter().map(|f| &f.uuid).collect::<HashSet<&FolderId>>();
+    let provided_folder_ids = data.folders.iter().filter_map(|f| f.id.as_ref()).collect::<HashSet<&FolderId>>();
     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::<HashSet<_>>();
+        existing_emergency_access.iter().map(|ea| &ea.uuid).collect::<HashSet<&EmergencyAccessId>>();
     let provided_emergency_access_ids =
-        data.emergency_access_keys.iter().map(|ea| ea.id.as_str()).collect::<HashSet<_>>();
+        data.emergency_access_keys.iter().map(|ea| &ea.id).collect::<HashSet<&EmergencyAccessId>>();
     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_user_orgs.iter().map(|uo| uo.org_uuid.as_str()).collect::<HashSet<_>>();
+    let existing_reset_password_ids =
+        existing_memberships.iter().map(|m| &m.org_uuid).collect::<HashSet<&OrganizationId>>();
     let provided_reset_password_ids =
-        data.reset_password_keys.iter().map(|rp| rp.organization_id.as_str()).collect::<HashSet<_>>();
+        data.reset_password_keys.iter().map(|rp| &rp.organization_id).collect::<HashSet<&OrganizationId>>();
     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::<HashSet<_>>();
-    let provided_send_ids = data.sends.iter().filter_map(|s| s.id.as_deref()).collect::<HashSet<_>>();
+    let existing_send_ids = existing_sends.iter().map(|s| &s.uuid).collect::<HashSet<&SendId>>();
+    let provided_send_ids = data.sends.iter().filter_map(|s| s.id.as_ref()).collect::<HashSet<&SendId>>();
     if !provided_send_ids.is_superset(&existing_send_ids) {
         err!("All existing sends must be included in the rotation")
     }
@@ -553,24 +554,24 @@ async fn post_rotatekey(data: Json<KeyData>, 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_user_orgs = UserOrganization::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_user_orgs.retain(|uo| uo.reset_password_key.is_some());
-    let mut existing_sends = Send::find_by_user(user_uuid, &mut conn).await;
+    existing_memberships.retain(|m| m.reset_password_key.is_some());
+    let mut existing_sends = Send::find_by_user(user_id, &mut conn).await;
 
     validate_keydata(
         &data,
         &existing_ciphers,
         &existing_folders,
         &existing_emergency_access,
-        &existing_user_orgs,
+        &existing_memberships,
         &existing_sends,
     )?;
 
@@ -602,14 +603,14 @@ async fn post_rotatekey(data: Json<KeyData>, headers: Headers, mut conn: DbConn,
 
     // Update reset password data
     for reset_password_data in data.reset_password_keys {
-        let Some(user_org) =
-            existing_user_orgs.iter_mut().find(|uo| uo.org_uuid == reset_password_data.organization_id)
+        let Some(membership) =
+            existing_memberships.iter_mut().find(|m| m.org_uuid == reset_password_data.organization_id)
         else {
             err!("Reset password doesn't exist")
         };
 
-        user_org.reset_password_key = Some(reset_password_data.reset_password_key);
-        user_org.save(&mut conn).await?
+        membership.reset_password_key = Some(reset_password_data.reset_password_key);
+        membership.save(&mut conn).await?
     }
 
     // Update send data
@@ -650,7 +651,7 @@ async fn post_rotatekey(data: Json<KeyData>, 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<VerifyEmailTokenData>, 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<DeleteRecoverData>, 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<DeleteRecoverTokenData>, 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/<uuid>/token", data = "<data>")]
-async fn post_device_token(uuid: &str, data: Json<PushToken>, headers: Headers, conn: DbConn) -> EmptyResult {
-    put_device_token(uuid, data, headers, conn).await
+#[post("/devices/identifier/<device_id>/token", data = "<data>")]
+async fn post_device_token(device_id: DeviceId, data: Json<PushToken>, headers: Headers, conn: DbConn) -> EmptyResult {
+    put_device_token(device_id, data, headers, conn).await
 }
 
-#[put("/devices/identifier/<uuid>/token", data = "<data>")]
-async fn put_device_token(uuid: &str, data: Json<PushToken>, headers: Headers, mut conn: DbConn) -> EmptyResult {
+#[put("/devices/identifier/<device_id>/token", data = "<data>")]
+async fn put_device_token(
+    device_id: DeviceId,
+    data: Json<PushToken>,
+    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<PushToken>, headers: Headers, m
     Ok(())
 }
 
-#[put("/devices/identifier/<uuid>/clear-token")]
-async fn put_clear_device_token(uuid: &str, mut conn: DbConn) -> EmptyResult {
+#[put("/devices/identifier/<device_id>/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/<uuid>/clear-token")]
-async fn post_clear_device_token(uuid: &str, conn: DbConn) -> EmptyResult {
-    put_clear_device_token(uuid, conn).await
+#[post("/devices/identifier/<device_id>/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/<uuid>")]
-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/<auth_request_id>")]
+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<String>,
     request_approved: bool,
 }
 
-#[put("/auth-requests/<uuid>", data = "<data>")]
+#[put("/auth-requests/<auth_request_id>", data = "<data>")]
 async fn put_auth_request(
-    uuid: &str,
+    auth_request_id: AuthRequestId,
     data: Json<AuthResponseRequest>,
     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/<uuid>/response?<code>")]
+#[get("/auth-requests/<auth_request_id>/response?<code>")]
 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,

文件差异内容过多而无法显示
+ 226 - 190
src/api/core/ciphers.rs


+ 43 - 38
src/api/core/emergency_access.rs

@@ -93,10 +93,10 @@ async fn get_grantees(headers: Headers, mut conn: DbConn) -> Json<Value> {
 }
 
 #[get("/emergency-access/<emer_id>")]
-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/<emer_id>", data = "<data>")]
 async fn put_emergency_access(
-    emer_id: &str,
+    emer_id: EmergencyAccessId,
     data: Json<EmergencyAccessUpdateData>,
     headers: Headers,
     conn: DbConn,
@@ -128,7 +128,7 @@ async fn put_emergency_access(
 
 #[post("/emergency-access/<emer_id>", data = "<data>")]
 async fn post_emergency_access(
-    emer_id: &str,
+    emer_id: EmergencyAccessId,
     data: Json<EmergencyAccessUpdateData>,
     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/<emer_id>")]
-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/<emer_id>/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<EmergencyAccessInviteData>, 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<EmergencyAccessInviteData>, headers: Headers, mu
 }
 
 #[post("/emergency-access/<emer_id>/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/<emer_id>/accept", data = "<data>")]
-async fn accept_invite(emer_id: &str, data: Json<AcceptData>, headers: Headers, mut conn: DbConn) -> EmptyResult {
+async fn accept_invite(
+    emer_id: EmergencyAccessId,
+    data: Json<AcceptData>,
+    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<AcceptData>, 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/<emer_id>/confirm", data = "<data>")]
 async fn confirm_emergency_access(
-    emer_id: &str,
+    emer_id: EmergencyAccessId,
     data: Json<ConfirmData>,
     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/<emer_id>/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/<emer_id>/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/<emer_id>/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/<emer_id>/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/<emer_id>/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/<emer_id>/password", data = "<data>")]
 async fn password_emergency_access(
-    emer_id: &str,
+    emer_id: EmergencyAccessId,
     data: Json<EmergencyAccessPasswordData>,
     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.")
     };
@@ -662,9 +667,9 @@ async fn password_emergency_access(
     TwoFactor::delete_all_by_user(&grantor_user.uuid, &mut conn).await?;
 
     // Remove grantor from all organisations unless Owner
-    for user_org in UserOrganization::find_any_state_by_user(&grantor_user.uuid, &mut conn).await {
-        if user_org.atype != UserOrgType::Owner as i32 {
-            user_org.delete(&mut conn).await?;
+    for member in Membership::find_any_state_by_user(&grantor_user.uuid, &mut conn).await {
+        if member.atype != MembershipType::Owner as i32 {
+            member.delete(&mut conn).await?;
         }
     }
     Ok(())
@@ -673,10 +678,10 @@ async fn password_emergency_access(
 // endregion
 
 #[get("/emergency-access/<emer_id>/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
 }

+ 45 - 40
src/api/core/events.rs

@@ -8,7 +8,7 @@ use crate::{
     api::{EmptyResult, JsonResult},
     auth::{AdminHeaders, Headers},
     db::{
-        models::{Cipher, Event, UserOrganization},
+        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/<org_id>/events?<data..>")]
-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<Value> = 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/<cipher_id>/events?<data..>")]
-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<Value> = if !CONFIG.org_events_enabled() {
         Vec::with_capacity(0)
     } else {
         let mut events_json = Vec::with_capacity(0);
-        if UserOrganization::user_has_ge_admin_access_to_cipher(&headers.user.uuid, cipher_id, &mut conn).await {
+        if Membership::user_has_ge_admin_access_to_cipher(&headers.user.uuid, &cipher_id, &mut conn).await {
             let start_date = parse_date(&data.start);
             let end_date = if let Some(before_date) = &data.continuation_token {
                 parse_date(before_date)
@@ -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())
@@ -90,10 +95,10 @@ async fn get_cipher_events(cipher_id: &str, data: EventRange, headers: Headers,
     })))
 }
 
-#[get("/organizations/<org_id>/users/<user_org_id>/events?<data..>")]
+#[get("/organizations/<org_id>/users/<member_id>/events?<data..>")]
 async fn get_user_events(
-    org_id: &str,
-    user_org_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_user_org(org_id, user_org_id, &start_date, &end_date, &mut conn)
+        Event::find_by_org_and_member(&org_id, &member_id, &start_date, &end_date, &mut conn)
             .await
             .iter()
             .map(|e| e.to_json())
@@ -152,8 +157,8 @@ struct EventCollection {
     date: String,
 
     // Optional
-    cipher_id: Option<String>,
-    organization_id: Option<String>,
+    cipher_id: Option<CipherId>,
+    organization_id: Option<OrganizationId>,
 }
 
 // Upstream:
@@ -180,11 +185,11 @@ async fn post_events_collect(data: Json<Vec<EventCollection>>, 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<Vec<EventCollection>>, 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<Vec<EventCollection>>, 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<NaiveDateTime>,
     ip: &IpAddr,
     conn: &mut DbConn,
 ) {
-    let orgs = UserOrganization::get_org_uuid_by_user(user_uuid, conn).await;
+    let orgs = Membership::get_orgs_by_user(user_id, conn).await;
     let mut events: Vec<Event> = 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<NaiveDateTime>,
     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(());

+ 22 - 16
src/api/core/folders.rs

@@ -23,9 +23,9 @@ async fn get_folders(headers: Headers, mut conn: DbConn) -> Json<Value> {
     }))
 }
 
-#[get("/folders/<uuid>")]
-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/<folder_id>")]
+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<String>,
+    pub id: Option<FolderId>,
 }
 
 #[post("/folders", data = "<data>")]
@@ -50,14 +50,20 @@ async fn post_folders(data: Json<FolderData>, headers: Headers, mut conn: DbConn
     Ok(Json(folder.to_json()))
 }
 
-#[post("/folders/<uuid>", data = "<data>")]
-async fn post_folder(uuid: &str, data: Json<FolderData>, headers: Headers, conn: DbConn, nt: Notify<'_>) -> JsonResult {
-    put_folder(uuid, data, headers, conn, nt).await
+#[post("/folders/<folder_id>", data = "<data>")]
+async fn post_folder(
+    folder_id: FolderId,
+    data: Json<FolderData>,
+    headers: Headers,
+    conn: DbConn,
+    nt: Notify<'_>,
+) -> JsonResult {
+    put_folder(folder_id, data, headers, conn, nt).await
 }
 
-#[put("/folders/<uuid>", data = "<data>")]
+#[put("/folders/<folder_id>", data = "<data>")]
 async fn put_folder(
-    uuid: &str,
+    folder_id: FolderId,
     data: Json<FolderData>,
     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/<uuid>/delete")]
-async fn delete_folder_post(uuid: &str, headers: Headers, conn: DbConn, nt: Notify<'_>) -> EmptyResult {
-    delete_folder(uuid, headers, conn, nt).await
+#[post("/folders/<folder_id>/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/<uuid>")]
-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/<folder_id>")]
+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")
     };
 

文件差异内容过多而无法显示
+ 226 - 200
src/api/core/organizations.rs


+ 29 - 35
src/api/core/public.rs

@@ -55,38 +55,33 @@ async fn ldap_import(data: Json<OrgImportData>, token: PublicToken, mut conn: Db
         let mut user_created: bool = false;
         if user_data.deleted {
             // If user is marked for deletion and it exists, revoke it
-            if let Some(mut user_org) =
-                UserOrganization::find_by_email_and_org(&user_data.email, &org_id, &mut conn).await
-            {
+            if let Some(mut member) = Membership::find_by_email_and_org(&user_data.email, &org_id, &mut conn).await {
                 // Only revoke a user if it is not the last confirmed owner
-                let revoked = if user_org.atype == UserOrgType::Owner
-                    && user_org.status == UserOrgStatus::Confirmed as i32
+                let revoked = if member.atype == MembershipType::Owner
+                    && member.status == MembershipStatus::Confirmed as i32
                 {
-                    if UserOrganization::count_confirmed_by_org_and_type(&org_id, UserOrgType::Owner, &mut conn).await
-                        <= 1
+                    if Membership::count_confirmed_by_org_and_type(&org_id, MembershipType::Owner, &mut conn).await <= 1
                     {
                         warn!("Can't revoke the last owner");
                         false
                     } else {
-                        user_org.revoke()
+                        member.revoke()
                     }
                 } else {
-                    user_org.revoke()
+                    member.revoke()
                 };
 
-                let ext_modified = user_org.set_external_id(Some(user_data.external_id.clone()));
+                let ext_modified = member.set_external_id(Some(user_data.external_id.clone()));
                 if revoked || ext_modified {
-                    user_org.save(&mut conn).await?;
+                    member.save(&mut conn).await?;
                 }
             }
         // If user is part of the organization, restore it
-        } else if let Some(mut user_org) =
-            UserOrganization::find_by_email_and_org(&user_data.email, &org_id, &mut conn).await
-        {
-            let restored = user_org.restore();
-            let ext_modified = user_org.set_external_id(Some(user_data.external_id.clone()));
+        } else if let Some(mut member) = Membership::find_by_email_and_org(&user_data.email, &org_id, &mut conn).await {
+            let restored = member.restore();
+            let ext_modified = member.set_external_id(Some(user_data.external_id.clone()));
             if restored || ext_modified {
-                user_org.save(&mut conn).await?;
+                member.save(&mut conn).await?;
             }
         } else {
             // If user is not part of the organization
@@ -104,17 +99,17 @@ async fn ldap_import(data: Json<OrgImportData>, token: PublicToken, mut conn: Db
                     new_user
                 }
             };
-            let user_org_status = if CONFIG.mail_enabled() || user.password_hash.is_empty() {
-                UserOrgStatus::Invited as i32
+            let member_status = if CONFIG.mail_enabled() || user.password_hash.is_empty() {
+                MembershipStatus::Invited as i32
             } else {
-                UserOrgStatus::Accepted as i32 // Automatically mark user as accepted if no email invites
+                MembershipStatus::Accepted as i32 // Automatically mark user as accepted if no email invites
             };
 
-            let mut new_member = UserOrganization::new(user.uuid.clone(), org_id.clone());
+            let mut new_member = Membership::new(user.uuid.clone(), org_id.clone());
             new_member.set_external_id(Some(user_data.external_id.clone()));
             new_member.access_all = false;
-            new_member.atype = UserOrgType::User as i32;
-            new_member.status = user_org_status;
+            new_member.atype = MembershipType::User as i32;
+            new_member.status = member_status;
 
             new_member.save(&mut conn).await?;
 
@@ -166,9 +161,8 @@ async fn ldap_import(data: Json<OrgImportData>, token: PublicToken, mut conn: Db
             GroupUser::delete_all_by_group(&group_uuid, &mut conn).await?;
 
             for ext_id in &group_data.member_external_ids {
-                if let Some(user_org) = UserOrganization::find_by_external_id_and_org(ext_id, &org_id, &mut conn).await
-                {
-                    let mut group_user = GroupUser::new(group_uuid.clone(), user_org.uuid.clone());
+                if let Some(member) = Membership::find_by_external_id_and_org(ext_id, &org_id, &mut conn).await {
+                    let mut group_user = GroupUser::new(group_uuid.clone(), member.uuid.clone());
                     group_user.save(&mut conn).await?;
                 }
             }
@@ -181,20 +175,19 @@ async fn ldap_import(data: Json<OrgImportData>, token: PublicToken, mut conn: Db
     if data.overwrite_existing {
         // Generate a HashSet to quickly verify if a member is listed or not.
         let sync_members: HashSet<String> = data.members.into_iter().map(|m| m.external_id).collect();
-        for user_org in UserOrganization::find_by_org(&org_id, &mut conn).await {
-            if let Some(ref user_external_id) = user_org.external_id {
+        for member in Membership::find_by_org(&org_id, &mut conn).await {
+            if let Some(ref user_external_id) = member.external_id {
                 if !sync_members.contains(user_external_id) {
-                    if user_org.atype == UserOrgType::Owner && user_org.status == UserOrgStatus::Confirmed as i32 {
+                    if member.atype == MembershipType::Owner && member.status == MembershipStatus::Confirmed as i32 {
                         // Removing owner, check that there is at least one other confirmed owner
-                        if UserOrganization::count_confirmed_by_org_and_type(&org_id, UserOrgType::Owner, &mut conn)
-                            .await
+                        if Membership::count_confirmed_by_org_and_type(&org_id, MembershipType::Owner, &mut conn).await
                             <= 1
                         {
                             warn!("Can't delete the last owner");
                             continue;
                         }
                     }
-                    user_org.delete(&mut conn).await?;
+                    member.delete(&mut conn).await?;
                 }
             }
         }
@@ -203,7 +196,7 @@ async fn ldap_import(data: Json<OrgImportData>, token: PublicToken, mut conn: Db
     Ok(())
 }
 
-pub struct PublicToken(String);
+pub struct PublicToken(OrganizationId);
 
 #[rocket::async_trait]
 impl<'r> FromRequest<'r> for PublicToken {
@@ -243,10 +236,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 {

+ 44 - 38
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<NumberOrString>,
 
     // Used for key rotations
-    pub id: Option<String>,
+    pub id: Option<SendId>,
 }
 
 /// 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<Send> {
+fn create_send(data: SendData, user_id: UserId) -> ApiResult<Send> {
     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<Send> {
     }
 
     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<Value> {
     }))
 }
 
-#[get("/sends/<uuid>")]
-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/<send_id>")]
+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<UploadData<'_>>, 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<SendData>, 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<SendData>, 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/<send_uuid>/file/<file_id>", format = "multipart/form-data", data = "<data>")]
+#[post("/sends/<send_id>/file/<file_id>", format = "multipart/form-data", data = "<data>")]
 async fn post_send_file_v2_data(
-    send_uuid: &str,
-    file_id: &str,
+    send_id: SendId,
+    file_id: SendFileId,
     data: Form<UploadDataV2<'_>>,
     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/<send_id>/access/file/<file_id>", data = "<data>")]
 async fn post_access_file(
-    send_id: &str,
-    file_id: &str,
+    send_id: SendId,
+    file_id: SendFileId,
     data: Json<SendAccessData>,
     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/<send_id>/<file_id>?<t>")]
-async fn download_send(send_id: SafeString, file_id: SafeString, t: &str) -> Option<NamedFile> {
+async fn download_send(send_id: SendId, file_id: SendFileId, t: &str) -> Option<NamedFile> {
     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/<uuid>", data = "<data>")]
-async fn put_send(uuid: &str, data: Json<SendData>, headers: Headers, mut conn: DbConn, nt: Notify<'_>) -> JsonResult {
+#[put("/sends/<send_id>", data = "<data>")]
+async fn put_send(
+    send_id: SendId,
+    data: Json<SendData>,
+    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/<uuid>")]
-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/<send_id>")]
+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/<uuid>/remove-password")]
-async fn put_remove_password(uuid: &str, headers: Headers, mut conn: DbConn, nt: Notify<'_>) -> JsonResult {
+#[put("/sends/<send_id>/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")
     };
 

+ 9 - 9
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<EnableAuthenticatorData>, 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.

+ 3 - 3
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());
     };
 

+ 3 - 3
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<String, Error> {
 pub async fn get_duo_auth_url(
     email: &str,
     client_id: &str,
-    device_identifier: &String,
+    device_identifier: &DeviceId,
     conn: &mut DbConn,
 ) -> Result<String, Error> {
     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 "<authz code>|<state>"

+ 7 - 8
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<SendEmailLoginData>, 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<EmailData>, 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!");

+ 12 - 13
src/api/core/two_factor/mod.rs

@@ -173,17 +173,16 @@ async fn disable_twofactor_put(data: Json<DisableTwoFactorData>, 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,
 ) -> EmptyResult {
-    for member in UserOrganization::find_by_user_and_policy(&user.uuid, OrgPolicyType::TwoFactorAuthentication, conn)
-        .await
-        .into_iter()
+    for member in
+        Membership::find_by_user_and_policy(&user.uuid, OrgPolicyType::TwoFactorAuthentication, conn).await.into_iter()
     {
         // Policy only applies to non-Owner/non-Admin members who have accepted joining the org
-        if member.atype < UserOrgType::Admin {
+        if member.atype < MembershipType::Admin {
             if CONFIG.mail_enabled() {
                 let org = Organization::find_by_uuid(&member.org_uuid, conn).await.unwrap();
                 mail::send_2fa_removed_from_org(&user.email, &org.name).await?;
@@ -196,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,
@@ -209,16 +208,16 @@ 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 UserOrganization::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 < UserOrgType::Admin && TwoFactor::find_by_user(&member.user_uuid, conn).await.is_empty() {
+        if member.atype < MembershipType::Admin && TwoFactor::find_by_user(&member.user_uuid, conn).await.is_empty() {
             if CONFIG.mail_enabled() {
                 let user = User::find_by_uuid(&member.user_uuid, conn).await.unwrap();
                 mail::send_2fa_removed_from_org(&user.email, &org.name).await?;
@@ -230,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,

+ 3 - 3
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<ProtectedActionVerify>, 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)?;

+ 11 - 11
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<PasswordOrOtpData>, 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<DeleteU2FData>, headers: Headers, mut conn:
 }
 
 pub async fn get_webauthn_registrations(
-    user_uuid: &str,
+    user_id: &UserId,
     conn: &mut DbConn,
 ) -> Result<(bool, Vec<WebauthnRegistration>), 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<Credential> =
-        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 &reg.credential.cred_id == cred_id {
             reg.credential.counter = auth_data.counter;
 
-            TwoFactor::new(user_uuid.to_string(), TwoFactorType::Webauthn, serde_json::to_string(&registrations)?)
+            TwoFactor::new(user_id.clone(), TwoFactorType::Webauthn, serde_json::to_string(&registrations)?)
                 .save(conn)
                 .await?;
             return Ok(());

+ 2 - 2
src/api/core/two_factor/yubikey.rs

@@ -92,10 +92,10 @@ async fn generate_yubikey(data: Json<PasswordOrOtpData>, 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)?;

+ 33 - 32
src/api/identity.rs

@@ -31,7 +31,7 @@ pub fn routes() -> Vec<Route> {
 async fn login(data: Form<ConnectData>, client_header: ClientHeaders, mut conn: DbConn) -> JsonResult {
     let data: ConnectData = data.into_inner();
 
-    let mut user_uuid: Option<String> = None;
+    let mut user_id: Option<UserId> = None;
 
     let login_result = match data.grant_type.as_ref() {
         "refresh_token" => {
@@ -48,7 +48,7 @@ async fn login(data: Form<ConnectData>, 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<ConnectData>, 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<ConnectData>, 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,
@@ -111,7 +111,7 @@ async fn _refresh_login(data: ConnectData, conn: &mut DbConn) -> JsonResult {
     // Because this might get used in the future, and is add by the Bitwarden Server, lets keep it, but then commented out
     // See: https://github.com/dani-garcia/vaultwarden/issues/4156
     // ---
-    // let orgs = UserOrganization::find_confirmed_by_user(&user.uuid, conn).await;
+    // let members = Membership::find_confirmed_by_user(&user.uuid, conn).await;
     let (access_token, expires_in) = device.refresh_tokens(&user, scope_vec);
     device.save(conn).await?;
 
@@ -141,7 +141,7 @@ struct MasterPasswordPolicy {
 
 async fn _password_login(
     data: ConnectData,
-    user_uuid: &mut Option<String>,
+    user_id: &mut Option<UserId>,
     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),
@@ -291,7 +290,7 @@ async fn _password_login(
     // Because this might get used in the future, and is add by the Bitwarden Server, lets keep it, but then commented out
     // See: https://github.com/dani-garcia/vaultwarden/issues/4156
     // ---
-    // let orgs = UserOrganization::find_confirmed_by_user(&user.uuid, conn).await;
+    // let members = Membership::find_confirmed_by_user(&user.uuid, conn).await;
     let (access_token, expires_in) = device.refresh_tokens(&user, scope_vec);
     device.save(conn).await?;
 
@@ -359,7 +358,7 @@ async fn _password_login(
 
 async fn _api_key_login(
     data: ConnectData,
-    user_uuid: &mut Option<String>,
+    user_id: &mut Option<UserId>,
     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<String>,
+    user_id: &mut Option<UserId>,
     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 {
@@ -440,7 +440,7 @@ async fn _user_api_key_login(
     // Because this might get used in the future, and is add by the Bitwarden Server, lets keep it, but then commented out
     // See: https://github.com/dani-garcia/vaultwarden/issues/4156
     // ---
-    // let orgs = UserOrganization::find_confirmed_by_user(&user.uuid, conn).await;
+    // let members = Membership::find_confirmed_by_user(&user.uuid, conn).await;
     let (access_token, expires_in) = device.refresh_tokens(&user, scope_vec);
     device.save(conn).await?;
 
@@ -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<TwoFactor>) -> ApiResult<String> {
 
 async fn _json_err_twofactor(
     providers: &[i32],
-    user_uuid: &str,
+    user_id: &UserId,
     data: &ConnectData,
     conn: &mut DbConn,
 ) -> ApiResult<Value> {
@@ -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<String>,
+    device_identifier: Option<DeviceId>,
     #[field(name = uncased("device_name"))]
     #[field(name = uncased("devicename"))]
     device_name: Option<String>,
@@ -768,7 +769,7 @@ struct ConnectData {
     #[field(name = uncased("twofactorremember"))]
     two_factor_remember: Option<i32>,
     #[field(name = uncased("authrequest"))]
-    auth_request: Option<String>,
+    auth_request: Option<AuthRequestId>,
 }
 
 fn _check_is_some<T>(value: &Option<T>, msg: &str) -> EmptyResult {

+ 60 - 61
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<WebSocketUsers>,
-    user_uuid: String,
+    user_uuid: UserId,
     entry_uuid: uuid::Uuid,
     addr: IpAddr,
 }
 
 impl WSEntryMapGuard {
-    fn new(users: Arc<WebSocketUsers>, user_uuid: String, entry_uuid: uuid::Uuid, addr: IpAddr) -> Self {
+    fn new(users: Arc<WebSocketUsers>, 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);
         }
     }
@@ -130,7 +130,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::<Message>(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))
@@ -330,8 +330,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}");
@@ -347,7 +347,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,
         );
@@ -361,15 +361,15 @@ impl WebSocketUsers {
         }
     }
 
-    pub async fn send_logout(&self, user: &User, acting_device_uuid: Option<String>) {
+    pub async fn send_logout(&self, user: &User, acting_device_id: Option<DeviceId>) {
         // 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() {
@@ -377,7 +377,7 @@ impl WebSocketUsers {
         }
 
         if CONFIG.push_enabled() {
-            push_logout(user, acting_device_uuid);
+            push_logout(user, acting_device_id.clone());
         }
     }
 
@@ -385,7 +385,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
@@ -394,12 +394,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() {
@@ -407,7 +407,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;
         }
     }
 
@@ -415,48 +415,48 @@ impl WebSocketUsers {
         &self,
         ut: UpdateType,
         cipher: &Cipher,
-        user_uuids: &[String],
-        acting_device_uuid: &String,
-        collection_uuids: Option<Vec<String>>,
+        user_ids: &[UserId],
+        acting_device_id: &DeviceId,
+        collection_uuids: Option<Vec<CollectionId>>,
         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::<Vec<Value>>()),
+                Value::Array(collection_uuids.into_iter().map(|v| v.to_string().into()).collect::<Vec<Value>>()),
                 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;
         }
     }
 
@@ -464,20 +464,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,
@@ -485,20 +485,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
@@ -506,24 +506,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
@@ -531,17 +531,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;
         }
     }
 }
@@ -560,16 +559,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;
     }
 }
 
@@ -581,14 +580,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<String>) -> Vec<u8> {
+fn create_update(payload: Vec<(Value, Value)>, ut: UpdateType, acting_device_id: Option<DeviceId>) -> Vec<u8> {
     use rmpv::Value as V;
 
     let value = V::Array(vec![
@@ -597,7 +596,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()),
         ])]),
@@ -606,7 +605,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<u8> {
+fn create_anonymous_update(payload: Vec<(Value, Value)>, ut: UpdateType, user_id: UserId) -> Vec<u8> {
     use rmpv::Value as V;
 
     let value = V::Array(vec![
@@ -617,7 +616,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()),
         ])]),
     ]);
 

+ 34 - 34
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<String>) -> EmptyResult {
-    if !CONFIG.push_enabled() || push_uuid.is_none() {
+pub async fn unregister_push_device(push_id: Option<String>) -> 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<String>) -> 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<String>) {
-    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<DeviceId>) {
+    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,
             }
         })));
     }

+ 6 - 5
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<Option<NamedFile>> {
     Cached::long(NamedFile::open(Path::new(&CONFIG.web_vault_folder()).join(p)).await.ok(), true)
 }
 
-#[get("/attachments/<uuid>/<file_id>?<token>")]
-async fn attachments(uuid: SafeString, file_id: SafeString, token: String) -> Option<NamedFile> {
+#[get("/attachments/<cipher_id>/<file_id>?<token>")]
+async fn attachments(cipher_id: CipherId, file_id: AttachmentId, token: String) -> Option<NamedFile> {
     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.

+ 66 - 59
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<String>,
     // [ "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<String>,
-    pub user_org_id: Option<String>,
+    pub org_id: Option<OrganizationId>,
+    pub member_id: Option<MembershipId>,
     pub invited_by_email: Option<String>,
 }
 
 pub fn generate_invite_claims(
-    uuid: String,
+    user_id: UserId,
     email: String,
-    org_id: Option<String>,
-    user_org_id: Option<String>,
+    org_id: Option<OrganizationId>,
+    member_id: Option<MembershipId>,
     invited_by_email: Option<String>,
 ) -> InviteJwtClaims {
     let time_now = Utc::now();
@@ -208,10 +212,10 @@ 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,
-        user_org_id,
+        member_id,
         invited_by_email,
     }
 }
@@ -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<String>,
 }
 
-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(),
@@ -371,7 +378,7 @@ use rocket::{
 };
 
 use crate::db::{
-    models::{Collection, Device, User, UserOrgStatus, UserOrgType, UserOrganization, UserStampException},
+    models::{Collection, Device, Membership, MembershipStatus, MembershipType, User, UserStampException},
     DbConn,
 };
 
@@ -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")
         };
 
@@ -534,8 +541,8 @@ pub struct OrgHeaders {
     pub host: String,
     pub device: Device,
     pub user: User,
-    pub org_user_type: UserOrgType,
-    pub org_user: UserOrganization,
+    pub membership_type: MembershipType,
+    pub membership: Membership,
     pub ip: ClientIp,
 }
 
@@ -549,17 +556,17 @@ impl<'r> FromRequest<'r> for OrgHeaders {
         // org_id is usually the second path param ("/organizations/<org_id>"),
         // 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<OrganizationId> = {
             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,10 +581,10 @@ impl<'r> FromRequest<'r> for OrgHeaders {
                 };
 
                 let user = headers.user;
-                let org_user = match UserOrganization::find_by_user_and_org(&user.uuid, org_id, &mut conn).await {
-                    Some(user) => {
-                        if user.status == UserOrgStatus::Confirmed as i32 {
-                            user
+                let membership = match Membership::find_by_user_and_org(&user.uuid, &org_id, &mut conn).await {
+                    Some(member) => {
+                        if member.status == MembershipStatus::Confirmed as i32 {
+                            member
                         } else {
                             err_handler!("The current user isn't confirmed member of the organization")
                         }
@@ -589,15 +596,15 @@ impl<'r> FromRequest<'r> for OrgHeaders {
                     host: headers.host,
                     device: headers.device,
                     user,
-                    org_user_type: {
-                        if let Some(org_usr_type) = UserOrgType::from_i32(org_user.atype) {
+                    membership_type: {
+                        if let Some(org_usr_type) = MembershipType::from_i32(membership.atype) {
                             org_usr_type
                         } else {
                             // This should only happen if the DB is corrupted
                             err_handler!("Unknown user type in the database")
                         }
                     },
-                    org_user,
+                    membership,
                     ip: headers.ip,
                 })
             }
@@ -610,7 +617,7 @@ pub struct AdminHeaders {
     pub host: String,
     pub device: Device,
     pub user: User,
-    pub org_user_type: UserOrgType,
+    pub membership_type: MembershipType,
     pub ip: ClientIp,
 }
 
@@ -620,12 +627,12 @@ impl<'r> FromRequest<'r> for AdminHeaders {
 
     async fn from_request(request: &'r Request<'_>) -> Outcome<Self, Self::Error> {
         let headers = try_outcome!(OrgHeaders::from_request(request).await);
-        if headers.org_user_type >= UserOrgType::Admin {
+        if headers.membership_type >= MembershipType::Admin {
             Outcome::Success(Self {
                 host: headers.host,
                 device: headers.device,
                 user: headers.user,
-                org_user_type: headers.org_user_type,
+                membership_type: headers.membership_type,
                 ip: headers.ip,
             })
         } else {
@@ -648,16 +655,16 @@ impl From<AdminHeaders> for Headers {
 // col_id is usually the fourth path param ("/organizations/<org_id>/collections/<col_id>"),
 // 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<String> {
+fn get_col_id(request: &Request<'_>) -> Option<CollectionId> {
     if let Some(Ok(col_id)) = request.param::<String>(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::<String>("collectionId") {
         if uuid::Uuid::parse_str(&col_id).is_ok() {
-            return Some(col_id);
+            return Some(col_id.into());
         }
     }
 
@@ -680,7 +687,7 @@ impl<'r> FromRequest<'r> for ManagerHeaders {
 
     async fn from_request(request: &'r Request<'_>) -> Outcome<Self, Self::Error> {
         let headers = try_outcome!(OrgHeaders::from_request(request).await);
-        if headers.org_user_type >= UserOrgType::Manager {
+        if headers.membership_type >= MembershipType::Manager {
             match get_col_id(request) {
                 Some(col_id) => {
                     let mut conn = match DbConn::from_request(request).await {
@@ -688,7 +695,7 @@ impl<'r> FromRequest<'r> for ManagerHeaders {
                         _ => err_handler!("Error getting DB"),
                     };
 
-                    if !Collection::can_access_collection(&headers.org_user, &col_id, &mut conn).await {
+                    if !Collection::can_access_collection(&headers.membership, &col_id, &mut conn).await {
                         err_handler!("The current user isn't a manager for this collection")
                     }
                 }
@@ -724,7 +731,7 @@ pub struct ManagerHeadersLoose {
     pub host: String,
     pub device: Device,
     pub user: User,
-    pub org_user: UserOrganization,
+    pub membership: Membership,
     pub ip: ClientIp,
 }
 
@@ -734,12 +741,12 @@ impl<'r> FromRequest<'r> for ManagerHeadersLoose {
 
     async fn from_request(request: &'r Request<'_>) -> Outcome<Self, Self::Error> {
         let headers = try_outcome!(OrgHeaders::from_request(request).await);
-        if headers.org_user_type >= UserOrgType::Manager {
+        if headers.membership_type >= MembershipType::Manager {
             Outcome::Success(Self {
                 host: headers.host,
                 device: headers.device,
                 user: headers.user,
-                org_user: headers.org_user,
+                membership: headers.membership,
                 ip: headers.ip,
             })
         } else {
@@ -762,14 +769,14 @@ impl From<ManagerHeadersLoose> for Headers {
 impl ManagerHeaders {
     pub async fn from_loose(
         h: ManagerHeadersLoose,
-        collections: &Vec<String>,
+        collections: &Vec<CollectionId>,
         conn: &mut DbConn,
     ) -> Result<ManagerHeaders, Error> {
         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.org_user, col_id, conn).await {
+            if !Collection::can_access_collection(&h.membership, col_id, conn).await {
                 err!("You don't have access to all collections!");
             }
         }
@@ -795,7 +802,7 @@ impl<'r> FromRequest<'r> for OwnerHeaders {
 
     async fn from_request(request: &'r Request<'_>) -> Outcome<Self, Self::Error> {
         let headers = try_outcome!(OrgHeaders::from_request(request).await);
-        if headers.org_user_type == UserOrgType::Owner {
+        if headers.membership_type == MembershipType::Owner {
             Outcome::Success(Self {
                 device: headers.device,
                 user: headers.user,

+ 5 - 4
src/crypto.rs

@@ -84,14 +84,15 @@ pub fn generate_id<const N: usize>() -> String {
     encode_random_bytes::<N>(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.

+ 41 - 11
src/db/models/attachment.rs

@@ -1,9 +1,12 @@
 use std::io::ErrorKind;
 
 use bigdecimal::{BigDecimal, ToPrimitive};
+use derive_more::{AsRef, Deref, Display};
 use serde_json::Value;
 
+use super::{CipherId, OrganizationId, UserId};
 use crate::CONFIG;
+use macros::IdFromParam;
 
 db_object! {
     #[derive(Identifiable, Queryable, Insertable, AsChangeset)]
@@ -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<String>,
@@ -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<String>) -> Self {
+    pub const fn new(
+        id: AttachmentId,
+        cipher_uuid: CipherId,
+        file_name: String,
+        file_size: i64,
+        akey: Option<String>,
+    ) -> 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<Self> {
+    pub async fn find_by_id(id: &AttachmentId, conn: &mut DbConn) -> Option<Self> {
         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<Self> {
+    pub async fn find_by_cipher(cipher_uuid: &CipherId, conn: &mut DbConn) -> Vec<Self> {
         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<BigDecimal> = 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<BigDecimal> = 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<String>, conn: &mut DbConn) -> Vec<Self> {
+    pub async fn find_all_by_user_and_orgs(
+        user_uuid: &UserId,
+        org_uuids: &Vec<OrganizationId>,
+        conn: &mut DbConn,
+    ) -> Vec<Self> {
         db_run! { conn: {
             attachments::table
                 .left_join(ciphers::table.on(ciphers::uuid.eq(attachments::cipher_uuid)))
@@ -216,3 +229,20 @@ impl Attachment {
         }}
     }
 }
+
+#[derive(
+    Clone,
+    Debug,
+    AsRef,
+    Deref,
+    DieselNewType,
+    Display,
+    FromForm,
+    Hash,
+    PartialEq,
+    Eq,
+    Serialize,
+    Deserialize,
+    IdFromParam,
+)]
+pub struct AttachmentId(pub String);

+ 32 - 11
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 macros::UuidFromParam;
 
 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<String>,
+        pub uuid: AuthRequestId,
+        pub user_uuid: UserId,
+        pub organization_uuid: Option<OrganizationId>,
 
-        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<String>,
+        pub response_device_id: Option<DeviceId>,
 
         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<Self> {
+    pub async fn find_by_uuid(uuid: &AuthRequestId, conn: &mut DbConn) -> Option<Self> {
         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<Self> {
+    pub async fn find_by_uuid_and_user(uuid: &AuthRequestId, user_uuid: &UserId, conn: &mut DbConn) -> Option<Self> {
         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<Self> {
+    pub async fn find_by_user(user_uuid: &UserId, conn: &mut DbConn) -> Vec<Self> {
         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,
+    UuidFromParam,
+)]
+pub struct AuthRequestId(String);

+ 117 - 84
src/db/models/cipher.rs

@@ -1,13 +1,15 @@
 use crate::util::LowerCase;
 use crate::CONFIG;
 use chrono::{NaiveDateTime, TimeDelta, Utc};
+use derive_more::{AsRef, Deref, Display, From};
 use serde_json::Value;
 
 use super::{
-    Attachment, CollectionCipher, Favorite, FolderCipher, Group, User, UserOrgStatus, UserOrgType, UserOrganization,
+    Attachment, CollectionCipher, CollectionId, Favorite, FolderCipher, FolderId, Group, Membership, MembershipStatus,
+    MembershipType, OrganizationId, User, UserId,
 };
-
 use crate::api::core::{CipherData, CipherSyncData, CipherSyncType};
+use macros::UuidFromParam;
 
 use std::borrow::Cow;
 
@@ -17,12 +19,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<String>,
-        pub organization_uuid: Option<String>,
+        pub user_uuid: Option<UserId>,
+        pub organization_uuid: Option<OrganizationId>,
 
         pub key: Option<String>,
 
@@ -57,7 +59,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 +137,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 +304,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 +353,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 +382,7 @@ impl Cipher {
         json_object
     }
 
-    pub async fn update_users_revision(&self, conn: &mut DbConn) -> Vec<String> {
+    pub async fn update_users_revision(&self, conn: &mut DbConn) -> Vec<UserId> {
         let mut user_uuids = Vec::new();
         match self.user_uuid {
             Some(ref user_uuid) => {
@@ -391,17 +393,16 @@ impl Cipher {
                 // Belongs to Organization, need to update affected users
                 if let Some(ref org_uuid) = self.organization_uuid {
                     // users having access to the collection
-                    let mut collection_users =
-                        UserOrganization::find_by_cipher_and_org(&self.uuid, org_uuid, conn).await;
+                    let mut collection_users = Membership::find_by_cipher_and_org(&self.uuid, org_uuid, conn).await;
                     if CONFIG.org_groups_enabled() {
                         // members of a group having access to the collection
                         let group_users =
-                            UserOrganization::find_by_cipher_and_org_with_group(&self.uuid, org_uuid, conn).await;
+                            Membership::find_by_cipher_and_org_with_group(&self.uuid, org_uuid, conn).await;
                         collection_users.extend(group_users);
                     }
-                    for user_org in collection_users {
-                        User::update_uuid_revision(&user_org.user_uuid, conn).await;
-                        user_uuids.push(user_org.user_uuid.clone())
+                    for member in collection_users {
+                        User::update_uuid_revision(&member.user_uuid, conn).await;
+                        user_uuids.push(member.user_uuid.clone())
                     }
                 }
             }
@@ -459,7 +460,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?;
@@ -467,7 +468,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?;
         }
@@ -485,52 +486,59 @@ impl Cipher {
         }
     }
 
-    pub async fn move_to_folder(&self, folder_uuid: Option<String>, user_uuid: &str, conn: &mut DbConn) -> EmptyResult {
+    pub async fn move_to_folder(
+        &self,
+        folder_uuid: Option<FolderId>,
+        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 {
         if let Some(ref org_uuid) = self.organization_uuid {
             if let Some(cipher_sync_data) = cipher_sync_data {
-                if let Some(cached_user_org) = cipher_sync_data.user_organizations.get(org_uuid) {
-                    return cached_user_org.has_full_access();
+                if let Some(cached_member) = cipher_sync_data.members.get(org_uuid) {
+                    return cached_member.has_full_access();
                 }
-            } else if let Some(user_org) = UserOrganization::find_by_user_and_org(user_uuid, org_uuid, conn).await {
-                return user_org.has_full_access();
+            } else if let Some(member) = Membership::find_by_user_and_org(user_uuid, org_uuid, conn).await {
+                return member.has_full_access();
             }
         }
         false
@@ -539,7 +547,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 {
@@ -563,7 +571,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)> {
@@ -623,7 +631,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.
@@ -640,7 +648,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();
         }
@@ -666,43 +674,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<bool>, user_uuid: &str, conn: &mut DbConn) -> EmptyResult {
+    pub async fn set_favorite(&self, favorite: Option<bool>, 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<String> {
+    pub async fn get_folder_uuid(&self, user_uuid: &UserId, conn: &mut DbConn) -> Option<FolderId> {
         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::<String>(conn)
+                .first::<FolderId>(conn)
                 .ok()
         }}
     }
 
-    pub async fn find_by_uuid(uuid: &str, conn: &mut DbConn) -> Option<Self> {
+    pub async fn find_by_uuid(uuid: &CipherId, conn: &mut DbConn) -> Option<Self> {
         db_run! {conn: {
             ciphers::table
                 .filter(ciphers::uuid.eq(uuid))
@@ -712,7 +720,11 @@ impl Cipher {
         }}
     }
 
-    pub async fn find_by_uuid_and_org(cipher_uuid: &str, org_uuid: &str, conn: &mut DbConn) -> Option<Self> {
+    pub async fn find_by_uuid_and_org(
+        cipher_uuid: &CipherId,
+        org_uuid: &OrganizationId,
+        conn: &mut DbConn,
+    ) -> Option<Self> {
         db_run! {conn: {
             ciphers::table
                 .filter(ciphers::uuid.eq(cipher_uuid))
@@ -735,7 +747,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<Self> {
+    pub async fn find_by_user(user_uuid: &UserId, visible_only: bool, conn: &mut DbConn) -> Vec<Self> {
         if CONFIG.org_groups_enabled() {
             db_run! {conn: {
                 let mut query = ciphers::table
@@ -745,7 +757,7 @@ impl Cipher {
                     .left_join(users_organizations::table.on(
                             ciphers::organization_uuid.eq(users_organizations::org_uuid.nullable())
                             .and(users_organizations::user_uuid.eq(user_uuid))
-                            .and(users_organizations::status.eq(UserOrgStatus::Confirmed as i32))
+                            .and(users_organizations::status.eq(MembershipStatus::Confirmed as i32))
                             ))
                     .left_join(users_collections::table.on(
                             ciphers_collections::collection_uuid.eq(users_collections::collection_uuid)
@@ -772,7 +784,7 @@ impl Cipher {
 
                 if !visible_only {
                     query = query.or_filter(
-                        users_organizations::atype.le(UserOrgType::Admin as i32) // Org admin/owner
+                        users_organizations::atype.le(MembershipType::Admin as i32) // Org admin/owner
                         );
                 }
 
@@ -790,7 +802,7 @@ impl Cipher {
                     .left_join(users_organizations::table.on(
                             ciphers::organization_uuid.eq(users_organizations::org_uuid.nullable())
                             .and(users_organizations::user_uuid.eq(user_uuid))
-                            .and(users_organizations::status.eq(UserOrgStatus::Confirmed as i32))
+                            .and(users_organizations::status.eq(MembershipStatus::Confirmed as i32))
                             ))
                     .left_join(users_collections::table.on(
                             ciphers_collections::collection_uuid.eq(users_collections::collection_uuid)
@@ -804,7 +816,7 @@ impl Cipher {
 
                     if !visible_only {
                         query = query.or_filter(
-                            users_organizations::atype.le(UserOrgType::Admin as i32) // Org admin/owner
+                            users_organizations::atype.le(MembershipType::Admin as i32) // Org admin/owner
                             );
                     }
 
@@ -817,12 +829,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<Self> {
+    pub async fn find_by_user_visible(user_uuid: &UserId, conn: &mut DbConn) -> Vec<Self> {
         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<Self> {
+    pub async fn find_owned_by_user(user_uuid: &UserId, conn: &mut DbConn) -> Vec<Self> {
         db_run! {conn: {
             ciphers::table
                 .filter(
@@ -833,7 +845,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))
@@ -844,7 +856,7 @@ impl Cipher {
         }}
     }
 
-    pub async fn find_by_org(org_uuid: &str, conn: &mut DbConn) -> Vec<Self> {
+    pub async fn find_by_org(org_uuid: &OrganizationId, conn: &mut DbConn) -> Vec<Self> {
         db_run! {conn: {
             ciphers::table
                 .filter(ciphers::organization_uuid.eq(org_uuid))
@@ -852,7 +864,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))
@@ -863,7 +875,7 @@ impl Cipher {
         }}
     }
 
-    pub async fn find_by_folder(folder_uuid: &str, conn: &mut DbConn) -> Vec<Self> {
+    pub async fn find_by_folder(folder_uuid: &FolderId, conn: &mut DbConn) -> Vec<Self> {
         db_run! {conn: {
             folders_ciphers::table.inner_join(ciphers::table)
                 .filter(folders_ciphers::folder_uuid.eq(folder_uuid))
@@ -881,7 +893,7 @@ impl Cipher {
         }}
     }
 
-    pub async fn get_collections(&self, user_id: String, conn: &mut DbConn) -> Vec<String> {
+    pub async fn get_collections(&self, user_uuid: UserId, conn: &mut DbConn) -> Vec<CollectionId> {
         if CONFIG.org_groups_enabled() {
             db_run! {conn: {
                 ciphers_collections::table
@@ -891,11 +903,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)
@@ -906,14 +918,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::<String>(conn).unwrap_or_default()
+                    .load::<CollectionId>(conn).unwrap_or_default()
             }}
         } else {
             db_run! {conn: {
@@ -924,23 +936,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::<String>(conn).unwrap_or_default()
+                    .load::<CollectionId>(conn).unwrap_or_default()
             }}
         }
     }
 
-    pub async fn get_admin_collections(&self, user_id: String, conn: &mut DbConn) -> Vec<String> {
+    pub async fn get_admin_collections(&self, user_uuid: UserId, conn: &mut DbConn) -> Vec<CollectionId> {
         if CONFIG.org_groups_enabled() {
             db_run! {conn: {
                 ciphers_collections::table
@@ -950,11 +962,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)
@@ -965,15 +977,15 @@ 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)))
-                        .or(users_organizations::atype.le(UserOrgType::Admin as i32)) // User is admin or owner
+                        .or(users_organizations::atype.le(MembershipType::Admin as i32)) // User is admin or owner
                     )
                     .select(ciphers_collections::collection_uuid)
-                    .load::<String>(conn).unwrap_or_default()
+                    .load::<CollectionId>(conn).unwrap_or_default()
             }}
         } else {
             db_run! {conn: {
@@ -984,26 +996,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(UserOrgType::Admin as i32)) // User is admin or owner
+                        .or(users_organizations::atype.le(MembershipType::Admin as i32)) // User is admin or owner
                     )
                     .select(ciphers_collections::collection_uuid)
-                    .load::<String>(conn).unwrap_or_default()
+                    .load::<CollectionId>(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(
@@ -1011,12 +1026,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(
@@ -1030,14 +1045,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(UserOrgType::Admin as i32)) // User is admin or owner
+            .or_filter(users_organizations::atype.le(MembershipType::Admin as i32)) // User is admin or owner
             .or_filter(groups::access_all.eq(true)) //Access via group
             .or_filter(collections_groups::collections_uuid.is_not_null()) //Access via group
             .select(ciphers_collections::all_columns)
             .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,
+    UuidFromParam,
+)]
+pub struct CipherId(String);

+ 147 - 69
src/db/models/collection.rs

@@ -1,15 +1,20 @@
+use derive_more::{AsRef, Deref, Display, From};
 use serde_json::Value;
 
-use super::{CollectionGroup, GroupUser, User, UserOrgStatus, UserOrgType, UserOrganization};
+use super::{
+    CipherId, CollectionGroup, GroupUser, Membership, MembershipId, MembershipStatus, MembershipType, OrganizationId,
+    User, UserId,
+};
 use crate::CONFIG;
+use macros::UuidFromParam;
 
 db_object! {
     #[derive(Identifiable, Queryable, Insertable, AsChangeset)]
     #[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<String>,
     }
@@ -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<String>) -> Self {
+    pub fn new(org_uuid: OrganizationId, name: String, external_id: Option<String>) -> Self {
         let mut new_model = Self {
-            uuid: crate::util::get_uuid(),
+            uuid: CollectionId(crate::util::get_uuid()),
             org_uuid,
             name,
             external_id: None,
@@ -74,18 +79,18 @@ 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 {
         let (read_only, hide_passwords, can_manage) = if let Some(cipher_sync_data) = cipher_sync_data {
-            match cipher_sync_data.user_organizations.get(&self.org_uuid) {
+            match cipher_sync_data.members.get(&self.org_uuid) {
                 // Only for Manager types Bitwarden returns true for the can_manage option
                 // Owners and Admins always have true
-                Some(uo) if uo.has_full_access() => (false, false, uo.atype >= UserOrgType::Manager),
-                Some(uo) => {
+                Some(m) if m.has_full_access() => (false, false, m.atype >= MembershipType::Manager),
+                Some(m) => {
                     // Only let a manager manage collections when the have full read/write access
-                    let is_manager = uo.atype == UserOrgType::Manager;
+                    let is_manager = m.atype == MembershipType::Manager;
                     if let Some(uc) = cipher_sync_data.user_collections.get(&self.uuid) {
                         (uc.read_only, uc.hide_passwords, is_manager && !uc.read_only && !uc.hide_passwords)
                     } else if let Some(cg) = cipher_sync_data.user_collections_groups.get(&self.uuid) {
@@ -97,10 +102,10 @@ impl Collection {
                 _ => (true, true, false),
             }
         } else {
-            match UserOrganization::find_confirmed_by_user_and_org(user_uuid, &self.org_uuid, conn).await {
-                Some(ou) if ou.has_full_access() => (false, false, ou.atype >= UserOrgType::Manager),
-                Some(ou) => {
-                    let is_manager = ou.atype == UserOrgType::Manager;
+            match Membership::find_confirmed_by_user_and_org(user_uuid, &self.org_uuid, conn).await {
+                Some(m) if m.has_full_access() => (false, false, m.atype >= MembershipType::Manager),
+                Some(m) => {
+                    let is_manager = m.atype == MembershipType::Manager;
                     let read_only = !self.is_writable_by_user(user_uuid, conn).await;
                     let hide_passwords = self.hide_passwords_for_user(user_uuid, conn).await;
                     (read_only, hide_passwords, is_manager && !read_only && !hide_passwords)
@@ -121,13 +126,13 @@ impl Collection {
         json_object
     }
 
-    pub async fn can_access_collection(org_user: &UserOrganization, col_id: &str, conn: &mut DbConn) -> bool {
-        org_user.has_status(UserOrgStatus::Confirmed)
-            && (org_user.has_full_access()
-                || CollectionUser::has_access_to_collection_by_user(col_id, &org_user.user_uuid, conn).await
+    pub async fn can_access_collection(member: &Membership, col_id: &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
                 || (CONFIG.org_groups_enabled()
-                    && (GroupUser::has_full_access_by_member(&org_user.org_uuid, &org_user.uuid, conn).await
-                        || GroupUser::has_access_to_collection_by_member(col_id, &org_user.uuid, conn).await)))
+                    && (GroupUser::has_full_access_by_member(&member.org_uuid, &member.uuid, conn).await
+                        || GroupUser::has_access_to_collection_by_member(col_id, &member.uuid, conn).await)))
     }
 }
 
@@ -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?;
         }
@@ -193,12 +198,12 @@ impl Collection {
     }
 
     pub async fn update_users_revision(&self, conn: &mut DbConn) {
-        for user_org in UserOrganization::find_by_collection_and_org(&self.uuid, &self.org_uuid, conn).await.iter() {
-            User::update_uuid_revision(&user_org.user_uuid, conn).await;
+        for member in Membership::find_by_collection_and_org(&self.uuid, &self.org_uuid, conn).await.iter() {
+            User::update_uuid_revision(&member.user_uuid, conn).await;
         }
     }
 
-    pub async fn find_by_uuid(uuid: &str, conn: &mut DbConn) -> Option<Self> {
+    pub async fn find_by_uuid(uuid: &CollectionId, conn: &mut DbConn) -> Option<Self> {
         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<Self> {
+    pub async fn find_by_user_uuid(user_uuid: UserId, conn: &mut DbConn) -> Vec<Self> {
         if CONFIG.org_groups_enabled() {
             db_run! { conn: {
                 collections::table
@@ -234,7 +239,7 @@ impl Collection {
                     )
                 ))
                 .filter(
-                    users_organizations::status.eq(UserOrgStatus::Confirmed as i32)
+                    users_organizations::status.eq(MembershipStatus::Confirmed as i32)
                 )
                 .filter(
                     users_collections::user_uuid.eq(user_uuid).or( // Directly accessed collection
@@ -265,7 +270,7 @@ impl Collection {
                     )
                 ))
                 .filter(
-                    users_organizations::status.eq(UserOrgStatus::Confirmed as i32)
+                    users_organizations::status.eq(MembershipStatus::Confirmed as i32)
                 )
                 .filter(
                     users_collections::user_uuid.eq(user_uuid).or( // Directly accessed collection
@@ -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<Self> {
+    pub async fn find_by_organization_and_user_uuid(
+        org_uuid: &OrganizationId,
+        user_uuid: &UserId,
+        conn: &mut DbConn,
+    ) -> Vec<Self> {
         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<Self> {
+    pub async fn find_by_organization(org_uuid: &OrganizationId, conn: &mut DbConn) -> Vec<Self> {
         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<Self> {
+    pub async fn find_by_uuid_and_org(
+        uuid: &CollectionId,
+        org_uuid: &OrganizationId,
+        conn: &mut DbConn,
+    ) -> Option<Self> {
         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<Self> {
+    pub async fn find_by_uuid_and_user(uuid: &CollectionId, user_uuid: UserId, conn: &mut DbConn) -> Option<Self> {
         if CONFIG.org_groups_enabled() {
             db_run! { conn: {
                 collections::table
@@ -349,7 +362,7 @@ impl Collection {
                 .filter(
                     users_collections::collection_uuid.eq(uuid).or( // Directly accessed collection
                         users_organizations::access_all.eq(true).or( // access_all in Organization
-                            users_organizations::atype.le(UserOrgType::Admin as i32) // Org admin or owner
+                            users_organizations::atype.le(MembershipType::Admin as i32) // Org admin or owner
                     )).or(
                         groups::access_all.eq(true) // access_all in groups
                     ).or( // access via groups
@@ -378,7 +391,7 @@ impl Collection {
                 .filter(
                     users_collections::collection_uuid.eq(uuid).or( // Directly accessed collection
                         users_organizations::access_all.eq(true).or( // access_all in Organization
-                            users_organizations::atype.le(UserOrgType::Admin as i32) // Org admin or owner
+                            users_organizations::atype.le(MembershipType::Admin as i32) // Org admin or owner
                     ))
                 ).select(collections::all_columns)
                 .first::<CollectionDb>(conn).ok()
@@ -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: {
@@ -411,7 +424,7 @@ impl Collection {
                         collections_groups::groups_uuid.eq(groups_users::groups_uuid)
                         .and(collections_groups::collections_uuid.eq(collections::uuid))
                     ))
-                    .filter(users_organizations::atype.le(UserOrgType::Admin as i32) // Org admin or owner
+                    .filter(users_organizations::atype.le(MembershipType::Admin as i32) // Org admin or owner
                         .or(users_organizations::access_all.eq(true)) // access_all via membership
                         .or(users_collections::collection_uuid.eq(&self.uuid) // write access given to collection
                             .and(users_collections::read_only.eq(false)))
@@ -436,7 +449,7 @@ impl Collection {
                         users_collections::collection_uuid.eq(collections::uuid)
                         .and(users_collections::user_uuid.eq(user_uuid))
                     ))
-                    .filter(users_organizations::atype.le(UserOrgType::Admin as i32) // Org admin or owner
+                    .filter(users_organizations::atype.le(MembershipType::Admin as i32) // Org admin or owner
                         .or(users_organizations::access_all.eq(true)) // access_all via membership
                         .or(users_collections::collection_uuid.eq(&self.uuid) // write access given to collection
                             .and(users_collections::read_only.eq(false)))
@@ -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
@@ -478,7 +491,7 @@ impl Collection {
             .filter(
                 users_collections::collection_uuid.eq(&self.uuid).and(users_collections::hide_passwords.eq(true)).or(// Directly accessed collection
                     users_organizations::access_all.eq(true).or( // access_all in Organization
-                        users_organizations::atype.le(UserOrgType::Admin as i32) // Org admin or owner
+                        users_organizations::atype.le(MembershipType::Admin as i32) // Org admin or owner
                 )).or(
                     groups::access_all.eq(true) // access_all in groups
                 ).or( // access via groups
@@ -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<Self> {
+    pub async fn find_by_organization_and_user_uuid(
+        org_uuid: &OrganizationId,
+        user_uuid: &UserId,
+        conn: &mut DbConn,
+    ) -> Vec<Self> {
         db_run! { conn: {
             users_collections::table
                 .filter(users_collections::user_uuid.eq(user_uuid))
@@ -511,11 +528,11 @@ impl CollectionUser {
         }}
     }
 
-    pub async fn find_by_organization_swap_user_uuid_with_org_user_uuid(
-        org_uuid: &str,
+    pub async fn find_by_organization_swap_user_uuid_with_member_uuid(
+        org_uuid: &OrganizationId,
         conn: &mut DbConn,
-    ) -> Vec<Self> {
-        db_run! { conn: {
+    ) -> Vec<CollectionMembership> {
+        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))
@@ -524,12 +541,13 @@ impl CollectionUser {
                 .load::<CollectionUserDb>(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,
@@ -599,7 +617,7 @@ impl CollectionUser {
         }}
     }
 
-    pub async fn find_by_collection(collection_uuid: &str, conn: &mut DbConn) -> Vec<Self> {
+    pub async fn find_by_collection(collection_uuid: &CollectionId, conn: &mut DbConn) -> Vec<Self> {
         db_run! { conn: {
             users_collections::table
                 .filter(users_collections::collection_uuid.eq(collection_uuid))
@@ -610,11 +628,11 @@ impl CollectionUser {
         }}
     }
 
-    pub async fn find_by_collection_swap_user_uuid_with_org_user_uuid(
-        collection_uuid: &str,
+    pub async fn find_by_collection_swap_user_uuid_with_member_uuid(
+        collection_uuid: &CollectionId,
         conn: &mut DbConn,
-    ) -> Vec<Self> {
-        db_run! { conn: {
+    ) -> Vec<CollectionMembership> {
+        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)))
@@ -622,12 +640,13 @@ impl CollectionUser {
                 .load::<CollectionUserDb>(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<Self> {
         db_run! { conn: {
@@ -641,7 +660,7 @@ impl CollectionUser {
         }}
     }
 
-    pub async fn find_by_user(user_uuid: &str, conn: &mut DbConn) -> Vec<Self> {
+    pub async fn find_by_user(user_uuid: &UserId, conn: &mut DbConn) -> Vec<Self> {
         db_run! { conn: {
             users_collections::table
                 .filter(users_collections::user_uuid.eq(user_uuid))
@@ -652,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;
         }
@@ -664,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: {
@@ -680,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:
@@ -717,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: {
@@ -731,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)
@@ -739,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)
@@ -747,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<CollectionUser> 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,
+    UuidFromParam,
+)]
+pub struct CollectionId(String);

+ 51 - 50
src/db/models/device.rs

@@ -1,7 +1,9 @@
 use chrono::{NaiveDateTime, Utc};
+use derive_more::{Display, From};
 
+use super::UserId;
 use crate::{crypto, CONFIG};
-use core::fmt;
+use macros::IdFromParam;
 
 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 {
@@ -75,12 +77,12 @@ impl Device {
         // Also These key/value pairs are not used anywhere by either Vaultwarden or Bitwarden Clients
         // Because these might get used in the future, and they are added by the Bitwarden Server, lets keep it, but then commented out
         // ---
-        // fn arg: orgs: Vec<super::UserOrganization>,
+        // fn arg: members: Vec<super::Membership>,
         // ---
-        // let orgowner: Vec<_> = orgs.iter().filter(|o| o.atype == 0).map(|o| o.org_uuid.clone()).collect();
-        // let orgadmin: Vec<_> = orgs.iter().filter(|o| o.atype == 1).map(|o| o.org_uuid.clone()).collect();
-        // let orguser: Vec<_> = orgs.iter().filter(|o| o.atype == 2).map(|o| o.org_uuid.clone()).collect();
-        // let orgmanager: Vec<_> = orgs.iter().filter(|o| o.atype == 3).map(|o| o.org_uuid.clone()).collect();
+        // let orgowner: Vec<_> = members.iter().filter(|m| m.atype == 0).map(|o| o.org_uuid.clone()).collect();
+        // let orgadmin: Vec<_> = members.iter().filter(|m| m.atype == 1).map(|o| o.org_uuid.clone()).collect();
+        // let orguser: Vec<_> = members.iter().filter(|m| m.atype == 2).map(|o| o.org_uuid.clone()).collect();
+        // let orgmanager: Vec<_> = members.iter().filter(|m| m.atype == 3).map(|o| o.org_uuid.clone()).collect();
 
         // Create the JWT claims struct, to send to the client
         use crate::auth::{encode_jwt, LoginJwtClaims, DEFAULT_VALIDITY, JWT_LOGIN_ISSUER};
@@ -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<Self> {
+    pub async fn find_by_uuid_and_user(uuid: &DeviceId, user_uuid: &UserId, conn: &mut DbConn) -> Option<Self> {
         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<Self> {
+    pub async fn find_by_user(user_uuid: &UserId, conn: &mut DbConn) -> Vec<Self> {
         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<Self> {
+    pub async fn find_by_uuid(uuid: &DeviceId, conn: &mut DbConn) -> Option<Self> {
         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<Self> {
+    pub async fn find_latest_active_by_user(user_uuid: &UserId, conn: &mut DbConn) -> Option<Self> {
         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<Self> {
+    pub async fn find_push_devices_by_user(user_uuid: &UserId, conn: &mut DbConn) -> Vec<Self> {
         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,8 @@ impl DeviceType {
         }
     }
 }
+
+#[derive(
+    Clone, Debug, DieselNewType, Display, From, FromForm, Hash, PartialEq, Eq, Serialize, Deserialize, IdFromParam,
+)]
+pub struct DeviceId(String);

+ 54 - 18
src/db/models/emergency_access.rs

@@ -1,9 +1,10 @@
 use chrono::{NaiveDateTime, Utc};
+use derive_more::{AsRef, Deref, Display, From};
 use serde_json::Value;
 
+use super::{User, UserId};
 use crate::{api::EmptyResult, db::DbConn, error::MapResult};
-
-use super::User;
+use macros::UuidFromParam;
 
 db_object! {
     #[derive(Identifiable, Queryable, Insertable, AsChangeset)]
@@ -11,9 +12,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<String>,
+        pub uuid: EmergencyAccessId,
+        pub grantor_uuid: UserId,
+        pub grantee_uuid: Option<UserId>,
         pub email: Option<String>,
         pub key_encrypted: Option<String>,
         pub atype: i32, //EmergencyAccessType
@@ -29,11 +30,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 +83,7 @@ impl EmergencyAccess {
     }
 
     pub async fn to_json_grantee_details(&self, conn: &mut DbConn) -> Option<Value> {
-        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 +212,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 +240,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<Self> {
@@ -262,7 +263,11 @@ impl EmergencyAccess {
         }}
     }
 
-    pub async fn find_by_uuid_and_grantor_uuid(uuid: &str, grantor_uuid: &str, conn: &mut DbConn) -> Option<Self> {
+    pub async fn find_by_uuid_and_grantor_uuid(
+        uuid: &EmergencyAccessId,
+        grantor_uuid: &UserId,
+        conn: &mut DbConn,
+    ) -> Option<Self> {
         db_run! { conn: {
             emergency_access::table
                 .filter(emergency_access::uuid.eq(uuid))
@@ -272,7 +277,11 @@ impl EmergencyAccess {
         }}
     }
 
-    pub async fn find_by_uuid_and_grantee_uuid(uuid: &str, grantee_uuid: &str, conn: &mut DbConn) -> Option<Self> {
+    pub async fn find_by_uuid_and_grantee_uuid(
+        uuid: &EmergencyAccessId,
+        grantee_uuid: &UserId,
+        conn: &mut DbConn,
+    ) -> Option<Self> {
         db_run! { conn: {
             emergency_access::table
                 .filter(emergency_access::uuid.eq(uuid))
@@ -282,7 +291,11 @@ impl EmergencyAccess {
         }}
     }
 
-    pub async fn find_by_uuid_and_grantee_email(uuid: &str, grantee_email: &str, conn: &mut DbConn) -> Option<Self> {
+    pub async fn find_by_uuid_and_grantee_email(
+        uuid: &EmergencyAccessId,
+        grantee_email: &str,
+        conn: &mut DbConn,
+    ) -> Option<Self> {
         db_run! { conn: {
             emergency_access::table
                 .filter(emergency_access::uuid.eq(uuid))
@@ -292,7 +305,7 @@ impl EmergencyAccess {
         }}
     }
 
-    pub async fn find_all_by_grantee_uuid(grantee_uuid: &str, conn: &mut DbConn) -> Vec<Self> {
+    pub async fn find_all_by_grantee_uuid(grantee_uuid: &UserId, conn: &mut DbConn) -> Vec<Self> {
         db_run! { conn: {
             emergency_access::table
                 .filter(emergency_access::grantee_uuid.eq(grantee_uuid))
@@ -319,7 +332,7 @@ impl EmergencyAccess {
         }}
     }
 
-    pub async fn find_all_by_grantor_uuid(grantor_uuid: &str, conn: &mut DbConn) -> Vec<Self> {
+    pub async fn find_all_by_grantor_uuid(grantor_uuid: &UserId, conn: &mut DbConn) -> Vec<Self> {
         db_run! { conn: {
             emergency_access::table
                 .filter(emergency_access::grantor_uuid.eq(grantor_uuid))
@@ -327,7 +340,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 +355,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,
+    UuidFromParam,
+)]
+pub struct EmergencyAccessId(String);

+ 24 - 21
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<String>,
-        pub org_uuid: Option<String>,
-        pub cipher_uuid: Option<String>,
-        pub collection_uuid: Option<String>,
-        pub group_uuid: Option<String>,
-        pub org_user_uuid: Option<String>,
-        pub act_user_uuid: Option<String>,
+        pub user_uuid: Option<UserId>,
+        pub org_uuid: Option<OrganizationId>,
+        pub cipher_uuid: Option<CipherId>,
+        pub collection_uuid: Option<CollectionId>,
+        pub group_uuid: Option<GroupId>,
+        pub org_user_uuid: Option<MembershipId>,
+        pub act_user_uuid: Option<UserId>,
         // Upstream enum: https://github.com/bitwarden/server/blob/8a22c0479e987e756ce7412c48a732f9002f0a2d/src/Core/Enums/DeviceType.cs
         pub device_type: Option<i32>,
         pub ip_address: Option<String>,
         pub event_date: NaiveDateTime,
-        pub policy_uuid: Option<String>,
+        pub policy_uuid: Option<OrgPolicyId>,
         pub provider_uuid: Option<String>,
         pub provider_user_uuid: Option<String>,
         pub provider_org_uuid: Option<String>,
@@ -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))
@@ -274,16 +274,16 @@ impl Event {
         }}
     }
 
-    pub async fn find_by_org_and_user_org(
-        org_uuid: &str,
-        user_org_uuid: &str,
+    pub async fn find_by_org_and_member(
+        org_uuid: &OrganizationId,
+        member_uuid: &MembershipId,
         start: &NaiveDateTime,
         end: &NaiveDateTime,
         conn: &mut DbConn,
     ) -> Vec<Self> {
         db_run! { conn: {
             event::table
-                .inner_join(users_organizations::table.on(users_organizations::uuid.eq(user_org_uuid)))
+                .inner_join(users_organizations::table.on(users_organizations::uuid.eq(member_uuid)))
                 .filter(event::org_uuid.eq(org_uuid))
                 .filter(event::event_date.between(start, end))
                 .filter(event::user_uuid.eq(users_organizations::user_uuid.nullable()).or(event::act_user_uuid.eq(users_organizations::user_uuid.nullable())))
@@ -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);

+ 14 - 9
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<String> {
+    pub async fn get_all_cipher_uuid_by_user(user_uuid: &UserId, conn: &mut DbConn) -> Vec<CipherId> {
         db_run! { conn: {
             favorites::table
                 .filter(favorites::user_uuid.eq(user_uuid))
                 .select(favorites::cipher_uuid)
-                .load::<String>(conn)
+                .load::<CipherId>(conn)
                 .unwrap_or_default()
         }}
     }

+ 43 - 19
src/db/models/folder.rs

@@ -1,17 +1,19 @@
 use chrono::{NaiveDateTime, Utc};
+use derive_more::{AsRef, Deref, Display, From};
 use serde_json::Value;
 
-use super::User;
+use super::{CipherId, User, UserId};
+use macros::UuidFromParam;
 
 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<Self> {
+    pub async fn find_by_uuid_and_user(uuid: &FolderId, user_uuid: &UserId, conn: &mut DbConn) -> Option<Self> {
         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<Self> {
+    pub async fn find_by_user(user_uuid: &UserId, conn: &mut DbConn) -> Vec<Self> {
         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<Self> {
+    pub async fn find_by_folder_and_cipher(
+        folder_uuid: &FolderId,
+        cipher_uuid: &CipherId,
+        conn: &mut DbConn,
+    ) -> Option<Self> {
         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<Self> {
+    pub async fn find_by_folder(folder_uuid: &FolderId, conn: &mut DbConn) -> Vec<Self> {
         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,
+    UuidFromParam,
+)]
+pub struct FolderId(String);

+ 95 - 50
src/db/models/group.rs

@@ -1,8 +1,10 @@
-use super::{User, UserOrganization};
+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 macros::UuidFromParam;
 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<String>,
@@ -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<String>) -> Self {
+    pub fn new(
+        organizations_uuid: OrganizationId,
+        name: String,
+        access_all: bool,
+        external_id: Option<String>,
+    ) -> 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<Self> {
+    pub async fn find_by_organization(org_uuid: &OrganizationId, conn: &mut DbConn) -> Vec<Self> {
         db_run! { conn: {
             groups::table
-                .filter(groups::organizations_uuid.eq(organizations_uuid))
+                .filter(groups::organizations_uuid.eq(org_uuid))
                 .load::<GroupDb>(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::<i64>(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<Self> {
+    pub async fn find_by_uuid_and_org(uuid: &GroupId, org_uuid: &OrganizationId, conn: &mut DbConn) -> Option<Self> {
         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<Self> {
+    pub async fn find_by_external_id_and_org(
+        external_id: &str,
+        org_uuid: &OrganizationId,
+        conn: &mut DbConn,
+    ) -> Option<Self> {
         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 gather_user_organizations_full_access(user_uuid: &str, conn: &mut DbConn) -> Vec<String> {
+    pub async fn get_orgs_by_user_with_full_access(user_uuid: &UserId, conn: &mut DbConn) -> Vec<OrganizationId> {
         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::<String>(conn)
+                .load::<OrganizationId>(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<Self> {
+    pub async fn find_by_group(group_uuid: &GroupId, conn: &mut DbConn) -> Vec<Self> {
         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<Self> {
+    pub async fn find_by_user(user_uuid: &UserId, conn: &mut DbConn) -> Vec<Self> {
         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<Self> {
+    pub async fn find_by_collection(collection_uuid: &CollectionId, conn: &mut DbConn) -> Vec<Self> {
         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<Self> {
+    pub async fn find_by_group(group_uuid: &GroupId, conn: &mut DbConn) -> Vec<Self> {
         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<Self> {
+    pub async fn find_by_member(member_uuid: &MembershipId, conn: &mut DbConn) -> Vec<Self> {
         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::<GroupUserDb>(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(
@@ -523,32 +550,32 @@ impl GroupUser {
     }
 
     pub async fn update_user_revision(&self, conn: &mut DbConn) {
-        match UserOrganization::find_by_uuid(&self.users_organizations_uuid, conn).await {
-            Some(user) => User::update_uuid_revision(&user.user_uuid, conn).await,
-            None => warn!("User could not be found!"),
+        match Membership::find_by_uuid(&self.users_organizations_uuid, conn).await {
+            Some(member) => User::update_uuid_revision(&member.user_uuid, conn).await,
+            None => warn!("Member could not be found!"),
         }
     }
 
-    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 UserOrganization::find_by_uuid(users_organizations_uuid, conn).await {
-            Some(user) => User::update_uuid_revision(&user.user_uuid, conn).await,
-            None => warn!("User could not be found!"),
+        match Membership::find_by_uuid(member_uuid, conn).await {
+            Some(member) => User::update_uuid_revision(&member.user_uuid, conn).await,
+            None => warn!("Member could not be found!"),
         };
 
         db_run! { conn: {
             diesel::delete(groups_users::table)
                 .filter(groups_users::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,17 +589,35 @@ impl GroupUser {
         }}
     }
 
-    pub async fn delete_all_by_user(users_organizations_uuid: &str, conn: &mut DbConn) -> EmptyResult {
-        match UserOrganization::find_by_uuid(users_organizations_uuid, conn).await {
-            Some(user) => User::update_uuid_revision(&user.user_uuid, conn).await,
-            None => warn!("User could not be found!"),
+    pub async fn delete_all_by_member(member_uuid: &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!"),
         }
 
         db_run! { conn: {
             diesel::delete(groups_users::table)
-                .filter(groups_users::users_organizations_uuid.eq(users_organizations_uuid))
+                .filter(groups_users::users_organizations_uuid.eq(member_uuid))
                 .execute(conn)
                 .map_res("Error deleting user groups")
         }}
     }
 }
+
+#[derive(
+    Clone,
+    Debug,
+    AsRef,
+    Deref,
+    DieselNewType,
+    Display,
+    From,
+    FromForm,
+    Hash,
+    PartialEq,
+    Eq,
+    Serialize,
+    Deserialize,
+    UuidFromParam,
+)]
+pub struct GroupId(String);

+ 18 - 12
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::{Organization, OrganizationApiKey, UserOrgStatus, UserOrgType, UserOrganization};
-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};

+ 41 - 29
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::{TwoFactor, UserOrgStatus, UserOrgType, UserOrganization};
+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<Self> {
+    pub async fn find_by_org(org_uuid: &OrganizationId, conn: &mut DbConn) -> Vec<Self> {
         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<Self> {
+    pub async fn find_confirmed_by_user(user_uuid: &UserId, conn: &mut DbConn) -> Vec<Self> {
         db_run! { conn: {
             org_policies::table
                 .inner_join(
@@ -161,7 +162,7 @@ impl OrgPolicy {
                             .and(users_organizations::user_uuid.eq(user_uuid)))
                 )
                 .filter(
-                    users_organizations::status.eq(UserOrgStatus::Confirmed as i32)
+                    users_organizations::status.eq(MembershipStatus::Confirmed as i32)
                 )
                 .select(org_policies::all_columns)
                 .load::<OrgPolicyDb>(conn)
@@ -170,7 +171,11 @@ impl OrgPolicy {
         }}
     }
 
-    pub async fn find_by_org_and_type(org_uuid: &str, policy_type: OrgPolicyType, conn: &mut DbConn) -> Option<Self> {
+    pub async fn find_by_org_and_type(
+        org_uuid: &OrganizationId,
+        policy_type: OrgPolicyType,
+        conn: &mut DbConn,
+    ) -> Option<Self> {
         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<Self> {
@@ -202,10 +207,10 @@ impl OrgPolicy {
                             .and(users_organizations::user_uuid.eq(user_uuid)))
                 )
                 .filter(
-                    users_organizations::status.eq(UserOrgStatus::Accepted as i32)
+                    users_organizations::status.eq(MembershipStatus::Accepted as i32)
                 )
                 .or_filter(
-                    users_organizations::status.eq(UserOrgStatus::Confirmed as i32)
+                    users_organizations::status.eq(MembershipStatus::Confirmed as i32)
                 )
                 .filter(org_policies::atype.eq(policy_type as i32))
                 .filter(org_policies::enabled.eq(true))
@@ -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<Self> {
@@ -229,7 +234,7 @@ impl OrgPolicy {
                             .and(users_organizations::user_uuid.eq(user_uuid)))
                 )
                 .filter(
-                    users_organizations::status.eq(UserOrgStatus::Confirmed as i32)
+                    users_organizations::status.eq(MembershipStatus::Confirmed as i32)
                 )
                 .filter(org_policies::atype.eq(policy_type as i32))
                 .filter(org_policies::enabled.eq(true))
@@ -244,21 +249,21 @@ 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;
             }
 
-            if let Some(user) = UserOrganization::find_by_user_and_org(user_uuid, &policy.org_uuid, conn).await {
-                if user.atype < UserOrgType::Admin {
+            if let Some(user) = Membership::find_by_user_and_org(user_uuid, &policy.org_uuid, conn).await {
+                if user.atype < MembershipType::Admin {
                     return true;
                 }
             }
@@ -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::<ResetPasswordDataModel>(&policy.data) {
                 Ok(opts) => {
@@ -312,12 +317,12 @@ 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
         {
-            if let Some(user) = UserOrganization::find_by_user_and_org(user_uuid, &policy.org_uuid, conn).await {
-                if user.atype < UserOrgType::Admin {
+            if let Some(user) = Membership::find_by_user_and_org(user_uuid, &policy.org_uuid, conn).await {
+                if user.atype < MembershipType::Admin {
                     match serde_json::from_str::<SendOptionsPolicyData>(&policy.data) {
                         Ok(opts) => {
                             if opts.disable_hide_email {
@@ -332,12 +337,19 @@ impl OrgPolicy {
         false
     }
 
-    pub async fn is_enabled_for_member(org_user_uuid: &str, policy_type: OrgPolicyType, conn: &mut DbConn) -> bool {
-        if let Some(membership) = UserOrganization::find_by_uuid(org_user_uuid, conn).await {
-            if let Some(policy) = OrgPolicy::find_by_org_and_type(&membership.org_uuid, policy_type, conn).await {
+    pub async fn is_enabled_for_member(
+        member_uuid: &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;
             }
         }
         false
     }
 }
+
+#[derive(Clone, Debug, AsRef, DieselNewType, From, FromForm, PartialEq, Eq, Hash, Serialize, Deserialize)]
+pub struct OrgPolicyId(String);

+ 228 - 136
src/db/models/organization.rs

@@ -1,4 +1,5 @@
 use chrono::{NaiveDateTime, Utc};
+use derive_more::{AsRef, Deref, Display, From};
 use num_traits::FromPrimitive;
 use serde_json::Value;
 use std::{
@@ -6,16 +7,19 @@ use std::{
     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;
+use macros::UuidFromParam;
 
 db_object! {
     #[derive(Identifiable, Queryable, Insertable, AsChangeset)]
     #[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<String>,
@@ -25,10 +29,10 @@ db_object! {
     #[derive(Identifiable, Queryable, Insertable, AsChangeset)]
     #[diesel(table_name = users_organizations)]
     #[diesel(primary_key(uuid))]
-    pub struct UserOrganization {
-        pub uuid: String,
-        pub user_uuid: String,
-        pub org_uuid: String,
+    pub struct Membership {
+        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,
@@ -51,7 +55,7 @@ db_object! {
 }
 
 // https://github.com/bitwarden/server/blob/b86a04cef9f1e1b82cf18e49fc94e017c641130c/src/Core/Enums/OrganizationUserStatusType.cs
-pub enum UserOrgStatus {
+pub enum MembershipStatus {
     Revoked = -1,
     Invited = 0,
     Accepted = 1,
@@ -59,29 +63,29 @@ pub enum UserOrgStatus {
 }
 
 #[derive(Copy, Clone, PartialEq, Eq, num_derive::FromPrimitive)]
-pub enum UserOrgType {
+pub enum MembershipType {
     Owner = 0,
     Admin = 1,
     User = 2,
     Manager = 3,
 }
 
-impl UserOrgType {
+impl MembershipType {
     pub fn from_str(s: &str) -> Option<Self> {
         match s {
-            "0" | "Owner" => Some(UserOrgType::Owner),
-            "1" | "Admin" => Some(UserOrgType::Admin),
-            "2" | "User" => Some(UserOrgType::User),
-            "3" | "Manager" => Some(UserOrgType::Manager),
+            "0" | "Owner" => Some(MembershipType::Owner),
+            "1" | "Admin" => Some(MembershipType::Admin),
+            "2" | "User" => Some(MembershipType::User),
+            "3" | "Manager" => Some(MembershipType::Manager),
             // HACK: We convert the custom role to a manager role
-            "4" | "Custom" => Some(UserOrgType::Manager),
+            "4" | "Custom" => Some(MembershipType::Manager),
             _ => None,
         }
     }
 }
 
-impl Ord for UserOrgType {
-    fn cmp(&self, other: &UserOrgType) -> Ordering {
+impl Ord for MembershipType {
+    fn cmp(&self, other: &MembershipType) -> Ordering {
         // For easy comparison, map each variant to an access level (where 0 is lowest).
         static ACCESS_LEVEL: [i32; 4] = [
             3, // Owner
@@ -93,19 +97,19 @@ impl Ord for UserOrgType {
     }
 }
 
-impl PartialOrd for UserOrgType {
-    fn partial_cmp(&self, other: &UserOrgType) -> Option<Ordering> {
+impl PartialOrd for MembershipType {
+    fn partial_cmp(&self, other: &MembershipType) -> Option<Ordering> {
         Some(self.cmp(other))
     }
 }
 
-impl PartialEq<i32> for UserOrgType {
+impl PartialEq<i32> for MembershipType {
     fn eq(&self, other: &i32) -> bool {
         *other == *self as i32
     }
 }
 
-impl PartialOrd<i32> for UserOrgType {
+impl PartialOrd<i32> for MembershipType {
     fn partial_cmp(&self, other: &i32) -> Option<Ordering> {
         if let Some(other) = Self::from_i32(*other) {
             return Some(self.cmp(&other));
@@ -122,25 +126,25 @@ impl PartialOrd<i32> for UserOrgType {
     }
 }
 
-impl PartialEq<UserOrgType> for i32 {
-    fn eq(&self, other: &UserOrgType) -> bool {
+impl PartialEq<MembershipType> for i32 {
+    fn eq(&self, other: &MembershipType) -> bool {
         *self == *other as i32
     }
 }
 
-impl PartialOrd<UserOrgType> for i32 {
-    fn partial_cmp(&self, other: &UserOrgType) -> Option<Ordering> {
-        if let Some(self_type) = UserOrgType::from_i32(*self) {
+impl PartialOrd<MembershipType> for i32 {
+    fn partial_cmp(&self, other: &MembershipType) -> Option<Ordering> {
+        if let Some(self_type) = MembershipType::from_i32(*self) {
             return Some(self_type.cmp(other));
         }
         None
     }
 
-    fn lt(&self, other: &UserOrgType) -> bool {
+    fn lt(&self, other: &MembershipType) -> bool {
         matches!(self.partial_cmp(other), Some(Ordering::Less) | None)
     }
 
-    fn le(&self, other: &UserOrgType) -> bool {
+    fn le(&self, other: &MembershipType) -> bool {
         matches!(self.partial_cmp(other), Some(Ordering::Less | Ordering::Equal) | None)
     }
 }
@@ -149,7 +153,7 @@ impl PartialOrd<UserOrgType> for i32 {
 impl Organization {
     pub fn new(name: String, billing_email: String, private_key: Option<String>, public_key: Option<String>) -> Self {
         Self {
-            uuid: crate::util::get_uuid(),
+            uuid: OrganizationId(crate::util::get_uuid()),
             name,
             billing_email,
             private_key,
@@ -214,25 +218,25 @@ impl Organization {
 // It should also provide enough room for 100+ types, which i doubt will ever happen.
 static ACTIVATE_REVOKE_DIFF: i32 = 128;
 
-impl UserOrganization {
-    pub fn new(user_uuid: String, org_uuid: String) -> Self {
+impl Membership {
+    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,
 
             access_all: false,
             akey: String::new(),
-            status: UserOrgStatus::Accepted as i32,
-            atype: UserOrgType::User as i32,
+            status: MembershipStatus::Accepted as i32,
+            atype: MembershipType::User as i32,
             reset_password_key: None,
             external_id: None,
         }
     }
 
     pub fn restore(&mut self) -> bool {
-        if self.status < UserOrgStatus::Invited as i32 {
+        if self.status < MembershipStatus::Invited as i32 {
             self.status += ACTIVATE_REVOKE_DIFF;
             return true;
         }
@@ -240,7 +244,7 @@ impl UserOrganization {
     }
 
     pub fn revoke(&mut self) -> bool {
-        if self.status > UserOrgStatus::Revoked as i32 {
+        if self.status > MembershipStatus::Revoked as i32 {
             self.status -= ACTIVATE_REVOKE_DIFF;
             return true;
         }
@@ -249,7 +253,7 @@ impl UserOrganization {
 
     /// Return the status of the user in an unrevoked state
     pub fn get_unrevoked_status(&self) -> i32 {
-        if self.status <= UserOrgStatus::Revoked as i32 {
+        if self.status <= MembershipStatus::Revoked as i32 {
             return self.status + ACTIVATE_REVOKE_DIFF;
         }
         self.status
@@ -279,9 +283,9 @@ impl UserOrganization {
 }
 
 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
@@ -307,8 +311,8 @@ impl Organization {
             err!(format!("BillingEmail {} is not a valid email address", self.billing_email.trim()))
         }
 
-        for user_org in UserOrganization::find_by_org(&self.uuid, conn).await.iter() {
-            User::update_uuid_revision(&user_org.user_uuid, conn).await;
+        for member in Membership::find_by_org(&self.uuid, conn).await.iter() {
+            User::update_uuid_revision(&member.user_uuid, conn).await;
         }
 
         db_run! { conn:
@@ -348,7 +352,7 @@ impl Organization {
 
         Cipher::delete_all_by_organization(&self.uuid, conn).await?;
         Collection::delete_all_by_organization(&self.uuid, conn).await?;
-        UserOrganization::delete_all_by_organization(&self.uuid, conn).await?;
+        Membership::delete_all_by_organization(&self.uuid, conn).await?;
         OrgPolicy::delete_all_by_organization(&self.uuid, conn).await?;
         Group::delete_all_by_organization(&self.uuid, conn).await?;
         OrganizationApiKey::delete_all_by_organization(&self.uuid, conn).await?;
@@ -360,7 +364,7 @@ impl Organization {
         }}
     }
 
-    pub async fn find_by_uuid(uuid: &str, conn: &mut DbConn) -> Option<Self> {
+    pub async fn find_by_uuid(uuid: &OrganizationId, conn: &mut DbConn) -> Option<Self> {
         db_run! { conn: {
             organizations::table
                 .filter(organizations::uuid.eq(uuid))
@@ -376,13 +380,13 @@ impl Organization {
     }
 }
 
-impl UserOrganization {
+impl Membership {
     pub async fn to_json(&self, conn: &mut DbConn) -> Value {
         let org = Organization::find_by_uuid(&self.org_uuid, conn).await.unwrap();
 
         // HACK: Convert the manager type to a custom type
         // It will be converted back on other locations
-        let user_org_type = self.type_manager_as_custom();
+        let membership_type = self.type_manager_as_custom();
 
         let permissions = json!({
                 // TODO: Add full support for Custom User Roles
@@ -392,9 +396,9 @@ impl UserOrganization {
                 "accessImportExport": false,
                 "accessReports": false,
                 // If the following 3 Collection roles are set to true a custom user has access all permission
-                "createNewCollections": user_org_type == 4 && self.access_all,
-                "editAnyCollection": user_org_type == 4 && self.access_all,
-                "deleteAnyCollection": user_org_type == 4 && self.access_all,
+                "createNewCollections": membership_type == 4 && self.access_all,
+                "editAnyCollection": membership_type == 4 && self.access_all,
+                "deleteAnyCollection": membership_type == 4 && self.access_all,
                 "manageGroups": false,
                 "managePolicies": false,
                 "manageSso": false, // Not supported
@@ -459,7 +463,7 @@ impl UserOrganization {
             "userId": self.user_uuid,
             "key": self.akey,
             "status": self.status,
-            "type": user_org_type,
+            "type": membership_type,
             "enabled": true,
 
             "object": "profileOrganization",
@@ -476,16 +480,16 @@ impl UserOrganization {
 
         // Because BitWarden want the status to be -1 for revoked users we need to catch that here.
         // We subtract/add a number so we can restore/activate the user to it's previous state again.
-        let status = if self.status < UserOrgStatus::Revoked as i32 {
-            UserOrgStatus::Revoked as i32
+        let status = if self.status < MembershipStatus::Revoked as i32 {
+            MembershipStatus::Revoked as i32
         } else {
             self.status
         };
 
         let twofactor_enabled = !TwoFactor::find_by_user(&user.uuid, conn).await.is_empty();
 
-        let groups: Vec<String> = 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<GroupId> = 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 UserOrganization {
         // 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<Value> = 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<String, CollectionUser> =
+            let cu: HashMap<CollectionId, CollectionUser> =
                 CollectionUser::find_by_organization_and_user_uuid(&self.org_uuid, &self.user_uuid, conn)
                     .await
                     .into_iter()
@@ -508,7 +512,7 @@ impl UserOrganization {
                     .collect();
 
             // Get all collection groups for this user to prevent there inclusion
-            let cg: HashSet<String> = CollectionGroup::find_by_user(&self.user_uuid, conn)
+            let cg: HashSet<CollectionId> = CollectionGroup::find_by_user(&self.user_uuid, conn)
                 .await
                 .into_iter()
                 .map(|cg| cg.collections_uuid)
@@ -519,12 +523,12 @@ impl UserOrganization {
                 .into_iter()
                 .filter_map(|c| {
                     let (read_only, hide_passwords, can_manage) = if self.has_full_access() {
-                        (false, false, self.atype >= UserOrgType::Manager)
+                        (false, false, self.atype >= MembershipType::Manager)
                     } else if let Some(cu) = cu.get(&c.uuid) {
                         (
                             cu.read_only,
                             cu.hide_passwords,
-                            self.atype == UserOrgType::Manager && !cu.read_only && !cu.hide_passwords,
+                            self.atype == MembershipType::Manager && !cu.read_only && !cu.hide_passwords,
                         )
                     // If previous checks failed it might be that this user has access via a group, but we should not return those elements here
                     // Those are returned via a special group endpoint
@@ -548,11 +552,11 @@ impl UserOrganization {
 
         // HACK: Convert the manager type to a custom type
         // It will be converted back on other locations
-        let user_org_type = self.type_manager_as_custom();
+        let membership_type = self.type_manager_as_custom();
 
         // HACK: Only return permissions if the user is of type custom and has access_all
         // Else Bitwarden will assume the defaults of all false
-        let permissions = if user_org_type == 4 && self.access_all {
+        let permissions = if membership_type == 4 && self.access_all {
             json!({
                 // TODO: Add full support for Custom User Roles
                 // See: https://bitwarden.com/help/article/user-types-access-control/#custom-role
@@ -578,7 +582,7 @@ impl UserOrganization {
         json!({
             "id": self.uuid,
             "userId": self.user_uuid,
-            "name": if self.get_unrevoked_status() >= UserOrgStatus::Accepted as i32 { Some(user.name) } else { None },
+            "name": if self.get_unrevoked_status() >= MembershipStatus::Accepted as i32 { Some(user.name) } else { None },
             "email": user.email,
             "externalId": self.external_id,
             "avatarColor": user.avatar_color,
@@ -586,7 +590,7 @@ impl UserOrganization {
             "collections": collections,
 
             "status": status,
-            "type": user_org_type,
+            "type": membership_type,
             "accessAll": self.access_all,
             "twoFactorEnabled": twofactor_enabled,
             "resetPasswordEnrolled": self.reset_password_key.is_some(),
@@ -630,8 +634,8 @@ impl UserOrganization {
 
         // Because BitWarden want the status to be -1 for revoked users we need to catch that here.
         // We subtract/add a number so we can restore/activate the user to it's previous state again.
-        let status = if self.status < UserOrgStatus::Revoked as i32 {
-            UserOrgStatus::Revoked as i32
+        let status = if self.status < MembershipStatus::Revoked as i32 {
+            MembershipStatus::Revoked as i32
         } else {
             self.status
         };
@@ -654,8 +658,8 @@ impl UserOrganization {
 
         // Because Bitwarden wants the status to be -1 for revoked users we need to catch that here.
         // We subtract/add a number so we can restore/activate the user to it's previous state again.
-        let status = if self.status < UserOrgStatus::Revoked as i32 {
-            UserOrgStatus::Revoked as i32
+        let status = if self.status < MembershipStatus::Revoked as i32 {
+            MembershipStatus::Revoked as i32
         } else {
             self.status
         };
@@ -677,7 +681,7 @@ impl UserOrganization {
         db_run! { conn:
             sqlite, mysql {
                 match diesel::replace_into(users_organizations::table)
-                    .values(UserOrganizationDb::to_db(self))
+                    .values(MembershipDb::to_db(self))
                     .execute(conn)
                 {
                     Ok(_) => Ok(()),
@@ -685,7 +689,7 @@ impl UserOrganization {
                     Err(diesel::result::Error::DatabaseError(diesel::result::DatabaseErrorKind::ForeignKeyViolation, _)) => {
                         diesel::update(users_organizations::table)
                             .filter(users_organizations::uuid.eq(&self.uuid))
-                            .set(UserOrganizationDb::to_db(self))
+                            .set(MembershipDb::to_db(self))
                             .execute(conn)
                             .map_res("Error adding user to organization")
                     },
@@ -693,7 +697,7 @@ impl UserOrganization {
                 }.map_res("Error adding user to organization")
             }
             postgresql {
-                let value = UserOrganizationDb::to_db(self);
+                let value = MembershipDb::to_db(self);
                 diesel::insert_into(users_organizations::table)
                     .values(&value)
                     .on_conflict(users_organizations::uuid)
@@ -709,7 +713,7 @@ impl UserOrganization {
         User::update_uuid_revision(&self.user_uuid, conn).await;
 
         CollectionUser::delete_all_by_user_and_org(&self.user_uuid, &self.org_uuid, conn).await?;
-        GroupUser::delete_all_by_user(&self.uuid, conn).await?;
+        GroupUser::delete_all_by_member(&self.uuid, conn).await?;
 
         db_run! { conn: {
             diesel::delete(users_organizations::table.filter(users_organizations::uuid.eq(self.uuid)))
@@ -718,121 +722,129 @@ impl UserOrganization {
         }}
     }
 
-    pub async fn delete_all_by_organization(org_uuid: &str, conn: &mut DbConn) -> EmptyResult {
-        for user_org in Self::find_by_org(org_uuid, conn).await {
-            user_org.delete(conn).await?;
+    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 {
-        for user_org in Self::find_any_state_by_user(user_uuid, conn).await {
-            user_org.delete(conn).await?;
+    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<UserOrganization> {
+    pub async fn find_by_email_and_org(
+        email: &str,
+        org_uuid: &OrganizationId,
+        conn: &mut DbConn,
+    ) -> Option<Membership> {
         if let Some(user) = User::find_by_mail(email, conn).await {
-            if let Some(user_org) = UserOrganization::find_by_user_and_org(&user.uuid, org_id, conn).await {
-                return Some(user_org);
+            if let Some(member) = Membership::find_by_user_and_org(&user.uuid, org_uuid, conn).await {
+                return Some(member);
             }
         }
 
         None
     }
 
-    pub fn has_status(&self, status: UserOrgStatus) -> bool {
+    pub fn has_status(&self, status: MembershipStatus) -> bool {
         self.status == status as i32
     }
 
-    pub fn has_type(&self, user_type: UserOrgType) -> bool {
+    pub fn has_type(&self, user_type: MembershipType) -> bool {
         self.atype == user_type as i32
     }
 
     pub fn has_full_access(&self) -> bool {
-        (self.access_all || self.atype >= UserOrgType::Admin) && self.has_status(UserOrgStatus::Confirmed)
+        (self.access_all || self.atype >= MembershipType::Admin) && self.has_status(MembershipStatus::Confirmed)
     }
 
-    pub async fn find_by_uuid(uuid: &str, conn: &mut DbConn) -> Option<Self> {
+    pub async fn find_by_uuid(uuid: &MembershipId, conn: &mut DbConn) -> Option<Self> {
         db_run! { conn: {
             users_organizations::table
                 .filter(users_organizations::uuid.eq(uuid))
-                .first::<UserOrganizationDb>(conn)
+                .first::<MembershipDb>(conn)
                 .ok().from_db()
         }}
     }
 
-    pub async fn find_by_uuid_and_org(uuid: &str, org_uuid: &str, conn: &mut DbConn) -> Option<Self> {
+    pub async fn find_by_uuid_and_org(
+        uuid: &MembershipId,
+        org_uuid: &OrganizationId,
+        conn: &mut DbConn,
+    ) -> Option<Self> {
         db_run! { conn: {
             users_organizations::table
                 .filter(users_organizations::uuid.eq(uuid))
                 .filter(users_organizations::org_uuid.eq(org_uuid))
-                .first::<UserOrganizationDb>(conn)
+                .first::<MembershipDb>(conn)
                 .ok().from_db()
         }}
     }
 
-    pub async fn find_confirmed_by_user(user_uuid: &str, conn: &mut DbConn) -> Vec<Self> {
+    pub async fn find_confirmed_by_user(user_uuid: &UserId, conn: &mut DbConn) -> Vec<Self> {
         db_run! { conn: {
             users_organizations::table
                 .filter(users_organizations::user_uuid.eq(user_uuid))
-                .filter(users_organizations::status.eq(UserOrgStatus::Confirmed as i32))
-                .load::<UserOrganizationDb>(conn)
+                .filter(users_organizations::status.eq(MembershipStatus::Confirmed as i32))
+                .load::<MembershipDb>(conn)
                 .unwrap_or_default().from_db()
         }}
     }
 
-    pub async fn find_invited_by_user(user_uuid: &str, conn: &mut DbConn) -> Vec<Self> {
+    pub async fn find_invited_by_user(user_uuid: &UserId, conn: &mut DbConn) -> Vec<Self> {
         db_run! { conn: {
             users_organizations::table
                 .filter(users_organizations::user_uuid.eq(user_uuid))
-                .filter(users_organizations::status.eq(UserOrgStatus::Invited as i32))
-                .load::<UserOrganizationDb>(conn)
+                .filter(users_organizations::status.eq(MembershipStatus::Invited as i32))
+                .load::<MembershipDb>(conn)
                 .unwrap_or_default().from_db()
         }}
     }
 
-    pub async fn find_any_state_by_user(user_uuid: &str, conn: &mut DbConn) -> Vec<Self> {
+    pub async fn find_any_state_by_user(user_uuid: &UserId, conn: &mut DbConn) -> Vec<Self> {
         db_run! { conn: {
             users_organizations::table
                 .filter(users_organizations::user_uuid.eq(user_uuid))
-                .load::<UserOrganizationDb>(conn)
+                .load::<MembershipDb>(conn)
                 .unwrap_or_default().from_db()
         }}
     }
 
-    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))
-                .filter(users_organizations::status.eq(UserOrgStatus::Accepted as i32).or(users_organizations::status.eq(UserOrgStatus::Confirmed as i32)))
+                .filter(users_organizations::status.eq(MembershipStatus::Accepted as i32).or(users_organizations::status.eq(MembershipStatus::Confirmed as i32)))
                 .count()
                 .first::<i64>(conn)
                 .unwrap_or(0)
         }}
     }
 
-    pub async fn find_by_org(org_uuid: &str, conn: &mut DbConn) -> Vec<Self> {
+    pub async fn find_by_org(org_uuid: &OrganizationId, conn: &mut DbConn) -> Vec<Self> {
         db_run! { conn: {
             users_organizations::table
                 .filter(users_organizations::org_uuid.eq(org_uuid))
-                .load::<UserOrganizationDb>(conn)
+                .load::<MembershipDb>(conn)
                 .expect("Error loading user organizations").from_db()
         }}
     }
 
-    pub async fn find_confirmed_by_org(org_uuid: &str, conn: &mut DbConn) -> Vec<Self> {
+    pub async fn find_confirmed_by_org(org_uuid: &OrganizationId, conn: &mut DbConn) -> Vec<Self> {
         db_run! { conn: {
             users_organizations::table
                 .filter(users_organizations::org_uuid.eq(org_uuid))
-                .filter(users_organizations::status.eq(UserOrgStatus::Confirmed as i32))
-                .load::<UserOrganizationDb>(conn)
+                .filter(users_organizations::status.eq(MembershipStatus::Confirmed as i32))
+                .load::<MembershipDb>(conn)
                 .unwrap_or_default().from_db()
         }}
     }
 
-    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,71 +855,91 @@ impl UserOrganization {
         }}
     }
 
-    pub async fn find_by_org_and_type(org_uuid: &str, atype: UserOrgType, conn: &mut DbConn) -> Vec<Self> {
+    pub async fn find_by_org_and_type(
+        org_uuid: &OrganizationId,
+        atype: MembershipType,
+        conn: &mut DbConn,
+    ) -> Vec<Self> {
         db_run! { conn: {
             users_organizations::table
                 .filter(users_organizations::org_uuid.eq(org_uuid))
                 .filter(users_organizations::atype.eq(atype as i32))
-                .load::<UserOrganizationDb>(conn)
+                .load::<MembershipDb>(conn)
                 .expect("Error loading user organizations").from_db()
         }}
     }
 
-    pub async fn count_confirmed_by_org_and_type(org_uuid: &str, atype: UserOrgType, conn: &mut DbConn) -> i64 {
+    pub async fn count_confirmed_by_org_and_type(
+        org_uuid: &OrganizationId,
+        atype: MembershipType,
+        conn: &mut DbConn,
+    ) -> i64 {
         db_run! { conn: {
             users_organizations::table
                 .filter(users_organizations::org_uuid.eq(org_uuid))
                 .filter(users_organizations::atype.eq(atype as i32))
-                .filter(users_organizations::status.eq(UserOrgStatus::Confirmed as i32))
+                .filter(users_organizations::status.eq(MembershipStatus::Confirmed as i32))
                 .count()
                 .first::<i64>(conn)
                 .unwrap_or(0)
         }}
     }
 
-    pub async fn find_by_user_and_org(user_uuid: &str, org_uuid: &str, conn: &mut DbConn) -> Option<Self> {
+    pub async fn find_by_user_and_org(
+        user_uuid: &UserId,
+        org_uuid: &OrganizationId,
+        conn: &mut DbConn,
+    ) -> Option<Self> {
         db_run! { conn: {
             users_organizations::table
                 .filter(users_organizations::user_uuid.eq(user_uuid))
                 .filter(users_organizations::org_uuid.eq(org_uuid))
-                .first::<UserOrganizationDb>(conn)
+                .first::<MembershipDb>(conn)
                 .ok().from_db()
         }}
     }
 
-    pub async fn find_confirmed_by_user_and_org(user_uuid: &str, org_uuid: &str, conn: &mut DbConn) -> Option<Self> {
+    pub async fn find_confirmed_by_user_and_org(
+        user_uuid: &UserId,
+        org_uuid: &OrganizationId,
+        conn: &mut DbConn,
+    ) -> Option<Self> {
         db_run! { conn: {
             users_organizations::table
                 .filter(users_organizations::user_uuid.eq(user_uuid))
                 .filter(users_organizations::org_uuid.eq(org_uuid))
                 .filter(
-                    users_organizations::status.eq(UserOrgStatus::Confirmed as i32)
+                    users_organizations::status.eq(MembershipStatus::Confirmed as i32)
                 )
-                .first::<UserOrganizationDb>(conn)
+                .first::<MembershipDb>(conn)
                 .ok().from_db()
         }}
     }
 
-    pub async fn find_by_user(user_uuid: &str, conn: &mut DbConn) -> Vec<Self> {
+    pub async fn find_by_user(user_uuid: &UserId, conn: &mut DbConn) -> Vec<Self> {
         db_run! { conn: {
             users_organizations::table
                 .filter(users_organizations::user_uuid.eq(user_uuid))
-                .load::<UserOrganizationDb>(conn)
+                .load::<MembershipDb>(conn)
                 .expect("Error loading user organizations").from_db()
         }}
     }
 
-    pub async fn get_org_uuid_by_user(user_uuid: &str, conn: &mut DbConn) -> Vec<String> {
+    pub async fn get_orgs_by_user(user_uuid: &UserId, conn: &mut DbConn) -> Vec<OrganizationId> {
         db_run! { conn: {
             users_organizations::table
                 .filter(users_organizations::user_uuid.eq(user_uuid))
                 .select(users_organizations::org_uuid)
-                .load::<String>(conn)
+                .load::<OrganizationId>(conn)
                 .unwrap_or_default()
         }}
     }
 
-    pub async fn find_by_user_and_policy(user_uuid: &str, policy_type: OrgPolicyType, conn: &mut DbConn) -> Vec<Self> {
+    pub async fn find_by_user_and_policy(
+        user_uuid: &UserId,
+        policy_type: OrgPolicyType,
+        conn: &mut DbConn,
+    ) -> Vec<Self> {
         db_run! { conn: {
             users_organizations::table
                 .inner_join(
@@ -918,15 +950,19 @@ impl UserOrganization {
                             .and(org_policies::enabled.eq(true)))
                 )
                 .filter(
-                    users_organizations::status.eq(UserOrgStatus::Confirmed as i32)
+                    users_organizations::status.eq(MembershipStatus::Confirmed as i32)
                 )
                 .select(users_organizations::all_columns)
-                .load::<UserOrganizationDb>(conn)
+                .load::<MembershipDb>(conn)
                 .unwrap_or_default().from_db()
         }}
     }
 
-    pub async fn find_by_cipher_and_org(cipher_uuid: &str, org_uuid: &str, conn: &mut DbConn) -> Vec<Self> {
+    pub async fn find_by_cipher_and_org(
+        cipher_uuid: &CipherId,
+        org_uuid: &OrganizationId,
+        conn: &mut DbConn,
+    ) -> Vec<Self> {
         db_run! { conn: {
             users_organizations::table
             .filter(users_organizations::org_uuid.eq(org_uuid))
@@ -945,11 +981,15 @@ impl UserOrganization {
             )
             .select(users_organizations::all_columns)
             .distinct()
-            .load::<UserOrganizationDb>(conn).expect("Error loading user organizations").from_db()
+            .load::<MembershipDb>(conn).expect("Error loading user organizations").from_db()
         }}
     }
 
-    pub async fn find_by_cipher_and_org_with_group(cipher_uuid: &str, org_uuid: &str, conn: &mut DbConn) -> Vec<Self> {
+    pub async fn find_by_cipher_and_org_with_group(
+        cipher_uuid: &CipherId,
+        org_uuid: &OrganizationId,
+        conn: &mut DbConn,
+    ) -> Vec<Self> {
         db_run! { conn: {
             users_organizations::table
             .filter(users_organizations::org_uuid.eq(org_uuid))
@@ -971,23 +1011,31 @@ impl UserOrganization {
                 )
                 .select(users_organizations::all_columns)
                 .distinct()
-            .load::<UserOrganizationDb>(conn).expect("Error loading user organizations with groups").from_db()
+            .load::<MembershipDb>(conn).expect("Error loading user organizations with groups").from_db()
         }}
     }
 
-    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()))))
             .filter(users_organizations::user_uuid.eq(user_uuid))
-            .filter(users_organizations::atype.eq_any(vec![UserOrgType::Owner as i32, UserOrgType::Admin as i32]))
+            .filter(users_organizations::atype.eq_any(vec![MembershipType::Owner as i32, MembershipType::Admin as i32]))
             .count()
             .first::<i64>(conn)
             .ok().unwrap_or(0) != 0
         }}
     }
 
-    pub async fn find_by_collection_and_org(collection_uuid: &str, org_uuid: &str, conn: &mut DbConn) -> Vec<Self> {
+    pub async fn find_by_collection_and_org(
+        collection_uuid: &CollectionId,
+        org_uuid: &OrganizationId,
+        conn: &mut DbConn,
+    ) -> Vec<Self> {
         db_run! { conn: {
             users_organizations::table
             .filter(users_organizations::org_uuid.eq(org_uuid))
@@ -1000,18 +1048,22 @@ impl UserOrganization {
                 )
             )
             .select(users_organizations::all_columns)
-            .load::<UserOrganizationDb>(conn).expect("Error loading user organizations").from_db()
+            .load::<MembershipDb>(conn).expect("Error loading user organizations").from_db()
         }}
     }
 
-    pub async fn find_by_external_id_and_org(ext_id: &str, org_uuid: &str, conn: &mut DbConn) -> Option<Self> {
+    pub async fn find_by_external_id_and_org(
+        ext_id: &str,
+        org_uuid: &OrganizationId,
+        conn: &mut DbConn,
+    ) -> Option<Self> {
         db_run! {conn: {
             users_organizations::table
             .filter(
                 users_organizations::external_id.eq(ext_id)
                 .and(users_organizations::org_uuid.eq(org_uuid))
             )
-            .first::<UserOrganizationDb>(conn).ok().from_db()
+            .first::<MembershipDb>(conn).ok().from_db()
         }}
     }
 }
@@ -1050,7 +1102,7 @@ impl OrganizationApiKey {
         }
     }
 
-    pub async fn find_by_org_uuid(org_uuid: &str, conn: &DbConn) -> Option<Self> {
+    pub async fn find_by_org_uuid(org_uuid: &OrganizationId, conn: &DbConn) -> Option<Self> {
         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,16 +1120,56 @@ impl OrganizationApiKey {
     }
 }
 
+#[derive(
+    Clone,
+    Debug,
+    AsRef,
+    Deref,
+    DieselNewType,
+    Display,
+    From,
+    FromForm,
+    Hash,
+    PartialEq,
+    Eq,
+    Serialize,
+    Deserialize,
+    UuidFromParam,
+)]
+#[deref(forward)]
+#[from(forward)]
+pub struct OrganizationId(String);
+
+#[derive(
+    Clone,
+    Debug,
+    Deref,
+    DieselNewType,
+    Display,
+    From,
+    FromForm,
+    Hash,
+    PartialEq,
+    Eq,
+    Serialize,
+    Deserialize,
+    UuidFromParam,
+)]
+pub struct MembershipId(String);
+
+#[derive(Clone, Debug, DieselNewType, Display, FromForm, Hash, PartialEq, Eq, Serialize, Deserialize)]
+pub struct OrgApiKeyId(String);
+
 #[cfg(test)]
 mod tests {
     use super::*;
 
     #[test]
     #[allow(non_snake_case)]
-    fn partial_cmp_UserOrgType() {
-        assert!(UserOrgType::Owner > UserOrgType::Admin);
-        assert!(UserOrgType::Admin > UserOrgType::Manager);
-        assert!(UserOrgType::Manager > UserOrgType::User);
-        assert!(UserOrgType::Manager == UserOrgType::from_str("4").unwrap());
+    fn partial_cmp_MembershipType() {
+        assert!(MembershipType::Owner > MembershipType::Admin);
+        assert!(MembershipType::Admin > MembershipType::Manager);
+        assert!(MembershipType::Manager > MembershipType::User);
+        assert!(MembershipType::Manager == MembershipType::from_str("4").unwrap());
     }
 }

+ 59 - 14
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<String>,
-        pub organization_uuid: Option<String>,
+        pub uuid: SendId,
 
+        pub user_uuid: Option<UserId>,
+        pub organization_uuid: Option<OrganizationId>,
 
         pub name: String,
         pub notes: Option<String>,
@@ -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<String> {
+    pub async fn update_users_revision(&self, conn: &mut DbConn) -> Vec<UserId> {
         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<Self> {
+    pub async fn find_by_uuid(uuid: &SendId, conn: &mut DbConn) -> Option<Self> {
         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<Self> {
+    pub async fn find_by_uuid_and_user(uuid: &SendId, user_uuid: &UserId, conn: &mut DbConn) -> Option<Self> {
         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<Self> {
+    pub async fn find_by_user(user_uuid: &UserId, conn: &mut DbConn) -> Vec<Self> {
         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<i64> {
+    pub async fn size_by_user(user_uuid: &UserId, conn: &mut DbConn) -> Option<i64> {
         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<Self> {
+    pub async fn find_by_org(org_uuid: &OrganizationId, conn: &mut DbConn) -> Vec<Self> {
         db_run! {conn: {
             sends::table
                 .filter(sends::organization_uuid.eq(org_uuid))
@@ -349,3 +349,48 @@ impl Send {
         }}
     }
 }
+
+// separate namespace to avoid name collision with std::marker::Send
+pub mod id {
+    use derive_more::{AsRef, Deref, Display, From};
+    use macros::{IdFromParam, UuidFromParam};
+    use std::marker::Send;
+    use std::path::Path;
+
+    #[derive(
+        Clone,
+        Debug,
+        AsRef,
+        Deref,
+        DieselNewType,
+        Display,
+        From,
+        FromForm,
+        Hash,
+        PartialEq,
+        Eq,
+        Serialize,
+        Deserialize,
+        UuidFromParam,
+    )]
+    pub struct SendId(String);
+
+    impl AsRef<Path> for SendId {
+        #[inline]
+        fn as_ref(&self) -> &Path {
+            Path::new(&self.0)
+        }
+    }
+
+    #[derive(
+        Clone, Debug, AsRef, Deref, Display, From, FromForm, Hash, PartialEq, Eq, Serialize, Deserialize, IdFromParam,
+    )]
+    pub struct SendFileId(String);
+
+    impl AsRef<Path> for SendFileId {
+        #[inline]
+        fn as_ref(&self) -> &Path {
+            Path::new(&self.0)
+        }
+    }
+}

+ 11 - 7
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<Self> {
+    pub async fn find_by_user(user_uuid: &UserId, conn: &mut DbConn) -> Vec<Self> {
         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<Self> {
+    pub async fn find_by_user_and_type(user_uuid: &UserId, atype: i32, conn: &mut DbConn) -> Option<Self> {
         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);

+ 26 - 9
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<Self> {
+    pub async fn find_by_user_and_device(
+        user_uuid: &UserId,
+        device_uuid: &DeviceId,
+        conn: &mut DbConn,
+    ) -> Option<Self> {
         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)

+ 44 - 24
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 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,
+};
+use macros::UuidFromParam;
 
 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,20 +224,11 @@ impl User {
     }
 }
 
-use super::{
-    Cipher, Device, EmergencyAccess, Favorite, Folder, Send, TwoFactor, TwoFactorIncomplete, UserOrgType,
-    UserOrganization,
-};
-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 {
         let mut orgs_json = Vec::new();
-        for c in UserOrganization::find_confirmed_by_user(&self.uuid, conn).await {
+        for c in Membership::find_confirmed_by_user(&self.uuid, conn).await {
             orgs_json.push(c.to_json(conn).await);
         }
 
@@ -304,19 +305,18 @@ impl User {
     }
 
     pub async fn delete(self, conn: &mut DbConn) -> EmptyResult {
-        for user_org in UserOrganization::find_confirmed_by_user(&self.uuid, conn).await {
-            if user_org.atype == UserOrgType::Owner
-                && UserOrganization::count_confirmed_by_org_and_type(&user_org.org_uuid, UserOrgType::Owner, conn).await
-                    <= 1
+        for member in Membership::find_confirmed_by_user(&self.uuid, conn).await {
+            if member.atype == MembershipType::Owner
+                && Membership::count_confirmed_by_org_and_type(&member.org_uuid, MembershipType::Owner, conn).await <= 1
             {
                 err!("Can't delete last owner")
             }
         }
 
-        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?;
-        UserOrganization::delete_all_by_user(&self.uuid, conn).await?;
+        Membership::delete_all_by_user(&self.uuid, conn).await?;
         Cipher::delete_all_by_user(&self.uuid, conn).await?;
         Favorite::delete_all_by_user(&self.uuid, conn).await?;
         Folder::delete_all_by_user(&self.uuid, conn).await?;
@@ -332,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);
         }
@@ -357,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)))
@@ -379,7 +379,7 @@ impl User {
         }}
     }
 
-    pub async fn find_by_uuid(uuid: &str, conn: &mut DbConn) -> Option<Self> {
+    pub async fn find_by_uuid(uuid: &UserId, conn: &mut DbConn) -> Option<Self> {
         db_run! {conn: {
             users::table.filter(users::uuid.eq(uuid)).first::<UserDb>(conn).ok().from_db()
         }}
@@ -458,3 +458,23 @@ impl Invitation {
         }
     }
 }
+
+#[derive(
+    Clone,
+    Debug,
+    DieselNewType,
+    FromForm,
+    PartialEq,
+    Eq,
+    Hash,
+    Serialize,
+    Deserialize,
+    AsRef,
+    Deref,
+    Display,
+    From,
+    UuidFromParam,
+)]
+#[deref(forward)]
+#[from(forward)]
+pub struct UserId(String);

+ 28 - 20
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<String>) -> 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<String>,
-    org_user_id: Option<String>,
+    org_id: Option<OrganizationId>,
+    member_id: Option<MembershipId>,
     org_name: &str,
     invited_by_email: Option<String>,
 ) -> EmptyResult {
@@ -268,18 +268,26 @@ pub async fn send_invite(
         user.uuid.clone(),
         user.email.clone(),
         org_id.clone(),
-        org_user_id.clone(),
+        member_id.clone(),
         invited_by_email,
     );
     let invite_token = encode_jwt(&claims);
+    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", org_user_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));

+ 2 - 0
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,

+ 1 - 38
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<Path> 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<Self, Self::Error> {
-        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"];

部分文件因为文件数量过多而无法显示