mirror of
https://github.com/bitwarden/browser
synced 2025-12-11 22:03:36 +00:00
[PM-20642] - [Vault] [Web App] Front End Changes to Enforce "Remove card item type policy" (#15097)
* add restricted item types service and apply it to filter web cipher * code cleanup. add shareReplay * account for multiple orgs when restricting item types * restrict item types for specific orgs * clean up logic. use policiesByType$ * track by item.type * clean up filtering. prefer observable. do not exempt owners for restricted item types * simplify in vault-filter. move item filter logic to vault. fix tests * don't return early in filter-function
This commit is contained in:
@@ -12,6 +12,7 @@ import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.servic
|
|||||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||||
import { TreeNode } from "@bitwarden/common/vault/models/domain/tree-node";
|
import { TreeNode } from "@bitwarden/common/vault/models/domain/tree-node";
|
||||||
import { DialogService, ToastService } from "@bitwarden/components";
|
import { DialogService, ToastService } from "@bitwarden/components";
|
||||||
|
import { RestrictedItemTypesService } from "@bitwarden/vault";
|
||||||
|
|
||||||
import { VaultFilterComponent as BaseVaultFilterComponent } from "../../../../vault/individual-vault/vault-filter/components/vault-filter.component";
|
import { VaultFilterComponent as BaseVaultFilterComponent } from "../../../../vault/individual-vault/vault-filter/components/vault-filter.component";
|
||||||
import { VaultFilterService } from "../../../../vault/individual-vault/vault-filter/services/abstractions/vault-filter.service";
|
import { VaultFilterService } from "../../../../vault/individual-vault/vault-filter/services/abstractions/vault-filter.service";
|
||||||
@@ -51,6 +52,7 @@ export class VaultFilterComponent
|
|||||||
protected dialogService: DialogService,
|
protected dialogService: DialogService,
|
||||||
protected configService: ConfigService,
|
protected configService: ConfigService,
|
||||||
protected accountService: AccountService,
|
protected accountService: AccountService,
|
||||||
|
protected restrictedItemTypesService: RestrictedItemTypesService,
|
||||||
) {
|
) {
|
||||||
super(
|
super(
|
||||||
vaultFilterService,
|
vaultFilterService,
|
||||||
@@ -62,6 +64,7 @@ export class VaultFilterComponent
|
|||||||
dialogService,
|
dialogService,
|
||||||
configService,
|
configService,
|
||||||
accountService,
|
accountService,
|
||||||
|
restrictedItemTypesService,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import { BasePolicy, BasePolicyComponent } from "./base-policy.component";
|
|||||||
export class RestrictedItemTypesPolicy extends BasePolicy {
|
export class RestrictedItemTypesPolicy extends BasePolicy {
|
||||||
name = "restrictedItemTypesPolicy";
|
name = "restrictedItemTypesPolicy";
|
||||||
description = "restrictedItemTypesPolicyDesc";
|
description = "restrictedItemTypesPolicyDesc";
|
||||||
type = PolicyType.RestrictedItemTypesPolicy;
|
type = PolicyType.RestrictedItemTypes;
|
||||||
component = RestrictedItemTypesPolicyComponent;
|
component = RestrictedItemTypesPolicyComponent;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -342,8 +342,6 @@ export class VaultItemsComponent {
|
|||||||
const ciphers: VaultItem[] = this.ciphers.map((cipher) => ({ cipher }));
|
const ciphers: VaultItem[] = this.ciphers.map((cipher) => ({ cipher }));
|
||||||
const items: VaultItem[] = [].concat(collections).concat(ciphers);
|
const items: VaultItem[] = [].concat(collections).concat(ciphers);
|
||||||
|
|
||||||
this.selection.clear();
|
|
||||||
|
|
||||||
// All ciphers are selectable, collections only if they can be edited or deleted
|
// All ciphers are selectable, collections only if they can be edited or deleted
|
||||||
this.editableItems = items.filter(
|
this.editableItems = items.filter(
|
||||||
(item) =>
|
(item) =>
|
||||||
|
|||||||
@@ -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 { LoginUriView } from "@bitwarden/common/vault/models/view/login-uri.view";
|
||||||
import { LoginView } from "@bitwarden/common/vault/models/view/login.view";
|
import { LoginView } from "@bitwarden/common/vault/models/view/login.view";
|
||||||
import { CipherAuthorizationService } from "@bitwarden/common/vault/services/cipher-authorization.service";
|
import { CipherAuthorizationService } from "@bitwarden/common/vault/services/cipher-authorization.service";
|
||||||
|
import { RestrictedItemTypesService } from "@bitwarden/vault";
|
||||||
|
|
||||||
import { GroupView } from "../../../admin-console/organizations/core";
|
import { GroupView } from "../../../admin-console/organizations/core";
|
||||||
import { PreloadedEnglishI18nModule } from "../../../core/tests";
|
import { PreloadedEnglishI18nModule } from "../../../core/tests";
|
||||||
@@ -125,6 +126,12 @@ export default {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
provide: RestrictedItemTypesService,
|
||||||
|
useValue: {
|
||||||
|
restricted$: of([]), // No restricted item types for this story
|
||||||
|
},
|
||||||
|
},
|
||||||
],
|
],
|
||||||
}),
|
}),
|
||||||
applicationConfig({
|
applicationConfig({
|
||||||
|
|||||||
@@ -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 { Component, EventEmitter, inject, Input, OnDestroy, OnInit, Output } from "@angular/core";
|
||||||
import { Router } from "@angular/router";
|
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 { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
|
||||||
import { PolicyType } from "@bitwarden/common/admin-console/enums";
|
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 { CipherType } from "@bitwarden/common/vault/enums";
|
||||||
import { TreeNode } from "@bitwarden/common/vault/models/domain/tree-node";
|
import { TreeNode } from "@bitwarden/common/vault/models/domain/tree-node";
|
||||||
import { DialogService, ToastService } from "@bitwarden/components";
|
import { DialogService, ToastService } from "@bitwarden/components";
|
||||||
|
import { RestrictedItemTypesService } from "@bitwarden/vault";
|
||||||
|
|
||||||
import { TrialFlowService } from "../../../../billing/services/trial-flow.service";
|
import { TrialFlowService } from "../../../../billing/services/trial-flow.service";
|
||||||
import { VaultFilterService } from "../services/abstractions/vault-filter.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) : [];
|
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() {
|
get searchPlaceholder() {
|
||||||
if (this.activeFilter.isFavorites) {
|
if (this.activeFilter.isFavorites) {
|
||||||
return "searchFavorites";
|
return "searchFavorites";
|
||||||
@@ -107,12 +154,17 @@ export class VaultFilterComponent implements OnInit, OnDestroy {
|
|||||||
protected dialogService: DialogService,
|
protected dialogService: DialogService,
|
||||||
protected configService: ConfigService,
|
protected configService: ConfigService,
|
||||||
protected accountService: AccountService,
|
protected accountService: AccountService,
|
||||||
|
protected restrictedItemTypesService: RestrictedItemTypesService,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
async ngOnInit(): Promise<void> {
|
async ngOnInit(): Promise<void> {
|
||||||
this.filters = await this.buildAllFilters();
|
this.filters = await this.buildAllFilters();
|
||||||
this.activeFilter.selectedCipherTypeNode =
|
if (this.filters?.typeFilter?.data$) {
|
||||||
(await this.getDefaultFilter()) as TreeNode<CipherTypeFilter>;
|
this.activeFilter.selectedCipherTypeNode = (await firstValueFrom(
|
||||||
|
this.filters?.typeFilter.data$,
|
||||||
|
)) as TreeNode<CipherTypeFilter>;
|
||||||
|
}
|
||||||
|
|
||||||
this.isLoaded = true;
|
this.isLoaded = true;
|
||||||
|
|
||||||
// Without refactoring the entire component, we need to manually update the organization filter whenever the policies update
|
// 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$),
|
takeUntil(this.destroy$),
|
||||||
)
|
)
|
||||||
.subscribe((orgFilters) => {
|
.subscribe((orgFilters) => {
|
||||||
|
if (!this.filters) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
this.filters.organizationFilter = orgFilters;
|
this.filters.organizationFilter = orgFilters;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -151,7 +206,6 @@ export class VaultFilterComponent implements OnInit, OnDestroy {
|
|||||||
if (!orgNode?.node.enabled) {
|
if (!orgNode?.node.enabled) {
|
||||||
this.toastService.showToast({
|
this.toastService.showToast({
|
||||||
variant: "error",
|
variant: "error",
|
||||||
title: null,
|
|
||||||
message: this.i18nService.t("disabledOrganizationFilterError"),
|
message: this.i18nService.t("disabledOrganizationFilterError"),
|
||||||
});
|
});
|
||||||
const metadata = await this.billingApiService.getOrganizationBillingMetadata(orgNode.node.id);
|
const metadata = await this.billingApiService.getOrganizationBillingMetadata(orgNode.node.id);
|
||||||
@@ -190,10 +244,6 @@ export class VaultFilterComponent implements OnInit, OnDestroy {
|
|||||||
this.onEditFolder.emit(folder);
|
this.onEditFolder.emit(folder);
|
||||||
};
|
};
|
||||||
|
|
||||||
async getDefaultFilter(): Promise<TreeNode<VaultFilterType>> {
|
|
||||||
return await firstValueFrom(this.filters?.typeFilter.data$);
|
|
||||||
}
|
|
||||||
|
|
||||||
async buildAllFilters(): Promise<VaultFilterList> {
|
async buildAllFilters(): Promise<VaultFilterList> {
|
||||||
const builderFilter = {} as VaultFilterList;
|
const builderFilter = {} as VaultFilterList;
|
||||||
builderFilter.organizationFilter = await this.addOrganizationFilter();
|
builderFilter.organizationFilter = await this.addOrganizationFilter();
|
||||||
@@ -225,7 +275,7 @@ export class VaultFilterComponent implements OnInit, OnDestroy {
|
|||||||
|
|
||||||
const addAction = !singleOrgPolicy
|
const addAction = !singleOrgPolicy
|
||||||
? { text: "newOrganization", route: "/create-organization" }
|
? { text: "newOrganization", route: "/create-organization" }
|
||||||
: null;
|
: undefined;
|
||||||
|
|
||||||
const orgFilterSection: VaultFilterSection = {
|
const orgFilterSection: VaultFilterSection = {
|
||||||
data$: this.vaultFilterService.organizationTree$,
|
data$: this.vaultFilterService.organizationTree$,
|
||||||
@@ -233,7 +283,7 @@ export class VaultFilterComponent implements OnInit, OnDestroy {
|
|||||||
showHeader: !(singleOrgPolicy && personalVaultPolicy),
|
showHeader: !(singleOrgPolicy && personalVaultPolicy),
|
||||||
isSelectable: true,
|
isSelectable: true,
|
||||||
},
|
},
|
||||||
action: this.applyOrganizationFilter,
|
action: this.applyOrganizationFilter as (orgNode: TreeNode<VaultFilterType>) => Promise<void>,
|
||||||
options: { component: OrganizationOptionsComponent },
|
options: { component: OrganizationOptionsComponent },
|
||||||
add: addAction,
|
add: addAction,
|
||||||
divider: true,
|
divider: true,
|
||||||
@@ -243,55 +293,31 @@ export class VaultFilterComponent implements OnInit, OnDestroy {
|
|||||||
}
|
}
|
||||||
|
|
||||||
protected async addTypeFilter(excludeTypes: CipherStatus[] = []): Promise<VaultFilterSection> {
|
protected async addTypeFilter(excludeTypes: CipherStatus[] = []): Promise<VaultFilterSection> {
|
||||||
const allTypeFilters: CipherTypeFilter[] = [
|
const allFilter: CipherTypeFilter = { id: "AllItems", name: "allItems", type: "all", icon: "" };
|
||||||
{
|
|
||||||
id: "favorites",
|
const data$ = this.restrictedItemTypesService.restricted$.pipe(
|
||||||
name: this.i18nService.t("favorites"),
|
map((restricted) => {
|
||||||
type: "favorites",
|
// List of types restricted by all orgs
|
||||||
icon: "bwi-star",
|
const restrictedByAll = restricted
|
||||||
},
|
.filter((r) => r.allowViewOrgIds.length === 0)
|
||||||
{
|
.map((r) => r.cipherType);
|
||||||
id: "login",
|
const toExclude = [...excludeTypes, ...restrictedByAll];
|
||||||
name: this.i18nService.t("typeLogin"),
|
return this.allTypeFilters.filter(
|
||||||
type: CipherType.Login,
|
(f) => typeof f.type !== "string" && !toExclude.includes(f.type),
|
||||||
icon: "bwi-globe",
|
);
|
||||||
},
|
}),
|
||||||
{
|
switchMap((allowed) => this.vaultFilterService.buildTypeTree(allFilter, allowed)),
|
||||||
id: "card",
|
distinctUntilChanged(),
|
||||||
name: this.i18nService.t("typeCard"),
|
shareReplay({ bufferSize: 1, refCount: true }),
|
||||||
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 typeFilterSection: VaultFilterSection = {
|
const typeFilterSection: VaultFilterSection = {
|
||||||
data$: this.vaultFilterService.buildTypeTree(
|
data$,
|
||||||
{ id: "AllItems", name: "allItems", type: "all", icon: "" },
|
|
||||||
allTypeFilters.filter((f) => !excludeTypes.includes(f.type)),
|
|
||||||
),
|
|
||||||
header: {
|
header: {
|
||||||
showHeader: true,
|
showHeader: true,
|
||||||
isSelectable: true,
|
isSelectable: true,
|
||||||
},
|
},
|
||||||
action: this.applyTypeFilter,
|
action: this.applyTypeFilter as (filterNode: TreeNode<VaultFilterType>) => Promise<void>,
|
||||||
};
|
};
|
||||||
return typeFilterSection;
|
return typeFilterSection;
|
||||||
}
|
}
|
||||||
@@ -303,10 +329,10 @@ export class VaultFilterComponent implements OnInit, OnDestroy {
|
|||||||
showHeader: true,
|
showHeader: true,
|
||||||
isSelectable: false,
|
isSelectable: false,
|
||||||
},
|
},
|
||||||
action: this.applyFolderFilter,
|
action: this.applyFolderFilter as (filterNode: TreeNode<VaultFilterType>) => Promise<void>,
|
||||||
edit: {
|
edit: {
|
||||||
filterName: this.i18nService.t("folder"),
|
filterName: this.i18nService.t("folder"),
|
||||||
action: this.editFolder,
|
action: this.editFolder as (filter: VaultFilterType) => void,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
return folderFilterSection;
|
return folderFilterSection;
|
||||||
@@ -319,7 +345,9 @@ export class VaultFilterComponent implements OnInit, OnDestroy {
|
|||||||
showHeader: true,
|
showHeader: true,
|
||||||
isSelectable: true,
|
isSelectable: true,
|
||||||
},
|
},
|
||||||
action: this.applyCollectionFilter,
|
action: this.applyCollectionFilter as (
|
||||||
|
filterNode: TreeNode<VaultFilterType>,
|
||||||
|
) => Promise<void>,
|
||||||
};
|
};
|
||||||
return collectionFilterSection;
|
return collectionFilterSection;
|
||||||
}
|
}
|
||||||
@@ -346,7 +374,7 @@ export class VaultFilterComponent implements OnInit, OnDestroy {
|
|||||||
showHeader: false,
|
showHeader: false,
|
||||||
isSelectable: true,
|
isSelectable: true,
|
||||||
},
|
},
|
||||||
action: this.applyTypeFilter,
|
action: this.applyTypeFilter as (filterNode: TreeNode<VaultFilterType>) => Promise<void>,
|
||||||
};
|
};
|
||||||
return trashFilterSection;
|
return trashFilterSection;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
import { Unassigned } from "@bitwarden/admin-console/common";
|
import { Unassigned } from "@bitwarden/admin-console/common";
|
||||||
import { CipherType } from "@bitwarden/common/vault/enums";
|
import { CipherType } from "@bitwarden/common/vault/enums";
|
||||||
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
||||||
|
import { RestrictedCipherType } from "@bitwarden/vault";
|
||||||
|
|
||||||
import { createFilterFunction } from "./filter-function";
|
import { createFilterFunction } from "./filter-function";
|
||||||
import { All } from "./routed-vault-filter.model";
|
import { All } from "./routed-vault-filter.model";
|
||||||
@@ -214,6 +215,46 @@ describe("createFilter", () => {
|
|||||||
expect(result).toBe(true);
|
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<CipherView> = {}) {
|
function createCipher(options: Partial<CipherView> = {}) {
|
||||||
|
|||||||
@@ -1,12 +1,16 @@
|
|||||||
import { Unassigned } from "@bitwarden/admin-console/common";
|
import { Unassigned } from "@bitwarden/admin-console/common";
|
||||||
import { CipherType } from "@bitwarden/common/vault/enums";
|
import { CipherType } from "@bitwarden/common/vault/enums";
|
||||||
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
||||||
|
import { RestrictedCipherType } from "@bitwarden/vault";
|
||||||
|
|
||||||
import { All, RoutedVaultFilterModel } from "./routed-vault-filter.model";
|
import { All, RoutedVaultFilterModel } from "./routed-vault-filter.model";
|
||||||
|
|
||||||
export type FilterFunction = (cipher: CipherView) => boolean;
|
export type FilterFunction = (cipher: CipherView) => boolean;
|
||||||
|
|
||||||
export function createFilterFunction(filter: RoutedVaultFilterModel): FilterFunction {
|
export function createFilterFunction(
|
||||||
|
filter: RoutedVaultFilterModel,
|
||||||
|
restrictedTypes?: RestrictedCipherType[],
|
||||||
|
): FilterFunction {
|
||||||
return (cipher) => {
|
return (cipher) => {
|
||||||
if (filter.type === "favorites" && !cipher.favorite) {
|
if (filter.type === "favorites" && !cipher.favorite) {
|
||||||
return false;
|
return false;
|
||||||
@@ -80,6 +84,24 @@ export function createFilterFunction(filter: RoutedVaultFilterModel): FilterFunc
|
|||||||
return false;
|
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;
|
return true;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -81,26 +81,12 @@
|
|||||||
{{ "new" | i18n }}<i class="bwi tw-ml-2" aria-hidden="true"></i>
|
{{ "new" | i18n }}<i class="bwi tw-ml-2" aria-hidden="true"></i>
|
||||||
</button>
|
</button>
|
||||||
<bit-menu #addOptions aria-labelledby="newItemDropdown">
|
<bit-menu #addOptions aria-labelledby="newItemDropdown">
|
||||||
<button type="button" bitMenuItem (click)="addCipher(CipherType.Login)">
|
@for (item of cipherMenuItems$ | async; track item.type) {
|
||||||
<i class="bwi bwi-globe" slot="start" aria-hidden="true"></i>
|
<button type="button" bitMenuItem (click)="addCipher(item.type)">
|
||||||
{{ "typeLogin" | i18n }}
|
<i class="bwi {{ item.icon }}" slot="start" aria-hidden="true"></i>
|
||||||
</button>
|
{{ item.labelKey | i18n }}
|
||||||
<button type="button" bitMenuItem (click)="addCipher(CipherType.Card)">
|
|
||||||
<i class="bwi bwi-credit-card" slot="start" aria-hidden="true"></i>
|
|
||||||
{{ "typeCard" | i18n }}
|
|
||||||
</button>
|
|
||||||
<button type="button" bitMenuItem (click)="addCipher(CipherType.Identity)">
|
|
||||||
<i class="bwi bwi-id-card" slot="start" aria-hidden="true"></i>
|
|
||||||
{{ "typeIdentity" | i18n }}
|
|
||||||
</button>
|
|
||||||
<button type="button" bitMenuItem (click)="addCipher(CipherType.SecureNote)">
|
|
||||||
<i class="bwi bwi-sticky-note" slot="start" aria-hidden="true"></i>
|
|
||||||
{{ "note" | i18n }}
|
|
||||||
</button>
|
|
||||||
<button type="button" bitMenuItem (click)="addCipher(CipherType.SshKey)">
|
|
||||||
<i class="bwi bwi-key" slot="start" aria-hidden="true"></i>
|
|
||||||
{{ "typeSshKey" | i18n }}
|
|
||||||
</button>
|
</button>
|
||||||
|
}
|
||||||
<bit-menu-divider />
|
<bit-menu-divider />
|
||||||
<button type="button" bitMenuItem (click)="addFolder()">
|
<button type="button" bitMenuItem (click)="addFolder()">
|
||||||
<i class="bwi bwi-fw bwi-folder" aria-hidden="true"></i>
|
<i class="bwi bwi-fw bwi-folder" aria-hidden="true"></i>
|
||||||
|
|||||||
@@ -1,16 +1,9 @@
|
|||||||
// FIXME: Update this file to be type safe and remove this and next line
|
// FIXME: Update this file to be type safe and remove this and next line
|
||||||
// @ts-strict-ignore
|
// @ts-strict-ignore
|
||||||
import { CommonModule } from "@angular/common";
|
import { CommonModule } from "@angular/common";
|
||||||
import {
|
import { ChangeDetectionStrategy, Component, EventEmitter, Input, Output } from "@angular/core";
|
||||||
ChangeDetectionStrategy,
|
|
||||||
Component,
|
|
||||||
EventEmitter,
|
|
||||||
Input,
|
|
||||||
OnInit,
|
|
||||||
Output,
|
|
||||||
} from "@angular/core";
|
|
||||||
import { Router } from "@angular/router";
|
import { Router } from "@angular/router";
|
||||||
import { firstValueFrom } from "rxjs";
|
import { firstValueFrom, map, shareReplay } from "rxjs";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
Unassigned,
|
Unassigned,
|
||||||
@@ -31,6 +24,7 @@ import {
|
|||||||
MenuModule,
|
MenuModule,
|
||||||
SimpleDialogOptions,
|
SimpleDialogOptions,
|
||||||
} from "@bitwarden/components";
|
} from "@bitwarden/components";
|
||||||
|
import { RestrictedItemTypesService } from "@bitwarden/vault";
|
||||||
|
|
||||||
import { CollectionDialogTabType } from "../../../admin-console/organizations/shared/components/collection-dialog";
|
import { CollectionDialogTabType } from "../../../admin-console/organizations/shared/components/collection-dialog";
|
||||||
import { HeaderModule } from "../../../layouts/header/header.module";
|
import { HeaderModule } from "../../../layouts/header/header.module";
|
||||||
@@ -55,11 +49,26 @@ import {
|
|||||||
],
|
],
|
||||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||||
})
|
})
|
||||||
export class VaultHeaderComponent implements OnInit {
|
export class VaultHeaderComponent {
|
||||||
protected Unassigned = Unassigned;
|
protected Unassigned = Unassigned;
|
||||||
protected All = All;
|
protected All = All;
|
||||||
protected CollectionDialogTabType = CollectionDialogTabType;
|
protected CollectionDialogTabType = CollectionDialogTabType;
|
||||||
protected CipherType = CipherType;
|
protected CipherType = CipherType;
|
||||||
|
protected allCipherMenuItems = [
|
||||||
|
{ type: CipherType.Login, icon: "bwi-globe", labelKey: "typeLogin" },
|
||||||
|
{ type: CipherType.Card, icon: "bwi-credit-card", labelKey: "typeCard" },
|
||||||
|
{ type: CipherType.Identity, icon: "bwi-id-card", labelKey: "typeIdentity" },
|
||||||
|
{ type: CipherType.SecureNote, icon: "bwi-sticky-note", labelKey: "note" },
|
||||||
|
{ type: CipherType.SshKey, icon: "bwi-key", labelKey: "typeSshKey" },
|
||||||
|
];
|
||||||
|
protected cipherMenuItems$ = this.restrictedItemTypesService.restricted$.pipe(
|
||||||
|
map((restrictedTypes) => {
|
||||||
|
return this.allCipherMenuItems.filter((item) => {
|
||||||
|
return !restrictedTypes.some((restrictedType) => restrictedType.cipherType === item.type);
|
||||||
|
});
|
||||||
|
}),
|
||||||
|
shareReplay({ bufferSize: 1, refCount: true }),
|
||||||
|
);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Boolean to determine the loading state of the header.
|
* Boolean to determine the loading state of the header.
|
||||||
@@ -100,10 +109,9 @@ export class VaultHeaderComponent implements OnInit {
|
|||||||
private dialogService: DialogService,
|
private dialogService: DialogService,
|
||||||
private router: Router,
|
private router: Router,
|
||||||
private configService: ConfigService,
|
private configService: ConfigService,
|
||||||
|
private restrictedItemTypesService: RestrictedItemTypesService,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
async ngOnInit() {}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The id of the organization that is currently being filtered on.
|
* The id of the organization that is currently being filtered on.
|
||||||
* This can come from a collection filter or organization filter, if applied.
|
* This can come from a collection filter or organization filter, if applied.
|
||||||
|
|||||||
@@ -79,6 +79,7 @@ import {
|
|||||||
DecryptionFailureDialogComponent,
|
DecryptionFailureDialogComponent,
|
||||||
DefaultCipherFormConfigService,
|
DefaultCipherFormConfigService,
|
||||||
PasswordRepromptService,
|
PasswordRepromptService,
|
||||||
|
RestrictedItemTypesService,
|
||||||
} from "@bitwarden/vault";
|
} from "@bitwarden/vault";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
@@ -273,6 +274,7 @@ export class VaultComponent implements OnInit, OnDestroy {
|
|||||||
private organizationBillingService: OrganizationBillingServiceAbstraction,
|
private organizationBillingService: OrganizationBillingServiceAbstraction,
|
||||||
private billingNotificationService: BillingNotificationService,
|
private billingNotificationService: BillingNotificationService,
|
||||||
private configService: ConfigService,
|
private configService: ConfigService,
|
||||||
|
private restrictedItemTypesService: RestrictedItemTypesService,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
async ngOnInit() {
|
async ngOnInit() {
|
||||||
@@ -356,12 +358,13 @@ export class VaultComponent implements OnInit, OnDestroy {
|
|||||||
this.cipherService.cipherViews$(activeUserId).pipe(filter((c) => c !== null)),
|
this.cipherService.cipherViews$(activeUserId).pipe(filter((c) => c !== null)),
|
||||||
filter$,
|
filter$,
|
||||||
this.currentSearchText$,
|
this.currentSearchText$,
|
||||||
|
this.restrictedItemTypesService.restricted$,
|
||||||
]).pipe(
|
]).pipe(
|
||||||
filter(([ciphers, filter]) => ciphers != undefined && filter != undefined),
|
filter(([ciphers, filter]) => ciphers != undefined && filter != undefined),
|
||||||
concatMap(async ([ciphers, filter, searchText]) => {
|
concatMap(async ([ciphers, filter, searchText, restrictedTypes]) => {
|
||||||
const failedCiphers =
|
const failedCiphers =
|
||||||
(await firstValueFrom(this.cipherService.failedToDecryptCiphers$(activeUserId))) ?? [];
|
(await firstValueFrom(this.cipherService.failedToDecryptCiphers$(activeUserId))) ?? [];
|
||||||
const filterFunction = createFilterFunction(filter);
|
const filterFunction = createFilterFunction(filter, restrictedTypes);
|
||||||
// Append any failed to decrypt ciphers to the top of the cipher list
|
// Append any failed to decrypt ciphers to the top of the cipher list
|
||||||
const allCiphers = [...failedCiphers, ...ciphers];
|
const allCiphers = [...failedCiphers, ...ciphers];
|
||||||
|
|
||||||
|
|||||||
@@ -16,5 +16,5 @@ export enum PolicyType {
|
|||||||
AutomaticAppLogIn = 12, // Enables automatic log in of apps from configured identity provider
|
AutomaticAppLogIn = 12, // Enables automatic log in of apps from configured identity provider
|
||||||
FreeFamiliesSponsorshipPolicy = 13, // Disables free families plan for organization
|
FreeFamiliesSponsorshipPolicy = 13, // Disables free families plan for organization
|
||||||
RemoveUnlockWithPin = 14, // Do not allow members to unlock their account with a PIN.
|
RemoveUnlockWithPin = 14, // Do not allow members to unlock their account with a PIN.
|
||||||
RestrictedItemTypesPolicy = 15, // Restricts item types that can be created within an organization
|
RestrictedItemTypes = 15, // Restricts item types that can be created within an organization
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -228,15 +228,19 @@ export class DefaultPolicyService implements PolicyService {
|
|||||||
case PolicyType.MaximumVaultTimeout:
|
case PolicyType.MaximumVaultTimeout:
|
||||||
// Max Vault Timeout applies to everyone except owners
|
// Max Vault Timeout applies to everyone except owners
|
||||||
return organization.isOwner;
|
return organization.isOwner;
|
||||||
|
// the following policies apply to everyone
|
||||||
case PolicyType.PasswordGenerator:
|
case PolicyType.PasswordGenerator:
|
||||||
// password generation policy applies to everyone
|
// password generation policy
|
||||||
|
return false;
|
||||||
|
case PolicyType.FreeFamiliesSponsorshipPolicy:
|
||||||
|
// free Bitwarden families policy
|
||||||
|
return false;
|
||||||
|
case PolicyType.RestrictedItemTypes:
|
||||||
|
// restricted item types policy
|
||||||
return false;
|
return false;
|
||||||
case PolicyType.PersonalOwnership:
|
case PolicyType.PersonalOwnership:
|
||||||
// individual vault policy applies to everyone except admins and owners
|
// individual vault policy applies to everyone except admins and owners
|
||||||
return organization.isAdmin;
|
return organization.isAdmin;
|
||||||
case PolicyType.FreeFamiliesSponsorshipPolicy:
|
|
||||||
// free Bitwarden families policy applies to everyone
|
|
||||||
return false;
|
|
||||||
default:
|
default:
|
||||||
return organization.canManagePolicies;
|
return organization.canManagePolicies;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,6 +24,10 @@ export * as VaultIcons from "./icons";
|
|||||||
|
|
||||||
export { DefaultSshImportPromptService } from "./services/default-ssh-import-prompt.service";
|
export { DefaultSshImportPromptService } from "./services/default-ssh-import-prompt.service";
|
||||||
export { SshImportPromptService } from "./services/ssh-import-prompt.service";
|
export { SshImportPromptService } from "./services/ssh-import-prompt.service";
|
||||||
|
export {
|
||||||
|
RestrictedItemTypesService,
|
||||||
|
RestrictedCipherType,
|
||||||
|
} from "./services/restricted-item-types.service";
|
||||||
|
|
||||||
export * from "./abstractions/change-login-password.service";
|
export * from "./abstractions/change-login-password.service";
|
||||||
export * from "./services/default-change-login-password.service";
|
export * from "./services/default-change-login-password.service";
|
||||||
|
|||||||
137
libs/vault/src/services/restricted-item-types.service.spec.ts
Normal file
137
libs/vault/src/services/restricted-item-types.service.spec.ts
Normal file
@@ -0,0 +1,137 @@
|
|||||||
|
import { TestBed } from "@angular/core/testing";
|
||||||
|
import { mock, MockProxy } from "jest-mock-extended";
|
||||||
|
import { firstValueFrom, of } from "rxjs";
|
||||||
|
|
||||||
|
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 { PolicyType } from "@bitwarden/common/admin-console/enums";
|
||||||
|
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
|
||||||
|
import { Policy } from "@bitwarden/common/admin-console/models/domain/policy";
|
||||||
|
import { Account, AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||||
|
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||||
|
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
||||||
|
import { UserId } from "@bitwarden/common/types/guid";
|
||||||
|
import { CipherType } from "@bitwarden/common/vault/enums";
|
||||||
|
|
||||||
|
import { RestrictedItemTypesService, RestrictedCipherType } from "./restricted-item-types.service";
|
||||||
|
|
||||||
|
describe("RestrictedItemTypesService", () => {
|
||||||
|
let service: RestrictedItemTypesService;
|
||||||
|
let policyService: MockProxy<PolicyService>;
|
||||||
|
let organizationService: MockProxy<OrganizationService>;
|
||||||
|
let accountService: MockProxy<AccountService>;
|
||||||
|
let configService: MockProxy<ConfigService>;
|
||||||
|
let fakeAccount: Account | null;
|
||||||
|
|
||||||
|
const org1: Organization = { id: "org1" } as any;
|
||||||
|
const org2: Organization = { id: "org2" } as any;
|
||||||
|
|
||||||
|
const policyOrg1 = {
|
||||||
|
organizationId: "org1",
|
||||||
|
type: PolicyType.RestrictedItemTypes,
|
||||||
|
enabled: true,
|
||||||
|
data: [CipherType.Card],
|
||||||
|
} as Policy;
|
||||||
|
|
||||||
|
const policyOrg2 = {
|
||||||
|
organizationId: "org2",
|
||||||
|
type: PolicyType.RestrictedItemTypes,
|
||||||
|
enabled: true,
|
||||||
|
data: [CipherType.Card],
|
||||||
|
} as Policy;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
policyService = mock<PolicyService>();
|
||||||
|
organizationService = mock<OrganizationService>();
|
||||||
|
accountService = mock<AccountService>();
|
||||||
|
configService = mock<ConfigService>();
|
||||||
|
|
||||||
|
fakeAccount = { id: Utils.newGuid() as UserId } as Account;
|
||||||
|
accountService.activeAccount$ = of(fakeAccount);
|
||||||
|
|
||||||
|
TestBed.configureTestingModule({
|
||||||
|
providers: [
|
||||||
|
{ provide: PolicyService, useValue: policyService },
|
||||||
|
{ provide: OrganizationService, useValue: organizationService },
|
||||||
|
{ provide: AccountService, useValue: accountService },
|
||||||
|
{ provide: ConfigService, useValue: configService },
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
configService.getFeatureFlag$.mockReturnValue(of(true));
|
||||||
|
organizationService.organizations$.mockReturnValue(of([org1, org2]));
|
||||||
|
policyService.policiesByType$.mockReturnValue(of([]));
|
||||||
|
service = TestBed.inject(RestrictedItemTypesService);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("emits empty array when feature flag is disabled", async () => {
|
||||||
|
configService.getFeatureFlag$.mockReturnValue(of(false));
|
||||||
|
|
||||||
|
const result = await firstValueFrom(service.restricted$);
|
||||||
|
expect(result).toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("emits empty array if no organizations exist", async () => {
|
||||||
|
organizationService.organizations$.mockReturnValue(of([]));
|
||||||
|
policyService.policiesByType$.mockReturnValue(of([]));
|
||||||
|
|
||||||
|
const result = await firstValueFrom(service.restricted$);
|
||||||
|
expect(result).toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("defaults undefined data to [Card] and returns empty allowViewOrgIds", async () => {
|
||||||
|
organizationService.organizations$.mockReturnValue(of([org1]));
|
||||||
|
|
||||||
|
const policyForOrg1 = {
|
||||||
|
organizationId: "org1",
|
||||||
|
type: PolicyType.RestrictedItemTypes,
|
||||||
|
enabled: true,
|
||||||
|
data: undefined,
|
||||||
|
} as Policy;
|
||||||
|
policyService.policiesByType$.mockReturnValue(of([policyForOrg1]));
|
||||||
|
|
||||||
|
const result = await firstValueFrom(service.restricted$);
|
||||||
|
expect(result).toEqual<RestrictedCipherType[]>([
|
||||||
|
{ cipherType: CipherType.Card, allowViewOrgIds: [] },
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("if one org restricts Card and another has no policy, allowViewOrgIds contains the unrestricted org", async () => {
|
||||||
|
policyService.policiesByType$.mockReturnValue(of([policyOrg1]));
|
||||||
|
|
||||||
|
const result = await firstValueFrom(service.restricted$);
|
||||||
|
expect(result).toEqual<RestrictedCipherType[]>([
|
||||||
|
{ cipherType: CipherType.Card, allowViewOrgIds: ["org2"] },
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns empty allowViewOrgIds when all orgs restrict the same type", async () => {
|
||||||
|
configService.getFeatureFlag$.mockReturnValue(of(true));
|
||||||
|
organizationService.organizations$.mockReturnValue(of([org1, org2]));
|
||||||
|
policyService.policiesByType$.mockReturnValue(of([policyOrg1, policyOrg2]));
|
||||||
|
|
||||||
|
const result = await firstValueFrom(service.restricted$);
|
||||||
|
expect(result).toEqual<RestrictedCipherType[]>([
|
||||||
|
{ cipherType: CipherType.Card, allowViewOrgIds: [] },
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("aggregates multiple types and computes allowViewOrgIds correctly", async () => {
|
||||||
|
configService.getFeatureFlag$.mockReturnValue(of(true));
|
||||||
|
organizationService.organizations$.mockReturnValue(of([org1, org2]));
|
||||||
|
policyService.policiesByType$.mockReturnValue(
|
||||||
|
of([
|
||||||
|
{ ...policyOrg1, data: [CipherType.Card, CipherType.Login] } as Policy,
|
||||||
|
{ ...policyOrg2, data: [CipherType.Card, CipherType.Identity] } as Policy,
|
||||||
|
]),
|
||||||
|
);
|
||||||
|
|
||||||
|
const result = await firstValueFrom(service.restricted$);
|
||||||
|
|
||||||
|
expect(result).toEqual<RestrictedCipherType[]>([
|
||||||
|
{ cipherType: CipherType.Card, allowViewOrgIds: [] },
|
||||||
|
{ cipherType: CipherType.Login, allowViewOrgIds: ["org2"] },
|
||||||
|
{ cipherType: CipherType.Identity, allowViewOrgIds: ["org1"] },
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
});
|
||||||
80
libs/vault/src/services/restricted-item-types.service.ts
Normal file
80
libs/vault/src/services/restricted-item-types.service.ts
Normal file
@@ -0,0 +1,80 @@
|
|||||||
|
import { Injectable } from "@angular/core";
|
||||||
|
import { combineLatest, map, of, Observable } from "rxjs";
|
||||||
|
import { switchMap, distinctUntilChanged, shareReplay } from "rxjs/operators";
|
||||||
|
|
||||||
|
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 { PolicyType } from "@bitwarden/common/admin-console/enums";
|
||||||
|
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||||
|
import { getUserId } from "@bitwarden/common/auth/services/account.service";
|
||||||
|
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
|
||||||
|
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||||
|
import { CipherType } from "@bitwarden/common/vault/enums";
|
||||||
|
|
||||||
|
export type RestrictedCipherType = {
|
||||||
|
cipherType: CipherType;
|
||||||
|
allowViewOrgIds: string[];
|
||||||
|
};
|
||||||
|
|
||||||
|
@Injectable({ providedIn: "root" })
|
||||||
|
export class RestrictedItemTypesService {
|
||||||
|
/**
|
||||||
|
* Emits an array of RestrictedCipherType objects:
|
||||||
|
* - cipherType: each type restricted by at least one org-level policy
|
||||||
|
* - allowViewOrgIds: org IDs that allow viewing that type
|
||||||
|
*/
|
||||||
|
readonly restricted$: Observable<RestrictedCipherType[]> = this.configService
|
||||||
|
.getFeatureFlag$(FeatureFlag.RemoveCardItemTypePolicy)
|
||||||
|
.pipe(
|
||||||
|
switchMap((flagOn) => {
|
||||||
|
if (!flagOn) {
|
||||||
|
return of([]);
|
||||||
|
}
|
||||||
|
return this.accountService.activeAccount$.pipe(
|
||||||
|
getUserId,
|
||||||
|
switchMap((userId) =>
|
||||||
|
combineLatest([
|
||||||
|
this.organizationService.organizations$(userId),
|
||||||
|
this.policyService.policiesByType$(PolicyType.RestrictedItemTypes, userId),
|
||||||
|
]),
|
||||||
|
),
|
||||||
|
map(([orgs, enabledPolicies]) => {
|
||||||
|
// Helper to extract restricted types, defaulting to [Card]
|
||||||
|
const restrictedTypes = (p: (typeof enabledPolicies)[number]) =>
|
||||||
|
(p.data as CipherType[]) ?? [CipherType.Card];
|
||||||
|
|
||||||
|
// Union across all enabled policies
|
||||||
|
const allRestrictedTypes = Array.from(
|
||||||
|
new Set(enabledPolicies.flatMap(restrictedTypes)),
|
||||||
|
);
|
||||||
|
|
||||||
|
return allRestrictedTypes.map((cipherType) => {
|
||||||
|
// Determine which orgs allow viewing this type
|
||||||
|
const allowViewOrgIds = orgs
|
||||||
|
.filter((org) => {
|
||||||
|
const orgPolicy = enabledPolicies.find((p) => p.organizationId === org.id);
|
||||||
|
// no policy for this org => allows everything
|
||||||
|
if (!orgPolicy) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
// if this type not in their restricted list => they allow it
|
||||||
|
return !restrictedTypes(orgPolicy).includes(cipherType);
|
||||||
|
})
|
||||||
|
.map((org) => org.id);
|
||||||
|
|
||||||
|
return { cipherType, allowViewOrgIds };
|
||||||
|
});
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
distinctUntilChanged(),
|
||||||
|
shareReplay({ bufferSize: 1, refCount: true }),
|
||||||
|
);
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private configService: ConfigService,
|
||||||
|
private accountService: AccountService,
|
||||||
|
private organizationService: OrganizationService,
|
||||||
|
private policyService: PolicyService,
|
||||||
|
) {}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user