diff --git a/docker/DockerSettings.yaml b/docker/DockerSettings.yaml index 7a2ea17b..a38e327a 100644 --- a/docker/DockerSettings.yaml +++ b/docker/DockerSettings.yaml @@ -1,6 +1,6 @@ --- -vault_version: "v2025.1.1" -vault_image_digest: "sha256:cb6b2095a4afc1d9d243a33f6d09211f40e3d82c7ae829fd025df5ff175a4918" +vault_version: "v2025.1.0" +vault_image_digest: "sha256:72d636334b4ad6fe9ba1d12e0cda562cd31772cf28772f6b2fe4121a537b72a8" # Cross Compile Docker Helper Scripts v1.6.1 # We use the linux/amd64 platform shell scripts since there is no difference between the different platform scripts # https://github.com/tonistiigi/xx | https://hub.docker.com/r/tonistiigi/xx/tags diff --git a/docker/Dockerfile.alpine b/docker/Dockerfile.alpine index 3335aa62..2757f6cc 100644 --- a/docker/Dockerfile.alpine +++ b/docker/Dockerfile.alpine @@ -19,15 +19,15 @@ # - From https://hub.docker.com/r/vaultwarden/web-vault/tags, # click the tag name to view the digest of the image it currently points to. # - From the command line: -# $ docker pull docker.io/vaultwarden/web-vault:v2025.1.1 -# $ docker image inspect --format "{{.RepoDigests}}" docker.io/vaultwarden/web-vault:v2025.1.1 -# [docker.io/vaultwarden/web-vault@sha256:cb6b2095a4afc1d9d243a33f6d09211f40e3d82c7ae829fd025df5ff175a4918] +# $ docker pull docker.io/vaultwarden/web-vault:v2025.1.0 +# $ docker image inspect --format "{{.RepoDigests}}" docker.io/vaultwarden/web-vault:v2025.1.0 +# [docker.io/vaultwarden/web-vault@sha256:72d636334b4ad6fe9ba1d12e0cda562cd31772cf28772f6b2fe4121a537b72a8] # # - Conversely, to get the tag name from the digest: -# $ docker image inspect --format "{{.RepoTags}}" docker.io/vaultwarden/web-vault@sha256:cb6b2095a4afc1d9d243a33f6d09211f40e3d82c7ae829fd025df5ff175a4918 -# [docker.io/vaultwarden/web-vault:v2025.1.1] +# $ docker image inspect --format "{{.RepoTags}}" docker.io/vaultwarden/web-vault@sha256:72d636334b4ad6fe9ba1d12e0cda562cd31772cf28772f6b2fe4121a537b72a8 +# [docker.io/vaultwarden/web-vault:v2025.1.0] # -FROM --platform=linux/amd64 docker.io/vaultwarden/web-vault@sha256:cb6b2095a4afc1d9d243a33f6d09211f40e3d82c7ae829fd025df5ff175a4918 AS vault +FROM --platform=linux/amd64 docker.io/vaultwarden/web-vault@sha256:72d636334b4ad6fe9ba1d12e0cda562cd31772cf28772f6b2fe4121a537b72a8 AS vault ########################## ALPINE BUILD IMAGES ########################## ## NOTE: The Alpine Base Images do not support other platforms then linux/amd64 diff --git a/docker/Dockerfile.debian b/docker/Dockerfile.debian index cf45a36d..ff1ff453 100644 --- a/docker/Dockerfile.debian +++ b/docker/Dockerfile.debian @@ -19,15 +19,15 @@ # - From https://hub.docker.com/r/vaultwarden/web-vault/tags, # click the tag name to view the digest of the image it currently points to. # - From the command line: -# $ docker pull docker.io/vaultwarden/web-vault:v2025.1.1 -# $ docker image inspect --format "{{.RepoDigests}}" docker.io/vaultwarden/web-vault:v2025.1.1 -# [docker.io/vaultwarden/web-vault@sha256:cb6b2095a4afc1d9d243a33f6d09211f40e3d82c7ae829fd025df5ff175a4918] +# $ docker pull docker.io/vaultwarden/web-vault:v2025.1.0 +# $ docker image inspect --format "{{.RepoDigests}}" docker.io/vaultwarden/web-vault:v2025.1.0 +# [docker.io/vaultwarden/web-vault@sha256:72d636334b4ad6fe9ba1d12e0cda562cd31772cf28772f6b2fe4121a537b72a8] # # - Conversely, to get the tag name from the digest: -# $ docker image inspect --format "{{.RepoTags}}" docker.io/vaultwarden/web-vault@sha256:cb6b2095a4afc1d9d243a33f6d09211f40e3d82c7ae829fd025df5ff175a4918 -# [docker.io/vaultwarden/web-vault:v2025.1.1] +# $ docker image inspect --format "{{.RepoTags}}" docker.io/vaultwarden/web-vault@sha256:72d636334b4ad6fe9ba1d12e0cda562cd31772cf28772f6b2fe4121a537b72a8 +# [docker.io/vaultwarden/web-vault:v2025.1.0] # -FROM --platform=linux/amd64 docker.io/vaultwarden/web-vault@sha256:cb6b2095a4afc1d9d243a33f6d09211f40e3d82c7ae829fd025df5ff175a4918 AS vault +FROM --platform=linux/amd64 docker.io/vaultwarden/web-vault@sha256:72d636334b4ad6fe9ba1d12e0cda562cd31772cf28772f6b2fe4121a537b72a8 AS vault ########################## Cross Compile Docker Helper Scripts ########################## ## We use the linux/amd64 no matter which Build Platform, since these are all bash scripts diff --git a/migrations/mysql/2025-01-09-172300_add_manage/down.sql b/migrations/mysql/2025-01-09-172300_add_manage/down.sql deleted file mode 100644 index e69de29b..00000000 diff --git a/migrations/mysql/2025-01-09-172300_add_manage/up.sql b/migrations/mysql/2025-01-09-172300_add_manage/up.sql deleted file mode 100644 index e234cc6e..00000000 --- a/migrations/mysql/2025-01-09-172300_add_manage/up.sql +++ /dev/null @@ -1,5 +0,0 @@ -ALTER TABLE users_collections -ADD COLUMN manage BOOLEAN NOT NULL DEFAULT FALSE; - -ALTER TABLE collections_groups -ADD COLUMN manage BOOLEAN NOT NULL DEFAULT FALSE; diff --git a/migrations/postgresql/2025-01-09-172300_add_manage/down.sql b/migrations/postgresql/2025-01-09-172300_add_manage/down.sql deleted file mode 100644 index e69de29b..00000000 diff --git a/migrations/postgresql/2025-01-09-172300_add_manage/up.sql b/migrations/postgresql/2025-01-09-172300_add_manage/up.sql deleted file mode 100644 index e234cc6e..00000000 --- a/migrations/postgresql/2025-01-09-172300_add_manage/up.sql +++ /dev/null @@ -1,5 +0,0 @@ -ALTER TABLE users_collections -ADD COLUMN manage BOOLEAN NOT NULL DEFAULT FALSE; - -ALTER TABLE collections_groups -ADD COLUMN manage BOOLEAN NOT NULL DEFAULT FALSE; diff --git a/migrations/sqlite/2025-01-09-172300_add_manage/down.sql b/migrations/sqlite/2025-01-09-172300_add_manage/down.sql deleted file mode 100644 index e69de29b..00000000 diff --git a/migrations/sqlite/2025-01-09-172300_add_manage/up.sql b/migrations/sqlite/2025-01-09-172300_add_manage/up.sql deleted file mode 100644 index 4b4b07a5..00000000 --- a/migrations/sqlite/2025-01-09-172300_add_manage/up.sql +++ /dev/null @@ -1,5 +0,0 @@ -ALTER TABLE users_collections -ADD COLUMN manage BOOLEAN NOT NULL DEFAULT 0; -- FALSE - -ALTER TABLE collections_groups -ADD COLUMN manage BOOLEAN NOT NULL DEFAULT 0; -- FALSE diff --git a/src/api/admin.rs b/src/api/admin.rs index c653f4c8..f3fb0b1f 100644 --- a/src/api/admin.rs +++ b/src/api/admin.rs @@ -99,7 +99,6 @@ const DT_FMT: &str = "%Y-%m-%d %H:%M:%S %Z"; const BASE_TEMPLATE: &str = "admin/base"; const ACTING_ADMIN_USER: &str = "vaultwarden-admin-00000-000000000000"; -pub const FAKE_ADMIN_UUID: &str = "00000000-0000-0000-0000-000000000000"; fn admin_path() -> String { format!("{}{}", CONFIG.domain_path(), ADMIN_PATH) @@ -300,9 +299,7 @@ async fn invite_user(data: Json, _token: AdminToken, mut conn: DbCon async fn _generate_invite(user: &User, conn: &mut DbConn) -> EmptyResult { if CONFIG.mail_enabled() { - let org_id: OrganizationId = FAKE_ADMIN_UUID.to_string().into(); - let member_id: MembershipId = FAKE_ADMIN_UUID.to_string().into(); - mail::send_invite(user, org_id, member_id, &CONFIG.invitation_org_name(), None).await + mail::send_invite(user, None, None, &CONFIG.invitation_org_name(), None).await } else { let invitation = Invitation::new(&user.email); invitation.save(conn).await @@ -478,9 +475,7 @@ async fn resend_user_invite(user_id: UserId, _token: AdminToken, mut conn: DbCon } if CONFIG.mail_enabled() { - let org_id: OrganizationId = FAKE_ADMIN_UUID.to_string().into(); - let member_id: MembershipId = FAKE_ADMIN_UUID.to_string().into(); - mail::send_invite(&user, org_id, member_id, &CONFIG.invitation_org_name(), None).await + mail::send_invite(&user, None, None, &CONFIG.invitation_org_name(), None).await } else { Ok(()) } diff --git a/src/api/core/accounts.rs b/src/api/core/accounts.rs index a959064b..fef3323c 100644 --- a/src/api/core/accounts.rs +++ b/src/api/core/accounts.rs @@ -30,7 +30,6 @@ pub fn routes() -> Vec { profile, put_profile, post_profile, - put_avatar, get_public_keys, post_keys, post_password, @@ -44,8 +43,9 @@ pub fn routes() -> Vec { post_verify_email_token, post_delete_recover, post_delete_recover_token, - post_delete_account, + post_device_token, delete_account, + post_delete_account, revision_date, password_hint, prelogin, @@ -53,9 +53,7 @@ pub fn routes() -> Vec { api_key, rotate_api_key, get_known_device, - get_all_devices, - get_device, - post_device_token, + put_avatar, put_device_token, put_clear_device_token, post_clear_device_token, @@ -1159,26 +1157,6 @@ impl<'r> FromRequest<'r> for KnownDevice { } } -#[get("/devices")] -async fn get_all_devices(headers: Headers, mut conn: DbConn) -> JsonResult { - let devices = Device::find_with_auth_request_by_user(&headers.user.uuid, &mut conn).await; - let devices = devices.iter().map(|device| device.to_json()).collect::>(); - - Ok(Json(json!({ - "data": devices, - "continuationToken": null, - "object": "list" - }))) -} - -#[get("/devices/identifier/")] -async fn get_device(device_id: DeviceId, headers: Headers, mut conn: DbConn) -> JsonResult { - let Some(device) = Device::find_by_uuid_and_user(&device_id, &headers.user.uuid, &mut conn).await else { - err!("No device found"); - }; - Ok(Json(device.to_json())) -} - #[derive(Deserialize)] #[serde(rename_all = "camelCase")] struct PushToken { diff --git a/src/api/core/organizations.rs b/src/api/core/organizations.rs index 3b694bd2..8486d669 100644 --- a/src/api/core/organizations.rs +++ b/src/api/core/organizations.rs @@ -4,7 +4,6 @@ use rocket::Route; use serde_json::Value; use std::collections::{HashMap, HashSet}; -use crate::api::admin::FAKE_ADMIN_UUID; use crate::{ api::{ core::{log_event, two_factor, CipherSyncData, CipherSyncType}, @@ -143,7 +142,6 @@ struct NewCollectionGroupData { hide_passwords: bool, id: GroupId, read_only: bool, - manage: bool, } #[derive(Deserialize)] @@ -152,7 +150,6 @@ struct NewCollectionMemberData { hide_passwords: bool, id: MembershipId, read_only: bool, - manage: bool, } #[derive(Deserialize)] @@ -378,13 +375,18 @@ async fn get_org_collections_details( || (CONFIG.org_groups_enabled() && GroupUser::has_access_to_collection_by_member(&col.uuid, &member.uuid, &mut conn).await); + // Not assigned collections should not be returned + if !assigned { + continue; + } + // get the users assigned directly to the given collection let users: Vec = col_users .iter() - .filter(|collection_member| collection_member.collection_uuid == col.uuid) - .map(|collection_member| { - collection_member.to_json_details_for_member( - *membership_type.get(&collection_member.membership_uuid).unwrap_or(&(MembershipType::User as i32)), + .filter(|collection_user| collection_user.collection_uuid == col.uuid) + .map(|collection_user| { + collection_user.to_json_details_for_user( + *membership_type.get(&collection_user.membership_uuid).unwrap_or(&(MembershipType::User as i32)), ) }) .collect(); @@ -448,7 +450,7 @@ async fn post_organization_collections( .await; for group in data.groups { - CollectionGroup::new(collection.uuid.clone(), group.id, group.read_only, group.hide_passwords, group.manage) + CollectionGroup::new(collection.uuid.clone(), group.id, group.read_only, group.hide_passwords) .save(&mut conn) .await?; } @@ -462,19 +464,12 @@ async fn post_organization_collections( continue; } - CollectionUser::save( - &member.user_uuid, - &collection.uuid, - user.read_only, - user.hide_passwords, - user.manage, - &mut conn, - ) - .await?; + CollectionUser::save(&member.user_uuid, &collection.uuid, user.read_only, user.hide_passwords, &mut conn) + .await?; } if headers.membership.atype == MembershipType::Manager && !headers.membership.access_all { - CollectionUser::save(&headers.membership.user_uuid, &collection.uuid, false, false, false, &mut conn).await?; + CollectionUser::save(&headers.membership.user_uuid, &collection.uuid, false, false, &mut conn).await?; } Ok(Json(collection.to_json())) @@ -531,9 +526,7 @@ async fn post_organization_collection_update( CollectionGroup::delete_all_by_collection(&col_id, &mut conn).await?; for group in data.groups { - CollectionGroup::new(col_id.clone(), group.id, group.read_only, group.hide_passwords, group.manage) - .save(&mut conn) - .await?; + CollectionGroup::new(col_id.clone(), group.id, group.read_only, group.hide_passwords).save(&mut conn).await?; } CollectionUser::delete_all_by_collection(&col_id, &mut conn).await?; @@ -547,8 +540,7 @@ async fn post_organization_collection_update( continue; } - CollectionUser::save(&member.user_uuid, &col_id, user.read_only, user.hide_passwords, user.manage, &mut conn) - .await?; + CollectionUser::save(&member.user_uuid, &col_id, user.read_only, user.hide_passwords, &mut conn).await?; } Ok(Json(collection.to_json_details(&headers.user.uuid, None, &mut conn).await)) @@ -706,10 +698,10 @@ async fn get_org_collection_detail( CollectionUser::find_by_collection_swap_user_uuid_with_member_uuid(&collection.uuid, &mut conn) .await .iter() - .map(|collection_member| { - collection_member.to_json_details_for_member( + .map(|collection_user| { + collection_user.to_json_details_for_user( *membership_type - .get(&collection_member.membership_uuid) + .get(&collection_user.membership_uuid) .unwrap_or(&(MembershipType::User as i32)), ) }) @@ -779,7 +771,7 @@ async fn put_collection_users( continue; } - CollectionUser::save(&user.user_uuid, &col_id, d.read_only, d.hide_passwords, d.manage, &mut conn).await?; + CollectionUser::save(&user.user_uuid, &col_id, d.read_only, d.hide_passwords, &mut conn).await?; } Ok(()) @@ -899,7 +891,6 @@ struct CollectionData { id: CollectionId, read_only: bool, hide_passwords: bool, - manage: bool, } #[derive(Deserialize)] @@ -908,7 +899,6 @@ struct MembershipData { id: MembershipId, read_only: bool, hide_passwords: bool, - manage: bool, } #[derive(Deserialize)] @@ -1007,8 +997,8 @@ async fn send_invite( if let Err(e) = mail::send_invite( &user, - org_id.clone(), - new_member.uuid.clone(), + Some(org_id.clone()), + Some(new_member.uuid.clone()), &org_name, Some(headers.user.email.clone()), ) @@ -1047,7 +1037,6 @@ async fn send_invite( &collection.uuid, col.read_only, col.hide_passwords, - col.manage, &mut conn, ) .await?; @@ -1135,7 +1124,14 @@ async fn _reinvite_member( }; if CONFIG.mail_enabled() { - mail::send_invite(&user, org_id.clone(), member.uuid, &org_name, Some(invited_by_email.to_string())).await?; + mail::send_invite( + &user, + Some(org_id.clone()), + Some(member.uuid), + &org_name, + Some(invited_by_email.to_string()), + ) + .await?; } else if user.password_hash.is_empty() { let invitation = Invitation::new(&user.email); invitation.save(conn).await?; @@ -1161,81 +1157,79 @@ async fn accept_invite( org_id: OrganizationId, member_id: MembershipId, data: Json, - headers: Headers, mut conn: DbConn, ) -> EmptyResult { // The web-vault passes org_id and member_id in the URL, but we are just reading them from the JWT instead let data: AcceptData = data.into_inner(); let claims = decode_invite(&data.token)?; - // Don't allow other users from accepting an invitation. - if !claims.email.eq(&headers.user.email) { - err!("Invitation was issued to a different account", "Claim does not match user_id") - } - // If a claim does not have a member_id or it does not match the one in from the URI, something is wrong. - if !claims.member_id.eq(&member_id) { - err!("Error accepting the invitation", "Claim does not match the member_id") + match &claims.member_id { + Some(ou_id) if ou_id.eq(&member_id) => {} + _ => err!("Error accepting the invitation", "Claim does not match the member_id"), } - let member = &claims.member_id; - let org = &claims.org_id; + match User::find_by_mail(&claims.email, &mut conn).await { + Some(user) => { + Invitation::take(&claims.email, &mut conn).await; - Invitation::take(&claims.email, &mut conn).await; + if let (Some(member), Some(org)) = (&claims.member_id, &claims.org_id) { + let Some(mut member) = Membership::find_by_uuid_and_org(member, org, &mut conn).await else { + err!("Error accepting the invitation") + }; - // skip invitation logic when we were invited via the /admin panel - if **member != FAKE_ADMIN_UUID { - let Some(mut member) = Membership::find_by_uuid_and_org(member, org, &mut conn).await else { - err!("Error accepting the invitation") - }; + if member.status != MembershipStatus::Invited as i32 { + err!("User already accepted the invitation") + } - if member.status != MembershipStatus::Invited as i32 { - err!("User already accepted the invitation") - } + let master_password_required = OrgPolicy::org_is_reset_password_auto_enroll(org, &mut conn).await; + if data.reset_password_key.is_none() && master_password_required { + err!("Reset password key is required, but not provided."); + } - let master_password_required = OrgPolicy::org_is_reset_password_auto_enroll(org, &mut conn).await; - if data.reset_password_key.is_none() && master_password_required { - err!("Reset password key is required, but not provided."); - } - - // This check is also done at accept_invite, _confirm_invite, _activate_member, edit_member, admin::update_membership_type - // It returns different error messages per function. - if member.atype < MembershipType::Admin { - match OrgPolicy::is_user_allowed(&member.user_uuid, &org_id, false, &mut conn).await { - Ok(_) => {} - Err(OrgPolicyErr::TwoFactorMissing) => { - if CONFIG.email_2fa_auto_fallback() { - two_factor::email::activate_email_2fa(&headers.user, &mut conn).await?; - } else { - err!("You cannot join this organization until you enable two-step login on your user account"); + // This check is also done at accept_invite, _confirm_invite, _activate_member, edit_member, admin::update_membership_type + // It returns different error messages per function. + if member.atype < MembershipType::Admin { + match OrgPolicy::is_user_allowed(&member.user_uuid, &org_id, false, &mut conn).await { + Ok(_) => {} + Err(OrgPolicyErr::TwoFactorMissing) => { + if CONFIG.email_2fa_auto_fallback() { + two_factor::email::activate_email_2fa(&user, &mut conn).await?; + } else { + err!("You cannot join this organization until you enable two-step login on your user account"); + } + } + Err(OrgPolicyErr::SingleOrgEnforced) => { + err!("You cannot join this organization because you are a member of an organization which forbids it"); + } } } - Err(OrgPolicyErr::SingleOrgEnforced) => { - err!("You cannot join this organization because you are a member of an organization which forbids it"); + + member.status = MembershipStatus::Accepted as i32; + + if master_password_required { + member.reset_password_key = data.reset_password_key; } + + member.save(&mut conn).await?; } } - - member.status = MembershipStatus::Accepted as i32; - - if master_password_required { - member.reset_password_key = data.reset_password_key; - } - - member.save(&mut conn).await?; + None => err!("Invited user not found"), } if CONFIG.mail_enabled() { - if let Some(invited_by_email) = &claims.invited_by_email { - let org_name = match Organization::find_by_uuid(&claims.org_id, &mut conn).await { + let mut org_name = CONFIG.invitation_org_name(); + if let Some(org_id) = &claims.org_id { + org_name = match Organization::find_by_uuid(org_id, &mut conn).await { Some(org) => org.name, None => err!("Organization not found."), }; + }; + if let Some(invited_by_email) = &claims.invited_by_email { // User was invited to an organization, so they must be confirmed manually after acceptance mail::send_invite_accepted(&claims.email, invited_by_email, &org_name).await?; } else { // User was invited from /admin, so they are automatically confirmed - let org_name = CONFIG.invitation_org_name(); mail::send_invite_confirmed(&claims.email, &org_name).await?; } } @@ -1540,7 +1534,6 @@ async fn edit_member( &collection.uuid, col.read_only, col.hide_passwords, - col.manage, &mut conn, ) .await?; @@ -1858,15 +1851,21 @@ async fn list_policies(org_id: OrganizationId, _headers: AdminHeaders, mut conn: #[get("/organizations//policies/token?")] async fn list_policies_token(org_id: OrganizationId, token: &str, mut conn: DbConn) -> JsonResult { - let invite = decode_invite(token)?; - - if invite.org_id != org_id { - err!("Token doesn't match request organization"); + // web-vault 2024.6.2 seems to send these values and cause logs to output errors + // Catch this and prevent errors in the logs + // TODO: CleanUp after 2024.6.x is not used anymore. + if org_id.as_ref() == "undefined" && token == "undefined" { + return Ok(Json(json!({}))); } - // exit early when we have been invited via /admin panel - if org_id.as_ref() == FAKE_ADMIN_UUID { - return Ok(Json(json!({}))); + let invite = decode_invite(token)?; + + let Some(invite_org_id) = invite.org_id else { + err!("Invalid token") + }; + + if invite_org_id != org_id { + err!("Token doesn't match request organization"); } // TODO: We receive the invite token as ?token=<>, validate it contains the org id @@ -2183,8 +2182,8 @@ async fn import(org_id: OrganizationId, data: Json, headers: Head mail::send_invite( &user, - org_id.clone(), - new_member.uuid.clone(), + Some(org_id.clone()), + Some(new_member.uuid.clone()), &org_name, Some(headers.user.email.clone()), ) @@ -2527,12 +2526,11 @@ struct SelectedCollection { id: CollectionId, read_only: bool, hide_passwords: bool, - manage: bool, } impl SelectedCollection { pub fn to_collection_group(&self, groups_uuid: GroupId) -> CollectionGroup { - CollectionGroup::new(self.id.clone(), groups_uuid, self.read_only, self.hide_passwords, self.manage) + CollectionGroup::new(self.id.clone(), groups_uuid, self.read_only, self.hide_passwords) } } diff --git a/src/api/core/public.rs b/src/api/core/public.rs index 9cdd594f..cafbf97b 100644 --- a/src/api/core/public.rs +++ b/src/api/core/public.rs @@ -119,8 +119,14 @@ async fn ldap_import(data: Json, token: PublicToken, mut conn: Db None => err!("Error looking up organization"), }; - if let Err(e) = - mail::send_invite(&user, org_id.clone(), new_member.uuid.clone(), &org_name, Some(org_email)).await + if let Err(e) = mail::send_invite( + &user, + Some(org_id.clone()), + Some(new_member.uuid.clone()), + &org_name, + Some(org_email), + ) + .await { // Upon error delete the user, invite and org member records when needed if user_created { diff --git a/src/api/icons.rs b/src/api/icons.rs index fc4e0ccf..921d48b9 100644 --- a/src/api/icons.rs +++ b/src/api/icons.rs @@ -291,7 +291,9 @@ fn get_favicons_node(dom: Tokenizer, FaviconEmitter>, icons: &m TAG_HEAD if token.closing => { break; } - _ => {} + _ => { + continue; + } } } diff --git a/src/api/notifications.rs b/src/api/notifications.rs index de97be6f..a8083a9f 100644 --- a/src/api/notifications.rs +++ b/src/api/notifications.rs @@ -157,6 +157,7 @@ fn websockets_hub<'r>( if serde_json::from_str(msg).ok() == Some(INITIAL_MESSAGE) { yield Message::binary(INITIAL_RESPONSE); + continue; } } @@ -224,6 +225,7 @@ fn anonymous_websockets_hub<'r>(ws: WebSocket, token: String, ip: ClientIp) -> R if serde_json::from_str(msg).ok() == Some(INITIAL_MESSAGE) { yield Message::binary(INITIAL_RESPONSE); + continue; } } diff --git a/src/auth.rs b/src/auth.rs index 80a54f61..d5aea41e 100644 --- a/src/auth.rs +++ b/src/auth.rs @@ -272,16 +272,16 @@ pub struct InviteJwtClaims { pub sub: UserId, pub email: String, - pub org_id: OrganizationId, - pub member_id: MembershipId, + pub org_id: Option, + pub member_id: Option, pub invited_by_email: Option, } pub fn generate_invite_claims( user_id: UserId, email: String, - org_id: OrganizationId, - member_id: MembershipId, + org_id: Option, + member_id: Option, invited_by_email: Option, ) -> InviteJwtClaims { let time_now = Utc::now(); diff --git a/src/db/models/auth_request.rs b/src/db/models/auth_request.rs index d8ca3fac..dd4f098a 100644 --- a/src/db/models/auth_request.rs +++ b/src/db/models/auth_request.rs @@ -1,9 +1,8 @@ use super::{DeviceId, OrganizationId, UserId}; -use crate::{crypto::ct_eq, util::format_date}; +use crate::crypto::ct_eq; use chrono::{NaiveDateTime, Utc}; use derive_more::{AsRef, Deref, Display, From}; use macros::UuidFromParam; -use serde_json::Value; db_object! { #[derive(Debug, Identifiable, Queryable, Insertable, AsChangeset, Deserialize, Serialize)] @@ -65,13 +64,6 @@ impl AuthRequest { authentication_date: None, } } - - pub fn to_json_for_pending_device(&self) -> Value { - json!({ - "id": self.uuid, - "creationDate": format_date(&self.creation_date), - }) - } } use crate::db::DbConn; @@ -141,20 +133,6 @@ impl AuthRequest { }} } - pub async fn find_by_user_and_requested_device( - user_uuid: &UserId, - device_uuid: &DeviceId, - conn: &mut DbConn, - ) -> Option { - db_run! {conn: { - auth_requests::table - .filter(auth_requests::user_uuid.eq(user_uuid)) - .filter(auth_requests::request_device_identifier.eq(device_uuid)) - .order_by(auth_requests::creation_date.desc()) - .first::(conn).ok().from_db() - }} - } - pub async fn find_created_before(dt: &NaiveDateTime, conn: &mut DbConn) -> Vec { db_run! {conn: { auth_requests::table diff --git a/src/db/models/cipher.rs b/src/db/models/cipher.rs index c751491e..3b8d2384 100644 --- a/src/db/models/cipher.rs +++ b/src/db/models/cipher.rs @@ -158,16 +158,16 @@ impl Cipher { // We don't need these values at all for Organizational syncs // Skip any other database calls if this is the case and just return false. - let (read_only, hide_passwords, _) = if sync_type == CipherSyncType::User { + let (read_only, hide_passwords) = if sync_type == CipherSyncType::User { match self.get_access_restrictions(user_uuid, cipher_sync_data, conn).await { - Some((ro, hp, mn)) => (ro, hp, mn), + Some((ro, hp)) => (ro, hp), None => { error!("Cipher ownership assertion failure"); - (true, true, false) + (true, true) } } } else { - (false, false, false) + (false, false) }; let fields_json: Vec<_> = self @@ -567,14 +567,14 @@ impl Cipher { /// Returns the user's access restrictions to this cipher. A return value /// of None means that this cipher does not belong to the user, and is /// not in any collection the user has access to. Otherwise, the user has - /// access to this cipher, and Some(read_only, hide_passwords, manage) represents + /// access to this cipher, and Some(read_only, hide_passwords) represents /// the access restrictions. pub async fn get_access_restrictions( &self, user_uuid: &UserId, cipher_sync_data: Option<&CipherSyncData>, conn: &mut DbConn, - ) -> Option<(bool, bool, bool)> { + ) -> Option<(bool, bool)> { // Check whether this cipher is directly owned by the user, or is in // a collection that the user has full access to. If so, there are no // access restrictions. @@ -582,21 +582,21 @@ impl Cipher { || self.is_in_full_access_org(user_uuid, cipher_sync_data, conn).await || self.is_in_full_access_group(user_uuid, cipher_sync_data, conn).await { - return Some((false, false, true)); + return Some((false, false)); } let rows = if let Some(cipher_sync_data) = cipher_sync_data { - let mut rows: Vec<(bool, bool, bool)> = Vec::new(); + let mut rows: Vec<(bool, bool)> = Vec::new(); if let Some(collections) = cipher_sync_data.cipher_collections.get(&self.uuid) { for collection in collections { //User permissions - if let Some(cu) = cipher_sync_data.user_collections.get(collection) { - rows.push((cu.read_only, cu.hide_passwords, cu.manage)); + if let Some(uc) = cipher_sync_data.user_collections.get(collection) { + rows.push((uc.read_only, uc.hide_passwords)); } //Group permissions if let Some(cg) = cipher_sync_data.user_collections_groups.get(collection) { - rows.push((cg.read_only, cg.hide_passwords, cg.manage)); + rows.push((cg.read_only, cg.hide_passwords)); } } } @@ -623,21 +623,15 @@ impl Cipher { // booleans and this behavior isn't portable anyway. let mut read_only = true; let mut hide_passwords = true; - let mut manage = false; - for (ro, hp, mn) in rows.iter() { + for (ro, hp) in rows.iter() { read_only &= ro; hide_passwords &= hp; - manage &= mn; } - Some((read_only, hide_passwords, manage)) + Some((read_only, hide_passwords)) } - async fn get_user_collections_access_flags( - &self, - user_uuid: &UserId, - conn: &mut DbConn, - ) -> Vec<(bool, 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. @@ -648,17 +642,13 @@ impl Cipher { .inner_join(users_collections::table.on( ciphers_collections::collection_uuid.eq(users_collections::collection_uuid) .and(users_collections::user_uuid.eq(user_uuid)))) - .select((users_collections::read_only, users_collections::hide_passwords, users_collections::manage)) - .load::<(bool, bool, bool)>(conn) + .select((users_collections::read_only, users_collections::hide_passwords)) + .load::<(bool, bool)>(conn) .expect("Error getting user access restrictions") }} } - async fn get_group_collections_access_flags( - &self, - user_uuid: &UserId, - conn: &mut DbConn, - ) -> Vec<(bool, 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(); } @@ -678,15 +668,15 @@ impl Cipher { users_organizations::uuid.eq(groups_users::users_organizations_uuid) )) .filter(users_organizations::user_uuid.eq(user_uuid)) - .select((collections_groups::read_only, collections_groups::hide_passwords, collections_groups::manage)) - .load::<(bool, bool, bool)>(conn) + .select((collections_groups::read_only, collections_groups::hide_passwords)) + .load::<(bool, bool)>(conn) .expect("Error getting group access restrictions") }} } 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, manage)) => !read_only || manage, + Some((read_only, _hide_passwords)) => !read_only, None => false, } } diff --git a/src/db/models/collection.rs b/src/db/models/collection.rs index 2286ee04..2302b493 100644 --- a/src/db/models/collection.rs +++ b/src/db/models/collection.rs @@ -27,7 +27,6 @@ db_object! { pub collection_uuid: CollectionId, pub read_only: bool, pub hide_passwords: bool, - pub manage: bool, } #[derive(Identifiable, Queryable, Insertable)] @@ -84,26 +83,18 @@ impl Collection { cipher_sync_data: Option<&crate::api::core::CipherSyncData>, conn: &mut DbConn, ) -> Value { - let (read_only, hide_passwords, manage) = if let Some(cipher_sync_data) = cipher_sync_data { + let (read_only, hide_passwords, can_manage) = if let Some(cipher_sync_data) = cipher_sync_data { match cipher_sync_data.members.get(&self.org_uuid) { - // Only for Manager types Bitwarden returns true for the manage option - // Owners and Admins always have true. Users are not able to have full access + // Only for Manager types Bitwarden returns true for the can_manage option + // Owners and Admins always have true 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 = m.atype == MembershipType::Manager; - if let Some(cu) = cipher_sync_data.user_collections.get(&self.uuid) { - ( - cu.read_only, - cu.hide_passwords, - cu.manage || (is_manager && !cu.read_only && !cu.hide_passwords), - ) + 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) { - ( - cg.read_only, - cg.hide_passwords, - cg.manage || (is_manager && !cg.read_only && !cg.hide_passwords), - ) + (cg.read_only, cg.hide_passwords, is_manager && !cg.read_only && !cg.hide_passwords) } else { (false, false, false) } @@ -113,14 +104,17 @@ impl Collection { } else { 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(_) if self.is_manageable_by_user(user_uuid, conn).await => (false, false, true), 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) } - _ => (true, true, false), + _ => ( + !self.is_writable_by_user(user_uuid, conn).await, + self.hide_passwords_for_user(user_uuid, conn).await, + false, + ), } }; @@ -128,7 +122,7 @@ impl Collection { json_object["object"] = json!("collectionDetails"); json_object["readOnly"] = json!(read_only); json_object["hidePasswords"] = json!(hide_passwords); - json_object["manage"] = json!(manage); + json_object["manage"] = json!(can_manage); json_object } @@ -513,52 +507,6 @@ impl Collection { .unwrap_or(0) != 0 }} } - - pub async fn is_manageable_by_user(&self, user_uuid: &UserId, conn: &mut DbConn) -> bool { - let user_uuid = user_uuid.to_string(); - db_run! { conn: { - collections::table - .left_join(users_collections::table.on( - users_collections::collection_uuid.eq(collections::uuid).and( - users_collections::user_uuid.eq(user_uuid.clone()) - ) - )) - .left_join(users_organizations::table.on( - collections::org_uuid.eq(users_organizations::org_uuid).and( - users_organizations::user_uuid.eq(user_uuid) - ) - )) - .left_join(groups_users::table.on( - groups_users::users_organizations_uuid.eq(users_organizations::uuid) - )) - .left_join(groups::table.on( - groups::uuid.eq(groups_users::groups_uuid) - )) - .left_join(collections_groups::table.on( - collections_groups::groups_uuid.eq(groups_users::groups_uuid).and( - collections_groups::collections_uuid.eq(collections::uuid) - ) - )) - .filter(collections::uuid.eq(&self.uuid)) - .filter( - users_collections::collection_uuid.eq(&self.uuid).and(users_collections::manage.eq(true)).or(// Directly accessed collection - users_organizations::access_all.eq(true).or( // access_all in Organization - 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 - groups_users::users_organizations_uuid.eq(users_organizations::uuid).and( - collections_groups::collections_uuid.is_not_null().and( - collections_groups::manage.eq(true)) - ) - ) - ) - .count() - .first::(conn) - .ok() - .unwrap_or(0) != 0 - }} - } } /// Database methods @@ -589,7 +537,7 @@ impl CollectionUser { .inner_join(collections::table.on(collections::uuid.eq(users_collections::collection_uuid))) .filter(collections::org_uuid.eq(org_uuid)) .inner_join(users_organizations::table.on(users_organizations::user_uuid.eq(users_collections::user_uuid))) - .select((users_organizations::uuid, users_collections::collection_uuid, users_collections::read_only, users_collections::hide_passwords, users_collections::manage)) + .select((users_organizations::uuid, users_collections::collection_uuid, users_collections::read_only, users_collections::hide_passwords)) .load::(conn) .expect("Error loading users_collections") .from_db() @@ -602,7 +550,6 @@ impl CollectionUser { collection_uuid: &CollectionId, read_only: bool, hide_passwords: bool, - manage: bool, conn: &mut DbConn, ) -> EmptyResult { User::update_uuid_revision(user_uuid, conn).await; @@ -615,7 +562,6 @@ impl CollectionUser { users_collections::collection_uuid.eq(collection_uuid), users_collections::read_only.eq(read_only), users_collections::hide_passwords.eq(hide_passwords), - users_collections::manage.eq(manage), )) .execute(conn) { @@ -630,7 +576,6 @@ impl CollectionUser { users_collections::collection_uuid.eq(collection_uuid), users_collections::read_only.eq(read_only), users_collections::hide_passwords.eq(hide_passwords), - users_collections::manage.eq(manage), )) .execute(conn) .map_res("Error adding user to collection") @@ -645,14 +590,12 @@ impl CollectionUser { users_collections::collection_uuid.eq(collection_uuid), users_collections::read_only.eq(read_only), users_collections::hide_passwords.eq(hide_passwords), - users_collections::manage.eq(manage), )) .on_conflict((users_collections::user_uuid, users_collections::collection_uuid)) .do_update() .set(( users_collections::read_only.eq(read_only), users_collections::hide_passwords.eq(hide_passwords), - users_collections::manage.eq(manage), )) .execute(conn) .map_res("Error adding user to collection") @@ -693,7 +636,7 @@ impl CollectionUser { 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))) - .select((users_organizations::uuid, users_collections::collection_uuid, users_collections::read_only, users_collections::hide_passwords, users_collections::manage)) + .select((users_organizations::uuid, users_collections::collection_uuid, users_collections::read_only, users_collections::hide_passwords)) .load::(conn) .expect("Error loading users_collections") .from_db() @@ -844,17 +787,15 @@ pub struct CollectionMembership { pub collection_uuid: CollectionId, pub read_only: bool, pub hide_passwords: bool, - pub manage: bool, } impl CollectionMembership { - pub fn to_json_details_for_member(&self, membership_type: i32) -> Value { + 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 - || self.manage || (membership_type == MembershipType::Manager && !self.read_only && !self.hide_passwords), @@ -869,7 +810,6 @@ impl From for CollectionMembership { collection_uuid: c.collection_uuid, read_only: c.read_only, hide_passwords: c.hide_passwords, - manage: c.manage, } } } diff --git a/src/db/models/device.rs b/src/db/models/device.rs index 3b4b501f..55af538e 100644 --- a/src/db/models/device.rs +++ b/src/db/models/device.rs @@ -2,10 +2,9 @@ use chrono::{NaiveDateTime, Utc}; use data_encoding::{BASE64, BASE64URL}; use derive_more::{Display, From}; -use serde_json::Value; -use super::{AuthRequest, UserId}; -use crate::{crypto, util::format_date}; +use super::UserId; +use crate::crypto; use macros::IdFromParam; db_object! { @@ -26,6 +25,7 @@ db_object! { pub push_token: Option, pub refresh_token: String, + pub twofactor_remember: Option, } } @@ -51,18 +51,6 @@ impl Device { } } - pub fn to_json(&self) -> Value { - json!({ - "id": self.uuid, - "name": self.name, - "type": self.atype, - "identifier": self.push_uuid, - "creationDate": format_date(&self.created_at), - "isTrusted": false, - "object":"device" - }) - } - pub fn refresh_twofactor_remember(&mut self) -> String { let twofactor_remember = crypto::encode_random_bytes::<180>(BASE64); self.twofactor_remember = Some(twofactor_remember.clone()); @@ -83,36 +71,6 @@ impl Device { } } -pub struct DeviceWithAuthRequest { - pub device: Device, - pub pending_auth_request: Option, -} - -impl DeviceWithAuthRequest { - pub fn to_json(&self) -> Value { - let auth_request = match &self.pending_auth_request { - Some(auth_request) => auth_request.to_json_for_pending_device(), - None => Value::Null, - }; - json!({ - "id": self.device.uuid, - "name": self.device.name, - "type": self.device.atype, - "identifier": self.device.push_uuid, - "creationDate": format_date(&self.device.created_at), - "devicePendingAuthRequest": auth_request, - "isTrusted": false, - "object": "device", - }) - } - - pub fn from(c: Device, a: Option) -> Self { - Self { - device: c, - pending_auth_request: a, - } - } -} use crate::db::DbConn; use crate::api::EmptyResult; @@ -159,16 +117,6 @@ impl Device { }} } - pub async fn find_with_auth_request_by_user(user_uuid: &UserId, conn: &mut DbConn) -> Vec { - let devices = Self::find_by_user(user_uuid, conn).await; - let mut result = Vec::new(); - for device in devices { - let auth_request = AuthRequest::find_by_user_and_requested_device(user_uuid, &device.uuid, conn).await; - result.push(DeviceWithAuthRequest::from(device, auth_request)); - } - result - } - pub async fn find_by_user(user_uuid: &UserId, conn: &mut DbConn) -> Vec { db_run! { conn: { devices::table diff --git a/src/db/models/group.rs b/src/db/models/group.rs index e85b8c05..5a72418d 100644 --- a/src/db/models/group.rs +++ b/src/db/models/group.rs @@ -29,7 +29,6 @@ db_object! { pub groups_uuid: GroupId, pub read_only: bool, pub hide_passwords: bool, - pub manage: bool, } #[derive(Identifiable, Queryable, Insertable)] @@ -93,7 +92,7 @@ impl Group { "id": entry.collections_uuid, "readOnly": entry.read_only, "hidePasswords": entry.hide_passwords, - "manage": entry.manage, + "manage": !entry.read_only && !entry.hide_passwords, }) }) .collect(); @@ -119,19 +118,12 @@ impl Group { } impl CollectionGroup { - pub fn new( - collections_uuid: CollectionId, - groups_uuid: GroupId, - read_only: bool, - hide_passwords: bool, - manage: bool, - ) -> Self { + pub fn new(collections_uuid: CollectionId, groups_uuid: GroupId, read_only: bool, hide_passwords: bool) -> Self { Self { collections_uuid, groups_uuid, read_only, hide_passwords, - manage, } } @@ -139,12 +131,11 @@ impl CollectionGroup { // 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 - // For backwards compaibility and migration proposes we keep checking read_only and hide_password json!({ "id": self.groups_uuid, "readOnly": self.read_only, "hidePasswords": self.hide_passwords, - "manage": self.manage || (!self.read_only && !self.hide_passwords), + "manage": !self.read_only && !self.hide_passwords, }) } } @@ -328,7 +319,6 @@ impl CollectionGroup { collections_groups::groups_uuid.eq(&self.groups_uuid), collections_groups::read_only.eq(&self.read_only), collections_groups::hide_passwords.eq(&self.hide_passwords), - collections_groups::manage.eq(&self.manage), )) .execute(conn) { @@ -343,7 +333,6 @@ impl CollectionGroup { collections_groups::groups_uuid.eq(&self.groups_uuid), collections_groups::read_only.eq(&self.read_only), collections_groups::hide_passwords.eq(&self.hide_passwords), - collections_groups::manage.eq(&self.manage), )) .execute(conn) .map_res("Error adding group to collection") @@ -358,14 +347,12 @@ impl CollectionGroup { collections_groups::groups_uuid.eq(&self.groups_uuid), collections_groups::read_only.eq(self.read_only), collections_groups::hide_passwords.eq(self.hide_passwords), - collections_groups::manage.eq(self.manage), )) .on_conflict((collections_groups::collections_uuid, collections_groups::groups_uuid)) .do_update() .set(( collections_groups::read_only.eq(self.read_only), collections_groups::hide_passwords.eq(self.hide_passwords), - collections_groups::manage.eq(self.manage), )) .execute(conn) .map_res("Error adding group to collection") diff --git a/src/db/models/organization.rs b/src/db/models/organization.rs index 4f4ccd13..40a74995 100644 --- a/src/db/models/organization.rs +++ b/src/db/models/organization.rs @@ -522,13 +522,13 @@ impl Membership { .await .into_iter() .filter_map(|c| { - let (read_only, hide_passwords, manage) = if self.has_full_access() { + let (read_only, hide_passwords, can_manage) = if self.has_full_access() { (false, false, self.atype >= MembershipType::Manager) } else if let Some(cu) = cu.get(&c.uuid) { ( cu.read_only, cu.hide_passwords, - cu.manage || (self.atype == MembershipType::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 @@ -542,7 +542,7 @@ impl Membership { "id": c.uuid, "readOnly": read_only, "hidePasswords": hide_passwords, - "manage": manage, + "manage": can_manage, })) }) .collect() @@ -611,7 +611,6 @@ impl Membership { "id": self.uuid, "readOnly": col_user.read_only, "hidePasswords": col_user.hide_passwords, - "manage": col_user.manage, }) } @@ -623,12 +622,11 @@ impl Membership { CollectionUser::find_by_organization_and_user_uuid(&self.org_uuid, &self.user_uuid, conn).await; collections .iter() - .map(|cu| { + .map(|c| { json!({ - "id": cu.collection_uuid, - "readOnly": cu.read_only, - "hidePasswords": cu.hide_passwords, - "manage": cu.manage, + "id": c.collection_uuid, + "readOnly": c.read_only, + "hidePasswords": c.hide_passwords, }) }) .collect() diff --git a/src/db/schemas/mysql/schema.rs b/src/db/schemas/mysql/schema.rs index a8d4a76a..f5446ff2 100644 --- a/src/db/schemas/mysql/schema.rs +++ b/src/db/schemas/mysql/schema.rs @@ -226,7 +226,6 @@ table! { collection_uuid -> Text, read_only -> Bool, hide_passwords -> Bool, - manage -> Bool, } } @@ -313,7 +312,6 @@ table! { groups_uuid -> Text, read_only -> Bool, hide_passwords -> Bool, - manage -> Bool, } } diff --git a/src/db/schemas/postgresql/schema.rs b/src/db/schemas/postgresql/schema.rs index 1ee20c6f..07792e1e 100644 --- a/src/db/schemas/postgresql/schema.rs +++ b/src/db/schemas/postgresql/schema.rs @@ -226,7 +226,6 @@ table! { collection_uuid -> Text, read_only -> Bool, hide_passwords -> Bool, - manage -> Bool, } } @@ -313,7 +312,6 @@ table! { groups_uuid -> Text, read_only -> Bool, hide_passwords -> Bool, - manage -> Bool, } } diff --git a/src/db/schemas/sqlite/schema.rs b/src/db/schemas/sqlite/schema.rs index 1ee20c6f..07792e1e 100644 --- a/src/db/schemas/sqlite/schema.rs +++ b/src/db/schemas/sqlite/schema.rs @@ -226,7 +226,6 @@ table! { collection_uuid -> Text, read_only -> Bool, hide_passwords -> Bool, - manage -> Bool, } } @@ -313,7 +312,6 @@ table! { groups_uuid -> Text, read_only -> Bool, hide_passwords -> Bool, - manage -> Bool, } } diff --git a/src/mail.rs b/src/mail.rs index 27cdf5f8..42b5aed4 100644 --- a/src/mail.rs +++ b/src/mail.rs @@ -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: OrganizationId, - member_id: MembershipId, + org_id: Option, + member_id: Option, org_name: &str, invited_by_email: Option, ) -> EmptyResult { @@ -272,14 +272,22 @@ pub async fn send_invite( invited_by_email, ); let invite_token = encode_jwt(&claims); + let org_id = match org_id { + Some(ref org_id) => org_id.as_ref(), + None => "_", + }; + let member_id = match member_id { + Some(ref member_id) => member_id.as_ref(), + None => "_", + }; let mut query = url::Url::parse("https://query.builder").unwrap(); { let mut query_params = query.query_pairs_mut(); query_params .append_pair("email", &user.email) .append_pair("organizationName", org_name) - .append_pair("organizationId", &org_id) - .append_pair("organizationUserId", &member_id) + .append_pair("organizationId", org_id) + .append_pair("organizationUserId", member_id) .append_pair("token", &invite_token); if CONFIG.sso_enabled() && CONFIG.sso_only() {