mirror of
https://github.com/bitwarden/browser
synced 2025-12-14 15:23:33 +00:00
[AC-1373] Flexible Collections (#6336)
* [AC-1117] Add manage permission (#5910) * Add 'manage' option to collection access permissions * Add 'manage' to collection permissions * remove service accidentally committed from another branch * Update CLI commands * update message casing to be consistent * access selector model updates * [AC-1374] Limit collection create/delete (#5963) * feat: udate request/response/data/domain models for new column, refs AC-1374 * feat: create collection management ui, refs AC-1374 * fix: remove limitCollectionCdOwnerAdmin boolean from org update request, refs AC-1374 * fix: moved collection management UI, removed comments, refs AC-1374 * fix: observable chaining now properly calls API when local org updated, refs AC-1374 * fix: remove unused form template variables, refs AC-1374 * fix: clean up observable chain, refs AC-1374 * fix: remove parent.parent route, refs AC-1374 * fix: add cd explaination, refs AC-1374 * [AC-1649] Remove organizationId from collection-bulk-delete.request (#6343) * refactor: remove organizationId from collection-bulk-delete-request, refs AC-1649 * refactor: remove request model from dialog component, refs AC-1649 * [AC-1174] Bulk collection management (#6133) * [AC-1174] Add bulk edit collection access event type * [AC-1174] Add bulk edit collection access menu option * [AC-1174] Add initial bulk collections access dialog * [AC-1174] Add logic to open bulk edit collections dialog * [AC-1174] Move AccessItemView helper methods to access selector model to be shared * [AC-1174] Add access selector to bulk collections dialog * [AC-1174] Add bulk assign access method to collection-admin service * [AC-1174] Introduce strongly typed BulkCollectionAccessRequest model * [AC-1174] Update vault item event type name * Update DialogService dependency --------- Co-authored-by: Thomas Rittson <trittson@bitwarden.com> * Rename LimitCollectionCdOwnerAdmin -> LimitCollectionCreationDeletion (#6409) * Add manage property to synced Collection data * Revert "Add manage property to synced Collection data" Pushed to feature branch instead of a new one This reverts commit65cd39589c. * Add manage property to synced Collection data * Revert "Add manage property to synced Collection data" This reverts commitf7fa30b79a. * [AC-1680] Add manage property to collection view and response models (#6417) * Add manage property to synced Collection data * Update tests * feat: add LimitCollectionCreationDeletion conditional to canCreateNewCollections logic, refs AC-1659 (#6429) * [AC-1669] Enforce Can Manage permission on Collection dialog (#6493) * [AC-1669] Cleanup unhandled promise warnings * [AC-1669] Force change detection to ensure AccessSelector has the most recent items * [AC-1669] Initially select acting member when creating a new collection * [AC-1669] Add validator to ensure manage permission is selected * [AC-1669] Update error toast logic to support access tab errors * [AC-1669] Add error icon * [AC-1713] [Flexible collections] Add feature flags to clients (#6486) * Add FlexibleCollections and BulkCollectionAccess flags * Flag Collection Management settings * Flag bulk collection access dialog * Flag collection access modal changes * [AC-1662] Add LimitCollecitonCreationDeletion conditional to CanDelete logic (#6526) * feat: implement limitCollectionCreationDeletion into canDelete logic, refs AC-1662 * feat: make canDelete functions backwards compatible with feature flag, refs AC-1662 * feat: update vault-items.component for async getter, refs AC-1662 * feat: update configService injection, refs AC-1662 * feat: add config service to canDelete reference, refs AC-1662 * fix: remove configservice dependency from views, refs AC-1757 (#6686) * Add missing provider to vault-items.stories (#6690) * Fix imports after update from master --------- Co-authored-by: Robyn MacCallum <robyntmaccallum@gmail.com> Co-authored-by: Vincent Salucci <26154748+vincentsalucci@users.noreply.github.com> Co-authored-by: Vincent Salucci <vincesalucci21@gmail.com> Co-authored-by: Shane Melton <smelton@bitwarden.com>
This commit is contained in:
@@ -99,7 +99,6 @@ import { PlanResponse } from "../billing/models/response/plan.response";
|
||||
import { SubscriptionResponse } from "../billing/models/response/subscription.response";
|
||||
import { TaxInfoResponse } from "../billing/models/response/tax-info.response";
|
||||
import { TaxRateResponse } from "../billing/models/response/tax-rate.response";
|
||||
import { CollectionBulkDeleteRequest } from "../models/request/collection-bulk-delete.request";
|
||||
import { DeleteRecoverRequest } from "../models/request/delete-recover.request";
|
||||
import { EventRequest } from "../models/request/event.request";
|
||||
import { IapCheckRequest } from "../models/request/iap-check.request";
|
||||
@@ -301,7 +300,7 @@ export abstract class ApiService {
|
||||
request: CollectionRequest
|
||||
) => Promise<CollectionResponse>;
|
||||
deleteCollection: (organizationId: string, id: string) => Promise<any>;
|
||||
deleteManyCollections: (request: CollectionBulkDeleteRequest) => Promise<any>;
|
||||
deleteManyCollections: (organizationId: string, collectionIds: string[]) => Promise<any>;
|
||||
deleteCollectionUser: (
|
||||
organizationId: string,
|
||||
id: string,
|
||||
|
||||
@@ -18,6 +18,7 @@ import { StorageRequest } from "../../../models/request/storage.request";
|
||||
import { VerifyBankRequest } from "../../../models/request/verify-bank.request";
|
||||
import { ListResponse } from "../../../models/response/list.response";
|
||||
import { OrganizationApiKeyType } from "../../enums";
|
||||
import { OrganizationCollectionManagementUpdateRequest } from "../../models/request/organization-collection-management-update.request";
|
||||
import { OrganizationCreateRequest } from "../../models/request/organization-create.request";
|
||||
import { OrganizationKeysRequest } from "../../models/request/organization-keys.request";
|
||||
import { OrganizationUpdateRequest } from "../../models/request/organization-update.request";
|
||||
@@ -73,4 +74,8 @@ export class OrganizationApiServiceAbstraction {
|
||||
id: string,
|
||||
request: SecretsManagerSubscribeRequest
|
||||
) => Promise<ProfileOrganizationResponse>;
|
||||
updateCollectionManagement: (
|
||||
id: string,
|
||||
request: OrganizationCollectionManagementUpdateRequest
|
||||
) => Promise<OrganizationResponse>;
|
||||
}
|
||||
|
||||
@@ -49,6 +49,7 @@ export class OrganizationData {
|
||||
familySponsorshipValidUntil?: Date;
|
||||
familySponsorshipToDelete?: boolean;
|
||||
accessSecretsManager: boolean;
|
||||
limitCollectionCreationDeletion: boolean;
|
||||
|
||||
constructor(
|
||||
response: ProfileOrganizationResponse,
|
||||
@@ -100,6 +101,7 @@ export class OrganizationData {
|
||||
this.familySponsorshipValidUntil = response.familySponsorshipValidUntil;
|
||||
this.familySponsorshipToDelete = response.familySponsorshipToDelete;
|
||||
this.accessSecretsManager = response.accessSecretsManager;
|
||||
this.limitCollectionCreationDeletion = response.limitCollectionCreationDeletion;
|
||||
|
||||
this.isMember = options.isMember;
|
||||
this.isProviderUser = options.isProviderUser;
|
||||
|
||||
@@ -64,6 +64,10 @@ export class Organization {
|
||||
familySponsorshipValidUntil?: Date;
|
||||
familySponsorshipToDelete?: boolean;
|
||||
accessSecretsManager: boolean;
|
||||
/**
|
||||
* Refers to the ability for an organization to limit collection creation and deletion to owners and admins only
|
||||
*/
|
||||
limitCollectionCreationDeletion: boolean;
|
||||
|
||||
constructor(obj?: OrganizationData) {
|
||||
if (obj == null) {
|
||||
@@ -115,6 +119,7 @@ export class Organization {
|
||||
this.familySponsorshipValidUntil = obj.familySponsorshipValidUntil;
|
||||
this.familySponsorshipToDelete = obj.familySponsorshipToDelete;
|
||||
this.accessSecretsManager = obj.accessSecretsManager;
|
||||
this.limitCollectionCreationDeletion = obj.limitCollectionCreationDeletion;
|
||||
}
|
||||
|
||||
get canAccess() {
|
||||
@@ -158,7 +163,9 @@ export class Organization {
|
||||
}
|
||||
|
||||
get canCreateNewCollections() {
|
||||
return this.isManager || this.permissions.createNewCollections;
|
||||
return (
|
||||
!this.limitCollectionCreationDeletion || this.isAdmin || this.permissions.createNewCollections
|
||||
);
|
||||
}
|
||||
|
||||
get canEditAnyCollection() {
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
export class OrganizationCollectionManagementUpdateRequest {
|
||||
limitCreateDeleteOwnerAdmin: boolean;
|
||||
}
|
||||
@@ -2,10 +2,12 @@ export class SelectionReadOnlyRequest {
|
||||
id: string;
|
||||
readOnly: boolean;
|
||||
hidePasswords: boolean;
|
||||
manage: boolean;
|
||||
|
||||
constructor(id: string, readOnly: boolean, hidePasswords: boolean) {
|
||||
constructor(id: string, readOnly: boolean, hidePasswords: boolean, manage: boolean) {
|
||||
this.id = id;
|
||||
this.readOnly = readOnly;
|
||||
this.hidePasswords = hidePasswords;
|
||||
this.manage = manage;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -32,6 +32,7 @@ export class OrganizationResponse extends BaseResponse {
|
||||
smServiceAccounts?: number;
|
||||
maxAutoscaleSmSeats?: number;
|
||||
maxAutoscaleSmServiceAccounts?: number;
|
||||
limitCollectionCreationDeletion: boolean;
|
||||
|
||||
constructor(response: any) {
|
||||
super(response);
|
||||
@@ -67,5 +68,8 @@ export class OrganizationResponse extends BaseResponse {
|
||||
this.smServiceAccounts = this.getResponseProperty("SmServiceAccounts");
|
||||
this.maxAutoscaleSmSeats = this.getResponseProperty("MaxAutoscaleSmSeats");
|
||||
this.maxAutoscaleSmServiceAccounts = this.getResponseProperty("MaxAutoscaleSmServiceAccounts");
|
||||
this.limitCollectionCreationDeletion = this.getResponseProperty(
|
||||
"LimitCollectionCreationDeletion"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -48,6 +48,7 @@ export class ProfileOrganizationResponse extends BaseResponse {
|
||||
familySponsorshipValidUntil?: Date;
|
||||
familySponsorshipToDelete?: boolean;
|
||||
accessSecretsManager: boolean;
|
||||
limitCollectionCreationDeletion: boolean;
|
||||
|
||||
constructor(response: any) {
|
||||
super(response);
|
||||
@@ -105,5 +106,8 @@ export class ProfileOrganizationResponse extends BaseResponse {
|
||||
}
|
||||
this.familySponsorshipToDelete = this.getResponseProperty("FamilySponsorshipToDelete");
|
||||
this.accessSecretsManager = this.getResponseProperty("AccessSecretsManager");
|
||||
this.limitCollectionCreationDeletion = this.getResponseProperty(
|
||||
"LimitCollectionCreationDeletion"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,11 +4,13 @@ export class SelectionReadOnlyResponse extends BaseResponse {
|
||||
id: string;
|
||||
readOnly: boolean;
|
||||
hidePasswords: boolean;
|
||||
manage: boolean;
|
||||
|
||||
constructor(response: any) {
|
||||
super(response);
|
||||
this.id = this.getResponseProperty("Id");
|
||||
this.readOnly = this.getResponseProperty("ReadOnly");
|
||||
this.hidePasswords = this.getResponseProperty("HidePasswords");
|
||||
this.manage = this.getResponseProperty("Manage");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,6 +21,7 @@ import { ListResponse } from "../../../models/response/list.response";
|
||||
import { SyncService } from "../../../vault/abstractions/sync/sync.service.abstraction";
|
||||
import { OrganizationApiServiceAbstraction } from "../../abstractions/organization/organization-api.service.abstraction";
|
||||
import { OrganizationApiKeyType } from "../../enums";
|
||||
import { OrganizationCollectionManagementUpdateRequest } from "../../models/request/organization-collection-management-update.request";
|
||||
import { OrganizationCreateRequest } from "../../models/request/organization-create.request";
|
||||
import { OrganizationKeysRequest } from "../../models/request/organization-keys.request";
|
||||
import { OrganizationUpdateRequest } from "../../models/request/organization-update.request";
|
||||
@@ -322,4 +323,20 @@ export class OrganizationApiService implements OrganizationApiServiceAbstraction
|
||||
);
|
||||
return new ProfileOrganizationResponse(r);
|
||||
}
|
||||
|
||||
async updateCollectionManagement(
|
||||
id: string,
|
||||
request: OrganizationCollectionManagementUpdateRequest
|
||||
): Promise<OrganizationResponse> {
|
||||
const r = await this.apiService.send(
|
||||
"PUT",
|
||||
"/organizations/" + id + "/collection-management",
|
||||
request,
|
||||
true,
|
||||
true
|
||||
);
|
||||
const data = new OrganizationResponse(r);
|
||||
await this.syncService.fullSync(true);
|
||||
return data;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,6 +7,8 @@ export enum FeatureFlag {
|
||||
AutofillV2 = "autofill-v2",
|
||||
BrowserFilelessImport = "browser-fileless-import",
|
||||
ItemShare = "item-share",
|
||||
FlexibleCollections = "flexible-collections",
|
||||
BulkCollectionAccess = "bulk-collection-access",
|
||||
}
|
||||
|
||||
// Replace this with a type safe lookup of the feature flag values in PM-2282
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
export class CollectionBulkDeleteRequest {
|
||||
ids: string[];
|
||||
organizationId: string;
|
||||
|
||||
constructor(ids: string[], organizationId?: string) {
|
||||
constructor(ids: string[]) {
|
||||
this.ids = ids == null ? [] : ids;
|
||||
this.organizationId = organizationId;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -834,11 +834,11 @@ export class ApiService implements ApiServiceAbstraction {
|
||||
);
|
||||
}
|
||||
|
||||
deleteManyCollections(request: CollectionBulkDeleteRequest): Promise<any> {
|
||||
deleteManyCollections(organizationId: string, collectionIds: string[]): Promise<any> {
|
||||
return this.send(
|
||||
"DELETE",
|
||||
"/organizations/" + request.organizationId + "/collections",
|
||||
request,
|
||||
"/organizations/" + organizationId + "/collections",
|
||||
new CollectionBulkDeleteRequest(collectionIds),
|
||||
true,
|
||||
false
|
||||
);
|
||||
|
||||
@@ -6,6 +6,7 @@ export class CollectionData {
|
||||
name: string;
|
||||
externalId: string;
|
||||
readOnly: boolean;
|
||||
manage: boolean;
|
||||
hidePasswords: boolean;
|
||||
|
||||
constructor(response: CollectionDetailsResponse) {
|
||||
@@ -14,6 +15,7 @@ export class CollectionData {
|
||||
this.name = response.name;
|
||||
this.externalId = response.externalId;
|
||||
this.readOnly = response.readOnly;
|
||||
this.manage = response.manage;
|
||||
this.hidePasswords = response.hidePasswords;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@ describe("Collection", () => {
|
||||
name: "encName",
|
||||
externalId: "extId",
|
||||
readOnly: true,
|
||||
manage: true,
|
||||
hidePasswords: true,
|
||||
};
|
||||
});
|
||||
@@ -28,6 +29,7 @@ describe("Collection", () => {
|
||||
name: null,
|
||||
organizationId: null,
|
||||
readOnly: null,
|
||||
manage: null,
|
||||
});
|
||||
});
|
||||
|
||||
@@ -40,6 +42,7 @@ describe("Collection", () => {
|
||||
name: { encryptedString: "encName", encryptionType: 0 },
|
||||
externalId: "extId",
|
||||
readOnly: true,
|
||||
manage: true,
|
||||
hidePasswords: true,
|
||||
});
|
||||
});
|
||||
@@ -52,6 +55,7 @@ describe("Collection", () => {
|
||||
collection.externalId = "extId";
|
||||
collection.readOnly = false;
|
||||
collection.hidePasswords = false;
|
||||
collection.manage = true;
|
||||
|
||||
const view = await collection.decrypt();
|
||||
|
||||
@@ -62,6 +66,7 @@ describe("Collection", () => {
|
||||
name: "encName",
|
||||
organizationId: "orgId",
|
||||
readOnly: false,
|
||||
manage: true,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -10,6 +10,7 @@ export class Collection extends Domain {
|
||||
externalId: string;
|
||||
readOnly: boolean;
|
||||
hidePasswords: boolean;
|
||||
manage: boolean;
|
||||
|
||||
constructor(obj?: CollectionData) {
|
||||
super();
|
||||
@@ -27,8 +28,9 @@ export class Collection extends Domain {
|
||||
externalId: null,
|
||||
readOnly: null,
|
||||
hidePasswords: null,
|
||||
manage: null,
|
||||
},
|
||||
["id", "organizationId", "externalId", "readOnly", "hidePasswords"]
|
||||
["id", "organizationId", "externalId", "readOnly", "hidePasswords", "manage"]
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -18,11 +18,13 @@ export class CollectionResponse extends BaseResponse {
|
||||
|
||||
export class CollectionDetailsResponse extends CollectionResponse {
|
||||
readOnly: boolean;
|
||||
manage: boolean;
|
||||
hidePasswords: boolean;
|
||||
|
||||
constructor(response: any) {
|
||||
super(response);
|
||||
this.readOnly = this.getResponseProperty("ReadOnly") || false;
|
||||
this.manage = this.getResponseProperty("Manage") || false;
|
||||
this.hidePasswords = this.getResponseProperty("HidePasswords") || false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@ export class CollectionView implements View, ITreeNodeObject {
|
||||
// readOnly applies to the items within a collection
|
||||
readOnly: boolean = null;
|
||||
hidePasswords: boolean = null;
|
||||
manage: boolean = null;
|
||||
|
||||
constructor(c?: Collection | CollectionAccessDetailsResponse) {
|
||||
if (!c) {
|
||||
@@ -26,6 +27,7 @@ export class CollectionView implements View, ITreeNodeObject {
|
||||
if (c instanceof Collection) {
|
||||
this.readOnly = c.readOnly;
|
||||
this.hidePasswords = c.hidePasswords;
|
||||
this.manage = c.manage;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -40,12 +42,17 @@ export class CollectionView implements View, ITreeNodeObject {
|
||||
}
|
||||
|
||||
// For deleting a collection, not the items within it.
|
||||
canDelete(org: Organization): boolean {
|
||||
canDelete(org: Organization, flexibleCollectionsEnabled: boolean): boolean {
|
||||
if (org.id !== this.organizationId) {
|
||||
throw new Error(
|
||||
"Id of the organization provided does not match the org id of the collection."
|
||||
);
|
||||
}
|
||||
return org?.canDeleteAnyCollection || org?.canDeleteAssignedCollections;
|
||||
|
||||
if (flexibleCollectionsEnabled) {
|
||||
return org?.canDeleteAnyCollection || (!org?.limitCollectionCreationDeletion && this.manage);
|
||||
} else {
|
||||
return org?.canDeleteAnyCollection || org?.canDeleteAssignedCollections;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user