Browse Source

Merge 5e7cf66d67 into 169aa5efcc

pull/7452/merge
Raphaël Roumezin 1 day ago
committed by GitHub
parent
commit
6bf0aa3de4
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 4
      migrations/mysql/2026-07-21-120000_add_collection_management/down.sql
  2. 4
      migrations/mysql/2026-07-21-120000_add_collection_management/up.sql
  3. 4
      migrations/postgresql/2026-07-21-120000_add_collection_management/down.sql
  4. 4
      migrations/postgresql/2026-07-21-120000_add_collection_management/up.sql
  5. 4
      migrations/sqlite/2026-07-21-120000_add_collection_management/down.sql
  6. 4
      migrations/sqlite/2026-07-21-120000_add_collection_management/up.sql
  7. 5
      src/api/core/ciphers.rs
  8. 56
      src/api/core/organizations.rs
  9. 23
      src/db/models/cipher.rs
  10. 27
      src/db/models/organization.rs
  11. 4
      src/db/schema.rs
  12. 5
      src/static/templates/scss/vaultwarden.scss.hbs

4
migrations/mysql/2026-07-21-120000_add_collection_management/down.sql

@ -0,0 +1,4 @@
ALTER TABLE organizations DROP COLUMN allow_admin_access_to_all_collection_items;
ALTER TABLE organizations DROP COLUMN limit_collection_creation;
ALTER TABLE organizations DROP COLUMN limit_collection_deletion;
ALTER TABLE organizations DROP COLUMN limit_item_deletion;

4
migrations/mysql/2026-07-21-120000_add_collection_management/up.sql

@ -0,0 +1,4 @@
ALTER TABLE organizations ADD COLUMN allow_admin_access_to_all_collection_items BOOLEAN NOT NULL DEFAULT TRUE;
ALTER TABLE organizations ADD COLUMN limit_collection_creation BOOLEAN NOT NULL DEFAULT FALSE;
ALTER TABLE organizations ADD COLUMN limit_collection_deletion BOOLEAN NOT NULL DEFAULT FALSE;
ALTER TABLE organizations ADD COLUMN limit_item_deletion BOOLEAN NOT NULL DEFAULT FALSE;

4
migrations/postgresql/2026-07-21-120000_add_collection_management/down.sql

@ -0,0 +1,4 @@
ALTER TABLE organizations DROP COLUMN allow_admin_access_to_all_collection_items;
ALTER TABLE organizations DROP COLUMN limit_collection_creation;
ALTER TABLE organizations DROP COLUMN limit_collection_deletion;
ALTER TABLE organizations DROP COLUMN limit_item_deletion;

4
migrations/postgresql/2026-07-21-120000_add_collection_management/up.sql

@ -0,0 +1,4 @@
ALTER TABLE organizations ADD COLUMN allow_admin_access_to_all_collection_items BOOLEAN NOT NULL DEFAULT TRUE;
ALTER TABLE organizations ADD COLUMN limit_collection_creation BOOLEAN NOT NULL DEFAULT FALSE;
ALTER TABLE organizations ADD COLUMN limit_collection_deletion BOOLEAN NOT NULL DEFAULT FALSE;
ALTER TABLE organizations ADD COLUMN limit_item_deletion BOOLEAN NOT NULL DEFAULT FALSE;

4
migrations/sqlite/2026-07-21-120000_add_collection_management/down.sql

@ -0,0 +1,4 @@
ALTER TABLE organizations DROP COLUMN allow_admin_access_to_all_collection_items;
ALTER TABLE organizations DROP COLUMN limit_collection_creation;
ALTER TABLE organizations DROP COLUMN limit_collection_deletion;
ALTER TABLE organizations DROP COLUMN limit_item_deletion;

4
migrations/sqlite/2026-07-21-120000_add_collection_management/up.sql

@ -0,0 +1,4 @@
ALTER TABLE organizations ADD COLUMN allow_admin_access_to_all_collection_items BOOLEAN NOT NULL DEFAULT TRUE;
ALTER TABLE organizations ADD COLUMN limit_collection_creation BOOLEAN NOT NULL DEFAULT FALSE;
ALTER TABLE organizations ADD COLUMN limit_collection_deletion BOOLEAN NOT NULL DEFAULT FALSE;
ALTER TABLE organizations ADD COLUMN limit_item_deletion BOOLEAN NOT NULL DEFAULT FALSE;

