1
0
mirror of https://github.com/bitwarden/browser synced 2025-12-16 00:03:56 +00:00

[PM-22100] Enforce restrictions based on collection type (#15336)

* enforce restrictions based on collection type, set default collection type

* fix ts strict errors

* fix default collection enforcement in vault header

* enforce default collection restrictions in vault collection row

* enforce default collection restrictions in AC vault header

* enforce default collection restriction for select all

* fix ts strict error

* switch to signal, fix feature flag

* fix story

* clean up

* remove feature flag, move check for defaultCollecion to CollecitonView

* fix test

* remove unused configService

* fix test: coerce null to undefined for collection Id

* clean up leaky abstraction for default collection

* fix ts-strict error

* fix parens

* rename defaultCollection getter

* clean up
This commit is contained in:
Brandon Treston
2025-07-18 10:53:12 -04:00
committed by GitHub
parent 8811ec41ab
commit 92bbe0a3c2
14 changed files with 182 additions and 148 deletions

View File

@@ -1,5 +1,3 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
import { CollectionAccessSelectionView } from "./collection-access-selection.view";
@@ -16,12 +14,12 @@ export class CollectionAdminView extends CollectionView {
* Flag indicating the collection has no active user or group assigned to it with CanManage permissions
* In this case, the collection can be managed by admins/owners or custom users with appropriate permissions
*/
unmanaged: boolean;
unmanaged: boolean = false;
/**
* Flag indicating the user has been explicitly assigned to this Collection
*/
assigned: boolean;
assigned: boolean = false;
constructor(response?: CollectionAccessDetailsResponse) {
super(response);
@@ -45,6 +43,10 @@ export class CollectionAdminView extends CollectionView {
* Returns true if the user can edit a collection (including user and group access) from the Admin Console.
*/
override canEdit(org: Organization): boolean {
if (this.isDefaultCollection) {
return false;
}
return (
org?.canEditAnyCollection ||
(this.unmanaged && org?.canEditUnmanagedCollections) ||
@@ -56,6 +58,10 @@ export class CollectionAdminView extends CollectionView {
* Returns true if the user can delete a collection from the Admin Console.
*/
override canDelete(org: Organization): boolean {
if (this.isDefaultCollection) {
return false;
}
return org?.canDeleteAnyCollection || super.canDelete(org);
}
@@ -63,6 +69,10 @@ export class CollectionAdminView extends CollectionView {
* Whether the user can modify user access to this collection
*/
canEditUserAccess(org: Organization): boolean {
if (this.isDefaultCollection) {
return false;
}
return (
(org.permissions.manageUsers && org.allowAdminAccessToAllCollectionItems) || this.canEdit(org)
);
@@ -72,6 +82,10 @@ export class CollectionAdminView extends CollectionView {
* Whether the user can modify group access to this collection
*/
canEditGroupAccess(org: Organization): boolean {
if (this.isDefaultCollection) {
return false;
}
return (
(org.permissions.manageGroups && org.allowAdminAccessToAllCollectionItems) ||
this.canEdit(org)
@@ -82,11 +96,13 @@ export class CollectionAdminView extends CollectionView {
* Returns true if the user can view collection info and access in a read-only state from the Admin Console
*/
override canViewCollectionInfo(org: Organization | undefined): boolean {
if (this.isUnassignedCollection) {
if (this.isUnassignedCollection || this.isDefaultCollection) {
return false;
}
const isAdmin = org?.isAdmin ?? false;
const permissions = org?.permissions.editAnyCollection ?? false;
return this.manage || org?.isAdmin || org?.permissions.editAnyCollection;
return this.manage || isAdmin || permissions;
}
/**

View File

@@ -1,27 +1,25 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { Jsonify } from "type-fest";
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
import { View } from "@bitwarden/common/models/view/view";
import { ITreeNodeObject } from "@bitwarden/common/vault/models/domain/tree-node";
import { Collection, CollectionType } from "./collection";
import { Collection, CollectionType, CollectionTypes } from "./collection";
import { CollectionAccessDetailsResponse } from "./collection.response";
export const NestingDelimiter = "/";
export class CollectionView implements View, ITreeNodeObject {
id: string = null;
organizationId: string = null;
name: string = null;
externalId: string = null;
id: string | undefined;
organizationId: string | undefined;
name: string | undefined;
externalId: string | undefined;
// readOnly applies to the items within a collection
readOnly: boolean = null;
hidePasswords: boolean = null;
manage: boolean = null;
assigned: boolean = null;
type: CollectionType = null;
readOnly: boolean = false;
hidePasswords: boolean = false;
manage: boolean = false;
assigned: boolean = false;
type: CollectionType = CollectionTypes.SharedCollection;
constructor(c?: Collection | CollectionAccessDetailsResponse) {
if (!c) {
@@ -57,7 +55,11 @@ export class CollectionView implements View, ITreeNodeObject {
* Returns true if the user can edit a collection (including user and group access) from the individual vault.
* Does not include admin permissions - see {@link CollectionAdminView.canEdit}.
*/
canEdit(org: Organization): boolean {
canEdit(org: Organization | undefined): boolean {
if (this.isDefaultCollection) {
return false;
}
if (org != null && org.id !== this.organizationId) {
throw new Error(
"Id of the organization provided does not match the org id of the collection.",
@@ -71,7 +73,7 @@ export class CollectionView implements View, ITreeNodeObject {
* Returns true if the user can delete a collection from the individual vault.
* Does not include admin permissions - see {@link CollectionAdminView.canDelete}.
*/
canDelete(org: Organization): boolean {
canDelete(org: Organization | undefined): boolean {
if (org != null && org.id !== this.organizationId) {
throw new Error(
"Id of the organization provided does not match the org id of the collection.",
@@ -81,7 +83,7 @@ export class CollectionView implements View, ITreeNodeObject {
const canDeleteManagedCollections = !org?.limitCollectionDeletion || org.isAdmin;
// Only use individual permissions, not admin permissions
return canDeleteManagedCollections && this.manage;
return canDeleteManagedCollections && this.manage && !this.isDefaultCollection;
}
/**
@@ -94,4 +96,8 @@ export class CollectionView implements View, ITreeNodeObject {
static fromJSON(obj: Jsonify<CollectionView>) {
return Object.assign(new CollectionView(new Collection()), obj);
}
get isDefaultCollection() {
return this.type == CollectionTypes.DefaultUserCollection;
}
}

View File

@@ -42,7 +42,7 @@ export class EmptyVaultNudgeService extends DefaultSingleNudgeService {
const orgIds = new Set(orgs.map((org) => org.id));
const canCreateCollections = orgs.some((org) => org.canCreateNewCollections);
const hasManageCollections = collections.some(
(c) => c.manage && orgIds.has(c.organizationId),
(c) => c.manage && orgIds.has(c.organizationId!),
);
// When the user has dismissed the nudge or spotlight, return the nudge status directly

View File

@@ -46,7 +46,7 @@ export class VaultSettingsImportNudgeService extends DefaultSingleNudgeService {
const orgIds = new Set(orgs.map((org) => org.id));
const canCreateCollections = orgs.some((org) => org.canCreateNewCollections);
const hasManageCollections = collections.some(
(c) => c.manage && orgIds.has(c.organizationId),
(c) => c.manage && orgIds.has(c.organizationId!),
);
// When the user has dismissed the nudge or spotlight, return the nudge status directly

View File

@@ -191,6 +191,9 @@ export function sortDefaultCollections(
.sort((a, b) => {
const aName = orgs.find((o) => o.id === a.organizationId)?.name ?? a.organizationId;
const bName = orgs.find((o) => o.id === b.organizationId)?.name ?? b.organizationId;
if (!aName || !bName) {
throw new Error("Collection does not have an organizationId.");
}
return collator.compare(aName, bName);
});
return [

View File

@@ -16,6 +16,6 @@ export class TreeNode<T extends ITreeNodeObject> {
}
export interface ITreeNodeObject {
id: string;
name: string;
id: string | undefined;
name: string | undefined;
}

View File

@@ -279,7 +279,7 @@ export abstract class BaseImporter {
result.collections = result.folders.map((f) => {
const collection = new CollectionView();
collection.name = f.name;
collection.id = f.id;
collection.id = f.id ?? undefined; // folder id may be null, which is not suitable for collections.
return collection;
});
result.folderRelationships = [];

View File

@@ -31,7 +31,7 @@ const createMockCollection = (
organizationId: string,
readOnly = false,
canEdit = true,
) => {
): CollectionView => {
return {
id,
name,
@@ -42,6 +42,7 @@ const createMockCollection = (
manage: true,
assigned: true,
type: CollectionTypes.DefaultUserCollection,
isDefaultCollection: true,
canEditItems: jest.fn().mockReturnValue(canEdit),
canEdit: jest.fn(),
canDelete: jest.fn(),