mirror of
https://github.com/bitwarden/browser
synced 2025-12-12 14:23:32 +00:00
[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:
@@ -4,6 +4,7 @@ import { ActivatedRoute } from "@angular/router";
|
|||||||
import { first } from "rxjs/operators";
|
import { first } from "rxjs/operators";
|
||||||
|
|
||||||
import { CollectionsComponent as BaseCollectionsComponent } from "@bitwarden/angular/admin-console/components/collections.component";
|
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 { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||||
@@ -21,11 +22,19 @@ export class CollectionsComponent extends BaseCollectionsComponent {
|
|||||||
platformUtilsService: PlatformUtilsService,
|
platformUtilsService: PlatformUtilsService,
|
||||||
i18nService: I18nService,
|
i18nService: I18nService,
|
||||||
cipherService: CipherService,
|
cipherService: CipherService,
|
||||||
|
organizationService: OrganizationService,
|
||||||
private route: ActivatedRoute,
|
private route: ActivatedRoute,
|
||||||
private location: Location,
|
private location: Location,
|
||||||
logService: LogService,
|
logService: LogService,
|
||||||
) {
|
) {
|
||||||
super(collectionService, platformUtilsService, i18nService, cipherService, logService);
|
super(
|
||||||
|
collectionService,
|
||||||
|
platformUtilsService,
|
||||||
|
i18nService,
|
||||||
|
cipherService,
|
||||||
|
organizationService,
|
||||||
|
logService,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
async ngOnInit() {
|
async ngOnInit() {
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { Component } from "@angular/core";
|
import { Component } from "@angular/core";
|
||||||
|
|
||||||
import { CollectionsComponent as BaseCollectionsComponent } from "@bitwarden/angular/admin-console/components/collections.component";
|
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 { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||||
@@ -17,8 +18,16 @@ export class CollectionsComponent extends BaseCollectionsComponent {
|
|||||||
i18nService: I18nService,
|
i18nService: I18nService,
|
||||||
collectionService: CollectionService,
|
collectionService: CollectionService,
|
||||||
platformUtilsService: PlatformUtilsService,
|
platformUtilsService: PlatformUtilsService,
|
||||||
|
organizationService: OrganizationService,
|
||||||
logService: LogService,
|
logService: LogService,
|
||||||
) {
|
) {
|
||||||
super(collectionService, platformUtilsService, i18nService, cipherService, logService);
|
super(
|
||||||
|
collectionService,
|
||||||
|
platformUtilsService,
|
||||||
|
i18nService,
|
||||||
|
cipherService,
|
||||||
|
organizationService,
|
||||||
|
logService,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -40,6 +40,7 @@
|
|||||||
[(ngModel)]="$any(c).checked"
|
[(ngModel)]="$any(c).checked"
|
||||||
name="Collection[{{ i }}].Checked"
|
name="Collection[{{ i }}].Checked"
|
||||||
appStopProp
|
appStopProp
|
||||||
|
[disabled]="!c.canEditItems(this.organization, this.flexibleCollectionsV1Enabled)"
|
||||||
/>
|
/>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { Component, OnDestroy } from "@angular/core";
|
import { Component, OnDestroy } from "@angular/core";
|
||||||
|
|
||||||
import { CollectionsComponent as BaseCollectionsComponent } from "@bitwarden/angular/admin-console/components/collections.component";
|
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 { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||||
@@ -18,9 +19,17 @@ export class CollectionsComponent extends BaseCollectionsComponent implements On
|
|||||||
platformUtilsService: PlatformUtilsService,
|
platformUtilsService: PlatformUtilsService,
|
||||||
i18nService: I18nService,
|
i18nService: I18nService,
|
||||||
cipherService: CipherService,
|
cipherService: CipherService,
|
||||||
|
organizationSerivce: OrganizationService,
|
||||||
logService: LogService,
|
logService: LogService,
|
||||||
) {
|
) {
|
||||||
super(collectionService, platformUtilsService, i18nService, cipherService, logService);
|
super(
|
||||||
|
collectionService,
|
||||||
|
platformUtilsService,
|
||||||
|
i18nService,
|
||||||
|
cipherService,
|
||||||
|
organizationSerivce,
|
||||||
|
logService,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
ngOnDestroy() {
|
ngOnDestroy() {
|
||||||
@@ -28,6 +37,9 @@ export class CollectionsComponent extends BaseCollectionsComponent implements On
|
|||||||
}
|
}
|
||||||
|
|
||||||
check(c: CollectionView, select?: boolean) {
|
check(c: CollectionView, select?: boolean) {
|
||||||
|
if (!c.canEditItems(this.organization, this.flexibleCollectionsV1Enabled)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
(c as any).checked = select == null ? !(c as any).checked : select;
|
(c as any).checked = select == null ? !(c as any).checked : select;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { Component } from "@angular/core";
|
import { Component } from "@angular/core";
|
||||||
|
|
||||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
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 { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
|
||||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||||
@@ -25,15 +26,27 @@ export class CollectionsComponent extends BaseCollectionsComponent {
|
|||||||
platformUtilsService: PlatformUtilsService,
|
platformUtilsService: PlatformUtilsService,
|
||||||
i18nService: I18nService,
|
i18nService: I18nService,
|
||||||
cipherService: CipherService,
|
cipherService: CipherService,
|
||||||
|
organizationService: OrganizationService,
|
||||||
private apiService: ApiService,
|
private apiService: ApiService,
|
||||||
logService: LogService,
|
logService: LogService,
|
||||||
) {
|
) {
|
||||||
super(collectionService, platformUtilsService, i18nService, cipherService, logService);
|
super(
|
||||||
|
collectionService,
|
||||||
|
platformUtilsService,
|
||||||
|
i18nService,
|
||||||
|
cipherService,
|
||||||
|
organizationService,
|
||||||
|
logService,
|
||||||
|
);
|
||||||
this.allowSelectNone = true;
|
this.allowSelectNone = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected async loadCipher() {
|
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();
|
return await super.loadCipher();
|
||||||
}
|
}
|
||||||
const response = await this.apiService.getCipherAdmin(this.cipherId);
|
const response = await this.apiService.getCipherAdmin(this.cipherId);
|
||||||
@@ -55,7 +68,10 @@ export class CollectionsComponent extends BaseCollectionsComponent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
protected saveCollections() {
|
protected saveCollections() {
|
||||||
if (this.organization.canEditAnyCollection) {
|
if (
|
||||||
|
this.organization.canEditAllCiphers(this.flexibleCollectionsV1Enabled) ||
|
||||||
|
this.collectionIds.length === 0
|
||||||
|
) {
|
||||||
const request = new CipherCollectionsRequest(this.cipherDomain.collectionIds);
|
const request = new CipherCollectionsRequest(this.cipherDomain.collectionIds);
|
||||||
return this.apiService.putCipherCollectionsAdmin(this.cipherId, request);
|
return this.apiService.putCipherCollectionsAdmin(this.cipherId, request);
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -137,6 +137,7 @@ export class VaultComponent implements OnInit, OnDestroy {
|
|||||||
protected showCollectionAccessRestricted: boolean;
|
protected showCollectionAccessRestricted: boolean;
|
||||||
protected currentSearchText$: Observable<string>;
|
protected currentSearchText$: Observable<string>;
|
||||||
protected editableCollections$: Observable<CollectionView[]>;
|
protected editableCollections$: Observable<CollectionView[]>;
|
||||||
|
protected allCollectionsWithoutUnassigned$: Observable<CollectionAdminView[]>;
|
||||||
protected showBulkEditCollectionAccess$ = this.configService.getFeatureFlag$(
|
protected showBulkEditCollectionAccess$ = this.configService.getFeatureFlag$(
|
||||||
FeatureFlag.BulkCollectionAccess,
|
FeatureFlag.BulkCollectionAccess,
|
||||||
false,
|
false,
|
||||||
@@ -253,7 +254,7 @@ export class VaultComponent implements OnInit, OnDestroy {
|
|||||||
|
|
||||||
this.currentSearchText$ = this.route.queryParams.pipe(map((queryParams) => queryParams.search));
|
this.currentSearchText$ = this.route.queryParams.pipe(map((queryParams) => queryParams.search));
|
||||||
|
|
||||||
const allCollectionsWithoutUnassigned$ = combineLatest([
|
this.allCollectionsWithoutUnassigned$ = combineLatest([
|
||||||
organizationId$.pipe(switchMap((orgId) => this.collectionAdminService.getAll(orgId))),
|
organizationId$.pipe(switchMap((orgId) => this.collectionAdminService.getAll(orgId))),
|
||||||
defer(() => this.collectionService.getAllDecrypted()),
|
defer(() => this.collectionService.getAllDecrypted()),
|
||||||
]).pipe(
|
]).pipe(
|
||||||
@@ -276,7 +277,7 @@ export class VaultComponent implements OnInit, OnDestroy {
|
|||||||
shareReplay({ refCount: true, bufferSize: 1 }),
|
shareReplay({ refCount: true, bufferSize: 1 }),
|
||||||
);
|
);
|
||||||
|
|
||||||
this.editableCollections$ = allCollectionsWithoutUnassigned$.pipe(
|
this.editableCollections$ = this.allCollectionsWithoutUnassigned$.pipe(
|
||||||
map((collections) => {
|
map((collections) => {
|
||||||
// Users that can edit all ciphers can implicitly edit all collections
|
// Users that can edit all ciphers can implicitly edit all collections
|
||||||
if (this.organization.canEditAllCiphers(this.flexibleCollectionsV1Enabled)) {
|
if (this.organization.canEditAllCiphers(this.flexibleCollectionsV1Enabled)) {
|
||||||
@@ -287,7 +288,10 @@ export class VaultComponent implements OnInit, OnDestroy {
|
|||||||
shareReplay({ refCount: true, bufferSize: 1 }),
|
shareReplay({ refCount: true, bufferSize: 1 }),
|
||||||
);
|
);
|
||||||
|
|
||||||
const allCollections$ = combineLatest([organizationId$, allCollectionsWithoutUnassigned$]).pipe(
|
const allCollections$ = combineLatest([
|
||||||
|
organizationId$,
|
||||||
|
this.allCollectionsWithoutUnassigned$,
|
||||||
|
]).pipe(
|
||||||
map(([organizationId, allCollections]) => {
|
map(([organizationId, allCollections]) => {
|
||||||
const noneCollection = new CollectionAdminView();
|
const noneCollection = new CollectionAdminView();
|
||||||
noneCollection.name = this.i18nService.t("unassigned");
|
noneCollection.name = this.i18nService.t("unassigned");
|
||||||
@@ -680,16 +684,35 @@ export class VaultComponent implements OnInit, OnDestroy {
|
|||||||
|
|
||||||
if (this.flexibleCollectionsV1Enabled) {
|
if (this.flexibleCollectionsV1Enabled) {
|
||||||
// V1 limits admins to only adding items to collections they have access to.
|
// V1 limits admins to only adding items to collections they have access to.
|
||||||
collections = await firstValueFrom(this.editableCollections$);
|
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 {
|
} else {
|
||||||
collections = (await firstValueFrom(this.vaultFilterService.filteredCollections$)).filter(
|
return a.name.localeCompare(b.name);
|
||||||
(c) => !c.readOnly && c.id != Unassigned,
|
}
|
||||||
|
});
|
||||||
|
}),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
|
} else {
|
||||||
|
collections = await firstValueFrom(this.allCollectionsWithoutUnassigned$);
|
||||||
}
|
}
|
||||||
const [modal] = await this.modalService.openViewRef(
|
const [modal] = await this.modalService.openViewRef(
|
||||||
CollectionsComponent,
|
CollectionsComponent,
|
||||||
this.collectionsModalRef,
|
this.collectionsModalRef,
|
||||||
(comp) => {
|
(comp) => {
|
||||||
|
comp.flexibleCollectionsV1Enabled = this.flexibleCollectionsV1Enabled;
|
||||||
comp.collectionIds = cipher.collectionIds;
|
comp.collectionIds = cipher.collectionIds;
|
||||||
comp.collections = collections;
|
comp.collections = collections;
|
||||||
comp.organization = this.organization;
|
comp.organization = this.organization;
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
import { Directive, EventEmitter, Input, OnInit, Output } from "@angular/core";
|
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 { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||||
@@ -19,6 +21,8 @@ export class CollectionsComponent implements OnInit {
|
|||||||
cipher: CipherView;
|
cipher: CipherView;
|
||||||
collectionIds: string[];
|
collectionIds: string[];
|
||||||
collections: CollectionView[] = [];
|
collections: CollectionView[] = [];
|
||||||
|
organization: Organization;
|
||||||
|
flexibleCollectionsV1Enabled: boolean;
|
||||||
|
|
||||||
protected cipherDomain: Cipher;
|
protected cipherDomain: Cipher;
|
||||||
|
|
||||||
@@ -27,6 +31,7 @@ export class CollectionsComponent implements OnInit {
|
|||||||
protected platformUtilsService: PlatformUtilsService,
|
protected platformUtilsService: PlatformUtilsService,
|
||||||
protected i18nService: I18nService,
|
protected i18nService: I18nService,
|
||||||
protected cipherService: CipherService,
|
protected cipherService: CipherService,
|
||||||
|
protected organizationService: OrganizationService,
|
||||||
private logService: LogService,
|
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;
|
(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() {
|
async submit() {
|
||||||
const selectedCollectionIds = this.collections
|
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);
|
.map((c) => c.id);
|
||||||
if (!this.allowSelectNone && selectedCollectionIds.length === 0) {
|
if (!this.allowSelectNone && selectedCollectionIds.length === 0) {
|
||||||
this.platformUtilsService.showToast(
|
this.platformUtilsService.showToast(
|
||||||
|
|||||||
@@ -68,6 +68,7 @@ describe("Collection", () => {
|
|||||||
organizationId: "orgId",
|
organizationId: "orgId",
|
||||||
readOnly: false,
|
readOnly: false,
|
||||||
manage: true,
|
manage: true,
|
||||||
|
assigned: true,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ export class CollectionView implements View, ITreeNodeObject {
|
|||||||
readOnly: boolean = null;
|
readOnly: boolean = null;
|
||||||
hidePasswords: boolean = null;
|
hidePasswords: boolean = null;
|
||||||
manage: boolean = null;
|
manage: boolean = null;
|
||||||
|
assigned: boolean = null;
|
||||||
|
|
||||||
constructor(c?: Collection | CollectionAccessDetailsResponse) {
|
constructor(c?: Collection | CollectionAccessDetailsResponse) {
|
||||||
if (!c) {
|
if (!c) {
|
||||||
@@ -30,7 +31,29 @@ export class CollectionView implements View, ITreeNodeObject {
|
|||||||
this.readOnly = c.readOnly;
|
this.readOnly = c.readOnly;
|
||||||
this.hidePasswords = c.hidePasswords;
|
this.hidePasswords = c.hidePasswords;
|
||||||
this.manage = c.manage;
|
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.
|
// For editing collection details, not the items within it.
|
||||||
|
|||||||
Reference in New Issue
Block a user