[AC-1124] Restrict admins from accessing items in the Collections tab (#7537)

* [AC-1124] Add getManyFromApiForOrganization to cipher.service.ts

* [AC-1124] Use getManyFromApiForOrganization when a user does not have access to all ciphers

* [AC-1124] Vault changes
- Show new collection access restricted view
- Include unassigned ciphers for restricted admins
- Restrict collections when creating/cloning/editing ciphers

* [AC-1124] Update edit cipher on page navigation to check if user can access the cipher

* [AC-1124] Hide ciphers from restricted collections

* [AC-1124] Ensure providers are not shown collection access restricted view

* [AC-1124] Modify add-edit component to call the correct endpoint when a restricted admin attempts to add-edit a cipher

* [AC-1124] Fix bug after merge with main

* [AC-1124] Use private this._organization

* [AC-1124] Fix broken builds
This commit is contained in:
Shane Melton 2024-02-08 14:07:42 -08:00 committed by GitHub
parent 3ee27fc61f
commit 5c6245aaae
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
14 changed files with 284 additions and 76 deletions

View File

@ -10,6 +10,7 @@ import { AuditService } from "@bitwarden/common/abstractions/audit.service";
import { EventCollectionService } from "@bitwarden/common/abstractions/event/event-collection.service";
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
import { ConfigServiceAbstraction } from "@bitwarden/common/platform/abstractions/config/config.service.abstraction";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
@ -29,7 +30,7 @@ import BrowserPopupUtils from "../../../../platform/popup/browser-popup-utils";
import { PopupCloseWarningService } from "../../../../popup/services/popup-close-warning.service";
import { BrowserFido2UserInterfaceSession } from "../../../fido2/browser-fido2-user-interface.service";
import { fido2PopoutSessionData$ } from "../../utils/fido2-popout-session-data";
import { VaultPopoutType, closeAddEditVaultItemPopout } from "../../utils/vault-popout-window";
import { closeAddEditVaultItemPopout, VaultPopoutType } from "../../utils/vault-popout-window";
@Component({
selector: "app-vault-add-edit",
@ -66,6 +67,7 @@ export class AddEditComponent extends BaseAddEditComponent {
sendApiService: SendApiService,
dialogService: DialogService,
datePipe: DatePipe,
configService: ConfigServiceAbstraction,
) {
super(
cipherService,
@ -85,6 +87,7 @@ export class AddEditComponent extends BaseAddEditComponent {
dialogService,
window,
datePipe,
configService,
);
}

View File

@ -8,6 +8,7 @@ import { EventCollectionService } from "@bitwarden/common/abstractions/event/eve
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
import { BroadcasterService } from "@bitwarden/common/platform/abstractions/broadcaster.service";
import { ConfigServiceAbstraction } from "@bitwarden/common/platform/abstractions/config/config.service.abstraction";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
@ -48,6 +49,7 @@ export class AddEditComponent extends BaseAddEditComponent implements OnChanges,
sendApiService: SendApiService,
dialogService: DialogService,
datePipe: DatePipe,
configService: ConfigServiceAbstraction,
) {
super(
cipherService,
@ -67,6 +69,7 @@ export class AddEditComponent extends BaseAddEditComponent implements OnChanges,
dialogService,
window,
datePipe,
configService,
);
}

View File

@ -5,6 +5,7 @@ import { AuditService } from "@bitwarden/common/abstractions/audit.service";
import { EventCollectionService } from "@bitwarden/common/abstractions/event/event-collection.service";
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
import { ConfigServiceAbstraction } from "@bitwarden/common/platform/abstractions/config/config.service.abstraction";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
@ -50,6 +51,7 @@ export class EmergencyAddEditCipherComponent extends BaseAddEditComponent {
sendApiService: SendApiService,
dialogService: DialogService,
datePipe: DatePipe,
configService: ConfigServiceAbstraction,
) {
super(
cipherService,
@ -70,6 +72,7 @@ export class EmergencyAddEditCipherComponent extends BaseAddEditComponent {
sendApiService,
dialogService,
datePipe,
configService,
);
}

View File

@ -7,6 +7,7 @@ import { EventCollectionService } from "@bitwarden/common/abstractions/event/eve
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
import { EventType, ProductType } from "@bitwarden/common/enums";
import { ConfigServiceAbstraction } from "@bitwarden/common/platform/abstractions/config/config.service.abstraction";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
@ -62,6 +63,7 @@ export class AddEditComponent extends BaseAddEditComponent implements OnInit, On
sendApiService: SendApiService,
dialogService: DialogService,
datePipe: DatePipe,
configService: ConfigServiceAbstraction,
) {
super(
cipherService,
@ -81,6 +83,7 @@ export class AddEditComponent extends BaseAddEditComponent implements OnInit, On
dialogService,
window,
datePipe,
configService,
);
}

View File

@ -6,6 +6,7 @@ import { AuditService } from "@bitwarden/common/abstractions/audit.service";
import { EventCollectionService } from "@bitwarden/common/abstractions/event/event-collection.service";
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
import { ConfigServiceAbstraction } from "@bitwarden/common/platform/abstractions/config/config.service.abstraction";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
@ -52,6 +53,7 @@ export class AddEditComponent extends BaseAddEditComponent {
sendApiService: SendApiService,
dialogService: DialogService,
datePipe: DatePipe,
configService: ConfigServiceAbstraction,
) {
super(
cipherService,
@ -72,6 +74,7 @@ export class AddEditComponent extends BaseAddEditComponent {
sendApiService,
dialogService,
datePipe,
configService,
);
}
@ -81,7 +84,9 @@ export class AddEditComponent extends BaseAddEditComponent {
(this.ownershipOptions.length > 1 || !this.allowPersonal)
) {
if (this.organization != null) {
return this.cloneMode && this.organization.canEditAnyCollection;
return (
this.cloneMode && this.organization.canEditAllCiphers(this.flexibleCollectionsV1Enabled)
);
} else {
return !this.editMode || this.cloneMode;
}
@ -90,14 +95,14 @@ export class AddEditComponent extends BaseAddEditComponent {
}
protected loadCollections() {
if (!this.organization.canEditAnyCollection) {
if (!this.organization.canEditAllCiphers(this.flexibleCollectionsV1Enabled)) {
return super.loadCollections();
}
return Promise.resolve(this.collections);
}
protected async loadCipher() {
if (!this.organization.canEditAnyCollection) {
if (!this.organization.canEditAllCiphers(this.flexibleCollectionsV1Enabled)) {
return await super.loadCipher();
}
const response = await this.apiService.getCipherAdmin(this.cipherId);
@ -110,14 +115,14 @@ export class AddEditComponent extends BaseAddEditComponent {
}
protected encryptCipher() {
if (!this.organization.canEditAnyCollection) {
if (!this.organization.canEditAllCiphers(this.flexibleCollectionsV1Enabled)) {
return super.encryptCipher();
}
return this.cipherService.encrypt(this.cipher, null, null, this.originalCipher);
}
protected async deleteCipher() {
if (!this.organization.canEditAnyCollection) {
if (!this.organization.canEditAllCiphers(this.flexibleCollectionsV1Enabled)) {
return super.deleteCipher();
}
return this.cipher.isDeleted

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";
@ -13,9 +13,10 @@ const icon = svgIcon`<svg xmlns="http://www.w3.org/2000/svg" width="140" height=
selector: "collection-access-restricted",
standalone: true,
imports: [SharedModule, ButtonModule, NoItemsModule],
template: `<bit-no-items [icon]="icon">
template: `<bit-no-items [icon]="icon" class="tw-mt-2 tw-block">
<span slot="title" class="tw-mt-4 tw-block">{{ "collectionAccessRestricted" | i18n }}</span>
<button
*ngIf="canEdit"
slot="button"
bitButton
(click)="editInfoClicked.emit()"
@ -29,5 +30,7 @@ const icon = svgIcon`<svg xmlns="http://www.w3.org/2000/svg" width="140" height=
export class CollectionAccessRestrictedComponent {
protected icon = icon;
@Input() canEdit = false;
@Output() editInfoClicked = new EventEmitter<void>();
}

View File

@ -11,8 +11,8 @@ import { VaultFilterComponent as BaseVaultFilterComponent } from "../../individu
import { VaultFilterService } from "../../individual-vault/vault-filter/services/abstractions/vault-filter.service";
import {
VaultFilterList,
VaultFilterType,
VaultFilterSection,
VaultFilterType,
} from "../../individual-vault/vault-filter/shared/models/vault-filter-section.type";
import { CollectionFilter } from "../../individual-vault/vault-filter/shared/models/vault-filter.type";

View File

@ -61,30 +61,53 @@
[viewingOrgVault]="true"
>
</app-vault-items>
<div
class="tw-mt-6 tw-flex tw-h-full tw-flex-col tw-items-center tw-justify-start"
*ngIf="showMissingCollectionPermissionMessage"
>
<bit-icon [icon]="noItemIcon" aria-hidden="true"></bit-icon>
<p>{{ "noPermissionToViewAllCollectionItems" | i18n }}</p>
</div>
<div
class="tw-mt-6 tw-flex tw-h-full tw-flex-col tw-items-center tw-justify-start"
*ngIf="isEmpty && !showMissingCollectionPermissionMessage && !performingInitialLoad"
>
<bit-icon [icon]="noItemIcon" aria-hidden="true"></bit-icon>
<p>{{ "noItemsInList" | i18n }}</p>
<button
type="button"
buttonType="primary"
bitButton
(click)="addCipher()"
*ngIf="filter.type !== 'trash' && filter.collectionId !== Unassigned"
<ng-container *ngIf="!flexibleCollectionsV1Enabled">
<div
class="tw-mt-6 tw-flex tw-h-full tw-flex-col tw-items-center tw-justify-start"
*ngIf="showMissingCollectionPermissionMessage"
>
<i class="bwi bwi-plus bwi-fw" aria-hidden="true"></i>
{{ "newItem" | i18n }}
</button>
</div>
<bit-icon [icon]="noItemIcon" aria-hidden="true"></bit-icon>
<p>{{ "noPermissionToViewAllCollectionItems" | i18n }}</p>
</div>
<div
class="tw-mt-6 tw-flex tw-h-full tw-flex-col tw-items-center tw-justify-start"
*ngIf="isEmpty && !showMissingCollectionPermissionMessage && !performingInitialLoad"
>
<bit-icon [icon]="noItemIcon" aria-hidden="true"></bit-icon>
<p>{{ "noItemsInList" | i18n }}</p>
<button
type="button"
buttonType="primary"
bitButton
(click)="addCipher()"
*ngIf="filter.type !== 'trash' && filter.collectionId !== Unassigned"
>
<i class="bwi bwi-plus bwi-fw" aria-hidden="true"></i>
{{ "newItem" | i18n }}
</button>
</div>
</ng-container>
<ng-container *ngIf="flexibleCollectionsV1Enabled && !performingInitialLoad">
<bit-no-items *ngIf="isEmpty && !showCollectionAccessRestricted">
<span slot="title" class="tw-mt-4 tw-block">{{ "noItemsInList" | i18n }}</span>
<button
slot="button"
bitButton
(click)="addCipher()"
buttonType="primary"
type="button"
*ngIf="filter.type !== 'trash' && filter.collectionId !== Unassigned"
>
<i aria-hidden="true" class="bwi bwi-plus"></i> {{ "newItem" | i18n }}
</button>
</bit-no-items>
<collection-access-restricted
*ngIf="showCollectionAccessRestricted"
[canEdit]="selectedCollection != null && selectedCollection.node.canEdit(organization)"
(editInfoClicked)="editCollection(selectedCollection.node, CollectionDialogTabType.Info)"
>
</collection-access-restricted>
</ng-container>
<div
class="tw-mt-6 tw-flex tw-h-full tw-flex-col tw-items-center tw-justify-start"
*ngIf="performingInitialLoad"

View File

@ -127,13 +127,20 @@ export class VaultComponent implements OnInit, OnDestroy {
protected collections: CollectionAdminView[];
protected selectedCollection: TreeNode<CollectionAdminView> | undefined;
protected isEmpty: boolean;
/**
* Used to show an old missing permission message for custom users with DeleteAnyCollection
* @deprecated Replaced with showCollectionAccessRestricted$ and this should be removed after flexible collections V1
* is released
*/
protected showMissingCollectionPermissionMessage: boolean;
protected showCollectionAccessRestricted: boolean;
protected currentSearchText$: Observable<string>;
protected editableCollections$: Observable<CollectionView[]>;
protected showBulkEditCollectionAccess$ = this.configService.getFeatureFlag$(
FeatureFlag.BulkCollectionAccess,
false,
);
protected flexibleCollectionsEnabled: boolean;
protected flexibleCollectionsV1Enabled: boolean;
private searchText$ = new Subject<string>();
private refresh$ = new BehaviorSubject<void>(null);
@ -176,6 +183,11 @@ export class VaultComponent implements OnInit, OnDestroy {
: "trashCleanupWarning",
);
this.flexibleCollectionsV1Enabled = await this.configService.getFeatureFlag(
FeatureFlag.FlexibleCollectionsV1,
false,
);
const filter$ = this.routedVaultFilterService.filter$;
const organizationId$ = filter$.pipe(
map((filter) => filter.organizationId),
@ -259,6 +271,22 @@ export class VaultComponent implements OnInit, OnDestroy {
shareReplay({ refCount: true, bufferSize: 1 }),
);
this.editableCollections$ = allCollectionsWithoutUnassigned$.pipe(
map((collections) => {
if (
this.organization.canEditAnyCollection &&
this.organization.allowAdminAccessToAllCollectionItems
) {
return collections;
}
if (this.organization.isProviderUser) {
return collections;
}
return collections.filter((c) => c.assigned && !c.readOnly);
}),
shareReplay({ refCount: true, bufferSize: 1 }),
);
const allCollections$ = combineLatest([organizationId$, allCollectionsWithoutUnassigned$]).pipe(
map(([organizationId, allCollections]) => {
const noneCollection = new CollectionAdminView();
@ -277,32 +305,35 @@ export class VaultComponent implements OnInit, OnDestroy {
const allCiphers$ = organization$.pipe(
concatMap(async (organization) => {
let ciphers;
if (organization.canEditAnyCollection) {
ciphers = await this.cipherService.getAllFromApiForOrganization(organization.id);
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)) {
ciphers = await this.cipherService.getAllFromApiForOrganization(organization.id);
} else {
// Otherwise, only fetch ciphers they have access to (includes unassigned for admins).
ciphers = await this.cipherService.getManyFromApiForOrganization(organization.id);
}
} else {
ciphers = (await this.cipherService.getAllDecrypted()).filter(
(c) => c.organizationId === organization.id,
);
// Pre-flexible collections logic, to be removed after flexible collections is fully released
if (organization.canEditAnyCollection) {
ciphers = await this.cipherService.getAllFromApiForOrganization(organization.id);
} else {
ciphers = (await this.cipherService.getAllDecrypted()).filter(
(c) => c.organizationId === organization.id,
);
}
}
await this.searchService.indexCiphers(ciphers, organization.id);
this.searchService.indexCiphers(ciphers, organization.id);
return ciphers;
}),
);
const ciphers$ = combineLatest([allCiphers$, filter$, this.currentSearchText$]).pipe(
filter(([ciphers, filter]) => ciphers != undefined && filter != undefined),
concatMap(async ([ciphers, filter, searchText]) => {
if (filter.collectionId === undefined && filter.type === undefined) {
return [];
}
const filterFunction = createFilterFunction(filter);
if (this.searchService.isSearchable(searchText)) {
return await this.searchService.searchCiphers(searchText, [filterFunction], ciphers);
}
return ciphers.filter(filterFunction);
const allCipherMap$ = allCiphers$.pipe(
map((ciphers) => {
return Object.fromEntries(ciphers.map((c) => [c.id, c]));
}),
shareReplay({ refCount: true, bufferSize: 1 }),
);
@ -364,6 +395,52 @@ export class VaultComponent implements OnInit, OnDestroy {
shareReplay({ refCount: true, bufferSize: 1 }),
);
const showCollectionAccessRestricted$ = combineLatest([
filter$,
selectedCollection$,
organization$,
]).pipe(
map(([filter, collection, organization]) => {
return (
(filter.collectionId === Unassigned && !organization.canUseAdminCollections) ||
(!organization.allowAdminAccessToAllCollectionItems &&
!organization.canEditAllCiphers(this.flexibleCollectionsV1Enabled) &&
collection != undefined &&
!collection.node.assigned)
);
}),
shareReplay({ refCount: true, bufferSize: 1 }),
);
const ciphers$ = combineLatest([
allCiphers$,
filter$,
this.currentSearchText$,
showCollectionAccessRestricted$,
]).pipe(
filter(([ciphers, filter]) => ciphers != undefined && filter != undefined),
concatMap(async ([ciphers, filter, searchText, showCollectionAccessRestricted]) => {
if (filter.collectionId === undefined && filter.type === undefined) {
return [];
}
if (this.flexibleCollectionsV1Enabled && showCollectionAccessRestricted) {
// Do not show ciphers for restricted collections
// Ciphers belonging to multiple collections may still be present in $allCiphers and shouldn't be visible
return [];
}
const filterFunction = createFilterFunction(filter);
if (this.searchService.isSearchable(searchText)) {
return await this.searchService.searchCiphers(searchText, [filterFunction], ciphers);
}
return ciphers.filter(filterFunction);
}),
shareReplay({ refCount: true, bufferSize: 1 }),
);
const showMissingCollectionPermissionMessage$ = combineLatest([
filter$,
selectedCollection$,
@ -390,23 +467,28 @@ export class VaultComponent implements OnInit, OnDestroy {
if (!cipherId) {
return;
}
if (
// Handle users with implicit collection access since they use the admin endpoint
organization.canUseAdminCollections ||
(await this.cipherService.get(cipherId)) != null
) {
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
// eslint-disable-next-line @typescript-eslint/no-floating-promises
this.editCipherId(cipherId);
let canEditCipher: boolean;
if (this.flexibleCollectionsV1Enabled) {
canEditCipher =
organization.canEditAllCiphers(true) ||
(await firstValueFrom(allCipherMap$))[cipherId] != undefined;
} else {
canEditCipher =
organization.canUseAdminCollections ||
(await this.cipherService.get(cipherId)) != null;
}
if (canEditCipher) {
await this.editCipherId(cipherId);
} else {
this.platformUtilsService.showToast(
"error",
this.i18nService.t("errorOccurred"),
this.i18nService.t("unknownCipher"),
);
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
// eslint-disable-next-line @typescript-eslint/no-floating-promises
this.router.navigate([], {
await this.router.navigate([], {
queryParams: { cipherId: null, itemId: null },
queryParamsHandling: "merge",
});
@ -461,6 +543,7 @@ export class VaultComponent implements OnInit, OnDestroy {
collections$,
selectedCollection$,
showMissingCollectionPermissionMessage$,
showCollectionAccessRestricted$,
]),
),
takeUntil(this.destroy$),
@ -475,6 +558,7 @@ export class VaultComponent implements OnInit, OnDestroy {
collections,
selectedCollection,
showMissingCollectionPermissionMessage,
showCollectionAccessRestricted,
]) => {
this.organization = organization;
this.filter = filter;
@ -484,6 +568,8 @@ export class VaultComponent implements OnInit, OnDestroy {
this.collections = collections;
this.selectedCollection = selectedCollection;
this.showMissingCollectionPermissionMessage = showMissingCollectionPermissionMessage;
this.showCollectionAccessRestricted = showCollectionAccessRestricted;
this.isEmpty = collections?.length === 0 && ciphers?.length === 0;
// This is a temporary fix to avoid double fetching collections.
@ -591,13 +677,22 @@ export class VaultComponent implements OnInit, OnDestroy {
}
async editCipherCollections(cipher: CipherView) {
const currCollections = await firstValueFrom(this.vaultFilterService.filteredCollections$);
let collections: CollectionView[] = [];
if (this.flexibleCollectionsV1Enabled) {
// V1 limits admins to only adding items to collections they have access to.
collections = await firstValueFrom(this.editableCollections$);
} else {
collections = (await firstValueFrom(this.vaultFilterService.filteredCollections$)).filter(
(c) => !c.readOnly && c.id != Unassigned,
);
}
const [modal] = await this.modalService.openViewRef(
CollectionsComponent,
this.collectionsModalRef,
(comp) => {
comp.collectionIds = cipher.collectionIds;
comp.collections = currCollections.filter((c) => !c.readOnly && c.id != Unassigned);
comp.collections = collections;
comp.organization = this.organization;
comp.cipherId = cipher.id;
comp.onSavedCollections.pipe(takeUntil(this.destroy$)).subscribe(() => {
@ -609,9 +704,16 @@ export class VaultComponent implements OnInit, OnDestroy {
}
async addCipher() {
const collections = (await firstValueFrom(this.vaultFilterService.filteredCollections$)).filter(
(c) => !c.readOnly && c.id != Unassigned,
);
let collections: CollectionView[] = [];
if (this.flexibleCollectionsV1Enabled) {
// V1 limits admins to only adding items to collections they have access to.
collections = await firstValueFrom(this.editableCollections$);
} else {
collections = (await firstValueFrom(this.vaultFilterService.filteredCollections$)).filter(
(c) => !c.readOnly && c.id != Unassigned,
);
}
await this.editCipher(null, (comp) => {
comp.type = this.activeFilter.cipherType;
@ -701,9 +803,16 @@ export class VaultComponent implements OnInit, OnDestroy {
}
}
const collections = (await firstValueFrom(this.vaultFilterService.filteredCollections$)).filter(
(c) => !c.readOnly && c.id != Unassigned,
);
let collections: CollectionView[] = [];
if (this.flexibleCollectionsV1Enabled) {
// V1 limits admins to only adding items to collections they have access to.
collections = await firstValueFrom(this.editableCollections$);
} else {
collections = (await firstValueFrom(this.vaultFilterService.filteredCollections$)).filter(
(c) => !c.readOnly && c.id != Unassigned,
);
}
await this.editCipher(cipher, (comp) => {
comp.cloneMode = true;
@ -1008,6 +1117,8 @@ export class VaultComponent implements OnInit, OnDestroy {
replaceUrl: true,
});
}
protected readonly CollectionDialogTabType = CollectionDialogTabType;
}
/**

View File

@ -1,6 +1,6 @@
import { NgModule } from "@angular/core";
import { BreadcrumbsModule } from "@bitwarden/components";
import { BreadcrumbsModule, NoItemsModule } from "@bitwarden/components";
import { LooseComponentsModule } from "../../shared/loose-components.module";
import { SharedModule } from "../../shared/shared.module";
@ -31,6 +31,7 @@ import { VaultComponent } from "./vault.component";
VaultItemsModule,
CollectionDialogModule,
CollectionAccessRestrictedComponent,
NoItemsModule,
],
declarations: [VaultComponent, VaultHeaderComponent],
exports: [VaultComponent],

View File

@ -1,6 +1,6 @@
import { DatePipe } from "@angular/common";
import { Directive, EventEmitter, Input, OnDestroy, OnInit, Output } from "@angular/core";
import { Observable, Subject, takeUntil, concatMap } from "rxjs";
import { concatMap, Observable, Subject, takeUntil } from "rxjs";
import { AuditService } from "@bitwarden/common/abstractions/audit.service";
import { EventCollectionService } from "@bitwarden/common/abstractions/event/event-collection.service";
@ -12,6 +12,8 @@ import { PolicyService } from "@bitwarden/common/admin-console/abstractions/poli
import { OrganizationUserStatusType, PolicyType } from "@bitwarden/common/admin-console/enums";
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
import { EventType } from "@bitwarden/common/enums";
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
import { ConfigServiceAbstraction } from "@bitwarden/common/platform/abstractions/config/config.service.abstraction";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
@ -22,7 +24,7 @@ import { SendApiService } from "@bitwarden/common/tools/send/services/send-api.s
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
import { CollectionService } from "@bitwarden/common/vault/abstractions/collection.service";
import { FolderService } from "@bitwarden/common/vault/abstractions/folder/folder.service.abstraction";
import { SecureNoteType, UriMatchType, CipherType } from "@bitwarden/common/vault/enums";
import { CipherType, SecureNoteType, UriMatchType } from "@bitwarden/common/vault/enums";
import { CipherRepromptType } from "@bitwarden/common/vault/enums/cipher-reprompt-type";
import { Cipher } from "@bitwarden/common/vault/models/domain/cipher";
import { CardView } from "@bitwarden/common/vault/models/view/card.view";
@ -87,6 +89,8 @@ export class AddEditComponent implements OnInit, OnDestroy {
private personalOwnershipPolicyAppliesToActiveUser: boolean;
private previousCipherId: string;
protected flexibleCollectionsV1Enabled = false;
get fido2CredentialCreationDateValue(): string {
const dateCreated = this.i18nService.t("dateCreated");
const creationDate = this.datePipe.transform(
@ -114,6 +118,7 @@ export class AddEditComponent implements OnInit, OnDestroy {
protected dialogService: DialogService,
protected win: Window,
protected datePipe: DatePipe,
protected configService: ConfigServiceAbstraction,
) {
this.typeOptions = [
{ name: i18nService.t("typeLogin"), value: CipherType.Login },
@ -174,6 +179,10 @@ export class AddEditComponent implements OnInit, OnDestroy {
}
async ngOnInit() {
this.flexibleCollectionsV1Enabled = await this.configService.getFeatureFlag(
FeatureFlag.FlexibleCollectionsV1,
false,
);
this.writeableCollections = await this.loadCollections();
this.canUseReprompt = await this.passwordRepromptService.enabled();
@ -650,7 +659,13 @@ export class AddEditComponent implements OnInit, OnDestroy {
protected saveCipher(cipher: Cipher) {
const isNotClone = this.editMode && !this.cloneMode;
const orgAdmin = this.organization?.isAdmin;
let orgAdmin = this.organization?.isAdmin;
if (this.flexibleCollectionsV1Enabled) {
// Flexible Collections V1 restricts admins, check the organization setting via canEditAllCiphers
orgAdmin = this.organization?.canEditAllCiphers(true);
}
return this.cipher.id == null
? this.cipherService.createWithServer(cipher, orgAdmin)
: this.cipherService.updateWithServer(cipher, orgAdmin, isNotClone);

View File

@ -196,6 +196,20 @@ export class Organization {
return this.canEditAnyCollection;
}
canEditAllCiphers(flexibleCollectionsV1Enabled: boolean) {
// Before Flexible Collections, anyone with editAnyCollection permission could edit all ciphers
if (!flexibleCollectionsV1Enabled) {
return this.canEditAnyCollection;
}
// Post Flexible Collections V1, the allowAdminAccessToAllCollectionItems flag can restrict admins
// Providers are not affected by allowAdminAccessToAllCollectionItems flag
// note: canEditAnyCollection may change in the V1 to also ignore the allowAdminAccessToAllCollectionItems flag
return (
this.isProviderUser ||
(this.allowAdminAccessToAllCollectionItems && this.canEditAnyCollection)
);
}
get canDeleteAnyCollection() {
return this.isAdmin || this.permissions.deleteAnyCollection;
}

View File

@ -27,6 +27,11 @@ export abstract class CipherService {
defaultMatch?: UriMatchType,
) => Promise<CipherView[]>;
getAllFromApiForOrganization: (organizationId: string) => Promise<CipherView[]>;
/**
* Gets ciphers belonging to the specified organization that the user has explicit collection level access to.
* Ciphers that are not assigned to any collections are only included for users with admin access.
*/
getManyFromApiForOrganization: (organizationId: string) => Promise<CipherView[]>;
getLastUsedForUrl: (url: string, autofillOnPageLoad: boolean) => Promise<CipherView>;
getLastLaunchedForUrl: (url: string, autofillOnPageLoad: boolean) => Promise<CipherView>;
getNextCipherForUrl: (url: string) => Promise<CipherView>;

View File

@ -5,6 +5,7 @@ import { ApiService } from "../../abstractions/api.service";
import { SearchService } from "../../abstractions/search.service";
import { SettingsService } from "../../abstractions/settings.service";
import { ErrorResponse } from "../../models/response/error.response";
import { ListResponse } from "../../models/response/list.response";
import { View } from "../../models/view/view";
import { ConfigServiceAbstraction } from "../../platform/abstractions/config/config.service.abstraction";
import { CryptoService } from "../../platform/abstractions/crypto.service";
@ -387,6 +388,24 @@ export class CipherService implements CipherServiceAbstraction {
async getAllFromApiForOrganization(organizationId: string): Promise<CipherView[]> {
const response = await this.apiService.getCiphersOrganization(organizationId);
return await this.decryptOrganizationCiphersResponse(response, organizationId);
}
async getManyFromApiForOrganization(organizationId: string): Promise<CipherView[]> {
const response = await this.apiService.send(
"GET",
"/ciphers/organization-details/assigned?organizationId=" + organizationId,
null,
true,
true,
);
return this.decryptOrganizationCiphersResponse(response, organizationId);
}
private async decryptOrganizationCiphersResponse(
response: ListResponse<CipherResponse>,
organizationId: string,
): Promise<CipherView[]> {
if (response?.data == null || response.data.length < 1) {
return [];
}