diff --git a/apps/web/src/app/vault/individual-vault/bulk-action-dialogs/bulk-delete-dialog/bulk-delete-dialog.component.ts b/apps/web/src/app/vault/individual-vault/bulk-action-dialogs/bulk-delete-dialog/bulk-delete-dialog.component.ts index a678a05ae3..f49c54ac32 100644 --- a/apps/web/src/app/vault/individual-vault/bulk-action-dialogs/bulk-delete-dialog/bulk-delete-dialog.component.ts +++ b/apps/web/src/app/vault/individual-vault/bulk-action-dialogs/bulk-delete-dialog/bulk-delete-dialog.component.ts @@ -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, @@ -81,10 +85,11 @@ export class BulkDeleteDialogComponent { const deletePromises: Promise[] = []; 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 { 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 { 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 5adf9c4e58..d9c2145f0b 100644 --- a/apps/web/src/app/vault/individual-vault/collections.component.html +++ b/apps/web/src/app/vault/individual-vault/collections.component.html @@ -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 }} 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 3bf9181905..af9c3476bd 100644 --- a/apps/web/src/app/vault/individual-vault/collections.component.ts +++ b/apps/web/src/app/vault/individual-vault/collections.component.ts @@ -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; diff --git a/apps/web/src/app/vault/org-vault/add-edit.component.ts b/apps/web/src/app/vault/org-vault/add-edit.component.ts index 01e4dbaadf..82055cc916 100644 --- a/apps/web/src/app/vault/org-vault/add-edit.component.ts +++ b/apps/web/src/app/vault/org-vault/add-edit.component.ts @@ -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 diff --git a/apps/web/src/app/vault/org-vault/attachments.component.ts b/apps/web/src/app/vault/org-vault/attachments.component.ts index 2aecf277e6..30189e8021 100644 --- a/apps/web/src/app/vault/org-vault/attachments.component.ts +++ b/apps/web/src/app/vault/org-vault/attachments.component.ts @@ -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, + ) ); } } diff --git a/apps/web/src/app/vault/org-vault/bulk-collection-assignment-dialog/bulk-collection-assignment-dialog.component.ts b/apps/web/src/app/vault/org-vault/bulk-collection-assignment-dialog/bulk-collection-assignment-dialog.component.ts index e9f8401d73..e13ef49fc3 100644 --- a/apps/web/src/app/vault/org-vault/bulk-collection-assignment-dialog/bulk-collection-assignment-dialog.component.ts +++ b/apps/web/src/app/vault/org-vault/bulk-collection-assignment-dialog/bulk-collection-assignment-dialog.component.ts @@ -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); diff --git a/apps/web/src/app/vault/org-vault/collection-access-restricted.component.ts b/apps/web/src/app/vault/org-vault/collection-access-restricted.component.ts index 337d73b315..7a51f01577 100644 --- a/apps/web/src/app/vault/org-vault/collection-access-restricted.component.ts +++ b/apps/web/src/app/vault/org-vault/collection-access-restricted.component.ts @@ -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` - {{ "viewCollection" | i18n }} + {{ buttonText | i18n }} `, }) export class CollectionAccessRestrictedComponent { protected icon = icon; + @Input() canEditCollection = false; + @Output() viewCollectionClicked = new EventEmitter(); + + get buttonText() { + return this.canEditCollection ? "editCollection" : "viewCollection"; + } } 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 89e4884559..557b048a7b 100644 --- a/apps/web/src/app/vault/org-vault/collections.component.ts +++ b/apps/web/src/app/vault/org-vault/collections.component.ts @@ -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); diff --git a/apps/web/src/app/vault/org-vault/vault-header/vault-header.component.html b/apps/web/src/app/vault/org-vault/vault-header/vault-header.component.html index 97d99d5821..8388f4ea9d 100644 --- a/apps/web/src/app/vault/org-vault/vault-header/vault-header.component.html +++ b/apps/web/src/app/vault/org-vault/vault-header/vault-header.component.html @@ -73,8 +73,16 @@ + +
-
+
+ +
diff --git a/apps/web/src/app/vault/org-vault/vault-header/vault-header.component.ts b/apps/web/src/app/vault/org-vault/vault-header/vault-header.component.ts index a5cd468008..eecd2f434a 100644 --- a/apps/web/src/app/vault/org-vault/vault-header/vault-header.component.ts +++ b/apps/web/src/app/vault/org-vault/vault-header/vault-header.component.ts @@ -43,6 +43,9 @@ export class VaultHeaderComponent implements OnInit { /** Currently selected collection */ @Input() collection?: TreeNode; + /** 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(); @@ -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(); + /** Emits an event when the search text changes in the header*/ + @Output() searchTextChanged = new EventEmitter(); + 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); + } } diff --git a/apps/web/src/app/vault/org-vault/vault.component.html b/apps/web/src/app/vault/org-vault/vault.component.html index af7b5059e5..096389021f 100644 --- a/apps/web/src/app/vault/org-vault/vault.component.html +++ b/apps/web/src/app/vault/org-vault/vault.component.html @@ -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)" >
-
+
-
+
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 f037170dda..103b29fad7 100644 --- a/apps/web/src/app/vault/org-vault/vault.component.ts +++ b/apps/web/src/app/vault/org-vault/vault.component.ts @@ -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; + /** + * A list of collections that the user can assign items to and edit those items within. + * @protected + */ protected editableCollections$: Observable; protected allCollectionsWithoutUnassigned$: Observable; 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(); private refresh$ = new BehaviorSubject(null); private destroy$ = new Subject(); @@ -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); diff --git a/apps/web/src/app/vault/org-vault/vault.module.ts b/apps/web/src/app/vault/org-vault/vault.module.ts index 47365bb4b1..a478307123 100644 --- a/apps/web/src/app/vault/org-vault/vault.module.ts +++ b/apps/web/src/app/vault/org-vault/vault.module.ts @@ -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], diff --git a/libs/angular/src/admin-console/components/collections.component.ts b/libs/angular/src/admin-console/components/collections.component.ts index d1f4f93072..445727ac61 100644 --- a/libs/angular/src/admin-console/components/collections.component.ts +++ b/libs/angular/src/admin-console/components/collections.component.ts @@ -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 { 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; diff --git a/libs/angular/src/vault/components/add-edit.component.ts b/libs/angular/src/vault/components/add-edit.component.ts index 0397a7a663..74c368d726 100644 --- a/libs/angular/src/vault/components/add-edit.component.ts +++ b/libs/angular/src/vault/components/add-edit.component.ts @@ -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); } diff --git a/libs/common/src/admin-console/models/domain/organization.ts b/libs/common/src/admin-console/models/domain/organization.ts index bdf0b8fbbf..04840477df 100644 --- a/libs/common/src/admin-console/models/domain/organization.ts +++ b/libs/common/src/admin-console/models/domain/organization.ts @@ -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)) ); } diff --git a/libs/common/src/enums/feature-flag.enum.ts b/libs/common/src/enums/feature-flag.enum.ts index 5ed3724f2f..221b251f3c 100644 --- a/libs/common/src/enums/feature-flag.enum.ts +++ b/libs/common/src/enums/feature-flag.enum.ts @@ -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; export type DefaultFeatureFlagValueType = typeof DefaultFeatureFlagValue; diff --git a/libs/common/src/vault/models/view/collection.view.ts b/libs/common/src/vault/models/view/collection.view.ts index f742b283bd..ebc0229f4e 100644 --- a/libs/common/src/vault/models/view/collection.view.ts +++ b/libs/common/src/vault/models/view/collection.view.ts @@ -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) );