-
-
0">
- {{ "reviewAtRiskPasswords" | i18n }}
-
- @let isRunningReport = dataService.isGeneratingReport$ | async;
-
-
- @if (dataLastUpdated) {
- {{
- "dataLastUpdated" | i18n: (dataLastUpdated | date: "MMMM d, y 'at' h:mm a")
- }}
- }
-
-
-
-
-
-
-
+
+ @if (isRiskInsightsActivityTabFeatureEnabled && !(dataService.hasReportData$ | async)) {
+
+
+ @if (!hasCiphers) {
+
+
+ } @else {
+
+
+ }
+ } @else {
+
+
+
+
0">
+ {{ "reviewAtRiskPasswords" | i18n }}
+
+ @let isRunningReport = dataService.isGeneratingReport$ | async;
+
+
+ @if (dataLastUpdated) {
+ {{
+ "dataLastUpdated" | i18n: (dataLastUpdated | date: "MMMM d, y 'at' h:mm a")
+ }}
+ }
+
+
+
+
+
+
+
+
- @if (status == ReportStatusEnum.Loading && isGeneratingReport) {
-
-
- } @else {
@if (isRiskInsightsActivityTabFeatureEnabled) {
@@ -105,8 +105,8 @@
- }
-
+
+ }
}
}
diff --git a/bitwarden_license/bit-web/src/app/dirt/access-intelligence/risk-insights.component.ts b/bitwarden_license/bit-web/src/app/dirt/access-intelligence/risk-insights.component.ts
index 9e6901572c3..18afdf8c8ab 100644
--- a/bitwarden_license/bit-web/src/app/dirt/access-intelligence/risk-insights.component.ts
+++ b/bitwarden_license/bit-web/src/app/dirt/access-intelligence/risk-insights.component.ts
@@ -6,16 +6,18 @@ import {
OnDestroy,
OnInit,
inject,
+ signal,
ChangeDetectionStrategy,
} from "@angular/core";
import { takeUntilDestroyed } from "@angular/core/rxjs-interop";
import { ActivatedRoute, Router } from "@angular/router";
-import { EMPTY, firstValueFrom } from "rxjs";
-import { distinctUntilChanged, map, tap } from "rxjs/operators";
+import { concat, EMPTY, firstValueFrom, of } from "rxjs";
+import { concatMap, delay, distinctUntilChanged, map, skip, tap } from "rxjs/operators";
import { JslibModule } from "@bitwarden/angular/jslib.module";
import {
DrawerType,
+ ReportProgress,
ReportStatus,
RiskInsightsDataService,
} from "@bitwarden/bit-common/dirt/reports/risk-insights";
@@ -42,8 +44,11 @@ import { CriticalApplicationsComponent } from "./critical-applications/critical-
import { EmptyStateCardComponent } from "./empty-state-card.component";
import { RiskInsightsTabType } from "./models/risk-insights.models";
import { PageLoadingComponent } from "./shared/page-loading.component";
+import { ReportLoadingComponent } from "./shared/report-loading.component";
import { RiskInsightsDrawerDialogComponent } from "./shared/risk-insights-drawer-dialog.component";
-import { ApplicationsLoadingComponent } from "./shared/risk-insights-loading.component";
+
+// Type alias for progress step (used in concatMap emissions)
+type ProgressStep = ReportProgress | null;
@Component({
changeDetection: ChangeDetectionStrategy.OnPush,
@@ -59,7 +64,7 @@ import { ApplicationsLoadingComponent } from "./shared/risk-insights-loading.com
HeaderModule,
TabsModule,
AllActivityComponent,
- ApplicationsLoadingComponent,
+ ReportLoadingComponent,
PageLoadingComponent,
],
animations: [
@@ -95,6 +100,13 @@ export class RiskInsightsComponent implements OnInit, OnDestroy {
protected IMPORT_ICON = "bwi bwi-download";
protected currentDialogRef: DialogRef
| null = null;
+ // Current progress step for loading component (null = not loading)
+ // Uses concatMap with delay to ensure each step is displayed for a minimum time
+ protected readonly currentProgressStep = signal(null);
+
+ // Minimum time to display each progress step (in milliseconds)
+ private readonly STEP_DISPLAY_DELAY_MS = 250;
+
// TODO: See https://github.com/bitwarden/clients/pull/16832#discussion_r2474523235
constructor(
@@ -170,6 +182,44 @@ export class RiskInsightsComponent implements OnInit, OnDestroy {
// this happens when navigating between orgs
// or just navigating away from the page and back
this.currentDialogRef?.close();
+
+ // Subscribe to progress steps with delay to ensure each step is displayed for a minimum time
+ // - skip(1): Skip initial BehaviorSubject emission (may contain stale Complete from previous run)
+ // - concatMap: Queue steps and process them sequentially
+ // - First visible step (FetchingMembers) shows immediately so loading appears instantly
+ // - Subsequent steps are delayed to prevent jarring quick transitions
+ // - After Complete step is shown, emit null to hide loading
+ this.dataService.reportProgress$
+ .pipe(
+ // Skip the initial emission from _reportProgressSubject (BehaviorSubject in orchestrator).
+ // Without this, navigating to the page would flash the loading component briefly
+ // because BehaviorSubject emits its current value (e.g., Complete from last run) to new subscribers.
+ skip(1),
+ concatMap((step) => {
+ // Show null and FetchingMembers immediately (first visible step)
+ // This ensures loading component appears instantly when user clicks "Run Report"
+ if (step === null || step === ReportProgress.FetchingMembers) {
+ return of(step);
+ }
+ // Delay subsequent steps to prevent jarring quick transitions
+ if (step === ReportProgress.Complete) {
+ // Show Complete step, wait, then emit null to hide loading
+ // Why concat is needed:
+ // - The orchestrator emits Complete but never emits null afterward
+ // - Without this concat, the loading would stay on "Compiling insights..." forever
+ // - The concat automatically emits null to hide the loader
+ return concat(
+ of(step as ProgressStep).pipe(delay(this.STEP_DISPLAY_DELAY_MS)),
+ of(null as ProgressStep).pipe(delay(this.STEP_DISPLAY_DELAY_MS)),
+ );
+ }
+ return of(step).pipe(delay(this.STEP_DISPLAY_DELAY_MS));
+ }),
+ takeUntilDestroyed(this.destroyRef),
+ )
+ .subscribe((step) => {
+ this.currentProgressStep.set(step);
+ });
}
ngOnDestroy(): void {
@@ -194,7 +244,9 @@ export class RiskInsightsComponent implements OnInit, OnDestroy {
queryParamsHandling: "merge",
});
- // close drawer when tabs are changed
+ // Reset drawer state and close drawer when tabs are changed
+ // This ensures card selection state is cleared (PM-29263)
+ this.dataService.closeDrawer();
this.currentDialogRef?.close();
}
diff --git a/bitwarden_license/bit-web/src/app/dirt/access-intelligence/shared/risk-insights-loading.component.html b/bitwarden_license/bit-web/src/app/dirt/access-intelligence/shared/report-loading.component.html
similarity index 86%
rename from bitwarden_license/bit-web/src/app/dirt/access-intelligence/shared/risk-insights-loading.component.html
rename to bitwarden_license/bit-web/src/app/dirt/access-intelligence/shared/report-loading.component.html
index 6e6bb786336..0b5a63c8f03 100644
--- a/bitwarden_license/bit-web/src/app/dirt/access-intelligence/shared/risk-insights-loading.component.html
+++ b/bitwarden_license/bit-web/src/app/dirt/access-intelligence/shared/report-loading.component.html
@@ -3,7 +3,7 @@
- {{ currentMessage() | i18n }}
+ {{ stepConfig[progressStep()].message | i18n }}
{{ "thisMightTakeFewMinutes" | i18n }}
diff --git a/bitwarden_license/bit-web/src/app/dirt/access-intelligence/shared/report-loading.component.ts b/bitwarden_license/bit-web/src/app/dirt/access-intelligence/shared/report-loading.component.ts
new file mode 100644
index 00000000000..f3cb89dff55
--- /dev/null
+++ b/bitwarden_license/bit-web/src/app/dirt/access-intelligence/shared/report-loading.component.ts
@@ -0,0 +1,33 @@
+import { CommonModule } from "@angular/common";
+import { Component, input } from "@angular/core";
+
+import { JslibModule } from "@bitwarden/angular/jslib.module";
+import { ReportProgress } from "@bitwarden/bit-common/dirt/reports/risk-insights";
+import { ProgressModule } from "@bitwarden/components";
+
+// Map of progress step to display config
+const ProgressStepConfig = Object.freeze({
+ [ReportProgress.FetchingMembers]: { message: "fetchingMemberData", progress: 20 },
+ [ReportProgress.AnalyzingPasswords]: { message: "analyzingPasswordHealth", progress: 40 },
+ [ReportProgress.CalculatingRisks]: { message: "calculatingRiskScores", progress: 60 },
+ [ReportProgress.GeneratingReport]: { message: "generatingReportData", progress: 80 },
+ [ReportProgress.Saving]: { message: "savingReport", progress: 95 },
+ [ReportProgress.Complete]: { message: "compilingInsights", progress: 100 },
+} as const);
+
+// 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: "dirt-report-loading",
+ imports: [CommonModule, JslibModule, ProgressModule],
+ templateUrl: "./report-loading.component.html",
+})
+export class ReportLoadingComponent {
+ // Progress step input from parent component.
+ // Recommended: delay emissions to this input to ensure each step displays for a minimum time.
+ // Refer to risk-insights.component for implementation example.
+ readonly progressStep = input(ReportProgress.FetchingMembers);
+
+ // Expose config map to template for direct lookup
+ protected readonly stepConfig = ProgressStepConfig;
+}
diff --git a/bitwarden_license/bit-web/src/app/dirt/access-intelligence/shared/risk-insights-drawer-dialog.component.html b/bitwarden_license/bit-web/src/app/dirt/access-intelligence/shared/risk-insights-drawer-dialog.component.html
index 3fa72358f25..63ff7fd07b6 100644
--- a/bitwarden_license/bit-web/src/app/dirt/access-intelligence/shared/risk-insights-drawer-dialog.component.html
+++ b/bitwarden_license/bit-web/src/app/dirt/access-intelligence/shared/risk-insights-drawer-dialog.component.html
@@ -1,5 +1,5 @@
@if (isActiveDrawerType(drawerTypes.OrgAtRiskMembers)) {
-
+
{{
"atRiskMembersWithCount" | i18n: drawerDetails.atRiskMemberDetails?.length ?? 0
@@ -45,7 +45,7 @@
}
@if (isActiveDrawerType(drawerTypes.AppAtRiskMembers)) {
-
+
{{ drawerDetails.appAtRiskMembers?.applicationName }}
@@ -71,7 +71,7 @@
}
@if (isActiveDrawerType(drawerTypes.OrgAtRiskApps)) {
-
+
{{
"atRiskApplicationsWithCount" | i18n: drawerDetails.atRiskAppDetails?.length ?? 0
diff --git a/bitwarden_license/bit-web/src/app/dirt/access-intelligence/shared/risk-insights-loading.component.ts b/bitwarden_license/bit-web/src/app/dirt/access-intelligence/shared/risk-insights-loading.component.ts
deleted file mode 100644
index d4c97a6fd5c..00000000000
--- a/bitwarden_license/bit-web/src/app/dirt/access-intelligence/shared/risk-insights-loading.component.ts
+++ /dev/null
@@ -1,57 +0,0 @@
-import { CommonModule } from "@angular/common";
-import { Component, DestroyRef, inject, OnInit, signal } from "@angular/core";
-import { takeUntilDestroyed } from "@angular/core/rxjs-interop";
-
-import { JslibModule } from "@bitwarden/angular/jslib.module";
-import {
- ReportProgress,
- RiskInsightsDataService,
-} from "@bitwarden/bit-common/dirt/reports/risk-insights";
-import { ProgressModule } from "@bitwarden/components";
-
-const PROGRESS_STEPS = [
- { step: ReportProgress.FetchingMembers, message: "fetchingMemberData", progress: 20 },
- { step: ReportProgress.AnalyzingPasswords, message: "analyzingPasswordHealth", progress: 40 },
- { step: ReportProgress.CalculatingRisks, message: "calculatingRiskScores", progress: 60 },
- { step: ReportProgress.GeneratingReport, message: "generatingReportData", progress: 80 },
- { step: ReportProgress.Saving, message: "savingReport", progress: 95 },
- { step: ReportProgress.Complete, message: "compilingInsights", progress: 100 },
-] as const;
-
-type LoadingMessage = (typeof PROGRESS_STEPS)[number]["message"];
-
-// 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: "dirt-risk-insights-loading",
- imports: [CommonModule, JslibModule, ProgressModule],
- templateUrl: "./risk-insights-loading.component.html",
-})
-export class ApplicationsLoadingComponent implements OnInit {
- private dataService = inject(RiskInsightsDataService);
- private destroyRef = inject(DestroyRef);
-
- readonly currentMessage = signal(PROGRESS_STEPS[0].message);
- readonly progress = signal(PROGRESS_STEPS[0].progress);
-
- ngOnInit(): void {
- // Subscribe to actual progress events from the orchestrator
- this.dataService.reportProgress$
- .pipe(takeUntilDestroyed(this.destroyRef))
- .subscribe((progressStep) => {
- if (progressStep === null) {
- // Reset to initial state
- this.currentMessage.set(PROGRESS_STEPS[0].message);
- this.progress.set(PROGRESS_STEPS[0].progress);
- return;
- }
-
- // Find the matching step configuration
- const stepConfig = PROGRESS_STEPS.find((config) => config.step === progressStep);
- if (stepConfig) {
- this.currentMessage.set(stepConfig.message);
- this.progress.set(stepConfig.progress);
- }
- });
- }
-}
diff --git a/bitwarden_license/bit-web/src/app/dirt/organization-integrations/integration-card/integration-card.component.spec.ts b/bitwarden_license/bit-web/src/app/dirt/organization-integrations/integration-card/integration-card.component.spec.ts
index 8beaae7f10a..37bd504643c 100644
--- a/bitwarden_license/bit-web/src/app/dirt/organization-integrations/integration-card/integration-card.component.spec.ts
+++ b/bitwarden_license/bit-web/src/app/dirt/organization-integrations/integration-card/integration-card.component.spec.ts
@@ -4,9 +4,10 @@ import { mock } from "jest-mock-extended";
import { BehaviorSubject, of } from "rxjs";
import { SYSTEM_THEME_OBSERVABLE } from "@bitwarden/angular/services/injection-tokens";
-import { OrganizationIntegrationServiceType } from "@bitwarden/bit-common/dirt/organization-integrations/models/organization-integration-service-type";
-import { DatadogOrganizationIntegrationService } from "@bitwarden/bit-common/dirt/organization-integrations/services/datadog-organization-integration-service";
-import { HecOrganizationIntegrationService } from "@bitwarden/bit-common/dirt/organization-integrations/services/hec-organization-integration-service";
+import { OrgIntegrationBuilder } from "@bitwarden/bit-common/dirt/organization-integrations/models/integration-builder";
+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 { ErrorResponse } from "@bitwarden/common/models/response/error.response";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { ThemeType } from "@bitwarden/common/platform/enums";
@@ -29,8 +30,7 @@ describe("IntegrationCardComponent", () => {
let fixture: ComponentFixture;
const mockI18nService = mock();
const activatedRoute = mock();
- const mockIntegrationService = mock();
- const mockDatadogIntegrationService = mock();
+ const mockIntegrationService = mock();
const dialogService = mock();
const toastService = mock();
@@ -54,8 +54,7 @@ describe("IntegrationCardComponent", () => {
{ provide: I18nPipe, useValue: mock() },
{ provide: I18nService, useValue: mockI18nService },
{ provide: ActivatedRoute, useValue: activatedRoute },
- { provide: HecOrganizationIntegrationService, useValue: mockIntegrationService },
- { provide: DatadogOrganizationIntegrationService, useValue: mockDatadogIntegrationService },
+ { provide: OrganizationIntegrationService, useValue: mockIntegrationService },
{ provide: ToastService, useValue: toastService },
{ provide: DialogService, useValue: dialogService },
],
@@ -259,7 +258,7 @@ describe("IntegrationCardComponent", () => {
configuration: {},
integrationConfiguration: [{ id: "config-id" }],
},
- name: OrganizationIntegrationServiceType.CrowdStrike,
+ name: OrganizationIntegrationServiceName.CrowdStrike,
} as any;
component.organizationId = "org-id" as any;
jest.resetAllMocks();
@@ -270,8 +269,8 @@ describe("IntegrationCardComponent", () => {
closed: of({ success: false }),
});
await component.setupConnection();
- expect(mockIntegrationService.updateHec).not.toHaveBeenCalled();
- expect(mockIntegrationService.saveHec).not.toHaveBeenCalled();
+ expect(mockIntegrationService.update).not.toHaveBeenCalled();
+ expect(mockIntegrationService.save).not.toHaveBeenCalled();
});
it("should call updateHec if isUpdateAvailable is true", async () => {
@@ -284,26 +283,35 @@ describe("IntegrationCardComponent", () => {
}),
});
+ const config = OrgIntegrationBuilder.buildHecConfiguration(
+ "test-url",
+ "token",
+ OrganizationIntegrationServiceName.CrowdStrike,
+ );
+ const template = OrgIntegrationBuilder.buildHecTemplate(
+ "index",
+ OrganizationIntegrationServiceName.CrowdStrike,
+ );
+
jest.spyOn(component, "isUpdateAvailable", "get").mockReturnValue(true);
await component.setupConnection();
- expect(mockIntegrationService.updateHec).toHaveBeenCalledWith(
+ expect(mockIntegrationService.update).toHaveBeenCalledWith(
"org-id",
"integration-id",
+ OrganizationIntegrationType.Hec,
"config-id",
- OrganizationIntegrationServiceType.CrowdStrike,
- "test-url",
- "token",
- "index",
+ config,
+ template,
);
- expect(mockIntegrationService.saveHec).not.toHaveBeenCalled();
+ expect(mockIntegrationService.save).not.toHaveBeenCalled();
});
it("should call saveHec if isUpdateAvailable is false", async () => {
component.integrationSettings = {
organizationIntegration: null,
- name: OrganizationIntegrationServiceType.CrowdStrike,
+ name: OrganizationIntegrationServiceName.CrowdStrike,
} as any;
component.organizationId = "org-id" as any;
@@ -316,23 +324,32 @@ describe("IntegrationCardComponent", () => {
}),
});
+ const config = OrgIntegrationBuilder.buildHecConfiguration(
+ "test-url",
+ "token",
+ OrganizationIntegrationServiceName.CrowdStrike,
+ );
+ const template = OrgIntegrationBuilder.buildHecTemplate(
+ "index",
+ OrganizationIntegrationServiceName.CrowdStrike,
+ );
+
jest.spyOn(component, "isUpdateAvailable", "get").mockReturnValue(false);
- mockIntegrationService.saveHec.mockResolvedValue({ mustBeOwner: false, success: true });
+ mockIntegrationService.save.mockResolvedValue({ mustBeOwner: false, success: true });
await component.setupConnection();
- expect(mockIntegrationService.saveHec).toHaveBeenCalledWith(
+ expect(mockIntegrationService.save).toHaveBeenCalledWith(
"org-id",
- OrganizationIntegrationServiceType.CrowdStrike,
- "test-url",
- "token",
- "index",
+ OrganizationIntegrationType.Hec,
+ config,
+ template,
);
- expect(mockIntegrationService.updateHec).not.toHaveBeenCalled();
+ expect(mockIntegrationService.update).not.toHaveBeenCalled();
});
- it("should call deleteHec when a delete is requested", async () => {
+ it("should call delete with Hec type when a delete is requested", async () => {
component.organizationId = "org-id" as any;
(openHecConnectDialog as jest.Mock).mockReturnValue({
@@ -344,22 +361,22 @@ describe("IntegrationCardComponent", () => {
}),
});
- mockIntegrationService.deleteHec.mockResolvedValue({ mustBeOwner: false, success: true });
+ mockIntegrationService.delete.mockResolvedValue({ mustBeOwner: false, success: true });
await component.setupConnection();
- expect(mockIntegrationService.deleteHec).toHaveBeenCalledWith(
+ expect(mockIntegrationService.delete).toHaveBeenCalledWith(
"org-id",
"integration-id",
"config-id",
);
- expect(mockIntegrationService.saveHec).not.toHaveBeenCalled();
+ expect(mockIntegrationService.save).not.toHaveBeenCalled();
});
- it("should not call deleteHec if no existing configuration", async () => {
+ it("should not call delete if no existing configuration", async () => {
component.integrationSettings = {
organizationIntegration: null,
- name: OrganizationIntegrationServiceType.CrowdStrike,
+ name: OrganizationIntegrationServiceName.CrowdStrike,
} as any;
component.organizationId = "org-id" as any;
@@ -372,20 +389,16 @@ describe("IntegrationCardComponent", () => {
}),
});
- mockIntegrationService.deleteHec.mockResolvedValue({ mustBeOwner: false, success: true });
+ mockIntegrationService.delete.mockResolvedValue({ mustBeOwner: false, success: true });
await component.setupConnection();
- expect(mockIntegrationService.deleteHec).not.toHaveBeenCalledWith(
+ expect(mockIntegrationService.delete).not.toHaveBeenCalledWith(
"org-id",
"integration-id",
"config-id",
- OrganizationIntegrationServiceType.CrowdStrike,
- "test-url",
- "token",
- "index",
);
- expect(mockIntegrationService.updateHec).not.toHaveBeenCalled();
+ expect(mockIntegrationService.update).not.toHaveBeenCalled();
});
it("should show toast on error while saving", async () => {
@@ -399,11 +412,11 @@ describe("IntegrationCardComponent", () => {
});
jest.spyOn(component, "isUpdateAvailable", "get").mockReturnValue(true);
- mockIntegrationService.updateHec.mockRejectedValue(new Error("fail"));
+ mockIntegrationService.update.mockRejectedValue(new Error("fail"));
await component.setupConnection();
- expect(mockIntegrationService.updateHec).toHaveBeenCalled();
+ expect(mockIntegrationService.update).toHaveBeenCalled();
expect(toastService.showToast).toHaveBeenCalledWith({
variant: "error",
title: "",
@@ -422,11 +435,11 @@ describe("IntegrationCardComponent", () => {
});
jest.spyOn(component, "isUpdateAvailable", "get").mockReturnValue(true);
- mockIntegrationService.updateHec.mockRejectedValue(new ErrorResponse("Not Found", 404));
+ mockIntegrationService.update.mockRejectedValue(new ErrorResponse("Not Found", 404));
await component.setupConnection();
- expect(mockIntegrationService.updateHec).toHaveBeenCalled();
+ expect(mockIntegrationService.update).toHaveBeenCalled();
expect(toastService.showToast).toHaveBeenCalledWith({
variant: "error",
title: "",
@@ -445,11 +458,10 @@ describe("IntegrationCardComponent", () => {
});
jest.spyOn(component, "isUpdateAvailable", "get").mockReturnValue(true);
- mockIntegrationService.updateHec.mockRejectedValue(new ErrorResponse("Not Found", 404));
-
+ mockIntegrationService.update.mockRejectedValue(new ErrorResponse("Not Found", 404));
await component.setupConnection();
- expect(mockIntegrationService.updateHec).toHaveBeenCalled();
+ expect(mockIntegrationService.update).toHaveBeenCalled();
expect(toastService.showToast).toHaveBeenCalledWith({
variant: "error",
title: "",
@@ -468,11 +480,11 @@ describe("IntegrationCardComponent", () => {
});
jest.spyOn(component, "isUpdateAvailable", "get").mockReturnValue(true);
- mockIntegrationService.deleteHec.mockRejectedValue(new Error("fail"));
+ mockIntegrationService.delete.mockRejectedValue(new Error("fail"));
await component.setupConnection();
- expect(mockIntegrationService.deleteHec).toHaveBeenCalled();
+ expect(mockIntegrationService.delete).toHaveBeenCalled();
expect(toastService.showToast).toHaveBeenCalledWith({
variant: "error",
title: "",
@@ -491,11 +503,10 @@ describe("IntegrationCardComponent", () => {
});
jest.spyOn(component, "isUpdateAvailable", "get").mockReturnValue(true);
- mockIntegrationService.deleteHec.mockRejectedValue(new ErrorResponse("Not Found", 404));
-
+ mockIntegrationService.delete.mockRejectedValue(new ErrorResponse("Not Found", 404));
await component.setupConnection();
- expect(mockIntegrationService.deleteHec).toHaveBeenCalled();
+ expect(mockIntegrationService.delete).toHaveBeenCalled();
expect(toastService.showToast).toHaveBeenCalledWith({
variant: "error",
title: "",
diff --git a/bitwarden_license/bit-web/src/app/dirt/organization-integrations/integration-card/integration-card.component.ts b/bitwarden_license/bit-web/src/app/dirt/organization-integrations/integration-card/integration-card.component.ts
index e6d4aff05fb..8026e14c2fc 100644
--- a/bitwarden_license/bit-web/src/app/dirt/organization-integrations/integration-card/integration-card.component.ts
+++ b/bitwarden_license/bit-web/src/app/dirt/organization-integrations/integration-card/integration-card.component.ts
@@ -12,10 +12,10 @@ import { Observable, Subject, combineLatest, lastValueFrom, takeUntil } from "rx
import { SYSTEM_THEME_OBSERVABLE } from "@bitwarden/angular/services/injection-tokens";
import { Integration } from "@bitwarden/bit-common/dirt/organization-integrations/models/integration";
-import { OrganizationIntegrationServiceType } from "@bitwarden/bit-common/dirt/organization-integrations/models/organization-integration-service-type";
+import { OrgIntegrationBuilder } from "@bitwarden/bit-common/dirt/organization-integrations/models/integration-builder";
+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 { DatadogOrganizationIntegrationService } from "@bitwarden/bit-common/dirt/organization-integrations/services/datadog-organization-integration-service";
-import { HecOrganizationIntegrationService } from "@bitwarden/bit-common/dirt/organization-integrations/services/hec-organization-integration-service";
+import { OrganizationIntegrationService } from "@bitwarden/bit-common/dirt/organization-integrations/services/organization-integration-service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { ThemeType } from "@bitwarden/common/platform/enums";
import { ThemeStateService } from "@bitwarden/common/platform/theming/theme-state.service";
@@ -96,8 +96,7 @@ export class IntegrationCardComponent implements AfterViewInit, OnDestroy {
private systemTheme$: Observable,
private dialogService: DialogService,
private activatedRoute: ActivatedRoute,
- private hecOrganizationIntegrationService: HecOrganizationIntegrationService,
- private datadogOrganizationIntegrationService: DatadogOrganizationIntegrationService,
+ private organizationIntegrationService: OrganizationIntegrationService,
private toastService: ToastService,
private i18nService: I18nService,
) {
@@ -250,7 +249,18 @@ export class IntegrationCardComponent implements AfterViewInit, OnDestroy {
}
async saveHec(result: HecConnectDialogResult) {
- let saveResponse = { mustBeOwner: false, success: false };
+ let response = { mustBeOwner: false, success: false };
+
+ const config = OrgIntegrationBuilder.buildHecConfiguration(
+ result.url,
+ result.bearerToken,
+ this.integrationSettings.name as OrganizationIntegrationServiceName,
+ );
+ const template = OrgIntegrationBuilder.buildHecTemplate(
+ result.index,
+ this.integrationSettings.name as OrganizationIntegrationServiceName,
+ );
+
if (this.isUpdateAvailable) {
// retrieve org integration and configuration ids
const orgIntegrationId = this.integrationSettings.organizationIntegration?.id;
@@ -262,27 +272,25 @@ export class IntegrationCardComponent implements AfterViewInit, OnDestroy {
}
// update existing integration and configuration
- saveResponse = await this.hecOrganizationIntegrationService.updateHec(
+ response = await this.organizationIntegrationService.update(
this.organizationId,
orgIntegrationId,
+ OrganizationIntegrationType.Hec,
orgIntegrationConfigurationId,
- this.integrationSettings.name as OrganizationIntegrationServiceType,
- result.url,
- result.bearerToken,
- result.index,
+ config,
+ template,
);
} else {
// create new integration and configuration
- saveResponse = await this.hecOrganizationIntegrationService.saveHec(
+ response = await this.organizationIntegrationService.save(
this.organizationId,
- this.integrationSettings.name as OrganizationIntegrationServiceType,
- result.url,
- result.bearerToken,
- result.index,
+ OrganizationIntegrationType.Hec,
+ config,
+ template,
);
}
- if (saveResponse.mustBeOwner) {
+ if (response.mustBeOwner) {
this.showMustBeOwnerToast();
return;
}
@@ -303,7 +311,7 @@ export class IntegrationCardComponent implements AfterViewInit, OnDestroy {
throw Error("Organization Integration ID or Configuration ID is missing");
}
- const response = await this.hecOrganizationIntegrationService.deleteHec(
+ const response = await this.organizationIntegrationService.delete(
this.organizationId,
orgIntegrationId,
orgIntegrationConfigurationId,
@@ -322,6 +330,13 @@ export class IntegrationCardComponent implements AfterViewInit, OnDestroy {
}
async saveDatadog(result: DatadogConnectDialogResult) {
+ let response = { mustBeOwner: false, success: false };
+
+ const config = OrgIntegrationBuilder.buildDataDogConfiguration(result.url, result.apiKey);
+ const template = OrgIntegrationBuilder.buildDataDogTemplate(
+ this.integrationSettings.name as OrganizationIntegrationServiceName,
+ );
+
if (this.isUpdateAvailable) {
// retrieve org integration and configuration ids
const orgIntegrationId = this.integrationSettings.organizationIntegration?.id;
@@ -333,23 +348,29 @@ export class IntegrationCardComponent implements AfterViewInit, OnDestroy {
}
// update existing integration and configuration
- await this.datadogOrganizationIntegrationService.updateDatadog(
+ response = await this.organizationIntegrationService.update(
this.organizationId,
orgIntegrationId,
+ OrganizationIntegrationType.Datadog,
orgIntegrationConfigurationId,
- this.integrationSettings.name as OrganizationIntegrationServiceType,
- result.url,
- result.apiKey,
+ config,
+ template,
);
} else {
// create new integration and configuration
- await this.datadogOrganizationIntegrationService.saveDatadog(
+ response = await this.organizationIntegrationService.save(
this.organizationId,
- this.integrationSettings.name as OrganizationIntegrationServiceType,
- result.url,
- result.apiKey,
+ OrganizationIntegrationType.Datadog,
+ config,
+ template,
);
}
+
+ if (response.mustBeOwner) {
+ this.showMustBeOwnerToast();
+ return;
+ }
+
this.toastService.showToast({
variant: "success",
title: "",
@@ -366,7 +387,7 @@ export class IntegrationCardComponent implements AfterViewInit, OnDestroy {
throw Error("Organization Integration ID or Configuration ID is missing");
}
- const response = await this.datadogOrganizationIntegrationService.deleteDatadog(
+ const response = await this.organizationIntegrationService.delete(
this.organizationId,
orgIntegrationId,
orgIntegrationConfigurationId,
diff --git a/bitwarden_license/bit-web/src/app/dirt/organization-integrations/integration-dialog/connect-dialog/connect-dialog-datadog.component.html b/bitwarden_license/bit-web/src/app/dirt/organization-integrations/integration-dialog/connect-dialog/connect-dialog-datadog.component.html
index c129216b694..ddc108201b0 100644
--- a/bitwarden_license/bit-web/src/app/dirt/organization-integrations/integration-dialog/connect-dialog/connect-dialog-datadog.component.html
+++ b/bitwarden_license/bit-web/src/app/dirt/organization-integrations/integration-dialog/connect-dialog/connect-dialog-datadog.component.html
@@ -47,8 +47,8 @@
bitIconButton="bwi-trash"
type="button"
buttonType="danger"
- label="'delete' | i18n"
- [appA11yTitle]="'delete' | i18n"
+ label="{{ 'delete' | i18n }}"
+ appA11yTitle="{{ 'delete' | i18n }}"
[bitAction]="delete"
>
diff --git a/bitwarden_license/bit-web/src/app/dirt/organization-integrations/integration-dialog/connect-dialog/connect-dialog-hec.component.html b/bitwarden_license/bit-web/src/app/dirt/organization-integrations/integration-dialog/connect-dialog/connect-dialog-hec.component.html
index 557e0fc382f..0dad1621440 100644
--- a/bitwarden_license/bit-web/src/app/dirt/organization-integrations/integration-dialog/connect-dialog/connect-dialog-hec.component.html
+++ b/bitwarden_license/bit-web/src/app/dirt/organization-integrations/integration-dialog/connect-dialog/connect-dialog-hec.component.html
@@ -53,8 +53,8 @@
bitIconButton="bwi-trash"
type="button"
buttonType="danger"
- label="'delete' | i18n"
- [appA11yTitle]="'delete' | i18n"
+ label="{{ 'delete' | i18n }}"
+ appA11yTitle="{{ 'delete' | i18n }}"
[bitAction]="delete"
>
diff --git a/bitwarden_license/bit-web/src/app/dirt/organization-integrations/integration-grid/integration-grid.component.spec.ts b/bitwarden_license/bit-web/src/app/dirt/organization-integrations/integration-grid/integration-grid.component.spec.ts
index 2908fe0c089..3560a32fb40 100644
--- a/bitwarden_license/bit-web/src/app/dirt/organization-integrations/integration-grid/integration-grid.component.spec.ts
+++ b/bitwarden_license/bit-web/src/app/dirt/organization-integrations/integration-grid/integration-grid.component.spec.ts
@@ -6,8 +6,7 @@ import { of } from "rxjs";
import { SYSTEM_THEME_OBSERVABLE } from "@bitwarden/angular/services/injection-tokens";
import { Integration } from "@bitwarden/bit-common/dirt/organization-integrations/models/integration";
-import { DatadogOrganizationIntegrationService } from "@bitwarden/bit-common/dirt/organization-integrations/services/datadog-organization-integration-service";
-import { HecOrganizationIntegrationService } from "@bitwarden/bit-common/dirt/organization-integrations/services/hec-organization-integration-service";
+import { OrganizationIntegrationService } from "@bitwarden/bit-common/dirt/organization-integrations/services/organization-integration-service";
import { IntegrationType } from "@bitwarden/common/enums";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { ThemeTypes } from "@bitwarden/common/platform/enums";
@@ -24,8 +23,7 @@ describe("IntegrationGridComponent", () => {
let component: IntegrationGridComponent;
let fixture: ComponentFixture;
const mockActivatedRoute = mock();
- const mockIntegrationService = mock();
- const mockDatadogIntegrationService = mock();
+ const mockIntegrationService = mock();
const integrations: Integration[] = [
{
name: "Integration 1",
@@ -71,8 +69,7 @@ describe("IntegrationGridComponent", () => {
provide: ActivatedRoute,
useValue: mockActivatedRoute,
},
- { provide: HecOrganizationIntegrationService, useValue: mockIntegrationService },
- { provide: DatadogOrganizationIntegrationService, useValue: mockDatadogIntegrationService },
+ { provide: OrganizationIntegrationService, useValue: mockIntegrationService },
{
provide: ToastService,
useValue: mock(),
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 58c52e4f40a..a35df3677bb 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,69 +1,78 @@
-
-
-
-
+@let organization = organization$ | async;
-
-
-
- {{ "scimIntegration" | i18n }}
-
-
- {{ "scimIntegrationDescStart" | i18n }}
- {{ "scimIntegration" | i18n }}
- {{ "scimIntegrationDescEnd" | i18n }}
-
-
-
-
-
- {{ "bwdc" | i18n }}
-
- {{ "bwdcDesc" | i18n }}
-
-
-
+@if (organization) {
+
+ @if (organization?.useSso) {
+
+
+
+ }
-
-
-
- {{ "eventManagement" | i18n }}
-
- {{ "eventManagementDesc" | i18n }}
-
-
-
+ @if (organization?.useScim || organization?.useDirectory) {
+
+
+
+ {{ "scimIntegration" | i18n }}
+
+
+ {{ "scimIntegrationDescStart" | i18n }}
+ {{ "scimIntegration" | i18n }}
+ {{ "scimIntegrationDescEnd" | i18n }}
+
+
+
+
+
+ {{ "bwdc" | i18n }}
+
+ {{ "bwdcDesc" | i18n }}
+
+
+
+ }
-
-
-
- {{ "deviceManagement" | i18n }}
-
- {{ "deviceManagementDesc" | 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 894a8e9a25c..6517182b21e 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
@@ -3,10 +3,9 @@ import { ActivatedRoute } from "@angular/router";
import { firstValueFrom, Observable, Subject, switchMap, takeUntil, takeWhile } from "rxjs";
import { Integration } from "@bitwarden/bit-common/dirt/organization-integrations/models/integration";
-import { OrganizationIntegrationServiceType } from "@bitwarden/bit-common/dirt/organization-integrations/models/organization-integration-service-type";
+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 { DatadogOrganizationIntegrationService } from "@bitwarden/bit-common/dirt/organization-integrations/services/datadog-organization-integration-service";
-import { HecOrganizationIntegrationService } from "@bitwarden/bit-common/dirt/organization-integrations/services/hec-organization-integration-service";
+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";
@@ -21,6 +20,7 @@ import { SharedModule } from "@bitwarden/web-vault/app/shared";
import { IntegrationGridComponent } from "./integration-grid/integration-grid.component";
import { FilterIntegrationsPipe } from "./integrations.pipe";
+// 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({
@@ -236,10 +236,12 @@ export class AdminConsoleIntegrationsComponent implements OnInit, OnDestroy {
);
// Sets the organization ID which also loads the integrations$
- this.organization$.pipe(takeUntil(this.destroy$)).subscribe((org) => {
- this.hecOrganizationIntegrationService.setOrganizationIntegrations(org.id);
- this.datadogOrganizationIntegrationService.setOrganizationIntegrations(org.id);
- });
+ this.organization$
+ .pipe(
+ switchMap((org) => this.organizationIntegrationService.setOrganizationId(org.id)),
+ takeUntil(this.destroy$),
+ )
+ .subscribe();
}
constructor(
@@ -247,8 +249,7 @@ export class AdminConsoleIntegrationsComponent implements OnInit, OnDestroy {
private organizationService: OrganizationService,
private accountService: AccountService,
private configService: ConfigService,
- private hecOrganizationIntegrationService: HecOrganizationIntegrationService,
- private datadogOrganizationIntegrationService: DatadogOrganizationIntegrationService,
+ private organizationIntegrationService: OrganizationIntegrationService,
) {
this.configService
.getFeatureFlag$(FeatureFlag.EventManagementForDataDogAndCrowdStrike)
@@ -260,7 +261,7 @@ export class AdminConsoleIntegrationsComponent implements OnInit, OnDestroy {
// Add the new event based items to the list
if (this.isEventManagementForDataDogAndCrowdStrikeEnabled) {
const crowdstrikeIntegration: Integration = {
- name: OrganizationIntegrationServiceType.CrowdStrike,
+ name: OrganizationIntegrationServiceName.CrowdStrike,
linkURL: "https://bitwarden.com/help/crowdstrike-siem/",
image: "../../../../../../../images/integrations/logo-crowdstrike-black.svg",
type: IntegrationType.EVENT,
@@ -272,7 +273,7 @@ export class AdminConsoleIntegrationsComponent implements OnInit, OnDestroy {
this.integrationsList.push(crowdstrikeIntegration);
const datadogIntegration: Integration = {
- name: OrganizationIntegrationServiceType.Datadog,
+ name: OrganizationIntegrationServiceName.Datadog,
linkURL: "https://bitwarden.com/help/datadog-siem/",
image: "../../../../../../../images/integrations/logo-datadog-color.svg",
type: IntegrationType.EVENT,
@@ -286,42 +287,23 @@ export class AdminConsoleIntegrationsComponent implements OnInit, OnDestroy {
// For all existing event based configurations loop through and assign the
// organizationIntegration for the correct services.
- this.hecOrganizationIntegrationService.integrations$
+ this.organizationIntegrationService.integrations$
.pipe(takeUntil(this.destroy$))
.subscribe((integrations) => {
- // reset all integrations to null first - in case one was deleted
+ // reset all event based integrations to null first - in case one was deleted
this.integrationsList.forEach((i) => {
- if (i.integrationType === OrganizationIntegrationType.Hec) {
- i.organizationIntegration = null;
- }
+ i.organizationIntegration = null;
});
- integrations.map((integration) => {
- const item = this.integrationsList.find((i) => i.name === integration.serviceType);
- if (item) {
- item.organizationIntegration = integration;
- }
- });
- });
-
- this.datadogOrganizationIntegrationService.integrations$
- .pipe(takeUntil(this.destroy$))
- .subscribe((integrations) => {
- // reset all integrations to null first - in case one was deleted
- this.integrationsList.forEach((i) => {
- if (i.integrationType === OrganizationIntegrationType.Datadog) {
- i.organizationIntegration = null;
- }
- });
-
- integrations.map((integration) => {
- const item = this.integrationsList.find((i) => i.name === integration.serviceType);
+ 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();
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 e3c37b4a42b..789ae548521 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,9 +1,8 @@
import { NgModule } from "@angular/core";
-import { DatadogOrganizationIntegrationService } from "@bitwarden/bit-common/dirt/organization-integrations/services/datadog-organization-integration-service";
-import { HecOrganizationIntegrationService } from "@bitwarden/bit-common/dirt/organization-integrations/services/hec-organization-integration-service";
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";
@@ -14,13 +13,8 @@ import { OrganizationIntegrationsRoutingModule } from "./organization-integratio
imports: [AdminConsoleIntegrationsComponent, OrganizationIntegrationsRoutingModule],
providers: [
safeProvider({
- provide: DatadogOrganizationIntegrationService,
- useClass: DatadogOrganizationIntegrationService,
- deps: [OrganizationIntegrationApiService, OrganizationIntegrationConfigurationApiService],
- }),
- safeProvider({
- provide: HecOrganizationIntegrationService,
- useClass: HecOrganizationIntegrationService,
+ provide: OrganizationIntegrationService,
+ useClass: OrganizationIntegrationService,
deps: [OrganizationIntegrationApiService, OrganizationIntegrationConfigurationApiService],
}),
safeProvider({
diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/integrations/integrations.component.spec.ts b/bitwarden_license/bit-web/src/app/secrets-manager/integrations/integrations.component.spec.ts
index 0e8c46c8864..7a02e3fb04e 100644
--- a/bitwarden_license/bit-web/src/app/secrets-manager/integrations/integrations.component.spec.ts
+++ b/bitwarden_license/bit-web/src/app/secrets-manager/integrations/integrations.component.spec.ts
@@ -9,8 +9,7 @@ import {} from "@bitwarden/web-vault/app/shared";
import { JslibModule } from "@bitwarden/angular/jslib.module";
import { SYSTEM_THEME_OBSERVABLE } from "@bitwarden/angular/services/injection-tokens";
-import { DatadogOrganizationIntegrationService } from "@bitwarden/bit-common/dirt/organization-integrations/services/datadog-organization-integration-service";
-import { HecOrganizationIntegrationService } from "@bitwarden/bit-common/dirt/organization-integrations/services/hec-organization-integration-service";
+import { OrganizationIntegrationService } from "@bitwarden/bit-common/dirt/organization-integrations/services/organization-integration-service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { ThemeType } from "@bitwarden/common/platform/enums";
import { ThemeStateService } from "@bitwarden/common/platform/theming/theme-state.service";
@@ -41,8 +40,7 @@ class MockNewMenuComponent {}
describe("IntegrationsComponent", () => {
let fixture: ComponentFixture;
- const hecOrgIntegrationSvc = mock();
- const datadogOrgIntegrationSvc = mock();
+ const orgIntegrationSvc = mock();
const activatedRouteMock = {
snapshot: { paramMap: { get: jest.fn() } },
@@ -60,8 +58,7 @@ describe("IntegrationsComponent", () => {
{ provide: ActivatedRoute, useValue: activatedRouteMock },
{ provide: I18nPipe, useValue: mock() },
{ provide: I18nService, useValue: mockI18nService },
- { provide: HecOrganizationIntegrationService, useValue: hecOrgIntegrationSvc },
- { provide: DatadogOrganizationIntegrationService, useValue: datadogOrgIntegrationSvc },
+ { provide: OrganizationIntegrationService, useValue: orgIntegrationSvc },
],
}).compileComponents();
fixture = TestBed.createComponent(IntegrationsComponent);
diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/integrations/integrations.module.ts b/bitwarden_license/bit-web/src/app/secrets-manager/integrations/integrations.module.ts
index 04240da3176..bcfbb9b3f2c 100644
--- a/bitwarden_license/bit-web/src/app/secrets-manager/integrations/integrations.module.ts
+++ b/bitwarden_license/bit-web/src/app/secrets-manager/integrations/integrations.module.ts
@@ -1,9 +1,8 @@
import { NgModule } from "@angular/core";
-import { DatadogOrganizationIntegrationService } from "@bitwarden/bit-common/dirt/organization-integrations/services/datadog-organization-integration-service";
-import { HecOrganizationIntegrationService } from "@bitwarden/bit-common/dirt/organization-integrations/services/hec-organization-integration-service";
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";
@@ -23,13 +22,8 @@ import { IntegrationsComponent } from "./integrations.component";
],
providers: [
safeProvider({
- provide: DatadogOrganizationIntegrationService,
- useClass: DatadogOrganizationIntegrationService,
- deps: [OrganizationIntegrationApiService, OrganizationIntegrationConfigurationApiService],
- }),
- safeProvider({
- provide: HecOrganizationIntegrationService,
- useClass: HecOrganizationIntegrationService,
+ provide: OrganizationIntegrationService,
+ useClass: OrganizationIntegrationService,
deps: [OrganizationIntegrationApiService, OrganizationIntegrationConfigurationApiService],
}),
safeProvider({
diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/layout/navigation.component.html b/bitwarden_license/bit-web/src/app/secrets-manager/layout/navigation.component.html
index 0ea8caef4d6..9cefdac685d 100644
--- a/bitwarden_license/bit-web/src/app/secrets-manager/layout/navigation.component.html
+++ b/bitwarden_license/bit-web/src/app/secrets-manager/layout/navigation.component.html
@@ -52,12 +52,12 @@
[relativeTo]="route.parent"
>
diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/settings/porting/sm-export.component.html b/bitwarden_license/bit-web/src/app/secrets-manager/settings/porting/sm-export.component.html
index 9e1f2e01591..9fd38408547 100644
--- a/bitwarden_license/bit-web/src/app/secrets-manager/settings/porting/sm-export.component.html
+++ b/bitwarden_license/bit-web/src/app/secrets-manager/settings/porting/sm-export.component.html
@@ -17,6 +17,6 @@
diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/settings/porting/sm-export.component.ts b/bitwarden_license/bit-web/src/app/secrets-manager/settings/porting/sm-export.component.ts
index e2b66d9ffa6..f2af3d4e8f8 100644
--- a/bitwarden_license/bit-web/src/app/secrets-manager/settings/porting/sm-export.component.ts
+++ b/bitwarden_license/bit-web/src/app/secrets-manager/settings/porting/sm-export.component.ts
@@ -124,7 +124,7 @@ export class SecretsManagerExportComponent implements OnInit, OnDestroy {
const ref = openUserVerificationPrompt(this.dialogService, {
data: {
confirmDescription: "exportSecretsWarningDesc",
- confirmButtonText: "exportSecrets",
+ confirmButtonText: "exportVerb",
modalTitle: "confirmSecretsExport",
},
});
diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/settings/porting/sm-import.component.html b/bitwarden_license/bit-web/src/app/secrets-manager/settings/porting/sm-import.component.html
index 353d8d8c8ed..b1bdee73f90 100644
--- a/bitwarden_license/bit-web/src/app/secrets-manager/settings/porting/sm-import.component.html
+++ b/bitwarden_license/bit-web/src/app/secrets-manager/settings/porting/sm-import.component.html
@@ -36,6 +36,6 @@
{{ "acceptedFormats" | i18n }} Bitwarden (json)
diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/settings/settings-routing.module.ts b/bitwarden_license/bit-web/src/app/secrets-manager/settings/settings-routing.module.ts
index ddc9964060e..283f877722c 100644
--- a/bitwarden_license/bit-web/src/app/secrets-manager/settings/settings-routing.module.ts
+++ b/bitwarden_license/bit-web/src/app/secrets-manager/settings/settings-routing.module.ts
@@ -12,7 +12,7 @@ const routes: Routes = [
component: SecretsManagerImportComponent,
canActivate: [organizationPermissionsGuard((org) => org.isAdmin)],
data: {
- titleId: "importData",
+ titleId: "importNoun",
},
},
{
@@ -20,7 +20,7 @@ const routes: Routes = [
component: SecretsManagerExportComponent,
canActivate: [organizationPermissionsGuard((org) => org.isAdmin)],
data: {
- titleId: "exportData",
+ titleId: "exportNoun",
},
},
];
diff --git a/jest.config.js b/jest.config.js
index e5aeb536172..37d15eb8f92 100644
--- a/jest.config.js
+++ b/jest.config.js
@@ -59,6 +59,7 @@ module.exports = {
"/libs/tools/send/send-ui/jest.config.js",
"/libs/user-core/jest.config.js",
"/libs/vault/jest.config.js",
+ "/libs/subscription/jest.config.js",
],
// Workaround for a memory leak that crashes tests in CI:
diff --git a/libs/admin-console/src/common/collections/abstractions/collection.service.ts b/libs/admin-console/src/common/collections/abstractions/collection.service.ts
index f879831324d..f0f02ee377e 100644
--- a/libs/admin-console/src/common/collections/abstractions/collection.service.ts
+++ b/libs/admin-console/src/common/collections/abstractions/collection.service.ts
@@ -9,6 +9,14 @@ import { CollectionData, Collection, CollectionView } from "../models";
export abstract class CollectionService {
abstract encryptedCollections$(userId: UserId): Observable;
abstract decryptedCollections$(userId: UserId): Observable;
+
+ /**
+ * Gets the default collection for a user in a given organization, if it exists.
+ */
+ abstract defaultUserCollection$(
+ userId: UserId,
+ orgId: OrganizationId,
+ ): Observable;
abstract upsert(collection: CollectionData, userId: UserId): Promise;
abstract replace(collections: { [id: string]: CollectionData }, userId: UserId): Promise;
/**
diff --git a/libs/admin-console/src/common/collections/services/default-collection.service.spec.ts b/libs/admin-console/src/common/collections/services/default-collection.service.spec.ts
index ced3b720e3c..2eaaa48594e 100644
--- a/libs/admin-console/src/common/collections/services/default-collection.service.spec.ts
+++ b/libs/admin-console/src/common/collections/services/default-collection.service.spec.ts
@@ -15,9 +15,10 @@ import {
} from "@bitwarden/common/spec";
import { CollectionId, OrganizationId, UserId } from "@bitwarden/common/types/guid";
import { OrgKey } from "@bitwarden/common/types/key";
+import { newGuid } from "@bitwarden/guid";
import { KeyService } from "@bitwarden/key-management";
-import { CollectionData, CollectionView } from "../models";
+import { CollectionData, CollectionTypes, CollectionView } from "../models";
import { DECRYPTED_COLLECTION_DATA_KEY, ENCRYPTED_COLLECTION_DATA_KEY } from "./collection.state";
import { DefaultCollectionService } from "./default-collection.service";
@@ -389,6 +390,83 @@ describe("DefaultCollectionService", () => {
});
});
+ describe("defaultUserCollection$", () => {
+ it("returns the default collection when one exists matching the org", async () => {
+ const orgId = newGuid() as OrganizationId;
+ const defaultCollection = collectionViewDataFactory(orgId);
+ defaultCollection.type = CollectionTypes.DefaultUserCollection;
+
+ const regularCollection = collectionViewDataFactory(orgId);
+ regularCollection.type = CollectionTypes.SharedCollection;
+
+ await setDecryptedState([defaultCollection, regularCollection]);
+
+ const result = await firstValueFrom(collectionService.defaultUserCollection$(userId, orgId));
+
+ expect(result).toBeDefined();
+ expect(result?.id).toBe(defaultCollection.id);
+ expect(result?.isDefaultCollection).toBe(true);
+ });
+
+ it("returns undefined when no default collection exists", async () => {
+ const orgId = newGuid() as OrganizationId;
+ const collection1 = collectionViewDataFactory(orgId);
+ collection1.type = CollectionTypes.SharedCollection;
+
+ const collection2 = collectionViewDataFactory(orgId);
+ collection2.type = CollectionTypes.SharedCollection;
+
+ await setDecryptedState([collection1, collection2]);
+
+ const result = await firstValueFrom(collectionService.defaultUserCollection$(userId, orgId));
+
+ expect(result).toBeUndefined();
+ });
+
+ it("returns undefined when default collection exists but for different org", async () => {
+ const orgA = newGuid() as OrganizationId;
+ const orgB = newGuid() as OrganizationId;
+
+ const defaultCollectionForOrgA = collectionViewDataFactory(orgA);
+ defaultCollectionForOrgA.type = CollectionTypes.DefaultUserCollection;
+
+ await setDecryptedState([defaultCollectionForOrgA]);
+
+ const result = await firstValueFrom(collectionService.defaultUserCollection$(userId, orgB));
+
+ expect(result).toBeUndefined();
+ });
+
+ it("returns undefined when collections array is empty", async () => {
+ const orgId = newGuid() as OrganizationId;
+
+ await setDecryptedState([]);
+
+ const result = await firstValueFrom(collectionService.defaultUserCollection$(userId, orgId));
+
+ expect(result).toBeUndefined();
+ });
+
+ it("returns correct collection when multiple orgs have default collections", async () => {
+ const orgA = newGuid() as OrganizationId;
+ const orgB = newGuid() as OrganizationId;
+
+ const defaultCollectionForOrgA = collectionViewDataFactory(orgA);
+ defaultCollectionForOrgA.type = CollectionTypes.DefaultUserCollection;
+
+ const defaultCollectionForOrgB = collectionViewDataFactory(orgB);
+ defaultCollectionForOrgB.type = CollectionTypes.DefaultUserCollection;
+
+ await setDecryptedState([defaultCollectionForOrgA, defaultCollectionForOrgB]);
+
+ const result = await firstValueFrom(collectionService.defaultUserCollection$(userId, orgB));
+
+ expect(result).toBeDefined();
+ expect(result?.id).toBe(defaultCollectionForOrgB.id);
+ expect(result?.organizationId).toBe(orgB);
+ });
+ });
+
const setEncryptedState = (collectionData: CollectionData[] | null) =>
stateProvider.setUserState(
ENCRYPTED_COLLECTION_DATA_KEY,
diff --git a/libs/admin-console/src/common/collections/services/default-collection.service.ts b/libs/admin-console/src/common/collections/services/default-collection.service.ts
index 0511b692b38..ccc2e6f0de5 100644
--- a/libs/admin-console/src/common/collections/services/default-collection.service.ts
+++ b/libs/admin-console/src/common/collections/services/default-collection.service.ts
@@ -87,6 +87,17 @@ export class DefaultCollectionService implements CollectionService {
return result$;
}
+ defaultUserCollection$(
+ userId: UserId,
+ orgId: OrganizationId,
+ ): Observable {
+ return this.decryptedCollections$(userId).pipe(
+ map((collections) => {
+ return collections.find((c) => c.isDefaultCollection && c.organizationId === orgId);
+ }),
+ );
+ }
+
private initializeDecryptedState(userId: UserId): Observable {
return combineLatest([
this.encryptedCollections$(userId),
diff --git a/libs/angular/src/auth/password-management/set-initial-password/default-set-initial-password.service.implementation.ts b/libs/angular/src/auth/password-management/set-initial-password/default-set-initial-password.service.implementation.ts
index df5220b5255..2f5c43e2db9 100644
--- a/libs/angular/src/auth/password-management/set-initial-password/default-set-initial-password.service.implementation.ts
+++ b/libs/angular/src/auth/password-management/set-initial-password/default-set-initial-password.service.implementation.ts
@@ -15,9 +15,11 @@ import { MasterPasswordApiService } from "@bitwarden/common/auth/abstractions/ma
import { ForceSetPasswordReason } from "@bitwarden/common/auth/models/domain/force-set-password-reason";
import { SetPasswordRequest } from "@bitwarden/common/auth/models/request/set-password.request";
import { UpdateTdeOffboardingPasswordRequest } from "@bitwarden/common/auth/models/request/update-tde-offboarding-password.request";
+import { AccountCryptographicStateService } from "@bitwarden/common/key-management/account-cryptography/account-cryptographic-state.service";
import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service";
import { EncString } from "@bitwarden/common/key-management/crypto/models/enc-string";
import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/key-management/master-password/abstractions/master-password.service.abstraction";
+import { MasterPasswordSalt } from "@bitwarden/common/key-management/master-password/types/master-password.types";
import { KeysRequest } from "@bitwarden/common/models/request/keys.request";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { Utils } from "@bitwarden/common/platform/misc/utils";
@@ -44,6 +46,7 @@ export class DefaultSetInitialPasswordService implements SetInitialPasswordServi
protected organizationApiService: OrganizationApiServiceAbstraction,
protected organizationUserApiService: OrganizationUserApiService,
protected userDecryptionOptionsService: InternalUserDecryptionOptionsServiceAbstraction,
+ protected accountCryptographicStateService: AccountCryptographicStateService,
) {}
async setInitialPassword(
@@ -60,6 +63,8 @@ export class DefaultSetInitialPasswordService implements SetInitialPasswordServi
orgSsoIdentifier,
orgId,
resetPasswordAutoEnroll,
+ newPassword,
+ salt,
} = credentials;
for (const [key, value] of Object.entries(credentials)) {
@@ -153,6 +158,20 @@ export class DefaultSetInitialPasswordService implements SetInitialPasswordServi
userId,
);
+ // Set master password unlock data for unlock path pointed to with
+ // MasterPasswordUnlockData feature development
+ // (requires: password, salt, kdf, userKey).
+ // As migration to this strategy continues, both unlock paths need supported.
+ // Several invocations in this file become redundant and can be removed once
+ // the feature is enshrined/unwound. These are marked with [PM-23246] below.
+ await this.setMasterPasswordUnlockData(
+ newPassword,
+ salt,
+ kdfConfig,
+ masterKeyEncryptedUserKey[0],
+ userId,
+ );
+
/**
* Set the private key only for new JIT provisioned users in MP encryption orgs.
* (Existing TDE users will have their private key set on sync or on login.)
@@ -162,8 +181,17 @@ export class DefaultSetInitialPasswordService implements SetInitialPasswordServi
throw new Error("encrypted private key not found. Could not set private key in state.");
}
await this.keyService.setPrivateKey(keyPair[1].encryptedString, userId);
+ await this.accountCryptographicStateService.setAccountCryptographicState(
+ {
+ V1: {
+ private_key: keyPair[1].encryptedString,
+ },
+ },
+ userId,
+ );
}
+ // [PM-23246] "Legacy" master key setting path - to be removed once unlock path migration is complete
await this.masterPasswordService.setMasterKeyHash(newLocalMasterKeyHash, userId);
if (resetPasswordAutoEnroll) {
@@ -206,10 +234,40 @@ export class DefaultSetInitialPasswordService implements SetInitialPasswordServi
userDecryptionOpts,
);
await this.kdfConfigService.setKdfConfig(userId, kdfConfig);
+ // [PM-23246] "Legacy" master key setting path - to be removed once unlock path migration is complete
await this.masterPasswordService.setMasterKey(masterKey, userId);
+ // [PM-23246] "Legacy" master key setting path - to be removed once unlock path migration is complete
+ await this.masterPasswordService.setMasterKeyEncryptedUserKey(
+ masterKeyEncryptedUserKey[1],
+ userId,
+ );
await this.keyService.setUserKey(masterKeyEncryptedUserKey[0], userId);
}
+ /**
+ * As part of [PM-28494], adding this setting path to accommodate the changes that are
+ * emerging with pm-23246-unlock-with-master-password-unlock-data.
+ * Without this, immediately locking/unlocking the vault with the new password _may_ still fail
+ * if sync has not completed. Sync will eventually set this data, but we want to ensure it's
+ * set right away here to prevent a race condition UX issue that prevents immediate unlock.
+ */
+ private async setMasterPasswordUnlockData(
+ password: string,
+ salt: MasterPasswordSalt,
+ kdfConfig: KdfConfig,
+ userKey: UserKey,
+ userId: UserId,
+ ): Promise {
+ const masterPasswordUnlockData = await this.masterPasswordService.makeMasterPasswordUnlockData(
+ password,
+ kdfConfig,
+ salt,
+ userKey,
+ );
+
+ await this.masterPasswordService.setMasterPasswordUnlockData(masterPasswordUnlockData, userId);
+ }
+
private async handleResetPasswordAutoEnroll(
masterKeyHash: string,
orgId: string,
diff --git a/libs/angular/src/auth/password-management/set-initial-password/default-set-initial-password.service.spec.ts b/libs/angular/src/auth/password-management/set-initial-password/default-set-initial-password.service.spec.ts
index 8b95090e776..af4505371d3 100644
--- a/libs/angular/src/auth/password-management/set-initial-password/default-set-initial-password.service.spec.ts
+++ b/libs/angular/src/auth/password-management/set-initial-password/default-set-initial-password.service.spec.ts
@@ -20,6 +20,7 @@ import { MasterPasswordApiService } from "@bitwarden/common/auth/abstractions/ma
import { ForceSetPasswordReason } from "@bitwarden/common/auth/models/domain/force-set-password-reason";
import { SetPasswordRequest } from "@bitwarden/common/auth/models/request/set-password.request";
import { UpdateTdeOffboardingPasswordRequest } from "@bitwarden/common/auth/models/request/update-tde-offboarding-password.request";
+import { AccountCryptographicStateService } from "@bitwarden/common/key-management/account-cryptography/account-cryptographic-state.service";
import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service";
import {
EncryptedString,
@@ -56,6 +57,7 @@ describe("DefaultSetInitialPasswordService", () => {
let organizationApiService: MockProxy;
let organizationUserApiService: MockProxy;
let userDecryptionOptionsService: MockProxy;
+ let accountCryptographicStateService: MockProxy;
let userId: UserId;
let userKey: UserKey;
@@ -73,6 +75,7 @@ describe("DefaultSetInitialPasswordService", () => {
organizationApiService = mock();
organizationUserApiService = mock();
userDecryptionOptionsService = mock();
+ accountCryptographicStateService = mock();
userId = "userId" as UserId;
userKey = new SymmetricCryptoKey(new Uint8Array(64).buffer as CsprngArray) as UserKey;
@@ -90,6 +93,7 @@ describe("DefaultSetInitialPasswordService", () => {
organizationApiService,
organizationUserApiService,
userDecryptionOptionsService,
+ accountCryptographicStateService,
);
});
@@ -130,6 +134,8 @@ describe("DefaultSetInitialPasswordService", () => {
orgSsoIdentifier: "orgSsoIdentifier",
orgId: "orgId",
resetPasswordAutoEnroll: false,
+ newPassword: "Test@Password123!",
+ salt: "user@example.com" as any,
};
userType = SetInitialPasswordUserType.JIT_PROVISIONED_MP_ORG_USER;
@@ -222,6 +228,8 @@ describe("DefaultSetInitialPasswordService", () => {
"orgSsoIdentifier",
"orgId",
"resetPasswordAutoEnroll",
+ "newPassword",
+ "salt",
].forEach((key) => {
it(`should throw if ${key} is not provided on the SetInitialPasswordCredentials object`, async () => {
// Arrange
@@ -353,6 +361,10 @@ describe("DefaultSetInitialPasswordService", () => {
ForceSetPasswordReason.None,
userId,
);
+ expect(masterPasswordService.setMasterKeyEncryptedUserKey).toHaveBeenCalledWith(
+ masterKeyEncryptedUserKey[1],
+ userId,
+ );
});
it("should update account decryption properties", async () => {
@@ -386,6 +398,16 @@ describe("DefaultSetInitialPasswordService", () => {
// Assert
expect(masterPasswordApiService.setPassword).toHaveBeenCalledWith(setPasswordRequest);
expect(keyService.setPrivateKey).toHaveBeenCalledWith(keyPair[1].encryptedString, userId);
+ expect(
+ accountCryptographicStateService.setAccountCryptographicState,
+ ).toHaveBeenCalledWith(
+ {
+ V1: {
+ private_key: keyPair[1].encryptedString as EncryptedString,
+ },
+ },
+ userId,
+ );
});
it("should set the local master key hash to state", async () => {
@@ -403,6 +425,36 @@ describe("DefaultSetInitialPasswordService", () => {
);
});
+ it("should create and set master password unlock data to prevent race condition with sync", async () => {
+ // Arrange
+ setupMocks();
+
+ const mockUnlockData = {
+ salt: credentials.salt,
+ kdf: credentials.kdfConfig,
+ masterKeyWrappedUserKey: "wrapped_key_string",
+ };
+
+ masterPasswordService.makeMasterPasswordUnlockData.mockResolvedValue(
+ mockUnlockData as any,
+ );
+
+ // Act
+ await sut.setInitialPassword(credentials, userType, userId);
+
+ // Assert
+ expect(masterPasswordService.makeMasterPasswordUnlockData).toHaveBeenCalledWith(
+ credentials.newPassword,
+ credentials.kdfConfig,
+ credentials.salt,
+ masterKeyEncryptedUserKey[0],
+ );
+ expect(masterPasswordService.setMasterPasswordUnlockData).toHaveBeenCalledWith(
+ mockUnlockData,
+ userId,
+ );
+ });
+
describe("given resetPasswordAutoEnroll is true", () => {
it(`should handle reset password (account recovery) auto enroll`, async () => {
// Arrange
@@ -572,6 +624,10 @@ describe("DefaultSetInitialPasswordService", () => {
credentials.newMasterKey,
userId,
);
+ expect(masterPasswordService.setMasterKeyEncryptedUserKey).toHaveBeenCalledWith(
+ masterKeyEncryptedUserKey[1],
+ userId,
+ );
expect(keyService.setUserKey).toHaveBeenCalledWith(masterKeyEncryptedUserKey[0], userId);
});
@@ -602,6 +658,36 @@ describe("DefaultSetInitialPasswordService", () => {
);
});
+ it("should create and set master password unlock data to prevent race condition with sync", async () => {
+ // Arrange
+ setupMocks({ ...defaultMockConfig, userType });
+
+ const mockUnlockData = {
+ salt: credentials.salt,
+ kdf: credentials.kdfConfig,
+ masterKeyWrappedUserKey: "wrapped_key_string",
+ };
+
+ masterPasswordService.makeMasterPasswordUnlockData.mockResolvedValue(
+ mockUnlockData as any,
+ );
+
+ // Act
+ await sut.setInitialPassword(credentials, userType, userId);
+
+ // Assert
+ expect(masterPasswordService.makeMasterPasswordUnlockData).toHaveBeenCalledWith(
+ credentials.newPassword,
+ credentials.kdfConfig,
+ credentials.salt,
+ masterKeyEncryptedUserKey[0],
+ );
+ expect(masterPasswordService.setMasterPasswordUnlockData).toHaveBeenCalledWith(
+ mockUnlockData,
+ userId,
+ );
+ });
+
describe("given resetPasswordAutoEnroll is true", () => {
it(`should handle reset password (account recovery) auto enroll`, async () => {
// Arrange
diff --git a/libs/angular/src/auth/password-management/set-initial-password/set-initial-password.component.ts b/libs/angular/src/auth/password-management/set-initial-password/set-initial-password.component.ts
index 805fe3c0173..0e0bae62b9a 100644
--- a/libs/angular/src/auth/password-management/set-initial-password/set-initial-password.component.ts
+++ b/libs/angular/src/auth/password-management/set-initial-password/set-initial-password.component.ts
@@ -214,6 +214,8 @@ export class SetInitialPasswordComponent implements OnInit {
assertTruthy(passwordInputResult.newServerMasterKeyHash, "newServerMasterKeyHash", ctx);
assertTruthy(passwordInputResult.newLocalMasterKeyHash, "newLocalMasterKeyHash", ctx);
assertTruthy(passwordInputResult.kdfConfig, "kdfConfig", ctx);
+ assertTruthy(passwordInputResult.newPassword, "newPassword", ctx);
+ assertTruthy(passwordInputResult.salt, "salt", ctx);
assertTruthy(this.orgSsoIdentifier, "orgSsoIdentifier", ctx);
assertTruthy(this.orgId, "orgId", ctx);
assertTruthy(this.userType, "userType", ctx);
@@ -231,6 +233,8 @@ export class SetInitialPasswordComponent implements OnInit {
orgSsoIdentifier: this.orgSsoIdentifier,
orgId: this.orgId,
resetPasswordAutoEnroll: this.resetPasswordAutoEnroll,
+ newPassword: passwordInputResult.newPassword,
+ salt: passwordInputResult.salt,
};
await this.setInitialPasswordService.setInitialPassword(
diff --git a/libs/angular/src/auth/password-management/set-initial-password/set-initial-password.service.abstraction.ts b/libs/angular/src/auth/password-management/set-initial-password/set-initial-password.service.abstraction.ts
index c1f6ba1a5ec..5620194e1bb 100644
--- a/libs/angular/src/auth/password-management/set-initial-password/set-initial-password.service.abstraction.ts
+++ b/libs/angular/src/auth/password-management/set-initial-password/set-initial-password.service.abstraction.ts
@@ -1,3 +1,4 @@
+import { MasterPasswordSalt } from "@bitwarden/common/key-management/master-password/types/master-password.types";
import { UserId } from "@bitwarden/common/types/guid";
import { MasterKey } from "@bitwarden/common/types/key";
import { KdfConfig } from "@bitwarden/key-management";
@@ -50,6 +51,8 @@ export interface SetInitialPasswordCredentials {
orgSsoIdentifier: string;
orgId: string;
resetPasswordAutoEnroll: boolean;
+ newPassword: string;
+ salt: MasterPasswordSalt;
}
export interface SetInitialPasswordTdeOffboardingCredentials {
diff --git a/libs/angular/src/key-management/encrypted-migration/prompt-migration-password.component.ts b/libs/angular/src/key-management/encrypted-migration/prompt-migration-password.component.ts
index 060901d68fb..59fec1a6f70 100644
--- a/libs/angular/src/key-management/encrypted-migration/prompt-migration-password.component.ts
+++ b/libs/angular/src/key-management/encrypted-migration/prompt-migration-password.component.ts
@@ -5,8 +5,7 @@ import { filter, firstValueFrom, map } from "rxjs";
import { JslibModule } from "@bitwarden/angular/jslib.module";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
-import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction";
-import { VerificationType } from "@bitwarden/common/auth/enums/verification-type";
+import { MasterPasswordUnlockService } from "@bitwarden/common/key-management/master-password/abstractions/master-password-unlock.service";
import {
LinkModule,
AsyncActionsModule,
@@ -39,7 +38,7 @@ import {
export class PromptMigrationPasswordComponent {
private dialogRef = inject(DialogRef);
private formBuilder = inject(FormBuilder);
- private uvService = inject(UserVerificationService);
+ private masterPasswordUnlockService = inject(MasterPasswordUnlockService);
private accountService = inject(AccountService);
migrationPasswordForm = this.formBuilder.group({
@@ -57,23 +56,21 @@ export class PromptMigrationPasswordComponent {
return;
}
- const { userId, email } = await firstValueFrom(
+ const { userId } = await firstValueFrom(
this.accountService.activeAccount$.pipe(
filter((account) => account != null),
map((account) => {
return {
userId: account!.id,
- email: account!.email,
};
}),
),
);
if (
- !(await this.uvService.verifyUserByMasterPassword(
- { type: VerificationType.MasterPassword, secret: masterPasswordControl.value },
+ !(await this.masterPasswordUnlockService.proofOfDecryption(
+ masterPasswordControl.value,
userId,
- email,
))
) {
return;
diff --git a/libs/angular/src/services/jslib-services.module.ts b/libs/angular/src/services/jslib-services.module.ts
index 816e09fd45d..ab6ca7295e3 100644
--- a/libs/angular/src/services/jslib-services.module.ts
+++ b/libs/angular/src/services/jslib-services.module.ts
@@ -168,6 +168,8 @@ import { OrganizationBillingService } from "@bitwarden/common/billing/services/o
import { DefaultSubscriptionPricingService } from "@bitwarden/common/billing/services/subscription-pricing.service";
import { HibpApiService } from "@bitwarden/common/dirt/services/hibp-api.service";
import { ProcessReloadServiceAbstraction } from "@bitwarden/common/key-management/abstractions/process-reload.service";
+import { AccountCryptographicStateService } from "@bitwarden/common/key-management/account-cryptography/account-cryptographic-state.service";
+import { DefaultAccountCryptographicStateService } from "@bitwarden/common/key-management/account-cryptography/default-account-cryptographic-state.service";
import {
DefaultKeyGenerationService,
KeyGenerationService,
@@ -528,7 +530,7 @@ const safeProviders: SafeProvider[] = [
safeProvider({
provide: ChangeKdfService,
useClass: DefaultChangeKdfService,
- deps: [ChangeKdfApiService, SdkService],
+ deps: [ChangeKdfApiService, SdkService, KeyService, InternalMasterPasswordServiceAbstraction],
}),
safeProvider({
provide: EncryptedMigrator,
@@ -572,6 +574,7 @@ const safeProviders: SafeProvider[] = [
KdfConfigService,
TaskSchedulerService,
ConfigService,
+ AccountCryptographicStateService,
],
}),
safeProvider({
@@ -894,8 +897,14 @@ const safeProviders: SafeProvider[] = [
StateProvider,
SecurityStateService,
KdfConfigService,
+ AccountCryptographicStateService,
],
}),
+ safeProvider({
+ provide: AccountCryptographicStateService,
+ useClass: DefaultAccountCryptographicStateService,
+ deps: [StateProvider],
+ }),
safeProvider({
provide: BroadcasterService,
useClass: DefaultBroadcasterService,
@@ -1333,7 +1342,7 @@ const safeProviders: SafeProvider[] = [
safeProvider({
provide: ChangeKdfService,
useClass: DefaultChangeKdfService,
- deps: [ChangeKdfApiService, SdkService],
+ deps: [ChangeKdfApiService, SdkService, KeyService, InternalMasterPasswordServiceAbstraction],
}),
safeProvider({
provide: AuthRequestServiceAbstraction,
@@ -1565,6 +1574,7 @@ const safeProviders: SafeProvider[] = [
OrganizationApiServiceAbstraction,
OrganizationUserApiService,
InternalUserDecryptionOptionsServiceAbstraction,
+ AccountCryptographicStateService,
],
}),
safeProvider({
diff --git a/libs/auth/src/angular/login-decryption-options/login-decryption-options.component.spec.ts b/libs/auth/src/angular/login-decryption-options/login-decryption-options.component.spec.ts
new file mode 100644
index 00000000000..248eaa608af
--- /dev/null
+++ b/libs/auth/src/angular/login-decryption-options/login-decryption-options.component.spec.ts
@@ -0,0 +1,377 @@
+// Mock asUuid to return the input value for test consistency
+jest.mock("@bitwarden/common/platform/abstractions/sdk/sdk.service", () => ({
+ asUuid: (x: any) => x,
+}));
+
+import { DestroyRef } from "@angular/core";
+import { FormBuilder } from "@angular/forms";
+import { Router } from "@angular/router";
+import { mock, MockProxy } from "jest-mock-extended";
+import { BehaviorSubject, of } from "rxjs";
+
+import {
+ LoginEmailServiceAbstraction,
+ LogoutService,
+ UserDecryptionOptionsServiceAbstraction,
+} from "@bitwarden/auth/common";
+import { ApiService } from "@bitwarden/common/abstractions/api.service";
+import { OrganizationApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization/organization-api.service.abstraction";
+import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
+import { PasswordResetEnrollmentServiceAbstraction } from "@bitwarden/common/auth/abstractions/password-reset-enrollment.service.abstraction";
+import { SsoLoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/sso-login.service.abstraction";
+import { ClientType } from "@bitwarden/common/enums";
+import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
+import { DeviceTrustServiceAbstraction } from "@bitwarden/common/key-management/device-trust/abstractions/device-trust.service.abstraction";
+import { SecurityStateService } from "@bitwarden/common/key-management/security-state/abstractions/security-state.service";
+import { SignedSecurityState } from "@bitwarden/common/key-management/types";
+import { AppIdService } from "@bitwarden/common/platform/abstractions/app-id.service";
+import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
+import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
+import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
+import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
+import { RegisterSdkService } from "@bitwarden/common/platform/abstractions/sdk/register-sdk.service";
+import { ValidationService } from "@bitwarden/common/platform/abstractions/validation.service";
+import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key";
+import { UserId } from "@bitwarden/common/types/guid";
+// eslint-disable-next-line no-restricted-imports
+import { AnonLayoutWrapperDataService, DialogService, ToastService } from "@bitwarden/components";
+import { KeyService } from "@bitwarden/key-management";
+
+import { LoginDecryptionOptionsComponent } from "./login-decryption-options.component";
+import { LoginDecryptionOptionsService } from "./login-decryption-options.service";
+
+describe("LoginDecryptionOptionsComponent", () => {
+ let component: LoginDecryptionOptionsComponent;
+ let accountService: MockProxy;
+ let anonLayoutWrapperDataService: MockProxy;
+ let apiService: MockProxy;
+ let destroyRef: MockProxy;
+ let deviceTrustService: MockProxy;
+ let dialogService: MockProxy;
+ let formBuilder: FormBuilder;
+ let i18nService: MockProxy;
+ let keyService: MockProxy;
+ let loginDecryptionOptionsService: MockProxy;
+ let loginEmailService: MockProxy;
+ let messagingService: MockProxy;
+ let organizationApiService: MockProxy;
+ let passwordResetEnrollmentService: MockProxy;
+ let platformUtilsService: MockProxy;
+ let router: MockProxy;
+ let ssoLoginService: MockProxy;
+ let toastService: MockProxy;
+ let userDecryptionOptionsService: MockProxy;
+ let validationService: MockProxy;
+ let logoutService: MockProxy;
+ let registerSdkService: MockProxy;
+ let securityStateService: MockProxy;
+ let appIdService: MockProxy;
+ let configService: MockProxy;
+ let accountCryptographicStateService: MockProxy;
+
+ const mockUserId = "user-id-123" as UserId;
+ const mockEmail = "test@example.com";
+ const mockOrgId = "org-id-456";
+
+ beforeEach(() => {
+ accountService = mock();
+ anonLayoutWrapperDataService = mock();
+ apiService = mock();
+ destroyRef = mock();
+ deviceTrustService = mock