1
0
mirror of https://github.com/bitwarden/browser.git synced 2024-12-21 16:18:28 +01:00

[AC-2320] Update canEditAnyCollection logic for Flexible Collections v1 (#8394)

* also update calling locations to use canEditAllCiphers where applicable
This commit is contained in:
Thomas Rittson 2024-04-04 13:48:41 +10:00 committed by GitHub
parent 678ba04781
commit 32981ce30d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
13 changed files with 140 additions and 44 deletions

View File

@ -45,6 +45,7 @@ export class VaultItemsComponent {
@Input() showBulkAddToCollections = false;
@Input() showPermissionsColumn = false;
@Input() viewingOrgVault: boolean;
@Input({ required: true }) flexibleCollectionsV1Enabled = false;
private _ciphers?: CipherView[] = [];
@Input() get ciphers(): CipherView[] {
@ -101,7 +102,7 @@ export class VaultItemsComponent {
}
const organization = this.allOrganizations.find((o) => o.id === collection.organizationId);
return collection.canEdit(organization);
return collection.canEdit(organization, this.flexibleCollectionsV1Enabled);
}
protected canDeleteCollection(collection: CollectionView): boolean {

View File

@ -31,10 +31,11 @@ export class CollectionAdminView extends CollectionView {
this.assigned = response.assigned;
}
override canEdit(org: Organization): boolean {
override canEdit(org: Organization, flexibleCollectionsV1Enabled: boolean): boolean {
return org?.flexibleCollections
? org?.canEditAnyCollection || this.manage
: org?.canEditAnyCollection || (org?.canEditAssignedCollections && this.assigned);
? org?.canEditAnyCollection(flexibleCollectionsV1Enabled) || this.manage
: org?.canEditAnyCollection(flexibleCollectionsV1Enabled) ||
(org?.canEditAssignedCollections && this.assigned);
}
override canDelete(org: Organization): boolean {

View File

@ -1,8 +1,11 @@
import { DialogConfig, DialogRef, DIALOG_DATA } from "@angular/cdk/dialog";
import { Component, Inject } from "@angular/core";
import { firstValueFrom } from "rxjs";
import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
@ -49,6 +52,11 @@ export class BulkDeleteDialogComponent {
organizations: Organization[];
collections: CollectionView[];
private flexibleCollectionsV1Enabled$ = this.configService.getFeatureFlag$(
FeatureFlag.FlexibleCollectionsV1,
false,
);
constructor(
@Inject(DIALOG_DATA) params: BulkDeleteDialogParams,
private dialogRef: DialogRef<BulkDeleteDialogResult>,
@ -57,6 +65,7 @@ export class BulkDeleteDialogComponent {
private i18nService: I18nService,
private apiService: ApiService,
private collectionService: CollectionService,
private configService: ConfigService,
) {
this.cipherIds = params.cipherIds ?? [];
this.permanent = params.permanent;
@ -72,7 +81,12 @@ export class BulkDeleteDialogComponent {
protected submit = async () => {
const deletePromises: Promise<void>[] = [];
if (this.cipherIds.length) {
if (!this.organization || !this.organization.canEditAnyCollection) {
const flexibleCollectionsV1Enabled = await firstValueFrom(this.flexibleCollectionsV1Enabled$);
if (
!this.organization ||
!this.organization.canEditAllCiphers(flexibleCollectionsV1Enabled)
) {
deletePromises.push(this.deleteCiphers());
} else {
deletePromises.push(this.deleteCiphersAdmin());
@ -104,7 +118,8 @@ export class BulkDeleteDialogComponent {
};
private async deleteCiphers(): Promise<any> {
const asAdmin = this.organization?.canEditAnyCollection;
const flexibleCollectionsV1Enabled = await firstValueFrom(this.flexibleCollectionsV1Enabled$);
const asAdmin = this.organization?.canEditAllCiphers(flexibleCollectionsV1Enabled);
if (this.permanent) {
await this.cipherService.deleteManyWithServer(this.cipherIds, asAdmin);
} else {

View File

@ -1,6 +1,16 @@
import { ChangeDetectionStrategy, Component, EventEmitter, Input, Output } from "@angular/core";
import {
ChangeDetectionStrategy,
Component,
EventEmitter,
Input,
OnInit,
Output,
} from "@angular/core";
import { firstValueFrom } from "rxjs";
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { TreeNode } from "@bitwarden/common/vault/models/domain/tree-node";
import { CollectionView } from "@bitwarden/common/vault/models/view/collection.view";
@ -17,7 +27,7 @@ import {
templateUrl: "./vault-header.component.html",
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class VaultHeaderComponent {
export class VaultHeaderComponent implements OnInit {
protected Unassigned = Unassigned;
protected All = All;
protected CollectionDialogTabType = CollectionDialogTabType;
@ -55,7 +65,18 @@ export class VaultHeaderComponent {
/** Emits an event when the delete collection button is clicked in the header */
@Output() onDeleteCollection = new EventEmitter<void>();
constructor(private i18nService: I18nService) {}
private flexibleCollectionsV1Enabled = false;
constructor(
private i18nService: I18nService,
private configService: ConfigService,
) {}
async ngOnInit() {
this.flexibleCollectionsV1Enabled = await firstValueFrom(
this.configService.getFeatureFlag$(FeatureFlag.FlexibleCollectionsV1),
);
}
/**
* The id of the organization that is currently being filtered on.
@ -137,7 +158,7 @@ export class VaultHeaderComponent {
const organization = this.organizations.find(
(o) => o.id === this.collection?.node.organizationId,
);
return this.collection.node.canEdit(organization);
return this.collection.node.canEdit(organization, this.flexibleCollectionsV1Enabled);
}
async editCollection(tab: CollectionDialogTabType): Promise<void> {

View File

@ -50,6 +50,7 @@
[cloneableOrganizationCiphers]="false"
[showAdminActions]="false"
(onEvent)="onVaultItemsEvent($event)"
[flexibleCollectionsV1Enabled]="flexibleCollectionsV1Enabled$ | async"
>
</app-vault-items>
<div

View File

@ -39,6 +39,7 @@ import { TokenService } from "@bitwarden/common/auth/abstractions/token.service"
import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction";
import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service";
import { EventType } from "@bitwarden/common/enums";
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
import { BroadcasterService } from "@bitwarden/common/platform/abstractions/broadcaster.service";
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
@ -144,6 +145,10 @@ export class VaultComponent implements OnInit, OnDestroy {
protected selectedCollection: TreeNode<CollectionView> | undefined;
protected canCreateCollections = false;
protected currentSearchText$: Observable<string>;
protected flexibleCollectionsV1Enabled$ = this.configService.getFeatureFlag$(
FeatureFlag.FlexibleCollectionsV1,
false,
);
private searchText$ = new Subject<string>();
private refresh$ = new BehaviorSubject<void>(null);

View File

@ -1,8 +1,11 @@
import { Component } from "@angular/core";
import { Component, OnInit } from "@angular/core";
import { firstValueFrom } from "rxjs";
import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service";
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service";
import { FileDownloadService } from "@bitwarden/common/platform/abstractions/file-download/file-download.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
@ -21,10 +24,12 @@ import { AttachmentsComponent as BaseAttachmentsComponent } from "../individual-
selector: "app-org-vault-attachments",
templateUrl: "../individual-vault/attachments.component.html",
})
export class AttachmentsComponent extends BaseAttachmentsComponent {
export class AttachmentsComponent extends BaseAttachmentsComponent implements OnInit {
viewOnly = false;
organization: Organization;
private flexibleCollectionsV1Enabled = false;
constructor(
cipherService: CipherService,
i18nService: I18nService,
@ -36,6 +41,7 @@ export class AttachmentsComponent extends BaseAttachmentsComponent {
fileDownloadService: FileDownloadService,
dialogService: DialogService,
billingAccountProfileStateService: BillingAccountProfileStateService,
private configService: ConfigService,
) {
super(
cipherService,
@ -51,14 +57,24 @@ export class AttachmentsComponent extends BaseAttachmentsComponent {
);
}
async ngOnInit() {
await super.ngOnInit();
this.flexibleCollectionsV1Enabled = await firstValueFrom(
this.configService.getFeatureFlag$(FeatureFlag.FlexibleCollectionsV1, false),
);
}
protected async reupload(attachment: AttachmentView) {
if (this.organization.canEditAnyCollection && this.showFixOldAttachments(attachment)) {
if (
this.organization.canEditAllCiphers(this.flexibleCollectionsV1Enabled) &&
this.showFixOldAttachments(attachment)
) {
await super.reuploadCipherAttachment(attachment, true);
}
}
protected async loadCipher() {
if (!this.organization.canEditAnyCollection) {
if (!this.organization.canEditAllCiphers(this.flexibleCollectionsV1Enabled)) {
return await super.loadCipher();
}
const response = await this.apiService.getCipherAdmin(this.cipherId);
@ -69,18 +85,21 @@ export class AttachmentsComponent extends BaseAttachmentsComponent {
return this.cipherService.saveAttachmentWithServer(
this.cipherDomain,
file,
this.organization.canEditAnyCollection,
this.organization.canEditAllCiphers(this.flexibleCollectionsV1Enabled),
);
}
protected deleteCipherAttachment(attachmentId: string) {
if (!this.organization.canEditAnyCollection) {
if (!this.organization.canEditAllCiphers(this.flexibleCollectionsV1Enabled)) {
return super.deleteCipherAttachment(attachmentId);
}
return this.apiService.deleteCipherAttachmentAdmin(this.cipherId, attachmentId);
}
protected showFixOldAttachments(attachment: AttachmentView) {
return attachment.key == null && this.organization.canEditAnyCollection;
return (
attachment.key == null &&
this.organization.canEditAllCiphers(this.flexibleCollectionsV1Enabled)
);
}
}

View File

@ -1,10 +1,12 @@
import { Component, EventEmitter, Input, Output } from "@angular/core";
import { Component, EventEmitter, Input, OnInit, Output } from "@angular/core";
import { Router } from "@angular/router";
import { firstValueFrom } from "rxjs";
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
import { ProductType } from "@bitwarden/common/enums";
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { TreeNode } from "@bitwarden/common/vault/models/domain/tree-node";
import { DialogService, SimpleDialogOptions } from "@bitwarden/components";
@ -22,7 +24,7 @@ import {
selector: "app-org-vault-header",
templateUrl: "./vault-header.component.html",
})
export class VaultHeaderComponent {
export class VaultHeaderComponent implements OnInit {
protected All = All;
protected Unassigned = Unassigned;
@ -56,14 +58,23 @@ export class VaultHeaderComponent {
protected CollectionDialogTabType = CollectionDialogTabType;
protected organizations$ = this.organizationService.organizations$;
private flexibleCollectionsV1Enabled = false;
constructor(
private organizationService: OrganizationService,
private i18nService: I18nService,
private dialogService: DialogService,
private collectionAdminService: CollectionAdminService,
private router: Router,
private configService: ConfigService,
) {}
async ngOnInit() {
this.flexibleCollectionsV1Enabled = await firstValueFrom(
this.configService.getFeatureFlag$(FeatureFlag.FlexibleCollectionsV1),
);
}
get title() {
const headerType = this.organization?.flexibleCollections
? this.i18nService.t("collections").toLowerCase()
@ -153,7 +164,7 @@ export class VaultHeaderComponent {
}
// Otherwise, check if we can edit the specified collection
return this.collection.node.canEdit(this.organization);
return this.collection.node.canEdit(this.organization, this.flexibleCollectionsV1Enabled);
}
addCipher() {

View File

@ -54,6 +54,7 @@
[showBulkEditCollectionAccess]="organization?.flexibleCollections"
[showBulkAddToCollections]="organization?.flexibleCollections"
[viewingOrgVault]="true"
[flexibleCollectionsV1Enabled]="flexibleCollectionsV1Enabled"
>
</app-vault-items>
<ng-container *ngIf="!flexibleCollectionsV1Enabled">
@ -98,7 +99,10 @@
</bit-no-items>
<collection-access-restricted
*ngIf="showCollectionAccessRestricted"
[canEdit]="selectedCollection != null && selectedCollection.node.canEdit(organization)"
[canEdit]="
selectedCollection != null &&
selectedCollection.node.canEdit(organization, flexibleCollectionsV1Enabled)
"
(editInfoClicked)="editCollection(selectedCollection.node, CollectionDialogTabType.Info)"
>
</collection-access-restricted>

View File

@ -213,7 +213,7 @@ export class VaultComponent implements OnInit, OnDestroy {
switchMap(async ([organization]) => {
this.organization = organization;
if (!organization.canUseAdminCollections) {
if (!organization.canUseAdminCollections(this.flexibleCollectionsV1Enabled)) {
await this.syncService.fullSync(false);
}
@ -322,7 +322,7 @@ export class VaultComponent implements OnInit, OnDestroy {
}
} else {
// Pre-flexible collections logic, to be removed after flexible collections is fully released
if (organization.canEditAnyCollection) {
if (organization.canEditAllCiphers(this.flexibleCollectionsV1Enabled)) {
ciphers = await this.cipherService.getAllFromApiForOrganization(organization.id);
} else {
ciphers = (await this.cipherService.getAllDecrypted()).filter(
@ -407,7 +407,8 @@ export class VaultComponent implements OnInit, OnDestroy {
]).pipe(
map(([filter, collection, organization]) => {
return (
(filter.collectionId === Unassigned && !organization.canUseAdminCollections) ||
(filter.collectionId === Unassigned &&
!organization.canUseAdminCollections(this.flexibleCollectionsV1Enabled)) ||
(!organization.canEditAllCiphers(this.flexibleCollectionsV1Enabled) &&
collection != undefined &&
!collection.node.assigned)
@ -453,11 +454,12 @@ export class VaultComponent implements OnInit, OnDestroy {
map(([filter, collection, organization]) => {
return (
// Filtering by unassigned, show message if not admin
(filter.collectionId === Unassigned && !organization.canUseAdminCollections) ||
(filter.collectionId === Unassigned &&
!organization.canUseAdminCollections(this.flexibleCollectionsV1Enabled)) ||
// Filtering by a collection, so show message if user is not assigned
(collection != undefined &&
!collection.node.assigned &&
!organization.canUseAdminCollections)
!organization.canUseAdminCollections(this.flexibleCollectionsV1Enabled))
);
}),
shareReplay({ refCount: true, bufferSize: 1 }),
@ -480,7 +482,7 @@ export class VaultComponent implements OnInit, OnDestroy {
(await firstValueFrom(allCipherMap$))[cipherId] != undefined;
} else {
canEditCipher =
organization.canUseAdminCollections ||
organization.canUseAdminCollections(this.flexibleCollectionsV1Enabled) ||
(await this.cipherService.get(cipherId)) != null;
}
@ -856,7 +858,7 @@ export class VaultComponent implements OnInit, OnDestroy {
}
try {
const asAdmin = this.organization?.canEditAnyCollection;
const asAdmin = this.organization?.canEditAnyCollection(this.flexibleCollectionsV1Enabled);
await this.cipherService.restoreWithServer(c.id, asAdmin);
this.platformUtilsService.showToast("success", null, this.i18nService.t("restoredItem"));
this.refresh();
@ -1143,7 +1145,7 @@ export class VaultComponent implements OnInit, OnDestroy {
}
protected deleteCipherWithServer(id: string, permanent: boolean) {
const asAdmin = this.organization?.canEditAnyCollection;
const asAdmin = this.organization?.canEditAllCiphers(this.flexibleCollectionsV1Enabled);
return permanent
? this.cipherService.deleteWithServer(id, asAdmin)
: this.cipherService.softDeleteWithServer(id, asAdmin);

View File

@ -662,7 +662,7 @@ export class AddEditComponent implements OnInit, OnDestroy {
// if a cipher is unassigned we want to check if they are an admin or have permission to edit any collection
if (!cipher.collectionIds) {
orgAdmin = this.organization?.canEditAnyCollection;
orgAdmin = this.organization?.canEditAllCiphers(this.flexibleCollectionsV1Enabled);
}
return this.cipher.id == null
@ -671,14 +671,14 @@ export class AddEditComponent implements OnInit, OnDestroy {
}
protected deleteCipher() {
const asAdmin = this.organization?.canEditAnyCollection;
const asAdmin = this.organization?.canEditAllCiphers(this.flexibleCollectionsV1Enabled);
return this.cipher.isDeleted
? this.cipherService.deleteWithServer(this.cipher.id, asAdmin)
: this.cipherService.softDeleteWithServer(this.cipher.id, asAdmin);
}
protected restoreCipher() {
const asAdmin = this.organization?.canEditAnyCollection;
const asAdmin = this.organization?.canEditAllCiphers(this.flexibleCollectionsV1Enabled);
return this.cipherService.restoreWithServer(this.cipher.id, asAdmin);
}

View File

@ -188,18 +188,29 @@ export class Organization {
return this.isManager || this.permissions.createNewCollections;
}
get canEditAnyCollection() {
return this.isAdmin || this.permissions.editAnyCollection;
canEditAnyCollection(flexibleCollectionsV1Enabled: boolean) {
if (!this.flexibleCollections || !flexibleCollectionsV1Enabled) {
// Pre-Flexible Collections v1 logic
return this.isAdmin || this.permissions.editAnyCollection;
}
// Post Flexible Collections V1, the allowAdminAccessToAllCollectionItems flag can restrict admins
// Providers and custom users with canEditAnyCollection are not affected by allowAdminAccessToAllCollectionItems flag
return (
this.isProviderUser ||
(this.type === OrganizationUserType.Custom && this.permissions.editAnyCollection) ||
(this.allowAdminAccessToAllCollectionItems && this.isAdmin)
);
}
get canUseAdminCollections() {
return this.canEditAnyCollection;
canUseAdminCollections(flexibleCollectionsV1Enabled: boolean) {
return this.canEditAnyCollection(flexibleCollectionsV1Enabled);
}
canEditAllCiphers(flexibleCollectionsV1Enabled: boolean) {
// Before Flexible Collections, anyone with editAnyCollection permission could edit all ciphers
if (!flexibleCollectionsV1Enabled) {
return this.canEditAnyCollection;
// Before Flexible Collections, any admin or anyone with editAnyCollection permission could edit all ciphers
if (!this.flexibleCollections || !flexibleCollectionsV1Enabled) {
return this.isAdmin || this.permissions.editAnyCollection;
}
// Post Flexible Collections V1, the allowAdminAccessToAllCollectionItems flag can restrict admins
// Providers and custom users with canEditAnyCollection are not affected by allowAdminAccessToAllCollectionItems flag
@ -214,8 +225,13 @@ export class Organization {
return this.isAdmin || this.permissions.deleteAnyCollection;
}
/**
* Whether the user can view all collection information, such as collection name and access.
* This does not indicate that the user can view items inside any collection - for that, see {@link canEditAllCiphers}
*/
get canViewAllCollections() {
return this.canEditAnyCollection || this.canDeleteAnyCollection;
// Admins can always see all collections even if collection management settings prevent them from editing them or seeing items
return this.isAdmin || this.permissions.editAnyCollection || this.canDeleteAnyCollection;
}
/**

View File

@ -53,11 +53,11 @@ export class CollectionView implements View, ITreeNodeObject {
);
}
return org?.canEditAnyCollection || (org?.canEditAssignedCollections && this.assigned);
return org?.canEditAnyCollection(false) || (org?.canEditAssignedCollections && this.assigned);
}
// For editing collection details, not the items within it.
canEdit(org: Organization): boolean {
canEdit(org: Organization, flexibleCollectionsV1Enabled: boolean): 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.",
@ -65,8 +65,8 @@ export class CollectionView implements View, ITreeNodeObject {
}
return org?.flexibleCollections
? org?.canEditAnyCollection || this.manage
: org?.canEditAnyCollection || org?.canEditAssignedCollections;
? org?.canEditAnyCollection(flexibleCollectionsV1Enabled) || this.manage
: org?.canEditAnyCollection(flexibleCollectionsV1Enabled) || org?.canEditAssignedCollections;
}
// For deleting a collection, not the items within it.