-
-
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 ac70e1920ee..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 113c51327b2..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 5e6f81d99d6..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: "export",
+ 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 3a663dbcbe9..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 31029d134fa..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: "import",
+ titleId: "importNoun",
},
},
{
@@ -20,7 +20,7 @@ const routes: Routes = [
component: SecretsManagerExportComponent,
canActivate: [organizationPermissionsGuard((org) => org.isAdmin)],
data: {
- titleId: "export",
+ 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..bd3f78b9290 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,6 +15,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 { EncString } from "@bitwarden/common/key-management/crypto/models/enc-string";
import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/key-management/master-password/abstractions/master-password.service.abstraction";
@@ -44,6 +45,7 @@ export class DefaultSetInitialPasswordService implements SetInitialPasswordServi
protected organizationApiService: OrganizationApiServiceAbstraction,
protected organizationUserApiService: OrganizationUserApiService,
protected userDecryptionOptionsService: InternalUserDecryptionOptionsServiceAbstraction,
+ protected accountCryptographicStateService: AccountCryptographicStateService,
) {}
async setInitialPassword(
@@ -162,6 +164,14 @@ 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,
+ );
}
await this.masterPasswordService.setMasterKeyHash(newLocalMasterKeyHash, userId);
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..cfea011d0d9 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,
);
});
@@ -386,6 +390,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 () => {
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/sso/sso.component.ts b/libs/auth/src/angular/sso/sso.component.ts
index d0cc2bd83e5..f5167cb84cc 100644
--- a/libs/auth/src/angular/sso/sso.component.ts
+++ b/libs/auth/src/angular/sso/sso.component.ts
@@ -478,7 +478,7 @@ export class SsoComponent implements OnInit {
!userDecryptionOpts.hasMasterPassword &&
userDecryptionOpts.keyConnectorOption === undefined;
- if (requireSetPassword || authResult.resetMasterPassword) {
+ if (requireSetPassword) {
// Change implies going no password -> password in this case
return await this.handleChangePasswordRequired(orgSsoIdentifier);
}
diff --git a/libs/auth/src/angular/two-factor-auth/two-factor-auth.component.ts b/libs/auth/src/angular/two-factor-auth/two-factor-auth.component.ts
index 4c143cc59f9..91d24532a70 100644
--- a/libs/auth/src/angular/two-factor-auth/two-factor-auth.component.ts
+++ b/libs/auth/src/angular/two-factor-auth/two-factor-auth.component.ts
@@ -487,7 +487,7 @@ export class TwoFactorAuthComponent implements OnInit, OnDestroy {
!userDecryptionOpts.hasMasterPassword && userDecryptionOpts.keyConnectorOption === undefined;
// New users without a master password must set a master password before advancing.
- if (requireSetPassword || authResult.resetMasterPassword) {
+ if (requireSetPassword) {
// Change implies going no password -> password in this case
return await this.handleChangePasswordRequired(this.orgSsoIdentifier);
}
diff --git a/libs/auth/src/common/login-strategies/auth-request-login.strategy.spec.ts b/libs/auth/src/common/login-strategies/auth-request-login.strategy.spec.ts
index b536ae0dc4b..4703472d480 100644
--- a/libs/auth/src/common/login-strategies/auth-request-login.strategy.spec.ts
+++ b/libs/auth/src/common/login-strategies/auth-request-login.strategy.spec.ts
@@ -6,6 +6,7 @@ import { TokenService } from "@bitwarden/common/auth/abstractions/token.service"
import { IdentityTokenResponse } from "@bitwarden/common/auth/models/response/identity-token.response";
import { TwoFactorService } from "@bitwarden/common/auth/two-factor";
import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service";
+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 { DeviceTrustServiceAbstraction } from "@bitwarden/common/key-management/device-trust/abstractions/device-trust.service.abstraction";
import { FakeMasterPasswordService } from "@bitwarden/common/key-management/master-password/services/fake-master-password.service";
@@ -22,7 +23,7 @@ import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/pl
import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
import { Utils } from "@bitwarden/common/platform/misc/utils";
import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key";
-import { FakeAccountService, mockAccountServiceWith } from "@bitwarden/common/spec";
+import { makeEncString, FakeAccountService, mockAccountServiceWith } from "@bitwarden/common/spec";
import { CsprngArray } from "@bitwarden/common/types/csprng";
import { UserId } from "@bitwarden/common/types/guid";
import { MasterKey, UserKey } from "@bitwarden/common/types/key";
@@ -57,6 +58,7 @@ describe("AuthRequestLoginStrategy", () => {
let kdfConfigService: MockProxy;
let environmentService: MockProxy;
let configService: MockProxy;
+ let accountCryptographicStateService: MockProxy;
const mockUserId = Utils.newGuid() as UserId;
let accountService: FakeAccountService;
@@ -94,6 +96,7 @@ describe("AuthRequestLoginStrategy", () => {
kdfConfigService = mock();
environmentService = mock();
configService = mock();
+ accountCryptographicStateService = mock();
accountService = mockAccountServiceWith(mockUserId);
masterPasswordService = new FakeMasterPasswordService();
@@ -125,6 +128,7 @@ describe("AuthRequestLoginStrategy", () => {
kdfConfigService,
environmentService,
configService,
+ accountCryptographicStateService,
);
tokenResponse = identityTokenResponseFactory();
@@ -208,4 +212,41 @@ describe("AuthRequestLoginStrategy", () => {
// trustDeviceIfRequired should be called
expect(deviceTrustService.trustDeviceIfRequired).not.toHaveBeenCalled();
});
+
+ it("sets account cryptographic state when accountKeysResponseModel is present", async () => {
+ const accountKeysData = {
+ publicKeyEncryptionKeyPair: {
+ publicKey: "testPublicKey",
+ wrappedPrivateKey: "testPrivateKey",
+ },
+ };
+
+ tokenResponse = identityTokenResponseFactory();
+ tokenResponse.key = makeEncString("mockEncryptedUserKey");
+ // Add accountKeysResponseModel to the response
+ (tokenResponse as any).accountKeysResponseModel = {
+ publicKeyEncryptionKeyPair: accountKeysData.publicKeyEncryptionKeyPair,
+ toWrappedAccountCryptographicState: jest.fn().mockReturnValue({
+ V1: {
+ private_key: "testPrivateKey",
+ },
+ }),
+ };
+
+ apiService.postIdentityToken.mockResolvedValue(tokenResponse);
+ masterPasswordService.masterKeySubject.next(decMasterKey);
+ masterPasswordService.mock.decryptUserKeyWithMasterKey.mockResolvedValue(decUserKey);
+
+ await authRequestLoginStrategy.logIn(credentials);
+
+ expect(accountCryptographicStateService.setAccountCryptographicState).toHaveBeenCalledTimes(1);
+ expect(accountCryptographicStateService.setAccountCryptographicState).toHaveBeenCalledWith(
+ {
+ V1: {
+ private_key: "testPrivateKey",
+ },
+ },
+ mockUserId,
+ );
+ });
});
diff --git a/libs/auth/src/common/login-strategies/auth-request-login.strategy.ts b/libs/auth/src/common/login-strategies/auth-request-login.strategy.ts
index 7337b6733f8..16af5fa77dc 100644
--- a/libs/auth/src/common/login-strategies/auth-request-login.strategy.ts
+++ b/libs/auth/src/common/login-strategies/auth-request-login.strategy.ts
@@ -128,6 +128,12 @@ export class AuthRequestLoginStrategy extends LoginStrategy {
response.privateKey ?? (await this.createKeyPairForOldAccount(userId)),
userId,
);
+ if (response.accountKeysResponseModel) {
+ await this.accountCryptographicStateService.setAccountCryptographicState(
+ response.accountKeysResponseModel.toWrappedAccountCryptographicState(),
+ userId,
+ );
+ }
}
exportCache(): CacheData {
diff --git a/libs/auth/src/common/login-strategies/login.strategy.spec.ts b/libs/auth/src/common/login-strategies/login.strategy.spec.ts
index 82e1183a1d6..113f9f3f0d9 100644
--- a/libs/auth/src/common/login-strategies/login.strategy.spec.ts
+++ b/libs/auth/src/common/login-strategies/login.strategy.spec.ts
@@ -17,6 +17,7 @@ import { MasterPasswordPolicyResponse } from "@bitwarden/common/auth/models/resp
import { IUserDecryptionOptionsServerResponse } from "@bitwarden/common/auth/models/response/user-decryption-options/user-decryption-options.response";
import { TwoFactorService } from "@bitwarden/common/auth/two-factor";
import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service";
+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 { FakeMasterPasswordService } from "@bitwarden/common/key-management/master-password/services/fake-master-password.service";
@@ -101,7 +102,6 @@ export function identityTokenResponseFactory(
KdfIterations: kdfIterations,
Key: encryptedUserKey,
PrivateKey: privateKey,
- ResetMasterPassword: false,
access_token: accessToken,
expires_in: 3600,
refresh_token: refreshToken,
@@ -137,6 +137,7 @@ describe("LoginStrategy", () => {
let kdfConfigService: MockProxy;
let environmentService: MockProxy;
let configService: MockProxy;
+ let accountCryptographicStateService: MockProxy;
let passwordLoginStrategy: PasswordLoginStrategy;
let credentials: PasswordLoginCredentials;
@@ -163,6 +164,7 @@ describe("LoginStrategy", () => {
billingAccountProfileStateService = mock();
environmentService = mock();
configService = mock();
+ accountCryptographicStateService = mock();
vaultTimeoutSettingsService = mock();
@@ -193,6 +195,7 @@ describe("LoginStrategy", () => {
kdfConfigService,
environmentService,
configService,
+ accountCryptographicStateService,
);
credentials = new PasswordLoginCredentials(email, masterPassword);
});
@@ -301,7 +304,6 @@ describe("LoginStrategy", () => {
it("builds AuthResult", async () => {
const tokenResponse = identityTokenResponseFactory();
tokenResponse.forcePasswordReset = true;
- tokenResponse.resetMasterPassword = true;
apiService.postIdentityToken.mockResolvedValue(tokenResponse);
@@ -310,7 +312,6 @@ describe("LoginStrategy", () => {
const expected = new AuthResult();
expected.masterPassword = "password";
expected.userId = userId;
- expected.resetMasterPassword = true;
expected.twoFactorProviders = null;
expect(result).toEqual(expected);
});
@@ -326,7 +327,6 @@ describe("LoginStrategy", () => {
const expected = new AuthResult();
expected.masterPassword = "password";
expected.userId = userId;
- expected.resetMasterPassword = false;
expected.twoFactorProviders = null;
expect(result).toEqual(expected);
@@ -522,6 +522,7 @@ describe("LoginStrategy", () => {
kdfConfigService,
environmentService,
configService,
+ accountCryptographicStateService,
);
apiService.postIdentityToken.mockResolvedValue(identityTokenResponseFactory());
@@ -583,6 +584,7 @@ describe("LoginStrategy", () => {
kdfConfigService,
environmentService,
configService,
+ accountCryptographicStateService,
);
const result = await passwordLoginStrategy.logIn(credentials);
diff --git a/libs/auth/src/common/login-strategies/login.strategy.ts b/libs/auth/src/common/login-strategies/login.strategy.ts
index acb32969f08..16f5e1e4320 100644
--- a/libs/auth/src/common/login-strategies/login.strategy.ts
+++ b/libs/auth/src/common/login-strategies/login.strategy.ts
@@ -18,6 +18,7 @@ import { IdentityTokenResponse } from "@bitwarden/common/auth/models/response/id
import { IdentityTwoFactorResponse } from "@bitwarden/common/auth/models/response/identity-two-factor.response";
import { TwoFactorService } from "@bitwarden/common/auth/two-factor";
import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service";
+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 { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/key-management/master-password/abstractions/master-password.service.abstraction";
import {
@@ -89,6 +90,7 @@ export abstract class LoginStrategy {
protected KdfConfigService: KdfConfigService,
protected environmentService: EnvironmentService,
protected configService: ConfigService,
+ protected accountCryptographicStateService: AccountCryptographicStateService,
) {}
abstract exportCache(): CacheData;
@@ -254,8 +256,6 @@ export abstract class LoginStrategy {
const userId = await this.saveAccountInformation(response);
result.userId = userId;
- result.resetMasterPassword = response.resetMasterPassword;
-
if (response.twoFactorToken != null) {
// note: we can read email from access token b/c it was saved in saveAccountInformation
const userEmail = await this.tokenService.getEmail();
diff --git a/libs/auth/src/common/login-strategies/password-login.strategy.spec.ts b/libs/auth/src/common/login-strategies/password-login.strategy.spec.ts
index 26ae1270f39..4188b779d81 100644
--- a/libs/auth/src/common/login-strategies/password-login.strategy.spec.ts
+++ b/libs/auth/src/common/login-strategies/password-login.strategy.spec.ts
@@ -12,6 +12,7 @@ import { IdentityTwoFactorResponse } from "@bitwarden/common/auth/models/respons
import { MasterPasswordPolicyResponse } from "@bitwarden/common/auth/models/response/master-password-policy.response";
import { TwoFactorService } from "@bitwarden/common/auth/two-factor";
import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service";
+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 { FakeMasterPasswordService } from "@bitwarden/common/key-management/master-password/services/fake-master-password.service";
import {
@@ -28,7 +29,7 @@ import { StateService } from "@bitwarden/common/platform/abstractions/state.serv
import { HashPurpose } from "@bitwarden/common/platform/enums";
import { Utils } from "@bitwarden/common/platform/misc/utils";
import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key";
-import { FakeAccountService, mockAccountServiceWith } from "@bitwarden/common/spec";
+import { FakeAccountService, makeEncString, mockAccountServiceWith } from "@bitwarden/common/spec";
import {
PasswordStrengthServiceAbstraction,
PasswordStrengthService,
@@ -85,6 +86,7 @@ describe("PasswordLoginStrategy", () => {
let kdfConfigService: MockProxy;
let environmentService: MockProxy;
let configService: MockProxy;
+ let accountCryptographicStateService: MockProxy;
let passwordLoginStrategy: PasswordLoginStrategy;
let credentials: PasswordLoginCredentials;
@@ -113,6 +115,7 @@ describe("PasswordLoginStrategy", () => {
kdfConfigService = mock();
environmentService = mock();
configService = mock();
+ accountCryptographicStateService = mock();
appIdService.getAppId.mockResolvedValue(deviceId);
tokenService.decodeAccessToken.mockResolvedValue({
@@ -153,6 +156,7 @@ describe("PasswordLoginStrategy", () => {
kdfConfigService,
environmentService,
configService,
+ accountCryptographicStateService,
);
credentials = new PasswordLoginCredentials(email, masterPassword);
tokenResponse = identityTokenResponseFactory(masterPasswordPolicyResponse);
@@ -390,7 +394,45 @@ describe("PasswordLoginStrategy", () => {
newDeviceOtp: deviceVerificationOtp,
}),
);
- expect(result.resetMasterPassword).toBe(false);
expect(result.userId).toBe(userId);
});
+
+ it("sets account cryptographic state when accountKeysResponseModel is present", async () => {
+ const accountKeysData = {
+ publicKeyEncryptionKeyPair: {
+ publicKey: "testPublicKey",
+ wrappedPrivateKey: "testPrivateKey",
+ },
+ };
+
+ tokenResponse = identityTokenResponseFactory();
+ tokenResponse.key = makeEncString("mockEncryptedUserKey");
+ // Add accountKeysResponseModel to the response
+ (tokenResponse as any).accountKeysResponseModel = {
+ publicKeyEncryptionKeyPair: accountKeysData.publicKeyEncryptionKeyPair,
+ toWrappedAccountCryptographicState: jest.fn().mockReturnValue({
+ V1: {
+ private_key: "testPrivateKey",
+ },
+ }),
+ };
+
+ apiService.postIdentityToken.mockResolvedValue(tokenResponse);
+ masterPasswordService.masterKeySubject.next(masterKey);
+ masterPasswordService.mock.decryptUserKeyWithMasterKey.mockResolvedValue(
+ new SymmetricCryptoKey(new Uint8Array(64).buffer as CsprngArray) as UserKey,
+ );
+
+ await passwordLoginStrategy.logIn(credentials);
+
+ expect(accountCryptographicStateService.setAccountCryptographicState).toHaveBeenCalledTimes(1);
+ expect(accountCryptographicStateService.setAccountCryptographicState).toHaveBeenCalledWith(
+ {
+ V1: {
+ private_key: "testPrivateKey",
+ },
+ },
+ userId,
+ );
+ });
});
diff --git a/libs/auth/src/common/login-strategies/password-login.strategy.ts b/libs/auth/src/common/login-strategies/password-login.strategy.ts
index 842a48e28cd..63fff52194b 100644
--- a/libs/auth/src/common/login-strategies/password-login.strategy.ts
+++ b/libs/auth/src/common/login-strategies/password-login.strategy.ts
@@ -156,6 +156,12 @@ export class PasswordLoginStrategy extends LoginStrategy {
response.privateKey ?? (await this.createKeyPairForOldAccount(userId)),
userId,
);
+ if (response.accountKeysResponseModel) {
+ await this.accountCryptographicStateService.setAccountCryptographicState(
+ response.accountKeysResponseModel.toWrappedAccountCryptographicState(),
+ userId,
+ );
+ }
}
protected override encryptionKeyMigrationRequired(response: IdentityTokenResponse): boolean {
diff --git a/libs/auth/src/common/login-strategies/sso-login.strategy.spec.ts b/libs/auth/src/common/login-strategies/sso-login.strategy.spec.ts
index 3dbce6500a8..484beb785d3 100644
--- a/libs/auth/src/common/login-strategies/sso-login.strategy.spec.ts
+++ b/libs/auth/src/common/login-strategies/sso-login.strategy.spec.ts
@@ -10,6 +10,7 @@ import { IdentityTokenResponse } from "@bitwarden/common/auth/models/response/id
import { IUserDecryptionOptionsServerResponse } from "@bitwarden/common/auth/models/response/user-decryption-options/user-decryption-options.response";
import { TwoFactorService } from "@bitwarden/common/auth/two-factor";
import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service";
+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 } from "@bitwarden/common/key-management/crypto/models/enc-string";
import { DeviceTrustServiceAbstraction } from "@bitwarden/common/key-management/device-trust/abstractions/device-trust.service.abstraction";
@@ -30,7 +31,7 @@ import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/pl
import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
import { Utils } from "@bitwarden/common/platform/misc/utils";
import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key";
-import { FakeAccountService, mockAccountServiceWith } from "@bitwarden/common/spec";
+import { FakeAccountService, makeEncString, mockAccountServiceWith } from "@bitwarden/common/spec";
import { CsprngArray } from "@bitwarden/common/types/csprng";
import { UserId } from "@bitwarden/common/types/guid";
import { DeviceKey, MasterKey, UserKey } from "@bitwarden/common/types/key";
@@ -70,6 +71,7 @@ describe("SsoLoginStrategy", () => {
let kdfConfigService: MockProxy;
let environmentService: MockProxy;
let configService: MockProxy;
+ let accountCryptographicStateService: MockProxy;
let ssoLoginStrategy: SsoLoginStrategy;
let credentials: SsoLoginCredentials;
@@ -108,6 +110,7 @@ describe("SsoLoginStrategy", () => {
kdfConfigService = mock();
environmentService = mock();
configService = mock();
+ accountCryptographicStateService = mock();
tokenService.getTwoFactorToken.mockResolvedValue(null);
appIdService.getAppId.mockResolvedValue(deviceId);
@@ -162,6 +165,7 @@ describe("SsoLoginStrategy", () => {
kdfConfigService,
environmentService,
configService,
+ accountCryptographicStateService,
);
credentials = new SsoLoginCredentials(ssoCode, ssoCodeVerifier, ssoRedirectUrl, ssoOrgId);
});
@@ -556,4 +560,39 @@ describe("SsoLoginStrategy", () => {
expect(keyService.setUserKey).toHaveBeenCalledWith(userKey, userId);
});
});
+
+ it("sets account cryptographic state when accountKeysResponseModel is present", async () => {
+ const accountKeysData = {
+ publicKeyEncryptionKeyPair: {
+ publicKey: "testPublicKey",
+ wrappedPrivateKey: "testPrivateKey",
+ },
+ };
+
+ const tokenResponse = identityTokenResponseFactory();
+ tokenResponse.key = makeEncString("mockEncryptedUserKey");
+ // Add accountKeysResponseModel to the response
+ (tokenResponse as any).accountKeysResponseModel = {
+ publicKeyEncryptionKeyPair: accountKeysData.publicKeyEncryptionKeyPair,
+ toWrappedAccountCryptographicState: jest.fn().mockReturnValue({
+ V1: {
+ private_key: "testPrivateKey",
+ },
+ }),
+ };
+
+ apiService.postIdentityToken.mockResolvedValue(tokenResponse);
+
+ await ssoLoginStrategy.logIn(credentials);
+
+ expect(accountCryptographicStateService.setAccountCryptographicState).toHaveBeenCalledTimes(1);
+ expect(accountCryptographicStateService.setAccountCryptographicState).toHaveBeenCalledWith(
+ {
+ V1: {
+ private_key: "testPrivateKey",
+ },
+ },
+ userId,
+ );
+ });
});
diff --git a/libs/auth/src/common/login-strategies/sso-login.strategy.ts b/libs/auth/src/common/login-strategies/sso-login.strategy.ts
index 33802765aca..a9d60fca21a 100644
--- a/libs/auth/src/common/login-strategies/sso-login.strategy.ts
+++ b/libs/auth/src/common/login-strategies/sso-login.strategy.ts
@@ -339,6 +339,13 @@ export class SsoLoginStrategy extends LoginStrategy {
tokenResponse: IdentityTokenResponse,
userId: UserId,
): Promise {
+ if (tokenResponse.accountKeysResponseModel) {
+ await this.accountCryptographicStateService.setAccountCryptographicState(
+ tokenResponse.accountKeysResponseModel.toWrappedAccountCryptographicState(),
+ userId,
+ );
+ }
+
if (tokenResponse.hasMasterKeyEncryptedUserKey()) {
// User has masterKeyEncryptedUserKey, so set the userKeyEncryptedPrivateKey
// Note: new JIT provisioned SSO users will not yet have a user asymmetric key pair
diff --git a/libs/auth/src/common/login-strategies/user-api-login.strategy.spec.ts b/libs/auth/src/common/login-strategies/user-api-login.strategy.spec.ts
index 9bf282dee11..02613e527ec 100644
--- a/libs/auth/src/common/login-strategies/user-api-login.strategy.spec.ts
+++ b/libs/auth/src/common/login-strategies/user-api-login.strategy.spec.ts
@@ -5,6 +5,7 @@ import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { TokenService } from "@bitwarden/common/auth/abstractions/token.service";
import { TwoFactorService } from "@bitwarden/common/auth/two-factor";
import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service";
+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 { KeyConnectorService } from "@bitwarden/common/key-management/key-connector/abstractions/key-connector.service";
import { FakeMasterPasswordService } from "@bitwarden/common/key-management/master-password/services/fake-master-password.service";
@@ -58,6 +59,7 @@ describe("UserApiLoginStrategy", () => {
let vaultTimeoutSettingsService: MockProxy;
let kdfConfigService: MockProxy;
let configService: MockProxy;
+ let accountCryptographicStateService: MockProxy;
let apiLogInStrategy: UserApiLoginStrategy;
let credentials: UserApiLoginCredentials;
@@ -91,6 +93,7 @@ describe("UserApiLoginStrategy", () => {
vaultTimeoutSettingsService = mock();
kdfConfigService = mock();
configService = mock();
+ accountCryptographicStateService = mock();
appIdService.getAppId.mockResolvedValue(deviceId);
tokenService.getTwoFactorToken.mockResolvedValue(null);
@@ -119,6 +122,7 @@ describe("UserApiLoginStrategy", () => {
kdfConfigService,
environmentService,
configService,
+ accountCryptographicStateService,
);
credentials = new UserApiLoginCredentials(apiClientId, apiClientSecret);
@@ -226,4 +230,38 @@ describe("UserApiLoginStrategy", () => {
);
expect(keyService.setUserKey).toHaveBeenCalledWith(userKey, userId);
});
+
+ it("sets account cryptographic state when accountKeysResponseModel is present", async () => {
+ const accountKeysData = {
+ publicKeyEncryptionKeyPair: {
+ publicKey: "testPublicKey",
+ wrappedPrivateKey: "testPrivateKey",
+ },
+ };
+
+ const tokenResponse = identityTokenResponseFactory();
+ // Add accountKeysResponseModel to the response
+ (tokenResponse as any).accountKeysResponseModel = {
+ publicKeyEncryptionKeyPair: accountKeysData.publicKeyEncryptionKeyPair,
+ toWrappedAccountCryptographicState: jest.fn().mockReturnValue({
+ V1: {
+ private_key: "testPrivateKey",
+ },
+ }),
+ };
+
+ apiService.postIdentityToken.mockResolvedValue(tokenResponse);
+
+ await apiLogInStrategy.logIn(credentials);
+
+ expect(accountCryptographicStateService.setAccountCryptographicState).toHaveBeenCalledTimes(1);
+ expect(accountCryptographicStateService.setAccountCryptographicState).toHaveBeenCalledWith(
+ {
+ V1: {
+ private_key: "testPrivateKey",
+ },
+ },
+ userId,
+ );
+ });
});
diff --git a/libs/auth/src/common/login-strategies/user-api-login.strategy.ts b/libs/auth/src/common/login-strategies/user-api-login.strategy.ts
index df532799576..c5a9110d63e 100644
--- a/libs/auth/src/common/login-strategies/user-api-login.strategy.ts
+++ b/libs/auth/src/common/login-strategies/user-api-login.strategy.ts
@@ -87,6 +87,12 @@ export class UserApiLoginStrategy extends LoginStrategy {
response.privateKey ?? (await this.createKeyPairForOldAccount(userId)),
userId,
);
+ if (response.accountKeysResponseModel) {
+ await this.accountCryptographicStateService.setAccountCryptographicState(
+ response.accountKeysResponseModel.toWrappedAccountCryptographicState(),
+ userId,
+ );
+ }
}
// Overridden to save client ID and secret to token service
diff --git a/libs/auth/src/common/login-strategies/webauthn-login.strategy.spec.ts b/libs/auth/src/common/login-strategies/webauthn-login.strategy.spec.ts
index 53bc1c57905..2ae79f46d7c 100644
--- a/libs/auth/src/common/login-strategies/webauthn-login.strategy.spec.ts
+++ b/libs/auth/src/common/login-strategies/webauthn-login.strategy.spec.ts
@@ -9,6 +9,7 @@ import { IUserDecryptionOptionsServerResponse } from "@bitwarden/common/auth/mod
import { WebAuthnLoginAssertionResponseRequest } from "@bitwarden/common/auth/services/webauthn-login/request/webauthn-login-assertion-response.request";
import { TwoFactorService } from "@bitwarden/common/auth/two-factor";
import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service";
+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 { FakeMasterPasswordService } from "@bitwarden/common/key-management/master-password/services/fake-master-password.service";
import {
@@ -56,6 +57,7 @@ describe("WebAuthnLoginStrategy", () => {
let kdfConfigService: MockProxy;
let environmentService: MockProxy;
let configService: MockProxy;
+ let accountCryptographicStateService: MockProxy;
let webAuthnLoginStrategy!: WebAuthnLoginStrategy;
@@ -101,6 +103,7 @@ describe("WebAuthnLoginStrategy", () => {
kdfConfigService = mock();
environmentService = mock();
configService = mock();
+ accountCryptographicStateService = mock();
tokenService.getTwoFactorToken.mockResolvedValue(null);
appIdService.getAppId.mockResolvedValue(deviceId);
@@ -128,6 +131,7 @@ describe("WebAuthnLoginStrategy", () => {
kdfConfigService,
environmentService,
configService,
+ accountCryptographicStateService,
);
// Create credentials
@@ -212,7 +216,6 @@ describe("WebAuthnLoginStrategy", () => {
expect(authResult).toBeInstanceOf(AuthResult);
expect(authResult).toMatchObject({
- resetMasterPassword: false,
twoFactorProviders: null,
requiresTwoFactor: false,
});
@@ -341,6 +344,53 @@ describe("WebAuthnLoginStrategy", () => {
// Assert
expect(keyService.setUserKey).not.toHaveBeenCalled();
});
+
+ it("sets account cryptographic state when accountKeysResponseModel is present", async () => {
+ // Arrange
+ const accountKeysData = {
+ publicKeyEncryptionKeyPair: {
+ publicKey: "testPublicKey",
+ wrappedPrivateKey: "testPrivateKey",
+ },
+ };
+
+ const idTokenResponse: IdentityTokenResponse = identityTokenResponseFactory(
+ null,
+ userDecryptionOptsServerResponseWithWebAuthnPrfOption,
+ );
+ // Add accountKeysResponseModel to the response
+ (idTokenResponse as any).accountKeysResponseModel = {
+ publicKeyEncryptionKeyPair: accountKeysData.publicKeyEncryptionKeyPair,
+ toWrappedAccountCryptographicState: jest.fn().mockReturnValue({
+ V1: {
+ private_key: "testPrivateKey",
+ },
+ }),
+ };
+
+ apiService.postIdentityToken.mockResolvedValue(idTokenResponse);
+
+ const mockPrfPrivateKey: Uint8Array = randomBytes(32);
+ const mockUserKeyArray: Uint8Array = randomBytes(32);
+ encryptService.unwrapDecapsulationKey.mockResolvedValue(mockPrfPrivateKey);
+ encryptService.decapsulateKeyUnsigned.mockResolvedValue(
+ new SymmetricCryptoKey(mockUserKeyArray),
+ );
+
+ // Act
+ await webAuthnLoginStrategy.logIn(webAuthnCredentials);
+
+ // Assert
+ expect(accountCryptographicStateService.setAccountCryptographicState).toHaveBeenCalledTimes(1);
+ expect(accountCryptographicStateService.setAccountCryptographicState).toHaveBeenCalledWith(
+ {
+ V1: {
+ private_key: "testPrivateKey",
+ },
+ },
+ userId,
+ );
+ });
});
// Helpers and mocks
diff --git a/libs/auth/src/common/login-strategies/webauthn-login.strategy.ts b/libs/auth/src/common/login-strategies/webauthn-login.strategy.ts
index dbce7628335..77a881b5964 100644
--- a/libs/auth/src/common/login-strategies/webauthn-login.strategy.ts
+++ b/libs/auth/src/common/login-strategies/webauthn-login.strategy.ts
@@ -107,6 +107,12 @@ export class WebAuthnLoginStrategy extends LoginStrategy {
response.privateKey ?? (await this.createKeyPairForOldAccount(userId)),
userId,
);
+ if (response.accountKeysResponseModel) {
+ await this.accountCryptographicStateService.setAccountCryptographicState(
+ response.accountKeysResponseModel.toWrappedAccountCryptographicState(),
+ userId,
+ );
+ }
}
exportCache(): CacheData {
diff --git a/libs/auth/src/common/services/login-strategies/login-strategy.service.spec.ts b/libs/auth/src/common/services/login-strategies/login-strategy.service.spec.ts
index 20251e339a5..a79d6bb0514 100644
--- a/libs/auth/src/common/services/login-strategies/login-strategy.service.spec.ts
+++ b/libs/auth/src/common/services/login-strategies/login-strategy.service.spec.ts
@@ -13,6 +13,7 @@ import { PreloginResponse } from "@bitwarden/common/auth/models/response/prelogi
import { UserDecryptionOptionsResponse } from "@bitwarden/common/auth/models/response/user-decryption-options/user-decryption-options.response";
import { TwoFactorService } from "@bitwarden/common/auth/two-factor";
import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service";
+import { DefaultAccountCryptographicStateService } from "@bitwarden/common/key-management/account-cryptography/default-account-cryptographic-state.service";
import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service";
import { DeviceTrustServiceAbstraction } from "@bitwarden/common/key-management/device-trust/abstractions/device-trust.service.abstraction";
import { KeyConnectorService } from "@bitwarden/common/key-management/key-connector/abstractions/key-connector.service";
@@ -84,6 +85,7 @@ describe("LoginStrategyService", () => {
let kdfConfigService: MockProxy;
let taskSchedulerService: MockProxy;
let configService: MockProxy;
+ let accountCryptographicStateService: MockProxy;
let stateProvider: FakeGlobalStateProvider;
let loginStrategyCacheExpirationState: FakeGlobalState;
@@ -117,6 +119,7 @@ describe("LoginStrategyService", () => {
kdfConfigService = mock();
taskSchedulerService = mock();
configService = mock();
+ accountCryptographicStateService = mock();
sut = new LoginStrategyService(
accountService,
@@ -145,6 +148,7 @@ describe("LoginStrategyService", () => {
kdfConfigService,
taskSchedulerService,
configService,
+ accountCryptographicStateService,
);
loginStrategyCacheExpirationState = stateProvider.getFake(CACHE_EXPIRATION_KEY);
@@ -491,7 +495,6 @@ describe("LoginStrategyService", () => {
KdfParallelism: 1,
Key: "KEY",
PrivateKey: "PRIVATE_KEY",
- ResetMasterPassword: false,
access_token: "ACCESS_TOKEN",
expires_in: 3600,
refresh_token: "REFRESH_TOKEN",
@@ -559,7 +562,6 @@ describe("LoginStrategyService", () => {
KdfParallelism: 1,
Key: "KEY",
PrivateKey: "PRIVATE_KEY",
- ResetMasterPassword: false,
access_token: "ACCESS_TOKEN",
expires_in: 3600,
refresh_token: "REFRESH_TOKEN",
@@ -625,7 +627,6 @@ describe("LoginStrategyService", () => {
KdfIterations: PBKDF2KdfConfig.PRELOGIN_ITERATIONS_MIN - 1,
Key: "KEY",
PrivateKey: "PRIVATE_KEY",
- ResetMasterPassword: false,
access_token: "ACCESS_TOKEN",
expires_in: 3600,
refresh_token: "REFRESH_TOKEN",
@@ -689,7 +690,6 @@ describe("LoginStrategyService", () => {
KdfParallelism: 1,
Key: "KEY",
PrivateKey: "PRIVATE_KEY",
- ResetMasterPassword: false,
access_token: "ACCESS_TOKEN",
expires_in: 3600,
refresh_token: "REFRESH_TOKEN",
diff --git a/libs/auth/src/common/services/login-strategies/login-strategy.service.ts b/libs/auth/src/common/services/login-strategies/login-strategy.service.ts
index 5720fce254e..5f8cd304a18 100644
--- a/libs/auth/src/common/services/login-strategies/login-strategy.service.ts
+++ b/libs/auth/src/common/services/login-strategies/login-strategy.service.ts
@@ -19,6 +19,7 @@ import { TokenTwoFactorRequest } from "@bitwarden/common/auth/models/request/ide
import { TwoFactorService } from "@bitwarden/common/auth/two-factor";
import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service";
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
+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 { DeviceTrustServiceAbstraction } from "@bitwarden/common/key-management/device-trust/abstractions/device-trust.service.abstraction";
import { KeyConnectorService } from "@bitwarden/common/key-management/key-connector/abstractions/key-connector.service";
@@ -160,6 +161,7 @@ export class LoginStrategyService implements LoginStrategyServiceAbstraction {
protected kdfConfigService: KdfConfigService,
protected taskSchedulerService: TaskSchedulerService,
protected configService: ConfigService,
+ protected accountCryptographicStateService: AccountCryptographicStateService,
) {
this.currentAuthnTypeState = this.stateProvider.get(CURRENT_LOGIN_STRATEGY_KEY);
this.loginStrategyCacheState = this.stateProvider.get(CACHE_KEY);
@@ -509,6 +511,7 @@ export class LoginStrategyService implements LoginStrategyServiceAbstraction {
this.kdfConfigService,
this.environmentService,
this.configService,
+ this.accountCryptographicStateService,
];
return source.pipe(
diff --git a/libs/common/src/auth/models/domain/auth-result.ts b/libs/common/src/auth/models/domain/auth-result.ts
index 178866901d3..34467a18f2c 100644
--- a/libs/common/src/auth/models/domain/auth-result.ts
+++ b/libs/common/src/auth/models/domain/auth-result.ts
@@ -7,14 +7,6 @@ import { TwoFactorProviderType } from "../../enums/two-factor-provider-type";
export class AuthResult {
userId: UserId;
- // TODO: PM-3287 - Remove this after 3 releases of backwards compatibility. - Target release 2023.12 for removal
- /**
- * @deprecated
- * Replace with using UserDecryptionOptions to determine if the user does
- * not have a master password and is not using Key Connector.
- * */
- resetMasterPassword = false;
-
twoFactorProviders: Partial>> = null;
ssoEmail2FaSessionToken?: string;
email: string;
diff --git a/libs/common/src/auth/models/response/identity-token.response.spec.ts b/libs/common/src/auth/models/response/identity-token.response.spec.ts
index 4f8454968dc..6a32b89f1df 100644
--- a/libs/common/src/auth/models/response/identity-token.response.spec.ts
+++ b/libs/common/src/auth/models/response/identity-token.response.spec.ts
@@ -116,4 +116,36 @@ describe("IdentityTokenResponse", () => {
const identityTokenResponse = new IdentityTokenResponse(response);
expect(identityTokenResponse.userDecryptionOptions).toBeDefined();
});
+
+ it("should create response with accountKeys not present", () => {
+ const response = {
+ access_token: accessToken,
+ token_type: tokenType,
+ AccountKeys: null as unknown,
+ };
+
+ const identityTokenResponse = new IdentityTokenResponse(response);
+ expect(identityTokenResponse.accountKeysResponseModel).toBeNull();
+ });
+
+ it("should create response with accountKeys present", () => {
+ const accountKeysData = {
+ publicKeyEncryptionKeyPair: {
+ publicKey: "testPublicKey",
+ wrappedPrivateKey: "testPrivateKey",
+ },
+ };
+
+ const response = {
+ access_token: accessToken,
+ token_type: tokenType,
+ AccountKeys: accountKeysData,
+ };
+
+ const identityTokenResponse = new IdentityTokenResponse(response);
+ expect(identityTokenResponse.accountKeysResponseModel).toBeDefined();
+ expect(
+ identityTokenResponse.accountKeysResponseModel?.publicKeyEncryptionKeyPair,
+ ).toBeDefined();
+ });
});
diff --git a/libs/common/src/auth/models/response/identity-token.response.ts b/libs/common/src/auth/models/response/identity-token.response.ts
index 59e7eb98ec2..ae208ef1a36 100644
--- a/libs/common/src/auth/models/response/identity-token.response.ts
+++ b/libs/common/src/auth/models/response/identity-token.response.ts
@@ -5,6 +5,7 @@
import { Argon2KdfConfig, KdfConfig, KdfType, PBKDF2KdfConfig } from "@bitwarden/key-management";
import { EncString } from "../../../key-management/crypto/models/enc-string";
+import { PrivateKeysResponseModel } from "../../../key-management/keys/response/private-keys.response";
import { BaseResponse } from "../../../models/response/base.response";
import { MasterPasswordPolicyResponse } from "./master-password-policy.response";
@@ -18,8 +19,8 @@ export class IdentityTokenResponse extends BaseResponse {
tokenType: string;
// Decryption Information
- resetMasterPassword: boolean;
privateKey: string; // userKeyEncryptedPrivateKey
+ accountKeysResponseModel: PrivateKeysResponseModel | null = null;
key?: EncString; // masterKeyEncryptedUserKey
twoFactorToken: string;
kdfConfig: KdfConfig;
@@ -52,8 +53,12 @@ export class IdentityTokenResponse extends BaseResponse {
this.refreshToken = refreshToken;
}
- this.resetMasterPassword = this.getResponseProperty("ResetMasterPassword");
this.privateKey = this.getResponseProperty("PrivateKey");
+ if (this.getResponseProperty("AccountKeys") != null) {
+ this.accountKeysResponseModel = new PrivateKeysResponseModel(
+ this.getResponseProperty("AccountKeys"),
+ );
+ }
const key = this.getResponseProperty("Key");
if (key) {
this.key = new EncString(key);
diff --git a/libs/common/src/dirt/services/abstractions/phishing-detection-settings.service.abstraction.ts b/libs/common/src/dirt/services/abstractions/phishing-detection-settings.service.abstraction.ts
new file mode 100644
index 00000000000..6c915c2dcbe
--- /dev/null
+++ b/libs/common/src/dirt/services/abstractions/phishing-detection-settings.service.abstraction.ts
@@ -0,0 +1,37 @@
+import { Observable } from "rxjs";
+
+import { UserId } from "@bitwarden/user-core";
+
+/**
+ * Abstraction for phishing detection settings
+ */
+export abstract class PhishingDetectionSettingsServiceAbstraction {
+ /**
+ * An observable for whether phishing detection is available for the active user account.
+ *
+ * Access is granted only when the PhishingDetection feature flag is enabled and
+ * at least one of the following is true for the active account:
+ * - the user has a personal premium subscription
+ * - the user is a member of a Family org (ProductTierType.Families)
+ * - the user is a member of an Enterprise org with `usePhishingBlocker` enabled
+ *
+ * Note: Non-specified organization types (e.g., Team orgs) do not grant access.
+ */
+ abstract readonly available$: Observable;
+ /**
+ * An observable for whether phishing detection is on for the active user account
+ *
+ * This is true when {@link available$} is true and when {@link enabled$} is true
+ */
+ abstract readonly on$: Observable;
+ /**
+ * An observable for whether phishing detection is enabled
+ */
+ abstract readonly enabled$: Observable;
+ /**
+ * Sets whether phishing detection is enabled
+ *
+ * @param enabled True to enable, false to disable
+ */
+ abstract setEnabled: (userId: UserId, enabled: boolean) => Promise;
+}
diff --git a/libs/common/src/dirt/services/phishing-detection/phishing-detection-settings.service.spec.ts b/libs/common/src/dirt/services/phishing-detection/phishing-detection-settings.service.spec.ts
new file mode 100644
index 00000000000..23e311d9445
--- /dev/null
+++ b/libs/common/src/dirt/services/phishing-detection/phishing-detection-settings.service.spec.ts
@@ -0,0 +1,203 @@
+import { mock, MockProxy } from "jest-mock-extended";
+import { BehaviorSubject, firstValueFrom, Subject } from "rxjs";
+
+import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
+import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
+import { Account, AccountService } from "@bitwarden/common/auth/abstractions/account.service";
+import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions";
+import { ProductTierType } from "@bitwarden/common/billing/enums";
+import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
+
+import { FakeAccountService, FakeStateProvider, mockAccountServiceWith } from "../../../../spec";
+import { UserId } from "../../../types/guid";
+
+import { PhishingDetectionSettingsService } from "./phishing-detection-settings.service";
+
+describe("PhishingDetectionSettingsService", () => {
+ // Mock services
+ let mockAccountService: MockProxy;
+ let mockBillingService: MockProxy;
+ let mockConfigService: MockProxy;
+ let mockOrganizationService: MockProxy;
+
+ // RxJS Subjects we control in the tests
+ let activeAccountSubject: BehaviorSubject;
+ let featureFlagSubject: BehaviorSubject;
+ let premiumStatusSubject: BehaviorSubject;
+ let organizationsSubject: BehaviorSubject;
+
+ let service: PhishingDetectionSettingsService;
+ let stateProvider: FakeStateProvider;
+
+ // Constant mock data
+ const familyOrg = mock({
+ canAccess: true,
+ isMember: true,
+ usersGetPremium: true,
+ productTierType: ProductTierType.Families,
+ usePhishingBlocker: true,
+ });
+ const teamOrg = mock