mirror of
https://github.com/bitwarden/browser
synced 2025-12-12 14:23:32 +00:00
[PM-20644] [Vault] [Browser Extension] Front End Changes to Enforce "Remove card item type policy" (#15147)
* Added service to get restricted cipher and used that to hide in autofill settings * Referenced files from the work done on web * Fixed restrictedCardType$ observable * Created resuseable cipher menu items type (cherry picked from commit 34be7f7ffef135aea2449e11e45e638ebaf34ee8) * Updated new item dropdown to filter out restricted type and also render the menu items dynamically (cherry picked from commit 566099ba9f3dbd7f18077dbc5b8ed44f51a94bfc) * Updated service to have cipher types as an observable (cherry picked from commit 6848e5f75803eb45e2262c617c9805359861ad14) * Refactored service to have use CIPHER MENU ITEMS type and filter restricted rypes and return an observable (cherry picked from commit e25c4eb18af895deac762b9e2d7ae69cc235f224) * Fixed type enum * Referenced files from the work done on web * Referenced change from the work done on web * Remove comment * Remove cipher type from autofill suggestion list when enabled * revert autofillcipher$ change * Fixed test * Added sharereplay to restrictedCardType$ observable * Added startwith operator * Add organization exemptions to restricted filter
This commit is contained in:
@@ -65,7 +65,10 @@
|
|||||||
{{ "showInlineMenuIdentitiesLabel" | i18n }}
|
{{ "showInlineMenuIdentitiesLabel" | i18n }}
|
||||||
</bit-label>
|
</bit-label>
|
||||||
</bit-form-control>
|
</bit-form-control>
|
||||||
<bit-form-control *ngIf="enableInlineMenu" class="tw-ml-5">
|
<bit-form-control
|
||||||
|
*ngIf="enableInlineMenu && !(restrictedCardType$ | async)"
|
||||||
|
class="tw-ml-5"
|
||||||
|
>
|
||||||
<input
|
<input
|
||||||
bitCheckbox
|
bitCheckbox
|
||||||
id="show-inline-menu-cards"
|
id="show-inline-menu-cards"
|
||||||
@@ -114,7 +117,7 @@
|
|||||||
</a>
|
</a>
|
||||||
</bit-hint>
|
</bit-hint>
|
||||||
</bit-form-control>
|
</bit-form-control>
|
||||||
<bit-form-control>
|
<bit-form-control *ngIf="!(restrictedCardType$ | async)">
|
||||||
<input
|
<input
|
||||||
bitCheckbox
|
bitCheckbox
|
||||||
id="showCardsSuggestions"
|
id="showCardsSuggestions"
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import {
|
|||||||
ReactiveFormsModule,
|
ReactiveFormsModule,
|
||||||
} from "@angular/forms";
|
} from "@angular/forms";
|
||||||
import { RouterModule } from "@angular/router";
|
import { RouterModule } from "@angular/router";
|
||||||
import { filter, firstValueFrom, Observable, switchMap } from "rxjs";
|
import { filter, firstValueFrom, map, Observable, shareReplay, switchMap } from "rxjs";
|
||||||
|
|
||||||
import { JslibModule } from "@bitwarden/angular/jslib.module";
|
import { JslibModule } from "@bitwarden/angular/jslib.module";
|
||||||
import { NudgesService, NudgeType } from "@bitwarden/angular/vault";
|
import { NudgesService, NudgeType } from "@bitwarden/angular/vault";
|
||||||
@@ -44,6 +44,7 @@ import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.servic
|
|||||||
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
|
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
|
||||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||||
import { VaultSettingsService } from "@bitwarden/common/vault/abstractions/vault-settings/vault-settings.service";
|
import { VaultSettingsService } from "@bitwarden/common/vault/abstractions/vault-settings/vault-settings.service";
|
||||||
|
import { CipherType } from "@bitwarden/common/vault/enums";
|
||||||
import {
|
import {
|
||||||
CardComponent,
|
CardComponent,
|
||||||
CheckboxModule,
|
CheckboxModule,
|
||||||
@@ -57,6 +58,7 @@ import {
|
|||||||
SelectModule,
|
SelectModule,
|
||||||
TypographyModule,
|
TypographyModule,
|
||||||
} from "@bitwarden/components";
|
} from "@bitwarden/components";
|
||||||
|
import { RestrictedItemTypesService } from "@bitwarden/vault";
|
||||||
|
|
||||||
import { AutofillBrowserSettingsService } from "../../../autofill/services/autofill-browser-settings.service";
|
import { AutofillBrowserSettingsService } from "../../../autofill/services/autofill-browser-settings.service";
|
||||||
import { BrowserApi } from "../../../platform/browser/browser-api";
|
import { BrowserApi } from "../../../platform/browser/browser-api";
|
||||||
@@ -111,6 +113,11 @@ export class AutofillComponent implements OnInit {
|
|||||||
this.nudgesService.showNudgeSpotlight$(NudgeType.AutofillNudge, account.id),
|
this.nudgesService.showNudgeSpotlight$(NudgeType.AutofillNudge, account.id),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
protected restrictedCardType$: Observable<boolean> =
|
||||||
|
this.restrictedItemTypesService.restricted$.pipe(
|
||||||
|
map((restrictedTypes) => restrictedTypes.some((type) => type.cipherType === CipherType.Card)),
|
||||||
|
shareReplay({ bufferSize: 1, refCount: true }),
|
||||||
|
);
|
||||||
|
|
||||||
protected autofillOnPageLoadForm = new FormGroup({
|
protected autofillOnPageLoadForm = new FormGroup({
|
||||||
autofillOnPageLoad: new FormControl(),
|
autofillOnPageLoad: new FormControl(),
|
||||||
@@ -156,6 +163,7 @@ export class AutofillComponent implements OnInit {
|
|||||||
private nudgesService: NudgesService,
|
private nudgesService: NudgesService,
|
||||||
private accountService: AccountService,
|
private accountService: AccountService,
|
||||||
private autofillBrowserSettingsService: AutofillBrowserSettingsService,
|
private autofillBrowserSettingsService: AutofillBrowserSettingsService,
|
||||||
|
private restrictedItemTypesService: RestrictedItemTypesService,
|
||||||
) {
|
) {
|
||||||
this.autofillOnPageLoadOptions = [
|
this.autofillOnPageLoadOptions = [
|
||||||
{ name: this.i18nService.t("autoFillOnPageLoadYes"), value: true },
|
{ name: this.i18nService.t("autoFillOnPageLoadYes"), value: true },
|
||||||
|
|||||||
@@ -3,34 +3,12 @@
|
|||||||
{{ "new" | i18n }}
|
{{ "new" | i18n }}
|
||||||
</button>
|
</button>
|
||||||
<bit-menu #itemOptions>
|
<bit-menu #itemOptions>
|
||||||
<a bitMenuItem [routerLink]="['/add-cipher']" [queryParams]="buildQueryParams(cipherType.Login)">
|
@for (menuItem of cipherMenuItems$ | async; track menuItem.type) {
|
||||||
<i class="bwi bwi-globe" slot="start" aria-hidden="true"></i>
|
<a bitMenuItem [routerLink]="['/add-cipher']" [queryParams]="buildQueryParams(menuItem.type)">
|
||||||
{{ "typeLogin" | i18n }}
|
<i [class]="`bwi ${menuItem.icon}`" slot="start" aria-hidden="true"></i>
|
||||||
</a>
|
{{ menuItem.labelKey | i18n }}
|
||||||
<a bitMenuItem [routerLink]="['/add-cipher']" [queryParams]="buildQueryParams(cipherType.Card)">
|
</a>
|
||||||
<i class="bwi bwi-credit-card" slot="start" aria-hidden="true"></i>
|
}
|
||||||
{{ "typeCard" | i18n }}
|
|
||||||
</a>
|
|
||||||
<a
|
|
||||||
bitMenuItem
|
|
||||||
[routerLink]="['/add-cipher']"
|
|
||||||
[queryParams]="buildQueryParams(cipherType.Identity)"
|
|
||||||
>
|
|
||||||
<i class="bwi bwi-id-card" slot="start" aria-hidden="true"></i>
|
|
||||||
{{ "typeIdentity" | i18n }}
|
|
||||||
</a>
|
|
||||||
<a
|
|
||||||
bitMenuItem
|
|
||||||
[routerLink]="['/add-cipher']"
|
|
||||||
[queryParams]="buildQueryParams(cipherType.SecureNote)"
|
|
||||||
>
|
|
||||||
<i class="bwi bwi-sticky-note" slot="start" aria-hidden="true"></i>
|
|
||||||
{{ "note" | i18n }}
|
|
||||||
</a>
|
|
||||||
<a bitMenuItem [routerLink]="['/add-cipher']" [queryParams]="buildQueryParams(cipherType.SshKey)">
|
|
||||||
<i class="bwi bwi-key" slot="start" aria-hidden="true"></i>
|
|
||||||
{{ "typeSshKey" | i18n }}
|
|
||||||
</a>
|
|
||||||
<bit-menu-divider></bit-menu-divider>
|
<bit-menu-divider></bit-menu-divider>
|
||||||
<button type="button" bitMenuItem (click)="openFolderDialog()">
|
<button type="button" bitMenuItem (click)="openFolderDialog()">
|
||||||
<i class="bwi bwi-folder" slot="start" aria-hidden="true"></i>
|
<i class="bwi bwi-folder" slot="start" aria-hidden="true"></i>
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { CommonModule } from "@angular/common";
|
|||||||
import { ComponentFixture, TestBed } from "@angular/core/testing";
|
import { ComponentFixture, TestBed } from "@angular/core/testing";
|
||||||
import { ActivatedRoute, RouterLink } from "@angular/router";
|
import { ActivatedRoute, RouterLink } from "@angular/router";
|
||||||
import { mock } from "jest-mock-extended";
|
import { mock } from "jest-mock-extended";
|
||||||
|
import { BehaviorSubject } from "rxjs";
|
||||||
|
|
||||||
import { JslibModule } from "@bitwarden/angular/jslib.module";
|
import { JslibModule } from "@bitwarden/angular/jslib.module";
|
||||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||||
@@ -12,6 +13,7 @@ import { FolderApiServiceAbstraction } from "@bitwarden/common/vault/abstraction
|
|||||||
import { FolderService } from "@bitwarden/common/vault/abstractions/folder/folder.service.abstraction";
|
import { FolderService } from "@bitwarden/common/vault/abstractions/folder/folder.service.abstraction";
|
||||||
import { CipherType } from "@bitwarden/common/vault/enums";
|
import { CipherType } from "@bitwarden/common/vault/enums";
|
||||||
import { ButtonModule, DialogService, MenuModule, NoItemsModule } from "@bitwarden/components";
|
import { ButtonModule, DialogService, MenuModule, NoItemsModule } from "@bitwarden/components";
|
||||||
|
import { RestrictedCipherType, RestrictedItemTypesService } from "@bitwarden/vault";
|
||||||
|
|
||||||
import { BrowserApi } from "../../../../../platform/browser/browser-api";
|
import { BrowserApi } from "../../../../../platform/browser/browser-api";
|
||||||
import BrowserPopupUtils from "../../../../../platform/popup/browser-popup-utils";
|
import BrowserPopupUtils from "../../../../../platform/popup/browser-popup-utils";
|
||||||
@@ -23,6 +25,7 @@ describe("NewItemDropdownV2Component", () => {
|
|||||||
let fixture: ComponentFixture<NewItemDropdownV2Component>;
|
let fixture: ComponentFixture<NewItemDropdownV2Component>;
|
||||||
let dialogServiceMock: jest.Mocked<DialogService>;
|
let dialogServiceMock: jest.Mocked<DialogService>;
|
||||||
let browserApiMock: jest.Mocked<typeof BrowserApi>;
|
let browserApiMock: jest.Mocked<typeof BrowserApi>;
|
||||||
|
let restrictedItemTypesServiceMock: jest.Mocked<RestrictedItemTypesService>;
|
||||||
|
|
||||||
const mockTab = { url: "https://example.com" };
|
const mockTab = { url: "https://example.com" };
|
||||||
|
|
||||||
@@ -44,6 +47,9 @@ describe("NewItemDropdownV2Component", () => {
|
|||||||
const folderServiceMock = mock<FolderService>();
|
const folderServiceMock = mock<FolderService>();
|
||||||
const folderApiServiceAbstractionMock = mock<FolderApiServiceAbstraction>();
|
const folderApiServiceAbstractionMock = mock<FolderApiServiceAbstraction>();
|
||||||
const accountServiceMock = mock<AccountService>();
|
const accountServiceMock = mock<AccountService>();
|
||||||
|
restrictedItemTypesServiceMock = {
|
||||||
|
restricted$: new BehaviorSubject<RestrictedCipherType[]>([]),
|
||||||
|
} as any;
|
||||||
|
|
||||||
await TestBed.configureTestingModule({
|
await TestBed.configureTestingModule({
|
||||||
imports: [
|
imports: [
|
||||||
@@ -65,6 +71,7 @@ describe("NewItemDropdownV2Component", () => {
|
|||||||
{ provide: FolderService, useValue: folderServiceMock },
|
{ provide: FolderService, useValue: folderServiceMock },
|
||||||
{ provide: FolderApiServiceAbstraction, useValue: folderApiServiceAbstractionMock },
|
{ provide: FolderApiServiceAbstraction, useValue: folderApiServiceAbstractionMock },
|
||||||
{ provide: AccountService, useValue: accountServiceMock },
|
{ provide: AccountService, useValue: accountServiceMock },
|
||||||
|
{ provide: RestrictedItemTypesService, useValue: restrictedItemTypesServiceMock },
|
||||||
],
|
],
|
||||||
}).compileComponents();
|
}).compileComponents();
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -3,12 +3,14 @@
|
|||||||
import { CommonModule } from "@angular/common";
|
import { CommonModule } from "@angular/common";
|
||||||
import { Component, Input, OnInit } from "@angular/core";
|
import { Component, Input, OnInit } from "@angular/core";
|
||||||
import { RouterLink } from "@angular/router";
|
import { RouterLink } from "@angular/router";
|
||||||
|
import { map, Observable } from "rxjs";
|
||||||
|
|
||||||
import { JslibModule } from "@bitwarden/angular/jslib.module";
|
import { JslibModule } from "@bitwarden/angular/jslib.module";
|
||||||
import { CollectionId, OrganizationId } from "@bitwarden/common/types/guid";
|
import { CollectionId, OrganizationId } from "@bitwarden/common/types/guid";
|
||||||
import { CipherType } from "@bitwarden/common/vault/enums";
|
import { CipherType } from "@bitwarden/common/vault/enums";
|
||||||
|
import { CipherMenuItem, CIPHER_MENU_ITEMS } from "@bitwarden/common/vault/types/cipher-menu-items";
|
||||||
import { ButtonModule, DialogService, MenuModule, NoItemsModule } from "@bitwarden/components";
|
import { ButtonModule, DialogService, MenuModule, NoItemsModule } from "@bitwarden/components";
|
||||||
import { AddEditFolderDialogComponent } from "@bitwarden/vault";
|
import { AddEditFolderDialogComponent, RestrictedItemTypesService } from "@bitwarden/vault";
|
||||||
|
|
||||||
import { BrowserApi } from "../../../../../platform/browser/browser-api";
|
import { BrowserApi } from "../../../../../platform/browser/browser-api";
|
||||||
import BrowserPopupUtils from "../../../../../platform/popup/browser-popup-utils";
|
import BrowserPopupUtils from "../../../../../platform/popup/browser-popup-utils";
|
||||||
@@ -34,7 +36,22 @@ export class NewItemDropdownV2Component implements OnInit {
|
|||||||
@Input()
|
@Input()
|
||||||
initialValues: NewItemInitialValues;
|
initialValues: NewItemInitialValues;
|
||||||
|
|
||||||
constructor(private dialogService: DialogService) {}
|
/**
|
||||||
|
* Observable of cipher menu items that are not restricted by policy
|
||||||
|
*/
|
||||||
|
readonly cipherMenuItems$: Observable<CipherMenuItem[]> =
|
||||||
|
this.restrictedItemTypeService.restricted$.pipe(
|
||||||
|
map((restrictedTypes) => {
|
||||||
|
const restrictedTypeArr = restrictedTypes.map((item) => item.cipherType);
|
||||||
|
|
||||||
|
return CIPHER_MENU_ITEMS.filter((menuItem) => !restrictedTypeArr.includes(menuItem.type));
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private dialogService: DialogService,
|
||||||
|
private restrictedItemTypeService: RestrictedItemTypesService,
|
||||||
|
) {}
|
||||||
|
|
||||||
async ngOnInit() {
|
async ngOnInit() {
|
||||||
this.tab = await BrowserApi.getTabFromCurrentWindow();
|
this.tab = await BrowserApi.getTabFromCurrentWindow();
|
||||||
|
|||||||
@@ -42,7 +42,7 @@
|
|||||||
fullWidth
|
fullWidth
|
||||||
placeholderIcon="bwi-list"
|
placeholderIcon="bwi-list"
|
||||||
[placeholderText]="'type' | i18n"
|
[placeholderText]="'type' | i18n"
|
||||||
[options]="cipherTypes"
|
[options]="cipherTypes$ | async"
|
||||||
>
|
>
|
||||||
</bit-chip-select>
|
</bit-chip-select>
|
||||||
</form>
|
</form>
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ export class VaultListFiltersComponent {
|
|||||||
protected organizations$ = this.vaultPopupListFiltersService.organizations$;
|
protected organizations$ = this.vaultPopupListFiltersService.organizations$;
|
||||||
protected collections$ = this.vaultPopupListFiltersService.collections$;
|
protected collections$ = this.vaultPopupListFiltersService.collections$;
|
||||||
protected folders$ = this.vaultPopupListFiltersService.folders$;
|
protected folders$ = this.vaultPopupListFiltersService.folders$;
|
||||||
protected cipherTypes = this.vaultPopupListFiltersService.cipherTypes;
|
protected cipherTypes$ = this.vaultPopupListFiltersService.cipherTypes$;
|
||||||
|
|
||||||
// Combine all filters into a single observable to eliminate the filters from loading separately in the UI.
|
// Combine all filters into a single observable to eliminate the filters from loading separately in the UI.
|
||||||
protected allFilters$ = combineLatest([
|
protected allFilters$ = combineLatest([
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ import { FolderService } from "@bitwarden/common/vault/abstractions/folder/folde
|
|||||||
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 { FolderView } from "@bitwarden/common/vault/models/view/folder.view";
|
import { FolderView } from "@bitwarden/common/vault/models/view/folder.view";
|
||||||
|
import { RestrictedCipherType, RestrictedItemTypesService } from "@bitwarden/vault";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
CachedFilterState,
|
CachedFilterState,
|
||||||
@@ -70,6 +71,10 @@ describe("VaultPopupListFiltersService", () => {
|
|||||||
const state$ = new BehaviorSubject<boolean>(false);
|
const state$ = new BehaviorSubject<boolean>(false);
|
||||||
const update = jest.fn().mockResolvedValue(undefined);
|
const update = jest.fn().mockResolvedValue(undefined);
|
||||||
|
|
||||||
|
const restrictedItemTypesService = {
|
||||||
|
restricted$: new BehaviorSubject<RestrictedCipherType[]>([]),
|
||||||
|
};
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
_memberOrganizations$ = new BehaviorSubject<Organization[]>([]); // Fresh instance per test
|
_memberOrganizations$ = new BehaviorSubject<Organization[]>([]); // Fresh instance per test
|
||||||
folderViews$ = new BehaviorSubject([]); // Fresh instance per test
|
folderViews$ = new BehaviorSubject([]); // Fresh instance per test
|
||||||
@@ -125,21 +130,46 @@ describe("VaultPopupListFiltersService", () => {
|
|||||||
provide: ViewCacheService,
|
provide: ViewCacheService,
|
||||||
useValue: viewCacheService,
|
useValue: viewCacheService,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
provide: RestrictedItemTypesService,
|
||||||
|
useValue: restrictedItemTypesService,
|
||||||
|
},
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
|
|
||||||
service = TestBed.inject(VaultPopupListFiltersService);
|
service = TestBed.inject(VaultPopupListFiltersService);
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("cipherTypes", () => {
|
describe("cipherTypes$", () => {
|
||||||
it("returns all cipher types", () => {
|
it("returns all cipher types when no restrictions", (done) => {
|
||||||
expect(service.cipherTypes.map((c) => c.value)).toEqual([
|
restrictedItemTypesService.restricted$.next([]);
|
||||||
CipherType.Login,
|
|
||||||
CipherType.Card,
|
service.cipherTypes$.subscribe((cipherTypes) => {
|
||||||
CipherType.Identity,
|
expect(cipherTypes.map((c) => c.value)).toEqual([
|
||||||
CipherType.SecureNote,
|
CipherType.Login,
|
||||||
CipherType.SshKey,
|
CipherType.Card,
|
||||||
|
CipherType.Identity,
|
||||||
|
CipherType.SecureNote,
|
||||||
|
CipherType.SshKey,
|
||||||
|
]);
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("filters out restricted cipher types", (done) => {
|
||||||
|
restrictedItemTypesService.restricted$.next([
|
||||||
|
{ cipherType: CipherType.Card, allowViewOrgIds: [] },
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
service.cipherTypes$.subscribe((cipherTypes) => {
|
||||||
|
expect(cipherTypes.map((c) => c.value)).toEqual([
|
||||||
|
CipherType.Login,
|
||||||
|
CipherType.Identity,
|
||||||
|
CipherType.SecureNote,
|
||||||
|
CipherType.SshKey,
|
||||||
|
]);
|
||||||
|
done();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -452,6 +482,10 @@ describe("VaultPopupListFiltersService", () => {
|
|||||||
{ type: CipherType.SecureNote, collectionIds: [], organizationId: null },
|
{ type: CipherType.SecureNote, collectionIds: [], organizationId: null },
|
||||||
] as CipherView[];
|
] as CipherView[];
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
restrictedItemTypesService.restricted$.next([]);
|
||||||
|
});
|
||||||
|
|
||||||
it("filters by cipherType", (done) => {
|
it("filters by cipherType", (done) => {
|
||||||
service.filterFunction$.subscribe((filterFunction) => {
|
service.filterFunction$.subscribe((filterFunction) => {
|
||||||
expect(filterFunction(ciphers)).toEqual([ciphers[0]]);
|
expect(filterFunction(ciphers)).toEqual([ciphers[0]]);
|
||||||
@@ -690,6 +724,9 @@ function createSeededVaultPopupListFiltersService(
|
|||||||
} as any;
|
} as any;
|
||||||
|
|
||||||
const accountServiceMock = mockAccountServiceWith("userId" as UserId);
|
const accountServiceMock = mockAccountServiceWith("userId" as UserId);
|
||||||
|
const restrictedItemTypesServiceMock = {
|
||||||
|
restricted$: new BehaviorSubject<RestrictedCipherType[]>([]),
|
||||||
|
} as any;
|
||||||
const formBuilderInstance = new FormBuilder();
|
const formBuilderInstance = new FormBuilder();
|
||||||
|
|
||||||
const seededCachedSignal = createMockSignal<CachedFilterState>(cachedState);
|
const seededCachedSignal = createMockSignal<CachedFilterState>(cachedState);
|
||||||
@@ -713,6 +750,7 @@ function createSeededVaultPopupListFiltersService(
|
|||||||
stateProviderMock,
|
stateProviderMock,
|
||||||
accountServiceMock,
|
accountServiceMock,
|
||||||
viewCacheServiceMock,
|
viewCacheServiceMock,
|
||||||
|
restrictedItemTypesServiceMock,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -39,7 +39,9 @@ import { ITreeNodeObject, TreeNode } from "@bitwarden/common/vault/models/domain
|
|||||||
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
||||||
import { FolderView } from "@bitwarden/common/vault/models/view/folder.view";
|
import { FolderView } from "@bitwarden/common/vault/models/view/folder.view";
|
||||||
import { ServiceUtils } from "@bitwarden/common/vault/service-utils";
|
import { ServiceUtils } from "@bitwarden/common/vault/service-utils";
|
||||||
|
import { CIPHER_MENU_ITEMS } from "@bitwarden/common/vault/types/cipher-menu-items";
|
||||||
import { ChipSelectOption } from "@bitwarden/components";
|
import { ChipSelectOption } from "@bitwarden/components";
|
||||||
|
import { RestrictedItemTypesService } from "@bitwarden/vault";
|
||||||
|
|
||||||
const FILTER_VISIBILITY_KEY = new KeyDefinition<boolean>(VAULT_SETTINGS_DISK, "filterVisibility", {
|
const FILTER_VISIBILITY_KEY = new KeyDefinition<boolean>(VAULT_SETTINGS_DISK, "filterVisibility", {
|
||||||
deserializer: (obj) => obj,
|
deserializer: (obj) => obj,
|
||||||
@@ -178,6 +180,7 @@ export class VaultPopupListFiltersService {
|
|||||||
private stateProvider: StateProvider,
|
private stateProvider: StateProvider,
|
||||||
private accountService: AccountService,
|
private accountService: AccountService,
|
||||||
private viewCacheService: ViewCacheService,
|
private viewCacheService: ViewCacheService,
|
||||||
|
private restrictedItemTypesService: RestrictedItemTypesService,
|
||||||
) {
|
) {
|
||||||
this.filterForm.controls.organization.valueChanges
|
this.filterForm.controls.organization.valueChanges
|
||||||
.pipe(takeUntilDestroyed())
|
.pipe(takeUntilDestroyed())
|
||||||
@@ -210,74 +213,80 @@ export class VaultPopupListFiltersService {
|
|||||||
/**
|
/**
|
||||||
* Observable whose value is a function that filters an array of `CipherView` objects based on the current filters
|
* Observable whose value is a function that filters an array of `CipherView` objects based on the current filters
|
||||||
*/
|
*/
|
||||||
filterFunction$: Observable<(ciphers: CipherView[]) => CipherView[]> = this.filters$.pipe(
|
filterFunction$: Observable<(ciphers: CipherView[]) => CipherView[]> = combineLatest([
|
||||||
|
this.filters$,
|
||||||
|
this.restrictedItemTypesService.restricted$.pipe(startWith([])),
|
||||||
|
]).pipe(
|
||||||
map(
|
map(
|
||||||
(filters) => (ciphers: CipherView[]) =>
|
([filters, restrictions]) =>
|
||||||
ciphers.filter((cipher) => {
|
(ciphers: CipherView[]) =>
|
||||||
// Vault popup lists never shows deleted ciphers
|
ciphers.filter((cipher) => {
|
||||||
if (cipher.isDeleted) {
|
// Vault popup lists never shows deleted ciphers
|
||||||
return false;
|
if (cipher.isDeleted) {
|
||||||
}
|
|
||||||
|
|
||||||
if (filters.cipherType !== null && cipher.type !== filters.cipherType) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (filters.collection && !cipher.collectionIds?.includes(filters.collection.id)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (filters.folder && cipher.folderId !== filters.folder.id) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
const isMyVault = filters.organization?.id === MY_VAULT_ID;
|
|
||||||
|
|
||||||
if (isMyVault) {
|
|
||||||
if (cipher.organizationId !== null) {
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
} else if (filters.organization) {
|
|
||||||
if (cipher.organizationId !== filters.organization.id) {
|
// Check if cipher type is restricted (with organization exemptions)
|
||||||
|
if (restrictions && restrictions.length > 0) {
|
||||||
|
const isRestricted = restrictions.some(
|
||||||
|
(restrictedType) =>
|
||||||
|
restrictedType.cipherType === cipher.type &&
|
||||||
|
(cipher.organizationId
|
||||||
|
? !restrictedType.allowViewOrgIds.includes(cipher.organizationId)
|
||||||
|
: restrictedType.allowViewOrgIds.length === 0),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (isRestricted) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (filters.cipherType !== null && cipher.type !== filters.cipherType) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
if (filters.collection && !cipher.collectionIds?.includes(filters.collection.id)) {
|
||||||
}),
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (filters.folder && cipher.folderId !== filters.folder.id) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const isMyVault = filters.organization?.id === MY_VAULT_ID;
|
||||||
|
|
||||||
|
if (isMyVault) {
|
||||||
|
if (cipher.organizationId !== null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
} else if (filters.organization) {
|
||||||
|
if (cipher.organizationId !== filters.organization.id) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* All available cipher types
|
* All available cipher types (filtered by policy restrictions)
|
||||||
*/
|
*/
|
||||||
readonly cipherTypes: ChipSelectOption<CipherType>[] = [
|
readonly cipherTypes$: Observable<ChipSelectOption<CipherType>[]> =
|
||||||
{
|
this.restrictedItemTypesService.restricted$.pipe(
|
||||||
value: CipherType.Login,
|
map((restrictedTypes) => {
|
||||||
label: this.i18nService.t("typeLogin"),
|
const restrictedCipherTypes = restrictedTypes.map((r) => r.cipherType);
|
||||||
icon: "bwi-globe",
|
|
||||||
},
|
return CIPHER_MENU_ITEMS.filter((item) => !restrictedCipherTypes.includes(item.type)).map(
|
||||||
{
|
(item) => ({
|
||||||
value: CipherType.Card,
|
value: item.type,
|
||||||
label: this.i18nService.t("typeCard"),
|
label: this.i18nService.t(item.labelKey),
|
||||||
icon: "bwi-credit-card",
|
icon: item.icon,
|
||||||
},
|
}),
|
||||||
{
|
);
|
||||||
value: CipherType.Identity,
|
}),
|
||||||
label: this.i18nService.t("typeIdentity"),
|
);
|
||||||
icon: "bwi-id-card",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
value: CipherType.SecureNote,
|
|
||||||
label: this.i18nService.t("note"),
|
|
||||||
icon: "bwi-sticky-note",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
value: CipherType.SshKey,
|
|
||||||
label: this.i18nService.t("typeSshKey"),
|
|
||||||
icon: "bwi-key",
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
/** Resets `filterForm` to the original state */
|
/** Resets `filterForm` to the original state */
|
||||||
resetFilterForm(): void {
|
resetFilterForm(): void {
|
||||||
|
|||||||
24
libs/common/src/vault/types/cipher-menu-items.ts
Normal file
24
libs/common/src/vault/types/cipher-menu-items.ts
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
import { CipherType } from "../enums";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents a menu item for creating a new cipher of a specific type
|
||||||
|
*/
|
||||||
|
export type CipherMenuItem = {
|
||||||
|
/** The cipher type this menu item represents */
|
||||||
|
type: CipherType;
|
||||||
|
/** The icon class name (e.g., "bwi-globe") */
|
||||||
|
icon: string;
|
||||||
|
/** The i18n key for the label text */
|
||||||
|
labelKey: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* All available cipher menu items with their associated icons and labels
|
||||||
|
*/
|
||||||
|
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.SshKey, icon: "bwi-key", labelKey: "typeSshKey" },
|
||||||
|
] as const) satisfies readonly CipherMenuItem[];
|
||||||
Reference in New Issue
Block a user