1
0
mirror of https://github.com/bitwarden/browser synced 2025-12-12 06:13:38 +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": { "typeSshKey": {
"message": "SSH key" "message": "SSH key"
}, },
"typeNote": {
"message": "Note"
},
"newItemHeader": { "newItemHeader": {
"message": "New $TYPE$", "message": "New $TYPE$",
"placeholders": { "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 { 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 { CipherType } from "@bitwarden/common/vault/enums";
import { RestrictedItemTypesService } from "@bitwarden/common/vault/services/restricted-item-types.service";
import { import {
CardComponent, CardComponent,
CheckboxModule, CheckboxModule,
@@ -58,7 +59,6 @@ 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";

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 { FolderApiServiceAbstraction } from "@bitwarden/common/vault/abstractions/folder/folder-api.service.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 {
RestrictedCipherType,
RestrictedItemTypesService,
} from "@bitwarden/common/vault/services/restricted-item-types.service";
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";

View File

@@ -8,9 +8,10 @@ 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 { RestrictedItemTypesService } from "@bitwarden/common/vault/services/restricted-item-types.service";
import { CipherMenuItem, CIPHER_MENU_ITEMS } from "@bitwarden/common/vault/types/cipher-menu-items"; 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, RestrictedItemTypesService } from "@bitwarden/vault"; import { AddEditFolderDialogComponent } 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";

View File

@@ -20,7 +20,10 @@ 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 {
RestrictedCipherType,
RestrictedItemTypesService,
} from "@bitwarden/common/vault/services/restricted-item-types.service";
import { import {
CachedFilterState, 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 { 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 {
isCipherViewRestricted,
RestrictedItemTypesService,
} from "@bitwarden/common/vault/services/restricted-item-types.service";
import { CIPHER_MENU_ITEMS } from "@bitwarden/common/vault/types/cipher-menu-items"; 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,
@@ -227,18 +230,8 @@ export class VaultPopupListFiltersService {
} }
// Check if cipher type is restricted (with organization exemptions) // Check if cipher type is restricted (with organization exemptions)
if (restrictions && restrictions.length > 0) { if (isCipherViewRestricted(cipher, restrictions)) {
const isRestricted = restrictions.some( return false;
(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) { if (filters.cipherType !== null && cipher.type !== filters.cipherType) {

View File

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

View File

@@ -12,7 +12,9 @@
<div class="box-content-row" *ngIf="!editMode" appBoxRow> <div class="box-content-row" *ngIf="!editMode" appBoxRow>
<label for="type">{{ "type" | i18n }}</label> <label for="type">{{ "type" | i18n }}</label>
<select id="type" name="Type" [(ngModel)]="cipher.type" (change)="typeChange()"> <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> </select>
</div> </div>
<div class="box-content-row" appBoxRow> <div class="box-content-row" appBoxRow>

View File

@@ -3,6 +3,7 @@
import { DatePipe } from "@angular/common"; import { DatePipe } from "@angular/common";
import { Component, NgZone, OnChanges, OnDestroy, OnInit, ViewChild } from "@angular/core"; import { Component, NgZone, OnChanges, OnDestroy, OnInit, ViewChild } from "@angular/core";
import { NgForm } from "@angular/forms"; import { NgForm } from "@angular/forms";
import { map, shareReplay } from "rxjs";
import { CollectionService } from "@bitwarden/admin-console/common"; import { CollectionService } from "@bitwarden/admin-console/common";
import { AddEditComponent as BaseAddEditComponent } from "@bitwarden/angular/vault/components/add-edit.component"; 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 { FolderService } from "@bitwarden/common/vault/abstractions/folder/folder.service.abstraction";
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; import { CipherView } from "@bitwarden/common/vault/models/view/cipher.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/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 { DialogService, ToastService } from "@bitwarden/components";
import { PasswordRepromptService, SshImportPromptService } from "@bitwarden/vault"; import { PasswordRepromptService, SshImportPromptService } from "@bitwarden/vault";
@@ -35,6 +38,18 @@ const BroadcasterSubscriptionId = "AddEditComponent";
export class AddEditComponent extends BaseAddEditComponent implements OnInit, OnChanges, OnDestroy { export class AddEditComponent extends BaseAddEditComponent implements OnInit, OnChanges, OnDestroy {
@ViewChild("form") @ViewChild("form")
private form: NgForm; 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( constructor(
cipherService: CipherService, cipherService: CipherService,
@@ -59,6 +74,7 @@ export class AddEditComponent extends BaseAddEditComponent implements OnInit, On
cipherAuthorizationService: CipherAuthorizationService, cipherAuthorizationService: CipherAuthorizationService,
sdkService: SdkService, sdkService: SdkService,
sshImportPromptService: SshImportPromptService, sshImportPromptService: SshImportPromptService,
protected restrictedItemTypesService: RestrictedItemTypesService,
) { ) {
super( super(
cipherService, cipherService,

View File

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

View File

@@ -1,6 +1,9 @@
import { Component } from "@angular/core"; 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 { 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({ @Component({
selector: "app-type-filter", selector: "app-type-filter",
@@ -8,7 +11,22 @@ import { TypeFilterComponent as BaseTypeFilterComponent } from "@bitwarden/angul
standalone: false, standalone: false,
}) })
export class TypeFilterComponent extends BaseTypeFilterComponent { 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(); super();
} }
} }

View File

@@ -72,25 +72,11 @@
<i class="bwi bwi-plus bwi-lg" aria-hidden="true"></i> <i class="bwi bwi-plus bwi-lg" aria-hidden="true"></i>
</button> </button>
<bit-menu #addCipherMenu> <bit-menu #addCipherMenu>
<button type="button" bitMenuItem (click)="addCipher(CipherType.Login)"> @for (itemTypes of itemTypes$ | async; track itemTypes.type) {
<i class="bwi bwi-globe tw-mr-1" aria-hidden="true"></i> <button type="button" bitMenuItem (click)="addCipher(itemTypes.type)">
{{ "typeLogin" | i18n }} <i class="bwi {{ itemTypes.icon }} tw-mr-1" aria-hidden="true"></i>
</button> {{ itemTypes.labelKey | i18n }}
<button type="button" bitMenuItem (click)="addCipher(CipherType.Card)"> </button>
<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 }}
</button>
</bit-menu> </bit-menu>
</ng-template> </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 { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; 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 { MenuModule } from "@bitwarden/components";
import { SearchBarService } from "../../../app/layout/search/search-bar.service"; import { SearchBarService } from "../../../app/layout/search/search-bar.service";
@@ -25,8 +26,9 @@ export class VaultItemsV2Component extends BaseVaultItemsComponent {
private readonly searchBarService: SearchBarService, private readonly searchBarService: SearchBarService,
cipherService: CipherService, cipherService: CipherService,
accountService: AccountService, accountService: AccountService,
restrictedItemTypesService: RestrictedItemTypesService,
) { ) {
super(searchService, cipherService, accountService); super(searchService, cipherService, accountService, restrictedItemTypesService);
this.searchBarService.searchText$ this.searchBarService.searchText$
.pipe(distinctUntilChanged(), takeUntilDestroyed()) .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 { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; 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"; import { SearchBarService } from "../../../app/layout/search/search-bar.service";
@@ -22,8 +23,9 @@ export class VaultItemsComponent extends BaseVaultItemsComponent {
searchBarService: SearchBarService, searchBarService: SearchBarService,
cipherService: CipherService, cipherService: CipherService,
accountService: AccountService, accountService: AccountService,
protected restrictedItemTypesService: RestrictedItemTypesService,
) { ) {
super(searchService, cipherService, accountService); super(searchService, cipherService, accountService, restrictedItemTypesService);
// eslint-disable-next-line rxjs-angular/prefer-takeuntil // eslint-disable-next-line rxjs-angular/prefer-takeuntil
searchBarService.searchText$.pipe(distinctUntilChanged()).subscribe((searchText) => { 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 { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
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 { RestrictedItemTypesService } from "@bitwarden/common/vault/services/restricted-item-types.service";
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";

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 { 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/common/vault/services/restricted-item-types.service";
import { LayoutComponent } from "@bitwarden/components"; import { LayoutComponent } from "@bitwarden/components";
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";

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 { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
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 { RestrictedItemTypesService } from "@bitwarden/common/vault/services/restricted-item-types.service";
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";

View File

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

View File

@@ -1,7 +1,10 @@
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 {
isCipherViewRestricted,
RestrictedCipherType,
} from "@bitwarden/common/vault/services/restricted-item-types.service";
import { All, RoutedVaultFilterModel } from "./routed-vault-filter.model"; import { All, RoutedVaultFilterModel } from "./routed-vault-filter.model";
@@ -83,24 +86,9 @@ export function createFilterFunction(
) { ) {
return false; return false;
} }
// Restricted types // Restricted types
if (restrictedTypes && restrictedTypes.length > 0) { if (restrictedTypes && isCipherViewRestricted(cipher, restrictedTypes)) {
// Filter the cipher if that type is restricted unless return false;
// - 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;
}; };

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 { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
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 { RestrictedItemTypesService } from "@bitwarden/common/vault/services/restricted-item-types.service";
import { import {
BreadcrumbsModule, BreadcrumbsModule,
DialogService, DialogService,
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";

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 { TreeNode } from "@bitwarden/common/vault/models/domain/tree-node";
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
import { ServiceUtils } from "@bitwarden/common/vault/service-utils"; 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 { filterOutNullish } from "@bitwarden/common/vault/utils/observable-utilities";
import { DialogRef, DialogService, Icons, ToastService } from "@bitwarden/components"; import { DialogRef, DialogService, Icons, ToastService } from "@bitwarden/components";
import { import {
@@ -79,7 +80,6 @@ import {
DecryptionFailureDialogComponent, DecryptionFailureDialogComponent,
DefaultCipherFormConfigService, DefaultCipherFormConfigService,
PasswordRepromptService, PasswordRepromptService,
RestrictedItemTypesService,
} from "@bitwarden/vault"; } from "@bitwarden/vault";
import { 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 { CipherFileUploadService } from "@bitwarden/common/vault/services/file-upload/cipher-file-upload.service";
import { FolderApiService } from "@bitwarden/common/vault/services/folder/folder-api.service"; import { FolderApiService } from "@bitwarden/common/vault/services/folder/folder-api.service";
import { FolderService } from "@bitwarden/common/vault/services/folder/folder.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 { TotpService } from "@bitwarden/common/vault/services/totp.service";
import { VaultSettingsService } from "@bitwarden/common/vault/services/vault-settings/vault-settings.service"; import { VaultSettingsService } from "@bitwarden/common/vault/services/vault-settings/vault-settings.service";
import { DefaultTaskService, TaskService } from "@bitwarden/common/vault/tasks"; import { DefaultTaskService, TaskService } from "@bitwarden/common/vault/tasks";
@@ -680,6 +681,11 @@ const safeProviders: SafeProvider[] = [
KdfConfigService, KdfConfigService,
], ],
}), }),
safeProvider({
provide: RestrictedItemTypesService,
useClass: RestrictedItemTypesService,
deps: [ConfigService, AccountService, OrganizationServiceAbstraction, PolicyServiceAbstraction],
}),
safeProvider({ safeProvider({
provide: PasswordStrengthServiceAbstraction, provide: PasswordStrengthServiceAbstraction,
useClass: PasswordStrengthService, useClass: PasswordStrengthService,

View File

@@ -84,7 +84,6 @@ export class AddEditComponent implements OnInit, OnDestroy {
showCardNumber = false; showCardNumber = false;
showCardCode = false; showCardCode = false;
cipherType = CipherType; cipherType = CipherType;
typeOptions: any[];
cardBrandOptions: any[]; cardBrandOptions: any[];
cardExpMonthOptions: any[]; cardExpMonthOptions: any[];
identityTitleOptions: any[]; identityTitleOptions: any[];
@@ -139,13 +138,6 @@ export class AddEditComponent implements OnInit, OnDestroy {
protected sdkService: SdkService, protected sdkService: SdkService,
private sshImportPromptService: SshImportPromptService, 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 = [ this.cardBrandOptions = [
{ name: "-- " + i18nService.t("select") + " --", value: null }, { name: "-- " + i18nService.t("select") + " --", value: null },
{ name: "Visa", value: "Visa" }, { name: "Visa", value: "Visa" },
@@ -215,8 +207,6 @@ export class AddEditComponent implements OnInit, OnDestroy {
this.writeableCollections = await this.loadCollections(); this.writeableCollections = await this.loadCollections();
this.canUseReprompt = await this.passwordRepromptService.enabled(); this.canUseReprompt = await this.passwordRepromptService.enabled();
this.typeOptions.push({ name: this.i18nService.t("typeSshKey"), value: CipherType.SshKey });
} }
ngOnDestroy() { ngOnDestroy() {

View File

@@ -8,7 +8,9 @@ import {
combineLatest, combineLatest,
filter, filter,
from, from,
map,
of, of,
shareReplay,
switchMap, switchMap,
takeUntil, takeUntil,
} from "rxjs"; } 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 { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
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 {
isCipherViewRestricted,
RestrictedItemTypesService,
} from "@bitwarden/common/vault/services/restricted-item-types.service";
import { CIPHER_MENU_ITEMS } from "@bitwarden/common/vault/types/cipher-menu-items";
@Directive() @Directive()
export class VaultItemsComponent implements OnInit, OnDestroy { export class VaultItemsComponent implements OnInit, OnDestroy {
@@ -35,6 +42,19 @@ export class VaultItemsComponent implements OnInit, OnDestroy {
organization: Organization; organization: Organization;
CipherType = CipherType; 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; protected searchPending = false;
/** Construct filters as an observable so it can be appended to the cipher stream. */ /** 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 searchService: SearchService,
protected cipherService: CipherService, protected cipherService: CipherService,
protected accountService: AccountService, protected accountService: AccountService,
protected restrictedItemTypesService: RestrictedItemTypesService,
) { ) {
this.subscribeToCiphers(); this.subscribeToCiphers();
} }
@@ -143,18 +164,22 @@ export class VaultItemsComponent implements OnInit, OnDestroy {
this._searchText$, this._searchText$,
this._filter$, this._filter$,
of(userId), of(userId),
this.restrictedItemTypesService.restricted$,
]), ]),
), ),
switchMap(([indexedCiphers, failedCiphers, searchText, filter, userId]) => { switchMap(([indexedCiphers, failedCiphers, searchText, filter, userId, restricted]) => {
let allCiphers = indexedCiphers ?? []; let allCiphers = indexedCiphers ?? [];
const _failedCiphers = failedCiphers ?? []; const _failedCiphers = failedCiphers ?? [];
allCiphers = [..._failedCiphers, ...allCiphers]; allCiphers = [..._failedCiphers, ...allCiphers];
const restrictedTypeFilter = (cipher: CipherView) =>
isCipherViewRestricted(cipher, restricted);
return this.searchService.searchCiphers( return this.searchService.searchCiphers(
userId, userId,
searchText, searchText,
[filter, this.deletedFilter], [filter, this.deletedFilter, restrictedTypeFilter],
allCiphers, allCiphers,
); );
}), }),

View File

@@ -1,5 +1,6 @@
// 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 { ITreeNodeObject, TreeNode } from "./models/domain/tree-node"; import { ITreeNodeObject, TreeNode } from "./models/domain/tree-node";
export class ServiceUtils { export class ServiceUtils {

View File

@@ -1,4 +1,3 @@
import { TestBed } from "@angular/core/testing";
import { mock, MockProxy } from "jest-mock-extended"; import { mock, MockProxy } from "jest-mock-extended";
import { firstValueFrom, of } from "rxjs"; import { firstValueFrom, of } from "rxjs";
@@ -49,19 +48,16 @@ describe("RestrictedItemTypesService", () => {
fakeAccount = { id: Utils.newGuid() as UserId } as Account; fakeAccount = { id: Utils.newGuid() as UserId } as Account;
accountService.activeAccount$ = of(fakeAccount); 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)); configService.getFeatureFlag$.mockReturnValue(of(true));
organizationService.organizations$.mockReturnValue(of([org1, org2])); organizationService.organizations$.mockReturnValue(of([org1, org2]));
policyService.policiesByType$.mockReturnValue(of([])); 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 () => { 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 () => { it("returns empty allowViewOrgIds when all orgs restrict the same type", async () => {
configService.getFeatureFlag$.mockReturnValue(of(true));
organizationService.organizations$.mockReturnValue(of([org1, org2])); organizationService.organizations$.mockReturnValue(of([org1, org2]));
policyService.policiesByType$.mockReturnValue(of([policyOrg1, policyOrg2])); policyService.policiesByType$.mockReturnValue(of([policyOrg1, policyOrg2]));
@@ -117,7 +112,6 @@ describe("RestrictedItemTypesService", () => {
}); });
it("aggregates multiple types and computes allowViewOrgIds correctly", async () => { it("aggregates multiple types and computes allowViewOrgIds correctly", async () => {
configService.getFeatureFlag$.mockReturnValue(of(true));
organizationService.organizations$.mockReturnValue(of([org1, org2])); organizationService.organizations$.mockReturnValue(of([org1, org2]));
policyService.policiesByType$.mockReturnValue( policyService.policiesByType$.mockReturnValue(
of([ of([

View File

@@ -1,4 +1,3 @@
import { Injectable } from "@angular/core";
import { combineLatest, map, of, Observable } from "rxjs"; import { combineLatest, map, of, Observable } from "rxjs";
import { switchMap, distinctUntilChanged, shareReplay } from "rxjs/operators"; 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 { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
import { CipherType } from "@bitwarden/common/vault/enums"; import { CipherType } from "@bitwarden/common/vault/enums";
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
export type RestrictedCipherType = { export type RestrictedCipherType = {
cipherType: CipherType; cipherType: CipherType;
allowViewOrgIds: string[]; allowViewOrgIds: string[];
}; };
@Injectable({ providedIn: "root" })
export class RestrictedItemTypesService { export class RestrictedItemTypesService {
/** /**
* Emits an array of RestrictedCipherType objects: * Emits an array of RestrictedCipherType objects:
@@ -78,3 +77,25 @@ export class RestrictedItemTypesService {
private policyService: PolicyService, 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.Login, icon: "bwi-globe", labelKey: "typeLogin" },
{ type: CipherType.Card, icon: "bwi-credit-card", labelKey: "typeCard" }, { type: CipherType.Card, icon: "bwi-credit-card", labelKey: "typeCard" },
{ type: CipherType.Identity, icon: "bwi-id-card", labelKey: "typeIdentity" }, { 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" }, { type: CipherType.SshKey, icon: "bwi-key", labelKey: "typeSshKey" },
] as const) satisfies readonly CipherMenuItem[]; ] 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 { 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";