[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:
Shane Melton 2024-05-07 12:35:28 -07:00 committed by GitHub
parent 27d4178287
commit 3a71322510
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
18 changed files with 273 additions and 57 deletions

View File

@ -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 {

View File

@ -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>

View File

@ -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;

View File

@ -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

View File

@ -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,
)
);
}
}

View File

@ -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);

View File

@ -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";
}
}

View File

@ -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);

View File

@ -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>

View File

@ -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);
}
}

View File

@ -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>

View File

@ -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);

View File

@ -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],

View File

@ -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;

View File

@ -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);
}

View File

@ -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))
);
}

View File

@ -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;

View File

@ -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)
);