5
src/api/core/ciphers.rs

@ -13,8 +13,7 @@ use serde_json::Value;
use crate::{
CONFIG,
api::{self, EmptyResult, JsonResult, Notify, PasswordOrOtpData, UpdateType, core::log_event},
auth::ClientVersion,
auth::{Headers, OrgIdGuard, OwnerHeaders},
auth::{ClientVersion, Headers, OrgIdGuard, OwnerHeaders},
config::PathType,
crypto,
db::{
@ -1778,7 +1777,7 @@ async fn delete_cipher_by_uuid(
err!("Cipher doesn't exist")
};
if !cipher.is_write_accessible_to_user(&headers.user.uuid, conn).await {
if !cipher.is_deletable_by_user(&headers.user.uuid, conn).await {
err!("Cipher can't be deleted by user")
}

56
src/api/core/organizations.rs

@ -39,6 +39,7 @@ pub fn routes() -> Vec<Route> {
get_collection_users,
put_organization,
post_organization,
put_organization_collection_management,
post_organization_collections,
post_bulk_access_collections,
post_organization_collection_update,
@ -126,6 +127,15 @@ struct OrganizationUpdateData {
name: String,
}
#[derive(Deserialize, Debug)]
#[serde(rename_all = "camelCase")]
struct OrganizationCollectionManagementUpdateData {
allow_admin_access_to_all_collection_items: Option<bool>,
limit_collection_creation: Option<bool>,
limit_collection_deletion: Option<bool>,
limit_item_deletion: Option<bool>,
}
#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
struct FullCollectionData {
@ -340,6 +350,52 @@ async fn post_organization(
Ok(Json(org.to_json()))
}
#[put("/organizations/<org_id>/collection-management", data = "<data>")]
async fn put_organization_collection_management(
org_id: OrganizationId,
headers: OwnerHeaders,
data: Json<OrganizationCollectionManagementUpdateData>,
conn: DbConn,
) -> JsonResult {
if org_id != headers.org_id {
err!("Organization not found", "Organization id's do not match");
}
let data: OrganizationCollectionManagementUpdateData = data.into_inner();
let Some(mut org) = Organization::find_by_uuid(&org_id, &conn).await else {
err!("Organization not found")
};
if let Some(flag) = data.allow_admin_access_to_all_collection_items {
org.allow_admin_access_to_all_collection_items = flag;
};
if let Some(flag) = data.limit_collection_creation {
org.limit_collection_creation = flag;
};
if let Some(flag) = data.limit_collection_deletion {
org.limit_collection_deletion = flag;
};
if let Some(flag) = data.limit_item_deletion {
org.limit_item_deletion = flag;
};
org.save(&conn).await?;
log_event(
EventType::OrganizationUpdated as i32,
org_id.as_ref(),
&org_id,
&headers.user.uuid,
headers.device.atype,
&headers.ip.ip,
&conn,
)
.await;
Ok(Json(org.to_json()))
}
// GET /api/collections?writeOnly=false
#[get("/collections")]
async fn get_user_collections(headers: Headers, conn: DbConn) -> Json<Value> {

23
src/db/models/cipher.rs

@ -25,7 +25,7 @@ use macros::UuidFromParam;
use super::{
Archive, Attachment, CollectionCipher, CollectionId, Favorite, FolderCipher, FolderId, Group, Membership,
MembershipStatus, MembershipType, OrganizationId, User, UserId,
MembershipStatus, MembershipType, Organization, OrganizationId, User, UserId,
};
#[derive(Identifiable, Queryable, Insertable, AsChangeset)]
@ -187,6 +187,8 @@ impl Cipher {
(false, false, false)
};
let can_delete = self.is_deletable_by_user(user_uuid, conn).await;
let fields_json: Vec<_> = self
.fields
.as_ref()
@ -393,8 +395,8 @@ impl Cipher {
json_object["viewPassword"] = json!(!hide_passwords);
// The new key used by clients since v2025.6.0
json_object["permissions"] = json!({
"delete": !read_only,
"restore": !read_only,
"delete": can_delete && self.deleted_at.is_none(),
"restore": can_delete && self.deleted_at.is_none(),
});
}
@ -723,6 +725,21 @@ impl Cipher {
}
}
pub async fn is_deletable_by_user(&self, user_uuid: &UserId, conn: &DbConn) -> bool {
let manage_required = match self.organization_uuid {
Some(ref org_id) => match Organization::find_by_uuid(&org_id, &conn).await {
Some(org) => org.limit_item_deletion,
None => false,
},
None => false,
};
match self.get_access_restrictions(user_uuid, None, conn).await {
Some((read_only, _hide_passwords, manage)) => (!manage_required && !read_only) || manage,
None => false,
}
}
// 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: &DbConn) -> bool {

27
src/db/models/organization.rs

@ -38,6 +38,11 @@ pub struct Organization {
pub billing_email: String,
pub private_key: Option<String>,
pub public_key: Option<String>,
pub allow_admin_access_to_all_collection_items: bool,
pub limit_collection_creation: bool,
pub limit_collection_deletion: bool,
pub limit_item_deletion: bool,
}
#[derive(Identifiable, Queryable, Insertable, AsChangeset)]
@ -193,6 +198,10 @@ impl Organization {
billing_email,
private_key,
public_key,
allow_admin_access_to_all_collection_items: true,
limit_collection_creation: false,
limit_collection_deletion: false,
limit_item_deletion: false,
}
}
// https://github.com/bitwarden/server/blob/9ebe16587175b1c0e9208f84397bb75d0d595510/src/Api/AdminConsole/Models/Response/Organizations/OrganizationResponseModel.cs
@ -219,9 +228,10 @@ impl Organization {
"useApi": true,
"hasPublicAndPrivateKeys": self.private_key.is_some() && self.public_key.is_some(),
"useResetPassword": CONFIG.mail_enabled(),
"allowAdminAccessToAllCollectionItems": true,
"limitCollectionCreation": true,
"limitCollectionDeletion": true,
"allowAdminAccessToAllCollectionItems": self.allow_admin_access_to_all_collection_items,
"limitCollectionCreation": self.limit_collection_creation,
"limitCollectionDeletion": self.limit_collection_deletion,
"limitItemDeletion": self.limit_item_deletion,
"businessName": self.name,
"businessAddress1": null,
@ -509,11 +519,12 @@ impl Membership {
"familySponsorshipValidUntil": null,
"familySponsorshipToDelete": null,
"accessSecretsManager": false,
// limit collection creation to managers with access_all permission to prevent issues
"limitCollectionCreation": self.atype < MembershipType::Manager || !self.access_all,
"limitCollectionDeletion": true,
"limitItemDeletion": false,
"allowAdminAccessToAllCollectionItems": true,
"allowAdminAccessToAllCollectionItems": org.allow_admin_access_to_all_collection_items,
"limitCollectionCreation": org.limit_collection_creation,
"limitCollectionDeletion": org.limit_collection_deletion,
"limitItemDeletion": org.limit_item_deletion,
"userIsManagedByOrganization": false, // Means not managed via the Members UI, like SSO
"userIsClaimedByOrganization": false, // The new key instead of the obsolete userIsManagedByOrganization

4
src/db/schema.rs

@ -126,6 +126,10 @@ table! {
billing_email -> Text,
private_key -> Nullable<Text>,
public_key -> Nullable<Text>,
allow_admin_access_to_all_collection_items -> Bool,
limit_collection_creation -> Bool,
limit_collection_deletion -> Bool,
limit_item_deletion -> Bool,
}
}

5
src/static/templates/scss/vaultwarden.scss.hbs

@ -100,11 +100,6 @@ app-organization-plans > form > bit-section:nth-child(2) {
@extend %vw-hide;
}
/* Hide Collection Management Form */
app-org-account form.ng-untouched:nth-child(5) {
@extend %vw-hide;
}
/* Hide 'Member Access' Report Card from Org Reports */
app-org-reports-home > app-report-list > div.tw-inline-grid > div:nth-child(6) {
@extend %vw-hide;

Loading…
Cancel
Save