Compare commits

...

4 Commits

Author SHA1 Message Date
Timshel
3f010a50af Change OIDC dummy identifier (#6263)
* Change OIDC dummy identifier

* Update src/sso.rs

Co-authored-by: Helmut K. C. Tessarek <tessarek@evermeet.cx>

* Use Org uuid as identifier

---------

Co-authored-by: Helmut K. C. Tessarek <tessarek@evermeet.cx>
Co-authored-by: Mathijs van Veluw <black.dex@gmail.com>
2025-10-13 21:28:37 +02:00
Timshel
e83faad8d2 Fix sso_user dropped on User::save (#6262)
* Admin delete SSO association prompt

* User.save don't use replace_into

* User.save use upsert with sqlite

* User.save use upsert with mysql
2025-10-13 21:25:53 +02:00
Stefan Melmuk
a79cd40ea9 improve permission check for collections (#6278) 2025-10-13 21:14:53 +02:00
Stefan Melmuk
b1d84298cc update web vault to v2025.9.1 and allow new policy (#6340)
* update web-vault to v2025.9.1

* allow new card removal policy
2025-10-13 20:54:24 +02:00
11 changed files with 48 additions and 45 deletions

View File

@@ -1,6 +1,6 @@
--- ---
vault_version: "v2025.8.0" vault_version: "v2025.9.1"
vault_image_digest: "sha256:41c2b51c87882248f405d5a0ab37210d2672a312ec5d4f3b9afcdbbe8eb9d57d" vault_image_digest: "sha256:15a126ca967cd2efc4c9625fec49f0b972a3f7d7d81d7770bb0a2502d5e4b8a4"
# Cross Compile Docker Helper Scripts v1.6.1 # 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 # 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 # https://github.com/tonistiigi/xx | https://hub.docker.com/r/tonistiigi/xx/tags

View File

@@ -19,15 +19,15 @@
# - From https://hub.docker.com/r/vaultwarden/web-vault/tags, # - 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. # click the tag name to view the digest of the image it currently points to.
# - From the command line: # - From the command line:
# $ docker pull docker.io/vaultwarden/web-vault:v2025.8.0 # $ docker pull docker.io/vaultwarden/web-vault:v2025.9.1
# $ docker image inspect --format "{{.RepoDigests}}" docker.io/vaultwarden/web-vault:v2025.8.0 # $ docker image inspect --format "{{.RepoDigests}}" docker.io/vaultwarden/web-vault:v2025.9.1
# [docker.io/vaultwarden/web-vault@sha256:41c2b51c87882248f405d5a0ab37210d2672a312ec5d4f3b9afcdbbe8eb9d57d] # [docker.io/vaultwarden/web-vault@sha256:15a126ca967cd2efc4c9625fec49f0b972a3f7d7d81d7770bb0a2502d5e4b8a4]
# #
# - Conversely, to get the tag name from the digest: # - Conversely, to get the tag name from the digest:
# $ docker image inspect --format "{{.RepoTags}}" docker.io/vaultwarden/web-vault@sha256:41c2b51c87882248f405d5a0ab37210d2672a312ec5d4f3b9afcdbbe8eb9d57d # $ docker image inspect --format "{{.RepoTags}}" docker.io/vaultwarden/web-vault@sha256:15a126ca967cd2efc4c9625fec49f0b972a3f7d7d81d7770bb0a2502d5e4b8a4
# [docker.io/vaultwarden/web-vault:v2025.8.0] # [docker.io/vaultwarden/web-vault:v2025.9.1]
# #
FROM --platform=linux/amd64 docker.io/vaultwarden/web-vault@sha256:41c2b51c87882248f405d5a0ab37210d2672a312ec5d4f3b9afcdbbe8eb9d57d AS vault FROM --platform=linux/amd64 docker.io/vaultwarden/web-vault@sha256:15a126ca967cd2efc4c9625fec49f0b972a3f7d7d81d7770bb0a2502d5e4b8a4 AS vault
########################## ALPINE BUILD IMAGES ########################## ########################## ALPINE BUILD IMAGES ##########################
## NOTE: The Alpine Base Images do not support other platforms then linux/amd64 ## NOTE: The Alpine Base Images do not support other platforms then linux/amd64

View File

@@ -19,15 +19,15 @@
# - From https://hub.docker.com/r/vaultwarden/web-vault/tags, # - 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. # click the tag name to view the digest of the image it currently points to.
# - From the command line: # - From the command line:
# $ docker pull docker.io/vaultwarden/web-vault:v2025.8.0 # $ docker pull docker.io/vaultwarden/web-vault:v2025.9.1
# $ docker image inspect --format "{{.RepoDigests}}" docker.io/vaultwarden/web-vault:v2025.8.0 # $ docker image inspect --format "{{.RepoDigests}}" docker.io/vaultwarden/web-vault:v2025.9.1
# [docker.io/vaultwarden/web-vault@sha256:41c2b51c87882248f405d5a0ab37210d2672a312ec5d4f3b9afcdbbe8eb9d57d] # [docker.io/vaultwarden/web-vault@sha256:15a126ca967cd2efc4c9625fec49f0b972a3f7d7d81d7770bb0a2502d5e4b8a4]
# #
# - Conversely, to get the tag name from the digest: # - Conversely, to get the tag name from the digest:
# $ docker image inspect --format "{{.RepoTags}}" docker.io/vaultwarden/web-vault@sha256:41c2b51c87882248f405d5a0ab37210d2672a312ec5d4f3b9afcdbbe8eb9d57d # $ docker image inspect --format "{{.RepoTags}}" docker.io/vaultwarden/web-vault@sha256:15a126ca967cd2efc4c9625fec49f0b972a3f7d7d81d7770bb0a2502d5e4b8a4
# [docker.io/vaultwarden/web-vault:v2025.8.0] # [docker.io/vaultwarden/web-vault:v2025.9.1]
# #
FROM --platform=linux/amd64 docker.io/vaultwarden/web-vault@sha256:41c2b51c87882248f405d5a0ab37210d2672a312ec5d4f3b9afcdbbe8eb9d57d AS vault FROM --platform=linux/amd64 docker.io/vaultwarden/web-vault@sha256:15a126ca967cd2efc4c9625fec49f0b972a3f7d7d81d7770bb0a2502d5e4b8a4 AS vault
########################## Cross Compile Docker Helper Scripts ########################## ########################## Cross Compile Docker Helper Scripts ##########################
## We use the linux/amd64 no matter which Build Platform, since these are all bash scripts ## We use the linux/amd64 no matter which Build Platform, since these are all bash scripts

View File

@@ -367,7 +367,7 @@ async fn post_set_password(data: Json<SetPasswordData>, headers: Headers, mut co
if let Some(identifier) = data.org_identifier { if let Some(identifier) = data.org_identifier {
if identifier != crate::sso::FAKE_IDENTIFIER { if identifier != crate::sso::FAKE_IDENTIFIER {
let org = match Organization::find_by_name(&identifier, &mut conn).await { let org = match Organization::find_by_uuid(&identifier.into(), &mut conn).await {
None => err!("Failed to retrieve the associated organization"), None => err!("Failed to retrieve the associated organization"),
Some(org) => org, Some(org) => org,
}; };

View File

@@ -773,8 +773,8 @@ async fn post_collections_update(
err!("Cipher doesn't exist") err!("Cipher doesn't exist")
}; };
if !cipher.is_write_accessible_to_user(&headers.user.uuid, &mut conn).await { if !cipher.is_in_editable_collection_by_user(&headers.user.uuid, &mut conn).await {
err!("Cipher is not write accessible") err!("Collection cannot be changed")
} }
let posted_collections = HashSet::<CollectionId>::from_iter(data.collection_ids); let posted_collections = HashSet::<CollectionId>::from_iter(data.collection_ids);
@@ -850,8 +850,8 @@ async fn post_collections_admin(
err!("Cipher doesn't exist") err!("Cipher doesn't exist")
}; };
if !cipher.is_write_accessible_to_user(&headers.user.uuid, &mut conn).await { if !cipher.is_in_editable_collection_by_user(&headers.user.uuid, &mut conn).await {
err!("Cipher is not write accessible") err!("Collection cannot be changed")
} }
let posted_collections = HashSet::<CollectionId>::from_iter(data.collection_ids); let posted_collections = HashSet::<CollectionId>::from_iter(data.collection_ids);

View File

@@ -339,7 +339,7 @@ async fn get_user_collections(headers: Headers, mut conn: DbConn) -> Json<Value>
} }
// Called during the SSO enrollment // Called during the SSO enrollment
// The `identifier` should be the value returned by `get_org_domain_sso_details` // The `identifier` should be the value returned by `get_org_domain_sso_verified`
// The returned `Id` will then be passed to `get_master_password_policy` which will mainly ignore it // The returned `Id` will then be passed to `get_master_password_policy` which will mainly ignore it
#[get("/organizations/<identifier>/auto-enroll-status")] #[get("/organizations/<identifier>/auto-enroll-status")]
async fn get_auto_enroll_status(identifier: &str, headers: Headers, mut conn: DbConn) -> JsonResult { async fn get_auto_enroll_status(identifier: &str, headers: Headers, mut conn: DbConn) -> JsonResult {
@@ -349,7 +349,7 @@ async fn get_auto_enroll_status(identifier: &str, headers: Headers, mut conn: Db
None => None, None => None,
} }
} else { } else {
Organization::find_by_name(identifier, &mut conn).await Organization::find_by_uuid(&identifier.into(), &mut conn).await
}; };
let (id, identifier, rp_auto_enroll) = match org { let (id, identifier, rp_auto_enroll) = match org {
@@ -977,17 +977,17 @@ async fn get_org_domain_sso_verified(data: Json<OrgDomainDetails>, mut conn: DbC
let identifiers = match Organization::find_org_user_email(&data.email, &mut conn) let identifiers = match Organization::find_org_user_email(&data.email, &mut conn)
.await .await
.into_iter() .into_iter()
.map(|o| o.name) .map(|o| (o.name, o.uuid.to_string()))
.collect::<Vec<String>>() .collect::<Vec<(String, String)>>()
{ {
v if !v.is_empty() => v, v if !v.is_empty() => v,
_ => vec![crate::sso::FAKE_IDENTIFIER.to_string()], _ => vec![(crate::sso::FAKE_IDENTIFIER.to_string(), crate::sso::FAKE_IDENTIFIER.to_string())],
}; };
Ok(Json(json!({ Ok(Json(json!({
"object": "list", "object": "list",
"data": identifiers.into_iter().map(|identifier| json!({ "data": identifiers.into_iter().map(|(name, identifier)| json!({
"organizationName": identifier, // appear unused "organizationName": name, // appear unused
"organizationIdentifier": identifier, "organizationIdentifier": identifier,
"domainName": CONFIG.domain(), // appear unused "domainName": CONFIG.domain(), // appear unused
})).collect::<Vec<Value>>() })).collect::<Vec<Value>>()

View File

@@ -717,6 +717,15 @@ impl Cipher {
} }
} }
// used for checking if collection can be edited (only if user has access to a collection they
// can write to and also passwords are not hidden to prevent privilege escalation)
pub async fn is_in_editable_collection_by_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 && !hide_passwords) || manage,
None => false,
}
}
pub async fn is_accessible_to_user(&self, user_uuid: &UserId, conn: &mut DbConn) -> bool { pub async fn is_accessible_to_user(&self, user_uuid: &UserId, conn: &mut DbConn) -> bool {
self.get_access_restrictions(user_uuid, None, conn).await.is_some() self.get_access_restrictions(user_uuid, None, conn).await.is_some()
} }

View File

@@ -39,6 +39,7 @@ pub enum OrgPolicyType {
// AutomaticAppLogIn = 12, // AutomaticAppLogIn = 12,
// FreeFamiliesSponsorshipPolicy = 13, // FreeFamiliesSponsorshipPolicy = 13,
RemoveUnlockWithPin = 14, RemoveUnlockWithPin = 14,
RestrictedItemTypes = 15,
} }
// https://github.com/bitwarden/server/blob/9ebe16587175b1c0e9208f84397bb75d0d595510/src/Core/AdminConsole/Models/Data/Organizations/Policies/SendOptionsPolicyData.cs#L5 // https://github.com/bitwarden/server/blob/9ebe16587175b1c0e9208f84397bb75d0d595510/src/Core/AdminConsole/Models/Data/Organizations/Policies/SendOptionsPolicyData.cs#L5

View File

@@ -283,24 +283,17 @@ impl User {
self.updated_at = Utc::now().naive_utc(); self.updated_at = Utc::now().naive_utc();
db_run! {conn: db_run! {conn:
sqlite, mysql { mysql {
match diesel::replace_into(users::table) let value = UserDb::to_db(self);
.values(UserDb::to_db(self)) diesel::insert_into(users::table)
.execute(conn) .values(&value)
{ .on_conflict(diesel::dsl::DuplicatedKeys)
Ok(_) => Ok(()), .do_update()
// Record already exists and causes a Foreign Key Violation because replace_into() wants to delete the record first. .set(&value)
Err(diesel::result::Error::DatabaseError(diesel::result::DatabaseErrorKind::ForeignKeyViolation, _)) => {
diesel::update(users::table)
.filter(users::uuid.eq(&self.uuid))
.set(UserDb::to_db(self))
.execute(conn) .execute(conn)
.map_res("Error saving user") .map_res("Error saving user")
} }
Err(e) => Err(e.into()), postgresql, sqlite {
}.map_res("Error saving user")
}
postgresql {
let value = UserDb::to_db(self); let value = UserDb::to_db(self);
diesel::insert_into(users::table) // Insert or update diesel::insert_into(users::table) // Insert or update
.values(&value) .values(&value)

View File

@@ -19,7 +19,7 @@ use crate::{
CONFIG, CONFIG,
}; };
pub static FAKE_IDENTIFIER: &str = "Vaultwarden"; pub static FAKE_IDENTIFIER: &str = "VW_DUMMY_IDENTIFIER_FOR_OIDC";
static AC_CACHE: Lazy<Cache<OIDCState, AuthenticatedUser>> = static AC_CACHE: Lazy<Cache<OIDCState, AuthenticatedUser>> =
Lazy::new(|| Cache::builder().max_capacity(1000).time_to_live(Duration::from_secs(10 * 60)).build()); Lazy::new(|| Cache::builder().max_capacity(1000).time_to_live(Duration::from_secs(10 * 60)).build());

View File

@@ -33,11 +33,11 @@ function deleteSSOUser(event) {
alert("Required parameters not found!"); alert("Required parameters not found!");
return false; return false;
} }
const input_email = prompt(`To delete user "${email}", please type the email below`); const input_email = prompt(`To delete user "${email}" SSO association, please type the email below`);
if (input_email != null) { if (input_email != null) {
if (input_email == email) { if (input_email == email) {
_delete(`${BASE_URL}/admin/users/${id}/sso`, _delete(`${BASE_URL}/admin/users/${id}/sso`,
"User SSO Associtation deleted correctly", "User SSO association deleted correctly",
"Error deleting user SSO association" "Error deleting user SSO association"
); );
} else { } else {