From c3d0a2d858b226d89b6fd1d26d3b0b7ccbd435cb Mon Sep 17 00:00:00 2001 From: Oscar Hinton Date: Wed, 4 Feb 2026 16:03:43 +0100 Subject: [PATCH 01/26] Add a way to add folders in the desktop ui migration milestone 1 (#18632) --- .../src/vault/app/vault-v3/vault.component.html | 1 + .../src/vault/app/vault/vault-items-v2.component.html | 8 ++++++++ .../src/vault/app/vault/vault-items-v2.component.ts | 11 +++++++++-- .../src/vault/components/vault-items.component.ts | 2 +- 4 files changed, 19 insertions(+), 3 deletions(-) diff --git a/apps/desktop/src/vault/app/vault-v3/vault.component.html b/apps/desktop/src/vault/app/vault-v3/vault.component.html index 42151500964..51f6426a1ba 100644 --- a/apps/desktop/src/vault/app/vault-v3/vault.component.html +++ b/apps/desktop/src/vault/app/vault-v3/vault.component.html @@ -6,6 +6,7 @@ (onCipherClicked)="viewCipher($event)" (onCipherRightClicked)="viewCipherMenu($event)" (onAddCipher)="addCipher($event)" + (onAddFolder)="addFolder()" [showPremiumCallout]="showPremiumCallout$ | async" > diff --git a/apps/desktop/src/vault/app/vault/vault-items-v2.component.html b/apps/desktop/src/vault/app/vault/vault-items-v2.component.html index 84c0cd8a1fb..7f402d7bfb9 100644 --- a/apps/desktop/src/vault/app/vault/vault-items-v2.component.html +++ b/apps/desktop/src/vault/app/vault/vault-items-v2.component.html @@ -95,5 +95,13 @@ {{ itemTypes.labelKey | i18n }} } + @if (desktopMigrationMilestone1()) { + + + + } diff --git a/apps/desktop/src/vault/app/vault/vault-items-v2.component.ts b/apps/desktop/src/vault/app/vault/vault-items-v2.component.ts index a6582f6de58..50f00025238 100644 --- a/apps/desktop/src/vault/app/vault/vault-items-v2.component.ts +++ b/apps/desktop/src/vault/app/vault/vault-items-v2.component.ts @@ -1,12 +1,13 @@ import { ScrollingModule } from "@angular/cdk/scrolling"; import { CommonModule } from "@angular/common"; -import { Component, input } from "@angular/core"; -import { takeUntilDestroyed } from "@angular/core/rxjs-interop"; +import { Component, input, output } from "@angular/core"; +import { takeUntilDestroyed, toSignal } from "@angular/core/rxjs-interop"; import { distinctUntilChanged, debounceTime } from "rxjs"; import { JslibModule } from "@bitwarden/angular/jslib.module"; import { VaultItemsComponent as BaseVaultItemsComponent } from "@bitwarden/angular/vault/components/vault-items.component"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { uuidAsString } from "@bitwarden/common/platform/abstractions/sdk/sdk.service"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; @@ -32,6 +33,12 @@ import { SearchBarService } from "../../../app/layout/search/search-bar.service" export class VaultItemsV2Component extends BaseVaultItemsComponent { readonly showPremiumCallout = input(false); + readonly onAddFolder = output(); + + protected readonly desktopMigrationMilestone1 = toSignal( + this.configService.getFeatureFlag$(FeatureFlag.DesktopUiMigrationMilestone1), + ); + protected CipherViewLikeUtils = CipherViewLikeUtils; constructor( diff --git a/libs/angular/src/vault/components/vault-items.component.ts b/libs/angular/src/vault/components/vault-items.component.ts index c4fe2741e11..6058955788e 100644 --- a/libs/angular/src/vault/components/vault-items.component.ts +++ b/libs/angular/src/vault/components/vault-items.component.ts @@ -94,7 +94,7 @@ export class VaultItemsComponent implements OnDestroy protected cipherService: CipherService, protected accountService: AccountService, protected restrictedItemTypesService: RestrictedItemTypesService, - private configService: ConfigService, + protected configService: ConfigService, ) { this.subscribeToCiphers(); From 5bceadd29b1f4681134b606d24355003157fe5a8 Mon Sep 17 00:00:00 2001 From: Vijay Oommen Date: Wed, 4 Feb 2026 09:11:06 -0600 Subject: [PATCH 02/26] [PM-31584] Minor UI fixes (#18736) --- .../all-applications/applications.component.html | 7 +++---- .../all-applications/applications.component.ts | 2 ++ 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/bitwarden_license/bit-web/src/app/dirt/access-intelligence/all-applications/applications.component.html b/bitwarden_license/bit-web/src/app/dirt/access-intelligence/all-applications/applications.component.html index 7c4f6d04a6b..9af071a1268 100644 --- a/bitwarden_license/bit-web/src/app/dirt/access-intelligence/all-applications/applications.component.html +++ b/bitwarden_license/bit-web/src/app/dirt/access-intelligence/all-applications/applications.component.html @@ -3,12 +3,10 @@ } @else { @let drawerDetails = dataService.drawerDetails$ | async;
-

{{ "allApplications" | i18n }}

-
@@ -20,7 +18,8 @@ (ngModelChange)="setFilterApplicationsByStatus($event)" fullWidth="false" class="tw-min-w-48" - > + > +
From a2916084eec5da058b5fad4fb3db96b0130cbe0a Mon Sep 17 00:00:00 2001 From: Vijay Oommen Date: Wed, 4 Feb 2026 10:42:13 -0600 Subject: [PATCH 08/26] [PM-30547] Table empty state message (#18752) --- apps/web/src/locales/en/messages.json | 3 +++ .../all-applications/applications.component.html | 6 ++++++ .../all-applications/applications.component.ts | 7 +++++++ 3 files changed, 16 insertions(+) diff --git a/apps/web/src/locales/en/messages.json b/apps/web/src/locales/en/messages.json index 160ad4e867a..89b3b3ac5c6 100644 --- a/apps/web/src/locales/en/messages.json +++ b/apps/web/src/locales/en/messages.json @@ -38,6 +38,9 @@ "accessIntelligence": { "message": "Access Intelligence" }, + "noApplicationsMatchTheseFilters": { + "message": "No applications match these filters" + }, "passwordRisk": { "message": "Password Risk" }, diff --git a/bitwarden_license/bit-web/src/app/dirt/access-intelligence/all-applications/applications.component.html b/bitwarden_license/bit-web/src/app/dirt/access-intelligence/all-applications/applications.component.html index 9af071a1268..2fa9fabf73d 100644 --- a/bitwarden_license/bit-web/src/app/dirt/access-intelligence/all-applications/applications.component.html +++ b/bitwarden_license/bit-web/src/app/dirt/access-intelligence/all-applications/applications.component.html @@ -43,5 +43,11 @@ [checkboxChange]="onCheckboxChange" [showAppAtRiskMembers]="showAppAtRiskMembers" > + + @if (emptyTableExplanation()) { +
+ {{ emptyTableExplanation() }} +
+ }
} diff --git a/bitwarden_license/bit-web/src/app/dirt/access-intelligence/all-applications/applications.component.ts b/bitwarden_license/bit-web/src/app/dirt/access-intelligence/all-applications/applications.component.ts index 07c531d6907..8cd0c2640f5 100644 --- a/bitwarden_license/bit-web/src/app/dirt/access-intelligence/all-applications/applications.component.ts +++ b/bitwarden_license/bit-web/src/app/dirt/access-intelligence/all-applications/applications.component.ts @@ -104,6 +104,7 @@ export class ApplicationsComponent implements OnInit { icon: " ", }, ]); + protected readonly emptyTableExplanation = signal(""); constructor( protected i18nService: I18nService, @@ -164,6 +165,12 @@ export class ApplicationsComponent implements OnInit { this.dataSource.filter = (app) => filterFunction(app) && app.applicationName.toLowerCase().includes(searchText.toLowerCase()); + + if (this.dataSource?.filteredData?.length === 0) { + this.emptyTableExplanation.set(this.i18nService.t("noApplicationsMatchTheseFilters")); + } else { + this.emptyTableExplanation.set(""); + } }); } From 2876ef15ae9f7095e0ac41d0cb1670979d2ff423 Mon Sep 17 00:00:00 2001 From: Jordan Aasen <166539328+jaasen-livefront@users.noreply.github.com> Date: Wed, 4 Feb 2026 09:31:23 -0800 Subject: [PATCH 09/26] [PM-31606] - Clone should not be an option for archived item for non-premium user (#18726) * do not allow cloning of archived items for non-premium users * add tests --- .../popup/settings/archive.component.html | 8 +- .../popup/settings/archive.component.spec.ts | 107 ++++++++++++++++-- .../vault/popup/settings/archive.component.ts | 4 + 3 files changed, 109 insertions(+), 10 deletions(-) diff --git a/apps/browser/src/vault/popup/settings/archive.component.html b/apps/browser/src/vault/popup/settings/archive.component.html index 01ac799ba29..5024a22ff16 100644 --- a/apps/browser/src/vault/popup/settings/archive.component.html +++ b/apps/browser/src/vault/popup/settings/archive.component.html @@ -74,9 +74,11 @@ - + @if (userHasPremium$ | async) { + + } @if (canAssignCollections$ | async) {
-
diff --git a/libs/components/src/callout/callout.stories.ts b/libs/components/src/callout/callout.stories.ts index ff1a8c16d5f..fb1a2d67a40 100644 --- a/libs/components/src/callout/callout.stories.ts +++ b/libs/components/src/callout/callout.stories.ts @@ -113,7 +113,7 @@ export const WithTextButton: Story = { template: ` (args)}>

The content of the callout

- Visit the help center + Visit the help center
`, }), diff --git a/libs/components/src/card/base-card/base-card.stories.ts b/libs/components/src/card/base-card/base-card.stories.ts index bae07dd1468..98814c1f9f4 100644 --- a/libs/components/src/card/base-card/base-card.stories.ts +++ b/libs/components/src/card/base-card/base-card.stories.ts @@ -1,6 +1,6 @@ import { Meta, StoryObj, moduleMetadata } from "@storybook/angular"; -import { AnchorLinkDirective } from "../../link"; +import { LinkComponent } from "../../link"; import { TypographyModule } from "../../typography"; import { BaseCardComponent } from "./base-card.component"; @@ -10,7 +10,7 @@ export default { component: BaseCardComponent, decorators: [ moduleMetadata({ - imports: [AnchorLinkDirective, TypographyModule], + imports: [LinkComponent, TypographyModule], }), ], parameters: { diff --git a/libs/components/src/layout/layout.component.ts b/libs/components/src/layout/layout.component.ts index da30b76a9f0..c71c4e73c6e 100644 --- a/libs/components/src/layout/layout.component.ts +++ b/libs/components/src/layout/layout.component.ts @@ -5,7 +5,7 @@ import { booleanAttribute, Component, ElementRef, inject, input, viewChild } fro import { RouterModule } from "@angular/router"; import { DrawerService } from "../dialog/drawer.service"; -import { LinkModule } from "../link"; +import { LinkComponent, LinkModule } from "../link"; import { SideNavService } from "../navigation/side-nav.service"; import { SharedModule } from "../shared"; @@ -52,11 +52,11 @@ export class LayoutComponent { * * @see https://github.com/angular/components/issues/10247#issuecomment-384060265 **/ - private readonly skipLink = viewChild.required>("skipLink"); + private readonly skipLink = viewChild.required("skipLink"); handleKeydown(ev: KeyboardEvent) { if (isNothingFocused()) { ev.preventDefault(); - this.skipLink().nativeElement.focus(); + this.skipLink().el.nativeElement.focus(); } } } diff --git a/libs/components/src/link/index.ts b/libs/components/src/link/index.ts index 480f5396de7..08617e813f5 100644 --- a/libs/components/src/link/index.ts +++ b/libs/components/src/link/index.ts @@ -1,2 +1,2 @@ -export * from "./link.directive"; +export * from "./link.component"; export * from "./link.module"; diff --git a/libs/components/src/link/link.component.html b/libs/components/src/link/link.component.html new file mode 100644 index 00000000000..810b65db519 --- /dev/null +++ b/libs/components/src/link/link.component.html @@ -0,0 +1,11 @@ +
+ @if (startIcon()) { + + } + + + + @if (endIcon()) { + + } +
diff --git a/libs/components/src/link/link.directive.ts b/libs/components/src/link/link.component.ts similarity index 59% rename from libs/components/src/link/link.directive.ts rename to libs/components/src/link/link.component.ts index 62f0d8b878f..d826a4633a9 100644 --- a/libs/components/src/link/link.directive.ts +++ b/libs/components/src/link/link.component.ts @@ -1,6 +1,14 @@ -import { input, HostBinding, Directive, inject, ElementRef, booleanAttribute } from "@angular/core"; +import { + ChangeDetectionStrategy, + Component, + computed, + input, + booleanAttribute, + inject, + ElementRef, +} from "@angular/core"; -import { AriaDisableDirective } from "../a11y"; +import { BitwardenIcon } from "../shared/icon"; import { ariaDisableElement } from "../utils"; export const LinkTypes = [ @@ -46,16 +54,16 @@ const commonStyles = [ "tw-transition", "tw-no-underline", "tw-cursor-pointer", - "hover:tw-underline", - "hover:tw-decoration-1", + "[&:hover_span]:tw-underline", + "[&.tw-test-hover_span]:tw-underline", + "[&:hover_span]:tw-decoration-[.125em]", + "[&.tw-test-hover_span]:tw-decoration-[.125em]", "disabled:tw-no-underline", "disabled:tw-cursor-not-allowed", "disabled:!tw-text-fg-disabled", "disabled:hover:!tw-text-fg-disabled", "disabled:hover:tw-no-underline", "focus-visible:tw-outline-none", - "focus-visible:tw-underline", - "focus-visible:tw-decoration-1", "focus-visible:before:tw-ring-border-focus", // Workaround for html button tag not being able to be set to `display: inline` @@ -72,8 +80,12 @@ const commonStyles = [ "before:tw-block", "before:tw-absolute", "before:-tw-inset-x-[0.1em]", + "before:-tw-inset-y-[0]", "before:tw-rounded-md", "before:tw-transition", + "before:tw-h-full", + "before:tw-w-[calc(100%_+_.25rem)]", + "before:tw-pointer-events-none", "focus-visible:before:tw-ring-2", "focus-visible:tw-z-10", "aria-disabled:tw-no-underline", @@ -83,47 +95,57 @@ const commonStyles = [ "aria-disabled:hover:tw-no-underline", ]; -@Directive() -abstract class LinkDirective { - readonly linkType = input("default"); -} - -/** - * Text Links and Buttons can use either the `` or `
-
-
@@ -203,7 +201,7 @@ export const Buttons: Story = { }, }; -export const Anchors: StoryObj = { +export const Anchors: StoryObj = { render: (args) => ({ props: { linkType: args.linkType, @@ -220,14 +218,12 @@ export const Anchors: StoryObj = { Anchor @@ -247,20 +243,57 @@ export const Inline: Story = { props: args, template: /*html*/ ` - On the internet paragraphs often contain inline links, but few know that can be used for similar purposes. + On the internet paragraphs often contain inline links with very long text that might break, but few know that can be used for similar purposes. `, }), }; -export const Inactive: Story = { +export const WithIcons: Story = { render: (args) => ({ props: args, template: /*html*/ ` - - -
- +
+ + + +
+ +
+
+ +
+
+ +
+
+ `, + }), + args: { + linkType: "primary", + }, +}; + +export const Inactive: Story = { + render: (args) => ({ + props: { + ...args, + onClick: () => { + alert("Button clicked! (This should not appear when disabled)"); + }, + }, + template: /*html*/ ` + + Links can not be inactive + +
+
`, }), diff --git a/libs/vault/src/cipher-form/components/autofill-options/advanced-uri-option-dialog.component.ts b/libs/vault/src/cipher-form/components/autofill-options/advanced-uri-option-dialog.component.ts index 3580b1fada8..04545730172 100644 --- a/libs/vault/src/cipher-form/components/autofill-options/advanced-uri-option-dialog.component.ts +++ b/libs/vault/src/cipher-form/components/autofill-options/advanced-uri-option-dialog.component.ts @@ -3,13 +3,13 @@ import { Component, inject } from "@angular/core"; import { JslibModule } from "@bitwarden/angular/jslib.module"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { - ButtonLinkDirective, ButtonModule, + CenterPositionStrategy, DialogModule, + DialogRef, DialogService, DIALOG_DATA, - DialogRef, - CenterPositionStrategy, + LinkComponent, } from "@bitwarden/components"; export type AdvancedUriOptionDialogParams = { @@ -22,7 +22,7 @@ export type AdvancedUriOptionDialogParams = { // eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ templateUrl: "advanced-uri-option-dialog.component.html", - imports: [ButtonLinkDirective, ButtonModule, DialogModule, JslibModule], + imports: [LinkComponent, ButtonModule, DialogModule, JslibModule], }) export class AdvancedUriOptionDialogComponent { constructor(private dialogRef: DialogRef) {} diff --git a/libs/vault/src/cipher-view/cipher-view.component.html b/libs/vault/src/cipher-view/cipher-view.component.html index 3d0cc4c4414..05d2ecede72 100644 --- a/libs/vault/src/cipher-view/cipher-view.component.html +++ b/libs/vault/src/cipher-view/cipher-view.component.html @@ -12,9 +12,15 @@ - + {{ "changeAtRiskPassword" | i18n }} - diff --git a/libs/vault/src/cipher-view/cipher-view.component.ts b/libs/vault/src/cipher-view/cipher-view.component.ts index 26e3f18b542..24713d3f612 100644 --- a/libs/vault/src/cipher-view/cipher-view.component.ts +++ b/libs/vault/src/cipher-view/cipher-view.component.ts @@ -30,7 +30,7 @@ import { CalloutModule, SearchModule, TypographyModule, - AnchorLinkDirective, + LinkComponent, } from "@bitwarden/components"; import { ChangeLoginPasswordService } from "../abstractions/change-login-password.service"; @@ -66,7 +66,7 @@ import { ViewIdentitySectionsComponent } from "./view-identity-sections/view-ide ViewIdentitySectionsComponent, LoginCredentialsViewComponent, AutofillOptionsViewComponent, - AnchorLinkDirective, + LinkComponent, TypographyModule, ], }) diff --git a/libs/vault/src/cipher-view/item-details/item-details-v2.component.ts b/libs/vault/src/cipher-view/item-details/item-details-v2.component.ts index eb0e468fa4f..73e7c2706be 100644 --- a/libs/vault/src/cipher-view/item-details/item-details-v2.component.ts +++ b/libs/vault/src/cipher-view/item-details/item-details-v2.component.ts @@ -19,9 +19,9 @@ import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; import { FolderView } from "@bitwarden/common/vault/models/view/folder.view"; import { BadgeModule, - ButtonLinkDirective, CardComponent, FormFieldModule, + LinkComponent, TypographyModule, } from "@bitwarden/components"; @@ -39,7 +39,7 @@ import { OrgIconDirective } from "../../components/org-icon.directive"; TypographyModule, OrgIconDirective, FormFieldModule, - ButtonLinkDirective, + LinkComponent, BadgeModule, ], }) diff --git a/libs/vault/src/components/decryption-failure-dialog/decryption-failure-dialog.component.ts b/libs/vault/src/components/decryption-failure-dialog/decryption-failure-dialog.component.ts index 6b1a0e0d8aa..e829c003c5a 100644 --- a/libs/vault/src/components/decryption-failure-dialog/decryption-failure-dialog.component.ts +++ b/libs/vault/src/components/decryption-failure-dialog/decryption-failure-dialog.component.ts @@ -7,7 +7,7 @@ import { CipherId } from "@bitwarden/common/types/guid"; import { DIALOG_DATA, DialogRef, - AnchorLinkDirective, + LinkComponent, AsyncActionsModule, ButtonModule, DialogModule, @@ -32,7 +32,7 @@ export type DecryptionFailureDialogParams = { JslibModule, AsyncActionsModule, ButtonModule, - AnchorLinkDirective, + LinkComponent, ], }) export class DecryptionFailureDialogComponent { From a686ea1640450128591aa01bbe42de9ce5f793ec Mon Sep 17 00:00:00 2001 From: Jackson Engstrom Date: Wed, 4 Feb 2026 11:21:20 -0800 Subject: [PATCH 12/26] [PM-26706] Update search results header for extension (#18676) * dynamically changes the allItems title from 'All items' to 'Search results' based on search text length * updates logic and copy for changing the allItems header text * changes how ciphers are displayed when a user has a search term and/or filters applied * Update apps/browser/src/vault/popup/components/vault-v2/vault-v2.component.html Co-authored-by: Nick Krantz <125900171+nick-livefront@users.noreply.github.com> * refactors tests --------- Co-authored-by: Nick Krantz <125900171+nick-livefront@users.noreply.github.com> --- apps/browser/src/_locales/en/messages.json | 6 + .../vault-v2/vault-v2.component.html | 40 +++-- .../vault-v2/vault-v2.component.spec.ts | 145 +++++++++++++++++- .../components/vault-v2/vault-v2.component.ts | 5 + .../vault-popup-items.service.spec.ts | 19 +++ .../services/vault-popup-items.service.ts | 9 ++ 6 files changed, 202 insertions(+), 22 deletions(-) diff --git a/apps/browser/src/_locales/en/messages.json b/apps/browser/src/_locales/en/messages.json index 9f15bfd840f..5026f5e2799 100644 --- a/apps/browser/src/_locales/en/messages.json +++ b/apps/browser/src/_locales/en/messages.json @@ -6123,6 +6123,12 @@ "whyAmISeeingThis": { "message": "Why am I seeing this?" }, + "items": { + "message": "Items" + }, + "searchResults": { + "message": "Search results" + }, "resizeSideNavigation": { "message": "Resize side navigation" }, diff --git a/apps/browser/src/vault/popup/components/vault-v2/vault-v2.component.html b/apps/browser/src/vault/popup/components/vault-v2/vault-v2.component.html index 20871b4b134..f0a6b0d6000 100644 --- a/apps/browser/src/vault/popup/components/vault-v2/vault-v2.component.html +++ b/apps/browser/src/vault/popup/components/vault-v2/vault-v2.component.html @@ -107,20 +107,32 @@ @if (vaultState === null) { @if (!(loading$ | async)) { - - - + + @if (hasSearchText$ | async) { + + } @else { + + + + + } } } diff --git a/apps/browser/src/vault/popup/components/vault-v2/vault-v2.component.spec.ts b/apps/browser/src/vault/popup/components/vault-v2/vault-v2.component.spec.ts index d7824f3df58..a322fbc53dd 100644 --- a/apps/browser/src/vault/popup/components/vault-v2/vault-v2.component.spec.ts +++ b/apps/browser/src/vault/popup/components/vault-v2/vault-v2.component.spec.ts @@ -44,6 +44,7 @@ import { VaultPopupAutofillService } from "../../services/vault-popup-autofill.s import { VaultPopupCopyButtonsService } from "../../services/vault-popup-copy-buttons.service"; import { VaultPopupItemsService } from "../../services/vault-popup-items.service"; import { VaultPopupListFiltersService } from "../../services/vault-popup-list-filters.service"; +import { VaultPopupLoadingService } from "../../services/vault-popup-loading.service"; import { VaultPopupScrollPositionService } from "../../services/vault-popup-scroll-position.service"; import { AtRiskPasswordCalloutComponent } from "../at-risk-callout/at-risk-password-callout.component"; @@ -174,15 +175,21 @@ describe("VaultV2Component", () => { showDeactivatedOrg$: new BehaviorSubject(false), favoriteCiphers$: new BehaviorSubject([]), remainingCiphers$: new BehaviorSubject([]), + filteredCiphers$: new BehaviorSubject([]), cipherCount$: new BehaviorSubject(0), - loading$: new BehaviorSubject(true), + hasSearchText$: new BehaviorSubject(false), } as Partial; - const filtersSvc = { + const filtersSvc: any = { allFilters$: new Subject(), filters$: new BehaviorSubject({}), filterVisibilityState$: new BehaviorSubject({}), - } as Partial; + numberOfAppliedFilters$: new BehaviorSubject(0), + }; + + const loadingSvc: any = { + loading$: new BehaviorSubject(false), + }; const activeAccount$ = new BehaviorSubject({ id: "user-1" }); @@ -240,6 +247,7 @@ describe("VaultV2Component", () => { provideNoopAnimations(), { provide: VaultPopupItemsService, useValue: itemsSvc }, { provide: VaultPopupListFiltersService, useValue: filtersSvc }, + { provide: VaultPopupLoadingService, useValue: loadingSvc }, { provide: VaultPopupScrollPositionService, useValue: scrollSvc }, { provide: AccountService, @@ -366,18 +374,18 @@ describe("VaultV2Component", () => { }); it("loading$ is true when items loading or filters missing; false when both ready", () => { - const itemsLoading$ = itemsSvc.loading$ as unknown as BehaviorSubject; + const vaultLoading$ = loadingSvc.loading$ as unknown as BehaviorSubject; const allFilters$ = filtersSvc.allFilters$ as unknown as Subject; const readySubject$ = component["readySubject"] as unknown as BehaviorSubject; const values: boolean[] = []; getObs(component, "loading$").subscribe((v) => values.push(!!v)); - itemsLoading$.next(true); + vaultLoading$.next(true); allFilters$.next({}); - itemsLoading$.next(false); + vaultLoading$.next(false); readySubject$.next(true); @@ -389,7 +397,7 @@ describe("VaultV2Component", () => { const component = fixture.componentInstance; const readySubject$ = component["readySubject"] as unknown as BehaviorSubject; - const itemsLoading$ = itemsSvc.loading$ as unknown as BehaviorSubject; + const vaultLoading$ = loadingSvc.loading$ as unknown as BehaviorSubject; const allFilters$ = filtersSvc.allFilters$ as unknown as Subject; fixture.detectChanges(); @@ -400,7 +408,7 @@ describe("VaultV2Component", () => { ) as HTMLElement; // Unblock loading - itemsLoading$.next(false); + vaultLoading$.next(false); readySubject$.next(true); allFilters$.next({}); tick(); @@ -607,6 +615,127 @@ describe("VaultV2Component", () => { expect(spotlights.length).toBe(0); })); + it("does not render app-autofill-vault-list-items or favorites item container when hasSearchText$ is true", () => { + itemsSvc.hasSearchText$.next(true); + + const fixture = TestBed.createComponent(VaultV2Component); + component = fixture.componentInstance; + + const readySubject$ = component["readySubject"]; + const allFilters$ = filtersSvc.allFilters$ as unknown as Subject; + + // Unblock loading + readySubject$.next(true); + allFilters$.next({}); + fixture.detectChanges(); + + const autofillElement = fixture.debugElement.query(By.css("app-autofill-vault-list-items")); + expect(autofillElement).toBeFalsy(); + + const favoritesElement = fixture.debugElement.query(By.css("#favorites")); + expect(favoritesElement).toBeFalsy(); + }); + + it("does render app-autofill-vault-list-items and favorites item container when hasSearchText$ is false", () => { + // Ensure vaultState is null (not Empty, NoResults, or DeactivatedOrg) + itemsSvc.emptyVault$.next(false); + itemsSvc.noFilteredResults$.next(false); + itemsSvc.showDeactivatedOrg$.next(false); + itemsSvc.hasSearchText$.next(false); + loadingSvc.loading$.next(false); + + const fixture = TestBed.createComponent(VaultV2Component); + component = fixture.componentInstance; + + const readySubject$ = component["readySubject"]; + const allFilters$ = filtersSvc.allFilters$ as unknown as Subject; + + // Unblock loading + readySubject$.next(true); + allFilters$.next({}); + fixture.detectChanges(); + + const autofillElement = fixture.debugElement.query(By.css("app-autofill-vault-list-items")); + expect(autofillElement).toBeTruthy(); + + const favoritesElement = fixture.debugElement.query(By.css("#favorites")); + expect(favoritesElement).toBeTruthy(); + }); + + it("does set the title for allItems container to allItems when hasSearchText$ and numberOfAppliedFilters$ are false and 0 respectively", () => { + // Ensure vaultState is null (not Empty, NoResults, or DeactivatedOrg) + itemsSvc.emptyVault$.next(false); + itemsSvc.noFilteredResults$.next(false); + itemsSvc.showDeactivatedOrg$.next(false); + itemsSvc.hasSearchText$.next(false); + filtersSvc.numberOfAppliedFilters$.next(0); + loadingSvc.loading$.next(false); + + const fixture = TestBed.createComponent(VaultV2Component); + component = fixture.componentInstance; + + const readySubject$ = component["readySubject"]; + const allFilters$ = filtersSvc.allFilters$ as unknown as Subject; + + // Unblock loading + readySubject$.next(true); + allFilters$.next({}); + fixture.detectChanges(); + + const allItemsElement = fixture.debugElement.query(By.css("#allItems")); + const allItemsTitle = allItemsElement.componentInstance.title(); + expect(allItemsTitle).toBe("allItems"); + }); + + it("does set the title for allItems container to searchResults when hasSearchText$ is true", () => { + // Ensure vaultState is null (not Empty, NoResults, or DeactivatedOrg) + itemsSvc.emptyVault$.next(false); + itemsSvc.noFilteredResults$.next(false); + itemsSvc.showDeactivatedOrg$.next(false); + itemsSvc.hasSearchText$.next(true); + loadingSvc.loading$.next(false); + + const fixture = TestBed.createComponent(VaultV2Component); + component = fixture.componentInstance; + + const readySubject$ = component["readySubject"]; + const allFilters$ = filtersSvc.allFilters$ as unknown as Subject; + + // Unblock loading + readySubject$.next(true); + allFilters$.next({}); + fixture.detectChanges(); + + const allItemsElement = fixture.debugElement.query(By.css("#allItems")); + const allItemsTitle = allItemsElement.componentInstance.title(); + expect(allItemsTitle).toBe("searchResults"); + }); + + it("does set the title for allItems container to items when numberOfAppliedFilters$ is > 0", fakeAsync(() => { + // Ensure vaultState is null (not Empty, NoResults, or DeactivatedOrg) + itemsSvc.emptyVault$.next(false); + itemsSvc.noFilteredResults$.next(false); + itemsSvc.showDeactivatedOrg$.next(false); + itemsSvc.hasSearchText$.next(false); + filtersSvc.numberOfAppliedFilters$.next(1); + loadingSvc.loading$.next(false); + + const fixture = TestBed.createComponent(VaultV2Component); + component = fixture.componentInstance; + + const readySubject$ = component["readySubject"]; + const allFilters$ = filtersSvc.allFilters$ as unknown as Subject; + + // Unblock loading + readySubject$.next(true); + allFilters$.next({}); + fixture.detectChanges(); + + const allItemsElement = fixture.debugElement.query(By.css("#allItems")); + const allItemsTitle = allItemsElement.componentInstance.title(); + expect(allItemsTitle).toBe("items"); + })); + describe("AutoConfirmExtensionSetupDialog", () => { beforeEach(() => { autoConfirmDialogSpy.mockClear(); diff --git a/apps/browser/src/vault/popup/components/vault-v2/vault-v2.component.ts b/apps/browser/src/vault/popup/components/vault-v2/vault-v2.component.ts index 51e735fb1ef..fce084542a9 100644 --- a/apps/browser/src/vault/popup/components/vault-v2/vault-v2.component.ts +++ b/apps/browser/src/vault/popup/components/vault-v2/vault-v2.component.ts @@ -160,6 +160,11 @@ export class VaultV2Component implements OnInit, OnDestroy { FeatureFlag.BrowserPremiumSpotlight, ); + protected readonly hasSearchText$ = this.vaultPopupItemsService.hasSearchText$; + protected readonly numberOfAppliedFilters$ = + this.vaultPopupListFiltersService.numberOfAppliedFilters$; + + protected filteredCiphers$ = this.vaultPopupItemsService.filteredCiphers$; protected favoriteCiphers$ = this.vaultPopupItemsService.favoriteCiphers$; protected remainingCiphers$ = this.vaultPopupItemsService.remainingCiphers$; protected allFilters$ = this.vaultPopupListFiltersService.allFilters$; diff --git a/apps/browser/src/vault/popup/services/vault-popup-items.service.spec.ts b/apps/browser/src/vault/popup/services/vault-popup-items.service.spec.ts index 7cd73279c3d..093fdbfb66d 100644 --- a/apps/browser/src/vault/popup/services/vault-popup-items.service.spec.ts +++ b/apps/browser/src/vault/popup/services/vault-popup-items.service.spec.ts @@ -323,6 +323,25 @@ describe("VaultPopupItemsService", () => { }); }); + describe("filteredCiphers$", () => { + it("should filter filteredCipher$ down to search term", (done) => { + const cipherList = Object.values(allCiphers); + const searchText = "Login"; + + searchService.searchCiphers.mockImplementation(async () => { + return cipherList.filter((cipher) => { + return cipher.name.includes(searchText); + }); + }); + + service.filteredCiphers$.subscribe((ciphers) => { + // There are 10 ciphers but only 3 with "Login" in the name + expect(ciphers.length).toBe(3); + done(); + }); + }); + }); + describe("favoriteCiphers$", () => { it("should exclude autofill ciphers", (done) => { service.favoriteCiphers$.subscribe((ciphers) => { diff --git a/apps/browser/src/vault/popup/services/vault-popup-items.service.ts b/apps/browser/src/vault/popup/services/vault-popup-items.service.ts index 321d7936806..7ccfc834c87 100644 --- a/apps/browser/src/vault/popup/services/vault-popup-items.service.ts +++ b/apps/browser/src/vault/popup/services/vault-popup-items.service.ts @@ -201,6 +201,15 @@ export class VaultPopupItemsService { shareReplay({ refCount: true, bufferSize: 1 }), ); + /** + * List of ciphers that are filtered using filters and search. + * Includes favorite ciphers and ciphers currently suggested for autofill. + * Ciphers are sorted by name. + */ + filteredCiphers$: Observable = this._filteredCipherList$.pipe( + shareReplay({ refCount: false, bufferSize: 1 }), + ); + /** * List of ciphers that can be used for autofill on the current tab. Includes cards and/or identities * if enabled in the vault settings. Ciphers are sorted by type, then by last used date, then by name. From afc46cc50a432961a56e051df6afd0510ee3735d Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 4 Feb 2026 14:40:50 -0600 Subject: [PATCH 13/26] [deps] Vault: Update @koa/router to v15 (#18086) * [deps] Vault: Update @koa/router to v15 * update router imports from `@koa/router` * remove `@types/koa__router` no longer needed with update to `@koa/router` --------- Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Co-authored-by: Nick Krantz <125900171+nick-livefront@users.noreply.github.com> Co-authored-by: Nick Krantz --- .github/renovate.json5 | 1 - apps/cli/package.json | 2 +- apps/cli/src/commands/serve.command.ts | 4 +- apps/cli/src/oss-serve-configurator.ts | 4 +- .../bit-cli/src/bit-serve-configurator.ts | 6 +- package-lock.json | 72 ++++++++++++------- package.json | 3 +- 7 files changed, 54 insertions(+), 38 deletions(-) diff --git a/.github/renovate.json5 b/.github/renovate.json5 index 718586a9b1a..6845e5f3829 100644 --- a/.github/renovate.json5 +++ b/.github/renovate.json5 @@ -313,7 +313,6 @@ "@types/inquirer", "@types/koa", "@types/koa__multer", - "@types/koa__router", "@types/koa-bodyparser", "@types/koa-json", "@types/lunr", diff --git a/apps/cli/package.json b/apps/cli/package.json index 93658cce361..79653ec970f 100644 --- a/apps/cli/package.json +++ b/apps/cli/package.json @@ -64,7 +64,7 @@ }, "dependencies": { "@koa/multer": "4.0.0", - "@koa/router": "14.0.0", + "@koa/router": "15.2.0", "big-integer": "1.6.52", "browser-hrtime": "1.1.8", "chalk": "4.1.2", diff --git a/apps/cli/src/commands/serve.command.ts b/apps/cli/src/commands/serve.command.ts index 5bf19333f35..92632981154 100644 --- a/apps/cli/src/commands/serve.command.ts +++ b/apps/cli/src/commands/serve.command.ts @@ -1,7 +1,7 @@ import http from "node:http"; import net from "node:net"; -import * as koaRouter from "@koa/router"; +import { Router } from "@koa/router"; import { OptionValues } from "commander"; import * as koa from "koa"; import * as koaBodyParser from "koa-bodyparser"; @@ -29,7 +29,7 @@ export class ServeCommand { ); const server = new koa(); - const router = new koaRouter(); + const router = new Router(); process.env.BW_SERVE = "true"; process.env.BW_NOINTERACTION = "true"; diff --git a/apps/cli/src/oss-serve-configurator.ts b/apps/cli/src/oss-serve-configurator.ts index e0385534cb7..fbf3c778725 100644 --- a/apps/cli/src/oss-serve-configurator.ts +++ b/apps/cli/src/oss-serve-configurator.ts @@ -1,7 +1,7 @@ // FIXME: Update this file to be type safe and remove this and next line // @ts-strict-ignore import * as koaMulter from "@koa/multer"; -import * as koaRouter from "@koa/router"; +import { Router } from "@koa/router"; import * as koa from "koa"; import { firstValueFrom, map } from "rxjs"; @@ -218,7 +218,7 @@ export class OssServeConfigurator { ); } - async configureRouter(router: koaRouter) { + async configureRouter(router: Router) { router.get("/generate", async (ctx, next) => { const response = await this.generateCommand.run(ctx.request.query); this.processResponse(ctx.response, response); diff --git a/bitwarden_license/bit-cli/src/bit-serve-configurator.ts b/bitwarden_license/bit-cli/src/bit-serve-configurator.ts index 71df651d9d0..5a00dccb59b 100644 --- a/bitwarden_license/bit-cli/src/bit-serve-configurator.ts +++ b/bitwarden_license/bit-cli/src/bit-serve-configurator.ts @@ -1,4 +1,4 @@ -import * as koaRouter from "@koa/router"; +import { Router } from "@koa/router"; import { OssServeConfigurator } from "@bitwarden/cli/oss-serve-configurator"; @@ -16,7 +16,7 @@ export class BitServeConfigurator extends OssServeConfigurator { super(serviceContainer); } - override async configureRouter(router: koaRouter): Promise { + override async configureRouter(router: Router): Promise { // Register OSS endpoints await super.configureRouter(router); @@ -24,7 +24,7 @@ export class BitServeConfigurator extends OssServeConfigurator { this.serveDeviceApprovals(router); } - private serveDeviceApprovals(router: koaRouter) { + private serveDeviceApprovals(router: Router) { router.get("/device-approval/:organizationId", async (ctx, next) => { if (await this.errorIfLocked(ctx.response)) { await next(); diff --git a/package-lock.json b/package-lock.json index 6352675d718..752f186e970 100644 --- a/package-lock.json +++ b/package-lock.json @@ -28,7 +28,7 @@ "@electron/fuses": "1.8.0", "@emotion/css": "11.13.5", "@koa/multer": "4.0.0", - "@koa/router": "14.0.0", + "@koa/router": "15.2.0", "@microsoft/signalr": "8.0.7", "@microsoft/signalr-protocol-msgpack": "8.0.7", "@ng-select/ng-select": "20.7.0", @@ -104,7 +104,6 @@ "@types/jsdom": "21.1.7", "@types/koa": "3.0.1", "@types/koa__multer": "2.0.7", - "@types/koa__router": "12.0.4", "@types/koa-bodyparser": "4.3.7", "@types/koa-json": "2.0.24", "@types/lowdb": "1.0.15", @@ -200,7 +199,7 @@ "license": "SEE LICENSE IN LICENSE.txt", "dependencies": { "@koa/multer": "4.0.0", - "@koa/router": "14.0.0", + "@koa/router": "15.2.0", "big-integer": "1.6.52", "browser-hrtime": "1.1.8", "chalk": "4.1.2", @@ -8746,18 +8745,46 @@ } }, "node_modules/@koa/router": { - "version": "14.0.0", - "resolved": "https://registry.npmjs.org/@koa/router/-/router-14.0.0.tgz", - "integrity": "sha512-LBSu5K0qAaaQcXX/0WIB9PGDevyCxxpnc1uq13vV/CgObaVxuis5hKl3Eboq/8gcb6ebnkAStW9NB/Em2eYyFA==", + "version": "15.2.0", + "resolved": "https://registry.npmjs.org/@koa/router/-/router-15.2.0.tgz", + "integrity": "sha512-7YUhq4W83cybfNa4E7JqJpWzoCTSvbnFltkvRaUaUX1ybFzlUoLNY1SqT8XmIAO6nGbFrev+FvJHw4mL+4WhuQ==", "license": "MIT", "dependencies": { - "debug": "^4.4.1", - "http-errors": "^2.0.0", + "debug": "^4.4.3", + "http-errors": "^2.0.1", "koa-compose": "^4.1.0", - "path-to-regexp": "^8.2.0" + "path-to-regexp": "^8.3.0" }, "engines": { "node": ">= 20" + }, + "peerDependencies": { + "koa": "^2.0.0 || ^3.0.0" + }, + "peerDependenciesMeta": { + "koa": { + "optional": false + } + } + }, + "node_modules/@koa/router/node_modules/http-errors": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", + "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", + "license": "MIT", + "dependencies": { + "depd": "~2.0.0", + "inherits": "~2.0.4", + "setprototypeof": "~1.2.0", + "statuses": "~2.0.2", + "toidentifier": "~1.0.1" + }, + "engines": { + "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, "node_modules/@leichtgewicht/ip-codec": { @@ -15735,16 +15762,6 @@ "@types/koa": "*" } }, - "node_modules/@types/koa__router": { - "version": "12.0.4", - "resolved": "https://registry.npmjs.org/@types/koa__router/-/koa__router-12.0.4.tgz", - "integrity": "sha512-Y7YBbSmfXZpa/m5UGGzb7XadJIRBRnwNY9cdAojZGp65Cpe5MAP3mOZE7e3bImt8dfKS4UFcR16SLH8L/z7PBw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/koa": "*" - } - }, "node_modules/@types/koa-bodyparser": { "version": "4.3.7", "resolved": "https://registry.npmjs.org/@types/koa-bodyparser/-/koa-bodyparser-4.3.7.tgz", @@ -21479,9 +21496,9 @@ } }, "node_modules/debug": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", - "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", "license": "MIT", "dependencies": { "ms": "^2.1.3" @@ -35514,12 +35531,13 @@ } }, "node_modules/path-to-regexp": { - "version": "8.2.0", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.2.0.tgz", - "integrity": "sha512-TdrF7fW9Rphjq4RjrW0Kp2AW0Ahwu9sRGTkS6bvDi0SCwZlEZYmcfDbEsTz8RVk0EHIS/Vd1bv3JhG+1xZuAyQ==", + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.3.0.tgz", + "integrity": "sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA==", "license": "MIT", - "engines": { - "node": ">=16" + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, "node_modules/path-type": { diff --git a/package.json b/package.json index 2cc60c08c9d..ea7dc32820c 100644 --- a/package.json +++ b/package.json @@ -71,7 +71,6 @@ "@types/jsdom": "21.1.7", "@types/koa": "3.0.1", "@types/koa__multer": "2.0.7", - "@types/koa__router": "12.0.4", "@types/koa-bodyparser": "4.3.7", "@types/koa-json": "2.0.24", "@types/lowdb": "1.0.15", @@ -167,7 +166,7 @@ "@electron/fuses": "1.8.0", "@emotion/css": "11.13.5", "@koa/multer": "4.0.0", - "@koa/router": "14.0.0", + "@koa/router": "15.2.0", "@microsoft/signalr": "8.0.7", "@microsoft/signalr-protocol-msgpack": "8.0.7", "@ng-select/ng-select": "20.7.0", From 34108d93e40b2238931dc35089702df889aada0f Mon Sep 17 00:00:00 2001 From: neuronull <9162534+neuronull@users.noreply.github.com> Date: Wed, 4 Feb 2026 13:01:18 -0800 Subject: [PATCH 14/26] SSH Agent v2: Add ssh key primitive types (#18583) Co-authored-by: Bernd Schoolmann --- .github/CODEOWNERS | 4 +- apps/desktop/desktop_native/Cargo.lock | 16 +- apps/desktop/desktop_native/Cargo.toml | 1 + .../desktop_native/ssh_agent/Cargo.toml | 19 ++ .../ssh_agent/src/crypto/mod.rs | 184 ++++++++++++++++++ .../desktop_native/ssh_agent/src/lib.rs | 7 + 6 files changed, 227 insertions(+), 4 deletions(-) create mode 100644 apps/desktop/desktop_native/ssh_agent/Cargo.toml create mode 100644 apps/desktop/desktop_native/ssh_agent/src/crypto/mod.rs create mode 100644 apps/desktop/desktop_native/ssh_agent/src/lib.rs diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 8bb15d37fdf..2582b96961d 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -156,6 +156,8 @@ apps/desktop/macos/autofill-extension @bitwarden/team-autofill-desktop-dev apps/desktop/src/app/components/fido2placeholder.component.ts @bitwarden/team-autofill-desktop-dev apps/desktop/desktop_native/windows_plugin_authenticator @bitwarden/team-autofill-desktop-dev apps/desktop/desktop_native/autotype @bitwarden/team-autofill-desktop-dev +apps/desktop/desktop_native/core/src/ssh_agent @bitwarden/team-autofill-desktop-dev @bitwarden/wg-ssh-keys +apps/desktop/desktop_native/ssh_agent @bitwarden/team-autofill-desktop-dev @bitwarden/wg-ssh-keys apps/desktop/desktop_native/napi/src/autofill.rs @bitwarden/team-autofill-desktop-dev apps/desktop/desktop_native/napi/src/autotype.rs @bitwarden/team-autofill-desktop-dev apps/desktop/desktop_native/napi/src/sshagent.rs @bitwarden/team-autofill-desktop-dev @@ -164,8 +166,6 @@ apps/desktop/native-messaging-test-runner @bitwarden/team-autofill-desktop-dev apps/desktop/src/services/duckduckgo-message-handler.service.ts @bitwarden/team-autofill-desktop-dev apps/desktop/src/services/encrypted-message-handler.service.ts @bitwarden/team-autofill-desktop-dev .github/workflows/alert-ddg-files-modified.yml @bitwarden/team-autofill-desktop-dev -# SSH Agent -apps/desktop/desktop_native/core/src/ssh_agent @bitwarden/team-autofill-desktop-dev @bitwarden/wg-ssh-keys ## UI Foundation ## .github/workflows/chromatic.yml @bitwarden/team-ui-foundation diff --git a/apps/desktop/desktop_native/Cargo.lock b/apps/desktop/desktop_native/Cargo.lock index e5c197ef51c..7154c42ac89 100644 --- a/apps/desktop/desktop_native/Cargo.lock +++ b/apps/desktop/desktop_native/Cargo.lock @@ -377,9 +377,9 @@ checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" [[package]] name = "base64ct" -version = "1.7.3" +version = "1.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89e25b6adfb930f02d1981565a6e5d9c547ac15a96606256d3b59040e5cd4ca3" +checksum = "2af50177e190e07a26ab74f8b1efbfe2ef87da2116221318cb1c2e82baf7de06" [[package]] name = "basic-toml" @@ -3295,6 +3295,9 @@ dependencies = [ "bcrypt-pbkdf", "ed25519-dalek", "num-bigint-dig", + "p256", + "p384", + "p521", "rand_core 0.6.4", "rsa", "sec1", @@ -3306,6 +3309,15 @@ dependencies = [ "zeroize", ] +[[package]] +name = "ssh_agent" +version = "0.0.0" +dependencies = [ + "anyhow", + "base64", + "ssh-key", +] + [[package]] name = "stable_deref_trait" version = "1.2.0" diff --git a/apps/desktop/desktop_native/Cargo.toml b/apps/desktop/desktop_native/Cargo.toml index b3fac851026..09a4d603327 100644 --- a/apps/desktop/desktop_native/Cargo.toml +++ b/apps/desktop/desktop_native/Cargo.toml @@ -9,6 +9,7 @@ members = [ "napi", "process_isolation", "proxy", + "ssh_agent", "windows_plugin_authenticator", ] diff --git a/apps/desktop/desktop_native/ssh_agent/Cargo.toml b/apps/desktop/desktop_native/ssh_agent/Cargo.toml new file mode 100644 index 00000000000..becab28c356 --- /dev/null +++ b/apps/desktop/desktop_native/ssh_agent/Cargo.toml @@ -0,0 +1,19 @@ +[package] +name = "ssh_agent" +edition = { workspace = true } +license = { workspace = true } +version = { workspace = true } +publish = { workspace = true } + +[dependencies] +anyhow = { workspace = true } +base64 = { workspace = true } +ssh-key = { version = "=0.6.7", features = [ + "encryption", + "ed25519", + "rsa", + "rand_core", +] } + +[lints] +workspace = true diff --git a/apps/desktop/desktop_native/ssh_agent/src/crypto/mod.rs b/apps/desktop/desktop_native/ssh_agent/src/crypto/mod.rs new file mode 100644 index 00000000000..655e440dc78 --- /dev/null +++ b/apps/desktop/desktop_native/ssh_agent/src/crypto/mod.rs @@ -0,0 +1,184 @@ +//! Cryptographic key management for the SSH agent. +//! +//! This module provides the core primitive types and functionality for managing +//! SSH keys in the Bitwarden SSH agent. +//! +//! # Supported signing algorithms +//! +//! - Ed25519 +//! - RSA +//! +//! ECDSA keys are not currently supported (PM-29894) + +use std::fmt; + +use anyhow::anyhow; +use ssh_key::private::{Ed25519Keypair, RsaKeypair}; + +/// Represents an SSH key and its associated metadata. +#[derive(Clone)] +pub(crate) struct SSHKeyData { + /// Private key of the key pair + private_key: PrivateKey, + /// Public key of the key pair + public_key: PublicKey, + /// Human-readable name + name: String, + /// Vault cipher ID associated with the key pair + cipher_id: String, +} + +impl SSHKeyData { + /// Creates a new `SSHKeyData` instance. + /// + /// # Arguments + /// + /// * `private_key` - The private key component + /// * `public_key` - The public key component + /// * `name` - A human-readable name for the key + /// * `cipher_id` - The vault cipher identifier associated with this key + pub(crate) fn new( + private_key: PrivateKey, + public_key: PublicKey, + name: String, + cipher_id: String, + ) -> Self { + Self { + private_key, + public_key, + name, + cipher_id, + } + } + + /// # Returns + /// + /// A reference to the [`PublicKey`]. + pub(crate) fn public_key(&self) -> &PublicKey { + &self.public_key + } + + /// # Returns + /// + /// A reference to the [`PrivateKey`]. + pub(crate) fn private_key(&self) -> &PrivateKey { + &self.private_key + } + + /// # Returns + /// + /// A reference to the human-readable name for this key. + pub(crate) fn name(&self) -> &String { + &self.name + } + + /// # Returns + /// + /// A reference to the cipher ID that links this key to a vault entry. + pub(crate) fn cipher_id(&self) -> &String { + &self.cipher_id + } +} + +/// Represents an SSH private key. +#[derive(Clone, PartialEq, Debug)] +pub(crate) enum PrivateKey { + Ed25519(Ed25519Keypair), + Rsa(RsaKeypair), +} + +impl TryFrom for PrivateKey { + type Error = anyhow::Error; + + fn try_from(key: ssh_key::private::PrivateKey) -> Result { + match key.algorithm() { + ssh_key::Algorithm::Ed25519 => Ok(Self::Ed25519( + key.key_data() + .ed25519() + .ok_or(anyhow!("Failed to parse ed25519 key"))? + .to_owned(), + )), + ssh_key::Algorithm::Rsa { hash: _ } => Ok(Self::Rsa( + key.key_data() + .rsa() + .ok_or(anyhow!("Failed to parse RSA key"))? + .to_owned(), + )), + _ => Err(anyhow!("Unsupported key type")), + } + } +} + +/// Represents an SSH public key. +/// +/// Contains the algorithm identifier (e.g., "ssh-ed25519", "ssh-rsa") +/// and the binary blob of the public key data. +#[derive(Clone, Ord, Eq, PartialOrd, PartialEq)] +pub(crate) struct PublicKey { + pub alg: String, + pub blob: Vec, +} + +impl PublicKey { + pub(crate) fn alg(&self) -> &str { + &self.alg + } + + pub(crate) fn blob(&self) -> &[u8] { + &self.blob + } +} + +impl fmt::Debug for PublicKey { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "PublicKey(\"{self}\")") + } +} + +impl fmt::Display for PublicKey { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + use base64::{prelude::BASE64_STANDARD, Engine as _}; + + write!(f, "{} {}", self.alg(), BASE64_STANDARD.encode(self.blob())) + } +} + +#[cfg(test)] +mod tests { + use ssh_key::{ + private::{Ed25519Keypair, RsaKeypair}, + rand_core::OsRng, + LineEnding, + }; + + use super::*; + + const MIN_KEY_BIT_SIZE: usize = 2048; + + fn create_valid_ed25519_key_string() -> String { + let ed25519_keypair = Ed25519Keypair::random(&mut OsRng); + let ssh_key = + ssh_key::PrivateKey::new(ssh_key::private::KeypairData::Ed25519(ed25519_keypair), "") + .unwrap(); + ssh_key.to_openssh(LineEnding::LF).unwrap().to_string() + } + + #[test] + fn test_privatekey_from_ed25519() { + let key_string = create_valid_ed25519_key_string(); + let ssh_key = ssh_key::PrivateKey::from_openssh(&key_string).unwrap(); + + let private_key = PrivateKey::try_from(ssh_key).unwrap(); + assert!(matches!(private_key, PrivateKey::Ed25519(_))); + } + + #[test] + fn test_privatekey_from_rsa() { + let rsa_keypair = RsaKeypair::random(&mut OsRng, MIN_KEY_BIT_SIZE).unwrap(); + let ssh_key = + ssh_key::PrivateKey::new(ssh_key::private::KeypairData::Rsa(rsa_keypair), "").unwrap(); + + let private_key = PrivateKey::try_from(ssh_key).unwrap(); + assert!(matches!(private_key, PrivateKey::Rsa(_))); + } +} diff --git a/apps/desktop/desktop_native/ssh_agent/src/lib.rs b/apps/desktop/desktop_native/ssh_agent/src/lib.rs new file mode 100644 index 00000000000..aaaf635e6c6 --- /dev/null +++ b/apps/desktop/desktop_native/ssh_agent/src/lib.rs @@ -0,0 +1,7 @@ +//! Bitwarden SSH Agent implementation +//! +//! + +#![allow(dead_code)] // TODO remove when all code is used in follow-up PR + +mod crypto; From 04d2394dbfeb9480456f89d0afcd0888e08a8b11 Mon Sep 17 00:00:00 2001 From: Vedant Madane <6527493+VedantMadane@users.noreply.github.com> Date: Thu, 5 Feb 2026 03:08:25 +0530 Subject: [PATCH 15/26] [PM-30845] fix(vault): preserve card brand when editing existing card (#18381) * fix(vault): preserve card brand when editing existing card Fixes #16978 The brand field was not being restored when editing an existing card cipher, causing it to show '--Select--' and potentially lose the brand data when saving. Added the brand field to initFromExistingCipher() to properly restore the card brand when opening a card for editing. Also updated the test to verify all card fields including brand, expMonth, and expYear are properly initialized from existing cipher data. * fix: add brand to OptionalInitialValues interface Addresses review feedback from @jengstrom-bw in PR #18381. The brand field was being used in card-details-section.component.ts but wasn't defined in the OptionalInitialValues type, causing a TypeScript compilation error. Adds brand?: string; to the Credit Card Information section of OptionalInitialValues in cipher-form-config.service.ts. * test: add coverage for initFromExistingCipher brand logic --- .../cipher-form-config.service.ts | 1 + .../card-details-section.component.spec.ts | 34 +++++++++++++++++-- .../card-details-section.component.ts | 1 + 3 files changed, 34 insertions(+), 2 deletions(-) diff --git a/libs/vault/src/cipher-form/abstractions/cipher-form-config.service.ts b/libs/vault/src/cipher-form/abstractions/cipher-form-config.service.ts index 35d3d8725ff..a4aabbb6f19 100644 --- a/libs/vault/src/cipher-form/abstractions/cipher-form-config.service.ts +++ b/libs/vault/src/cipher-form/abstractions/cipher-form-config.service.ts @@ -28,6 +28,7 @@ export type OptionalInitialValues = { // Credit Card Information cardholderName?: string; number?: string; + brand?: string; expMonth?: string; expYear?: string; code?: string; diff --git a/libs/vault/src/cipher-form/components/card-details-section/card-details-section.component.spec.ts b/libs/vault/src/cipher-form/components/card-details-section/card-details-section.component.spec.ts index 4b0cd0f5f90..650b4e29fe5 100644 --- a/libs/vault/src/cipher-form/components/card-details-section/card-details-section.component.spec.ts +++ b/libs/vault/src/cipher-form/components/card-details-section/card-details-section.component.spec.ts @@ -108,12 +108,17 @@ describe("CardDetailsSectionComponent", () => { const cardholderName = "Ron Burgundy"; const number = "4242 4242 4242 4242"; const code = "619"; + const brand = "Maestro"; + const expMonth = "5"; + const expYear = "2028"; const cardView = new CardView(); cardView.cardholderName = cardholderName; cardView.number = number; cardView.code = code; - cardView.brand = "Visa"; + cardView.brand = brand; + cardView.expMonth = expMonth; + cardView.expYear = expYear; getInitialCipherView.mockReturnValueOnce({ card: cardView }); @@ -123,7 +128,9 @@ describe("CardDetailsSectionComponent", () => { cardholderName, number, code, - brand: cardView.brand, + brand, + expMonth, + expYear, }); }); @@ -154,4 +161,27 @@ describe("CardDetailsSectionComponent", () => { expect(heading.nativeElement.textContent.trim()).toBe("cardDetails"); }); + + it("initializes `cardDetailsForm` from `initialValues` when provided and editing existing cipher", () => { + const initialCardholderName = "New Name"; + const initialBrand = "Amex"; + + (cipherFormProvider as any).config = { + initialValues: { + cardholderName: initialCardholderName, + brand: initialBrand, + }, + }; + + const existingCard = new CardView(); + existingCard.cardholderName = "Old Name"; + existingCard.brand = "Visa"; + + getInitialCipherView.mockReturnValueOnce({ card: existingCard }); + + component.ngOnInit(); + + expect(component.cardDetailsForm.value.cardholderName).toBe(initialCardholderName); + expect(component.cardDetailsForm.value.brand).toBe(initialBrand); + }); }); diff --git a/libs/vault/src/cipher-form/components/card-details-section/card-details-section.component.ts b/libs/vault/src/cipher-form/components/card-details-section/card-details-section.component.ts index 5fa8d0af131..056b93b6b99 100644 --- a/libs/vault/src/cipher-form/components/card-details-section/card-details-section.component.ts +++ b/libs/vault/src/cipher-form/components/card-details-section/card-details-section.component.ts @@ -158,6 +158,7 @@ export class CardDetailsSectionComponent implements OnInit { this.cardDetailsForm.patchValue({ cardholderName: this.initialValues?.cardholderName ?? existingCard.cardholderName, number: this.initialValues?.number ?? existingCard.number, + brand: this.initialValues?.brand ?? existingCard.brand, expMonth: this.initialValues?.expMonth ?? existingCard.expMonth, expYear: this.initialValues?.expYear ?? existingCard.expYear, code: this.initialValues?.code ?? existingCard.code, From f457abf60bca725b95f0f38bdeef98fc2be0e4d5 Mon Sep 17 00:00:00 2001 From: Thomas Avery <43214426+Thomas-Avery@users.noreply.github.com> Date: Thu, 5 Feb 2026 03:57:10 -0600 Subject: [PATCH 16/26] Add contact info to HAZMAT (#18759) --- .../abstractions/crypto-function.service.ts | 20 +++++++++---------- .../key-generation/key-generation.service.ts | 8 ++++---- 2 files changed, 14 insertions(+), 14 deletions(-) diff --git a/libs/common/src/key-management/crypto/abstractions/crypto-function.service.ts b/libs/common/src/key-management/crypto/abstractions/crypto-function.service.ts index b16371198b3..645666c582d 100644 --- a/libs/common/src/key-management/crypto/abstractions/crypto-function.service.ts +++ b/libs/common/src/key-management/crypto/abstractions/crypto-function.service.ts @@ -7,7 +7,7 @@ import { CsprngArray } from "../../../types/csprng"; export abstract class CryptoFunctionService { /** - * @deprecated HAZMAT WARNING: DO NOT USE THIS FOR NEW CODE. Implement low-level crypto operations + * @deprecated HAZMAT WARNING: DO NOT USE THIS FOR NEW CODE. CONTACT KEY MANAGEMENT IF YOU THINK YOU NEED TO. Implement low-level crypto operations * in the SDK instead. Further, you should probably never find yourself using this low-level crypto function. */ abstract pbkdf2( @@ -17,7 +17,7 @@ export abstract class CryptoFunctionService { iterations: number, ): Promise; /** - * @deprecated HAZMAT WARNING: DO NOT USE THIS FOR NEW CODE. Implement low-level crypto operations + * @deprecated HAZMAT WARNING: DO NOT USE THIS FOR NEW CODE. CONTACT KEY MANAGEMENT IF YOU THINK YOU NEED TO. Implement low-level crypto operations * in the SDK instead. Further, you should probably never find yourself using this low-level crypto function. */ abstract hkdf( @@ -28,7 +28,7 @@ export abstract class CryptoFunctionService { algorithm: "sha256" | "sha512", ): Promise; /** - * @deprecated HAZMAT WARNING: DO NOT USE THIS FOR NEW CODE. Implement low-level crypto operations + * @deprecated HAZMAT WARNING: DO NOT USE THIS FOR NEW CODE. CONTACT KEY MANAGEMENT IF YOU THINK YOU NEED TO. Implement low-level crypto operations * in the SDK instead. Further, you should probably never find yourself using this low-level crypto function. */ abstract hkdfExpand( @@ -38,7 +38,7 @@ export abstract class CryptoFunctionService { algorithm: "sha256" | "sha512", ): Promise; /** - * @deprecated HAZMAT WARNING: DO NOT USE THIS FOR NEW CODE. Implement low-level crypto operations + * @deprecated HAZMAT WARNING: DO NOT USE THIS FOR NEW CODE. CONTACT KEY MANAGEMENT IF YOU THINK YOU NEED TO. Implement low-level crypto operations * in the SDK instead. Further, you should probably never find yourself using this low-level crypto function. */ abstract hash( @@ -46,7 +46,7 @@ export abstract class CryptoFunctionService { algorithm: "sha1" | "sha256" | "sha512" | "md5", ): Promise; /** - * @deprecated HAZMAT WARNING: DO NOT USE THIS FOR NEW CODE. Implement low-level crypto operations + * @deprecated HAZMAT WARNING: DO NOT USE THIS FOR NEW CODE. CONTACT KEY MANAGEMENT IF YOU THINK YOU NEED TO. Implement low-level crypto operations * in the SDK instead. Further, you should probably never find yourself using this low-level crypto function. */ abstract hmacFast( @@ -56,7 +56,7 @@ export abstract class CryptoFunctionService { ): Promise; abstract compareFast(a: Uint8Array | string, b: Uint8Array | string): Promise; /** - * @deprecated HAZMAT WARNING: DO NOT USE THIS FOR NEW CODE. Implement low-level crypto operations + * @deprecated HAZMAT WARNING: DO NOT USE THIS FOR NEW CODE. CONTACT KEY MANAGEMENT IF YOU THINK YOU NEED TO. Implement low-level crypto operations * in the SDK instead. Further, you should probably never find yourself using this low-level crypto function. */ abstract aesDecryptFastParameters( @@ -66,7 +66,7 @@ export abstract class CryptoFunctionService { key: SymmetricCryptoKey, ): CbcDecryptParameters; /** - * @deprecated HAZMAT WARNING: DO NOT USE THIS FOR NEW CODE. Implement low-level crypto operations + * @deprecated HAZMAT WARNING: DO NOT USE THIS FOR NEW CODE. CONTACT KEY MANAGEMENT IF YOU THINK YOU NEED TO. Implement low-level crypto operations * in the SDK instead. Further, you should probably never find yourself using this low-level crypto function. */ abstract aesDecryptFast({ @@ -76,7 +76,7 @@ export abstract class CryptoFunctionService { | { mode: "cbc"; parameters: CbcDecryptParameters } | { mode: "ecb"; parameters: EcbDecryptParameters }): Promise; /** - * @deprecated HAZMAT WARNING: DO NOT USE THIS FOR NEW CODE. Only used by DDG integration until DDG uses PKCS#7 padding, and by lastpass importer. + * @deprecated HAZMAT WARNING: DO NOT USE THIS FOR NEW CODE. CONTACT KEY MANAGEMENT IF YOU THINK YOU NEED TO. Only used by DDG integration until DDG uses PKCS#7 padding, and by lastpass importer. */ abstract aesDecrypt( data: Uint8Array, @@ -85,7 +85,7 @@ export abstract class CryptoFunctionService { mode: "cbc" | "ecb", ): Promise; /** - * @deprecated HAZMAT WARNING: DO NOT USE THIS FOR NEW CODE. Implement low-level crypto operations + * @deprecated HAZMAT WARNING: DO NOT USE THIS FOR NEW CODE. CONTACT KEY MANAGEMENT IF YOU THINK YOU NEED TO. Implement low-level crypto operations * in the SDK instead. Further, you should probably never find yourself using this low-level crypto function. */ abstract rsaEncrypt( @@ -94,7 +94,7 @@ export abstract class CryptoFunctionService { algorithm: "sha1", ): Promise; /** - * @deprecated HAZMAT WARNING: DO NOT USE THIS FOR NEW CODE. Implement low-level crypto operations + * @deprecated HAZMAT WARNING: DO NOT USE THIS FOR NEW CODE. CONTACT KEY MANAGEMENT IF YOU THINK YOU NEED TO. Implement low-level crypto operations * in the SDK instead. Further, you should probably never find yourself using this low-level crypto function. */ abstract rsaDecrypt( diff --git a/libs/common/src/key-management/crypto/key-generation/key-generation.service.ts b/libs/common/src/key-management/crypto/key-generation/key-generation.service.ts index ddc3829aa9f..1114e892bb8 100644 --- a/libs/common/src/key-management/crypto/key-generation/key-generation.service.ts +++ b/libs/common/src/key-management/crypto/key-generation/key-generation.service.ts @@ -27,7 +27,7 @@ export abstract class KeyGenerationService { * Uses HKDF, see {@link https://datatracker.ietf.org/doc/html/rfc5869 RFC 5869} * for details. * - * @deprecated HAZMAT WARNING: DO NOT USE THIS FOR NEW CODE. This is a low-level cryptographic function. + * @deprecated HAZMAT WARNING: DO NOT USE THIS FOR NEW CODE. CONTACT KEY MANAGEMENT IF YOU THINK YOU NEED TO. This is a low-level cryptographic function. * New functionality should not be built on top of it, and instead should be built in the sdk. * * @param bitLength Length of key material. @@ -44,7 +44,7 @@ export abstract class KeyGenerationService { /** * Derives a 64 byte key from key material. * - * @deprecated HAZMAT WARNING: DO NOT USE THIS FOR NEW CODE. This is a low-level cryptographic function. + * @deprecated HAZMAT WARNING: DO NOT USE THIS FOR NEW CODE. CONTACT KEY MANAGEMENT IF YOU THINK YOU NEED TO. This is a low-level cryptographic function. * New functionality should not be built on top of it, and instead should be built in the sdk. * * @remark The key material should be generated from {@link createKey}, or {@link createKeyWithPurpose}. @@ -63,7 +63,7 @@ export abstract class KeyGenerationService { /** * Derives a 32 byte key from a password using a key derivation function. * - * @deprecated HAZMAT WARNING: DO NOT USE THIS FOR NEW CODE. This is a low-level cryptographic function. + * @deprecated HAZMAT WARNING: DO NOT USE THIS FOR NEW CODE. CONTACT KEY MANAGEMENT IF YOU THINK YOU NEED TO. This is a low-level cryptographic function. * New functionality should not be built on top of it, and instead should be built in the sdk. * * @param password Password to derive the key from. @@ -80,7 +80,7 @@ export abstract class KeyGenerationService { /** * Derives a 64 byte key from a 32 byte key using a key derivation function. * - * @deprecated HAZMAT WARNING: DO NOT USE THIS FOR NEW CODE. This is a low-level cryptographic function. + * @deprecated HAZMAT WARNING: DO NOT USE THIS FOR NEW CODE. CONTACT KEY MANAGEMENT IF YOU THINK YOU NEED TO. This is a low-level cryptographic function. * New functionality should not be built on top of it, and instead should be built in the sdk. * * @param key 32 byte key. From ba905dbf1200f43b8d008659d391991bb56a73ef Mon Sep 17 00:00:00 2001 From: Jared McCannon Date: Thu, 5 Feb 2026 08:03:09 -0600 Subject: [PATCH 17/26] Fixing bulk restore request property name to match server. (#18757) --- .../requests/organization-user-bulk-restore.request.ts | 6 +++--- .../services/default-organization-user.service.spec.ts | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/libs/admin-console/src/common/organization-user/models/requests/organization-user-bulk-restore.request.ts b/libs/admin-console/src/common/organization-user/models/requests/organization-user-bulk-restore.request.ts index 74a91897a58..f2b51d6747a 100644 --- a/libs/admin-console/src/common/organization-user/models/requests/organization-user-bulk-restore.request.ts +++ b/libs/admin-console/src/common/organization-user/models/requests/organization-user-bulk-restore.request.ts @@ -1,11 +1,11 @@ import { EncString } from "@bitwarden/sdk-internal"; export class OrganizationUserBulkRestoreRequest { - userIds: string[]; + ids: string[]; defaultUserCollectionName: EncString | undefined; - constructor(userIds: string[], defaultUserCollectionName?: EncString) { - this.userIds = userIds; + constructor(ids: string[], defaultUserCollectionName?: EncString) { + this.ids = ids; this.defaultUserCollectionName = defaultUserCollectionName; } } diff --git a/libs/admin-console/src/common/organization-user/services/default-organization-user.service.spec.ts b/libs/admin-console/src/common/organization-user/services/default-organization-user.service.spec.ts index 0448b23e4d2..7eca35fd36e 100644 --- a/libs/admin-console/src/common/organization-user/services/default-organization-user.service.spec.ts +++ b/libs/admin-console/src/common/organization-user/services/default-organization-user.service.spec.ts @@ -258,7 +258,7 @@ describe("DefaultOrganizationUserService", () => { ).toHaveBeenCalledWith( mockOrganization.id, expect.objectContaining({ - userIds: mockUserIds, + ids: mockUserIds, defaultUserCollectionName: mockEncryptedCollectionName.encryptedString, }), ); From 2cc87157115b27d34cbd97b84d9ef29569ebe609 Mon Sep 17 00:00:00 2001 From: Bernd Schoolmann Date: Thu, 5 Feb 2026 15:10:15 +0100 Subject: [PATCH 18/26] [PM-31640] Fix SDK tracing / cipher decryption performance issues (#18777) * Fix SDK tracing performance issues * Update package lock * Update package lock * Fix npm lock --- package-lock.json | 18 ++++++++++-------- package.json | 4 ++-- 2 files changed, 12 insertions(+), 10 deletions(-) diff --git a/package-lock.json b/package-lock.json index 752f186e970..d899e4479d5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -23,8 +23,8 @@ "@angular/platform-browser": "20.3.16", "@angular/platform-browser-dynamic": "20.3.16", "@angular/router": "20.3.16", - "@bitwarden/commercial-sdk-internal": "0.2.0-main.506", - "@bitwarden/sdk-internal": "0.2.0-main.506", + "@bitwarden/commercial-sdk-internal": "0.2.0-main.522", + "@bitwarden/sdk-internal": "0.2.0-main.522", "@electron/fuses": "1.8.0", "@emotion/css": "11.13.5", "@koa/multer": "4.0.0", @@ -4981,9 +4981,10 @@ "link": true }, "node_modules/@bitwarden/commercial-sdk-internal": { - "version": "0.2.0-main.506", - "resolved": "https://registry.npmjs.org/@bitwarden/commercial-sdk-internal/-/commercial-sdk-internal-0.2.0-main.506.tgz", - "integrity": "sha512-aRzcxOcj8vXxz0jN3q2xxj26zxBfjg3oRm5QXbWE7zXJ2PGrgxTaePca9pQYYpwgr7iufYMnZcq5dH+qttNEmA==", + "version": "0.2.0-main.522", + "resolved": "https://registry.npmjs.org/@bitwarden/commercial-sdk-internal/-/commercial-sdk-internal-0.2.0-main.522.tgz", + "integrity": "sha512-2wAbg30cGlDhSj14LaK2/ISuT91XPVeNgL/PU+eoxLhAehGKjAXdvZN3PSwFaAuaMbEFzlESvqC1pzzO4p/1zw==", + "license": "BITWARDEN SOFTWARE DEVELOPMENT KIT LICENSE AGREEMENT", "dependencies": { "type-fest": "^4.41.0" } @@ -5085,9 +5086,10 @@ "link": true }, "node_modules/@bitwarden/sdk-internal": { - "version": "0.2.0-main.506", - "resolved": "https://registry.npmjs.org/@bitwarden/sdk-internal/-/sdk-internal-0.2.0-main.506.tgz", - "integrity": "sha512-BbTSU5Acx74Hr32zDj2kV8sbdclyvdIti5t6kXnCvJmA5dZbu+5j5Xw1luS9mGL9Vfi4w3OjVug/TiSxyhwLzQ==", + "version": "0.2.0-main.522", + "resolved": "https://registry.npmjs.org/@bitwarden/sdk-internal/-/sdk-internal-0.2.0-main.522.tgz", + "integrity": "sha512-E+YqqX/FvGF0vGx6sNJfYaMj88C+rVo51fQPMSHoOePdryFcKQSJX706Glv86OMLMXE7Ln5Lua8LJRftlF/EFQ==", + "license": "GPL-3.0", "dependencies": { "type-fest": "^4.41.0" } diff --git a/package.json b/package.json index ea7dc32820c..751c67afcd1 100644 --- a/package.json +++ b/package.json @@ -161,8 +161,8 @@ "@angular/platform-browser": "20.3.16", "@angular/platform-browser-dynamic": "20.3.16", "@angular/router": "20.3.16", - "@bitwarden/commercial-sdk-internal": "0.2.0-main.506", - "@bitwarden/sdk-internal": "0.2.0-main.506", + "@bitwarden/commercial-sdk-internal": "0.2.0-main.522", + "@bitwarden/sdk-internal": "0.2.0-main.522", "@electron/fuses": "1.8.0", "@emotion/css": "11.13.5", "@koa/multer": "4.0.0", From 7c6d98b50ec9143f9e587122d8f880cdd2758caf Mon Sep 17 00:00:00 2001 From: adudek-bw Date: Thu, 5 Feb 2026 09:46:31 -0500 Subject: [PATCH 19/26] Remove feature flag check from password generation (#18003) * Remove feature flag check from password generation --- apps/browser/src/background/main.background.ts | 1 + .../cli/src/service-container/service-container.ts | 1 + libs/common/src/tools/providers.spec.ts | 11 +++++++++++ libs/common/src/tools/providers.ts | 6 ++++-- libs/importer/src/components/importer-providers.ts | 2 ++ .../components/src/generator-services.module.ts | 12 ++---------- .../core/src/engine/sdk-password-randomizer.ts | 14 ++++++++------ .../src/metadata/password/eff-word-list.spec.ts | 12 +----------- .../core/src/metadata/password/eff-word-list.ts | 5 +---- .../src/metadata/password/random-password.spec.ts | 10 +--------- .../core/src/metadata/password/random-password.ts | 5 +---- .../src/providers/generator-dependency-provider.ts | 4 ++-- .../providers/generator-metadata-provider.spec.ts | 6 ------ 13 files changed, 35 insertions(+), 54 deletions(-) diff --git a/apps/browser/src/background/main.background.ts b/apps/browser/src/background/main.background.ts index eb6d26357eb..e0cbcdc9f96 100644 --- a/apps/browser/src/background/main.background.ts +++ b/apps/browser/src/background/main.background.ts @@ -1110,6 +1110,7 @@ export default class MainBackground { this.logService, this.platformUtilsService, this.configService, + this.sdkService, ), ); diff --git a/apps/cli/src/service-container/service-container.ts b/apps/cli/src/service-container/service-container.ts index 3e78eb36577..105d80b290b 100644 --- a/apps/cli/src/service-container/service-container.ts +++ b/apps/cli/src/service-container/service-container.ts @@ -928,6 +928,7 @@ export class ServiceContainer { this.logService, this.platformUtilsService, this.configService, + this.sdkService, ), ); diff --git a/libs/common/src/tools/providers.spec.ts b/libs/common/src/tools/providers.spec.ts index 5953e5ebab2..d457b1df85e 100644 --- a/libs/common/src/tools/providers.spec.ts +++ b/libs/common/src/tools/providers.spec.ts @@ -4,6 +4,7 @@ import { PolicyService } from "../admin-console/abstractions/policy/policy.servi import { ConfigService } from "../platform/abstractions/config/config.service"; import { LogService } from "../platform/abstractions/log.service"; import { PlatformUtilsService } from "../platform/abstractions/platform-utils.service"; +import { SdkService } from "../platform/abstractions/sdk/sdk.service"; import { StateProvider } from "../platform/state"; import { LegacyEncryptorProvider } from "./cryptography/legacy-encryptor-provider"; @@ -20,6 +21,7 @@ describe("SystemServiceProvider", () => { let mockLogger: LogService; let mockEnvironment: MockProxy; let mockConfigService: ConfigService; + let mockSdkService: SdkService; beforeEach(() => { jest.resetAllMocks(); @@ -31,6 +33,7 @@ describe("SystemServiceProvider", () => { mockLogger = mock(); mockEnvironment = mock(); mockConfigService = mock(); + mockSdkService = mock(); }); describe("createSystemServiceProvider", () => { @@ -45,6 +48,7 @@ describe("SystemServiceProvider", () => { mockLogger, mockEnvironment, mockConfigService, + mockSdkService, ); expect(result).toHaveProperty("policy", mockPolicy); @@ -66,6 +70,7 @@ describe("SystemServiceProvider", () => { mockLogger, mockEnvironment, mockConfigService, + mockSdkService, ); expect(result.extension).toBeInstanceOf(ExtensionService); @@ -83,6 +88,7 @@ describe("SystemServiceProvider", () => { mockLogger, mockEnvironment, mockConfigService, + mockSdkService, ); expect(mockEnvironment.isDev).toHaveBeenCalledTimes(1); @@ -102,6 +108,7 @@ describe("SystemServiceProvider", () => { mockLogger, mockEnvironment, mockConfigService, + mockSdkService, ); expect(mockEnvironment.isDev).toHaveBeenCalledTimes(1); @@ -121,6 +128,7 @@ describe("SystemServiceProvider", () => { mockLogger, mockEnvironment, mockConfigService, + mockSdkService, ); expect(result.extension).toBeInstanceOf(ExtensionService); @@ -138,6 +146,7 @@ describe("SystemServiceProvider", () => { mockLogger, mockEnvironment, mockConfigService, + mockSdkService, ); expect(result.policy).toBe(mockPolicy); @@ -154,6 +163,7 @@ describe("SystemServiceProvider", () => { mockLogger, mockEnvironment, mockConfigService, + mockSdkService, ); expect(result.configService).toBe(mockConfigService); @@ -170,6 +180,7 @@ describe("SystemServiceProvider", () => { mockLogger, mockEnvironment, mockConfigService, + mockSdkService, ); expect(result.environment).toBe(mockEnvironment); diff --git a/libs/common/src/tools/providers.ts b/libs/common/src/tools/providers.ts index ac42c556042..b1621f19c21 100644 --- a/libs/common/src/tools/providers.ts +++ b/libs/common/src/tools/providers.ts @@ -1,10 +1,10 @@ import { LogService } from "@bitwarden/logging"; -import { BitwardenClient } from "@bitwarden/sdk-internal"; import { StateProvider } from "@bitwarden/state"; import { PolicyService } from "../admin-console/abstractions/policy/policy.service.abstraction"; import { ConfigService } from "../platform/abstractions/config/config.service"; import { PlatformUtilsService } from "../platform/abstractions/platform-utils.service"; +import { SdkService } from "../platform/abstractions/sdk/sdk.service"; import { LegacyEncryptorProvider } from "./cryptography/legacy-encryptor-provider"; import { ExtensionRegistry } from "./extension/extension-registry.abstraction"; @@ -29,7 +29,7 @@ export type SystemServiceProvider = { readonly environment: PlatformUtilsService; /** SDK Service */ - readonly sdk?: BitwardenClient; + readonly sdk: SdkService; }; /** Constructs a system service provider. */ @@ -41,6 +41,7 @@ export function createSystemServiceProvider( logger: LogService, environment: PlatformUtilsService, configService: ConfigService, + sdk: SdkService, ): SystemServiceProvider { let log: LogProvider; if (environment.isDev()) { @@ -62,5 +63,6 @@ export function createSystemServiceProvider( log, configService, environment, + sdk, }; } diff --git a/libs/importer/src/components/importer-providers.ts b/libs/importer/src/components/importer-providers.ts index 18c148ebe2e..eb7e58e9259 100644 --- a/libs/importer/src/components/importer-providers.ts +++ b/libs/importer/src/components/importer-providers.ts @@ -13,6 +13,7 @@ import { ConfigService } from "@bitwarden/common/platform/abstractions/config/co import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; +import { SdkService } from "@bitwarden/common/platform/abstractions/sdk/sdk.service"; import { KeyServiceLegacyEncryptorProvider } from "@bitwarden/common/tools/cryptography/key-service-legacy-encryptor-provider"; import { LegacyEncryptorProvider } from "@bitwarden/common/tools/cryptography/legacy-encryptor-provider"; import { ExtensionRegistry } from "@bitwarden/common/tools/extension/extension-registry.abstraction"; @@ -71,6 +72,7 @@ export const ImporterProviders: SafeProvider[] = [ LogService, PlatformUtilsService, ConfigService, + SdkService, ], }), safeProvider({ diff --git a/libs/tools/generator/components/src/generator-services.module.ts b/libs/tools/generator/components/src/generator-services.module.ts index 935f7dc2d60..39d0dd298a2 100644 --- a/libs/tools/generator/components/src/generator-services.module.ts +++ b/libs/tools/generator/components/src/generator-services.module.ts @@ -1,12 +1,10 @@ import { NgModule } from "@angular/core"; -import { from, take } from "rxjs"; import { JslibModule } from "@bitwarden/angular/jslib.module"; import { safeProvider } from "@bitwarden/angular/platform/utils/safe-provider"; import { SafeInjectionToken } from "@bitwarden/angular/services/injection-tokens"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction"; -import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service"; import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; @@ -126,7 +124,7 @@ export const SYSTEM_SERVICE_PROVIDER = new SafeInjectionToken (featureFlag = ff)); const metadata = new providers.GeneratorMetadataProvider( userStateDeps, system, Object.values(BuiltIn), ); - const sdkService = featureFlag ? system.sdk : undefined; const profile = new providers.GeneratorProfileProvider(userStateDeps, system.policy); const generator: providers.GeneratorDependencyProvider = { randomizer: random, client: new RestClient(api, i18n), i18nService: i18n, - sdk: sdkService, + sdk: system.sdk, now: Date.now, }; diff --git a/libs/tools/generator/core/src/engine/sdk-password-randomizer.ts b/libs/tools/generator/core/src/engine/sdk-password-randomizer.ts index 03be21eeefb..09c7d62b1ad 100644 --- a/libs/tools/generator/core/src/engine/sdk-password-randomizer.ts +++ b/libs/tools/generator/core/src/engine/sdk-password-randomizer.ts @@ -1,3 +1,6 @@ +import { firstValueFrom } from "rxjs"; + +import { SdkService } from "@bitwarden/common/platform/abstractions/sdk/sdk.service"; import { BitwardenClient, PassphraseGeneratorRequest, @@ -20,11 +23,11 @@ export class SdkPasswordRandomizer CredentialGenerator { /** Instantiates the password randomizer - * @param client access to SDK client to call upon password/passphrase generation + * @param service access to SDK client to call upon password/passphrase generation * @param currentTime gets the current datetime in epoch time */ constructor( - private client: BitwardenClient, + private service: SdkService, private currentTime: () => number, ) {} @@ -40,8 +43,9 @@ export class SdkPasswordRandomizer request: GenerateRequest, settings: PasswordGenerationOptions | PassphraseGenerationOptions, ) { + const sdk: BitwardenClient = await firstValueFrom(this.service.client$); if (isPasswordGenerationOptions(settings)) { - const password = await this.client.generator().password(convertPasswordRequest(settings)); + const password = await sdk.generator().password(convertPasswordRequest(settings)); return new GeneratedCredential( password, @@ -51,9 +55,7 @@ export class SdkPasswordRandomizer request.website, ); } else if (isPassphraseGenerationOptions(settings)) { - const passphrase = await this.client - .generator() - .passphrase(convertPassphraseRequest(settings)); + const passphrase = await sdk.generator().passphrase(convertPassphraseRequest(settings)); return new GeneratedCredential( passphrase, diff --git a/libs/tools/generator/core/src/metadata/password/eff-word-list.spec.ts b/libs/tools/generator/core/src/metadata/password/eff-word-list.spec.ts index bdf021c50f3..015cc25a8ec 100644 --- a/libs/tools/generator/core/src/metadata/password/eff-word-list.spec.ts +++ b/libs/tools/generator/core/src/metadata/password/eff-word-list.spec.ts @@ -3,7 +3,7 @@ import { mock } from "jest-mock-extended"; import { PolicyType } from "@bitwarden/common/admin-console/enums"; import { Policy } from "@bitwarden/common/admin-console/models/domain/policy"; -import { PasswordRandomizer, SdkPasswordRandomizer } from "../../engine"; +import { SdkPasswordRandomizer } from "../../engine"; import { PassphrasePolicyConstraints } from "../../policies"; import { GeneratorDependencyProvider } from "../../providers"; import { PassphraseGenerationOptions } from "../../types"; @@ -22,16 +22,6 @@ describe("password - eff words generator metadata", () => { }); }); - describe("engine.create", () => { - const nonSdkDependencyProvider = mock(); - nonSdkDependencyProvider.sdk = undefined; - it("returns a password randomizer", () => { - expect(effPassphrase.engine.create(nonSdkDependencyProvider)).toBeInstanceOf( - PasswordRandomizer, - ); - }); - }); - describe("profiles[account]", () => { let accountProfile: CoreProfileMetadata | null = null; beforeEach(() => { diff --git a/libs/tools/generator/core/src/metadata/password/eff-word-list.ts b/libs/tools/generator/core/src/metadata/password/eff-word-list.ts index fc96ce46c2b..d6d78c83293 100644 --- a/libs/tools/generator/core/src/metadata/password/eff-word-list.ts +++ b/libs/tools/generator/core/src/metadata/password/eff-word-list.ts @@ -3,7 +3,7 @@ import { GENERATOR_DISK } from "@bitwarden/common/platform/state"; import { PublicClassifier } from "@bitwarden/common/tools/public-classifier"; import { ObjectKey } from "@bitwarden/common/tools/state/object-key"; -import { PasswordRandomizer, SdkPasswordRandomizer } from "../../engine"; +import { SdkPasswordRandomizer } from "../../engine"; import { passphraseLeastPrivilege, PassphrasePolicyConstraints } from "../../policies"; import { GeneratorDependencyProvider } from "../../providers"; import { CredentialGenerator, PassphraseGenerationOptions } from "../../types"; @@ -30,9 +30,6 @@ const passphrase: GeneratorMetadata = { create( dependencies: GeneratorDependencyProvider, ): CredentialGenerator { - if (dependencies.sdk == undefined) { - return new PasswordRandomizer(dependencies.randomizer, dependencies.now); - } return new SdkPasswordRandomizer(dependencies.sdk, dependencies.now); }, }, diff --git a/libs/tools/generator/core/src/metadata/password/random-password.spec.ts b/libs/tools/generator/core/src/metadata/password/random-password.spec.ts index 9efd5350c21..d066b9f1597 100644 --- a/libs/tools/generator/core/src/metadata/password/random-password.spec.ts +++ b/libs/tools/generator/core/src/metadata/password/random-password.spec.ts @@ -3,7 +3,7 @@ import { mock } from "jest-mock-extended"; import { PolicyType } from "@bitwarden/common/admin-console/enums"; import { Policy } from "@bitwarden/common/admin-console/models/domain/policy"; -import { PasswordRandomizer, SdkPasswordRandomizer } from "../../engine"; +import { SdkPasswordRandomizer } from "../../engine"; import { DynamicPasswordPolicyConstraints } from "../../policies"; import { GeneratorDependencyProvider } from "../../providers"; import { PasswordGenerationOptions } from "../../types"; @@ -22,14 +22,6 @@ describe("password - characters generator metadata", () => { }); }); - describe("engine.create", () => { - const nonSdkDependencyProvider = mock(); - nonSdkDependencyProvider.sdk = undefined; - it("returns a password randomizer", () => { - expect(password.engine.create(nonSdkDependencyProvider)).toBeInstanceOf(PasswordRandomizer); - }); - }); - describe("profiles[account]", () => { let accountProfile: CoreProfileMetadata = null!; beforeEach(() => { diff --git a/libs/tools/generator/core/src/metadata/password/random-password.ts b/libs/tools/generator/core/src/metadata/password/random-password.ts index 721be8dc3f0..d25ea1e8f46 100644 --- a/libs/tools/generator/core/src/metadata/password/random-password.ts +++ b/libs/tools/generator/core/src/metadata/password/random-password.ts @@ -3,7 +3,7 @@ import { GENERATOR_DISK } from "@bitwarden/common/platform/state"; import { PublicClassifier } from "@bitwarden/common/tools/public-classifier"; import { deepFreeze } from "@bitwarden/common/tools/util"; -import { PasswordRandomizer, SdkPasswordRandomizer } from "../../engine"; +import { SdkPasswordRandomizer } from "../../engine"; import { DynamicPasswordPolicyConstraints, passwordLeastPrivilege } from "../../policies"; import { GeneratorDependencyProvider } from "../../providers"; import { CredentialGenerator, PasswordGeneratorSettings } from "../../types"; @@ -30,9 +30,6 @@ const password: GeneratorMetadata = deepFreeze({ create( dependencies: GeneratorDependencyProvider, ): CredentialGenerator { - if (dependencies.sdk == undefined) { - return new PasswordRandomizer(dependencies.randomizer, dependencies.now); - } return new SdkPasswordRandomizer(dependencies.sdk, dependencies.now); }, }, diff --git a/libs/tools/generator/core/src/providers/generator-dependency-provider.ts b/libs/tools/generator/core/src/providers/generator-dependency-provider.ts index a6dbbeaa537..8700bbc8a24 100644 --- a/libs/tools/generator/core/src/providers/generator-dependency-provider.ts +++ b/libs/tools/generator/core/src/providers/generator-dependency-provider.ts @@ -1,6 +1,6 @@ import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; +import { SdkService } from "@bitwarden/common/platform/abstractions/sdk/sdk.service"; import { RestClient } from "@bitwarden/common/tools/integration/rpc"; -import { BitwardenClient } from "@bitwarden/sdk-internal"; import { Randomizer } from "../abstractions"; @@ -10,6 +10,6 @@ export type GeneratorDependencyProvider = { // FIXME: introduce `I18nKeyOrLiteral` into forwarder // structures and remove this dependency i18nService: I18nService; - sdk?: BitwardenClient; + sdk: SdkService; now: () => number; }; diff --git a/libs/tools/generator/core/src/providers/generator-metadata-provider.spec.ts b/libs/tools/generator/core/src/providers/generator-metadata-provider.spec.ts index 39ff74ad901..f79bb986325 100644 --- a/libs/tools/generator/core/src/providers/generator-metadata-provider.spec.ts +++ b/libs/tools/generator/core/src/providers/generator-metadata-provider.spec.ts @@ -5,7 +5,6 @@ import { PolicyService } from "@bitwarden/common/admin-console/abstractions/poli import { PolicyType } from "@bitwarden/common/admin-console/enums"; import { Policy } from "@bitwarden/common/admin-console/models/domain/policy"; import { Account } from "@bitwarden/common/auth/abstractions/account.service"; -import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { LegacyEncryptorProvider } from "@bitwarden/common/tools/cryptography/legacy-encryptor-provider"; import { UserEncryptor } from "@bitwarden/common/tools/cryptography/user-encryptor.abstraction"; import { @@ -96,8 +95,6 @@ const SomePolicyService = mock(); const SomeExtensionService = mock(); -const SomeConfigService = mock; - const SomeSdkService = mock; const ApplicationProvider = { @@ -110,9 +107,6 @@ const ApplicationProvider = { /** Event monitoring and diagnostic interfaces */ log: disabledSemanticLoggerProvider, - /** Feature flag retrieval */ - configService: SomeConfigService, - /** SDK access for password generation */ sdk: SomeSdkService, } as unknown as SystemServiceProvider; From 3fb31fd0406ac2ea32bd3b0fc8832ca14cd8981a Mon Sep 17 00:00:00 2001 From: Jared Date: Thu, 5 Feb 2026 10:31:21 -0500 Subject: [PATCH 20/26] Add permission guard and only allow provider admin to visit billing page for provider portal users (#18639) --- .../app/admin-console/providers/providers-routing.module.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/bitwarden_license/bit-web/src/app/admin-console/providers/providers-routing.module.ts b/bitwarden_license/bit-web/src/app/admin-console/providers/providers-routing.module.ts index 79de741b67a..447481a8bcb 100644 --- a/bitwarden_license/bit-web/src/app/admin-console/providers/providers-routing.module.ts +++ b/bitwarden_license/bit-web/src/app/admin-console/providers/providers-routing.module.ts @@ -123,7 +123,9 @@ const routes: Routes = [ }, { path: "billing", - canActivate: [providerPermissionsGuard()], + canActivate: [ + providerPermissionsGuard((provider: Provider) => provider.isProviderAdmin), + ], children: [ { path: "", From 61763204eaf04e3feca09814c330fe5e162993ae Mon Sep 17 00:00:00 2001 From: Jared Date: Thu, 5 Feb 2026 10:35:39 -0500 Subject: [PATCH 21/26] Update bulk restore/revoke component to conditionally display non-compliant members callout. Adjusted logic to set statuses based on entry errors and isRevoking state. (#18654) --- .../components/bulk/bulk-restore-revoke.component.html | 2 +- .../components/bulk/bulk-restore-revoke.component.ts | 9 +++------ 2 files changed, 4 insertions(+), 7 deletions(-) diff --git a/apps/web/src/app/admin-console/organizations/members/components/bulk/bulk-restore-revoke.component.html b/apps/web/src/app/admin-console/organizations/members/components/bulk/bulk-restore-revoke.component.html index 1b711b366d6..f07568ac0c2 100644 --- a/apps/web/src/app/admin-console/organizations/members/components/bulk/bulk-restore-revoke.component.html +++ b/apps/web/src/app/admin-console/organizations/members/components/bulk/bulk-restore-revoke.component.html @@ -14,7 +14,7 @@ {{ "nonCompliantMembersError" | i18n }} diff --git a/apps/web/src/app/admin-console/organizations/members/components/bulk/bulk-restore-revoke.component.ts b/apps/web/src/app/admin-console/organizations/members/components/bulk/bulk-restore-revoke.component.ts index 154a683b0e1..3228b9a94ce 100644 --- a/apps/web/src/app/admin-console/organizations/members/components/bulk/bulk-restore-revoke.component.ts +++ b/apps/web/src/app/admin-console/organizations/members/components/bulk/bulk-restore-revoke.component.ts @@ -88,12 +88,9 @@ export class BulkRestoreRevokeComponent { const bulkMessage = this.isRevoking ? "bulkRevokedMessage" : "bulkRestoredMessage"; response.data.forEach(async (entry) => { - const error = - entry.error !== "" - ? this.i18nService.t("cannotRestoreAccessError") - : this.i18nService.t(bulkMessage); - this.statuses.set(entry.id, error); - if (entry.error !== "") { + const status = entry.error !== "" ? entry.error : this.i18nService.t(bulkMessage); + this.statuses.set(entry.id, status); + if (entry.error !== "" && !this.isRevoking) { this.nonCompliantMembers = true; } }); From 35773ae9a0fa7ee1c161657ece5c25bed419671b Mon Sep 17 00:00:00 2001 From: Jared Date: Thu, 5 Feb 2026 10:36:44 -0500 Subject: [PATCH 22/26] [PM-29771] Make invitation non-plural if only 1 member selected (#18684) * Make invitation non-plural if only 1 member selected * Add isSingleInvite as per Jimmy's suggestion --- .../members/deprecated_members.component.html | 2 +- .../members/deprecated_members.component.ts | 10 ++++++++++ .../organizations/members/members.component.html | 3 ++- .../organizations/members/members.component.ts | 10 ++++++++++ .../providers/manage/deprecated_members.component.html | 2 +- .../providers/manage/deprecated_members.component.ts | 10 ++++++++++ .../providers/manage/members.component.html | 3 ++- .../providers/manage/members.component.ts | 8 ++++++++ 8 files changed, 44 insertions(+), 4 deletions(-) diff --git a/apps/web/src/app/admin-console/organizations/members/deprecated_members.component.html b/apps/web/src/app/admin-console/organizations/members/deprecated_members.component.html index 921004e315d..65bab31c728 100644 --- a/apps/web/src/app/admin-console/organizations/members/deprecated_members.component.html +++ b/apps/web/src/app/admin-console/organizations/members/deprecated_members.component.html @@ -136,7 +136,7 @@ *ngIf="showBulkReinviteUsers" > - {{ "reinviteSelected" | i18n }} + {{ (isSingleInvite ? "resendInvitation" : "reinviteSelected") | i18n }} } @if (bulkActions.showBulkConfirmUsers) { diff --git a/apps/web/src/app/admin-console/organizations/members/members.component.ts b/apps/web/src/app/admin-console/organizations/members/members.component.ts index e3ed575d81b..36c207219a0 100644 --- a/apps/web/src/app/admin-console/organizations/members/members.component.ts +++ b/apps/web/src/app/admin-console/organizations/members/members.component.ts @@ -125,6 +125,16 @@ export class vNextMembersComponent { .usersUpdated() .pipe(map(() => showConfirmBanner(this.dataSource()))); + protected selectedInvitedCount$ = this.dataSource() + .usersUpdated() + .pipe( + map( + (members) => members.filter((m) => m.status === OrganizationUserStatusType.Invited).length, + ), + ); + + protected isSingleInvite$ = this.selectedInvitedCount$.pipe(map((count) => count === 1)); + protected isProcessing = this.memberActionsService.isProcessing; protected readonly canUseSecretsManager: Signal = computed( diff --git a/bitwarden_license/bit-web/src/app/admin-console/providers/manage/deprecated_members.component.html b/bitwarden_license/bit-web/src/app/admin-console/providers/manage/deprecated_members.component.html index e0b29dffeb8..5478601e72c 100644 --- a/bitwarden_license/bit-web/src/app/admin-console/providers/manage/deprecated_members.component.html +++ b/bitwarden_license/bit-web/src/app/admin-console/providers/manage/deprecated_members.component.html @@ -89,7 +89,7 @@ *ngIf="showBulkReinviteUsers" > - {{ "reinviteSelected" | i18n }} + {{ (isSingleInvite ? "resendInvitation" : "reinviteSelected") | i18n }} } @if (bulkMenuOptions.showBulkConfirmUsers) { diff --git a/bitwarden_license/bit-web/src/app/admin-console/providers/manage/members.component.ts b/bitwarden_license/bit-web/src/app/admin-console/providers/manage/members.component.ts index a2330be4c6f..3efeee17100 100644 --- a/bitwarden_license/bit-web/src/app/admin-console/providers/manage/members.component.ts +++ b/bitwarden_license/bit-web/src/app/admin-console/providers/manage/members.component.ts @@ -104,6 +104,14 @@ export class vNextMembersComponent { .usersUpdated() .pipe(map(() => showConfirmBanner(this.dataSource()))); + protected selectedInvitedCount$ = this.dataSource() + .usersUpdated() + .pipe( + map((members) => members.filter((m) => m.status === ProviderUserStatusType.Invited).length), + ); + + protected isSingleInvite$ = this.selectedInvitedCount$.pipe(map((count) => count === 1)); + protected isProcessing = this.providerActionsService.isProcessing; constructor() { From 2d8f74bf70a9478b519a10a141ba4f4496de99e2 Mon Sep 17 00:00:00 2001 From: Jared Date: Thu, 5 Feb 2026 10:37:35 -0500 Subject: [PATCH 23/26] Disable native icon for datetime-local field and use our own icons for stylizing (#18633) --- .../manage/events.component.html | 74 +++++++++++++++---- apps/web/src/scss/tailwind.css | 64 ++++++++++++++++ 2 files changed, 124 insertions(+), 14 deletions(-) diff --git a/apps/web/src/app/admin-console/organizations/manage/events.component.html b/apps/web/src/app/admin-console/organizations/manage/events.component.html index 83665a4b99e..da4de1846fa 100644 --- a/apps/web/src/app/admin-console/organizations/manage/events.component.html +++ b/apps/web/src/app/admin-console/organizations/manage/events.component.html @@ -14,24 +14,70 @@
{{ "from" | i18n }} - +
+ + + + +
- {{ "to" | i18n }} - +
+ + + + +
- - - diff --git a/apps/browser/src/tools/popup/send-v2/send-file-popout-dialog/send-file-popout-dialog.component.ts b/apps/browser/src/tools/popup/send-v2/send-file-popout-dialog/send-file-popout-dialog.component.ts deleted file mode 100644 index 23fa744995a..00000000000 --- a/apps/browser/src/tools/popup/send-v2/send-file-popout-dialog/send-file-popout-dialog.component.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { CommonModule } from "@angular/common"; -import { Component } from "@angular/core"; - -import { JslibModule } from "@bitwarden/angular/jslib.module"; -import { ButtonModule, DialogModule, DialogService, TypographyModule } from "@bitwarden/components"; - -import BrowserPopupUtils from "../../../../platform/browser/browser-popup-utils"; - -// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush -// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection -@Component({ - selector: "send-file-popout-dialog", - templateUrl: "./send-file-popout-dialog.component.html", - imports: [JslibModule, CommonModule, DialogModule, ButtonModule, TypographyModule], -}) -export class SendFilePopoutDialogComponent { - constructor(private dialogService: DialogService) {} - - async popOutWindow() { - await BrowserPopupUtils.openCurrentPagePopout(window); - } - - close() { - this.dialogService.closeAll(); - } -} diff --git a/apps/browser/src/tools/popup/services/file-popout-utils.service.ts b/apps/browser/src/tools/popup/services/file-popout-utils.service.ts deleted file mode 100644 index 9a04d4b8f23..00000000000 --- a/apps/browser/src/tools/popup/services/file-popout-utils.service.ts +++ /dev/null @@ -1,71 +0,0 @@ -import { Injectable } from "@angular/core"; - -import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; - -import BrowserPopupUtils from "../../../platform/browser/browser-popup-utils"; - -/** - * Service for determining whether to display file popout callout messages. - */ -@Injectable() -export class FilePopoutUtilsService { - /** - * Creates an instance of FilePopoutUtilsService. - */ - constructor(private platformUtilsService: PlatformUtilsService) {} - - /** - * Determines whether to show any file popout callout message in the current browser. - * @param win - The window context in which the check should be performed. - * @returns True if a file popout callout message should be displayed; otherwise, false. - */ - showFilePopoutMessage(win: Window): boolean { - return ( - this.showFirefoxFileWarning(win) || - this.showSafariFileWarning(win) || - this.showChromiumFileWarning(win) - ); - } - - /** - * Determines whether to show a file popout callout message for the Firefox browser - * @param win - The window context in which the check should be performed. - * @returns True if the extension is not in a sidebar or popout; otherwise, false. - */ - showFirefoxFileWarning(win: Window): boolean { - return ( - this.platformUtilsService.isFirefox() && - !(BrowserPopupUtils.inSidebar(win) || BrowserPopupUtils.inPopout(win)) - ); - } - - /** - * Determines whether to show a file popout message for the Safari browser - * @param win - The window context in which the check should be performed. - * @returns True if the extension is not in a popout; otherwise, false. - */ - showSafariFileWarning(win: Window): boolean { - return this.platformUtilsService.isSafari() && !BrowserPopupUtils.inPopout(win); - } - - /** - * Determines whether to show a file popout callout message for Chromium-based browsers in Linux and Mac OS X - * @param win - The window context in which the check should be performed. - * @returns True if the extension is not in a sidebar or popout; otherwise, false. - */ - showChromiumFileWarning(win: Window): boolean { - return ( - (this.isLinux(win) || this.isUnsupportedMac(win)) && - !this.platformUtilsService.isFirefox() && - !(BrowserPopupUtils.inSidebar(win) || BrowserPopupUtils.inPopout(win)) - ); - } - - private isLinux(win: Window): boolean { - return win?.navigator?.userAgent.indexOf("Linux") !== -1; - } - - private isUnsupportedMac(win: Window): boolean { - return this.platformUtilsService.isChrome() && win?.navigator?.appVersion.includes("Mac OS X"); - } -} diff --git a/apps/browser/src/vault/popup/components/vault-v2/attachments/open-attachments/open-attachments.component.html b/apps/browser/src/vault/popup/components/vault-v2/attachments/open-attachments/open-attachments.component.html index 0fbe1c55b0a..1e9d63b709b 100644 --- a/apps/browser/src/vault/popup/components/vault-v2/attachments/open-attachments/open-attachments.component.html +++ b/apps/browser/src/vault/popup/components/vault-v2/attachments/open-attachments/open-attachments.component.html @@ -4,14 +4,15 @@ type="button" (click)="openAttachments()" [disabled]="parentFormDisabled" + [title]="'popOutNewWindow' | i18n" >
{{ "attachments" | i18n }}
- - + {{ "popOutNewWindow" | i18n }} + diff --git a/apps/browser/src/vault/popup/components/vault-v2/attachments/open-attachments/open-attachments.component.spec.ts b/apps/browser/src/vault/popup/components/vault-v2/attachments/open-attachments/open-attachments.component.spec.ts index e9636e09873..b88b435c702 100644 --- a/apps/browser/src/vault/popup/components/vault-v2/attachments/open-attachments/open-attachments.component.spec.ts +++ b/apps/browser/src/vault/popup/components/vault-v2/attachments/open-attachments/open-attachments.component.spec.ts @@ -20,9 +20,6 @@ import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; import { ToastService } from "@bitwarden/components"; import { CipherFormContainer } from "@bitwarden/vault"; -import BrowserPopupUtils from "../../../../../../platform/browser/browser-popup-utils"; -import { FilePopoutUtilsService } from "../../../../../../tools/popup/services/file-popout-utils.service"; - import { OpenAttachmentsComponent } from "./open-attachments.component"; describe("OpenAttachmentsComponent", () => { @@ -31,9 +28,6 @@ describe("OpenAttachmentsComponent", () => { let router: Router; const showToast = jest.fn(); const hasPremiumFromAnySource$ = new BehaviorSubject(true); - const openCurrentPagePopout = jest - .spyOn(BrowserPopupUtils, "openCurrentPagePopout") - .mockResolvedValue(null); const cipherView = { id: "5555-444-3333", type: CipherType.Login, @@ -55,7 +49,6 @@ describe("OpenAttachmentsComponent", () => { const getCipher = jest.fn().mockResolvedValue(cipherDomain); const organizations$ = jest.fn().mockReturnValue(of([org])); - const showFilePopoutMessage = jest.fn().mockReturnValue(false); const mockUserId = Utils.newGuid() as UserId; const accountService = { @@ -70,11 +63,9 @@ describe("OpenAttachmentsComponent", () => { const formStatusChange$ = new BehaviorSubject<"enabled" | "disabled">("enabled"); beforeEach(async () => { - openCurrentPagePopout.mockClear(); getCipher.mockClear(); showToast.mockClear(); organizations$.mockClear(); - showFilePopoutMessage.mockClear(); hasPremiumFromAnySource$.next(true); formStatusChange$.next("enabled"); @@ -103,10 +94,6 @@ describe("OpenAttachmentsComponent", () => { provide: OrganizationService, useValue: { organizations$ }, }, - { - provide: FilePopoutUtilsService, - useValue: { showFilePopoutMessage }, - }, { provide: AccountService, useValue: accountService, @@ -130,8 +117,7 @@ describe("OpenAttachmentsComponent", () => { fixture.detectChanges(); }); - it("opens attachments in new popout", async () => { - showFilePopoutMessage.mockReturnValue(true); + it("navigates to attachments route", async () => { component.canAccessAttachments = true; await component.ngOnInit(); @@ -140,20 +126,6 @@ describe("OpenAttachmentsComponent", () => { expect(router.navigate).toHaveBeenCalledWith(["/attachments"], { queryParams: { cipherId: "5555-444-3333" }, }); - expect(openCurrentPagePopout).toHaveBeenCalledWith(window); - }); - - it("opens attachments in same window", async () => { - showFilePopoutMessage.mockReturnValue(false); - component.canAccessAttachments = true; - await component.ngOnInit(); - - await component.openAttachments(); - - expect(openCurrentPagePopout).not.toHaveBeenCalled(); - expect(router.navigate).toHaveBeenCalledWith(["/attachments"], { - queryParams: { cipherId: "5555-444-3333" }, - }); }); it("routes the user to the premium page when they cannot access premium features", async () => { diff --git a/apps/browser/src/vault/popup/components/vault-v2/attachments/open-attachments/open-attachments.component.ts b/apps/browser/src/vault/popup/components/vault-v2/attachments/open-attachments/open-attachments.component.ts index a267e7999ab..1a1f767ca8c 100644 --- a/apps/browser/src/vault/popup/components/vault-v2/attachments/open-attachments/open-attachments.component.ts +++ b/apps/browser/src/vault/popup/components/vault-v2/attachments/open-attachments/open-attachments.component.ts @@ -23,9 +23,6 @@ import { PremiumUpgradePromptService } from "@bitwarden/common/vault/abstraction import { BadgeModule, ItemModule, ToastService, TypographyModule } from "@bitwarden/components"; import { CipherFormContainer } from "@bitwarden/vault"; -import BrowserPopupUtils from "../../../../../../platform/browser/browser-popup-utils"; -import { FilePopoutUtilsService } from "../../../../../../tools/popup/services/file-popout-utils.service"; - // FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush // eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ @@ -46,9 +43,6 @@ export class OpenAttachmentsComponent implements OnInit { // eslint-disable-next-line @angular-eslint/prefer-signals @Input({ required: true }) cipherId: CipherId; - /** True when the attachments window should be opened in a popout */ - openAttachmentsInPopout: boolean; - /** True when the user has access to premium or h */ canAccessAttachments: boolean; @@ -65,7 +59,6 @@ export class OpenAttachmentsComponent implements OnInit { private organizationService: OrganizationService, private toastService: ToastService, private i18nService: I18nService, - private filePopoutUtilsService: FilePopoutUtilsService, private accountService: AccountService, private cipherFormContainer: CipherFormContainer, private premiumUpgradeService: PremiumUpgradePromptService, @@ -87,8 +80,6 @@ export class OpenAttachmentsComponent implements OnInit { } async ngOnInit(): Promise { - this.openAttachmentsInPopout = this.filePopoutUtilsService.showFilePopoutMessage(window); - if (!this.cipherId) { return; } @@ -131,12 +122,5 @@ export class OpenAttachmentsComponent implements OnInit { } await this.router.navigate(["/attachments"], { queryParams: { cipherId: this.cipherId } }); - - // Open the attachments page in a popout - // This is done after the router navigation to ensure that the navigation - // is included in the `PopupRouterCacheService` history - if (this.openAttachmentsInPopout) { - await BrowserPopupUtils.openCurrentPagePopout(window); - } } } diff --git a/apps/browser/src/vault/popup/components/vault-v2/vault-v2.component.spec.ts b/apps/browser/src/vault/popup/components/vault-v2/vault-v2.component.spec.ts index a322fbc53dd..a956b2fe68b 100644 --- a/apps/browser/src/vault/popup/components/vault-v2/vault-v2.component.spec.ts +++ b/apps/browser/src/vault/popup/components/vault-v2/vault-v2.component.spec.ts @@ -421,29 +421,13 @@ describe("VaultV2Component", () => { expect(PremiumUpgradeDialogComponent.open).toHaveBeenCalledTimes(1); }); - it("navigateToImport navigates and opens popout if popup is open", fakeAsync(async () => { - (BrowserApi.isPopupOpen as jest.Mock).mockResolvedValueOnce(true); - + it("navigateToImport navigates to import route", fakeAsync(async () => { const ngRouter = TestBed.inject(Router); jest.spyOn(ngRouter, "navigate").mockResolvedValue(true as any); await component["navigateToImport"](); expect(ngRouter.navigate).toHaveBeenCalledWith(["/import"]); - - expect(BrowserPopupUtils.openCurrentPagePopout).toHaveBeenCalled(); - })); - - it("navigateToImport does not popout when popup is not open", fakeAsync(async () => { - (BrowserApi.isPopupOpen as jest.Mock).mockResolvedValueOnce(false); - - const ngRouter = TestBed.inject(Router); - jest.spyOn(ngRouter, "navigate").mockResolvedValue(true as any); - - await component["navigateToImport"](); - - expect(ngRouter.navigate).toHaveBeenCalledWith(["/import"]); - expect(BrowserPopupUtils.openCurrentPagePopout).not.toHaveBeenCalled(); })); it("ngOnInit dismisses intro carousel and opens decryption dialog for non-deleted failures", fakeAsync(() => { diff --git a/apps/browser/src/vault/popup/components/vault-v2/vault-v2.component.ts b/apps/browser/src/vault/popup/components/vault-v2/vault-v2.component.ts index fce084542a9..a5a74eb8ab8 100644 --- a/apps/browser/src/vault/popup/components/vault-v2/vault-v2.component.ts +++ b/apps/browser/src/vault/popup/components/vault-v2/vault-v2.component.ts @@ -56,8 +56,6 @@ import { } from "@bitwarden/vault"; import { CurrentAccountComponent } from "../../../../auth/popup/account-switching/current-account.component"; -import { BrowserApi } from "../../../../platform/browser/browser-api"; -import BrowserPopupUtils from "../../../../platform/browser/browser-popup-utils"; import { PopOutComponent } from "../../../../platform/popup/components/pop-out.component"; import { PopupHeaderComponent } from "../../../../platform/popup/layout/popup-header.component"; import { PopupPageComponent } from "../../../../platform/popup/layout/popup-page.component"; @@ -370,9 +368,6 @@ export class VaultV2Component implements OnInit, OnDestroy { async navigateToImport() { await this.router.navigate(["/import"]); - if (await BrowserApi.isPopupOpen()) { - await BrowserPopupUtils.openCurrentPagePopout(window); - } } async dismissVaultNudgeSpotlight(type: NudgeType) { diff --git a/apps/browser/src/vault/popup/settings/vault-settings-v2.component.html b/apps/browser/src/vault/popup/settings/vault-settings-v2.component.html index ad009c7a60b..c84188af863 100644 --- a/apps/browser/src/vault/popup/settings/vault-settings-v2.component.html +++ b/apps/browser/src/vault/popup/settings/vault-settings-v2.component.html @@ -13,7 +13,7 @@ - diff --git a/apps/browser/src/vault/popup/settings/vault-settings-v2.component.ts b/apps/browser/src/vault/popup/settings/vault-settings-v2.component.ts index c1d90d678cb..c35345bd8ab 100644 --- a/apps/browser/src/vault/popup/settings/vault-settings-v2.component.ts +++ b/apps/browser/src/vault/popup/settings/vault-settings-v2.component.ts @@ -15,8 +15,6 @@ import { PremiumUpgradePromptService } from "@bitwarden/common/vault/abstraction import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction"; import { BadgeComponent, ItemModule, ToastOptions, ToastService } from "@bitwarden/components"; -import { BrowserApi } from "../../../platform/browser/browser-api"; -import BrowserPopupUtils from "../../../platform/browser/browser-popup-utils"; import { PopOutComponent } from "../../../platform/popup/components/pop-out.component"; import { PopupHeaderComponent } from "../../../platform/popup/layout/popup-header.component"; import { PopupPageComponent } from "../../../platform/popup/layout/popup-page.component"; @@ -90,9 +88,6 @@ export class VaultSettingsV2Component implements OnInit, OnDestroy { async import() { await this.router.navigate(["/import"]); - if (await BrowserApi.isPopupOpen()) { - await BrowserPopupUtils.openCurrentPagePopout(window); - } } async sync() { diff --git a/libs/tools/send/send-ui/src/new-send-dropdown/new-send-dropdown.component.html b/libs/tools/send/send-ui/src/new-send-dropdown/new-send-dropdown.component.html index fb9b82c44e5..7b966bb0345 100644 --- a/libs/tools/send/send-ui/src/new-send-dropdown/new-send-dropdown.component.html +++ b/libs/tools/send/send-ui/src/new-send-dropdown/new-send-dropdown.component.html @@ -7,11 +7,13 @@ {{ "sendTypeText" | i18n }} - +
{{ "sendTypeFile" | i18n }}
+ {{ "popOutNewWindow" | i18n }} +
From 5c7bba00f3785148e424791f6163a0b1d1327f45 Mon Sep 17 00:00:00 2001 From: Jared Date: Thu, 5 Feb 2026 10:58:42 -0500 Subject: [PATCH 26/26] [PM-16694] ac integration page background fill missing (#18508) * Fixing some tech debt before implementing actual fix of implementation * Adding new components to handle the different routes for the integrations page to make use of bit-tab-nav-bar to follow background-fill UI spec * Implement organization integrations page with routing and state management - Added routing for organization integrations including device management, event management, single sign-on, and user provisioning. - Created OrganizationIntegrationsState to manage integrations and organization data. - Introduced OrganizationIntegrationsResolver for preloading organization and integration data. - Updated components to utilize the new state management and resolver. - Refactored integration routes to follow updated naming conventions. * Refactor organization integrations components to use signals and observables; enhance async handling in templates and add debug logging * Enhance organization integrations module with routing updates and state management improvements - Added OrganizationIntegrationsState for better state management. - Updated routing to redirect to single sign-on by default. - Integrated OrganizationIntegrationsResolver for preloading data. - Refactored components to utilize new state management and improved async handling. * Refactor SingleSignOnComponent to remove OnInit lifecycle and debug logging - Simplified SingleSignOnComponent by removing the OnInit implementation. - Eliminated debug logging for integrations in ngOnInit. - Cleaned up imports for better readability. * Refactor WebHeaderComponent to simplify background handling - Removed the useAltBackground input signal from WebHeaderComponent. - Updated the HTML template to conditionally apply styles based solely on the child element count of the tabs container. * Refactor organization integrations components for improved readability and performance - Updated HTML templates to remove optional chaining for organization properties. - Removed unnecessary debug logging and comments in the OrganizationIntegrationsResolver. - Simplified DeviceManagementComponent by eliminating the OnInit lifecycle hook. * Refactor organization integrations components to use direct state properties - Updated components to access organization and integrations directly from state instead of using observables. - Simplified HTML templates by removing async pipes and using direct function calls for better readability. - Ensured consistent naming conventions for organization and integrations variables across components. * Enhance WebHeaderComponent by adding bitTypography attribute to the title element for improved styling consistency * Refactor organization state to use 'undefined' instead of 'null' for organization signal and remove OnInit lifecycle hook from UserProvisioningComponent for cleaner code. * Refactor EventManagementComponent to remove OnInit lifecycle hook for cleaner code and improved readability. * Update organization state to set organization value to 'undefined' when null is provided, enhancing state management consistency. * Update WebHeaderComponent to allow optional title and icon inputs, enhancing flexibility in header configuration. * Update WebHeaderComponent to allow account property to be nullable, improving type safety and handling of user data. --- .../organization-layout.component.html | 2 +- .../layouts/header/web-header.component.html | 6 +- .../layouts/header/web-header.component.ts | 14 +- .../device-management.component.html | 11 + .../device-management.component.ts | 25 ++ .../event-management.component.html | 11 + .../event-management.component.ts | 24 ++ .../integrations.component.html | 96 +---- .../integrations.component.ts | 328 +----------------- .../integrations.pipe.ts | 5 +- ...rganization-integrations-routing.module.ts | 17 +- .../organization-integrations.module.ts | 15 +- .../organization-integrations.resolver.ts | 285 +++++++++++++++ .../organization-integrations.state.ts | 22 ++ .../single-sign-on.component.html | 12 + .../single-sign-on.component.ts | 22 ++ .../user-provisioning.component.html | 25 ++ .../user-provisioning.component.ts | 26 ++ 18 files changed, 528 insertions(+), 418 deletions(-) create mode 100644 bitwarden_license/bit-web/src/app/dirt/organization-integrations/device-management/device-management.component.html create mode 100644 bitwarden_license/bit-web/src/app/dirt/organization-integrations/device-management/device-management.component.ts create mode 100644 bitwarden_license/bit-web/src/app/dirt/organization-integrations/event-management/event-management.component.html create mode 100644 bitwarden_license/bit-web/src/app/dirt/organization-integrations/event-management/event-management.component.ts create mode 100644 bitwarden_license/bit-web/src/app/dirt/organization-integrations/organization-integrations.resolver.ts create mode 100644 bitwarden_license/bit-web/src/app/dirt/organization-integrations/organization-integrations.state.ts create mode 100644 bitwarden_license/bit-web/src/app/dirt/organization-integrations/single-sign-on/single-sign-on.component.html create mode 100644 bitwarden_license/bit-web/src/app/dirt/organization-integrations/single-sign-on/single-sign-on.component.ts create mode 100644 bitwarden_license/bit-web/src/app/dirt/organization-integrations/user-provisioning/user-provisioning.component.html create mode 100644 bitwarden_license/bit-web/src/app/dirt/organization-integrations/user-provisioning/user-provisioning.component.ts diff --git a/apps/web/src/app/admin-console/organizations/layouts/organization-layout.component.html b/apps/web/src/app/admin-console/organizations/layouts/organization-layout.component.html index 198cb3a47cd..79cef26042d 100644 --- a/apps/web/src/app/admin-console/organizations/layouts/organization-layout.component.html +++ b/apps/web/src/app/admin-console/organizations/layouts/organization-layout.component.html @@ -79,7 +79,7 @@
- - {{ title || (routeData.titleId | i18n) }} + + {{ title() || (routeData.titleId | i18n) }}
diff --git a/apps/web/src/app/layouts/header/web-header.component.ts b/apps/web/src/app/layouts/header/web-header.component.ts index 694ee5c4ae9..45ed32e61bb 100644 --- a/apps/web/src/app/layouts/header/web-header.component.ts +++ b/apps/web/src/app/layouts/header/web-header.component.ts @@ -1,6 +1,4 @@ -// FIXME: Update this file to be type safe and remove this and next line -// @ts-strict-ignore -import { Component, Input } from "@angular/core"; +import { Component, input, InputSignal } from "@angular/core"; import { ActivatedRoute } from "@angular/router"; import { map, Observable } from "rxjs"; @@ -25,19 +23,15 @@ export class WebHeaderComponent { /** * Custom title that overrides the route data `titleId` */ - // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals - // eslint-disable-next-line @angular-eslint/prefer-signals - @Input() title: string; + readonly title: InputSignal = input(); /** * Icon to show before the title */ - // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals - // eslint-disable-next-line @angular-eslint/prefer-signals - @Input() icon: string; + readonly icon: InputSignal = input(); protected routeData$: Observable<{ titleId: string }>; - protected account$: Observable; + protected account$: Observable<(User & { id: UserId }) | null>; protected canLock$: Observable; protected selfHosted: boolean; protected hostname = location.hostname; diff --git a/bitwarden_license/bit-web/src/app/dirt/organization-integrations/device-management/device-management.component.html b/bitwarden_license/bit-web/src/app/dirt/organization-integrations/device-management/device-management.component.html new file mode 100644 index 00000000000..6c04ea87960 --- /dev/null +++ b/bitwarden_license/bit-web/src/app/dirt/organization-integrations/device-management/device-management.component.html @@ -0,0 +1,11 @@ +@let integrationsList = integrations(); + +
+

+ {{ "deviceManagement" | i18n }} +

+

{{ "deviceManagementDesc" | i18n }}

+ +
diff --git a/bitwarden_license/bit-web/src/app/dirt/organization-integrations/device-management/device-management.component.ts b/bitwarden_license/bit-web/src/app/dirt/organization-integrations/device-management/device-management.component.ts new file mode 100644 index 00000000000..18e6dc7e362 --- /dev/null +++ b/bitwarden_license/bit-web/src/app/dirt/organization-integrations/device-management/device-management.component.ts @@ -0,0 +1,25 @@ +import { Component } from "@angular/core"; + +import { IntegrationType } from "@bitwarden/common/enums/integration-type.enum"; +import { SharedModule } from "@bitwarden/web-vault/app/shared"; + +import { IntegrationGridComponent } from "../integration-grid/integration-grid.component"; +import { FilterIntegrationsPipe } from "../integrations.pipe"; +import { OrganizationIntegrationsState } from "../organization-integrations.state"; + +// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush +// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection +@Component({ + selector: "device-management", + templateUrl: "device-management.component.html", + imports: [SharedModule, IntegrationGridComponent, FilterIntegrationsPipe], +}) +export class DeviceManagementComponent { + integrations = this.state.integrations; + + constructor(private state: OrganizationIntegrationsState) {} + + get IntegrationType(): typeof IntegrationType { + return IntegrationType; + } +} diff --git a/bitwarden_license/bit-web/src/app/dirt/organization-integrations/event-management/event-management.component.html b/bitwarden_license/bit-web/src/app/dirt/organization-integrations/event-management/event-management.component.html new file mode 100644 index 00000000000..9a767e52c8b --- /dev/null +++ b/bitwarden_license/bit-web/src/app/dirt/organization-integrations/event-management/event-management.component.html @@ -0,0 +1,11 @@ +@let integrationsList = integrations(); + +
+

+ {{ "eventManagement" | i18n }} +

+

{{ "eventManagementDesc" | i18n }}

+ +
diff --git a/bitwarden_license/bit-web/src/app/dirt/organization-integrations/event-management/event-management.component.ts b/bitwarden_license/bit-web/src/app/dirt/organization-integrations/event-management/event-management.component.ts new file mode 100644 index 00000000000..70b17cabd35 --- /dev/null +++ b/bitwarden_license/bit-web/src/app/dirt/organization-integrations/event-management/event-management.component.ts @@ -0,0 +1,24 @@ +import { Component } from "@angular/core"; + +import { IntegrationType } from "@bitwarden/common/enums/integration-type.enum"; +import { SharedModule } from "@bitwarden/web-vault/app/shared"; + +import { IntegrationGridComponent } from "../integration-grid/integration-grid.component"; +import { FilterIntegrationsPipe } from "../integrations.pipe"; +import { OrganizationIntegrationsState } from "../organization-integrations.state"; + +// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush +// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection +@Component({ + selector: "event-management", + templateUrl: "event-management.component.html", + imports: [SharedModule, IntegrationGridComponent, FilterIntegrationsPipe], +}) +export class EventManagementComponent { + integrations = this.state.integrations; + constructor(private state: OrganizationIntegrationsState) {} + + get IntegrationType(): typeof IntegrationType { + return IntegrationType; + } +} diff --git a/bitwarden_license/bit-web/src/app/dirt/organization-integrations/integrations.component.html b/bitwarden_license/bit-web/src/app/dirt/organization-integrations/integrations.component.html index 14f20a0b71c..fbff31f026e 100644 --- a/bitwarden_license/bit-web/src/app/dirt/organization-integrations/integrations.component.html +++ b/bitwarden_license/bit-web/src/app/dirt/organization-integrations/integrations.component.html @@ -1,82 +1,18 @@ - +@let org = organization(); -@let organization = organization$ | async; + + @if (org) { + + {{ "singleSignOn" | i18n }} + @if (org.useScim || org.useDirectory) { + {{ "userProvisioning" | i18n }} + } + @if (org.useEvents) { + {{ "eventManagement" | i18n }} + } + {{ "deviceManagement" | i18n }} + + } + -@if (organization) { - - @if (organization?.useSso) { - -
-

{{ "singleSignOn" | i18n }}

-

- {{ "ssoDescStart" | i18n }} - {{ - "singleSignOn" | i18n - }} - {{ "ssoDescEnd" | i18n }} -

- -
-
- } - - @if (organization?.useScim || organization?.useDirectory) { - - @if (organization?.useScim) { -
-

- {{ "scimIntegration" | i18n }} -

-

- {{ "scimIntegrationDescStart" | i18n }} - {{ "scimIntegration" | i18n }} - {{ "scimIntegrationDescEnd" | i18n }} -

- -
- } - @if (organization?.useDirectory) { -
-

- {{ "bwdc" | i18n }} -

-

{{ "bwdcDesc" | i18n }}

- -
- } -
- } - - @if (organization?.useEvents) { - -
-

- {{ "eventManagement" | i18n }} -

-

{{ "eventManagementDesc" | i18n }}

- -
-
- } - - -
-

- {{ "deviceManagement" | i18n }} -

-

{{ "deviceManagementDesc" | i18n }}

- -
-
-
-} + diff --git a/bitwarden_license/bit-web/src/app/dirt/organization-integrations/integrations.component.ts b/bitwarden_license/bit-web/src/app/dirt/organization-integrations/integrations.component.ts index 5485410f735..786aa70bfc5 100644 --- a/bitwarden_license/bit-web/src/app/dirt/organization-integrations/integrations.component.ts +++ b/bitwarden_license/bit-web/src/app/dirt/organization-integrations/integrations.component.ts @@ -1,336 +1,22 @@ -import { Component, OnDestroy, OnInit } from "@angular/core"; -import { ActivatedRoute } from "@angular/router"; -import { firstValueFrom, Observable, Subject, switchMap, takeUntil, takeWhile } from "rxjs"; +import { Component } from "@angular/core"; -import { Integration } from "@bitwarden/bit-common/dirt/organization-integrations/models/integration"; -import { OrganizationIntegrationServiceName } from "@bitwarden/bit-common/dirt/organization-integrations/models/organization-integration-service-type"; -import { OrganizationIntegrationType } from "@bitwarden/bit-common/dirt/organization-integrations/models/organization-integration-type"; -import { OrganizationIntegrationService } from "@bitwarden/bit-common/dirt/organization-integrations/services/organization-integration-service"; -import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; -import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; -import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; -import { getUserId } from "@bitwarden/common/auth/services/account.service"; -import { IntegrationType } from "@bitwarden/common/enums"; -import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; -import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; -import { getById } from "@bitwarden/common/platform/misc"; +import { IntegrationType } from "@bitwarden/common/enums/integration-type.enum"; import { HeaderModule } from "@bitwarden/web-vault/app/layouts/header/header.module"; import { SharedModule } from "@bitwarden/web-vault/app/shared"; -import { IntegrationGridComponent } from "./integration-grid/integration-grid.component"; -import { FilterIntegrationsPipe } from "./integrations.pipe"; +import { OrganizationIntegrationsState } from "./organization-integrations.state"; -// attempted, but because bit-tab-group is not OnPush, caused more issues than it solved // FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush // eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ selector: "ac-integrations", templateUrl: "./integrations.component.html", - imports: [SharedModule, IntegrationGridComponent, HeaderModule, FilterIntegrationsPipe], + imports: [SharedModule, HeaderModule], }) -export class AdminConsoleIntegrationsComponent implements OnInit, OnDestroy { - tabIndex: number = 0; - organization$: Observable = new Observable(); - isEventManagementForDataDogAndCrowdStrikeEnabled: boolean = false; - isEventManagementForHuntressEnabled: boolean = false; - private destroy$ = new Subject(); +export class AdminConsoleIntegrationsComponent { + organization = this.state.organization; - // initialize the integrations list with default integrations - integrationsList: Integration[] = [ - { - name: "AD FS", - linkURL: "https://bitwarden.com/help/saml-adfs/", - image: "../../../../../../../images/integrations/azure-active-directory.svg", - type: IntegrationType.SSO, - }, - { - name: "Auth0", - linkURL: "https://bitwarden.com/help/saml-auth0/", - image: "../../../../../../../images/integrations/logo-auth0-badge-color.svg", - type: IntegrationType.SSO, - }, - { - name: "AWS", - linkURL: "https://bitwarden.com/help/saml-aws/", - image: "../../../../../../../images/integrations/aws-color.svg", - imageDarkMode: "../../../../../../../images/integrations/aws-darkmode.svg", - type: IntegrationType.SSO, - }, - { - name: "Microsoft Entra ID", - linkURL: "https://bitwarden.com/help/saml-azure/", - image: "../../../../../../../images/integrations/logo-microsoft-entra-id-color.svg", - type: IntegrationType.SSO, - }, - { - name: "Duo", - linkURL: "https://bitwarden.com/help/saml-duo/", - image: "../../../../../../../images/integrations/logo-duo-color.svg", - type: IntegrationType.SSO, - }, - { - name: "Google", - linkURL: "https://bitwarden.com/help/saml-google/", - image: "../../../../../../../images/integrations/logo-google-badge-color.svg", - type: IntegrationType.SSO, - }, - { - name: "JumpCloud", - linkURL: "https://bitwarden.com/help/saml-jumpcloud/", - image: "../../../../../../../images/integrations/logo-jumpcloud-badge-color.svg", - imageDarkMode: "../../../../../../../images/integrations/jumpcloud-darkmode.svg", - type: IntegrationType.SSO, - }, - { - name: "KeyCloak", - linkURL: "https://bitwarden.com/help/saml-keycloak/", - image: "../../../../../../../images/integrations/logo-keycloak-icon.svg", - type: IntegrationType.SSO, - }, - { - name: "Okta", - linkURL: "https://bitwarden.com/help/saml-okta/", - image: "../../../../../../../images/integrations/logo-okta-symbol-black.svg", - imageDarkMode: "../../../../../../../images/integrations/okta-darkmode.svg", - type: IntegrationType.SSO, - }, - { - name: "OneLogin", - linkURL: "https://bitwarden.com/help/saml-onelogin/", - image: "../../../../../../../images/integrations/logo-onelogin-badge-color.svg", - imageDarkMode: "../../../../../../../images/integrations/onelogin-darkmode.svg", - type: IntegrationType.SSO, - }, - { - name: "PingFederate", - linkURL: "https://bitwarden.com/help/saml-pingfederate/", - image: "../../../../../../../images/integrations/logo-ping-identity-badge-color.svg", - type: IntegrationType.SSO, - }, - { - name: "Microsoft Entra ID", - linkURL: "https://bitwarden.com/help/microsoft-entra-id-scim-integration/", - image: "../../../../../../../images/integrations/logo-microsoft-entra-id-color.svg", - type: IntegrationType.SCIM, - }, - { - name: "Okta", - linkURL: "https://bitwarden.com/help/okta-scim-integration/", - image: "../../../../../../../images/integrations/logo-okta-symbol-black.svg", - imageDarkMode: "../../../../../../../images/integrations/okta-darkmode.svg", - type: IntegrationType.SCIM, - }, - { - name: "OneLogin", - linkURL: "https://bitwarden.com/help/onelogin-scim-integration/", - image: "../../../../../../../images/integrations/logo-onelogin-badge-color.svg", - imageDarkMode: "../../../../../../../images/integrations/onelogin-darkmode.svg", - type: IntegrationType.SCIM, - }, - { - name: "JumpCloud", - linkURL: "https://bitwarden.com/help/jumpcloud-scim-integration/", - image: "../../../../../../../images/integrations/logo-jumpcloud-badge-color.svg", - imageDarkMode: "../../../../../../../images/integrations/jumpcloud-darkmode.svg", - type: IntegrationType.SCIM, - }, - { - name: "Ping Identity", - linkURL: "https://bitwarden.com/help/ping-identity-scim-integration/", - image: "../../../../../../../images/integrations/logo-ping-identity-badge-color.svg", - type: IntegrationType.SCIM, - }, - { - name: "Active Directory", - linkURL: "https://bitwarden.com/help/ldap-directory/", - image: "../../../../../../../images/integrations/azure-active-directory.svg", - type: IntegrationType.BWDC, - }, - { - name: "Microsoft Entra ID", - linkURL: "https://bitwarden.com/help/microsoft-entra-id/", - image: "../../../../../../../images/integrations/logo-microsoft-entra-id-color.svg", - type: IntegrationType.BWDC, - }, - { - name: "Google Workspace", - linkURL: "https://bitwarden.com/help/workspace-directory/", - image: "../../../../../../../images/integrations/logo-google-badge-color.svg", - type: IntegrationType.BWDC, - }, - { - name: "Okta", - linkURL: "https://bitwarden.com/help/okta-directory/", - image: "../../../../../../../images/integrations/logo-okta-symbol-black.svg", - imageDarkMode: "../../../../../../../images/integrations/okta-darkmode.svg", - type: IntegrationType.BWDC, - }, - { - name: "OneLogin", - linkURL: "https://bitwarden.com/help/onelogin-directory/", - image: "../../../../../../../images/integrations/logo-onelogin-badge-color.svg", - imageDarkMode: "../../../../../../../images/integrations/onelogin-darkmode.svg", - type: IntegrationType.BWDC, - }, - { - name: "Splunk", - linkURL: "https://bitwarden.com/help/splunk-siem/", - image: "../../../../../../../images/integrations/logo-splunk-black.svg", - imageDarkMode: "../../../../../../../images/integrations/splunk-darkmode.svg", - type: IntegrationType.EVENT, - }, - { - name: "Microsoft Sentinel", - linkURL: "https://bitwarden.com/help/microsoft-sentinel-siem/", - image: "../../../../../../../images/integrations/logo-microsoft-sentinel-color.svg", - type: IntegrationType.EVENT, - }, - { - name: "Rapid7", - linkURL: "https://bitwarden.com/help/rapid7-siem/", - image: "../../../../../../../images/integrations/logo-rapid7-black.svg", - imageDarkMode: "../../../../../../../images/integrations/rapid7-darkmode.svg", - type: IntegrationType.EVENT, - }, - { - name: "Elastic", - linkURL: "https://bitwarden.com/help/elastic-siem/", - image: "../../../../../../../images/integrations/logo-elastic-badge-color.svg", - type: IntegrationType.EVENT, - }, - { - name: "Panther", - linkURL: "https://bitwarden.com/help/panther-siem/", - image: "../../../../../../../images/integrations/logo-panther-round-color.svg", - type: IntegrationType.EVENT, - }, - { - name: "Sumo Logic", - linkURL: "https://bitwarden.com/help/sumo-logic-siem/", - image: "../../../../../../../images/integrations/logo-sumo-logic-siem.svg", - imageDarkMode: "../../../../../../../images/integrations/logo-sumo-logic-siem-darkmode.svg", - type: IntegrationType.EVENT, - newBadgeExpiration: "2025-12-31", - }, - { - name: "Microsoft Intune", - linkURL: "https://bitwarden.com/help/deploy-browser-extensions-with-intune/", - image: "../../../../../../../images/integrations/logo-microsoft-intune-color.svg", - type: IntegrationType.DEVICE, - }, - ]; - - async ngOnInit() { - const userId = await firstValueFrom(getUserId(this.accountService.activeAccount$)); - if (!userId) { - throw new Error("User ID not found"); - } - - this.organization$ = this.route.params.pipe( - switchMap((params) => - this.organizationService.organizations$(userId).pipe( - getById(params.organizationId), - // Filter out undefined values - takeWhile((org: Organization | undefined) => !!org), - ), - ), - ); - - // Sets the organization ID which also loads the integrations$ - this.organization$ - .pipe( - switchMap((org) => this.organizationIntegrationService.setOrganizationId(org.id)), - takeUntil(this.destroy$), - ) - .subscribe(); - } - - constructor( - private route: ActivatedRoute, - private organizationService: OrganizationService, - private accountService: AccountService, - private configService: ConfigService, - private organizationIntegrationService: OrganizationIntegrationService, - ) { - this.configService - .getFeatureFlag$(FeatureFlag.EventManagementForDataDogAndCrowdStrike) - .pipe(takeUntil(this.destroy$)) - .subscribe((isEnabled) => { - this.isEventManagementForDataDogAndCrowdStrikeEnabled = isEnabled; - }); - - this.configService - .getFeatureFlag$(FeatureFlag.EventManagementForHuntress) - .pipe(takeUntil(this.destroy$)) - .subscribe((isEnabled) => { - this.isEventManagementForHuntressEnabled = isEnabled; - }); - - // Add the new event based items to the list - if (this.isEventManagementForDataDogAndCrowdStrikeEnabled) { - const crowdstrikeIntegration: Integration = { - name: OrganizationIntegrationServiceName.CrowdStrike, - linkURL: "https://bitwarden.com/help/crowdstrike-siem/", - image: "../../../../../../../images/integrations/logo-crowdstrike-black.svg", - type: IntegrationType.EVENT, - description: "crowdstrikeEventIntegrationDesc", - canSetupConnection: true, - integrationType: OrganizationIntegrationType.Hec, - }; - - this.integrationsList.push(crowdstrikeIntegration); - - const datadogIntegration: Integration = { - name: OrganizationIntegrationServiceName.Datadog, - linkURL: "https://bitwarden.com/help/datadog-siem/", - image: "../../../../../../../images/integrations/logo-datadog-color.svg", - type: IntegrationType.EVENT, - description: "datadogEventIntegrationDesc", - canSetupConnection: true, - integrationType: OrganizationIntegrationType.Datadog, - }; - - this.integrationsList.push(datadogIntegration); - } - - // Add Huntress SIEM integration (separate feature flag) - if (this.isEventManagementForHuntressEnabled) { - const huntressIntegration: Integration = { - name: OrganizationIntegrationServiceName.Huntress, - linkURL: "https://bitwarden.com/help/huntress-siem/", - image: "../../../../../../../images/integrations/logo-huntress-siem.svg", - type: IntegrationType.EVENT, - description: "huntressEventIntegrationDesc", - canSetupConnection: true, - integrationType: OrganizationIntegrationType.Hec, - }; - - this.integrationsList.push(huntressIntegration); - } - - // For all existing event based configurations loop through and assign the - // organizationIntegration for the correct services. - this.organizationIntegrationService.integrations$ - .pipe(takeUntil(this.destroy$)) - .subscribe((integrations) => { - // reset all event based integrations to null first - in case one was deleted - this.integrationsList.forEach((i) => { - i.organizationIntegration = null; - }); - - integrations.forEach((integration) => { - const item = this.integrationsList.find((i) => i.name === integration.serviceName); - if (item) { - item.organizationIntegration = integration; - } - }); - }); - } - - ngOnDestroy(): void { - this.destroy$.next(); - this.destroy$.complete(); - } + constructor(private state: OrganizationIntegrationsState) {} // use in the view get IntegrationType(): typeof IntegrationType { diff --git a/bitwarden_license/bit-web/src/app/dirt/organization-integrations/integrations.pipe.ts b/bitwarden_license/bit-web/src/app/dirt/organization-integrations/integrations.pipe.ts index 7a420ade4b5..10ee251a921 100644 --- a/bitwarden_license/bit-web/src/app/dirt/organization-integrations/integrations.pipe.ts +++ b/bitwarden_license/bit-web/src/app/dirt/organization-integrations/integrations.pipe.ts @@ -7,7 +7,10 @@ import { IntegrationType } from "@bitwarden/common/enums"; name: "filterIntegrations", }) export class FilterIntegrationsPipe implements PipeTransform { - transform(integrations: Integration[], type: IntegrationType): Integration[] { + transform(integrations: Integration[] | null | undefined, type: IntegrationType): Integration[] { + if (!integrations) { + return []; + } return integrations.filter((integration) => integration.type === type); } } diff --git a/bitwarden_license/bit-web/src/app/dirt/organization-integrations/organization-integrations-routing.module.ts b/bitwarden_license/bit-web/src/app/dirt/organization-integrations/organization-integrations-routing.module.ts index 1667689b186..626fc5dee88 100644 --- a/bitwarden_license/bit-web/src/app/dirt/organization-integrations/organization-integrations-routing.module.ts +++ b/bitwarden_license/bit-web/src/app/dirt/organization-integrations/organization-integrations-routing.module.ts @@ -3,16 +3,31 @@ import { RouterModule, Routes } from "@angular/router"; import { organizationPermissionsGuard } from "@bitwarden/web-vault/app/admin-console/organizations/guards/org-permissions.guard"; +import { DeviceManagementComponent } from "./device-management/device-management.component"; +import { EventManagementComponent } from "./event-management/event-management.component"; import { AdminConsoleIntegrationsComponent } from "./integrations.component"; +import { OrganizationIntegrationsResolver } from "./organization-integrations.resolver"; +import { OrganizationIntegrationsState } from "./organization-integrations.state"; +import { SingleSignOnComponent } from "./single-sign-on/single-sign-on.component"; +import { UserProvisioningComponent } from "./user-provisioning/user-provisioning.component"; const routes: Routes = [ { path: "", canActivate: [organizationPermissionsGuard((org) => org.canAccessIntegrations)], - component: AdminConsoleIntegrationsComponent, data: { titleId: "integrations", }, + component: AdminConsoleIntegrationsComponent, + providers: [OrganizationIntegrationsState, OrganizationIntegrationsResolver], + resolve: { integrations: OrganizationIntegrationsResolver }, + children: [ + { path: "", redirectTo: "single-sign-on", pathMatch: "full" }, + { path: "single-sign-on", component: SingleSignOnComponent }, + { path: "user-provisioning", component: UserProvisioningComponent }, + { path: "event-management", component: EventManagementComponent }, + { path: "device-management", component: DeviceManagementComponent }, + ], }, ]; diff --git a/bitwarden_license/bit-web/src/app/dirt/organization-integrations/organization-integrations.module.ts b/bitwarden_license/bit-web/src/app/dirt/organization-integrations/organization-integrations.module.ts index 789ae548521..33f389a92a9 100644 --- a/bitwarden_license/bit-web/src/app/dirt/organization-integrations/organization-integrations.module.ts +++ b/bitwarden_license/bit-web/src/app/dirt/organization-integrations/organization-integrations.module.ts @@ -1,17 +1,30 @@ import { NgModule } from "@angular/core"; +import { DeviceManagementComponent } from "@bitwarden/angular/auth/device-management/device-management.component"; import { OrganizationIntegrationApiService } from "@bitwarden/bit-common/dirt/organization-integrations/services/organization-integration-api.service"; import { OrganizationIntegrationConfigurationApiService } from "@bitwarden/bit-common/dirt/organization-integrations/services/organization-integration-configuration-api.service"; import { OrganizationIntegrationService } from "@bitwarden/bit-common/dirt/organization-integrations/services/organization-integration-service"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { safeProvider } from "@bitwarden/ui-common"; +import { EventManagementComponent } from "./event-management/event-management.component"; import { AdminConsoleIntegrationsComponent } from "./integrations.component"; import { OrganizationIntegrationsRoutingModule } from "./organization-integrations-routing.module"; +import { OrganizationIntegrationsResolver } from "./organization-integrations.resolver"; +import { SingleSignOnComponent } from "./single-sign-on/single-sign-on.component"; +import { UserProvisioningComponent } from "./user-provisioning/user-provisioning.component"; @NgModule({ - imports: [AdminConsoleIntegrationsComponent, OrganizationIntegrationsRoutingModule], + imports: [ + AdminConsoleIntegrationsComponent, + OrganizationIntegrationsRoutingModule, + SingleSignOnComponent, + UserProvisioningComponent, + DeviceManagementComponent, + EventManagementComponent, + ], providers: [ + OrganizationIntegrationsResolver, safeProvider({ provide: OrganizationIntegrationService, useClass: OrganizationIntegrationService, diff --git a/bitwarden_license/bit-web/src/app/dirt/organization-integrations/organization-integrations.resolver.ts b/bitwarden_license/bit-web/src/app/dirt/organization-integrations/organization-integrations.resolver.ts new file mode 100644 index 00000000000..39bd0cc1dcc --- /dev/null +++ b/bitwarden_license/bit-web/src/app/dirt/organization-integrations/organization-integrations.resolver.ts @@ -0,0 +1,285 @@ +import { Injectable } from "@angular/core"; +import { ActivatedRouteSnapshot, Resolve } from "@angular/router"; +import { firstValueFrom } from "rxjs"; +import { take, takeWhile } from "rxjs/operators"; + +import { Integration } from "@bitwarden/bit-common/dirt/organization-integrations/models/integration"; +import { OrganizationIntegrationServiceName } from "@bitwarden/bit-common/dirt/organization-integrations/models/organization-integration-service-type"; +import { OrganizationIntegrationType } from "@bitwarden/bit-common/dirt/organization-integrations/models/organization-integration-type"; +import { OrganizationIntegrationService } from "@bitwarden/bit-common/dirt/organization-integrations/services/organization-integration-service"; +import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { getUserId } from "@bitwarden/common/auth/services/account.service"; +import { IntegrationType } from "@bitwarden/common/enums"; +import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; +import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; +import { getById } from "@bitwarden/common/platform/misc"; + +import { OrganizationIntegrationsState } from "./organization-integrations.state"; + +@Injectable() +export class OrganizationIntegrationsResolver implements Resolve { + constructor( + private organizationService: OrganizationService, + private accountService: AccountService, + private configService: ConfigService, + private organizationIntegrationService: OrganizationIntegrationService, + private state: OrganizationIntegrationsState, + ) {} + + async resolve(route: ActivatedRouteSnapshot): Promise { + const userId = await firstValueFrom(getUserId(this.accountService.activeAccount$)); + + if (!userId) { + throw new Error("User ID not found"); + } + + const orgId = route.paramMap.get("organizationId")!; + const org = await firstValueFrom( + this.organizationService.organizations$(userId).pipe(getById(orgId), takeWhile(Boolean)), + ); + + this.state.setOrganization(org); + + await firstValueFrom(this.organizationIntegrationService.setOrganizationId(org.id)); + + const integrations: Integration[] = [ + { + name: "AD FS", + linkURL: "https://bitwarden.com/help/saml-adfs/", + image: "../../../../../../../images/integrations/azure-active-directory.svg", + type: IntegrationType.SSO, + }, + { + name: "Auth0", + linkURL: "https://bitwarden.com/help/saml-auth0/", + image: "../../../../../../../images/integrations/logo-auth0-badge-color.svg", + type: IntegrationType.SSO, + }, + { + name: "AWS", + linkURL: "https://bitwarden.com/help/saml-aws/", + image: "../../../../../../../images/integrations/aws-color.svg", + imageDarkMode: "../../../../../../../images/integrations/aws-darkmode.svg", + type: IntegrationType.SSO, + }, + { + name: "Microsoft Entra ID", + linkURL: "https://bitwarden.com/help/saml-azure/", + image: "../../../../../../../images/integrations/logo-microsoft-entra-id-color.svg", + type: IntegrationType.SSO, + }, + { + name: "Duo", + linkURL: "https://bitwarden.com/help/saml-duo/", + image: "../../../../../../../images/integrations/logo-duo-color.svg", + type: IntegrationType.SSO, + }, + { + name: "Google", + linkURL: "https://bitwarden.com/help/saml-google/", + image: "../../../../../../../images/integrations/logo-google-badge-color.svg", + type: IntegrationType.SSO, + }, + { + name: "JumpCloud", + linkURL: "https://bitwarden.com/help/saml-jumpcloud/", + image: "../../../../../../../images/integrations/logo-jumpcloud-badge-color.svg", + imageDarkMode: "../../../../../../../images/integrations/jumpcloud-darkmode.svg", + type: IntegrationType.SSO, + }, + { + name: "KeyCloak", + linkURL: "https://bitwarden.com/help/saml-keycloak/", + image: "../../../../../../../images/integrations/logo-keycloak-icon.svg", + type: IntegrationType.SSO, + }, + { + name: "Okta", + linkURL: "https://bitwarden.com/help/saml-okta/", + image: "../../../../../../../images/integrations/logo-okta-symbol-black.svg", + imageDarkMode: "../../../../../../../images/integrations/okta-darkmode.svg", + type: IntegrationType.SSO, + }, + { + name: "OneLogin", + linkURL: "https://bitwarden.com/help/saml-onelogin/", + image: "../../../../../../../images/integrations/logo-onelogin-badge-color.svg", + imageDarkMode: "../../../../../../../images/integrations/onelogin-darkmode.svg", + type: IntegrationType.SSO, + }, + { + name: "PingFederate", + linkURL: "https://bitwarden.com/help/saml-pingfederate/", + image: "../../../../../../../images/integrations/logo-ping-identity-badge-color.svg", + type: IntegrationType.SSO, + }, + { + name: "Microsoft Entra ID", + linkURL: "https://bitwarden.com/help/microsoft-entra-id-scim-integration/", + image: "../../../../../../../images/integrations/logo-microsoft-entra-id-color.svg", + type: IntegrationType.SCIM, + }, + { + name: "Okta", + linkURL: "https://bitwarden.com/help/okta-scim-integration/", + image: "../../../../../../../images/integrations/logo-okta-symbol-black.svg", + imageDarkMode: "../../../../../../../images/integrations/okta-darkmode.svg", + type: IntegrationType.SCIM, + }, + { + name: "OneLogin", + linkURL: "https://bitwarden.com/help/onelogin-scim-integration/", + image: "../../../../../../../images/integrations/logo-onelogin-badge-color.svg", + imageDarkMode: "../../../../../../../images/integrations/onelogin-darkmode.svg", + type: IntegrationType.SCIM, + }, + { + name: "JumpCloud", + linkURL: "https://bitwarden.com/help/jumpcloud-scim-integration/", + image: "../../../../../../../images/integrations/logo-jumpcloud-badge-color.svg", + imageDarkMode: "../../../../../../../images/integrations/jumpcloud-darkmode.svg", + type: IntegrationType.SCIM, + }, + { + name: "Ping Identity", + linkURL: "https://bitwarden.com/help/ping-identity-scim-integration/", + image: "../../../../../../../images/integrations/logo-ping-identity-badge-color.svg", + type: IntegrationType.SCIM, + }, + { + name: "Active Directory", + linkURL: "https://bitwarden.com/help/ldap-directory/", + image: "../../../../../../../images/integrations/azure-active-directory.svg", + type: IntegrationType.BWDC, + }, + { + name: "Microsoft Entra ID", + linkURL: "https://bitwarden.com/help/microsoft-entra-id/", + image: "../../../../../../../images/integrations/logo-microsoft-entra-id-color.svg", + type: IntegrationType.BWDC, + }, + { + name: "Google Workspace", + linkURL: "https://bitwarden.com/help/workspace-directory/", + image: "../../../../../../../images/integrations/logo-google-badge-color.svg", + type: IntegrationType.BWDC, + }, + { + name: "Okta", + linkURL: "https://bitwarden.com/help/okta-directory/", + image: "../../../../../../../images/integrations/logo-okta-symbol-black.svg", + imageDarkMode: "../../../../../../../images/integrations/okta-darkmode.svg", + type: IntegrationType.BWDC, + }, + { + name: "OneLogin", + linkURL: "https://bitwarden.com/help/onelogin-directory/", + image: "../../../../../../../images/integrations/logo-onelogin-badge-color.svg", + imageDarkMode: "../../../../../../../images/integrations/onelogin-darkmode.svg", + type: IntegrationType.BWDC, + }, + { + name: "Splunk", + linkURL: "https://bitwarden.com/help/splunk-siem/", + image: "../../../../../../../images/integrations/logo-splunk-black.svg", + imageDarkMode: "../../../../../../../images/integrations/splunk-darkmode.svg", + type: IntegrationType.EVENT, + }, + { + name: "Microsoft Sentinel", + linkURL: "https://bitwarden.com/help/microsoft-sentinel-siem/", + image: "../../../../../../../images/integrations/logo-microsoft-sentinel-color.svg", + type: IntegrationType.EVENT, + }, + { + name: "Rapid7", + linkURL: "https://bitwarden.com/help/rapid7-siem/", + image: "../../../../../../../images/integrations/logo-rapid7-black.svg", + imageDarkMode: "../../../../../../../images/integrations/rapid7-darkmode.svg", + type: IntegrationType.EVENT, + }, + { + name: "Elastic", + linkURL: "https://bitwarden.com/help/elastic-siem/", + image: "../../../../../../../images/integrations/logo-elastic-badge-color.svg", + type: IntegrationType.EVENT, + }, + { + name: "Panther", + linkURL: "https://bitwarden.com/help/panther-siem/", + image: "../../../../../../../images/integrations/logo-panther-round-color.svg", + type: IntegrationType.EVENT, + }, + { + name: "Sumo Logic", + linkURL: "https://bitwarden.com/help/sumo-logic-siem/", + image: "../../../../../../../images/integrations/logo-sumo-logic-siem.svg", + imageDarkMode: "../../../../../../../images/integrations/logo-sumo-logic-siem-darkmode.svg", + type: IntegrationType.EVENT, + newBadgeExpiration: "2025-12-31", + }, + { + name: "Microsoft Intune", + linkURL: "https://bitwarden.com/help/deploy-browser-extensions-with-intune/", + image: "../../../../../../../images/integrations/logo-microsoft-intune-color.svg", + type: IntegrationType.DEVICE, + }, + ]; + + const featureEnabled = await firstValueFrom( + this.configService.getFeatureFlag$(FeatureFlag.EventManagementForDataDogAndCrowdStrike), + ); + + if (featureEnabled) { + integrations.push( + { + name: OrganizationIntegrationServiceName.CrowdStrike, + linkURL: "https://bitwarden.com/help/crowdstrike-siem/", + image: "../../../../../../../images/integrations/logo-crowdstrike-black.svg", + type: IntegrationType.EVENT, + canSetupConnection: true, + integrationType: OrganizationIntegrationType.Hec, + }, + { + name: OrganizationIntegrationServiceName.Datadog, + linkURL: "https://bitwarden.com/help/datadog-siem/", + image: "../../../../../../../images/integrations/logo-datadog-color.svg", + type: IntegrationType.EVENT, + canSetupConnection: true, + integrationType: OrganizationIntegrationType.Datadog, + }, + ); + } + + // Add Huntress SIEM integration (separate feature flag) + const huntressFeatureEnabled = await firstValueFrom( + this.configService.getFeatureFlag$(FeatureFlag.EventManagementForHuntress), + ); + + if (huntressFeatureEnabled) { + integrations.push({ + name: OrganizationIntegrationServiceName.Huntress, + linkURL: "https://bitwarden.com/help/huntress-siem/", + image: "../../../../../../../images/integrations/logo-huntress-siem.svg", + type: IntegrationType.EVENT, + description: "huntressEventIntegrationDesc", + canSetupConnection: true, + integrationType: OrganizationIntegrationType.Hec, + }); + } + + const orgIntegrations = await firstValueFrom( + this.organizationIntegrationService.integrations$.pipe(take(1)), + ); + + const merged = integrations.map((i) => ({ + ...i, + organizationIntegration: orgIntegrations.find((o) => o.serviceName === i.name) ?? null, + })); + + this.state.setIntegrations(merged); + + return true; + } +} diff --git a/bitwarden_license/bit-web/src/app/dirt/organization-integrations/organization-integrations.state.ts b/bitwarden_license/bit-web/src/app/dirt/organization-integrations/organization-integrations.state.ts new file mode 100644 index 00000000000..5e7e6a78ba4 --- /dev/null +++ b/bitwarden_license/bit-web/src/app/dirt/organization-integrations/organization-integrations.state.ts @@ -0,0 +1,22 @@ +import { Injectable, signal } from "@angular/core"; + +import { Integration } from "@bitwarden/bit-common/dirt/organization-integrations/models/integration"; +import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; + +@Injectable() +export class OrganizationIntegrationsState { + private readonly _integrations = signal([]); + private readonly _organization = signal(undefined); + + // Signals + integrations = this._integrations.asReadonly(); + organization = this._organization.asReadonly(); + + setOrganization(val: Organization | null) { + this._organization.set(val ?? undefined); + } + + setIntegrations(val: Integration[]) { + this._integrations.set(val); + } +} diff --git a/bitwarden_license/bit-web/src/app/dirt/organization-integrations/single-sign-on/single-sign-on.component.html b/bitwarden_license/bit-web/src/app/dirt/organization-integrations/single-sign-on/single-sign-on.component.html new file mode 100644 index 00000000000..ca5ed9ee30c --- /dev/null +++ b/bitwarden_license/bit-web/src/app/dirt/organization-integrations/single-sign-on/single-sign-on.component.html @@ -0,0 +1,12 @@ +@let integrationsList = integrations(); +
+

{{ "singleSignOn" | i18n }}

+

+ {{ "ssoDescStart" | i18n }} + {{ "singleSignOn" | i18n }} + {{ "ssoDescEnd" | i18n }} +

+ +
diff --git a/bitwarden_license/bit-web/src/app/dirt/organization-integrations/single-sign-on/single-sign-on.component.ts b/bitwarden_license/bit-web/src/app/dirt/organization-integrations/single-sign-on/single-sign-on.component.ts new file mode 100644 index 00000000000..d0d2a1666f2 --- /dev/null +++ b/bitwarden_license/bit-web/src/app/dirt/organization-integrations/single-sign-on/single-sign-on.component.ts @@ -0,0 +1,22 @@ +import { Component } from "@angular/core"; + +import { IntegrationType } from "@bitwarden/common/enums/integration-type.enum"; +import { SharedModule } from "@bitwarden/web-vault/app/shared"; + +import { IntegrationGridComponent } from "../integration-grid/integration-grid.component"; +import { FilterIntegrationsPipe } from "../integrations.pipe"; +import { OrganizationIntegrationsState } from "../organization-integrations.state"; + +// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush +// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection +@Component({ + selector: "single-sign-on", + templateUrl: "single-sign-on.component.html", + imports: [SharedModule, IntegrationGridComponent, FilterIntegrationsPipe], +}) +export class SingleSignOnComponent { + integrations = this.state.integrations; + IntegrationType = IntegrationType; + + constructor(private state: OrganizationIntegrationsState) {} +} diff --git a/bitwarden_license/bit-web/src/app/dirt/organization-integrations/user-provisioning/user-provisioning.component.html b/bitwarden_license/bit-web/src/app/dirt/organization-integrations/user-provisioning/user-provisioning.component.html new file mode 100644 index 00000000000..a254f334e21 --- /dev/null +++ b/bitwarden_license/bit-web/src/app/dirt/organization-integrations/user-provisioning/user-provisioning.component.html @@ -0,0 +1,25 @@ +@let org = organization(); +@let integrationsList = integrations(); + +
+

+ {{ "scimIntegration" | i18n }} +

+

+ {{ "scimIntegrationDescStart" | i18n }} + {{ "scimIntegration" | i18n }} + {{ "scimIntegrationDescEnd" | i18n }} +

+ +
+
+

+ {{ "bwdc" | i18n }} +

+

{{ "bwdcDesc" | i18n }}

+ +
diff --git a/bitwarden_license/bit-web/src/app/dirt/organization-integrations/user-provisioning/user-provisioning.component.ts b/bitwarden_license/bit-web/src/app/dirt/organization-integrations/user-provisioning/user-provisioning.component.ts new file mode 100644 index 00000000000..f484674d224 --- /dev/null +++ b/bitwarden_license/bit-web/src/app/dirt/organization-integrations/user-provisioning/user-provisioning.component.ts @@ -0,0 +1,26 @@ +import { Component } from "@angular/core"; + +import { IntegrationType } from "@bitwarden/common/enums/integration-type.enum"; +import { SharedModule } from "@bitwarden/web-vault/app/shared"; + +import { IntegrationGridComponent } from "../integration-grid/integration-grid.component"; +import { FilterIntegrationsPipe } from "../integrations.pipe"; +import { OrganizationIntegrationsState } from "../organization-integrations.state"; + +// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush +// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection +@Component({ + selector: "user-provisioning", + templateUrl: "user-provisioning.component.html", + imports: [SharedModule, IntegrationGridComponent, FilterIntegrationsPipe], +}) +export class UserProvisioningComponent { + organization = this.state.organization; + integrations = this.state.integrations; + + constructor(private state: OrganizationIntegrationsState) {} + + get IntegrationType(): typeof IntegrationType { + return IntegrationType; + } +}