[AC-2161] update cipher collections org vault modal (#8027)

* collections component shows disable readOnly collections in the org vault edit collections modal, and will check if org allows Owners up manage all collections in ciphers
This commit is contained in:
Jason Ng 2024-03-21 11:54:31 -04:00 committed by GitHub
parent cd5dc09d25
commit 8fd76eaf9c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 123 additions and 14 deletions

View File

@ -4,6 +4,7 @@ import { ActivatedRoute } from "@angular/router";
import { first } from "rxjs/operators";
import { CollectionsComponent as BaseCollectionsComponent } from "@bitwarden/angular/admin-console/components/collections.component";
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
@ -21,11 +22,19 @@ export class CollectionsComponent extends BaseCollectionsComponent {
platformUtilsService: PlatformUtilsService,
i18nService: I18nService,
cipherService: CipherService,
organizationService: OrganizationService,
private route: ActivatedRoute,
private location: Location,
logService: LogService,
) {
super(collectionService, platformUtilsService, i18nService, cipherService, logService);
super(
collectionService,
platformUtilsService,
i18nService,
cipherService,
organizationService,
logService,
);
}
async ngOnInit() {

View File

@ -1,6 +1,7 @@
import { Component } from "@angular/core";
import { CollectionsComponent as BaseCollectionsComponent } from "@bitwarden/angular/admin-console/components/collections.component";
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
@ -17,8 +18,16 @@ export class CollectionsComponent extends BaseCollectionsComponent {
i18nService: I18nService,
collectionService: CollectionService,
platformUtilsService: PlatformUtilsService,
organizationService: OrganizationService,
logService: LogService,
) {
super(collectionService, platformUtilsService, i18nService, cipherService, logService);
super(
collectionService,
platformUtilsService,
i18nService,
cipherService,
organizationService,
logService,
);
}
}

View File

@ -40,6 +40,7 @@
[(ngModel)]="$any(c).checked"
name="Collection[{{ i }}].Checked"
appStopProp
[disabled]="!c.canEditItems(this.organization, this.flexibleCollectionsV1Enabled)"
/>
</td>
<td>

View File

@ -1,6 +1,7 @@
import { Component, OnDestroy } from "@angular/core";
import { CollectionsComponent as BaseCollectionsComponent } from "@bitwarden/angular/admin-console/components/collections.component";
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
@ -18,9 +19,17 @@ export class CollectionsComponent extends BaseCollectionsComponent implements On
platformUtilsService: PlatformUtilsService,
i18nService: I18nService,
cipherService: CipherService,
organizationSerivce: OrganizationService,
logService: LogService,
) {
super(collectionService, platformUtilsService, i18nService, cipherService, logService);
super(
collectionService,
platformUtilsService,
i18nService,
cipherService,
organizationSerivce,
logService,
);
}
ngOnDestroy() {
@ -28,6 +37,9 @@ export class CollectionsComponent extends BaseCollectionsComponent implements On
}
check(c: CollectionView, select?: boolean) {
if (!c.canEditItems(this.organization, this.flexibleCollectionsV1Enabled)) {
return;
}
(c as any).checked = select == null ? !(c as any).checked : select;
}

View File

