mirror of
https://github.com/bitwarden/browser.git
synced 2024-12-21 16:18:28 +01:00
[AC-1707] Restrict provider access to items (#8265)
* [AC-1707] Add feature flag * [AC-1707] Prevent loading ciphers for provider users in the org vault when the feature flag is enabled * [AC-1707] Ensure new canEditAllCiphers logic only applies to organizations that have FC enabled * [AC-1707] Update editAllCiphers helper to check for restrictProviderAccess feature flag * [AC-1707] Remove un-used vaultFilterComponent reference * [AC-1707] Hide vault filter for providers * [AC-1707] Add search to vault header for provider users * [AC-1707] Hide New Item button for Providers when restrict provider access feature flag is enabled * [AC-1707] Remove leftover debug statement * [AC-1707] Update canEditAllCiphers references to consider the restrictProviderAccessFlag * [AC-1707] Fix collections component changes from main * [AC-1707] Fix some feature flag issues from merge with main * [AC-1707] Avoid 'readonly' collection dialog for providers * [AC-1707] Fix broken Browser component * [AC-1707] Fix broken Desktop component * [AC-1707] Add restrict provider flag to add access badge logic
This commit is contained in:
parent
27d4178287
commit
3a71322510
@ -1,4 +1,4 @@
|
||||
import { DialogConfig, DialogRef, DIALOG_DATA } from "@angular/cdk/dialog";
|
||||
import { DIALOG_DATA, DialogConfig, DialogRef } from "@angular/cdk/dialog";
|
||||
import { Component, Inject } from "@angular/core";
|
||||
import { firstValueFrom } from "rxjs";
|
||||
|
||||
@ -56,6 +56,10 @@ export class BulkDeleteDialogComponent {
|
||||
FeatureFlag.FlexibleCollectionsV1,
|
||||
);
|
||||
|
||||
private restrictProviderAccess$ = this.configService.getFeatureFlag$(
|
||||
FeatureFlag.RestrictProviderAccess,
|
||||
);
|
||||
|
||||
constructor(
|
||||
@Inject(DIALOG_DATA) params: BulkDeleteDialogParams,
|
||||
private dialogRef: DialogRef<BulkDeleteDialogResult>,
|
||||
@ -81,10 +85,11 @@ export class BulkDeleteDialogComponent {
|
||||
const deletePromises: Promise<void>[] = [];
|
||||
if (this.cipherIds.length) {
|
||||
const flexibleCollectionsV1Enabled = await firstValueFrom(this.flexibleCollectionsV1Enabled$);
|
||||
const restrictProviderAccess = await firstValueFrom(this.restrictProviderAccess$);
|
||||
|
||||
if (
|
||||
!this.organization ||
|
||||
!this.organization.canEditAllCiphers(flexibleCollectionsV1Enabled)
|
||||
!this.organization.canEditAllCiphers(flexibleCollectionsV1Enabled, restrictProviderAccess)
|
||||
) {
|
||||
deletePromises.push(this.deleteCiphers());
|
||||
} else {
|
||||
@ -118,7 +123,11 @@ export class BulkDeleteDialogComponent {
|
||||
|
||||
private async deleteCiphers(): Promise<any> {
|
||||
const flexibleCollectionsV1Enabled = await firstValueFrom(this.flexibleCollectionsV1Enabled$);
|
||||
const asAdmin = this.organization?.canEditAllCiphers(flexibleCollectionsV1Enabled);
|
||||
const restrictProviderAccess = await firstValueFrom(this.restrictProviderAccess$);
|
||||
const asAdmin = this.organization?.canEditAllCiphers(
|
||||
flexibleCollectionsV1Enabled,
|
||||
restrictProviderAccess,
|
||||
);
|
||||
if (this.permanent) {
|
||||
await this.cipherService.deleteManyWithServer(this.cipherIds, asAdmin);
|
||||
} else {
|
||||
|
@ -32,7 +32,13 @@
|
||||
[(ngModel)]="$any(c).checked"
|
||||
name="Collection[{{ i }}].Checked"
|
||||
appStopProp
|
||||
[disabled]="!c.canEditItems(this.organization, this.flexibleCollectionsV1Enabled)"
|
||||
[disabled]="
|
||||
!c.canEditItems(
|
||||
this.organization,
|
||||
this.flexibleCollectionsV1Enabled,
|
||||
this.restrictProviderAccess
|
||||
)
|
||||
"
|
||||
/>
|
||||
{{ c.name }}
|
||||
</td>
|
||||
|
@ -50,7 +50,13 @@ export class CollectionsComponent extends BaseCollectionsComponent implements On
|
||||
}
|
||||
|
||||
check(c: CollectionView, select?: boolean) {
|
||||
if (!c.canEditItems(this.organization, this.flexibleCollectionsV1Enabled)) {
|
||||
if (
|
||||
!c.canEditItems(
|
||||
this.organization,
|
||||
this.flexibleCollectionsV1Enabled,
|
||||
this.restrictProviderAccess,
|
||||
)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
(c as any).checked = select == null ? !(c as any).checked : select;
|
||||
|
@ -82,7 +82,12 @@ export class AddEditComponent extends BaseAddEditComponent {
|
||||
}
|
||||
|
||||
protected loadCollections() {
|
||||
if (!this.organization.canEditAllCiphers(this.flexibleCollectionsV1Enabled)) {
|
||||
if (
|
||||
!this.organization.canEditAllCiphers(
|
||||
this.flexibleCollectionsV1Enabled,
|
||||
this.restrictProviderAccess,
|
||||
)
|
||||
) {
|
||||
return super.loadCollections();
|
||||
}
|
||||
return Promise.resolve(this.collections);
|
||||
@ -93,7 +98,10 @@ export class AddEditComponent extends BaseAddEditComponent {
|
||||
const firstCipherCheck = await super.loadCipher();
|
||||
|
||||
if (
|
||||
!this.organization.canEditAllCiphers(this.flexibleCollectionsV1Enabled) &&
|
||||
!this.organization.canEditAllCiphers(
|
||||
this.flexibleCollectionsV1Enabled,
|
||||
this.restrictProviderAccess,
|
||||
) &&
|
||||
firstCipherCheck != null
|
||||
) {
|
||||
return firstCipherCheck;
|
||||
@ -108,14 +116,24 @@ export class AddEditComponent extends BaseAddEditComponent {
|
||||
}
|
||||
|
||||
protected encryptCipher() {
|
||||
if (!this.organization.canEditAllCiphers(this.flexibleCollectionsV1Enabled)) {
|
||||
if (
|
||||
!this.organization.canEditAllCiphers(
|
||||
this.flexibleCollectionsV1Enabled,
|
||||
this.restrictProviderAccess,
|
||||
)
|
||||
) {
|
||||
return super.encryptCipher();
|
||||
}
|
||||
return this.cipherService.encrypt(this.cipher, null, null, this.originalCipher);
|
||||
}
|
||||
|
||||
protected async deleteCipher() {
|
||||
if (!this.organization.canEditAllCiphers(this.flexibleCollectionsV1Enabled)) {
|
||||
if (
|
||||
!this.organization.canEditAllCiphers(
|
||||
this.flexibleCollectionsV1Enabled,
|
||||
this.restrictProviderAccess,
|
||||
)
|
||||
) {
|
||||
return super.deleteCipher();
|
||||
}
|
||||
return this.cipher.isDeleted
|
||||
|
@ -29,6 +29,7 @@ export class AttachmentsComponent extends BaseAttachmentsComponent implements On
|
||||
organization: Organization;
|
||||
|
||||
private flexibleCollectionsV1Enabled = false;
|
||||
private restrictProviderAccess = false;
|
||||
|
||||
constructor(
|
||||
cipherService: CipherService,
|
||||
@ -62,11 +63,17 @@ export class AttachmentsComponent extends BaseAttachmentsComponent implements On
|
||||
this.flexibleCollectionsV1Enabled = await firstValueFrom(
|
||||
this.configService.getFeatureFlag$(FeatureFlag.FlexibleCollectionsV1),
|
||||
);
|
||||
this.restrictProviderAccess = await firstValueFrom(
|
||||
this.configService.getFeatureFlag$(FeatureFlag.RestrictProviderAccess),
|
||||
);
|
||||
}
|
||||
|
||||
protected async reupload(attachment: AttachmentView) {
|
||||
if (
|
||||
this.organization.canEditAllCiphers(this.flexibleCollectionsV1Enabled) &&
|
||||
this.organization.canEditAllCiphers(
|
||||
this.flexibleCollectionsV1Enabled,
|
||||
this.restrictProviderAccess,
|
||||
) &&
|
||||
this.showFixOldAttachments(attachment)
|
||||
) {
|
||||
await super.reuploadCipherAttachment(attachment, true);
|
||||
@ -74,7 +81,12 @@ export class AttachmentsComponent extends BaseAttachmentsComponent implements On
|
||||
}
|
||||
|
||||
protected async loadCipher() {
|
||||
if (!this.organization.canEditAllCiphers(this.flexibleCollectionsV1Enabled)) {
|
||||
if (
|
||||
!this.organization.canEditAllCiphers(
|
||||
this.flexibleCollectionsV1Enabled,
|
||||
this.restrictProviderAccess,
|
||||
)
|
||||
) {
|
||||
return await super.loadCipher();
|
||||
}
|
||||
const response = await this.apiService.getCipherAdmin(this.cipherId);
|
||||
@ -85,12 +97,20 @@ export class AttachmentsComponent extends BaseAttachmentsComponent implements On
|
||||
return this.cipherService.saveAttachmentWithServer(
|
||||
this.cipherDomain,
|
||||
file,
|
||||
this.organization.canEditAllCiphers(this.flexibleCollectionsV1Enabled),
|
||||
this.organization.canEditAllCiphers(
|
||||
this.flexibleCollectionsV1Enabled,
|
||||
this.restrictProviderAccess,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
protected deleteCipherAttachment(attachmentId: string) {
|
||||
if (!this.organization.canEditAllCiphers(this.flexibleCollectionsV1Enabled)) {
|
||||
if (
|
||||
!this.organization.canEditAllCiphers(
|
||||
this.flexibleCollectionsV1Enabled,
|
||||
this.restrictProviderAccess,
|
||||
)
|
||||
) {
|
||||
return super.deleteCipherAttachment(attachmentId);
|
||||
}
|
||||
return this.apiService.deleteCipherAttachmentAdmin(this.cipherId, attachmentId);
|
||||
@ -99,7 +119,10 @@ export class AttachmentsComponent extends BaseAttachmentsComponent implements On
|
||||
protected showFixOldAttachments(attachment: AttachmentView) {
|
||||
return (
|
||||
attachment.key == null &&
|
||||
this.organization.canEditAllCiphers(this.flexibleCollectionsV1Enabled)
|
||||
this.organization.canEditAllCiphers(
|
||||
this.flexibleCollectionsV1Enabled,
|
||||
this.restrictProviderAccess,
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -71,9 +71,12 @@ export class BulkCollectionAssignmentDialogComponent implements OnDestroy, OnIni
|
||||
|
||||
async ngOnInit() {
|
||||
const v1FCEnabled = await this.configService.getFeatureFlag(FeatureFlag.FlexibleCollectionsV1);
|
||||
const restrictProviderAccess = await this.configService.getFeatureFlag(
|
||||
FeatureFlag.RestrictProviderAccess,
|
||||
);
|
||||
const org = await this.organizationService.get(this.params.organizationId);
|
||||
|
||||
if (org.canEditAllCiphers(v1FCEnabled)) {
|
||||
if (org.canEditAllCiphers(v1FCEnabled, restrictProviderAccess)) {
|
||||
this.editableItems = this.params.ciphers;
|
||||
} else {
|
||||
this.editableItems = this.params.ciphers.filter((c) => c.edit);
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { Component, EventEmitter, Output } from "@angular/core";
|
||||
import { Component, EventEmitter, Input, Output } from "@angular/core";
|
||||
|
||||
import { ButtonModule, NoItemsModule, svgIcon } from "@bitwarden/components";
|
||||
|
||||
@ -22,12 +22,18 @@ const icon = svgIcon`<svg xmlns="http://www.w3.org/2000/svg" width="120" height=
|
||||
buttonType="secondary"
|
||||
type="button"
|
||||
>
|
||||
<i aria-hidden="true" class="bwi bwi-pencil-square"></i> {{ "viewCollection" | i18n }}
|
||||
<i aria-hidden="true" class="bwi bwi-pencil-square"></i> {{ buttonText | i18n }}
|
||||
</button>
|
||||
</bit-no-items>`,
|
||||
})
|
||||
export class CollectionAccessRestrictedComponent {
|
||||
protected icon = icon;
|
||||
|
||||
@Input() canEditCollection = false;
|
||||
|
||||
@Output() viewCollectionClicked = new EventEmitter<void>();
|
||||
|
||||
get buttonText() {
|
||||
return this.canEditCollection ? "editCollection" : "viewCollection";
|
||||
}
|
||||
}
|
||||
|
@ -61,7 +61,10 @@ export class CollectionsComponent extends BaseCollectionsComponent {
|
||||
protected async loadCipher() {
|
||||
// if cipher is unassigned use apiService. We can see this by looking at this.collectionIds
|
||||
if (
|
||||
!this.organization.canEditAllCiphers(this.flexibleCollectionsV1Enabled) &&
|
||||
!this.organization.canEditAllCiphers(
|
||||
this.flexibleCollectionsV1Enabled,
|
||||
this.restrictProviderAccess,
|
||||
) &&
|
||||
this.collectionIds.length !== 0
|
||||
) {
|
||||
return await super.loadCipher();
|
||||
@ -86,7 +89,10 @@ export class CollectionsComponent extends BaseCollectionsComponent {
|
||||
|
||||
protected saveCollections() {
|
||||
if (
|
||||
this.organization.canEditAllCiphers(this.flexibleCollectionsV1Enabled) ||
|
||||
this.organization.canEditAllCiphers(
|
||||
this.flexibleCollectionsV1Enabled,
|
||||
this.restrictProviderAccess,
|
||||
) ||
|
||||
this.collectionIds.length === 0
|
||||
) {
|
||||
const request = new CipherCollectionsRequest(this.cipherDomain.collectionIds);
|
||||
|
@ -73,8 +73,16 @@
|
||||
</small>
|
||||
</ng-container>
|
||||
|
||||
<bit-search
|
||||
*ngIf="organization?.isProviderUser"
|
||||
class="tw-grow"
|
||||
[ngModel]="searchText"
|
||||
(ngModelChange)="onSearchTextChanged($event)"
|
||||
[placeholder]="'searchCollection' | i18n"
|
||||
></bit-search>
|
||||
|
||||
<div *ngIf="filter.type !== 'trash' && filter.collectionId !== Unassigned" class="tw-shrink-0">
|
||||
<div *ngIf="organization?.canCreateNewCollections" appListDropdown>
|
||||
<div *ngIf="canCreateCipher && canCreateCollection" appListDropdown>
|
||||
<button
|
||||
bitButton
|
||||
buttonType="primary"
|
||||
@ -97,7 +105,7 @@
|
||||
</bit-menu>
|
||||
</div>
|
||||
<button
|
||||
*ngIf="!organization?.canCreateNewCollections"
|
||||
*ngIf="canCreateCipher && !canCreateCollection"
|
||||
type="button"
|
||||
bitButton
|
||||
buttonType="primary"
|
||||
@ -106,5 +114,16 @@
|
||||
<i class="bwi bwi-plus bwi-fw" aria-hidden="true"></i>
|
||||
{{ "newItem" | i18n }}
|
||||
</button>
|
||||
|
||||
<button
|
||||
*ngIf="canCreateCollection && !canCreateCipher"
|
||||
type="button"
|
||||
bitButton
|
||||
buttonType="primary"
|
||||
(click)="addCollection()"
|
||||
>
|
||||
<i class="bwi bwi-plus bwi-fw" aria-hidden="true"></i>
|
||||
{{ "newCollection" | i18n }}
|
||||
</button>
|
||||
</div>
|
||||
</app-header>
|
||||
|
@ -43,6 +43,9 @@ export class VaultHeaderComponent implements OnInit {
|
||||
/** Currently selected collection */
|
||||
@Input() collection?: TreeNode<CollectionAdminView>;
|
||||
|
||||
/** The current search text in the header */
|
||||
@Input() searchText: string;
|
||||
|
||||
/** Emits an event when the new item button is clicked in the header */
|
||||
@Output() onAddCipher = new EventEmitter<void>();
|
||||
|
||||
@ -55,10 +58,14 @@ export class VaultHeaderComponent implements OnInit {
|
||||
/** Emits an event when the delete collection button is clicked in the header */
|
||||
@Output() onDeleteCollection = new EventEmitter<void>();
|
||||
|
||||
/** Emits an event when the search text changes in the header*/
|
||||
@Output() searchTextChanged = new EventEmitter<string>();
|
||||
|
||||
protected CollectionDialogTabType = CollectionDialogTabType;
|
||||
protected organizations$ = this.organizationService.organizations$;
|
||||
|
||||
private flexibleCollectionsV1Enabled = false;
|
||||
private restrictProviderAccessFlag = false;
|
||||
|
||||
constructor(
|
||||
private organizationService: OrganizationService,
|
||||
@ -73,6 +80,9 @@ export class VaultHeaderComponent implements OnInit {
|
||||
this.flexibleCollectionsV1Enabled = await firstValueFrom(
|
||||
this.configService.getFeatureFlag$(FeatureFlag.FlexibleCollectionsV1),
|
||||
);
|
||||
this.restrictProviderAccessFlag = await this.configService.getFeatureFlag(
|
||||
FeatureFlag.RestrictProviderAccess,
|
||||
);
|
||||
}
|
||||
|
||||
get title() {
|
||||
@ -197,7 +207,23 @@ export class VaultHeaderComponent implements OnInit {
|
||||
return this.collection.node.canDelete(this.organization);
|
||||
}
|
||||
|
||||
get canCreateCollection(): boolean {
|
||||
return this.organization?.canCreateNewCollections;
|
||||
}
|
||||
|
||||
get canCreateCipher(): boolean {
|
||||
if (this.organization?.isProviderUser && this.restrictProviderAccessFlag) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
deleteCollection() {
|
||||
this.onDeleteCollection.emit();
|
||||
}
|
||||
|
||||
onSearchTextChanged(t: string) {
|
||||
this.searchText = t;
|
||||
this.searchTextChanged.emit(t);
|
||||
}
|
||||
}
|
||||
|
@ -3,19 +3,20 @@
|
||||
[loading]="refreshing"
|
||||
[organization]="organization"
|
||||
[collection]="selectedCollection"
|
||||
[searchText]="currentSearchText$ | async"
|
||||
(onAddCipher)="addCipher()"
|
||||
(onAddCollection)="addCollection()"
|
||||
(onEditCollection)="editCollection(selectedCollection.node, $event.tab)"
|
||||
(onDeleteCollection)="deleteCollection(selectedCollection.node)"
|
||||
(searchTextChanged)="filterSearchText($event)"
|
||||
></app-org-vault-header>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-3">
|
||||
<div class="col-3" *ngIf="!organization?.isProviderUser">
|
||||
<div class="groupings">
|
||||
<div class="content">
|
||||
<div class="inner-content">
|
||||
<app-organization-vault-filter
|
||||
#vaultFilter
|
||||
[organization]="organization"
|
||||
[activeFilter]="activeFilter"
|
||||
[searchText]="currentSearchText$ | async"
|
||||
@ -25,7 +26,7 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-9">
|
||||
<div [class]="organization?.isProviderUser ? 'col-12' : 'col-9'">
|
||||
<bit-toggle-group
|
||||
*ngIf="showAddAccessToggle && activeFilter.selectedCollectionNode"
|
||||
[selected]="addAccessStatus$ | async"
|
||||
@ -114,8 +115,13 @@
|
||||
</bit-no-items>
|
||||
<collection-access-restricted
|
||||
*ngIf="showCollectionAccessRestricted"
|
||||
[canEditCollection]="organization.isProviderUser"
|
||||
(viewCollectionClicked)="
|
||||
editCollection(selectedCollection.node, CollectionDialogTabType.Info, true)
|
||||
editCollection(
|
||||
selectedCollection.node,
|
||||
CollectionDialogTabType.Info,
|
||||
!organization.isProviderUser
|
||||
)
|
||||
"
|
||||
>
|
||||
</collection-access-restricted>
|
||||
|
@ -100,7 +100,6 @@ import {
|
||||
BulkCollectionsDialogResult,
|
||||
} from "./bulk-collections-dialog";
|
||||
import { openOrgVaultCollectionsDialog } from "./collections.component";
|
||||
import { VaultFilterComponent } from "./vault-filter/vault-filter.component";
|
||||
|
||||
const BroadcasterSubscriptionId = "OrgVaultComponent";
|
||||
const SearchTextDebounceInterval = 200;
|
||||
@ -118,8 +117,6 @@ enum AddAccessStatusType {
|
||||
export class VaultComponent implements OnInit, OnDestroy {
|
||||
protected Unassigned = Unassigned;
|
||||
|
||||
@ViewChild("vaultFilter", { static: true })
|
||||
vaultFilterComponent: VaultFilterComponent;
|
||||
@ViewChild("attachments", { read: ViewContainerRef, static: true })
|
||||
attachmentsModalRef: ViewContainerRef;
|
||||
@ViewChild("cipherAddEdit", { read: ViewContainerRef, static: true })
|
||||
@ -151,6 +148,10 @@ export class VaultComponent implements OnInit, OnDestroy {
|
||||
protected showMissingCollectionPermissionMessage: boolean;
|
||||
protected showCollectionAccessRestricted: boolean;
|
||||
protected currentSearchText$: Observable<string>;
|
||||
/**
|
||||
* A list of collections that the user can assign items to and edit those items within.
|
||||
* @protected
|
||||
*/
|
||||
protected editableCollections$: Observable<CollectionView[]>;
|
||||
protected allCollectionsWithoutUnassigned$: Observable<CollectionAdminView[]>;
|
||||
private _flexibleCollectionsV1FlagEnabled: boolean;
|
||||
@ -160,6 +161,11 @@ export class VaultComponent implements OnInit, OnDestroy {
|
||||
}
|
||||
protected orgRevokedUsers: OrganizationUserUserDetailsResponse[];
|
||||
|
||||
private _restrictProviderAccessFlagEnabled: boolean;
|
||||
protected get restrictProviderAccessEnabled(): boolean {
|
||||
return this._restrictProviderAccessFlagEnabled && this.flexibleCollectionsV1Enabled;
|
||||
}
|
||||
|
||||
private searchText$ = new Subject<string>();
|
||||
private refresh$ = new BehaviorSubject<void>(null);
|
||||
private destroy$ = new Subject<void>();
|
||||
@ -207,6 +213,10 @@ export class VaultComponent implements OnInit, OnDestroy {
|
||||
FeatureFlag.FlexibleCollectionsV1,
|
||||
);
|
||||
|
||||
this._restrictProviderAccessFlagEnabled = await this.configService.getFeatureFlag(
|
||||
FeatureFlag.RestrictProviderAccess,
|
||||
);
|
||||
|
||||
const filter$ = this.routedVaultFilterService.filter$;
|
||||
const organizationId$ = filter$.pipe(
|
||||
map((filter) => filter.organizationId),
|
||||
@ -297,10 +307,20 @@ export class VaultComponent implements OnInit, OnDestroy {
|
||||
|
||||
this.editableCollections$ = this.allCollectionsWithoutUnassigned$.pipe(
|
||||
map((collections) => {
|
||||
// Users that can edit all ciphers can implicitly edit all collections
|
||||
if (this.organization.canEditAllCiphers(this.flexibleCollectionsV1Enabled)) {
|
||||
// If restricted, providers can not add items to any collections or edit those items
|
||||
if (this.organization.isProviderUser && this.restrictProviderAccessEnabled) {
|
||||
return [];
|
||||
}
|
||||
// Users that can edit all ciphers can implicitly add to / edit within any collection
|
||||
if (
|
||||
this.organization.canEditAllCiphers(
|
||||
this.flexibleCollectionsV1Enabled,
|
||||
this.restrictProviderAccessEnabled,
|
||||
)
|
||||
) {
|
||||
return collections;
|
||||
}
|
||||
// The user is only allowed to add/edit items to assigned collections that are not readonly
|
||||
return collections.filter((c) => c.assigned && !c.readOnly);
|
||||
}),
|
||||
shareReplay({ refCount: true, bufferSize: 1 }),
|
||||
@ -332,10 +352,19 @@ export class VaultComponent implements OnInit, OnDestroy {
|
||||
}
|
||||
let ciphers;
|
||||
|
||||
if (organization.isProviderUser && this.restrictProviderAccessEnabled) {
|
||||
return [];
|
||||
}
|
||||
|
||||
if (this.flexibleCollectionsV1Enabled) {
|
||||
// Flexible collections V1 logic.
|
||||
// If the user can edit all ciphers for the organization then fetch them ALL.
|
||||
if (organization.canEditAllCiphers(this.flexibleCollectionsV1Enabled)) {
|
||||
if (
|
||||
organization.canEditAllCiphers(
|
||||
this.flexibleCollectionsV1Enabled,
|
||||
this.restrictProviderAccessEnabled,
|
||||
)
|
||||
) {
|
||||
ciphers = await this.cipherService.getAllFromApiForOrganization(organization.id);
|
||||
} else {
|
||||
// Otherwise, only fetch ciphers they have access to (includes unassigned for admins).
|
||||
@ -343,7 +372,12 @@ export class VaultComponent implements OnInit, OnDestroy {
|
||||
}
|
||||
} else {
|
||||
// Pre-flexible collections logic, to be removed after flexible collections is fully released
|
||||
if (organization.canEditAllCiphers(this.flexibleCollectionsV1Enabled)) {
|
||||
if (
|
||||
organization.canEditAllCiphers(
|
||||
this.flexibleCollectionsV1Enabled,
|
||||
this.restrictProviderAccessEnabled,
|
||||
)
|
||||
) {
|
||||
ciphers = await this.cipherService.getAllFromApiForOrganization(organization.id);
|
||||
} else {
|
||||
ciphers = (await this.cipherService.getAllDecrypted()).filter(
|
||||
@ -443,9 +477,17 @@ export class VaultComponent implements OnInit, OnDestroy {
|
||||
organization$,
|
||||
]).pipe(
|
||||
map(([filter, collection, organization]) => {
|
||||
if (organization.isProviderUser && this.restrictProviderAccessEnabled) {
|
||||
return collection != undefined || filter.collectionId === Unassigned;
|
||||
}
|
||||
|
||||
return (
|
||||
(filter.collectionId === Unassigned && !organization.canEditUnassignedCiphers()) ||
|
||||
(!organization.canEditAllCiphers(this.flexibleCollectionsV1Enabled) &&
|
||||
(filter.collectionId === Unassigned &&
|
||||
!organization.canEditUnassignedCiphers(this.restrictProviderAccessEnabled)) ||
|
||||
(!organization.canEditAllCiphers(
|
||||
this.flexibleCollectionsV1Enabled,
|
||||
this.restrictProviderAccessEnabled,
|
||||
) &&
|
||||
collection != undefined &&
|
||||
!collection.node.assigned)
|
||||
);
|
||||
@ -490,7 +532,8 @@ export class VaultComponent implements OnInit, OnDestroy {
|
||||
map(([filter, collection, organization]) => {
|
||||
return (
|
||||
// Filtering by unassigned, show message if not admin
|
||||
(filter.collectionId === Unassigned && !organization.canEditUnassignedCiphers()) ||
|
||||
(filter.collectionId === Unassigned &&
|
||||
!organization.canEditUnassignedCiphers(this.restrictProviderAccessEnabled)) ||
|
||||
// Filtering by a collection, so show message if user is not assigned
|
||||
(collection != undefined &&
|
||||
!collection.node.assigned &&
|
||||
@ -513,7 +556,7 @@ export class VaultComponent implements OnInit, OnDestroy {
|
||||
|
||||
if (this.flexibleCollectionsV1Enabled) {
|
||||
canEditCipher =
|
||||
organization.canEditAllCiphers(true) ||
|
||||
organization.canEditAllCiphers(true, this.restrictProviderAccessEnabled) ||
|
||||
(await firstValueFrom(allCipherMap$))[cipherId] != undefined;
|
||||
} else {
|
||||
canEditCipher =
|
||||
@ -631,7 +674,10 @@ export class VaultComponent implements OnInit, OnDestroy {
|
||||
|
||||
const canEditCiphersCheck =
|
||||
this._flexibleCollectionsV1FlagEnabled &&
|
||||
!this.organization.canEditAllCiphers(this._flexibleCollectionsV1FlagEnabled);
|
||||
!this.organization.canEditAllCiphers(
|
||||
this._flexibleCollectionsV1FlagEnabled,
|
||||
this.restrictProviderAccessEnabled,
|
||||
);
|
||||
|
||||
// This custom type check will show addAccess badge for
|
||||
// Custom users with canEdit access AND owner/admin manage access setting is OFF
|
||||
@ -780,13 +826,13 @@ export class VaultComponent implements OnInit, OnDestroy {
|
||||
map((c) => {
|
||||
return c.sort((a, b) => {
|
||||
if (
|
||||
a.canEditItems(this.organization, true) &&
|
||||
!b.canEditItems(this.organization, true)
|
||||
a.canEditItems(this.organization, true, this.restrictProviderAccessEnabled) &&
|
||||
!b.canEditItems(this.organization, true, this.restrictProviderAccessEnabled)
|
||||
) {
|
||||
return -1;
|
||||
} else if (
|
||||
!a.canEditItems(this.organization, true) &&
|
||||
b.canEditItems(this.organization, true)
|
||||
!a.canEditItems(this.organization, true, this.restrictProviderAccessEnabled) &&
|
||||
b.canEditItems(this.organization, true, this.restrictProviderAccessEnabled)
|
||||
) {
|
||||
return 1;
|
||||
} else {
|
||||
@ -1247,7 +1293,10 @@ export class VaultComponent implements OnInit, OnDestroy {
|
||||
}
|
||||
|
||||
protected deleteCipherWithServer(id: string, permanent: boolean) {
|
||||
const asAdmin = this.organization?.canEditAllCiphers(this.flexibleCollectionsV1Enabled);
|
||||
const asAdmin = this.organization?.canEditAllCiphers(
|
||||
this.flexibleCollectionsV1Enabled,
|
||||
this.restrictProviderAccessEnabled,
|
||||
);
|
||||
return permanent
|
||||
? this.cipherService.deleteWithServer(id, asAdmin)
|
||||
: this.cipherService.softDeleteWithServer(id, asAdmin);
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { NgModule } from "@angular/core";
|
||||
|
||||
import { BreadcrumbsModule, NoItemsModule } from "@bitwarden/components";
|
||||
import { BreadcrumbsModule, NoItemsModule, SearchModule } from "@bitwarden/components";
|
||||
|
||||
import { LooseComponentsModule } from "../../shared/loose-components.module";
|
||||
import { SharedModule } from "../../shared/shared.module";
|
||||
@ -32,6 +32,7 @@ import { VaultComponent } from "./vault.component";
|
||||
CollectionDialogModule,
|
||||
CollectionAccessRestrictedComponent,
|
||||
NoItemsModule,
|
||||
SearchModule,
|
||||
],
|
||||
declarations: [VaultComponent, VaultHeaderComponent],
|
||||
exports: [VaultComponent],
|
||||
|
@ -25,6 +25,7 @@ export class CollectionsComponent implements OnInit {
|
||||
collections: CollectionView[] = [];
|
||||
organization: Organization;
|
||||
flexibleCollectionsV1Enabled: boolean;
|
||||
restrictProviderAccess: boolean;
|
||||
|
||||
protected cipherDomain: Cipher;
|
||||
|
||||
@ -42,6 +43,9 @@ export class CollectionsComponent implements OnInit {
|
||||
this.flexibleCollectionsV1Enabled = await this.configService.getFeatureFlag(
|
||||
FeatureFlag.FlexibleCollectionsV1,
|
||||
);
|
||||
this.restrictProviderAccess = await this.configService.getFeatureFlag(
|
||||
FeatureFlag.RestrictProviderAccess,
|
||||
);
|
||||
await this.load();
|
||||
}
|
||||
|
||||
@ -68,7 +72,12 @@ export class CollectionsComponent implements OnInit {
|
||||
async submit(): Promise<boolean> {
|
||||
const selectedCollectionIds = this.collections
|
||||
.filter((c) => {
|
||||
if (this.organization.canEditAllCiphers(this.flexibleCollectionsV1Enabled)) {
|
||||
if (
|
||||
this.organization.canEditAllCiphers(
|
||||
this.flexibleCollectionsV1Enabled,
|
||||
this.restrictProviderAccess,
|
||||
)
|
||||
) {
|
||||
return !!(c as any).checked;
|
||||
} else {
|
||||
return !!(c as any).checked && c.readOnly == null;
|
||||
|
@ -91,6 +91,7 @@ export class AddEditComponent implements OnInit, OnDestroy {
|
||||
private previousCipherId: string;
|
||||
|
||||
protected flexibleCollectionsV1Enabled = false;
|
||||
protected restrictProviderAccess = false;
|
||||
|
||||
get fido2CredentialCreationDateValue(): string {
|
||||
const dateCreated = this.i18nService.t("dateCreated");
|
||||
@ -183,6 +184,9 @@ export class AddEditComponent implements OnInit, OnDestroy {
|
||||
this.flexibleCollectionsV1Enabled = await this.configService.getFeatureFlag(
|
||||
FeatureFlag.FlexibleCollectionsV1,
|
||||
);
|
||||
this.restrictProviderAccess = await this.configService.getFeatureFlag(
|
||||
FeatureFlag.RestrictProviderAccess,
|
||||
);
|
||||
|
||||
this.policyService
|
||||
.policyAppliesToActiveUser$(PolicyType.PersonalOwnership)
|
||||
@ -668,11 +672,14 @@ export class AddEditComponent implements OnInit, OnDestroy {
|
||||
|
||||
protected saveCipher(cipher: Cipher) {
|
||||
const isNotClone = this.editMode && !this.cloneMode;
|
||||
let orgAdmin = this.organization?.canEditAllCiphers(this.flexibleCollectionsV1Enabled);
|
||||
let orgAdmin = this.organization?.canEditAllCiphers(
|
||||
this.flexibleCollectionsV1Enabled,
|
||||
this.restrictProviderAccess,
|
||||
);
|
||||
|
||||
// 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?.canEditUnassignedCiphers();
|
||||
orgAdmin = this.organization?.canEditUnassignedCiphers(this.restrictProviderAccess);
|
||||
}
|
||||
|
||||
return this.cipher.id == null
|
||||
@ -681,14 +688,20 @@ export class AddEditComponent implements OnInit, OnDestroy {
|
||||
}
|
||||
|
||||
protected deleteCipher() {
|
||||
const asAdmin = this.organization?.canEditAllCiphers(this.flexibleCollectionsV1Enabled);
|
||||
const asAdmin = this.organization?.canEditAllCiphers(
|
||||
this.flexibleCollectionsV1Enabled,
|
||||
this.restrictProviderAccess,
|
||||
);
|
||||
return this.cipher.isDeleted
|
||||
? this.cipherService.deleteWithServer(this.cipher.id, asAdmin)
|
||||
: this.cipherService.softDeleteWithServer(this.cipher.id, asAdmin);
|
||||
}
|
||||
|
||||
protected restoreCipher() {
|
||||
const asAdmin = this.organization?.canEditAllCiphers(this.flexibleCollectionsV1Enabled);
|
||||
const asAdmin = this.organization?.canEditAllCiphers(
|
||||
this.flexibleCollectionsV1Enabled,
|
||||
this.restrictProviderAccess,
|
||||
);
|
||||
return this.cipherService.restoreWithServer(this.cipher.id, asAdmin);
|
||||
}
|
||||
|
||||
|
@ -203,22 +203,32 @@ export class Organization {
|
||||
);
|
||||
}
|
||||
|
||||
canEditUnassignedCiphers() {
|
||||
// TODO: Update this to exclude Providers if provider access is restricted in AC-1707
|
||||
canEditUnassignedCiphers(restrictProviderAccessFlagEnabled: boolean) {
|
||||
if (this.isProviderUser) {
|
||||
return !restrictProviderAccessFlagEnabled;
|
||||
}
|
||||
return this.isAdmin || this.permissions.editAnyCollection;
|
||||
}
|
||||
|
||||
canEditAllCiphers(flexibleCollectionsV1Enabled: boolean) {
|
||||
canEditAllCiphers(
|
||||
flexibleCollectionsV1Enabled: boolean,
|
||||
restrictProviderAccessFlagEnabled: boolean,
|
||||
) {
|
||||
// Before Flexible Collections, any admin or anyone with editAnyCollection permission could edit all ciphers
|
||||
if (!this.flexibleCollections || !flexibleCollectionsV1Enabled) {
|
||||
if (!this.flexibleCollections || !flexibleCollectionsV1Enabled || !this.flexibleCollections) {
|
||||
return this.isAdmin || this.permissions.editAnyCollection;
|
||||
}
|
||||
|
||||
if (this.isProviderUser) {
|
||||
return !restrictProviderAccessFlagEnabled;
|
||||
}
|
||||
|
||||
// Post Flexible Collections V1, the allowAdminAccessToAllCollectionItems flag can restrict admins
|
||||
// Providers and custom users with canEditAnyCollection are not affected by allowAdminAccessToAllCollectionItems flag
|
||||
// Custom users with canEditAnyCollection are not affected by allowAdminAccessToAllCollectionItems flag
|
||||
return (
|
||||
this.isProviderUser ||
|
||||
(this.type === OrganizationUserType.Custom && this.permissions.editAnyCollection) ||
|
||||
(this.allowAdminAccessToAllCollectionItems && this.isAdmin)
|
||||
(this.allowAdminAccessToAllCollectionItems &&
|
||||
(this.type === OrganizationUserType.Admin || this.type === OrganizationUserType.Owner))
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -17,6 +17,7 @@ export enum FeatureFlag {
|
||||
UnassignedItemsBanner = "unassigned-items-banner",
|
||||
EnableDeleteProvider = "AC-1218-delete-provider",
|
||||
ExtensionRefresh = "extension-refresh",
|
||||
RestrictProviderAccess = "restrict-provider-access",
|
||||
}
|
||||
|
||||
export type AllowedFeatureFlagTypes = boolean | number | string;
|
||||
@ -44,6 +45,7 @@ export const DefaultFeatureFlagValue = {
|
||||
[FeatureFlag.UnassignedItemsBanner]: FALSE,
|
||||
[FeatureFlag.EnableDeleteProvider]: FALSE,
|
||||
[FeatureFlag.ExtensionRefresh]: FALSE,
|
||||
[FeatureFlag.RestrictProviderAccess]: FALSE,
|
||||
} satisfies Record<FeatureFlag, AllowedFeatureFlagTypes>;
|
||||
|
||||
export type DefaultFeatureFlagValueType = typeof DefaultFeatureFlagValue;
|
||||
|
@ -39,7 +39,11 @@ export class CollectionView implements View, ITreeNodeObject {
|
||||
}
|
||||
}
|
||||
|
||||
canEditItems(org: Organization, v1FlexibleCollections: boolean): boolean {
|
||||
canEditItems(
|
||||
org: Organization,
|
||||
v1FlexibleCollections: boolean,
|
||||
restrictProviderAccess: 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.",
|
||||
@ -48,7 +52,7 @@ export class CollectionView implements View, ITreeNodeObject {
|
||||
|
||||
if (org?.flexibleCollections) {
|
||||
return (
|
||||
org?.canEditAllCiphers(v1FlexibleCollections) ||
|
||||
org?.canEditAllCiphers(v1FlexibleCollections, restrictProviderAccess) ||
|
||||
this.manage ||
|
||||
(this.assigned && !this.readOnly)
|
||||
);
|
||||
|
Loading…
Reference in New Issue
Block a user