diff --git a/apps/web/src/app/platform/ipc/web-ipc.service.ts b/apps/web/src/app/platform/ipc/web-ipc.service.ts
index e088de2473b..06f3c660218 100644
--- a/apps/web/src/app/platform/ipc/web-ipc.service.ts
+++ b/apps/web/src/app/platform/ipc/web-ipc.service.ts
@@ -27,7 +27,7 @@ export class WebIpcService extends IpcService {
type: "bitwarden-ipc-message",
message: {
destination: message.destination,
- payload: message.payload,
+ payload: [...message.payload],
topic: message.topic,
},
} satisfies IpcMessage,
@@ -50,9 +50,16 @@ export class WebIpcService extends IpcService {
return;
}
- this.communicationBackend?.deliver_message(
+ if (
+ typeof message.message.destination !== "object" ||
+ message.message.destination.Web == undefined
+ ) {
+ return;
+ }
+
+ this.communicationBackend?.receive(
new IncomingMessage(
- message.message.payload,
+ new Uint8Array(message.message.payload),
message.message.destination,
"BrowserBackground",
message.message.topic,
diff --git a/apps/web/src/app/shared/loose-components.module.ts b/apps/web/src/app/shared/loose-components.module.ts
index 44323614f17..63e54c46a8f 100644
--- a/apps/web/src/app/shared/loose-components.module.ts
+++ b/apps/web/src/app/shared/loose-components.module.ts
@@ -26,6 +26,7 @@ import { UpdatePasswordComponent } from "../auth/update-password.component";
import { UpdateTempPasswordComponent } from "../auth/update-temp-password.component";
import { VerifyEmailTokenComponent } from "../auth/verify-email-token.component";
import { VerifyRecoverDeleteComponent } from "../auth/verify-recover-delete.component";
+import { FreeBitwardenFamiliesComponent } from "../billing/members/free-bitwarden-families.component";
import { SponsoredFamiliesComponent } from "../billing/settings/sponsored-families.component";
import { SponsoringOrgRowComponent } from "../billing/settings/sponsoring-org-row.component";
// eslint-disable-next-line no-restricted-imports -- Temporarily disabled until DIRT refactors these out of this module
@@ -46,7 +47,6 @@ import { OrganizationBadgeModule } from "../vault/individual-vault/organization-
import { PipesModule } from "../vault/individual-vault/pipes/pipes.module";
import { PurgeVaultComponent } from "../vault/settings/purge-vault.component";
-import { FreeBitwardenFamiliesComponent } from "./../billing/members/free-bitwarden-families.component";
import { AccountFingerprintComponent } from "./components/account-fingerprint/account-fingerprint.component";
import { SharedModule } from "./shared.module";
diff --git a/apps/web/src/app/vault/components/vault-items/vault-items.component.ts b/apps/web/src/app/vault/components/vault-items/vault-items.component.ts
index 9679f0879b9..9d94fb044b5 100644
--- a/apps/web/src/app/vault/components/vault-items/vault-items.component.ts
+++ b/apps/web/src/app/vault/components/vault-items/vault-items.component.ts
@@ -342,8 +342,6 @@ export class VaultItemsComponent {
const ciphers: VaultItem[] = this.ciphers.map((cipher) => ({ cipher }));
const items: VaultItem[] = [].concat(collections).concat(ciphers);
- this.selection.clear();
-
// All ciphers are selectable, collections only if they can be edited or deleted
this.editableItems = items.filter(
(item) =>
diff --git a/apps/web/src/app/vault/components/vault-items/vault-items.stories.ts b/apps/web/src/app/vault/components/vault-items/vault-items.stories.ts
index 55807ed855f..e2c6f204d72 100644
--- a/apps/web/src/app/vault/components/vault-items/vault-items.stories.ts
+++ b/apps/web/src/app/vault/components/vault-items/vault-items.stories.ts
@@ -29,6 +29,7 @@ import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
import { LoginUriView } from "@bitwarden/common/vault/models/view/login-uri.view";
import { LoginView } from "@bitwarden/common/vault/models/view/login.view";
import { CipherAuthorizationService } from "@bitwarden/common/vault/services/cipher-authorization.service";
+import { RestrictedItemTypesService } from "@bitwarden/vault";
import { GroupView } from "../../../admin-console/organizations/core";
import { PreloadedEnglishI18nModule } from "../../../core/tests";
@@ -125,6 +126,12 @@ export default {
},
},
},
+ {
+ provide: RestrictedItemTypesService,
+ useValue: {
+ restricted$: of([]), // No restricted item types for this story
+ },
+ },
],
}),
applicationConfig({
diff --git a/apps/web/src/app/vault/individual-vault/vault-filter/components/vault-filter.component.ts b/apps/web/src/app/vault/individual-vault/vault-filter/components/vault-filter.component.ts
index 6b974296f21..8987fff04cf 100644
--- a/apps/web/src/app/vault/individual-vault/vault-filter/components/vault-filter.component.ts
+++ b/apps/web/src/app/vault/individual-vault/vault-filter/components/vault-filter.component.ts
@@ -1,8 +1,15 @@
-// FIXME: Update this file to be type safe and remove this and next line
-// @ts-strict-ignore
import { Component, EventEmitter, inject, Input, OnDestroy, OnInit, Output } from "@angular/core";
import { Router } from "@angular/router";
-import { firstValueFrom, merge, Subject, switchMap, takeUntil } from "rxjs";
+import {
+ distinctUntilChanged,
+ firstValueFrom,
+ map,
+ merge,
+ shareReplay,
+ Subject,
+ switchMap,
+ takeUntil,
+} from "rxjs";
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
import { PolicyType } from "@bitwarden/common/admin-console/enums";
@@ -16,6 +23,7 @@ import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/pl
import { CipherType } from "@bitwarden/common/vault/enums";
import { TreeNode } from "@bitwarden/common/vault/models/domain/tree-node";
import { DialogService, ToastService } from "@bitwarden/components";
+import { RestrictedItemTypesService } from "@bitwarden/vault";
import { TrialFlowService } from "../../../../billing/services/trial-flow.service";
import { VaultFilterService } from "../services/abstractions/vault-filter.service";
@@ -56,6 +64,45 @@ export class VaultFilterComponent implements OnInit, OnDestroy {
return this.filters ? Object.values(this.filters) : [];
}
+ allTypeFilters: CipherTypeFilter[] = [
+ {
+ id: "favorites",
+ name: this.i18nService.t("favorites"),
+ type: "favorites",
+ icon: "bwi-star",
+ },
+ {
+ id: "login",
+ name: this.i18nService.t("typeLogin"),
+ type: CipherType.Login,
+ icon: "bwi-globe",
+ },
+ {
+ id: "card",
+ name: this.i18nService.t("typeCard"),
+ type: CipherType.Card,
+ icon: "bwi-credit-card",
+ },
+ {
+ id: "identity",
+ name: this.i18nService.t("typeIdentity"),
+ type: CipherType.Identity,
+ icon: "bwi-id-card",
+ },
+ {
+ id: "note",
+ name: this.i18nService.t("note"),
+ type: CipherType.SecureNote,
+ icon: "bwi-sticky-note",
+ },
+ {
+ id: "sshKey",
+ name: this.i18nService.t("typeSshKey"),
+ type: CipherType.SshKey,
+ icon: "bwi-key",
+ },
+ ];
+
get searchPlaceholder() {
if (this.activeFilter.isFavorites) {
return "searchFavorites";
@@ -107,12 +154,17 @@ export class VaultFilterComponent implements OnInit, OnDestroy {
protected dialogService: DialogService,
protected configService: ConfigService,
protected accountService: AccountService,
+ protected restrictedItemTypesService: RestrictedItemTypesService,
) {}
async ngOnInit(): Promise
{
this.filters = await this.buildAllFilters();
- this.activeFilter.selectedCipherTypeNode =
- (await this.getDefaultFilter()) as TreeNode;
+ if (this.filters?.typeFilter?.data$) {
+ this.activeFilter.selectedCipherTypeNode = (await firstValueFrom(
+ this.filters?.typeFilter.data$,
+ )) as TreeNode;
+ }
+
this.isLoaded = true;
// Without refactoring the entire component, we need to manually update the organization filter whenever the policies update
@@ -133,6 +185,9 @@ export class VaultFilterComponent implements OnInit, OnDestroy {
takeUntil(this.destroy$),
)
.subscribe((orgFilters) => {
+ if (!this.filters) {
+ return;
+ }
this.filters.organizationFilter = orgFilters;
});
}
@@ -151,7 +206,6 @@ export class VaultFilterComponent implements OnInit, OnDestroy {
if (!orgNode?.node.enabled) {
this.toastService.showToast({
variant: "error",
- title: null,
message: this.i18nService.t("disabledOrganizationFilterError"),
});
const metadata = await this.billingApiService.getOrganizationBillingMetadata(orgNode.node.id);
@@ -190,10 +244,6 @@ export class VaultFilterComponent implements OnInit, OnDestroy {
this.onEditFolder.emit(folder);
};
- async getDefaultFilter(): Promise> {
- return await firstValueFrom(this.filters?.typeFilter.data$);
- }
-
async buildAllFilters(): Promise {
const builderFilter = {} as VaultFilterList;
builderFilter.organizationFilter = await this.addOrganizationFilter();
@@ -225,7 +275,7 @@ export class VaultFilterComponent implements OnInit, OnDestroy {
const addAction = !singleOrgPolicy
? { text: "newOrganization", route: "/create-organization" }
- : null;
+ : undefined;
const orgFilterSection: VaultFilterSection = {
data$: this.vaultFilterService.organizationTree$,
@@ -233,7 +283,7 @@ export class VaultFilterComponent implements OnInit, OnDestroy {
showHeader: !(singleOrgPolicy && personalVaultPolicy),
isSelectable: true,
},
- action: this.applyOrganizationFilter,
+ action: this.applyOrganizationFilter as (orgNode: TreeNode) => Promise,
options: { component: OrganizationOptionsComponent },
add: addAction,
divider: true,
@@ -243,55 +293,31 @@ export class VaultFilterComponent implements OnInit, OnDestroy {
}
protected async addTypeFilter(excludeTypes: CipherStatus[] = []): Promise {
- const allTypeFilters: CipherTypeFilter[] = [
- {
- id: "favorites",
- name: this.i18nService.t("favorites"),
- type: "favorites",
- icon: "bwi-star",
- },
- {
- id: "login",
- name: this.i18nService.t("typeLogin"),
- type: CipherType.Login,
- icon: "bwi-globe",
- },
- {
- id: "card",
- name: this.i18nService.t("typeCard"),
- type: CipherType.Card,
- icon: "bwi-credit-card",
- },
- {
- id: "identity",
- name: this.i18nService.t("typeIdentity"),
- type: CipherType.Identity,
- icon: "bwi-id-card",
- },
- {
- id: "note",
- name: this.i18nService.t("note"),
- type: CipherType.SecureNote,
- icon: "bwi-sticky-note",
- },
- {
- id: "sshKey",
- name: this.i18nService.t("typeSshKey"),
- type: CipherType.SshKey,
- icon: "bwi-key",
- },
- ];
+ const allFilter: CipherTypeFilter = { id: "AllItems", name: "allItems", type: "all", icon: "" };
+
+ const data$ = this.restrictedItemTypesService.restricted$.pipe(
+ map((restricted) => {
+ // List of types restricted by all orgs
+ const restrictedByAll = restricted
+ .filter((r) => r.allowViewOrgIds.length === 0)
+ .map((r) => r.cipherType);
+ const toExclude = [...excludeTypes, ...restrictedByAll];
+ return this.allTypeFilters.filter(
+ (f) => typeof f.type === "string" || !toExclude.includes(f.type),
+ );
+ }),
+ switchMap((allowed) => this.vaultFilterService.buildTypeTree(allFilter, allowed)),
+ distinctUntilChanged(),
+ shareReplay({ bufferSize: 1, refCount: true }),
+ );
const typeFilterSection: VaultFilterSection = {
- data$: this.vaultFilterService.buildTypeTree(
- { id: "AllItems", name: "allItems", type: "all", icon: "" },
- allTypeFilters.filter((f) => !excludeTypes.includes(f.type)),
- ),
+ data$,
header: {
showHeader: true,
isSelectable: true,
},
- action: this.applyTypeFilter,
+ action: this.applyTypeFilter as (filterNode: TreeNode) => Promise,
};
return typeFilterSection;
}
@@ -303,10 +329,10 @@ export class VaultFilterComponent implements OnInit, OnDestroy {
showHeader: true,
isSelectable: false,
},
- action: this.applyFolderFilter,
+ action: this.applyFolderFilter as (filterNode: TreeNode) => Promise,
edit: {
filterName: this.i18nService.t("folder"),
- action: this.editFolder,
+ action: this.editFolder as (filter: VaultFilterType) => void,
},
};
return folderFilterSection;
@@ -319,7 +345,9 @@ export class VaultFilterComponent implements OnInit, OnDestroy {
showHeader: true,
isSelectable: true,
},
- action: this.applyCollectionFilter,
+ action: this.applyCollectionFilter as (
+ filterNode: TreeNode,
+ ) => Promise,
};
return collectionFilterSection;
}
@@ -346,7 +374,7 @@ export class VaultFilterComponent implements OnInit, OnDestroy {
showHeader: false,
isSelectable: true,
},
- action: this.applyTypeFilter,
+ action: this.applyTypeFilter as (filterNode: TreeNode) => Promise,
};
return trashFilterSection;
}
diff --git a/apps/web/src/app/vault/individual-vault/vault-filter/shared/models/filter-function.spec.ts b/apps/web/src/app/vault/individual-vault/vault-filter/shared/models/filter-function.spec.ts
index 3082d7cb809..660aeb293a4 100644
--- a/apps/web/src/app/vault/individual-vault/vault-filter/shared/models/filter-function.spec.ts
+++ b/apps/web/src/app/vault/individual-vault/vault-filter/shared/models/filter-function.spec.ts
@@ -3,6 +3,7 @@
import { Unassigned } from "@bitwarden/admin-console/common";
import { CipherType } from "@bitwarden/common/vault/enums";
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
+import { RestrictedCipherType } from "@bitwarden/vault";
import { createFilterFunction } from "./filter-function";
import { All } from "./routed-vault-filter.model";
@@ -214,6 +215,46 @@ describe("createFilter", () => {
expect(result).toBe(true);
});
});
+
+ describe("given restricted types", () => {
+ const restrictedTypes: RestrictedCipherType[] = [
+ { cipherType: CipherType.Login, allowViewOrgIds: [] },
+ ];
+
+ it("should filter out a cipher whose type is fully restricted", () => {
+ const cipher = createCipher({ type: CipherType.Login });
+ const filterFunction = createFilterFunction({}, restrictedTypes);
+
+ expect(filterFunction(cipher)).toBe(false);
+ });
+
+ it("should allow a cipher when the cipher's organization allows it", () => {
+ const cipher = createCipher({ type: CipherType.Login, organizationId: "org1" });
+ const restricted: RestrictedCipherType[] = [
+ { cipherType: CipherType.Login, allowViewOrgIds: ["org1"] },
+ ];
+ const filterFunction2 = createFilterFunction({}, restricted);
+
+ expect(filterFunction2(cipher)).toBe(true);
+ });
+
+ it("should filter out a personal vault cipher when the owning orgs does not allow it", () => {
+ const cipher = createCipher({ type: CipherType.Card, organizationId: "org1" });
+ const restricted2: RestrictedCipherType[] = [
+ { cipherType: CipherType.Card, allowViewOrgIds: [] },
+ ];
+ const filterFunction3 = createFilterFunction({}, restricted2);
+
+ expect(filterFunction3(cipher)).toBe(false);
+ });
+
+ it("should not filter a cipher if there are no restricted types", () => {
+ const cipher = createCipher({ type: CipherType.Login });
+ const filterFunction = createFilterFunction({}, []);
+
+ expect(filterFunction(cipher)).toBe(true);
+ });
+ });
});
function createCipher(options: Partial = {}) {
diff --git a/apps/web/src/app/vault/individual-vault/vault-filter/shared/models/filter-function.ts b/apps/web/src/app/vault/individual-vault/vault-filter/shared/models/filter-function.ts
index a39918df4a7..61305fa5e49 100644
--- a/apps/web/src/app/vault/individual-vault/vault-filter/shared/models/filter-function.ts
+++ b/apps/web/src/app/vault/individual-vault/vault-filter/shared/models/filter-function.ts
@@ -1,12 +1,16 @@
import { Unassigned } from "@bitwarden/admin-console/common";
import { CipherType } from "@bitwarden/common/vault/enums";
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
+import { RestrictedCipherType } from "@bitwarden/vault";
import { All, RoutedVaultFilterModel } from "./routed-vault-filter.model";
export type FilterFunction = (cipher: CipherView) => boolean;
-export function createFilterFunction(filter: RoutedVaultFilterModel): FilterFunction {
+export function createFilterFunction(
+ filter: RoutedVaultFilterModel,
+ restrictedTypes?: RestrictedCipherType[],
+): FilterFunction {
return (cipher) => {
if (filter.type === "favorites" && !cipher.favorite) {
return false;
@@ -80,6 +84,24 @@ export function createFilterFunction(filter: RoutedVaultFilterModel): FilterFunc
return false;
}
+ // Restricted types
+ if (restrictedTypes && restrictedTypes.length > 0) {
+ // Filter the cipher if that type is restricted unless
+ // - The cipher belongs to an organization and that organization allows viewing the cipher type
+ // OR
+ // - The cipher belongs to the user's personal vault and at least one other organization does not restrict that type
+ if (
+ restrictedTypes.some(
+ (restrictedType) =>
+ restrictedType.cipherType === cipher.type &&
+ (cipher.organizationId
+ ? !restrictedType.allowViewOrgIds.includes(cipher.organizationId)
+ : restrictedType.allowViewOrgIds.length === 0),
+ )
+ ) {
+ return false;
+ }
+ }
return true;
};
}
diff --git a/apps/web/src/app/vault/individual-vault/vault-header/vault-header.component.html b/apps/web/src/app/vault/individual-vault/vault-header/vault-header.component.html
index af95a71ba8d..4ef8204cdfc 100644
--- a/apps/web/src/app/vault/individual-vault/vault-header/vault-header.component.html
+++ b/apps/web/src/app/vault/individual-vault/vault-header/vault-header.component.html
@@ -81,26 +81,12 @@
{{ "new" | i18n }}
-
-
-
-
-
+ @for (item of cipherMenuItems$ | async; track item.type) {
+
+ }