@ -1,6 +1,7 @@
import { Component } from "@angular/core";
import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
@ -25,15 +26,27 @@ export class CollectionsComponent extends BaseCollectionsComponent {
platformUtilsService: PlatformUtilsService,
i18nService: I18nService,
cipherService: CipherService,
organizationService: OrganizationService,
private apiService: ApiService,
logService: LogService,
) {
super(collectionService, platformUtilsService, i18nService, cipherService, logService);
super(
collectionService,
platformUtilsService,
i18nService,
cipherService,
organizationService,
logService,
);
this.allowSelectNone = true;
}
protected async loadCipher() {
if (!this.organization.canViewAllCollections) {
// if cipher is unassigned use apiService. We can see this by looking at this.collectionIds
if (
!this.organization.canEditAllCiphers(this.flexibleCollectionsV1Enabled) &&
this.collectionIds.length !== 0
) {
return await super.loadCipher();
}
const response = await this.apiService.getCipherAdmin(this.cipherId);
@ -55,7 +68,10 @@ export class CollectionsComponent extends BaseCollectionsComponent {
}
protected saveCollections() {
if (this.organization.canEditAnyCollection) {
if (
this.organization.canEditAllCiphers(this.flexibleCollectionsV1Enabled) ||
this.collectionIds.length === 0
) {
const request = new CipherCollectionsRequest(this.cipherDomain.collectionIds);
return this.apiService.putCipherCollectionsAdmin(this.cipherId, request);
} else {

View File

@ -137,6 +137,7 @@ export class VaultComponent implements OnInit, OnDestroy {
protected showCollectionAccessRestricted: boolean;
protected currentSearchText$: Observable<string>;
protected editableCollections$: Observable<CollectionView[]>;
protected allCollectionsWithoutUnassigned$: Observable<CollectionAdminView[]>;
protected showBulkEditCollectionAccess$ = this.configService.getFeatureFlag$(
FeatureFlag.BulkCollectionAccess,
false,
@ -253,7 +254,7 @@ export class VaultComponent implements OnInit, OnDestroy {
this.currentSearchText$ = this.route.queryParams.pipe(map((queryParams) => queryParams.search));
const allCollectionsWithoutUnassigned$ = combineLatest([
this.allCollectionsWithoutUnassigned$ = combineLatest([
organizationId$.pipe(switchMap((orgId) => this.collectionAdminService.getAll(orgId))),
defer(() => this.collectionService.getAllDecrypted()),
]).pipe(
@ -276,7 +277,7 @@ export class VaultComponent implements OnInit, OnDestroy {
shareReplay({ refCount: true, bufferSize: 1 }),
);
this.editableCollections$ = allCollectionsWithoutUnassigned$.pipe(
this.editableCollections$ = this.allCollectionsWithoutUnassigned$.pipe(
map((collections) => {
// Users that can edit all ciphers can implicitly edit all collections
if (this.organization.canEditAllCiphers(this.flexibleCollectionsV1Enabled)) {
@ -287,7 +288,10 @@ export class VaultComponent implements OnInit, OnDestroy {
shareReplay({ refCount: true, bufferSize: 1 }),
);
const allCollections$ = combineLatest([organizationId$, allCollectionsWithoutUnassigned$]).pipe(
const allCollections$ = combineLatest([
organizationId$,
this.allCollectionsWithoutUnassigned$,
]).pipe(
map(([organizationId, allCollections]) => {
const noneCollection = new CollectionAdminView();
noneCollection.name = this.i18nService.t("unassigned");
@ -680,16 +684,35 @@ export class VaultComponent implements OnInit, OnDestroy {
if (this.flexibleCollectionsV1Enabled) {
// V1 limits admins to only adding items to collections they have access to.
collections = await firstValueFrom(this.editableCollections$);
} else {
collections = (await firstValueFrom(this.vaultFilterService.filteredCollections$)).filter(
(c) => !c.readOnly && c.id != Unassigned,
collections = await firstValueFrom(
this.allCollectionsWithoutUnassigned$.pipe(
map((c) => {
return c.sort((a, b) => {
if (
a.canEditItems(this.organization, true) &&
!b.canEditItems(this.organization, true)
) {
return -1;
} else if (
!a.canEditItems(this.organization, true) &&
b.canEditItems(this.organization, true)
) {
return 1;
} else {
return a.name.localeCompare(b.name);
}
});
}),
),
);
} else {
collections = await firstValueFrom(this.allCollectionsWithoutUnassigned$);
}
const [modal] = await this.modalService.openViewRef(
CollectionsComponent,
this.collectionsModalRef,
(comp) => {
comp.flexibleCollectionsV1Enabled = this.flexibleCollectionsV1Enabled;
comp.collectionIds = cipher.collectionIds;
comp.collections = collections;
comp.organization = this.organization;

View File

@ -1,5 +1,7 @@
import { Directive, EventEmitter, Input, OnInit, Output } from "@angular/core";
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
@ -19,6 +21,8 @@ export class CollectionsComponent implements OnInit {
cipher: CipherView;
collectionIds: string[];
collections: CollectionView[] = [];
organization: Organization;
flexibleCollectionsV1Enabled: boolean;
protected cipherDomain: Cipher;
@ -27,6 +31,7 @@ export class CollectionsComponent implements OnInit {
protected platformUtilsService: PlatformUtilsService,
protected i18nService: I18nService,
protected cipherService: CipherService,
protected organizationService: OrganizationService,
private logService: LogService,
) {}
@ -48,11 +53,21 @@ export class CollectionsComponent implements OnInit {
(c as any).checked = this.collectionIds != null && this.collectionIds.indexOf(c.id) > -1;
});
}
if (this.organization == null) {
this.organization = await this.organizationService.get(this.cipher.organizationId);
}
}
async submit() {
const selectedCollectionIds = this.collections
.filter((c) => !!(c as any).checked)
.filter((c) => {
if (this.organization.canEditAllCiphers(this.flexibleCollectionsV1Enabled)) {
return !!(c as any).checked;
} else {
return !!(c as any).checked && c.readOnly == null;
}
})
.map((c) => c.id);
if (!this.allowSelectNone && selectedCollectionIds.length === 0) {
this.platformUtilsService.showToast(

View File

@ -68,6 +68,7 @@ describe("Collection", () => {
organizationId: "orgId",
readOnly: false,
manage: true,
assigned: true,
});
});
});

View File

@ -17,6 +17,7 @@ export class CollectionView implements View, ITreeNodeObject {
readOnly: boolean = null;
hidePasswords: boolean = null;
manage: boolean = null;
assigned: boolean = null;
constructor(c?: Collection | CollectionAccessDetailsResponse) {
if (!c) {
@ -30,7 +31,29 @@ export class CollectionView implements View, ITreeNodeObject {
this.readOnly = c.readOnly;
this.hidePasswords = c.hidePasswords;
this.manage = c.manage;
this.assigned = true;
}
if (c instanceof CollectionAccessDetailsResponse) {
this.assigned = c.assigned;
}
}
canEditItems(org: Organization, v1FlexibleCollections: 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.",
);
}
if (org?.flexibleCollections) {
return (
org?.canEditAllCiphers(v1FlexibleCollections) ||
this.manage ||
(this.assigned && !this.readOnly)
);
}
return org?.canEditAnyCollection || (org?.canEditAssignedCollections && this.assigned);
}
// For editing collection details, not the items within it.