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-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)
## - "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=
## 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/dani-garcia/vaultwarden/discussions/7522
if xx-info is-cross; then \
XX_RUSTFLAGS="-C link-arg=-Wl,-rpath-link,/usr/lib/$(xx-info triple)"; \
export XX_RUSTFLAGS; \
XX_RUSTFLAGS="-Clink-arg=-Wl,-rpath-link,/usr/lib/$(xx-info triple)"; \
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}" && \
find . -not -path "./target*" -delete
@@ -123,9 +131,17 @@ RUN . /env-cargo && \
# https://github.com/tonistiigi/xx/pull/108#issuecomment-3700635977
# https://github.com/dani-garcia/vaultwarden/discussions/7522
if xx-info is-cross; then \
XX_RUSTFLAGS="-C link-arg=-Wl,-rpath-link,/usr/lib/$(xx-info triple)"; \
export XX_RUSTFLAGS; \
XX_RUSTFLAGS="-Clink-arg=-Wl,-rpath-link,/usr/lib/$(xx-info triple)"; \
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}" && \
if [ "${CARGO_PROFILE}" = "dev" ] ; then \
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/dani-garcia/vaultwarden/discussions/7522
if xx-info is-cross; then \
XX_RUSTFLAGS="-C link-arg=-Wl,-rpath-link,/usr/lib/$(xx-info triple)"; \
export XX_RUSTFLAGS; \
XX_RUSTFLAGS="-Clink-arg=-Wl,-rpath-link,/usr/lib/$(xx-info triple)"; \
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}"
{%- endmacro %}
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() {
// web_vault_compare(active, latest)
// Test normal versions
assert!(web_vault_compare("2025.12.0", "2025.12.1") == -1);
assert!(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.0", "2025.12.1"), -1);
assert_eq!(web_vault_compare("2025.12.1", "2025.12.1"), 0);
assert_eq!(web_vault_compare("2025.12.2", "2025.12.1"), 1);
// Test patched/+build.n versions
// Newer latest version
assert!(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!(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.0+build.1", "2025.12.1"), -1);
assert_eq!(web_vault_compare("2025.12.1", "2025.12.1+build.1"), -1);
assert_eq!(web_vault_compare("2025.12.0+build.1", "2025.12.1+build.1"), -1);
assert_eq!(web_vault_compare("2025.12.1+build.1", "2025.12.1+build.2"), -1);
// Equal versions
assert!(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.1+build.1", "2025.12.1+build.1"), 0);
assert_eq!(web_vault_compare("2025.12.2+build.2", "2025.12.2+build.2"), 0);
// Newer active version
assert!(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!(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.1", "2025.12.1"), 1);
assert_eq!(web_vault_compare("2025.12.2", "2025.12.1+build.1"), 1);
assert_eq!(web_vault_compare("2025.12.2+build.1", "2025.12.1+build.1"), 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
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 mut cipher_attachments: HashMap<CipherId, Vec<Attachment>> = HashMap::with_capacity(attachments.len());
for attachment in attachments {
@@ -2196,13 +2203,6 @@ impl CipherSyncData {
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
let user_collections: HashMap<CollectionId, CollectionUser> = CollectionUser::find_by_user(user_id, conn)
.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;
// 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
// This lists other members for admins, so it must not depend on the membership status
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)
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
// 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)
.await
.into_iter()
.map(|m| (m.uuid, m.atype))
.collect();
// This lists other members for admins, so it must not depend on the membership status
let membership_type: HashMap<MembershipId, i32> =
Membership::find_by_org(&org_id, &conn).await.into_iter().map(|m| (m.uuid, m.atype)).collect();
let users: Vec<Value> =
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();
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(
u.to_json_user_details(
u.to_json_details_for_admin(
data.include_collections.unwrap_or(false),
data.include_groups.unwrap_or(false),
&conn,
@@ -1516,7 +1516,9 @@ async fn get_user(
// 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.
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)]
@@ -1835,6 +1837,10 @@ async fn post_org_import(
if org_id != headers.membership.org_uuid {
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();
// 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
let css_options = json!({
"email_change_allowed": CONFIG.email_change_allowed(),
"emergency_access_allowed": CONFIG.emergency_access_allowed(),
"load_user_scss": true,
"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-32413-multi-client-password-management",
// Autofill Team
"undetermined-cipher-scenario-logic",
"enable-basic-auth-response",
"ssh-agent",
"ssh-agent-v2",
@@ -1434,6 +1435,7 @@ pub const SUPPORTED_FEATURE_FLAGS: &[&str] = &[
"ssh-key-vault-item",
"pm-25373-windows-biometrics-v2",
"pm-26340-linux-biometrics-v2",
"windows-native-credential-sync",
// Mobile Team
"anon-addy-self-host-alias",
"simple-login-self-host-alias",
+5
View File
@@ -1003,6 +1003,7 @@ impl Cipher {
.eq(ciphers_collections::collection_uuid)
.and(collections_groups::groups_uuid.eq(groups::uuid))),
)
.filter(users_organizations::status.eq(MembershipStatus::Confirmed as i32))
.filter(
users_organizations::access_all
.eq(true) // User has access all
@@ -1034,6 +1035,7 @@ impl Cipher {
.eq(ciphers_collections::collection_uuid)
.and(users_collections::user_uuid.eq(user_uuid.clone()))),
)
.filter(users_organizations::status.eq(MembershipStatus::Confirmed as i32))
.filter(
users_organizations::access_all
.eq(true) // User has access all
@@ -1078,6 +1080,7 @@ impl Cipher {
.eq(ciphers_collections::collection_uuid)
.and(collections_groups::groups_uuid.eq(groups::uuid))),
)
.filter(users_organizations::status.eq(MembershipStatus::Confirmed as i32))
.filter(
users_organizations::access_all
.eq(true) // User has access all
@@ -1110,6 +1113,7 @@ impl Cipher {
.eq(ciphers_collections::collection_uuid)
.and(users_collections::user_uuid.eq(user_uuid.clone()))),
)
.filter(users_organizations::status.eq(MembershipStatus::Confirmed as i32))
.filter(
users_organizations::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(groups::access_all.eq(true)) //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)
.distinct()
.load::<(CipherId, CollectionId)>(conn)
+17 -2
View File
@@ -367,6 +367,7 @@ impl Collection {
.and(collections_groups::collections_uuid.eq(collections::uuid))),
)
.filter(collections::uuid.eq(uuid))
.filter(users_organizations::status.eq(MembershipStatus::Confirmed as i32))
.filter(
users_collections::collection_uuid
.eq(uuid)
@@ -406,6 +407,7 @@ impl Collection {
.and(users_organizations::user_uuid.eq(user_uuid))),
)
.filter(collections::uuid.eq(uuid))
.filter(users_organizations::status.eq(MembershipStatus::Confirmed as i32))
.filter(users_collections::collection_uuid.eq(uuid).or(
// Directly accessed collection
users_organizations::access_all.eq(true).or(
@@ -430,7 +432,8 @@ impl Collection {
.inner_join(
users_organizations::table.on(collections::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(
users_collections::table.on(users_collections::collection_uuid
@@ -476,7 +479,8 @@ impl Collection {
.inner_join(
users_organizations::table.on(collections::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(
users_collections::table.on(users_collections::collection_uuid
@@ -527,6 +531,7 @@ impl Collection {
.and(collections_groups::collections_uuid.eq(collections::uuid))),
)
.filter(collections::uuid.eq(&self.uuid))
.filter(users_organizations::status.eq(MembershipStatus::Confirmed as i32))
.filter(
users_collections::collection_uuid
.eq(&self.uuid)
@@ -586,6 +591,7 @@ impl Collection {
.and(collections_groups::collections_uuid.eq(collections::uuid))),
)
.filter(collections::uuid.eq(&uuid))
.filter(users_organizations::status.eq(MembershipStatus::Confirmed as i32))
.filter(
users_collections::collection_uuid
.eq(&uuid)
@@ -650,6 +656,7 @@ impl Collection {
.and(collections_groups::collections_uuid.eq(collections::uuid))),
)
.filter(collections::org_uuid.eq(&org_uuid))
.filter(users_organizations::status.eq(MembershipStatus::Confirmed as i32))
.filter(
// Manage permission on a collection assigned directly or via a group.
users_collections::manage.eq(true).or(collections_groups::manage.eq(true)),
@@ -819,10 +826,18 @@ impl CollectionUser {
.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> {
conn.run(move |conn| {
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_organizations::status.eq(MembershipStatus::Confirmed as i32))
.select(users_collections::all_columns)
.load::<Self>(conn)
.expect("Error loading users_collections")
+2
View File
@@ -249,6 +249,7 @@ impl Group {
.and(groups::organizations_uuid.eq(users_organizations::org_uuid))),
)
.filter(users_organizations::user_uuid.eq(user_uuid))
.filter(users_organizations::status.eq(MembershipStatus::Confirmed as i32))
.filter(groups::access_all.eq(true))
.select(groups::organizations_uuid)
.distinct()
@@ -387,6 +388,7 @@ impl CollectionGroup {
.and(collections::org_uuid.eq(groups::organizations_uuid))),
)
.filter(users_organizations::user_uuid.eq(user_uuid))
.filter(users_organizations::status.eq(MembershipStatus::Confirmed as i32))
.select(collections_groups::all_columns)
.load::<Self>(conn)
.expect("Error loading user collection groups")
+22 -58
View File
@@ -1,7 +1,4 @@
use std::{
cmp::Ordering,
collections::{HashMap, HashSet},
};
use std::cmp::Ordering;
use chrono::{NaiveDateTime, Utc};
use derive_more::{AsRef, Deref, Display, From};
@@ -24,8 +21,8 @@ use crate::{
use macros::UuidFromParam;
use super::{
Cipher, CipherId, Collection, CollectionGroup, CollectionId, CollectionUser, Group, GroupId, GroupUser, OrgPolicy,
OrgPolicyType, TwoFactor, User, UserId,
Cipher, CipherId, Collection, CollectionId, CollectionUser, Group, GroupId, GroupUser, OrgPolicy, OrgPolicyType,
TwoFactor, User, UserId,
};
#[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();
// 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
// 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 =
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
// 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) {
// Get all collections for the user here already to prevent more queries
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)
CollectionUser::find_by_organization_and_user_uuid(&self.org_uuid, &self.user_uuid, conn)
.await
.into_iter()
.map(|cg| cg.collections_uuid)
.collect();
Collection::find_by_organization_and_user_uuid(&self.org_uuid, &self.user_uuid, conn)
.await
.into_iter()
.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,
}))
.map(|cu| {
json!({
"id": cu.collection_uuid,
"readOnly": cu.read_only,
"hidePasswords": cu.hide_passwords,
"manage": cu.manage || (self.atype == MembershipType::Manager && !cu.read_only && !cu.hide_passwords),
})
})
.collect()
} else {
@@ -1032,17 +1006,6 @@ impl Membership {
.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> {
conn.run(move |conn| {
users_organizations::table
@@ -1126,6 +1089,7 @@ impl Membership {
.and(ciphers::organization_uuid.eq(users_organizations::org_uuid.nullable()))),
)
.filter(users_organizations::user_uuid.eq(user_uuid))
.filter(users_organizations::status.eq(MembershipStatus::Confirmed as i32))
.filter(
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]
fn dotted_decimal_loopback_normalizes() {
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));
}
@@ -474,21 +474,21 @@ mod tests {
fn single_decimal_loopback_normalizes() {
// 127.0.0.1 == 2130706433
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));
}
#[test]
fn hex_loopback_normalizes() {
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));
}
#[test]
fn dotted_hex_loopback_normalizes() {
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));
}
@@ -496,14 +496,14 @@ mod tests {
fn octal_loopback_normalizes() {
// 017700000001 == 127.0.0.1
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));
}
#[test]
fn dotted_octal_loopback_normalizes() {
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));
}
@@ -535,19 +535,19 @@ mod tests {
#[test]
fn get_valid_host_normalizes_decimal_int() {
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]
fn get_valid_host_normalizes_hex() {
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]
fn get_valid_host_normalizes_octal() {
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;
}
{{/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 ****/
/**** Include a special user stylesheet for custom changes ****/
{{#if load_user_scss}}
+7 -7
View File
@@ -943,15 +943,15 @@ mod tests {
use std::net::IpAddr;
#[test]
#[ignore]
#[ignore = "exhaustive IPv4 check is too slow for the regular test suite"]
fn test_ipv4_global() {
for a in 0..u8::MAX {
println!("Iter: {}/255", a);
println!("Iter: {a}/255");
for b in 0..u8::MAX {
for c in 0..u8::MAX {
for d in 0..u8::MAX {
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]
#[ignore]
#[ignore = "random IPv6 check is too slow for the regular test suite"]
fn test_ipv6_global() {
use rand::Rng;
use rand::RngExt;
std::thread::scope(|s| {
for t in 0..16 {
let handle = s.spawn(move || {
let _handle = s.spawn(move || {
let mut v = [0u8; 16];
let mut rng = rand::thread_rng();
let mut rng = rand::rng();
for i in 0..20 {
println!("Thread {t} Iter: {i}/50");