mirror of
https://github.com/bitwarden/browser
synced 2025-12-10 13:23:34 +00:00
[PM-20643] - [Vault] [Desktop] Front End Changes to Enforce "Remove card item type policy" (#15176)
* add restricted item types to legacy vault components * filter out restricted item types from new menu item in desktop * use CIPHER_MENU_ITEMS * use CIPHER_MENU_ITEMS. move restricted cipher service to common * use move restricted item types service to libs. re-use cipher menu items * add shareReplay. change variable name * move restricted filter to search service. remove unecessary import * add reusable service method * clean up spec * add optional chain * remove duplicate import * move isCipherViewRestricted to service module * fix logic * fix logic * remove extra space --------- Co-authored-by: SmithThe4th <gsmith@bitwarden.com>
This commit is contained in:
@@ -294,6 +294,7 @@ import { DefaultCipherEncryptionService } from "@bitwarden/common/vault/services
|
||||
import { CipherFileUploadService } from "@bitwarden/common/vault/services/file-upload/cipher-file-upload.service";
|
||||
import { FolderApiService } from "@bitwarden/common/vault/services/folder/folder-api.service";
|
||||
import { FolderService } from "@bitwarden/common/vault/services/folder/folder.service";
|
||||
import { RestrictedItemTypesService } from "@bitwarden/common/vault/services/restricted-item-types.service";
|
||||
import { TotpService } from "@bitwarden/common/vault/services/totp.service";
|
||||
import { VaultSettingsService } from "@bitwarden/common/vault/services/vault-settings/vault-settings.service";
|
||||
import { DefaultTaskService, TaskService } from "@bitwarden/common/vault/tasks";
|
||||
@@ -680,6 +681,11 @@ const safeProviders: SafeProvider[] = [
|
||||
KdfConfigService,
|
||||
],
|
||||
}),
|
||||
safeProvider({
|
||||
provide: RestrictedItemTypesService,
|
||||
useClass: RestrictedItemTypesService,
|
||||
deps: [ConfigService, AccountService, OrganizationServiceAbstraction, PolicyServiceAbstraction],
|
||||
}),
|
||||
safeProvider({
|
||||
provide: PasswordStrengthServiceAbstraction,
|
||||
useClass: PasswordStrengthService,
|
||||
|
||||
@@ -84,7 +84,6 @@ export class AddEditComponent implements OnInit, OnDestroy {
|
||||
showCardNumber = false;
|
||||
showCardCode = false;
|
||||
cipherType = CipherType;
|
||||
typeOptions: any[];
|
||||
cardBrandOptions: any[];
|
||||
cardExpMonthOptions: any[];
|
||||
identityTitleOptions: any[];
|
||||
@@ -139,13 +138,6 @@ export class AddEditComponent implements OnInit, OnDestroy {
|
||||
protected sdkService: SdkService,
|
||||
private sshImportPromptService: SshImportPromptService,
|
||||
) {
|
||||
this.typeOptions = [
|
||||
{ name: i18nService.t("typeLogin"), value: CipherType.Login },
|
||||
{ name: i18nService.t("typeCard"), value: CipherType.Card },
|
||||
{ name: i18nService.t("typeIdentity"), value: CipherType.Identity },
|
||||
{ name: i18nService.t("typeSecureNote"), value: CipherType.SecureNote },
|
||||
];
|
||||
|
||||
this.cardBrandOptions = [
|
||||
{ name: "-- " + i18nService.t("select") + " --", value: null },
|
||||
{ name: "Visa", value: "Visa" },
|
||||
@@ -215,8 +207,6 @@ export class AddEditComponent implements OnInit, OnDestroy {
|
||||
|
||||
this.writeableCollections = await this.loadCollections();
|
||||
this.canUseReprompt = await this.passwordRepromptService.enabled();
|
||||
|
||||
this.typeOptions.push({ name: this.i18nService.t("typeSshKey"), value: CipherType.SshKey });
|
||||
}
|
||||
|
||||
ngOnDestroy() {
|
||||
|
||||
@@ -8,7 +8,9 @@ import {
|
||||
combineLatest,
|
||||
filter,
|
||||
from,
|
||||
map,
|
||||
of,
|
||||
shareReplay,
|
||||
switchMap,
|
||||
takeUntil,
|
||||
} from "rxjs";
|
||||
@@ -20,6 +22,11 @@ import { getUserId } from "@bitwarden/common/auth/services/account.service";
|
||||
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
||||
import { CipherType } from "@bitwarden/common/vault/enums";
|
||||
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
||||
import {
|
||||
isCipherViewRestricted,
|
||||
RestrictedItemTypesService,
|
||||
} from "@bitwarden/common/vault/services/restricted-item-types.service";
|
||||
import { CIPHER_MENU_ITEMS } from "@bitwarden/common/vault/types/cipher-menu-items";
|
||||
|
||||
@Directive()
|
||||
export class VaultItemsComponent implements OnInit, OnDestroy {
|
||||
@@ -35,6 +42,19 @@ export class VaultItemsComponent implements OnInit, OnDestroy {
|
||||
organization: Organization;
|
||||
CipherType = CipherType;
|
||||
|
||||
protected itemTypes$ = this.restrictedItemTypesService.restricted$.pipe(
|
||||
map((restrictedItemTypes) =>
|
||||
// Filter out restricted item types
|
||||
CIPHER_MENU_ITEMS.filter(
|
||||
(itemType) =>
|
||||
!restrictedItemTypes.some(
|
||||
(restrictedType) => restrictedType.cipherType === itemType.type,
|
||||
),
|
||||
),
|
||||
),
|
||||
shareReplay({ bufferSize: 1, refCount: true }),
|
||||
);
|
||||
|
||||
protected searchPending = false;
|
||||
|
||||
/** Construct filters as an observable so it can be appended to the cipher stream. */
|
||||
@@ -62,6 +82,7 @@ export class VaultItemsComponent implements OnInit, OnDestroy {
|
||||
protected searchService: SearchService,
|
||||
protected cipherService: CipherService,
|
||||
protected accountService: AccountService,
|
||||
protected restrictedItemTypesService: RestrictedItemTypesService,
|
||||
) {
|
||||
this.subscribeToCiphers();
|
||||
}
|
||||
@@ -143,18 +164,22 @@ export class VaultItemsComponent implements OnInit, OnDestroy {
|
||||
this._searchText$,
|
||||
this._filter$,
|
||||
of(userId),
|
||||
this.restrictedItemTypesService.restricted$,
|
||||
]),
|
||||
),
|
||||
switchMap(([indexedCiphers, failedCiphers, searchText, filter, userId]) => {
|
||||
switchMap(([indexedCiphers, failedCiphers, searchText, filter, userId, restricted]) => {
|
||||
let allCiphers = indexedCiphers ?? [];
|
||||
const _failedCiphers = failedCiphers ?? [];
|
||||
|
||||
allCiphers = [..._failedCiphers, ...allCiphers];
|
||||
|
||||
const restrictedTypeFilter = (cipher: CipherView) =>
|
||||
isCipherViewRestricted(cipher, restricted);
|
||||
|
||||
return this.searchService.searchCiphers(
|
||||
userId,
|
||||
searchText,
|
||||
[filter, this.deletedFilter],
|
||||
[filter, this.deletedFilter, restrictedTypeFilter],
|
||||
allCiphers,
|
||||
);
|
||||
}),
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
|
||||
import { ITreeNodeObject, TreeNode } from "./models/domain/tree-node";
|
||||
|
||||
export class ServiceUtils {
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { TestBed } from "@angular/core/testing";
|
||||
import { mock, MockProxy } from "jest-mock-extended";
|
||||
import { firstValueFrom, of } from "rxjs";
|
||||
|
||||
@@ -49,19 +48,16 @@ describe("RestrictedItemTypesService", () => {
|
||||
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);
|
||||
|
||||
service = new RestrictedItemTypesService(
|
||||
configService,
|
||||
accountService,
|
||||
organizationService,
|
||||
policyService,
|
||||
);
|
||||
});
|
||||
|
||||
it("emits empty array when feature flag is disabled", async () => {
|
||||
@@ -106,7 +102,6 @@ describe("RestrictedItemTypesService", () => {
|
||||
});
|
||||
|
||||
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]));
|
||||
|
||||
@@ -117,7 +112,6 @@ describe("RestrictedItemTypesService", () => {
|
||||
});
|
||||
|
||||
it("aggregates multiple types and computes allowViewOrgIds correctly", async () => {
|
||||
configService.getFeatureFlag$.mockReturnValue(of(true));
|
||||
organizationService.organizations$.mockReturnValue(of([org1, org2]));
|
||||
policyService.policiesByType$.mockReturnValue(
|
||||
of([
|
||||
@@ -1,4 +1,3 @@
|
||||
import { Injectable } from "@angular/core";
|
||||
import { combineLatest, map, of, Observable } from "rxjs";
|
||||
import { switchMap, distinctUntilChanged, shareReplay } from "rxjs/operators";
|
||||
|
||||
@@ -10,13 +9,13 @@ 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";
|
||||
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
||||
|
||||
export type RestrictedCipherType = {
|
||||
cipherType: CipherType;
|
||||
allowViewOrgIds: string[];
|
||||
};
|
||||
|
||||
@Injectable({ providedIn: "root" })
|
||||
export class RestrictedItemTypesService {
|
||||
/**
|
||||
* Emits an array of RestrictedCipherType objects:
|
||||
@@ -78,3 +77,25 @@ export class RestrictedItemTypesService {
|
||||
private policyService: PolicyService,
|
||||
) {}
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter that returns whether a cipher is restricted from being viewed by the user
|
||||
* Criteria:
|
||||
* - the cipher's type is restricted by at least one org
|
||||
* UNLESS
|
||||
* - the cipher belongs to an organization and that organization does not restrict that type
|
||||
* OR
|
||||
* - the cipher belongs to the user's personal vault and at least one other organization does not restrict that type
|
||||
*/
|
||||
export function isCipherViewRestricted(
|
||||
cipher: CipherView,
|
||||
restrictedTypes: RestrictedCipherType[],
|
||||
) {
|
||||
return restrictedTypes.some(
|
||||
(restrictedType) =>
|
||||
restrictedType.cipherType === cipher.type &&
|
||||
(cipher.organizationId
|
||||
? !restrictedType.allowViewOrgIds.includes(cipher.organizationId)
|
||||
: restrictedType.allowViewOrgIds.length === 0),
|
||||
);
|
||||
}
|
||||
@@ -19,6 +19,6 @@ export const CIPHER_MENU_ITEMS = Object.freeze([
|
||||
{ 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.SecureNote, icon: "bwi-sticky-note", labelKey: "typeNote" },
|
||||
{ type: CipherType.SshKey, icon: "bwi-key", labelKey: "typeSshKey" },
|
||||
] as const) satisfies readonly CipherMenuItem[];
|
||||
|
||||
@@ -24,10 +24,6 @@ export * as VaultIcons from "./icons";
|
||||
|
||||
export { DefaultSshImportPromptService } from "./services/default-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 "./services/default-change-login-password.service";
|
||||
|
||||
Reference in New Issue
Block a user