From aa2c5a0087fc728d5a45451d7def7798e35c2702 Mon Sep 17 00:00:00 2001 From: Jordan Aasen <166539328+jaasen-livefront@users.noreply.github.com> Date: Wed, 23 Jul 2025 08:40:59 -0700 Subject: [PATCH 1/9] [PM-22374] - [Vault] [Web] Hide vault header menu dropdown for My Items collection (#15742) * fix cloneCollection to include type * add newline --- .../organizations/collections/utils/collection-utils.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/apps/web/src/app/admin-console/organizations/collections/utils/collection-utils.ts b/apps/web/src/app/admin-console/organizations/collections/utils/collection-utils.ts index 95ae911bbf6..0697659c976 100644 --- a/apps/web/src/app/admin-console/organizations/collections/utils/collection-utils.ts +++ b/apps/web/src/app/admin-console/organizations/collections/utils/collection-utils.ts @@ -82,5 +82,7 @@ function cloneCollection( cloned.organizationId = collection.organizationId; cloned.readOnly = collection.readOnly; cloned.manage = collection.manage; + cloned.type = collection.type; + return cloned; } From d0082981a3d9541608293fe05ef0414f2ae34496 Mon Sep 17 00:00:00 2001 From: Jason Ng Date: Wed, 23 Jul 2025 12:04:31 -0400 Subject: [PATCH 2/9] [PM-23788] [PM-23793] Prevent Card Clone when Restricted (#15685) * add restricted policy check to vault items in web and browser --- .../item-more-options.component.ts | 23 +++++++++++++++---- .../vault-items/vault-items.component.ts | 14 +++++++++-- 2 files changed, 30 insertions(+), 7 deletions(-) diff --git a/apps/browser/src/vault/popup/components/vault-v2/item-more-options/item-more-options.component.ts b/apps/browser/src/vault/popup/components/vault-v2/item-more-options/item-more-options.component.ts index ce61e29e9ef..ce16ec2f3e0 100644 --- a/apps/browser/src/vault/popup/components/vault-v2/item-more-options/item-more-options.component.ts +++ b/apps/browser/src/vault/popup/components/vault-v2/item-more-options/item-more-options.component.ts @@ -3,8 +3,7 @@ import { CommonModule } from "@angular/common"; import { booleanAttribute, Component, Input } from "@angular/core"; import { Router, RouterModule } from "@angular/router"; -import { BehaviorSubject, combineLatest, firstValueFrom, map, switchMap } from "rxjs"; -import { filter } from "rxjs/operators"; +import { BehaviorSubject, combineLatest, filter, firstValueFrom, map, switchMap } from "rxjs"; import { CollectionService } from "@bitwarden/admin-console/common"; import { JslibModule } from "@bitwarden/angular/jslib.module"; @@ -15,6 +14,7 @@ import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.servic import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; import { CipherRepromptType, CipherType } from "@bitwarden/common/vault/enums"; import { CipherAuthorizationService } from "@bitwarden/common/vault/services/cipher-authorization.service"; +import { RestrictedItemTypesService } from "@bitwarden/common/vault/services/restricted-item-types.service"; import { CipherViewLike, CipherViewLikeUtils, @@ -70,9 +70,21 @@ export class ItemMoreOptionsComponent { * Observable that emits a boolean value indicating if the user is authorized to clone the cipher. * @protected */ - protected canClone$ = this._cipher$.pipe( - filter((c) => c != null), - switchMap((c) => this.cipherAuthorizationService.canCloneCipher$(c)), + protected canClone$ = combineLatest([ + this._cipher$, + this.restrictedItemTypesService.restricted$, + ]).pipe( + filter(([c]) => c != null), + switchMap(([c, restrictedTypes]) => { + // This will check for restrictions from org policies before allowing cloning. + const isItemRestricted = restrictedTypes.some( + (restrictType) => restrictType.cipherType === c.type, + ); + if (!isItemRestricted) { + return this.cipherAuthorizationService.canCloneCipher$(c); + } + return new BehaviorSubject(false); + }), ); /** Observable Boolean dependent on the current user having access to an organization and editable collections */ @@ -103,6 +115,7 @@ export class ItemMoreOptionsComponent { private organizationService: OrganizationService, private cipherAuthorizationService: CipherAuthorizationService, private collectionService: CollectionService, + private restrictedItemTypesService: RestrictedItemTypesService, ) {} get canEdit() { diff --git a/apps/web/src/app/vault/components/vault-items/vault-items.component.ts b/apps/web/src/app/vault/components/vault-items/vault-items.component.ts index 79ba9a6d2e1..ebee57878db 100644 --- a/apps/web/src/app/vault/components/vault-items/vault-items.component.ts +++ b/apps/web/src/app/vault/components/vault-items/vault-items.component.ts @@ -2,7 +2,7 @@ // @ts-strict-ignore import { SelectionModel } from "@angular/cdk/collections"; import { Component, EventEmitter, Input, Output } from "@angular/core"; -import { takeUntilDestroyed } from "@angular/core/rxjs-interop"; +import { toSignal, takeUntilDestroyed } from "@angular/core/rxjs-interop"; import { Observable, combineLatest, map, of, startWith, switchMap } from "rxjs"; import { CollectionView, Unassigned, CollectionAdminView } from "@bitwarden/admin-console/common"; @@ -64,6 +64,8 @@ export class VaultItemsComponent { @Input() addAccessToggle: boolean; @Input() activeCollection: CollectionView | undefined; + private restrictedPolicies = toSignal(this.restrictedItemTypesService.restricted$); + private _ciphers?: C[] = []; @Input() get ciphers(): C[] { return this._ciphers; @@ -94,7 +96,7 @@ export class VaultItemsComponent { constructor( protected cipherAuthorizationService: CipherAuthorizationService, - private restrictedItemTypesService: RestrictedItemTypesService, + protected restrictedItemTypesService: RestrictedItemTypesService, ) { this.canDeleteSelected$ = this.selection.changed.pipe( startWith(null), @@ -281,6 +283,14 @@ export class VaultItemsComponent { // TODO: PM-13944 Refactor to use cipherAuthorizationService.canClone$ instead protected canClone(vaultItem: VaultItem) { + // This will check for restrictions from org policies before allowing cloning. + const isItemRestricted = this.restrictedPolicies().some( + (rt) => rt.cipherType === vaultItem.cipher.type, + ); + if (isItemRestricted) { + return false; + } + if (vaultItem.cipher.organizationId == null) { return true; } From a74e95fbfe47ae37ee700f950116b823c9f4617d Mon Sep 17 00:00:00 2001 From: Ben Brooks <56796209+bensbits91@users.noreply.github.com> Date: Wed, 23 Jul 2025 09:17:47 -0700 Subject: [PATCH 3/9] [CL-601] Replace default reset button to enable it in more browsers (#14974) * bb/pm-19497/replace default reset button to enable it in more browsers * address feedback: add ngClass; improve accessibility * add signals for form hover and input focus; compute showResetButton * fix(style): [CL-601] Improve CSS per reviewer comments Signed-off-by: Ben Brooks * fix: [CL-601] add ngForm; remove standalone attributes Signed-off-by: Ben Brooks * fix: [CL-601] add translation strings Signed-off-by: Ben Brooks * fix: [CL-601] Use message key in aria label Signed-off-by: Ben Brooks * fix: [CL-601] Remove unnecessary aria-hidden attribute Signed-off-by: Ben Brooks * fix: [CL-601] Remove unecessary ngForm attributes Signed-off-by: Ben Brooks * fix: [CL-601] Add storybook description Signed-off-by: Ben Brooks * fix: [CL-601] Match main for recent signal input changs Signed-off-by: Ben Brooks --------- Signed-off-by: Ben Brooks --- apps/browser/src/_locales/en/messages.json | 3 ++ apps/desktop/src/locales/en/messages.json | 3 ++ apps/web/src/locales/en/messages.json | 3 ++ .../src/search/search.component.css | 18 ++++++------ .../src/search/search.component.html | 28 +++++++++++++++---- .../components/src/search/search.component.ts | 22 +++++++++++++-- libs/components/src/search/search.mdx | 3 +- 7 files changed, 62 insertions(+), 18 deletions(-) diff --git a/apps/browser/src/_locales/en/messages.json b/apps/browser/src/_locales/en/messages.json index 37d64c3416b..a1b41b44bfd 100644 --- a/apps/browser/src/_locales/en/messages.json +++ b/apps/browser/src/_locales/en/messages.json @@ -547,6 +547,9 @@ "searchVault": { "message": "Search vault" }, + "resetSearch": { + "message": "Reset search" + }, "edit": { "message": "Edit" }, diff --git a/apps/desktop/src/locales/en/messages.json b/apps/desktop/src/locales/en/messages.json index e5e6dcb2882..c269f7d32f8 100644 --- a/apps/desktop/src/locales/en/messages.json +++ b/apps/desktop/src/locales/en/messages.json @@ -41,6 +41,9 @@ "searchVault": { "message": "Search vault" }, + "resetSearch": { + "message": "Reset search" + }, "addItem": { "message": "Add item" }, diff --git a/apps/web/src/locales/en/messages.json b/apps/web/src/locales/en/messages.json index 5d7cbd7d479..b143d4da56a 100644 --- a/apps/web/src/locales/en/messages.json +++ b/apps/web/src/locales/en/messages.json @@ -629,6 +629,9 @@ "searchGroups": { "message": "Search groups" }, + "resetSearch": { + "message": "Reset search" + }, "allItems": { "message": "All items" }, diff --git a/libs/components/src/search/search.component.css b/libs/components/src/search/search.component.css index 35304438a88..6233de2a3ac 100644 --- a/libs/components/src/search/search.component.css +++ b/libs/components/src/search/search.component.css @@ -1,19 +1,17 @@ /** * Tailwind doesn't have a good way to style search-cancel-button. + * Hide the default reset button that only appears in some browsers. */ bit-search input[type="search"]::-webkit-search-cancel-button { -webkit-appearance: none; appearance: none; - height: 21px; - width: 21px; - margin: 0; - cursor: pointer; - background-repeat: no-repeat; - mask-image: url("./close-button.svg"); - -webkit-mask-image: url("./close-button.svg"); - background-color: rgba(var(--color-text-muted)); } -bit-search input[type="search"]::-webkit-search-cancel-button:hover { - background-color: rgba(var(--color-text-main)); +/** + * Style our custom reset button that works in all common browsers. + * Tailwind CSS does not natively support mask-image or -webkit-mask-image utilities (but can be extended if needed). + */ +.bw-reset-btn { + mask-image: url("./close-button.svg"); + -webkit-mask-image: url("./close-button.svg"); } diff --git a/libs/components/src/search/search.component.html b/libs/components/src/search/search.component.html index 803b61c6322..b1b92fb151a 100644 --- a/libs/components/src/search/search.component.html +++ b/libs/components/src/search/search.component.html @@ -1,9 +1,14 @@ - -
+
+ + diff --git a/libs/components/src/search/search.component.ts b/libs/components/src/search/search.component.ts index ef12e7eead6..c6c5f2757dd 100644 --- a/libs/components/src/search/search.component.ts +++ b/libs/components/src/search/search.component.ts @@ -1,6 +1,7 @@ // FIXME: Update this file to be type safe and remove this and next line // @ts-strict-ignore -import { Component, ElementRef, ViewChild, input, model } from "@angular/core"; +import { NgIf, NgClass } from "@angular/common"; +import { Component, ElementRef, ViewChild, input, model, signal, computed } from "@angular/core"; import { ControlValueAccessor, NG_VALUE_ACCESSOR, @@ -16,6 +17,9 @@ import { FocusableElement } from "../shared/focusable-element"; let nextId = 0; +/** + * Do not nest Search components inside another `
`, as they already contain their own standalone `` element for searching. + */ @Component({ selector: "bit-search", templateUrl: "./search.component.html", @@ -30,7 +34,7 @@ let nextId = 0; useExisting: SearchComponent, }, ], - imports: [InputModule, ReactiveFormsModule, FormsModule, I18nPipe], + imports: [InputModule, ReactiveFormsModule, FormsModule, I18nPipe, NgIf, NgClass], }) export class SearchComponent implements ControlValueAccessor, FocusableElement { private notifyOnChange: (v: string) => void; @@ -43,6 +47,11 @@ export class SearchComponent implements ControlValueAccessor, FocusableElement { // Use `type="text"` for Safari to improve rendering performance protected inputType = isBrowserSafariApi() ? ("text" as const) : ("search" as const); + protected isInputFocused = signal(false); + protected isFormHovered = signal(false); + + protected showResetButton = computed(() => this.isInputFocused() || this.isFormHovered()); + readonly disabled = model(); readonly placeholder = input(); readonly autocomplete = input(); @@ -52,11 +61,20 @@ export class SearchComponent implements ControlValueAccessor, FocusableElement { } onChange(searchText: string) { + this.searchText = searchText; // update the model when the input changes (so we can use it with *ngIf in the template) if (this.notifyOnChange != undefined) { this.notifyOnChange(searchText); } } + // Handle the reset button click + clearSearch() { + this.searchText = ""; + if (this.notifyOnChange) { + this.notifyOnChange(""); + } + } + onTouch() { if (this.notifyOnTouch != undefined) { this.notifyOnTouch(); diff --git a/libs/components/src/search/search.mdx b/libs/components/src/search/search.mdx index 7775225b8c2..98e91162c94 100644 --- a/libs/components/src/search/search.mdx +++ b/libs/components/src/search/search.mdx @@ -1,4 +1,4 @@ -import { Meta, Canvas, Source, Primary, Controls, Title } from "@storybook/addon-docs"; +import { Meta, Canvas, Source, Primary, Controls, Title, Description } from "@storybook/addon-docs"; import * as stories from "./search.stories"; @@ -9,6 +9,7 @@ import { SearchModule } from "@bitwarden/components"; ``` Search field + From aee23f72062cb7eee6065e7ce891294500e25766 Mon Sep 17 00:00:00 2001 From: Jason Ng Date: Wed, 23 Jul 2025 12:29:40 -0400 Subject: [PATCH 4/9] [PM-23722] remove previous change for the account security badge (#15739) --- .../tools/popup/settings/settings-v2.component.html | 11 +---------- .../src/tools/popup/settings/settings-v2.component.ts | 6 ------ libs/angular/src/vault/services/nudges.service.ts | 1 - 3 files changed, 1 insertion(+), 17 deletions(-) diff --git a/apps/browser/src/tools/popup/settings/settings-v2.component.html b/apps/browser/src/tools/popup/settings/settings-v2.component.html index 3f8bdb1cf2f..0b2e84712a4 100644 --- a/apps/browser/src/tools/popup/settings/settings-v2.component.html +++ b/apps/browser/src/tools/popup/settings/settings-v2.component.html @@ -10,16 +10,7 @@ -
-

{{ "accountSecurity" | i18n }}

- 1 -
+ {{ "accountSecurity" | i18n }}
diff --git a/apps/browser/src/tools/popup/settings/settings-v2.component.ts b/apps/browser/src/tools/popup/settings/settings-v2.component.ts index 7d3b9c776fc..a0383b99390 100644 --- a/apps/browser/src/tools/popup/settings/settings-v2.component.ts +++ b/apps/browser/src/tools/popup/settings/settings-v2.component.ts @@ -50,12 +50,6 @@ export class SettingsV2Component implements OnInit { shareReplay({ bufferSize: 1, refCount: true }), ); - protected showAcctSecurityNudge$: Observable = this.authenticatedAccount$.pipe( - switchMap((account) => - this.nudgesService.showNudgeBadge$(NudgeType.AccountSecurity, account.id), - ), - ); - showDownloadBitwardenNudge$: Observable = this.authenticatedAccount$.pipe( switchMap((account) => this.nudgesService.showNudgeBadge$(NudgeType.DownloadBitwarden, account.id), diff --git a/libs/angular/src/vault/services/nudges.service.ts b/libs/angular/src/vault/services/nudges.service.ts index 584aacd9837..6cb7ae4abf1 100644 --- a/libs/angular/src/vault/services/nudges.service.ts +++ b/libs/angular/src/vault/services/nudges.service.ts @@ -160,7 +160,6 @@ export class NudgesService { hasActiveBadges$(userId: UserId): Observable { // Add more nudge types here if they have the settings badge feature const nudgeTypes = [ - NudgeType.AccountSecurity, NudgeType.EmptyVaultNudge, NudgeType.DownloadBitwarden, NudgeType.AutofillNudge, From 417c4cd13b33a667c5bc9c9bba2eac5b070fcb11 Mon Sep 17 00:00:00 2001 From: Jordan Aasen <166539328+jaasen-livefront@users.noreply.github.com> Date: Wed, 23 Jul 2025 09:33:29 -0700 Subject: [PATCH 5/9] [PM-23479] - Can see card filter in AC if you belong to multiple orgs (#15661) * hide card filter if user does not have a cipher with the allowing org * fix restricted item type filter visibility * do not include deleted ciphers --- .../vault-filter/vault-filter.component.ts | 5 +- .../components/vault-filter.component.ts | 48 ++++++++++++++++--- 2 files changed, 45 insertions(+), 8 deletions(-) diff --git a/apps/web/src/app/admin-console/organizations/collections/vault-filter/vault-filter.component.ts b/apps/web/src/app/admin-console/organizations/collections/vault-filter/vault-filter.component.ts index 49bf43d60bf..bf0df14c8c6 100644 --- a/apps/web/src/app/admin-console/organizations/collections/vault-filter/vault-filter.component.ts +++ b/apps/web/src/app/admin-console/organizations/collections/vault-filter/vault-filter.component.ts @@ -10,6 +10,7 @@ import { BillingApiServiceAbstraction } from "@bitwarden/common/billing/abstract import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; +import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.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"; @@ -53,6 +54,7 @@ export class VaultFilterComponent protected configService: ConfigService, protected accountService: AccountService, protected restrictedItemTypesService: RestrictedItemTypesService, + protected cipherService: CipherService, ) { super( vaultFilterService, @@ -65,6 +67,7 @@ export class VaultFilterComponent configService, accountService, restrictedItemTypesService, + cipherService, ); } @@ -131,7 +134,7 @@ export class VaultFilterComponent async buildAllFilters(): Promise { const builderFilter = {} as VaultFilterList; - builderFilter.typeFilter = await this.addTypeFilter(["favorites"]); + builderFilter.typeFilter = await this.addTypeFilter(["favorites"], this._organization?.id); builderFilter.collectionFilter = await this.addCollectionFilter(); builderFilter.trashFilter = await this.addTrashFilter(); return builderFilter; diff --git a/apps/web/src/app/vault/individual-vault/vault-filter/components/vault-filter.component.ts b/apps/web/src/app/vault/individual-vault/vault-filter/components/vault-filter.component.ts index 61dd3e9ca80..4525d702153 100644 --- a/apps/web/src/app/vault/individual-vault/vault-filter/components/vault-filter.component.ts +++ b/apps/web/src/app/vault/individual-vault/vault-filter/components/vault-filter.component.ts @@ -1,6 +1,7 @@ import { Component, EventEmitter, inject, Input, OnDestroy, OnInit, Output } from "@angular/core"; import { Router } from "@angular/router"; import { + combineLatest, distinctUntilChanged, firstValueFrom, map, @@ -20,6 +21,7 @@ import { BillingApiServiceAbstraction } from "@bitwarden/common/billing/abstract import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; +import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.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"; @@ -155,6 +157,7 @@ export class VaultFilterComponent implements OnInit, OnDestroy { protected configService: ConfigService, protected accountService: AccountService, protected restrictedItemTypesService: RestrictedItemTypesService, + protected cipherService: CipherService, ) {} async ngOnInit(): Promise { @@ -292,16 +295,47 @@ export class VaultFilterComponent implements OnInit, OnDestroy { return orgFilterSection; } - protected async addTypeFilter(excludeTypes: CipherStatus[] = []): Promise { + protected async addTypeFilter( + excludeTypes: CipherStatus[] = [], + organizationId?: string, + ): Promise { const allFilter: CipherTypeFilter = { id: "AllItems", name: "allItems", type: "all", icon: "" }; - const data$ = this.restrictedItemTypesService.restricted$.pipe( - map((restricted) => { - // List of types restricted by all orgs - const restrictedByAll = restricted - .filter((r) => r.allowViewOrgIds.length === 0) + const userId = await firstValueFrom(this.activeUserId$); + + const data$ = combineLatest([ + this.restrictedItemTypesService.restricted$, + this.cipherService.cipherViews$(userId), + ]).pipe( + map(([restrictedTypes, ciphers]) => { + const restrictedForUser = restrictedTypes + .filter((r) => { + // - All orgs restrict the type + if (r.allowViewOrgIds.length === 0) { + return true; + } + // - Admin console: user has no ciphers of that type in the selected org + // - Individual vault view: user has no ciphers of that type in any allowed org + return !ciphers?.some((c) => { + if (c.deletedDate || c.type !== r.cipherType) { + return false; + } + // If the cipher doesn't belong to an org it is automatically restricted + if (!c.organizationId) { + return false; + } + if (organizationId) { + return ( + c.organizationId === organizationId && + r.allowViewOrgIds.includes(c.organizationId) + ); + } + return r.allowViewOrgIds.includes(c.organizationId); + }); + }) .map((r) => r.cipherType); - const toExclude = [...excludeTypes, ...restrictedByAll]; + + const toExclude = [...excludeTypes, ...restrictedForUser]; return this.allTypeFilters.filter( (f) => typeof f.type === "string" || !toExclude.includes(f.type), ); From 2040be68e3dbc8a3113ebbd5b1700954bb89f039 Mon Sep 17 00:00:00 2001 From: Jordan Aasen <166539328+jaasen-livefront@users.noreply.github.com> Date: Wed, 23 Jul 2025 09:33:45 -0700 Subject: [PATCH 6/9] [PM-23360] - Hide restricted cipher types in "File -> New Item" on desktop (#15743) * hide restricted cipher types in file menu on desktop * fix bitwarden menu * small fixes --- apps/desktop/src/app/app.component.ts | 7 ++++++ apps/desktop/src/main/menu/menu.file.ts | 25 +++++++++++++++++++++- apps/desktop/src/main/menu/menu.updater.ts | 6 ++++-- apps/desktop/src/main/menu/menubar.ts | 1 + 4 files changed, 36 insertions(+), 3 deletions(-) diff --git a/apps/desktop/src/app/app.component.ts b/apps/desktop/src/app/app.component.ts index b5c34cc95a3..10aa7ff9eeb 100644 --- a/apps/desktop/src/app/app.component.ts +++ b/apps/desktop/src/app/app.component.ts @@ -68,6 +68,7 @@ import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.servi import { InternalFolderService } from "@bitwarden/common/vault/abstractions/folder/folder.service.abstraction"; import { SearchService } from "@bitwarden/common/vault/abstractions/search.service"; import { CipherType } from "@bitwarden/common/vault/enums"; +import { RestrictedItemTypesService } from "@bitwarden/common/vault/services/restricted-item-types.service"; import { DialogRef, DialogService, ToastOptions, ToastService } from "@bitwarden/components"; import { CredentialGeneratorHistoryDialogComponent } from "@bitwarden/generator-components"; import { KeyService, BiometricStateService } from "@bitwarden/key-management"; @@ -172,6 +173,7 @@ export class AppComponent implements OnInit, OnDestroy { private userDecryptionOptionsService: UserDecryptionOptionsServiceAbstraction, private readonly destroyRef: DestroyRef, private readonly documentLangSetter: DocumentLangSetter, + private restrictedItemTypesService: RestrictedItemTypesService, ) { this.deviceTrustToastService.setupListeners$.pipe(takeUntilDestroyed()).subscribe(); @@ -523,10 +525,12 @@ export class AppComponent implements OnInit, OnDestroy { private async updateAppMenu() { let updateRequest: MenuUpdateRequest; const stateAccounts = await firstValueFrom(this.accountService.accounts$); + if (stateAccounts == null || Object.keys(stateAccounts).length < 1) { updateRequest = { accounts: null, activeUserId: null, + restrictedCipherTypes: null, }; } else { const accounts: { [userId: string]: MenuAccount } = {}; @@ -557,6 +561,9 @@ export class AppComponent implements OnInit, OnDestroy { activeUserId: await firstValueFrom( this.accountService.activeAccount$.pipe(map((a) => a?.id)), ), + restrictedCipherTypes: ( + await firstValueFrom(this.restrictedItemTypesService.restricted$) + ).map((restrictedItems) => restrictedItems.cipherType), }; } diff --git a/apps/desktop/src/main/menu/menu.file.ts b/apps/desktop/src/main/menu/menu.file.ts index 19ba5e99792..a8cdb347a77 100644 --- a/apps/desktop/src/main/menu/menu.file.ts +++ b/apps/desktop/src/main/menu/menu.file.ts @@ -2,6 +2,7 @@ import { BrowserWindow, MenuItemConstructorOptions } from "electron"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service"; +import { CipherType } from "@bitwarden/sdk-internal"; import { isMac, isMacAppStore } from "../../utils"; import { UpdaterMain } from "../updater.main"; @@ -54,6 +55,7 @@ export class FileMenu extends FirstMenu implements IMenubarMenu { accounts: { [userId: string]: MenuAccount }, isLocked: boolean, isLockable: boolean, + private restrictedCipherTypes: CipherType[], ) { super(i18nService, messagingService, updater, window, accounts, isLocked, isLockable); } @@ -77,6 +79,23 @@ export class FileMenu extends FirstMenu implements IMenubarMenu { }; } + private mapMenuItemToCipherType(itemId: string): CipherType { + switch (itemId) { + case "typeLogin": + return CipherType.Login; + case "typeCard": + return CipherType.Card; + case "typeIdentity": + return CipherType.Identity; + case "typeSecureNote": + return CipherType.SecureNote; + case "typeSshKey": + return CipherType.SshKey; + default: + throw new Error(`Unknown menu item id: ${itemId}`); + } + } + private get addNewItemSubmenu(): MenuItemConstructorOptions[] { return [ { @@ -109,7 +128,11 @@ export class FileMenu extends FirstMenu implements IMenubarMenu { click: () => this.sendMessage("newSshKey"), accelerator: "CmdOrCtrl+Shift+K", }, - ]; + ].filter((item) => { + return !this.restrictedCipherTypes?.some( + (restrictedType) => restrictedType === this.mapMenuItemToCipherType(item.id), + ); + }); } private get addNewFolder(): MenuItemConstructorOptions { diff --git a/apps/desktop/src/main/menu/menu.updater.ts b/apps/desktop/src/main/menu/menu.updater.ts index 6f82a78384f..8b658049de7 100644 --- a/apps/desktop/src/main/menu/menu.updater.ts +++ b/apps/desktop/src/main/menu/menu.updater.ts @@ -1,8 +1,10 @@ // FIXME: Update this file to be type safe and remove this and next line // @ts-strict-ignore +import { CipherType } from "@bitwarden/common/vault/enums"; export class MenuUpdateRequest { - activeUserId: string; - accounts: { [userId: string]: MenuAccount }; + activeUserId: string | null; + accounts: { [userId: string]: MenuAccount } | null; + restrictedCipherTypes: CipherType[] | null; } export class MenuAccount { diff --git a/apps/desktop/src/main/menu/menubar.ts b/apps/desktop/src/main/menu/menubar.ts index 825afdaa1e8..8ac3a084d95 100644 --- a/apps/desktop/src/main/menu/menubar.ts +++ b/apps/desktop/src/main/menu/menubar.ts @@ -83,6 +83,7 @@ export class Menubar { updateRequest?.accounts, isLocked, isLockable, + updateRequest?.restrictedCipherTypes, ), new EditMenu(i18nService, messagingService, isLocked), new ViewMenu(i18nService, messagingService, isLocked), From e47e1f79d9b8d48358e6b57f665a5455c4dab156 Mon Sep 17 00:00:00 2001 From: Jared Snider <116684653+JaredSnider-Bitwarden@users.noreply.github.com> Date: Wed, 23 Jul 2025 12:58:44 -0400 Subject: [PATCH 7/9] fix(ChangePasswordComp): [Auth/PM-23913] Extension popout now closes after a password change (#15681) --- .../extension-change-password.service.spec.ts | 50 +++++++++++++++++++ .../extension-change-password.service.ts | 29 +++++++++++ .../src/popup/services/services.module.ts | 8 +++ .../change-password.component.ts | 3 ++ .../change-password.service.abstraction.ts | 6 +++ 5 files changed, 96 insertions(+) create mode 100644 apps/browser/src/auth/popup/change-password/extension-change-password.service.spec.ts create mode 100644 apps/browser/src/auth/popup/change-password/extension-change-password.service.ts diff --git a/apps/browser/src/auth/popup/change-password/extension-change-password.service.spec.ts b/apps/browser/src/auth/popup/change-password/extension-change-password.service.spec.ts new file mode 100644 index 00000000000..a6a6b905218 --- /dev/null +++ b/apps/browser/src/auth/popup/change-password/extension-change-password.service.spec.ts @@ -0,0 +1,50 @@ +import { MockProxy, mock } from "jest-mock-extended"; + +import { ChangePasswordService } from "@bitwarden/angular/auth/password-management/change-password"; +import { MasterPasswordApiService } from "@bitwarden/common/auth/abstractions/master-password-api.service.abstraction"; +import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/key-management/master-password/abstractions/master-password.service.abstraction"; +import { KeyService } from "@bitwarden/key-management"; + +import { BrowserApi } from "../../../platform/browser/browser-api"; +import BrowserPopupUtils from "../../../platform/browser/browser-popup-utils"; + +import { ExtensionChangePasswordService } from "./extension-change-password.service"; + +describe("ExtensionChangePasswordService", () => { + let keyService: MockProxy; + let masterPasswordApiService: MockProxy; + let masterPasswordService: MockProxy; + let window: MockProxy; + + let changePasswordService: ChangePasswordService; + + beforeEach(() => { + keyService = mock(); + masterPasswordApiService = mock(); + masterPasswordService = mock(); + window = mock(); + + changePasswordService = new ExtensionChangePasswordService( + keyService, + masterPasswordApiService, + masterPasswordService, + window, + ); + }); + + it("should instantiate the service", () => { + expect(changePasswordService).toBeDefined(); + }); + + it("should close the browser extension popout", () => { + const closePopupSpy = jest.spyOn(BrowserApi, "closePopup"); + const browserPopupUtilsInPopupSpy = jest + .spyOn(BrowserPopupUtils, "inPopout") + .mockReturnValue(true); + + changePasswordService.closeBrowserExtensionPopout?.(); + + expect(closePopupSpy).toHaveBeenCalledWith(window); + expect(browserPopupUtilsInPopupSpy).toHaveBeenCalledWith(window); + }); +}); diff --git a/apps/browser/src/auth/popup/change-password/extension-change-password.service.ts b/apps/browser/src/auth/popup/change-password/extension-change-password.service.ts new file mode 100644 index 00000000000..dd2ce48d27a --- /dev/null +++ b/apps/browser/src/auth/popup/change-password/extension-change-password.service.ts @@ -0,0 +1,29 @@ +import { + DefaultChangePasswordService, + ChangePasswordService, +} from "@bitwarden/angular/auth/password-management/change-password"; +import { MasterPasswordApiService } from "@bitwarden/common/auth/abstractions/master-password-api.service.abstraction"; +import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/key-management/master-password/abstractions/master-password.service.abstraction"; +import { KeyService } from "@bitwarden/key-management"; + +import { BrowserApi } from "../../../platform/browser/browser-api"; +import BrowserPopupUtils from "../../../platform/browser/browser-popup-utils"; + +export class ExtensionChangePasswordService + extends DefaultChangePasswordService + implements ChangePasswordService +{ + constructor( + protected keyService: KeyService, + protected masterPasswordApiService: MasterPasswordApiService, + protected masterPasswordService: InternalMasterPasswordServiceAbstraction, + private win: Window, + ) { + super(keyService, masterPasswordApiService, masterPasswordService); + } + closeBrowserExtensionPopout(): void { + if (BrowserPopupUtils.inPopout(this.win)) { + BrowserApi.closePopup(this.win); + } + } +} diff --git a/apps/browser/src/popup/services/services.module.ts b/apps/browser/src/popup/services/services.module.ts index 3887c8c8b12..509f7554aef 100644 --- a/apps/browser/src/popup/services/services.module.ts +++ b/apps/browser/src/popup/services/services.module.ts @@ -5,6 +5,7 @@ import { merge, of, Subject } from "rxjs"; import { CollectionService } from "@bitwarden/admin-console/common"; import { DeviceManagementComponentServiceAbstraction } from "@bitwarden/angular/auth/device-management/device-management-component.service.abstraction"; +import { ChangePasswordService } from "@bitwarden/angular/auth/password-management/change-password"; import { AngularThemingService } from "@bitwarden/angular/platform/services/theming/angular-theming.service"; import { SafeProvider, safeProvider } from "@bitwarden/angular/platform/utils/safe-provider"; import { ViewCacheService } from "@bitwarden/angular/platform/view-cache"; @@ -45,6 +46,7 @@ import { AccountService as AccountServiceAbstraction, } from "@bitwarden/common/auth/abstractions/account.service"; import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service"; +import { MasterPasswordApiService } from "@bitwarden/common/auth/abstractions/master-password-api.service.abstraction"; import { SsoLoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/sso-login.service.abstraction"; import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction"; import { @@ -143,6 +145,7 @@ import { import { AccountSwitcherService } from "../../auth/popup/account-switching/services/account-switcher.service"; import { ForegroundLockService } from "../../auth/popup/accounts/foreground-lock.service"; +import { ExtensionChangePasswordService } from "../../auth/popup/change-password/extension-change-password.service"; import { ExtensionLoginComponentService } from "../../auth/popup/login/extension-login-component.service"; import { ExtensionSsoComponentService } from "../../auth/popup/login/extension-sso-component.service"; import { ExtensionLogoutService } from "../../auth/popup/logout/extension-logout.service"; @@ -664,6 +667,11 @@ const safeProviders: SafeProvider[] = [ useClass: DefaultSshImportPromptService, deps: [DialogService, ToastService, PlatformUtilsService, I18nServiceAbstraction], }), + safeProvider({ + provide: ChangePasswordService, + useClass: ExtensionChangePasswordService, + deps: [KeyService, MasterPasswordApiService, InternalMasterPasswordServiceAbstraction, WINDOW], + }), safeProvider({ provide: NotificationsService, useClass: ForegroundNotificationsService, diff --git a/libs/angular/src/auth/password-management/change-password/change-password.component.ts b/libs/angular/src/auth/password-management/change-password/change-password.component.ts index 02738d33321..7bb9584e934 100644 --- a/libs/angular/src/auth/password-management/change-password/change-password.component.ts +++ b/libs/angular/src/auth/password-management/change-password/change-password.component.ts @@ -178,6 +178,9 @@ export class ChangePasswordComponent implements OnInit { // TODO: PM-23515 eventually use the logout service instead of messaging service once it is available without circular dependencies this.messagingService.send("logout"); + + // Close the popout if we are in a browser extension popout. + this.changePasswordService.closeBrowserExtensionPopout?.(); } } catch (error) { this.logService.error(error); diff --git a/libs/angular/src/auth/password-management/change-password/change-password.service.abstraction.ts b/libs/angular/src/auth/password-management/change-password/change-password.service.abstraction.ts index 2fd3bbae67a..1d6d789cdc5 100644 --- a/libs/angular/src/auth/password-management/change-password/change-password.service.abstraction.ts +++ b/libs/angular/src/auth/password-management/change-password/change-password.service.abstraction.ts @@ -59,4 +59,10 @@ export abstract class ChangePasswordService { * - Currently only used on the web change password service. */ clearDeeplinkState?: () => Promise; + + /** + * Optional method that closes the browser extension popout if in a popout + * If not in a popout, does nothing. + */ + abstract closeBrowserExtensionPopout?(): void; } From d45cacc1aff9ce4ef66f9d163ba9091257e0bcd0 Mon Sep 17 00:00:00 2001 From: Vicki League Date: Wed, 23 Jul 2025 13:26:35 -0400 Subject: [PATCH 8/9] [CL-801] Fix chromatic.yml externals formatting (#15736) --- .github/workflows/chromatic.yml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.github/workflows/chromatic.yml b/.github/workflows/chromatic.yml index 4ee39305f84..d0b9cab4c45 100644 --- a/.github/workflows/chromatic.yml +++ b/.github/workflows/chromatic.yml @@ -102,6 +102,9 @@ jobs: storybookBuildDir: ./storybook-static exitOnceUploaded: true onlyChanged: true - externals: "[\"libs/components/**/*.scss\", \"libs/components/**/*.css\", \"libs/components/tailwind.config*.js\"]" + externals: | + libs/components/**/*.scss + libs/components/**/*.css + libs/components/tailwind.config*.js # Rather than use an `if` check on the whole publish step, we need to tell Chromatic to skip so that any Chromatic-spawned actions are properly skipped skip: ${{ steps.get-changed-files-for-chromatic.outputs.storyFiles == 'false' }} From fe1c04099355162b20a891ae73bee8f91cf4ab6a Mon Sep 17 00:00:00 2001 From: Jordan Aasen <166539328+jaasen-livefront@users.noreply.github.com> Date: Wed, 23 Jul 2025 10:37:40 -0700 Subject: [PATCH 9/9] [PM-23789][PM-237090][PM-23791][PM-23792] - [Web][Desktop][Browser] - Do not import cards if policy is enabled (#15740) * restrict item types in import * add comment * fix spec * fix dep * clean up logic --- apps/browser/src/background/main.background.ts | 1 + apps/cli/src/service-container/service-container.ts | 1 + libs/importer/src/components/import.component.ts | 1 + libs/importer/src/services/import.service.spec.ts | 4 ++++ libs/importer/src/services/import.service.ts | 13 +++++++++++++ 5 files changed, 20 insertions(+) diff --git a/apps/browser/src/background/main.background.ts b/apps/browser/src/background/main.background.ts index 1dfc947b284..2565f366870 100644 --- a/apps/browser/src/background/main.background.ts +++ b/apps/browser/src/background/main.background.ts @@ -1070,6 +1070,7 @@ export default class MainBackground { this.pinService, this.accountService, this.sdkService, + this.restrictedItemTypesService, ); this.individualVaultExportService = new IndividualVaultExportService( diff --git a/apps/cli/src/service-container/service-container.ts b/apps/cli/src/service-container/service-container.ts index aa507aec1d8..78f961973d9 100644 --- a/apps/cli/src/service-container/service-container.ts +++ b/apps/cli/src/service-container/service-container.ts @@ -821,6 +821,7 @@ export class ServiceContainer { this.pinService, this.accountService, this.sdkService, + this.restrictedItemTypesService, ); this.individualExportService = new IndividualVaultExportService( diff --git a/libs/importer/src/components/import.component.ts b/libs/importer/src/components/import.component.ts index 4f2715fe9cf..7bac6b0e0a5 100644 --- a/libs/importer/src/components/import.component.ts +++ b/libs/importer/src/components/import.component.ts @@ -100,6 +100,7 @@ const safeProviders: SafeProvider[] = [ PinServiceAbstraction, AccountService, SdkService, + RestrictedItemTypesService, ], }), ]; diff --git a/libs/importer/src/services/import.service.spec.ts b/libs/importer/src/services/import.service.spec.ts index f71c34bf209..a27b74c7ad5 100644 --- a/libs/importer/src/services/import.service.spec.ts +++ b/libs/importer/src/services/import.service.spec.ts @@ -13,6 +13,7 @@ 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 { FolderView } from "@bitwarden/common/vault/models/view/folder.view"; +import { RestrictedItemTypesService } from "@bitwarden/common/vault/services/restricted-item-types.service"; import { KeyService } from "@bitwarden/key-management"; import { BitwardenPasswordProtectedImporter } from "../importers/bitwarden/bitwarden-password-protected-importer"; @@ -34,6 +35,7 @@ describe("ImportService", () => { let pinService: MockProxy; let accountService: MockProxy; let sdkService: MockSdkService; + let restrictedItemTypesService: MockProxy; beforeEach(() => { cipherService = mock(); @@ -45,6 +47,7 @@ describe("ImportService", () => { encryptService = mock(); pinService = mock(); sdkService = new MockSdkService(); + restrictedItemTypesService = mock(); importService = new ImportService( cipherService, @@ -57,6 +60,7 @@ describe("ImportService", () => { pinService, accountService, sdkService, + restrictedItemTypesService, ); }); diff --git a/libs/importer/src/services/import.service.ts b/libs/importer/src/services/import.service.ts index c9cb325d10b..c6bff607633 100644 --- a/libs/importer/src/services/import.service.ts +++ b/libs/importer/src/services/import.service.ts @@ -26,6 +26,7 @@ import { CipherRequest } from "@bitwarden/common/vault/models/request/cipher.req import { FolderWithIdRequest } from "@bitwarden/common/vault/models/request/folder-with-id.request"; import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; import { FolderView } from "@bitwarden/common/vault/models/view/folder.view"; +import { RestrictedItemTypesService } from "@bitwarden/common/vault/services/restricted-item-types.service"; import { KeyService } from "@bitwarden/key-management"; import { @@ -119,6 +120,7 @@ export class ImportService implements ImportServiceAbstraction { private pinService: PinServiceAbstraction, private accountService: AccountService, private sdkService: SdkService, + private restrictedItemTypesService: RestrictedItemTypesService, ) {} getImportOptions(): ImportOption[] { @@ -166,6 +168,17 @@ export class ImportService implements ImportServiceAbstraction { } } + const restrictedItemTypes = await firstValueFrom( + this.restrictedItemTypesService.restricted$.pipe( + map((restrictedItemTypes) => restrictedItemTypes.map((r) => r.cipherType)), + ), + ); + + // Filter out restricted item types from the import result + importResult.ciphers = importResult.ciphers.filter( + (cipher) => !restrictedItemTypes.includes(cipher.type), + ); + if (organizationId && !selectedImportTarget && !canAccessImportExport) { const hasUnassignedCollections = importResult.collectionRelationships.length < importResult.ciphers.length;