Compare commits

...
Author SHA1 Message Date
Tom 3714b504f6 Fix Clippy warnings across all targets (#7782) 2026-10-03 13:50:29 +02:00
Tom f9f011d50c Hide the whole change-email section when EMAIL_CHANGE_ALLOWED is false (#7759) 2026-10-03 13:49:53 +02:00
Julian Speckmann cb89088bc3 Add Windows native credential sync to supported feature flags (#7798) 2026-10-03 13:49:44 +02:00
Pier Carlo Cadoppi 4444442075 Add undetermined-cipher-scenario-logic feature flag (#7802)
Introduced in Browser 2026.2.0 via bitwarden/clients#18395. Rewrites the
post-submit add/update login notification triggering logic, deferring
cipher-type inference until the vault is unlocked. Client-side only.
2026-10-03 13:49:37 +02:00
Mathijs van Veluw 061694d0cb Fix cortex-a53 build issues when using xx-cargo (#7774)
When using xx-cargo it uses a different linker then the default one.
This causes some default fixes Debian/Ubuntu add for the Cortex-A53
architectures to not be included.

This commit fixes that by checking if the target architecture is arm64
and if so, will add the needed fix argument. This should prevent issues
reported here #7754.

We can not say this with 100% certainty since this bug sometimes appears
and sometimes it does not.

Fixes #7754

Signed-off-by: BlackDex <black.dex@gmail.com>
2026-09-25 21:53:06 +02:00
Daniel García 32098ca7d1 Require confirmed membership for user access checks, not for admin views (#7763) 2026-09-23 23:22:43 +02:00
15 changed files with 138 additions and 111 deletions

No files matched your search

+1
View File
@@ -409,6 +409,7 @@
## - "desktop-ui-migration-milestone-3": Special feature flag for desktop UI (Desktop >= 2026.2.0) ## - "desktop-ui-migration-milestone-3": Special feature flag for desktop UI (Desktop >= 2026.2.0)
## - "desktop-ui-migration-milestone-4": Special feature flag for desktop UI (Desktop >= 2026.2.0) ## - "desktop-ui-migration-milestone-4": Special feature flag for desktop UI (Desktop >= 2026.2.0)
## - "enable-basic-auth-response": Enable HTTP Basic Auth autofill in the browser extension (Browser >= 2026.9.0) ## - "enable-basic-auth-response": Enable HTTP Basic Auth autofill in the browser extension (Browser >= 2026.9.0)
## - "undetermined-cipher-scenario-logic": Enable the rewritten add/update login notification triggering logic in the browser extension (Browser >= 2026.2.0)
# EXPERIMENTAL_CLIENT_FEATURE_FLAGS= # EXPERIMENTAL_CLIENT_FEATURE_FLAGS=
## Require new device emails. When a user logs in an email is required to be sent. ## Require new device emails. When a user logs in an email is required to be sent.
+20 -4
View File
@@ -101,9 +101,17 @@ RUN . /env-cargo && \
# https://github.com/tonistiigi/xx/pull/108#issuecomment-3700635977 # https://github.com/tonistiigi/xx/pull/108#issuecomment-3700635977
# https://github.com/dani-garcia/vaultwarden/discussions/7522 # https://github.com/dani-garcia/vaultwarden/discussions/7522
if xx-info is-cross; then \ if xx-info is-cross; then \
XX_RUSTFLAGS="-C link-arg=-Wl,-rpath-link,/usr/lib/$(xx-info triple)"; \ XX_RUSTFLAGS="-Clink-arg=-Wl,-rpath-link,/usr/lib/$(xx-info triple)"; \
export XX_RUSTFLAGS; \
fi && \ fi && \
# Debian's GCC adds a special fix for cortex-a53 arch's which gets losts when using xx-cargo, which uses clang
# Checking if the arch is arm64 and adding this extra link-arg should cause this fix to be applied
# This should fix issues reported when running on a RPi3 which is a Cortex-A35 chip
# See: https://salsa.debian.org/toolchain-team/gcc/-/blob/master/debian/rules2?pow_referer=#L543-548
# See: https://github.com/dani-garcia/vaultwarden/issues/7754
if [ "$(xx-info arch)" = "arm64" ]; then \
XX_RUSTFLAGS="${XX_RUSTFLAGS} -Clink-arg=-Wl,--fix-cortex-a53-843419"; \
fi && \
export XX_RUSTFLAGS && \
PKG_CONFIG="$(command -v "$(xx-info)-pkg-config")" xx-cargo build --features ${DB} --profile "${CARGO_PROFILE}" && \ PKG_CONFIG="$(command -v "$(xx-info)-pkg-config")" xx-cargo build --features ${DB} --profile "${CARGO_PROFILE}" && \
find . -not -path "./target*" -delete find . -not -path "./target*" -delete
@@ -123,9 +131,17 @@ RUN . /env-cargo && \
# https://github.com/tonistiigi/xx/pull/108#issuecomment-3700635977 # https://github.com/tonistiigi/xx/pull/108#issuecomment-3700635977
# https://github.com/dani-garcia/vaultwarden/discussions/7522 # https://github.com/dani-garcia/vaultwarden/discussions/7522
if xx-info is-cross; then \ if xx-info is-cross; then \
XX_RUSTFLAGS="-C link-arg=-Wl,-rpath-link,/usr/lib/$(xx-info triple)"; \ XX_RUSTFLAGS="-Clink-arg=-Wl,-rpath-link,/usr/lib/$(xx-info triple)"; \
export XX_RUSTFLAGS; \
fi && \ fi && \
# Debian's GCC adds a special fix for cortex-a53 arch's which gets losts when using xx-cargo, which uses clang
# Checking if the arch is arm64 and adding this extra link-arg should cause this fix to be applied
# This should fix issues reported when running on a RPi3 which is a Cortex-A35 chip
# See: https://salsa.debian.org/toolchain-team/gcc/-/blob/master/debian/rules2?pow_referer=#L543-548
# See: https://github.com/dani-garcia/vaultwarden/issues/7754
if [ "$(xx-info arch)" = "arm64" ]; then \
XX_RUSTFLAGS="${XX_RUSTFLAGS} -Clink-arg=-Wl,--fix-cortex-a53-843419"; \
fi && \
export XX_RUSTFLAGS && \
PKG_CONFIG="$(command -v "$(xx-info)-pkg-config")" xx-cargo build --features ${DB} --profile "${CARGO_PROFILE}" && \ PKG_CONFIG="$(command -v "$(xx-info)-pkg-config")" xx-cargo build --features ${DB} --profile "${CARGO_PROFILE}" && \
if [ "${CARGO_PROFILE}" = "dev" ] ; then \ if [ "${CARGO_PROFILE}" = "dev" ] ; then \
ln -vfsr "/app/target/${CARGO_TARGET}/debug" /app/target/final ; \ ln -vfsr "/app/target/${CARGO_TARGET}/debug" /app/target/final ; \
+10 -2
View File
@@ -32,9 +32,17 @@
# https://github.com/tonistiigi/xx/pull/108#issuecomment-3700635977 # https://github.com/tonistiigi/xx/pull/108#issuecomment-3700635977
# https://github.com/dani-garcia/vaultwarden/discussions/7522 # https://github.com/dani-garcia/vaultwarden/discussions/7522
if xx-info is-cross; then \ if xx-info is-cross; then \
XX_RUSTFLAGS="-C link-arg=-Wl,-rpath-link,/usr/lib/$(xx-info triple)"; \ XX_RUSTFLAGS="-Clink-arg=-Wl,-rpath-link,/usr/lib/$(xx-info triple)"; \
export XX_RUSTFLAGS; \
fi && \ fi && \
# Debian's GCC adds a special fix for cortex-a53 arch's which gets losts when using xx-cargo, which uses clang
# Checking if the arch is arm64 and adding this extra link-arg should cause this fix to be applied
# This should fix issues reported when running on a RPi3 which is a Cortex-A35 chip
# See: https://salsa.debian.org/toolchain-team/gcc/-/blob/master/debian/rules2?pow_referer=#L543-548
# See: https://github.com/dani-garcia/vaultwarden/issues/7754
if [ "$(xx-info arch)" = "arm64" ]; then \
XX_RUSTFLAGS="${XX_RUSTFLAGS} -Clink-arg=-Wl,--fix-cortex-a53-843419"; \
fi && \
export XX_RUSTFLAGS && \
PKG_CONFIG="$(command -v "$(xx-info)-pkg-config")" xx-cargo build --features ${DB} --profile "${CARGO_PROFILE}" PKG_CONFIG="$(command -v "$(xx-info)-pkg-config")" xx-cargo build --features ${DB} --profile "${CARGO_PROFILE}"
{%- endmacro %} {%- endmacro %}
FROM --platform=linux/amd64 docker.io/vaultwarden/web-vault@{{ vault_image_digest }} AS vault FROM --platform=linux/amd64 docker.io/vaultwarden/web-vault@{{ vault_image_digest }} AS vault
+13 -13
View File
@@ -905,23 +905,23 @@ mod tests {
fn validate_web_vault_compare() { fn validate_web_vault_compare() {
// web_vault_compare(active, latest) // web_vault_compare(active, latest)
// Test normal versions // Test normal versions
assert!(web_vault_compare("2025.12.0", "2025.12.1") == -1); assert_eq!(web_vault_compare("2025.12.0", "2025.12.1"), -1);
assert!(web_vault_compare("2025.12.1", "2025.12.1") == 0); assert_eq!(web_vault_compare("2025.12.1", "2025.12.1"), 0);
assert!(web_vault_compare("2025.12.2", "2025.12.1") == 1); assert_eq!(web_vault_compare("2025.12.2", "2025.12.1"), 1);
// Test patched/+build.n versions // Test patched/+build.n versions
// Newer latest version // Newer latest version
assert!(web_vault_compare("2025.12.0+build.1", "2025.12.1") == -1); assert_eq!(web_vault_compare("2025.12.0+build.1", "2025.12.1"), -1);
assert!(web_vault_compare("2025.12.1", "2025.12.1+build.1") == -1); assert_eq!(web_vault_compare("2025.12.1", "2025.12.1+build.1"), -1);
assert!(web_vault_compare("2025.12.0+build.1", "2025.12.1+build.1") == -1); assert_eq!(web_vault_compare("2025.12.0+build.1", "2025.12.1+build.1"), -1);
assert!(web_vault_compare("2025.12.1+build.1", "2025.12.1+build.2") == -1); assert_eq!(web_vault_compare("2025.12.1+build.1", "2025.12.1+build.2"), -1);
// Equal versions // Equal versions
assert!(web_vault_compare("2025.12.1+build.1", "2025.12.1+build.1") == 0); assert_eq!(web_vault_compare("2025.12.1+build.1", "2025.12.1+build.1"), 0);
assert!(web_vault_compare("2025.12.2+build.2", "2025.12.2+build.2") == 0); assert_eq!(web_vault_compare("2025.12.2+build.2", "2025.12.2+build.2"), 0);
// Newer active version // Newer active version
assert!(web_vault_compare("2025.12.1+build.1", "2025.12.1") == 1); assert_eq!(web_vault_compare("2025.12.1+build.1", "2025.12.1"), 1);
assert!(web_vault_compare("2025.12.2", "2025.12.1+build.1") == 1); assert_eq!(web_vault_compare("2025.12.2", "2025.12.1+build.1"), 1);
assert!(web_vault_compare("2025.12.2+build.1", "2025.12.1+build.1") == 1); assert_eq!(web_vault_compare("2025.12.2+build.1", "2025.12.1+build.1"), 1);
assert!(web_vault_compare("2025.12.1+build.3", "2025.12.1+build.2") == 1); assert_eq!(web_vault_compare("2025.12.1+build.3", "2025.12.1+build.2"), 1);
} }
} }
+8 -8
View File
@@ -2180,8 +2180,15 @@ impl CipherSyncData {
} }
} }
// Generate a HashMap with the Organization UUID as key and the Membership record
let members: HashMap<OrganizationId, Membership> = Membership::find_confirmed_by_user(user_id, conn)
.await
.into_iter()
.map(|m| (m.org_uuid.clone(), m))
.collect();
// Generate a list of Cipher UUID's containing a Vec with one or more Attachment records // Generate a list of Cipher UUID's containing a Vec with one or more Attachment records
let orgs = Membership::get_orgs_by_user(user_id, conn).await; let orgs: Vec<OrganizationId> = members.keys().cloned().collect();
let attachments = Attachment::find_all_by_user_and_orgs(user_id, &orgs, conn).await; let attachments = Attachment::find_all_by_user_and_orgs(user_id, &orgs, conn).await;
let mut cipher_attachments: HashMap<CipherId, Vec<Attachment>> = HashMap::with_capacity(attachments.len()); let mut cipher_attachments: HashMap<CipherId, Vec<Attachment>> = HashMap::with_capacity(attachments.len());
for attachment in attachments { for attachment in attachments {
@@ -2196,13 +2203,6 @@ impl CipherSyncData {
cipher_collections.entry(cipher).or_default().push(collection); cipher_collections.entry(cipher).or_default().push(collection);
} }
// Generate a HashMap with the Organization UUID as key and the Membership record
let members: HashMap<OrganizationId, Membership> = Membership::find_confirmed_by_user(user_id, conn)
.await
.into_iter()
.map(|m| (m.org_uuid.clone(), m))
.collect();
// Generate a HashMap with the User_Collections UUID as key and the CollectionUser record // Generate a HashMap with the User_Collections UUID as key and the CollectionUser record
let user_collections: HashMap<CollectionId, CollectionUser> = CollectionUser::find_by_user(user_id, conn) let user_collections: HashMap<CollectionId, CollectionUser> = CollectionUser::find_by_user(user_id, conn)
.await .await
+14 -8
View File
@@ -415,8 +415,9 @@ async fn get_org_collections_details(org_id: OrganizationId, headers: ManagerHea
let col_users = CollectionUser::find_by_organization_swap_user_uuid_with_member_uuid(&org_id, &conn).await; let col_users = CollectionUser::find_by_organization_swap_user_uuid_with_member_uuid(&org_id, &conn).await;
// Generate a HashMap to get the correct MembershipType per user to determine the manage permission // Generate a HashMap to get the correct MembershipType per user to determine the manage permission
// We use the uuid instead of the user_uuid here, since that is what is used in CollectionUser // We use the uuid instead of the user_uuid here, since that is what is used in CollectionUser
// This lists other members for admins, so it must not depend on the membership status
let membership_type: HashMap<MembershipId, i32> = let membership_type: HashMap<MembershipId, i32> =
Membership::find_confirmed_by_org(&org_id, &conn).await.into_iter().map(|m| (m.uuid, m.atype)).collect(); Membership::find_by_org(&org_id, &conn).await.into_iter().map(|m| (m.uuid, m.atype)).collect();
// check if current user has full access to the organization (either directly or via any group) // check if current user has full access to the organization (either directly or via any group)
let has_full_access_to_org = member.has_full_access() let has_full_access_to_org = member.has_full_access()
@@ -818,11 +819,9 @@ async fn get_org_collection_detail(
// Generate a HashMap to get the correct MembershipType per user to determine the manage permission // Generate a HashMap to get the correct MembershipType per user to determine the manage permission
// We use the uuid instead of the user_uuid here, since that is what is used in CollectionUser // We use the uuid instead of the user_uuid here, since that is what is used in CollectionUser
let membership_type: HashMap<MembershipId, i32> = Membership::find_confirmed_by_org(&org_id, &conn) // This lists other members for admins, so it must not depend on the membership status
.await let membership_type: HashMap<MembershipId, i32> =
.into_iter() Membership::find_by_org(&org_id, &conn).await.into_iter().map(|m| (m.uuid, m.atype)).collect();
.map(|m| (m.uuid, m.atype))
.collect();
let users: Vec<Value> = let users: Vec<Value> =
CollectionUser::find_by_org_and_coll_swap_user_uuid_with_member_uuid(&org_id, &collection.uuid, &conn) CollectionUser::find_by_org_and_coll_swap_user_uuid_with_member_uuid(&org_id, &collection.uuid, &conn)
@@ -960,8 +959,9 @@ async fn get_members(
let mut users_json = Vec::new(); let mut users_json = Vec::new();
for u in Membership::find_by_org(&org_id, &conn).await { for u in Membership::find_by_org(&org_id, &conn).await {
// The user can be a manager instead of an admin, but we've checked above that they have full access
users_json.push( users_json.push(
u.to_json_user_details( u.to_json_details_for_admin(
data.include_collections.unwrap_or(false), data.include_collections.unwrap_or(false),
data.include_groups.unwrap_or(false), data.include_groups.unwrap_or(false),
&conn, &conn,
@@ -1516,7 +1516,9 @@ async fn get_user(
// In this case, when groups are requested we also need to include collections. // In this case, when groups are requested we also need to include collections.
// Else these will not be shown in the interface, and could lead to missing collections when saved. // Else these will not be shown in the interface, and could lead to missing collections when saved.
let include_groups = data.include_groups.unwrap_or(false); let include_groups = data.include_groups.unwrap_or(false);
Ok(Json(user.to_json_user_details(data.include_collections.unwrap_or(include_groups), include_groups, &conn).await)) Ok(Json(
user.to_json_details_for_admin(data.include_collections.unwrap_or(include_groups), include_groups, &conn).await,
))
} }
#[derive(Deserialize)] #[derive(Deserialize)]
@@ -1835,6 +1837,10 @@ async fn post_org_import(
if org_id != headers.membership.org_uuid { if org_id != headers.membership.org_uuid {
err!("Organization not found", "Organization id's do not match"); err!("Organization not found", "Organization id's do not match");
} }
// OrgMemberHeaders also allows invited and accepted members, which are not allowed to import
if headers.membership.status != MembershipStatus::Confirmed as i32 {
err!("You need to be a Member of the Organization to call this endpoint")
}
let data: ImportData = data.into_inner(); let data: ImportData = data.into_inner();
// Validate the import before continuing // Validate the import before continuing
+1
View File
@@ -89,6 +89,7 @@ fn vaultwarden_css() -> EtagCached<Css<String>> {
// Else, there is either no cache, or reload_templates is true and we need to rebuild the CSS // Else, there is either no cache, or reload_templates is true and we need to rebuild the CSS
let css_options = json!({ let css_options = json!({
"email_change_allowed": CONFIG.email_change_allowed(),
"emergency_access_allowed": CONFIG.emergency_access_allowed(), "emergency_access_allowed": CONFIG.emergency_access_allowed(),
"load_user_scss": true, "load_user_scss": true,
"mail_2fa_enabled": CONFIG._enable_email_2fa(), "mail_2fa_enabled": CONFIG._enable_email_2fa(),
+2
View File
@@ -1427,6 +1427,7 @@ pub const SUPPORTED_FEATURE_FLAGS: &[&str] = &[
"pm-5594-safari-account-switching", "pm-5594-safari-account-switching",
"pm-32413-multi-client-password-management", "pm-32413-multi-client-password-management",
// Autofill Team // Autofill Team
"undetermined-cipher-scenario-logic",
"enable-basic-auth-response", "enable-basic-auth-response",
"ssh-agent", "ssh-agent",
"ssh-agent-v2", "ssh-agent-v2",
@@ -1434,6 +1435,7 @@ pub const SUPPORTED_FEATURE_FLAGS: &[&str] = &[
"ssh-key-vault-item", "ssh-key-vault-item",
"pm-25373-windows-biometrics-v2", "pm-25373-windows-biometrics-v2",
"pm-26340-linux-biometrics-v2", "pm-26340-linux-biometrics-v2",
"windows-native-credential-sync",
// Mobile Team // Mobile Team
"anon-addy-self-host-alias", "anon-addy-self-host-alias",
"simple-login-self-host-alias", "simple-login-self-host-alias",
+5
View File
@@ -1003,6 +1003,7 @@ impl Cipher {
.eq(ciphers_collections::collection_uuid) .eq(ciphers_collections::collection_uuid)
.and(collections_groups::groups_uuid.eq(groups::uuid))), .and(collections_groups::groups_uuid.eq(groups::uuid))),
) )
.filter(users_organizations::status.eq(MembershipStatus::Confirmed as i32))
.filter( .filter(
users_organizations::access_all users_organizations::access_all
.eq(true) // User has access all .eq(true) // User has access all
@@ -1034,6 +1035,7 @@ impl Cipher {
.eq(ciphers_collections::collection_uuid) .eq(ciphers_collections::collection_uuid)
.and(users_collections::user_uuid.eq(user_uuid.clone()))), .and(users_collections::user_uuid.eq(user_uuid.clone()))),
) )
.filter(users_organizations::status.eq(MembershipStatus::Confirmed as i32))
.filter( .filter(
users_organizations::access_all users_organizations::access_all
.eq(true) // User has access all .eq(true) // User has access all
@@ -1078,6 +1080,7 @@ impl Cipher {
.eq(ciphers_collections::collection_uuid) .eq(ciphers_collections::collection_uuid)
.and(collections_groups::groups_uuid.eq(groups::uuid))), .and(collections_groups::groups_uuid.eq(groups::uuid))),
) )
.filter(users_organizations::status.eq(MembershipStatus::Confirmed as i32))
.filter( .filter(
users_organizations::access_all users_organizations::access_all
.eq(true) // User has access all .eq(true) // User has access all
@@ -1110,6 +1113,7 @@ impl Cipher {
.eq(ciphers_collections::collection_uuid) .eq(ciphers_collections::collection_uuid)
.and(users_collections::user_uuid.eq(user_uuid.clone()))), .and(users_collections::user_uuid.eq(user_uuid.clone()))),
) )
.filter(users_organizations::status.eq(MembershipStatus::Confirmed as i32))
.filter( .filter(
users_organizations::access_all users_organizations::access_all
.eq(true) // User has access all .eq(true) // User has access all
@@ -1161,6 +1165,7 @@ impl Cipher {
.or_filter(users_organizations::atype.le(MembershipType::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(groups::access_all.eq(true)) //Access via group
.or_filter(collections_groups::collections_uuid.is_not_null()) //Access via group .or_filter(collections_groups::collections_uuid.is_not_null()) //Access via group
.filter(users_organizations::status.eq(MembershipStatus::Confirmed as i32))
.select(ciphers_collections::all_columns) .select(ciphers_collections::all_columns)
.distinct() .distinct()
.load::<(CipherId, CollectionId)>(conn) .load::<(CipherId, CollectionId)>(conn)
+17 -2
View File
@@ -367,6 +367,7 @@ impl Collection {
.and(collections_groups::collections_uuid.eq(collections::uuid))), .and(collections_groups::collections_uuid.eq(collections::uuid))),
) )
.filter(collections::uuid.eq(uuid)) .filter(collections::uuid.eq(uuid))
.filter(users_organizations::status.eq(MembershipStatus::Confirmed as i32))
.filter( .filter(
users_collections::collection_uuid users_collections::collection_uuid
.eq(uuid) .eq(uuid)
@@ -406,6 +407,7 @@ impl Collection {
.and(users_organizations::user_uuid.eq(user_uuid))), .and(users_organizations::user_uuid.eq(user_uuid))),
) )
.filter(collections::uuid.eq(uuid)) .filter(collections::uuid.eq(uuid))
.filter(users_organizations::status.eq(MembershipStatus::Confirmed as i32))
.filter(users_collections::collection_uuid.eq(uuid).or( .filter(users_collections::collection_uuid.eq(uuid).or(
// Directly accessed collection // Directly accessed collection
users_organizations::access_all.eq(true).or( users_organizations::access_all.eq(true).or(
@@ -430,7 +432,8 @@ impl Collection {
.inner_join( .inner_join(
users_organizations::table.on(collections::org_uuid users_organizations::table.on(collections::org_uuid
.eq(users_organizations::org_uuid) .eq(users_organizations::org_uuid)
.and(users_organizations::user_uuid.eq(user_uuid.clone()))), .and(users_organizations::user_uuid.eq(user_uuid.clone()))
.and(users_organizations::status.eq(MembershipStatus::Confirmed as i32))),
) )
.left_join( .left_join(
users_collections::table.on(users_collections::collection_uuid users_collections::table.on(users_collections::collection_uuid
@@ -476,7 +479,8 @@ impl Collection {
.inner_join( .inner_join(
users_organizations::table.on(collections::org_uuid users_organizations::table.on(collections::org_uuid
.eq(users_organizations::org_uuid) .eq(users_organizations::org_uuid)
.and(users_organizations::user_uuid.eq(user_uuid.clone()))), .and(users_organizations::user_uuid.eq(user_uuid.clone()))
.and(users_organizations::status.eq(MembershipStatus::Confirmed as i32))),
) )
.left_join( .left_join(
users_collections::table.on(users_collections::collection_uuid users_collections::table.on(users_collections::collection_uuid
@@ -527,6 +531,7 @@ impl Collection {
.and(collections_groups::collections_uuid.eq(collections::uuid))), .and(collections_groups::collections_uuid.eq(collections::uuid))),
) )
.filter(collections::uuid.eq(&self.uuid)) .filter(collections::uuid.eq(&self.uuid))
.filter(users_organizations::status.eq(MembershipStatus::Confirmed as i32))
.filter( .filter(
users_collections::collection_uuid users_collections::collection_uuid
.eq(&self.uuid) .eq(&self.uuid)
@@ -586,6 +591,7 @@ impl Collection {
.and(collections_groups::collections_uuid.eq(collections::uuid))), .and(collections_groups::collections_uuid.eq(collections::uuid))),
) )
.filter(collections::uuid.eq(&uuid)) .filter(collections::uuid.eq(&uuid))
.filter(users_organizations::status.eq(MembershipStatus::Confirmed as i32))
.filter( .filter(
users_collections::collection_uuid users_collections::collection_uuid
.eq(&uuid) .eq(&uuid)
@@ -650,6 +656,7 @@ impl Collection {
.and(collections_groups::collections_uuid.eq(collections::uuid))), .and(collections_groups::collections_uuid.eq(collections::uuid))),
) )
.filter(collections::org_uuid.eq(&org_uuid)) .filter(collections::org_uuid.eq(&org_uuid))
.filter(users_organizations::status.eq(MembershipStatus::Confirmed as i32))
.filter( .filter(
// Manage permission on a collection assigned directly or via a group. // Manage permission on a collection assigned directly or via a group.
users_collections::manage.eq(true).or(collections_groups::manage.eq(true)), users_collections::manage.eq(true).or(collections_groups::manage.eq(true)),
@@ -819,10 +826,18 @@ impl CollectionUser {
.await .await
} }
// Only returns the collections of organizations the user is a confirmed member of
pub async fn find_by_user(user_uuid: &UserId, conn: &DbConn) -> Vec<Self> { pub async fn find_by_user(user_uuid: &UserId, conn: &DbConn) -> Vec<Self> {
conn.run(move |conn| { conn.run(move |conn| {
users_collections::table users_collections::table
.inner_join(collections::table.on(collections::uuid.eq(users_collections::collection_uuid)))
.inner_join(
users_organizations::table.on(users_organizations::org_uuid
.eq(collections::org_uuid)
.and(users_organizations::user_uuid.eq(users_collections::user_uuid))),
)
.filter(users_collections::user_uuid.eq(user_uuid)) .filter(users_collections::user_uuid.eq(user_uuid))
.filter(users_organizations::status.eq(MembershipStatus::Confirmed as i32))
.select(users_collections::all_columns) .select(users_collections::all_columns)
.load::<Self>(conn) .load::<Self>(conn)
.expect("Error loading users_collections") .expect("Error loading users_collections")
+2
View File
@@ -249,6 +249,7 @@ impl Group {
.and(groups::organizations_uuid.eq(users_organizations::org_uuid))), .and(groups::organizations_uuid.eq(users_organizations::org_uuid))),
) )
.filter(users_organizations::user_uuid.eq(user_uuid)) .filter(users_organizations::user_uuid.eq(user_uuid))
.filter(users_organizations::status.eq(MembershipStatus::Confirmed as i32))
.filter(groups::access_all.eq(true)) .filter(groups::access_all.eq(true))
.select(groups::organizations_uuid) .select(groups::organizations_uuid)
.distinct() .distinct()
@@ -387,6 +388,7 @@ impl CollectionGroup {
.and(collections::org_uuid.eq(groups::organizations_uuid))), .and(collections::org_uuid.eq(groups::organizations_uuid))),
) )
.filter(users_organizations::user_uuid.eq(user_uuid)) .filter(users_organizations::user_uuid.eq(user_uuid))
.filter(users_organizations::status.eq(MembershipStatus::Confirmed as i32))
.select(collections_groups::all_columns) .select(collections_groups::all_columns)
.load::<Self>(conn) .load::<Self>(conn)
.expect("Error loading user collection groups") .expect("Error loading user collection groups")
+22 -58
View File
@@ -1,7 +1,4 @@
use std::{ use std::cmp::Ordering;
cmp::Ordering,
collections::{HashMap, HashSet},
};
use chrono::{NaiveDateTime, Utc}; use chrono::{NaiveDateTime, Utc};
use derive_more::{AsRef, Deref, Display, From}; use derive_more::{AsRef, Deref, Display, From};
@@ -24,8 +21,8 @@ use crate::{
use macros::UuidFromParam; use macros::UuidFromParam;
use super::{ use super::{
Cipher, CipherId, Collection, CollectionGroup, CollectionId, CollectionUser, Group, GroupId, GroupUser, OrgPolicy, Cipher, CipherId, Collection, CollectionId, CollectionUser, Group, GroupId, GroupUser, OrgPolicy, OrgPolicyType,
OrgPolicyType, TwoFactor, User, UserId, TwoFactor, User, UserId,
}; };
#[derive(Identifiable, Queryable, Insertable, AsChangeset)] #[derive(Identifiable, Queryable, Insertable, AsChangeset)]
@@ -536,7 +533,13 @@ impl Membership {
}) })
} }
pub async fn to_json_user_details(&self, include_collections: bool, include_groups: bool, conn: &DbConn) -> Value { // Used by admins to view other members, so nothing here may depend on this member's status
pub async fn to_json_details_for_admin(
&self,
include_collections: bool,
include_groups: bool,
conn: &DbConn,
) -> Value {
let user = User::find_by_uuid(&self.user_uuid, conn).await.unwrap(); let user = User::find_by_uuid(&self.user_uuid, conn).await.unwrap();
// Because BitWarden want the status to be -1 for revoked users we need to catch that here. // Because BitWarden want the status to be -1 for revoked users we need to catch that here.
@@ -559,52 +562,23 @@ impl Membership {
// Check if a user is in a group which has access to all collections // Check if a user is in a group which has access to all collections
// If that is the case, we should not return individual collections! // If that is the case, we should not return individual collections!
// This is used by admins to view other members, so it must not depend on the membership status
let full_access_group = let full_access_group =
CONFIG.org_groups_enabled() && Group::is_in_full_access_group(&self.user_uuid, &self.org_uuid, conn).await; CONFIG.org_groups_enabled() && GroupUser::has_full_access_by_member(&self.org_uuid, &self.uuid, conn).await;
// 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 // 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
// Only the collections assigned directly are returned, the ones assigned via a group are returned via a special group endpoint
let collections: Vec<Value> = if include_collections && !(full_access_group || self.access_all) { let collections: Vec<Value> = if include_collections && !(full_access_group || self.access_all) {
// Get all collections for the user here already to prevent more queries CollectionUser::find_by_organization_and_user_uuid(&self.org_uuid, &self.user_uuid, conn)
let cu: HashMap<CollectionId, CollectionUser> =
CollectionUser::find_by_organization_and_user_uuid(&self.org_uuid, &self.user_uuid, conn)
.await
.into_iter()
.map(|cu| (cu.collection_uuid.clone(), cu))
.collect();
// Get all collection groups for this user to prevent there inclusion
let cg: HashSet<CollectionId> = CollectionGroup::find_by_user(&self.user_uuid, conn)
.await .await
.into_iter() .into_iter()
.map(|cg| cg.collections_uuid) .map(|cu| {
.collect(); json!({
"id": cu.collection_uuid,
Collection::find_by_organization_and_user_uuid(&self.org_uuid, &self.user_uuid, conn) "readOnly": cu.read_only,
.await "hidePasswords": cu.hide_passwords,
.into_iter() "manage": cu.manage || (self.atype == MembershipType::Manager && !cu.read_only && !cu.hide_passwords),
.filter_map(|c| { })
let (read_only, hide_passwords, 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),
)
// 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
} else if cg.contains(&c.uuid) {
return None;
} else {
(true, true, false)
};
Some(json!({
"id": c.uuid,
"readOnly": read_only,
"hidePasswords": hide_passwords,
"manage": manage,
}))
}) })
.collect() .collect()
} else { } else {
@@ -1032,17 +1006,6 @@ impl Membership {
.await .await
} }
pub async fn get_orgs_by_user(user_uuid: &UserId, conn: &DbConn) -> Vec<OrganizationId> {
conn.run(move |conn| {
users_organizations::table
.filter(users_organizations::user_uuid.eq(user_uuid))
.select(users_organizations::org_uuid)
.load::<OrganizationId>(conn)
.unwrap_or_default()
})
.await
}
pub async fn find_by_user_and_policy(user_uuid: &UserId, policy_type: OrgPolicyType, conn: &DbConn) -> Vec<Self> { pub async fn find_by_user_and_policy(user_uuid: &UserId, policy_type: OrgPolicyType, conn: &DbConn) -> Vec<Self> {
conn.run(move |conn| { conn.run(move |conn| {
users_organizations::table users_organizations::table
@@ -1126,6 +1089,7 @@ impl Membership {
.and(ciphers::organization_uuid.eq(users_organizations::org_uuid.nullable()))), .and(ciphers::organization_uuid.eq(users_organizations::org_uuid.nullable()))),
) )
.filter(users_organizations::user_uuid.eq(user_uuid)) .filter(users_organizations::user_uuid.eq(user_uuid))
.filter(users_organizations::status.eq(MembershipStatus::Confirmed as i32))
.filter( .filter(
users_organizations::atype.eq_any(vec![MembershipType::Owner as i32, MembershipType::Admin as i32]), users_organizations::atype.eq_any(vec![MembershipType::Owner as i32, MembershipType::Admin as i32]),
) )
+9 -9
View File
@@ -466,7 +466,7 @@ mod tests {
#[test] #[test]
fn dotted_decimal_loopback_normalizes() { fn dotted_decimal_loopback_normalizes() {
let ip = parse_to_ip("127.0.0.1").unwrap(); let ip = parse_to_ip("127.0.0.1").unwrap();
assert_eq!(ip, IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1))); assert_eq!(ip, IpAddr::V4(Ipv4Addr::LOCALHOST));
assert!(!is_global_hardcoded(ip)); assert!(!is_global_hardcoded(ip));
} }
@@ -474,21 +474,21 @@ mod tests {
fn single_decimal_loopback_normalizes() { fn single_decimal_loopback_normalizes() {
// 127.0.0.1 == 2130706433 // 127.0.0.1 == 2130706433
let ip = parse_to_ip("2130706433").unwrap(); let ip = parse_to_ip("2130706433").unwrap();
assert_eq!(ip, IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1))); assert_eq!(ip, IpAddr::V4(Ipv4Addr::LOCALHOST));
assert!(!is_global_hardcoded(ip)); assert!(!is_global_hardcoded(ip));
} }
#[test] #[test]
fn hex_loopback_normalizes() { fn hex_loopback_normalizes() {
let ip = parse_to_ip("0x7f000001").unwrap(); let ip = parse_to_ip("0x7f000001").unwrap();
assert_eq!(ip, IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1))); assert_eq!(ip, IpAddr::V4(Ipv4Addr::LOCALHOST));
assert!(!is_global_hardcoded(ip)); assert!(!is_global_hardcoded(ip));
} }
#[test] #[test]
fn dotted_hex_loopback_normalizes() { fn dotted_hex_loopback_normalizes() {
let ip = parse_to_ip("0x7f.0.0.1").unwrap(); let ip = parse_to_ip("0x7f.0.0.1").unwrap();
assert_eq!(ip, IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1))); assert_eq!(ip, IpAddr::V4(Ipv4Addr::LOCALHOST));
assert!(!is_global_hardcoded(ip)); assert!(!is_global_hardcoded(ip));
} }
@@ -496,14 +496,14 @@ mod tests {
fn octal_loopback_normalizes() { fn octal_loopback_normalizes() {
// 017700000001 == 127.0.0.1 // 017700000001 == 127.0.0.1
let ip = parse_to_ip("017700000001").unwrap(); let ip = parse_to_ip("017700000001").unwrap();
assert_eq!(ip, IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1))); assert_eq!(ip, IpAddr::V4(Ipv4Addr::LOCALHOST));
assert!(!is_global_hardcoded(ip)); assert!(!is_global_hardcoded(ip));
} }
#[test] #[test]
fn dotted_octal_loopback_normalizes() { fn dotted_octal_loopback_normalizes() {
let ip = parse_to_ip("0177.0.0.01").unwrap(); let ip = parse_to_ip("0177.0.0.01").unwrap();
assert_eq!(ip, IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1))); assert_eq!(ip, IpAddr::V4(Ipv4Addr::LOCALHOST));
assert!(!is_global_hardcoded(ip)); assert!(!is_global_hardcoded(ip));
} }
@@ -535,19 +535,19 @@ mod tests {
#[test] #[test]
fn get_valid_host_normalizes_decimal_int() { fn get_valid_host_normalizes_decimal_int() {
let h = get_valid_host("2130706433").expect("valid"); let h = get_valid_host("2130706433").expect("valid");
assert!(matches!(h, Host::Ipv4(ip) if ip == Ipv4Addr::new(127, 0, 0, 1))); assert!(matches!(h, Host::Ipv4(ip) if ip == Ipv4Addr::LOCALHOST));
} }
#[test] #[test]
fn get_valid_host_normalizes_hex() { fn get_valid_host_normalizes_hex() {
let h = get_valid_host("0x7f000001").expect("valid"); let h = get_valid_host("0x7f000001").expect("valid");
assert!(matches!(h, Host::Ipv4(ip) if ip == Ipv4Addr::new(127, 0, 0, 1))); assert!(matches!(h, Host::Ipv4(ip) if ip == Ipv4Addr::LOCALHOST));
} }
#[test] #[test]
fn get_valid_host_normalizes_octal() { fn get_valid_host_normalizes_octal() {
let h = get_valid_host("017700000001").expect("valid"); let h = get_valid_host("017700000001").expect("valid");
assert!(matches!(h, Host::Ipv4(ip) if ip == Ipv4Addr::new(127, 0, 0, 1))); assert!(matches!(h, Host::Ipv4(ip) if ip == Ipv4Addr::LOCALHOST));
} }
// === // ===
@@ -220,6 +220,13 @@ auth-input-password > form > bit-form-field:nth-child(4) {
@extend %vw-hide; @extend %vw-hide;
} }
{{/unless}} {{/unless}}
{{#unless email_change_allowed}}
/* Hide the whole `Change email` section, including its heading, if not allowed */
div:has(> app-change-email) {
@extend %vw-hide;
}
{{/unless}}
/**** End Dynamic Vaultwarden Changes ****/ /**** End Dynamic Vaultwarden Changes ****/
/**** Include a special user stylesheet for custom changes ****/ /**** Include a special user stylesheet for custom changes ****/
{{#if load_user_scss}} {{#if load_user_scss}}
+7 -7
View File
@@ -943,15 +943,15 @@ mod tests {
use std::net::IpAddr; use std::net::IpAddr;
#[test] #[test]
#[ignore] #[ignore = "exhaustive IPv4 check is too slow for the regular test suite"]
fn test_ipv4_global() { fn test_ipv4_global() {
for a in 0..u8::MAX { for a in 0..u8::MAX {
println!("Iter: {}/255", a); println!("Iter: {a}/255");
for b in 0..u8::MAX { for b in 0..u8::MAX {
for c in 0..u8::MAX { for c in 0..u8::MAX {
for d in 0..u8::MAX { for d in 0..u8::MAX {
let ip = IpAddr::V4(std::net::Ipv4Addr::new(a, b, c, d)); let ip = IpAddr::V4(std::net::Ipv4Addr::new(a, b, c, d));
assert_eq!(ip.is_global(), is_global_hardcoded(ip), "IP mismatch: {}", ip) assert_eq!(ip.is_global(), is_global_hardcoded(ip), "IP mismatch: {ip}");
} }
} }
} }
@@ -959,15 +959,15 @@ mod tests {
} }
#[test] #[test]
#[ignore] #[ignore = "random IPv6 check is too slow for the regular test suite"]
fn test_ipv6_global() { fn test_ipv6_global() {
use rand::Rng; use rand::RngExt;
std::thread::scope(|s| { std::thread::scope(|s| {
for t in 0..16 { for t in 0..16 {
let handle = s.spawn(move || { let _handle = s.spawn(move || {
let mut v = [0u8; 16]; let mut v = [0u8; 16];
let mut rng = rand::thread_rng(); let mut rng = rand::rng();
for i in 0..20 { for i in 0..20 {
println!("Thread {t} Iter: {i}/50"); println!("Thread {t} Iter: {i}/50");