diff --git a/apps/browser/src/vault/popup/components/vault/collections.component.ts b/apps/browser/src/vault/popup/components/vault/collections.component.ts index acbdab3685..c8f85a8b7a 100644 --- a/apps/browser/src/vault/popup/components/vault/collections.component.ts +++ b/apps/browser/src/vault/popup/components/vault/collections.component.ts @@ -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() { diff --git a/apps/desktop/src/vault/app/vault/collections.component.ts b/apps/desktop/src/vault/app/vault/collections.component.ts index b8465669e7..cd08427016 100644 --- a/apps/desktop/src/vault/app/vault/collections.component.ts +++ b/apps/desktop/src/vault/app/vault/collections.component.ts @@ -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, + ); } } diff --git a/apps/web/src/app/vault/individual-vault/collections.component.html b/apps/web/src/app/vault/individual-vault/collections.component.html index 603e899c11..46bd94b316 100644 --- a/apps/web/src/app/vault/individual-vault/collections.component.html +++ b/apps/web/src/app/vault/individual-vault/collections.component.html @@ -40,6 +40,7 @@ [(ngModel)]="$any(c).checked" name="Collection[{{ i }}].Checked" appStopProp + [disabled]="!c.canEditItems(this.organization, this.flexibleCollectionsV1Enabled)" /> diff --git a/apps/web/src/app/vault/individual-vault/collections.component.ts b/apps/web/src/app/vault/individual-vault/collections.component.ts index 29e6293c99..6cf0901f33 100644 --- a/apps/web/src/app/vault/individual-vault/collections.component.ts +++ b/apps/web/src/app/vault/individual-vault/collections.component.ts @@ -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; } diff --git a/apps/web/src/app/vault/org-vault/collections.component.ts b/apps/web/src/app/vault/org-vault/collections.component.ts index 4b6bc26e7b..020d3fbe95 100644 --- a/apps/web/src/app/vault/org-vault/collections.component.ts +++ b/apps/web/src/app/vault/org-vault/collections.component.ts @@ -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 { diff --git a/apps/web/src/app/vault/org-vault/vault.component.ts b/apps/web/src/app/vault/org-vault/vault.component.ts index fa8c376630..ecec349482 100644 --- a/apps/web/src/app/vault/org-vault/vault.component.ts +++ b/apps/web/src/app/vault/org-vault/vault.component.ts @@ -137,6 +137,7 @@ export class VaultComponent implements OnInit, OnDestroy { protected showCollectionAccessRestricted: boolean; protected currentSearchText$: Observable; protected editableCollections$: Observable; + protected allCollectionsWithoutUnassigned$: Observable; 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; diff --git a/libs/angular/src/admin-console/components/collections.component.ts b/libs/angular/src/admin-console/components/collections.component.ts index 9c3ce48369..167fe0a97f 100644 --- a/libs/angular/src/admin-console/components/collections.component.ts +++ b/libs/angular/src/admin-console/components/collections.component.ts @@ -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( diff --git a/libs/common/src/vault/models/domain/collection.spec.ts b/libs/common/src/vault/models/domain/collection.spec.ts index 848633e501..cd1cab8b42 100644 --- a/libs/common/src/vault/models/domain/collection.spec.ts +++ b/libs/common/src/vault/models/domain/collection.spec.ts @@ -68,6 +68,7 @@ describe("Collection", () => { organizationId: "orgId", readOnly: false, manage: true, + assigned: true, }); }); }); diff --git a/libs/common/src/vault/models/view/collection.view.ts b/libs/common/src/vault/models/view/collection.view.ts index 1177d23220..74d369380b 100644 --- a/libs/common/src/vault/models/view/collection.view.ts +++ b/libs/common/src/vault/models/view/collection.view.ts @@ -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.