diff --git a/migrations/mysql/2026-07-21-120000_add_collection_management/down.sql b/migrations/mysql/2026-07-21-120000_add_collection_management/down.sql new file mode 100644 index 00000000..300a4665 --- /dev/null +++ b/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; \ No newline at end of file diff --git a/migrations/mysql/2026-07-21-120000_add_collection_management/up.sql b/migrations/mysql/2026-07-21-120000_add_collection_management/up.sql new file mode 100644 index 00000000..276752b8 --- /dev/null +++ b/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; \ No newline at end of file diff --git a/migrations/postgresql/2026-07-21-120000_add_collection_management/down.sql b/migrations/postgresql/2026-07-21-120000_add_collection_management/down.sql new file mode 100644 index 00000000..300a4665 --- /dev/null +++ b/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; \ No newline at end of file diff --git a/migrations/postgresql/2026-07-21-120000_add_collection_management/up.sql b/migrations/postgresql/2026-07-21-120000_add_collection_management/up.sql new file mode 100644 index 00000000..276752b8 --- /dev/null +++ b/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; \ No newline at end of file diff --git a/migrations/sqlite/2026-07-21-120000_add_collection_management/down.sql b/migrations/sqlite/2026-07-21-120000_add_collection_management/down.sql new file mode 100644 index 00000000..300a4665 --- /dev/null +++ b/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; \ No newline at end of file diff --git a/migrations/sqlite/2026-07-21-120000_add_collection_management/up.sql b/migrations/sqlite/2026-07-21-120000_add_collection_management/up.sql new file mode 100644 index 00000000..276752b8 --- /dev/null +++ b/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; \ No newline at end of file diff --git a/src/api/core/organizations.rs b/src/api/core/organizations.rs index c7e79aed..2cfbb3b4 100644 --- a/src/api/core/organizations.rs +++ b/src/api/core/organizations.rs @@ -39,6 +39,7 @@ pub fn routes() -> Vec { 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, + limit_collection_creation: Option, + limit_collection_deletion: Option, + limit_item_deletion: Option, +} + #[derive(Deserialize)] #[serde(rename_all = "camelCase")] struct FullCollectionData { @@ -340,6 +350,52 @@ async fn post_organization( Ok(Json(org.to_json())) } +#[put("/organizations//collection-management", data = "")] +async fn put_organization_collection_management( + org_id: OrganizationId, + headers: OwnerHeaders, + data: Json, + 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 { diff --git a/src/db/models/organization.rs b/src/db/models/organization.rs index 72b1df0b..56cdd340 100644 --- a/src/db/models/organization.rs +++ b/src/db/models/organization.rs @@ -38,6 +38,11 @@ pub struct Organization { pub billing_email: String, pub private_key: Option, pub public_key: Option, + + 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 diff --git a/src/db/schema.rs b/src/db/schema.rs index af342186..c146b23d 100644 --- a/src/db/schema.rs +++ b/src/db/schema.rs @@ -126,6 +126,10 @@ table! { billing_email -> Text, private_key -> Nullable, public_key -> Nullable, + allow_admin_access_to_all_collection_items -> Bool, + limit_collection_creation -> Bool, + limit_collection_deletion -> Bool, + limit_item_deletion -> Bool, } } diff --git a/src/static/templates/scss/vaultwarden.scss.hbs b/src/static/templates/scss/vaultwarden.scss.hbs index 477cdd34..e7fbb3c8 100644 --- a/src/static/templates/scss/vaultwarden.scss.hbs +++ b/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;