1
0
mirror of https://github.com/bitwarden/browser synced 2025-12-12 14:23:32 +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:
Jordan Aasen
2025-06-18 12:13:38 -07:00
committed by GitHub
parent 8d4fc91590
commit 5fa153e743
29 changed files with 166 additions and 171 deletions

View File

@@ -1922,6 +1922,9 @@
"typeSshKey": {
"message": "SSH key"
},
"typeNote": {
"message": "Note"
},
"newItemHeader": {
"message": "New $TYPE$",
"placeholders": {

View File

@@ -45,6 +45,7 @@ import { MessagingService } from "@bitwarden/common/platform/abstractions/messag
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
import { VaultSettingsService } from "@bitwarden/common/vault/abstractions/vault-settings/vault-settings.service";
import { CipherType } from "@bitwarden/common/vault/enums";
import { RestrictedItemTypesService } from "@bitwarden/common/vault/services/restricted-item-types.service";
import {
CardComponent,
CheckboxModule,
@@ -58,7 +59,6 @@ import {
SelectModule,
TypographyModule,
} from "@bitwarden/components";
import { RestrictedItemTypesService } from "@bitwarden/vault";
import { AutofillBrowserSettingsService } from "../../../autofill/services/autofill-browser-settings.service";
import { BrowserApi } from "../../../platform/browser/browser-api";

View File

@@ -12,8 +12,11 @@ import { Utils } from "@bitwarden/common/platform/misc/utils";
import { FolderApiServiceAbstraction } from "@bitwarden/common/vault/abstractions/folder/folder-api.service.abstraction";
import { FolderService } from "@bitwarden/common/vault/abstractions/folder/folder.service.abstraction";
import { CipherType } from "@bitwarden/common/vault/enums";
import {
RestrictedCipherType,
RestrictedItemTypesService,
} from "@bitwarden/common/vault/services/restricted-item-types.service";
import { ButtonModule, DialogService, MenuModule, NoItemsModule } from "@bitwarden/components";
import { RestrictedCipherType, RestrictedItemTypesService } from "@bitwarden/vault";
import { BrowserApi } from "../../../../../platform/browser/browser-api";
import BrowserPopupUtils from "../../../../../platform/popup/browser-popup-utils";

View File

@@ -8,9 +8,10 @@ import { map, Observable } from "rxjs";
import { JslibModule } from "@bitwarden/angular/jslib.module";
import { CollectionId, OrganizationId } from "@bitwarden/common/types/guid";
import { CipherType } from "@bitwarden/common/vault/enums";
import { RestrictedItemTypesService } from "@bitwarden/common/vault/services/restricted-item-types.service";
import { CipherMenuItem, CIPHER_MENU_ITEMS } from "@bitwarden/common/vault/types/cipher-menu-items";
import { ButtonModule, DialogService, MenuModule, NoItemsModule } from "@bitwarden/components";
import { AddEditFolderDialogComponent, RestrictedItemTypesService } from "@bitwarden/vault";
import { AddEditFolderDialogComponent } from "@bitwarden/vault";
import { BrowserApi } from "../../../../../platform/browser/browser-api";
import BrowserPopupUtils from "../../../../../platform/popup/browser-popup-utils";

View File

@@ -20,7 +20,10 @@ import { FolderService } from "@bitwarden/common/vault/abstractions/folder/folde
import { CipherType } from "@bitwarden/common/vault/enums";
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
import { FolderView } from "@bitwarden/common/vault/models/view/folder.view";
import { RestrictedCipherType, RestrictedItemTypesService } from "@bitwarden/vault";
import {
RestrictedCipherType,
RestrictedItemTypesService,
} from "@bitwarden/common/vault/services/restricted-item-types.service";
import {
CachedFilterState,

View File

@@ -39,9 +39,12 @@ import { ITreeNodeObject, TreeNode } from "@bitwarden/common/vault/models/domain
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
import { FolderView } from "@bitwarden/common/vault/models/view/folder.view";
import { ServiceUtils } from "@bitwarden/common/vault/service-utils";
import {
isCipherViewRestricted,
RestrictedItemTypesService,
} from "@bitwarden/common/vault/services/restricted-item-types.service";
import { CIPHER_MENU_ITEMS } from "@bitwarden/common/vault/types/cipher-menu-items";
import { ChipSelectOption } from "@bitwarden/components";
import { RestrictedItemTypesService } from "@bitwarden/vault";
const FILTER_VISIBILITY_KEY = new KeyDefinition<boolean>(VAULT_SETTINGS_DISK, "filterVisibility", {
deserializer: (obj) => obj,
@@ -227,19 +230,9 @@ export class VaultPopupListFiltersService {
}
// 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) {
if (isCipherViewRestricted(cipher, restrictions)) {
return false;
}
}
if (filters.cipherType !== null && cipher.type !== filters.cipherType) {
return false;

View File

@@ -23,6 +23,9 @@
"typeIdentity": {
"message": "Identity"
},
"typeNote": {
"message": "Note"
},
"typeSecureNote": {
"message": "Secure note"
},

View File

@@ -12,7 +12,9 @@
<div class="box-content-row" *ngIf="!editMode" appBoxRow>
<label for="type">{{ "type" | i18n }}</label>
<select id="type" name="Type" [(ngModel)]="cipher.type" (change)="typeChange()">
<option *ngFor="let o of typeOptions" [ngValue]="o.value">{{ o.name }}</option>
<option *ngFor="let item of menuItems$ | async" [ngValue]="item.type">
{{ item.labelKey | i18n }}
</option>
</select>
</div>
<div class="box-content-row" appBoxRow>

View File

@@ -3,6 +3,7 @@
import { DatePipe } from "@angular/common";
import { Component, NgZone, OnChanges, OnDestroy, OnInit, ViewChild } from "@angular/core";
import { NgForm } from "@angular/forms";
import { map, shareReplay } from "rxjs";
import { CollectionService } from "@bitwarden/admin-console/common";
import { AddEditComponent as BaseAddEditComponent } from "@bitwarden/angular/vault/components/add-edit.component";
@@ -22,6 +23,8 @@ import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.servi
import { FolderService } from "@bitwarden/common/vault/abstractions/folder/folder.service.abstraction";
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
import { CipherAuthorizationService } from "@bitwarden/common/vault/services/cipher-authorization.service";
import { RestrictedItemTypesService } from "@bitwarden/common/vault/services/restricted-item-types.service";
import { CIPHER_MENU_ITEMS } from "@bitwarden/common/vault/types/cipher-menu-items";
import { DialogService, ToastService } from "@bitwarden/components";
import { PasswordRepromptService, SshImportPromptService } from "@bitwarden/vault";
@@ -35,6 +38,18 @@ const BroadcasterSubscriptionId = "AddEditComponent";
export class AddEditComponent extends BaseAddEditComponent implements OnInit, OnChanges, OnDestroy {
@ViewChild("form")
private form: NgForm;
menuItems$ = this.restrictedItemTypesService.restricted$.pipe(
map((restrictedItemTypes) =>
// Filter out restricted item types from the default CIPHER_MENU_ITEMS array
CIPHER_MENU_ITEMS.filter(
(typeOption) =>
!restrictedItemTypes.some(
(restrictedType) => restrictedType.cipherType === typeOption.type,
),
),
),
shareReplay({ bufferSize: 1, refCount: true }),
);
constructor(
cipherService: CipherService,
@@ -59,6 +74,7 @@ export class AddEditComponent extends BaseAddEditComponent implements OnInit, On
cipherAuthorizationService: CipherAuthorizationService,
sdkService: SdkService,
sshImportPromptService: SshImportPromptService,
protected restrictedItemTypesService: RestrictedItemTypesService,
) {
super(
cipherService,

View File

@@ -20,78 +20,20 @@
</h2>
</div>
<ul id="type-filters" *ngIf="!isCollapsed" class="filter-options">
<li
class="filter-option"
[ngClass]="{ active: activeFilter.cipherType === cipherTypeEnum.Login }"
>
@for (typeFilter of typeFilters$ | async; track typeFilter) {
<li class="filter-option" [ngClass]="{ active: activeFilter.cipherType === typeFilter.type }">
<span class="filter-buttons">
<button
type="button"
class="filter-button"
(click)="applyFilter(cipherTypeEnum.Login)"
[attr.aria-pressed]="activeFilter.cipherType === cipherTypeEnum.Login"
(click)="applyFilter(typeFilter.type)"
[attr.aria-pressed]="activeFilter.cipherType === typeFilter.type"
>
<i class="bwi bwi-fw bwi-globe" aria-hidden="true"></i>&nbsp;{{ "typeLogin" | i18n }}
</button>
</span>
</li>
<li class="filter-option" [ngClass]="{ active: activeFilter.cipherType === cipherTypeEnum.Card }">
<span class="filter-buttons">
<button
type="button"
class="filter-button"
(click)="applyFilter(cipherTypeEnum.Card)"
[attr.aria-pressed]="activeFilter.cipherType === cipherTypeEnum.Card"
>
<i class="bwi bwi-fw bwi-credit-card" aria-hidden="true"></i>&nbsp;{{ "typeCard" | i18n }}
</button>
</span>
</li>
<li
class="filter-option"
[ngClass]="{ active: activeFilter.cipherType === cipherTypeEnum.Identity }"
>
<span class="filter-buttons">
<button
type="button"
class="filter-button"
(click)="applyFilter(cipherTypeEnum.Identity)"
[attr.aria-pressed]="activeFilter.cipherType === cipherTypeEnum.Identity"
>
<i class="bwi bwi-fw bwi-id-card" aria-hidden="true"></i>&nbsp;{{ "typeIdentity" | i18n }}
</button>
</span>
</li>
<li
class="filter-option"
[ngClass]="{ active: activeFilter.cipherType === cipherTypeEnum.SecureNote }"
>
<span class="filter-buttons">
<button
type="button"
class="filter-button"
(click)="applyFilter(cipherTypeEnum.SecureNote)"
[attr.aria-pressed]="activeFilter.cipherType === cipherTypeEnum.SecureNote"
>
<i class="bwi bwi-fw bwi-sticky-note" aria-hidden="true"></i>&nbsp;{{
"typeSecureNote" | i18n
<i class="bwi bwi-fw {{ typeFilter.icon }}" aria-hidden="true"></i>&nbsp;{{
typeFilter.labelKey | i18n
}}
</button>
</span>
</li>
<li
class="filter-option"
[ngClass]="{ active: activeFilter.cipherType === cipherTypeEnum.SshKey }"
>
<span class="filter-buttons">
<button
type="button"
class="filter-button"
(click)="applyFilter(cipherTypeEnum.SshKey)"
[attr.aria-pressed]="activeFilter.cipherType === cipherTypeEnum.SshKey"
>
<i class="bwi bwi-fw bwi-key" aria-hidden="true"></i>&nbsp;{{ "typeSshKey" | i18n }}
</button>
</span>
</li>
}
</ul>

View File

@@ -1,6 +1,9 @@
import { Component } from "@angular/core";
import { map, shareReplay } from "rxjs";
import { TypeFilterComponent as BaseTypeFilterComponent } from "@bitwarden/angular/vault/vault-filter/components/type-filter.component";
import { RestrictedItemTypesService } from "@bitwarden/common/vault/services/restricted-item-types.service";
import { CIPHER_MENU_ITEMS } from "@bitwarden/common/vault/types/cipher-menu-items";
@Component({
selector: "app-type-filter",
@@ -8,7 +11,22 @@ import { TypeFilterComponent as BaseTypeFilterComponent } from "@bitwarden/angul
standalone: false,
})
export class TypeFilterComponent extends BaseTypeFilterComponent {
constructor() {
protected typeFilters$ = this.restrictedItemTypesService.restricted$.pipe(
map((restrictedItemTypes) =>
// Filter out restricted item types from the typeFilters array
CIPHER_MENU_ITEMS.filter(
(typeFilter) =>
!restrictedItemTypes.some(
(restrictedType) =>
restrictedType.allowViewOrgIds.length === 0 &&
restrictedType.cipherType === typeFilter.type,
),
),
),
shareReplay({ bufferSize: 1, refCount: true }),
);
constructor(private restrictedItemTypesService: RestrictedItemTypesService) {
super();
}
}

View File

@@ -72,25 +72,11 @@
<i class="bwi bwi-plus bwi-lg" aria-hidden="true"></i>
</button>
<bit-menu #addCipherMenu>
<button type="button" bitMenuItem (click)="addCipher(CipherType.Login)">
<i class="bwi bwi-globe tw-mr-1" aria-hidden="true"></i>
{{ "typeLogin" | i18n }}
</button>
<button type="button" bitMenuItem (click)="addCipher(CipherType.Card)">
<i class="bwi bwi-credit-card tw-mr-1" aria-hidden="true"></i>
{{ "typeCard" | i18n }}
</button>
<button type="button" bitMenuItem (click)="addCipher(CipherType.Identity)">
<i class="bwi bwi-id-card tw-mr-1" aria-hidden="true"></i>
{{ "typeIdentity" | i18n }}
</button>
<button type="button" bitMenuItem (click)="addCipher(CipherType.SecureNote)">
<i class="bwi bwi-sticky-note tw-mr-1" aria-hidden="true"></i>
{{ "typeSecureNote" | i18n }}
</button>
<button type="button" bitMenuItem (click)="addCipher(CipherType.SshKey)">
<i class="bwi bwi-key tw-mr-1" aria-hidden="true"></i>
{{ "typeSshKey" | i18n }}
@for (itemTypes of itemTypes$ | async; track itemTypes.type) {
<button type="button" bitMenuItem (click)="addCipher(itemTypes.type)">
<i class="bwi {{ itemTypes.icon }} tw-mr-1" aria-hidden="true"></i>
{{ itemTypes.labelKey | i18n }}
</button>
}
</bit-menu>
</ng-template>

View File

@@ -10,6 +10,7 @@ import { SearchService } from "@bitwarden/common/abstractions/search.service";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
import { RestrictedItemTypesService } from "@bitwarden/common/vault/services/restricted-item-types.service";
import { MenuModule } from "@bitwarden/components";
import { SearchBarService } from "../../../app/layout/search/search-bar.service";
@@ -25,8 +26,9 @@ export class VaultItemsV2Component extends BaseVaultItemsComponent {
private readonly searchBarService: SearchBarService,
cipherService: CipherService,
accountService: AccountService,
restrictedItemTypesService: RestrictedItemTypesService,
) {
super(searchService, cipherService, accountService);
super(searchService, cipherService, accountService, restrictedItemTypesService);
this.searchBarService.searchText$
.pipe(distinctUntilChanged(), takeUntilDestroyed())

View File

@@ -8,6 +8,7 @@ import { SearchService } from "@bitwarden/common/abstractions/search.service";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
import { RestrictedItemTypesService } from "@bitwarden/common/vault/services/restricted-item-types.service";
import { SearchBarService } from "../../../app/layout/search/search-bar.service";
@@ -22,8 +23,9 @@ export class VaultItemsComponent extends BaseVaultItemsComponent {
searchBarService: SearchBarService,
cipherService: CipherService,
accountService: AccountService,
protected restrictedItemTypesService: RestrictedItemTypesService,
) {
super(searchService, cipherService, accountService);
super(searchService, cipherService, accountService, restrictedItemTypesService);
// eslint-disable-next-line rxjs-angular/prefer-takeuntil
searchBarService.searchText$.pipe(distinctUntilChanged()).subscribe((searchText) => {

View File

@@ -11,8 +11,8 @@ import { ConfigService } from "@bitwarden/common/platform/abstractions/config/co
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
import { TreeNode } from "@bitwarden/common/vault/models/domain/tree-node";
import { RestrictedItemTypesService } from "@bitwarden/common/vault/services/restricted-item-types.service";
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 { VaultFilterService } from "../../../../vault/individual-vault/vault-filter/services/abstractions/vault-filter.service";

View File

@@ -35,8 +35,8 @@ 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/common/vault/services/restricted-item-types.service";
import { LayoutComponent } from "@bitwarden/components";
import { RestrictedItemTypesService } from "@bitwarden/vault";
import { GroupView } from "../../../admin-console/organizations/core";
import { PreloadedEnglishI18nModule } from "../../../core/tests";

View File

@@ -22,8 +22,8 @@ import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.servic
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
import { CipherType } from "@bitwarden/common/vault/enums";
import { TreeNode } from "@bitwarden/common/vault/models/domain/tree-node";
import { RestrictedItemTypesService } from "@bitwarden/common/vault/services/restricted-item-types.service";
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";

View File

@@ -3,7 +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 { RestrictedCipherType } from "@bitwarden/common/vault/services/restricted-item-types.service";
import { createFilterFunction } from "./filter-function";
import { All } from "./routed-vault-filter.model";

View File

@@ -1,7 +1,10 @@
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 {
isCipherViewRestricted,
RestrictedCipherType,
} from "@bitwarden/common/vault/services/restricted-item-types.service";
import { All, RoutedVaultFilterModel } from "./routed-vault-filter.model";
@@ -83,25 +86,10 @@ export function createFilterFunction(
) {
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),
)
) {
if (restrictedTypes && isCipherViewRestricted(cipher, restrictedTypes)) {
return false;
}
}
return true;
};
}

View File

@@ -18,13 +18,13 @@ import { ConfigService } from "@bitwarden/common/platform/abstractions/config/co
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { CipherType } from "@bitwarden/common/vault/enums";
import { TreeNode } from "@bitwarden/common/vault/models/domain/tree-node";
import { RestrictedItemTypesService } from "@bitwarden/common/vault/services/restricted-item-types.service";
import {
BreadcrumbsModule,
DialogService,
MenuModule,
SimpleDialogOptions,
} from "@bitwarden/components";
import { RestrictedItemTypesService } from "@bitwarden/vault";
import { CollectionDialogTabType } from "../../../admin-console/organizations/shared/components/collection-dialog";
import { HeaderModule } from "../../../layouts/header/header.module";

View File

@@ -66,6 +66,7 @@ import { CipherRepromptType } from "@bitwarden/common/vault/enums/cipher-repromp
import { TreeNode } from "@bitwarden/common/vault/models/domain/tree-node";
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
import { ServiceUtils } from "@bitwarden/common/vault/service-utils";
import { RestrictedItemTypesService } from "@bitwarden/common/vault/services/restricted-item-types.service";
import { filterOutNullish } from "@bitwarden/common/vault/utils/observable-utilities";
import { DialogRef, DialogService, Icons, ToastService } from "@bitwarden/components";
import {
@@ -79,7 +80,6 @@ import {
DecryptionFailureDialogComponent,
DefaultCipherFormConfigService,
PasswordRepromptService,
RestrictedItemTypesService,
} from "@bitwarden/vault";
import {

View File

@@ -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,

View File

@@ -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() {

View File

@@ -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,
);
}),

View File

@@ -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 {

View File

@@ -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([

View File

@@ -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),
);
}

View File

@@ -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[];

View File

@@ -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";