From 44d50a70c25249a82b6db40341b9f3a4f6965135 Mon Sep 17 00:00:00 2001
From: Jared Snider <116684653+JaredSnider-Bitwarden@users.noreply.github.com>
Date: Tue, 25 Feb 2025 17:58:26 -0500
Subject: [PATCH 01/47] Auth/PM-5712 - Extension & Desktop Account Switcher -
Fix incorrect env showing when adding new accounts (#13362)
* PM-5712 - Refactor env service to require user id instead of having global and active user state fallbacks per working session with Justin.
* PM-5712 - AccountSwitcherService tests - fix tests and add env assertions.
---
.../services/account-switcher.service.spec.ts | 19 +++-
.../services/account-switcher.service.ts | 3 +-
.../app/layout/account-switcher.component.ts | 8 +-
.../abstractions/environment.service.ts | 2 +-
.../default-environment.service.spec.ts | 104 ++++++------------
.../services/default-environment.service.ts | 20 +---
6 files changed, 65 insertions(+), 91 deletions(-)
diff --git a/apps/browser/src/auth/popup/account-switching/services/account-switcher.service.spec.ts b/apps/browser/src/auth/popup/account-switching/services/account-switcher.service.spec.ts
index 3084c3e5407..1fdd0b1ecf2 100644
--- a/apps/browser/src/auth/popup/account-switching/services/account-switcher.service.spec.ts
+++ b/apps/browser/src/auth/popup/account-switching/services/account-switcher.service.spec.ts
@@ -9,7 +9,10 @@ import {
import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service";
import { AvatarService } from "@bitwarden/common/auth/abstractions/avatar.service";
import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status";
-import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service";
+import {
+ Environment,
+ EnvironmentService,
+} from "@bitwarden/common/platform/abstractions/environment.service";
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
import { UserId } from "@bitwarden/common/types/guid";
@@ -21,6 +24,12 @@ describe("AccountSwitcherService", () => {
let activeAccountSubject: BehaviorSubject;
let authStatusSubject: ReplaySubject>;
+ let envBSubject: BehaviorSubject;
+ const mockHostName = "mockHostName";
+ const mockEnv: Partial = {
+ getHostname: () => mockHostName,
+ };
+
const accountService = mock();
const avatarService = mock();
const messagingService = mock();
@@ -41,6 +50,9 @@ describe("AccountSwitcherService", () => {
accountService.activeAccount$ = activeAccountSubject;
authService.authStatuses$ = authStatusSubject;
+ envBSubject = new BehaviorSubject(mockEnv as Environment);
+ environmentService.getEnvironment$.mockReturnValue(envBSubject);
+
accountSwitcherService = new AccountSwitcherService(
accountService,
avatarService,
@@ -79,11 +91,16 @@ describe("AccountSwitcherService", () => {
expect(accounts).toHaveLength(3);
expect(accounts[0].id).toBe("1");
expect(accounts[0].isActive).toBeTruthy();
+
+ expect(accounts[0].server).toBe(mockHostName);
+
expect(accounts[1].id).toBe("2");
expect(accounts[1].isActive).toBeFalsy();
+ expect(accounts[1].server).toBe(mockHostName);
expect(accounts[2].id).toBe("addAccount");
expect(accounts[2].isActive).toBeFalsy();
+ expect(accounts[2].server).toBe(undefined);
});
it.each([5, 6])(
diff --git a/apps/browser/src/auth/popup/account-switching/services/account-switcher.service.ts b/apps/browser/src/auth/popup/account-switching/services/account-switcher.service.ts
index 535df3ec6bb..bfed7dc1408 100644
--- a/apps/browser/src/auth/popup/account-switching/services/account-switcher.service.ts
+++ b/apps/browser/src/auth/popup/account-switching/services/account-switcher.service.ts
@@ -66,11 +66,12 @@ export class AccountSwitcherService {
const hasMaxAccounts = loggedInIds.length >= this.ACCOUNT_LIMIT;
const options: AvailableAccount[] = await Promise.all(
loggedInIds.map(async (id: UserId) => {
+ const userEnv = await firstValueFrom(this.environmentService.getEnvironment$(id));
return {
name: accounts[id].name ?? accounts[id].email,
email: accounts[id].email,
id: id,
- server: (await this.environmentService.getEnvironment(id))?.getHostname(),
+ server: userEnv?.getHostname(),
status: accountStatuses[id],
isActive: id === activeAccount?.id,
avatarColor: await firstValueFrom(
diff --git a/apps/desktop/src/app/layout/account-switcher.component.ts b/apps/desktop/src/app/layout/account-switcher.component.ts
index db8c2a85bde..d8ffa5ae546 100644
--- a/apps/desktop/src/app/layout/account-switcher.component.ts
+++ b/apps/desktop/src/app/layout/account-switcher.component.ts
@@ -110,7 +110,9 @@ export class AccountSwitcherComponent implements OnInit {
name: active.name,
email: active.email,
avatarColor: await firstValueFrom(this.avatarService.avatarColor$),
- server: (await this.environmentService.getEnvironment())?.getHostname(),
+ server: (
+ await firstValueFrom(this.environmentService.getEnvironment$(active.id))
+ )?.getHostname(),
};
}),
);
@@ -221,7 +223,9 @@ export class AccountSwitcherComponent implements OnInit {
email: baseAccounts[userId].email,
authenticationStatus: await this.authService.getAuthStatus(userId),
avatarColor: await firstValueFrom(this.avatarService.getUserAvatarColor$(userId as UserId)),
- server: (await this.environmentService.getEnvironment(userId))?.getHostname(),
+ server: (
+ await firstValueFrom(this.environmentService.getEnvironment$(userId as UserId))
+ )?.getHostname(),
};
}
diff --git a/libs/common/src/platform/abstractions/environment.service.ts b/libs/common/src/platform/abstractions/environment.service.ts
index 8d32fc4231d..4a10f856893 100644
--- a/libs/common/src/platform/abstractions/environment.service.ts
+++ b/libs/common/src/platform/abstractions/environment.service.ts
@@ -128,7 +128,7 @@ export abstract class EnvironmentService {
/**
* Get the environment from state. Useful if you need to get the environment for another user.
*/
- abstract getEnvironment$(userId?: string): Observable;
+ abstract getEnvironment$(userId: UserId): Observable;
/**
* @deprecated Use {@link getEnvironment$} instead.
diff --git a/libs/common/src/platform/services/default-environment.service.spec.ts b/libs/common/src/platform/services/default-environment.service.spec.ts
index 870f887c160..553f80f83b8 100644
--- a/libs/common/src/platform/services/default-environment.service.spec.ts
+++ b/libs/common/src/platform/services/default-environment.service.spec.ts
@@ -304,85 +304,21 @@ describe("EnvironmentService", () => {
});
});
- describe("getEnvironment", () => {
+ describe("getEnvironment$", () => {
it.each([
{ region: Region.US, expectedHost: "bitwarden.com" },
{ region: Region.EU, expectedHost: "bitwarden.eu" },
- ])("gets it from user data if there is an active user", async ({ region, expectedHost }) => {
- setGlobalData(Region.US, new EnvironmentUrls());
- setUserData(region, new EnvironmentUrls());
+ ])("gets it from the passed in userId: %s", async ({ region, expectedHost }) => {
+ setUserData(Region.US, new EnvironmentUrls());
+ setUserData(region, new EnvironmentUrls(), alternateTestUser);
await switchUser(testUser);
- const env = await firstValueFrom(sut.getEnvironment$());
- expect(env.getHostname()).toBe(expectedHost);
+ const env = await firstValueFrom(sut.getEnvironment$(alternateTestUser));
+ expect(env?.getHostname()).toBe(expectedHost);
});
- it.each([
- { region: Region.US, expectedHost: "bitwarden.com" },
- { region: Region.EU, expectedHost: "bitwarden.eu" },
- ])("gets it from global data if there is no active user", async ({ region, expectedHost }) => {
- setGlobalData(region, new EnvironmentUrls());
- setUserData(Region.US, new EnvironmentUrls());
-
- const env = await firstValueFrom(sut.getEnvironment$());
- expect(env.getHostname()).toBe(expectedHost);
- });
-
- it.each([
- { region: Region.US, expectedHost: "bitwarden.com" },
- { region: Region.EU, expectedHost: "bitwarden.eu" },
- ])(
- "gets it from global state if there is no active user even if a user id is passed in.",
- async ({ region, expectedHost }) => {
- setGlobalData(region, new EnvironmentUrls());
- setUserData(Region.US, new EnvironmentUrls());
-
- const env = await firstValueFrom(sut.getEnvironment$(testUser));
- expect(env.getHostname()).toBe(expectedHost);
- },
- );
-
- it.each([
- { region: Region.US, expectedHost: "bitwarden.com" },
- { region: Region.EU, expectedHost: "bitwarden.eu" },
- ])(
- "gets it from the passed in userId if there is any active user: %s",
- async ({ region, expectedHost }) => {
- setGlobalData(Region.US, new EnvironmentUrls());
- setUserData(Region.US, new EnvironmentUrls());
- setUserData(region, new EnvironmentUrls(), alternateTestUser);
-
- await switchUser(testUser);
-
- const env = await firstValueFrom(sut.getEnvironment$(alternateTestUser));
- expect(env.getHostname()).toBe(expectedHost);
- },
- );
-
- it("gets it from base url saved in self host config", async () => {
- const globalSelfHostUrls = new EnvironmentUrls();
- globalSelfHostUrls.base = "https://base.example.com";
- setGlobalData(Region.SelfHosted, globalSelfHostUrls);
- setUserData(Region.EU, new EnvironmentUrls());
-
- const env = await firstValueFrom(sut.getEnvironment$());
- expect(env.getHostname()).toBe("base.example.com");
- });
-
- it("gets it from webVault url saved in self host config", async () => {
- const globalSelfHostUrls = new EnvironmentUrls();
- globalSelfHostUrls.webVault = "https://vault.example.com";
- globalSelfHostUrls.base = "https://base.example.com";
- setGlobalData(Region.SelfHosted, globalSelfHostUrls);
- setUserData(Region.EU, new EnvironmentUrls());
-
- const env = await firstValueFrom(sut.getEnvironment$());
- expect(env.getHostname()).toBe("vault.example.com");
- });
-
- it("gets it from saved self host config from passed in user when there is an active user", async () => {
- setGlobalData(Region.US, new EnvironmentUrls());
+ it("gets env from saved self host config from passed in user when there is a different active user", async () => {
setUserData(Region.EU, new EnvironmentUrls());
const selfHostUserUrls = new EnvironmentUrls();
@@ -392,7 +328,31 @@ describe("EnvironmentService", () => {
await switchUser(testUser);
const env = await firstValueFrom(sut.getEnvironment$(alternateTestUser));
- expect(env.getHostname()).toBe("base.example.com");
+ expect(env?.getHostname()).toBe("base.example.com");
+ });
+ });
+
+ describe("getEnvironment (deprecated)", () => {
+ it("gets self hosted env from active user when no user passed in", async () => {
+ const selfHostUserUrls = new EnvironmentUrls();
+ selfHostUserUrls.base = "https://base.example.com";
+ setUserData(Region.SelfHosted, selfHostUserUrls);
+
+ await switchUser(testUser);
+
+ const env = await sut.getEnvironment();
+ expect(env?.getHostname()).toBe("base.example.com");
+ });
+
+ it("gets self hosted env from passed in user", async () => {
+ const selfHostUserUrls = new EnvironmentUrls();
+ selfHostUserUrls.base = "https://base.example.com";
+ setUserData(Region.SelfHosted, selfHostUserUrls);
+
+ await switchUser(testUser);
+
+ const env = await sut.getEnvironment(testUser);
+ expect(env?.getHostname()).toBe("base.example.com");
});
});
diff --git a/libs/common/src/platform/services/default-environment.service.ts b/libs/common/src/platform/services/default-environment.service.ts
index ac3e39b2bb3..df55693ba0b 100644
--- a/libs/common/src/platform/services/default-environment.service.ts
+++ b/libs/common/src/platform/services/default-environment.service.ts
@@ -271,19 +271,8 @@ export class DefaultEnvironmentService implements EnvironmentService {
}
}
- getEnvironment$(userId?: UserId): Observable {
- if (userId == null) {
- return this.environment$;
- }
-
- return this.activeAccountId$.pipe(
- switchMap((activeUserId) => {
- // Previous rules dictated that we only get from user scoped state if there is an active user.
- if (activeUserId == null) {
- return this.globalState.state$;
- }
- return this.stateProvider.getUser(userId ?? activeUserId, USER_ENVIRONMENT_KEY).state$;
- }),
+ getEnvironment$(userId: UserId): Observable {
+ return this.stateProvider.getUser(userId, USER_ENVIRONMENT_KEY).state$.pipe(
map((state) => {
return this.buildEnvironment(state?.region, state?.urls);
}),
@@ -294,7 +283,10 @@ export class DefaultEnvironmentService implements EnvironmentService {
* @deprecated Use getEnvironment$ instead.
*/
async getEnvironment(userId?: UserId): Promise {
- return firstValueFrom(this.getEnvironment$(userId));
+ // Add backwards compatibility support for null userId
+ const definedUserId = userId ?? (await firstValueFrom(this.activeAccountId$));
+
+ return firstValueFrom(this.getEnvironment$(definedUserId));
}
async seedUserEnvironment(userId: UserId) {
From ce5a5e36498eddcf3e10222278a7f8b372a1fed2 Mon Sep 17 00:00:00 2001
From: Andreas Coroiu
Date: Wed, 26 Feb 2025 09:08:42 +0100
Subject: [PATCH 02/47] Improve SDK direct function usage (#13353)
* feat: initalize WASM/SDK directly after load
* fix: default sdk service trying to set log level
* feat: wait for sdk to load in sdk service
* fix: add required disposable polyfills
* feat: update sdk version
* feat: replace rc-specific workaround with global polyfill
* fix: sdk service tests
---
.../browser/src/background/main.background.ts | 4 +-
.../services/sdk/browser-sdk-load.service.ts | 6 ++-
apps/browser/src/popup/polyfills.ts | 1 +
.../src/popup/services/init.service.ts | 2 +-
apps/cli/src/bw.ts | 2 +
.../platform/services/cli-sdk-load.service.ts | 2 +-
.../service-container/service-container.ts | 2 +-
apps/desktop/src/app/main.ts | 2 +
apps/desktop/src/app/services/init.service.ts | 2 +-
apps/desktop/src/main.ts | 2 +
apps/web/src/app/core/init.service.ts | 2 +-
.../src/app/platform/web-sdk-load.service.ts | 2 +-
apps/web/src/polyfills.ts | 1 +
.../abstractions/sdk/sdk-load.service.ts | 53 ++++++++++++++++++-
.../misc/reference-counting/rc.spec.ts | 8 ---
.../services/sdk/default-sdk-load.service.ts | 2 +-
.../services/sdk/default-sdk.service.spec.ts | 12 ++++-
.../services/sdk/default-sdk.service.ts | 8 +--
.../services/sdk/noop-sdk-load.service.ts | 2 +-
libs/common/test.setup.ts | 2 +
package-lock.json | 8 +--
package.json | 2 +-
tsconfig.json | 2 +-
23 files changed, 98 insertions(+), 31 deletions(-)
diff --git a/apps/browser/src/background/main.background.ts b/apps/browser/src/background/main.background.ts
index bb9ec41cc7d..914938ba130 100644
--- a/apps/browser/src/background/main.background.ts
+++ b/apps/browser/src/background/main.background.ts
@@ -1,5 +1,7 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
+import "core-js/proposals/explicit-resource-management";
+
import { filter, firstValueFrom, map, merge, Subject, timeout } from "rxjs";
import { CollectionService, DefaultCollectionService } from "@bitwarden/admin-console/common";
@@ -1290,7 +1292,7 @@ export default class MainBackground {
}
this.containerService.attachToGlobal(self);
- await this.sdkLoadService.load();
+ await this.sdkLoadService.loadAndInit();
// Only the "true" background should run migrations
await this.stateService.init({ runMigrations: true });
diff --git a/apps/browser/src/platform/services/sdk/browser-sdk-load.service.ts b/apps/browser/src/platform/services/sdk/browser-sdk-load.service.ts
index ca41127407c..409ff0dea06 100644
--- a/apps/browser/src/platform/services/sdk/browser-sdk-load.service.ts
+++ b/apps/browser/src/platform/services/sdk/browser-sdk-load.service.ts
@@ -60,8 +60,10 @@ async function importModule(): Promise {
return (globalThis as GlobalWithWasmInit).initSdk;
}
-export class BrowserSdkLoadService implements SdkLoadService {
- constructor(readonly logService: LogService) {}
+export class BrowserSdkLoadService extends SdkLoadService {
+ constructor(readonly logService: LogService) {
+ super();
+ }
async load(): Promise {
const startTime = performance.now();
diff --git a/apps/browser/src/popup/polyfills.ts b/apps/browser/src/popup/polyfills.ts
index f76b9e632fc..4bb2aa0bbee 100644
--- a/apps/browser/src/popup/polyfills.ts
+++ b/apps/browser/src/popup/polyfills.ts
@@ -1,2 +1,3 @@
import "core-js/stable";
+import "core-js/proposals/explicit-resource-management";
import "zone.js";
diff --git a/apps/browser/src/popup/services/init.service.ts b/apps/browser/src/popup/services/init.service.ts
index 2ca25d690f1..fe6fba85a4b 100644
--- a/apps/browser/src/popup/services/init.service.ts
+++ b/apps/browser/src/popup/services/init.service.ts
@@ -32,7 +32,7 @@ export class InitService {
init() {
return async () => {
- await this.sdkLoadService.load();
+ await this.sdkLoadService.loadAndInit();
await this.stateService.init({ runMigrations: false }); // Browser background is responsible for migrations
await this.i18nService.init();
this.twoFactorService.init();
diff --git a/apps/cli/src/bw.ts b/apps/cli/src/bw.ts
index 5e9d3dfbc94..fcf0ef3dc8d 100644
--- a/apps/cli/src/bw.ts
+++ b/apps/cli/src/bw.ts
@@ -1,3 +1,5 @@
+import "core-js/proposals/explicit-resource-management";
+
import { program } from "commander";
import { OssServeConfigurator } from "./oss-serve-configurator";
diff --git a/apps/cli/src/platform/services/cli-sdk-load.service.ts b/apps/cli/src/platform/services/cli-sdk-load.service.ts
index ee3b48e34d7..638e64a8214 100644
--- a/apps/cli/src/platform/services/cli-sdk-load.service.ts
+++ b/apps/cli/src/platform/services/cli-sdk-load.service.ts
@@ -1,7 +1,7 @@
import { SdkLoadService } from "@bitwarden/common/platform/abstractions/sdk/sdk-load.service";
import * as sdk from "@bitwarden/sdk-internal";
-export class CliSdkLoadService implements SdkLoadService {
+export class CliSdkLoadService extends SdkLoadService {
async load(): Promise {
const module = await import("@bitwarden/sdk-internal/bitwarden_wasm_internal_bg.wasm");
(sdk as any).init(module);
diff --git a/apps/cli/src/service-container/service-container.ts b/apps/cli/src/service-container/service-container.ts
index 98926f7ae65..fcf18fd508f 100644
--- a/apps/cli/src/service-container/service-container.ts
+++ b/apps/cli/src/service-container/service-container.ts
@@ -867,7 +867,7 @@ export class ServiceContainer {
return;
}
- await this.sdkLoadService.load();
+ await this.sdkLoadService.loadAndInit();
await this.storageService.init();
await this.stateService.init();
this.containerService.attachToGlobal(global);
diff --git a/apps/desktop/src/app/main.ts b/apps/desktop/src/app/main.ts
index ba964177dbc..16d03aefbdd 100644
--- a/apps/desktop/src/app/main.ts
+++ b/apps/desktop/src/app/main.ts
@@ -1,3 +1,5 @@
+import "core-js/proposals/explicit-resource-management";
+
import { enableProdMode } from "@angular/core";
import { platformBrowserDynamic } from "@angular/platform-browser-dynamic";
diff --git a/apps/desktop/src/app/services/init.service.ts b/apps/desktop/src/app/services/init.service.ts
index 6a58f36cfb9..5e00e406360 100644
--- a/apps/desktop/src/app/services/init.service.ts
+++ b/apps/desktop/src/app/services/init.service.ts
@@ -54,7 +54,7 @@ export class InitService {
init() {
return async () => {
- await this.sdkLoadService.load();
+ await this.sdkLoadService.loadAndInit();
await this.sshAgentService.init();
this.nativeMessagingService.init();
await this.stateService.init({ runMigrations: false }); // Desktop will run them in main process
diff --git a/apps/desktop/src/main.ts b/apps/desktop/src/main.ts
index c6e074ead91..7e417e8e5a8 100644
--- a/apps/desktop/src/main.ts
+++ b/apps/desktop/src/main.ts
@@ -1,5 +1,7 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
+import "core-js/proposals/explicit-resource-management";
+
import * as path from "path";
import { app } from "electron";
diff --git a/apps/web/src/app/core/init.service.ts b/apps/web/src/app/core/init.service.ts
index 3623d9b0d2f..307eed4c1e4 100644
--- a/apps/web/src/app/core/init.service.ts
+++ b/apps/web/src/app/core/init.service.ts
@@ -42,7 +42,7 @@ export class InitService {
init() {
return async () => {
- await this.sdkLoadService.load();
+ await this.sdkLoadService.loadAndInit();
await this.stateService.init();
const activeAccount = await firstValueFrom(this.accountService.activeAccount$);
diff --git a/apps/web/src/app/platform/web-sdk-load.service.ts b/apps/web/src/app/platform/web-sdk-load.service.ts
index cae3399b81e..8be3d20b0a7 100644
--- a/apps/web/src/app/platform/web-sdk-load.service.ts
+++ b/apps/web/src/app/platform/web-sdk-load.service.ts
@@ -18,7 +18,7 @@ const supported = (() => {
return false;
})();
-export class WebSdkLoadService implements SdkLoadService {
+export class WebSdkLoadService extends SdkLoadService {
async load(): Promise {
let module: any;
if (supported) {
diff --git a/apps/web/src/polyfills.ts b/apps/web/src/polyfills.ts
index 33af553f786..3971ed3207f 100644
--- a/apps/web/src/polyfills.ts
+++ b/apps/web/src/polyfills.ts
@@ -1,4 +1,5 @@
import "core-js/stable";
+import "core-js/proposals/explicit-resource-management";
import "zone.js";
if (process.env.NODE_ENV === "production") {
diff --git a/libs/common/src/platform/abstractions/sdk/sdk-load.service.ts b/libs/common/src/platform/abstractions/sdk/sdk-load.service.ts
index 16482e797b2..fb443d61777 100644
--- a/libs/common/src/platform/abstractions/sdk/sdk-load.service.ts
+++ b/libs/common/src/platform/abstractions/sdk/sdk-load.service.ts
@@ -1,3 +1,52 @@
-export abstract class SdkLoadService {
- abstract load(): Promise;
+import { init_sdk } from "@bitwarden/sdk-internal";
+
+// eslint-disable-next-line @typescript-eslint/no-unused-vars -- used in docs
+import type { SdkService } from "./sdk.service";
+
+export class SdkLoadFailedError extends Error {
+ constructor(error: unknown) {
+ super(`SDK loading failed: ${error}`);
+ }
+}
+
+export abstract class SdkLoadService {
+ private static markAsReady: () => void;
+ private static markAsFailed: (error: unknown) => void;
+
+ /**
+ * This promise is resolved when the SDK is ready to be used. Use it when your code might run early and/or is not able to use DI.
+ * Beware that WASM always requires a load step which makes it tricky to use functions and classes directly, it is therefore recommended
+ * to use the SDK through the {@link SdkService}. Only use this promise in advanced scenarios!
+ *
+ * @example
+ * ```typescript
+ * import { pureFunction } from "@bitwarden/sdk-internal";
+ *
+ * async function myFunction() {
+ * await SdkLoadService.Ready;
+ * pureFunction();
+ * }
+ * ```
+ */
+ static readonly Ready = new Promise((resolve, reject) => {
+ SdkLoadService.markAsReady = resolve;
+ SdkLoadService.markAsFailed = (error: unknown) => reject(new SdkLoadFailedError(error));
+ });
+
+ /**
+ * Load WASM and initalize SDK-JS integrations such as logging.
+ * This method should be called once at the start of the application.
+ * Raw functions and classes from the SDK can be used after this method resolves.
+ */
+ async loadAndInit(): Promise {
+ try {
+ await this.load();
+ init_sdk();
+ SdkLoadService.markAsReady();
+ } catch (error) {
+ SdkLoadService.markAsFailed(error);
+ }
+ }
+
+ protected abstract load(): Promise;
}
diff --git a/libs/common/src/platform/misc/reference-counting/rc.spec.ts b/libs/common/src/platform/misc/reference-counting/rc.spec.ts
index 094abfe3615..f8767242ba5 100644
--- a/libs/common/src/platform/misc/reference-counting/rc.spec.ts
+++ b/libs/common/src/platform/misc/reference-counting/rc.spec.ts
@@ -1,11 +1,3 @@
-// Temporary workaround for Symbol.dispose
-// remove when https://github.com/jestjs/jest/issues/14874 is resolved and *released*
-const disposeSymbol: unique symbol = Symbol("Symbol.dispose");
-const asyncDisposeSymbol: unique symbol = Symbol("Symbol.asyncDispose");
-(Symbol as any).asyncDispose ??= asyncDisposeSymbol as unknown as SymbolConstructor["asyncDispose"];
-(Symbol as any).dispose ??= disposeSymbol as unknown as SymbolConstructor["dispose"];
-
-// Import needs to be after the workaround
import { Rc } from "./rc";
export class FreeableTestValue {
diff --git a/libs/common/src/platform/services/sdk/default-sdk-load.service.ts b/libs/common/src/platform/services/sdk/default-sdk-load.service.ts
index eff641f0351..0c4114b8796 100644
--- a/libs/common/src/platform/services/sdk/default-sdk-load.service.ts
+++ b/libs/common/src/platform/services/sdk/default-sdk-load.service.ts
@@ -8,7 +8,7 @@ import { SdkLoadService } from "../../abstractions/sdk/sdk-load.service";
*
* **Warning**: This requires WASM support and will fail if the environment does not support it.
*/
-export class DefaultSdkLoadService implements SdkLoadService {
+export class DefaultSdkLoadService extends SdkLoadService {
async load(): Promise {
(sdk as any).init(bitwardenModule);
}
diff --git a/libs/common/src/platform/services/sdk/default-sdk.service.spec.ts b/libs/common/src/platform/services/sdk/default-sdk.service.spec.ts
index fed4746acd3..a66b2a9cb6f 100644
--- a/libs/common/src/platform/services/sdk/default-sdk.service.spec.ts
+++ b/libs/common/src/platform/services/sdk/default-sdk.service.spec.ts
@@ -11,6 +11,7 @@ import { UserKey } from "../../../types/key";
import { Environment, EnvironmentService } from "../../abstractions/environment.service";
import { PlatformUtilsService } from "../../abstractions/platform-utils.service";
import { SdkClientFactory } from "../../abstractions/sdk/sdk-client-factory";
+import { SdkLoadService } from "../../abstractions/sdk/sdk-load.service";
import { UserNotLoggedInError } from "../../abstractions/sdk/sdk.service";
import { Rc } from "../../misc/reference-counting/rc";
import { EncryptedString } from "../../models/domain/enc-string";
@@ -18,6 +19,13 @@ import { SymmetricCryptoKey } from "../../models/domain/symmetric-crypto-key";
import { DefaultSdkService } from "./default-sdk.service";
+class TestSdkLoadService extends SdkLoadService {
+ protected override load(): Promise {
+ // Simulate successfull WASM load
+ return Promise.resolve();
+ }
+}
+
describe("DefaultSdkService", () => {
describe("userClient$", () => {
let sdkClientFactory!: MockProxy;
@@ -28,7 +36,9 @@ describe("DefaultSdkService", () => {
let keyService!: MockProxy;
let service!: DefaultSdkService;
- beforeEach(() => {
+ beforeEach(async () => {
+ await new TestSdkLoadService().loadAndInit();
+
sdkClientFactory = mock();
environmentService = mock();
platformUtilsService = mock();
diff --git a/libs/common/src/platform/services/sdk/default-sdk.service.ts b/libs/common/src/platform/services/sdk/default-sdk.service.ts
index 96a1dedf175..5c381c7dd1b 100644
--- a/libs/common/src/platform/services/sdk/default-sdk.service.ts
+++ b/libs/common/src/platform/services/sdk/default-sdk.service.ts
@@ -18,7 +18,6 @@ import { KeyService, KdfConfigService, KdfConfig, KdfType } from "@bitwarden/key
import {
BitwardenClient,
ClientSettings,
- LogLevel,
DeviceType as SdkDeviceType,
} from "@bitwarden/sdk-internal";
@@ -30,6 +29,7 @@ import { UserKey } from "../../../types/key";
import { Environment, EnvironmentService } from "../../abstractions/environment.service";
import { PlatformUtilsService } from "../../abstractions/platform-utils.service";
import { SdkClientFactory } from "../../abstractions/sdk/sdk-client-factory";
+import { SdkLoadService } from "../../abstractions/sdk/sdk-load.service";
import { SdkService, UserNotLoggedInError } from "../../abstractions/sdk/sdk.service";
import { compareValues } from "../../misc/compare-values";
import { Rc } from "../../misc/reference-counting/rc";
@@ -47,8 +47,9 @@ export class DefaultSdkService implements SdkService {
client$ = this.environmentService.environment$.pipe(
concatMap(async (env) => {
+ await SdkLoadService.Ready;
const settings = this.toSettings(env);
- return await this.sdkClientFactory.createSdkClient(settings, LogLevel.Info);
+ return await this.sdkClientFactory.createSdkClient(settings);
}),
shareReplay({ refCount: true, bufferSize: 1 }),
);
@@ -135,6 +136,7 @@ export class DefaultSdkService implements SdkService {
privateKey$,
userKey$,
orgKeys$,
+ SdkLoadService.Ready, // Makes sure we wait (once) for the SDK to be loaded
]).pipe(
// switchMap is required to allow the clean-up logic to be executed when `combineLatest` emits a new value.
switchMap(([env, account, kdfParams, privateKey, userKey, orgKeys]) => {
@@ -146,7 +148,7 @@ export class DefaultSdkService implements SdkService {
}
const settings = this.toSettings(env);
- const client = await this.sdkClientFactory.createSdkClient(settings, LogLevel.Info);
+ const client = await this.sdkClientFactory.createSdkClient(settings);
await this.initializeClient(client, account, kdfParams, privateKey, userKey, orgKeys);
diff --git a/libs/common/src/platform/services/sdk/noop-sdk-load.service.ts b/libs/common/src/platform/services/sdk/noop-sdk-load.service.ts
index 60dac4f21f1..9fd04fdf833 100644
--- a/libs/common/src/platform/services/sdk/noop-sdk-load.service.ts
+++ b/libs/common/src/platform/services/sdk/noop-sdk-load.service.ts
@@ -2,6 +2,6 @@ import { SdkLoadService } from "../../abstractions/sdk/sdk-load.service";
export class NoopSdkLoadService extends SdkLoadService {
async load() {
- return;
+ throw new Error("SDK not available in this environment");
}
}
diff --git a/libs/common/test.setup.ts b/libs/common/test.setup.ts
index aa71b3e508a..9087c15c6b6 100644
--- a/libs/common/test.setup.ts
+++ b/libs/common/test.setup.ts
@@ -1,3 +1,5 @@
+import "core-js/proposals/explicit-resource-management";
+
import { webcrypto } from "crypto";
import { addCustomMatchers } from "./spec";
diff --git a/package-lock.json b/package-lock.json
index 1431f31daac..ab526f2730b 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -24,7 +24,7 @@
"@angular/platform-browser": "18.2.13",
"@angular/platform-browser-dynamic": "18.2.13",
"@angular/router": "18.2.13",
- "@bitwarden/sdk-internal": "0.2.0-main.105",
+ "@bitwarden/sdk-internal": "0.2.0-main.107",
"@electron/fuses": "1.8.0",
"@emotion/css": "11.13.5",
"@koa/multer": "3.0.2",
@@ -4651,9 +4651,9 @@
"link": true
},
"node_modules/@bitwarden/sdk-internal": {
- "version": "0.2.0-main.105",
- "resolved": "https://registry.npmjs.org/@bitwarden/sdk-internal/-/sdk-internal-0.2.0-main.105.tgz",
- "integrity": "sha512-MaQFJbuKTCbN9oZC/+opYVeegaNNJpiUv9/zx+gu8KxWmX0hyEkNPtHKxBjDt3kLLz69CudDtUxEgqOfcDsYAw==",
+ "version": "0.2.0-main.107",
+ "resolved": "https://registry.npmjs.org/@bitwarden/sdk-internal/-/sdk-internal-0.2.0-main.107.tgz",
+ "integrity": "sha512-xpOF6NAS0/em3jFBv4FI1ASy1Nuc7I1v41TVmG56wS+80y+NH1RnfGjp+a+XiO7Xxh3jssrxmjzihJjWQQA0rg==",
"license": "GPL-3.0"
},
"node_modules/@bitwarden/send-ui": {
diff --git a/package.json b/package.json
index 54b1f642086..cb941238fc2 100644
--- a/package.json
+++ b/package.json
@@ -154,7 +154,7 @@
"@angular/platform-browser": "18.2.13",
"@angular/platform-browser-dynamic": "18.2.13",
"@angular/router": "18.2.13",
- "@bitwarden/sdk-internal": "0.2.0-main.105",
+ "@bitwarden/sdk-internal": "0.2.0-main.107",
"@electron/fuses": "1.8.0",
"@emotion/css": "11.13.5",
"@koa/multer": "3.0.2",
diff --git a/tsconfig.json b/tsconfig.json
index 37f7aac05d8..fb50f1e7033 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -6,7 +6,7 @@
"noImplicitAny": true,
"target": "ES2016",
"module": "ES2020",
- "lib": ["es5", "es6", "es7", "dom", "ES2021"],
+ "lib": ["es5", "es6", "es7", "dom", "ES2021", "ESNext.Disposable"],
"sourceMap": true,
"allowSyntheticDefaultImports": true,
"experimentalDecorators": true,
From cb028eadb5fb17263a4a1c0c0ffda6b23caeb0b5 Mon Sep 17 00:00:00 2001
From: Bernd Schoolmann
Date: Wed, 26 Feb 2025 12:12:27 +0100
Subject: [PATCH 03/47] [PM-15934] Add agent-forwarding detection and git
signing detection parsers (#12371)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
* Add agent-forwarding detection and git signing detection parsers
* Cleanup
* Pin russh version
* Run cargo fmt
* Fix build
* Update apps/desktop/desktop_native/core/src/ssh_agent/mod.rs
Co-authored-by: Daniel García
* Pass through entire namespace
* Move to bytes crate
* Fix clippy errors
* Fix clippy warning
* Run cargo fmt
* Fix build
* Add renovate for bytes
* Fix clippy warn
---------
Co-authored-by: Daniel García
---
.github/renovate.json5 | 1 +
apps/desktop/desktop_native/Cargo.lock | 3 +-
apps/desktop/desktop_native/core/Cargo.toml | 35 ++++++++--------
.../desktop_native/core/src/ssh_agent/mod.rs | 41 +++++++++++++++++--
.../peercred_unix_listener_stream.rs | 4 +-
.../core/src/ssh_agent/peerinfo/models.rs | 23 ++++++++++-
.../core/src/ssh_agent/request_parser.rs | 41 +++++++++++++++++++
apps/desktop/desktop_native/napi/index.d.ts | 9 +++-
apps/desktop/desktop_native/napi/src/lib.rs | 23 ++++++++---
apps/desktop/src/locales/en/messages.json | 18 ++++++++
.../components/approve-ssh-request.html | 11 ++++-
.../components/approve-ssh-request.ts | 19 ++++++++-
.../platform/main/main-ssh-agent.service.ts | 10 +++--
.../platform/services/ssh-agent.service.ts | 4 ++
14 files changed, 203 insertions(+), 39 deletions(-)
create mode 100644 apps/desktop/desktop_native/core/src/ssh_agent/request_parser.rs
diff --git a/.github/renovate.json5 b/.github/renovate.json5
index a0826039bb8..b9de0084c25 100644
--- a/.github/renovate.json5
+++ b/.github/renovate.json5
@@ -123,6 +123,7 @@
matchPackageNames: [
"@emotion/css",
"@webcomponents/custom-elements",
+ "bytes",
"concurrently",
"cross-env",
"del",
diff --git a/apps/desktop/desktop_native/Cargo.lock b/apps/desktop/desktop_native/Cargo.lock
index 948e03cd49b..58cc4552f1e 100644
--- a/apps/desktop/desktop_native/Cargo.lock
+++ b/apps/desktop/desktop_native/Cargo.lock
@@ -439,7 +439,7 @@ checksum = "8f68f53c83ab957f72c32642f3868eec03eb974d1fb82e453128456482613d36"
[[package]]
name = "bitwarden-russh"
version = "0.1.0"
-source = "git+https://github.com/bitwarden/bitwarden-russh.git?rev=23b50e3bbe6d56ef19ab0e98e8bb1462cb6d77ae#23b50e3bbe6d56ef19ab0e98e8bb1462cb6d77ae"
+source = "git+https://github.com/bitwarden/bitwarden-russh.git?rev=3d48f140fd506412d186203238993163a8c4e536#3d48f140fd506412d186203238993163a8c4e536"
dependencies = [
"anyhow",
"byteorder",
@@ -942,6 +942,7 @@ dependencies = [
"base64",
"bitwarden-russh",
"byteorder",
+ "bytes",
"cbc",
"core-foundation",
"desktop_objc",
diff --git a/apps/desktop/desktop_native/core/Cargo.toml b/apps/desktop/desktop_native/core/Cargo.toml
index c821662f112..8a0bcd0e0a7 100644
--- a/apps/desktop/desktop_native/core/Cargo.toml
+++ b/apps/desktop/desktop_native/core/Cargo.toml
@@ -21,7 +21,7 @@ manual_test = []
aes = "=0.8.4"
anyhow = { workspace = true }
arboard = { version = "=3.4.1", default-features = false, features = [
- "wayland-data-control",
+ "wayland-data-control",
] }
argon2 = { version = "=0.5.3", features = ["zeroize"] }
base64 = "=0.22.1"
@@ -39,12 +39,12 @@ scopeguard = "=1.2.0"
sha2 = "=0.10.8"
ssh-encoding = "=0.2.0"
ssh-key = { version = "=0.6.7", default-features = false, features = [
- "encryption",
- "ed25519",
- "rsa",
- "getrandom",
+ "encryption",
+ "ed25519",
+ "rsa",
+ "getrandom",
] }
-bitwarden-russh = { git = "https://github.com/bitwarden/bitwarden-russh.git", rev = "23b50e3bbe6d56ef19ab0e98e8bb1462cb6d77ae" }
+bitwarden-russh = { git = "https://github.com/bitwarden/bitwarden-russh.git", rev = "3d48f140fd506412d186203238993163a8c4e536" }
tokio = { workspace = true, features = ["io-util", "sync", "macros", "net"] }
tokio-stream = { workspace = true, features = ["net"] }
tokio-util = { workspace = true, features = ["codec"] }
@@ -53,21 +53,22 @@ typenum = "=1.17.0"
pkcs8 = { version = "=0.10.2", features = ["alloc", "encryption", "pem"] }
rsa = "=0.9.6"
ed25519 = { version = "=2.2.3", features = ["pkcs8"] }
-sysinfo = { version = "=0.33.1", features = ["windows"] }
+bytes = "1.9.0"
+sysinfo = { version = "0.33.1", features = ["windows"] }
[target.'cfg(windows)'.dependencies]
widestring = { version = "=1.1.0", optional = true }
windows = { version = "=0.58.0", features = [
- "Foundation",
- "Security_Credentials_UI",
- "Security_Cryptography",
- "Storage_Streams",
- "Win32_Foundation",
- "Win32_Security_Credentials",
- "Win32_System_WinRT",
- "Win32_UI_Input_KeyboardAndMouse",
- "Win32_UI_WindowsAndMessaging",
- "Win32_System_Pipes",
+ "Foundation",
+ "Security_Credentials_UI",
+ "Security_Cryptography",
+ "Storage_Streams",
+ "Win32_Foundation",
+ "Win32_Security_Credentials",
+ "Win32_System_WinRT",
+ "Win32_UI_Input_KeyboardAndMouse",
+ "Win32_UI_WindowsAndMessaging",
+ "Win32_System_Pipes",
], optional = true }
[target.'cfg(windows)'.dev-dependencies]
diff --git a/apps/desktop/desktop_native/core/src/ssh_agent/mod.rs b/apps/desktop/desktop_native/core/src/ssh_agent/mod.rs
index 4e304ccea78..3fe327948f8 100644
--- a/apps/desktop/desktop_native/core/src/ssh_agent/mod.rs
+++ b/apps/desktop/desktop_native/core/src/ssh_agent/mod.rs
@@ -18,6 +18,8 @@ mod peercred_unix_listener_stream;
pub mod importer;
pub mod peerinfo;
+mod request_parser;
+
#[derive(Clone)]
pub struct BitwardenDesktopAgent {
keystore: ssh_agent::KeyStore,
@@ -35,19 +37,37 @@ pub struct SshAgentUIRequest {
pub cipher_id: Option,
pub process_name: String,
pub is_list: bool,
+ pub namespace: Option,
+ pub is_forwarding: bool,
}
impl ssh_agent::Agent for BitwardenDesktopAgent {
- async fn confirm(&self, ssh_key: Key, info: &peerinfo::models::PeerInfo) -> bool {
+ async fn confirm(&self, ssh_key: Key, data: &[u8], info: &peerinfo::models::PeerInfo) -> bool {
if !self.is_running() {
println!("[BitwardenDesktopAgent] Agent is not running, but tried to call confirm");
return false;
}
let request_id = self.get_request_id().await;
+ let request_data = match request_parser::parse_request(data) {
+ Ok(data) => data,
+ Err(e) => {
+ println!("[SSH Agent] Error while parsing request: {}", e);
+ return false;
+ }
+ };
+ let namespace = match request_data {
+ request_parser::SshAgentSignRequest::SshSigRequest(ref req) => {
+ Some(req.namespace.clone())
+ }
+ _ => None,
+ };
+
println!(
- "[SSH Agent] Confirming request from application: {}",
- info.process_name()
+ "[SSH Agent] Confirming request from application: {}, is_forwarding: {}, namespace: {}",
+ info.process_name(),
+ info.is_forwarding(),
+ namespace.clone().unwrap_or_default(),
);
let mut rx_channel = self.get_ui_response_rx.lock().await.resubscribe();
@@ -57,6 +77,8 @@ impl ssh_agent::Agent for BitwardenDesktopAgent {
cipher_id: Some(ssh_key.cipher_uuid.clone()),
process_name: info.process_name().to_string(),
is_list: false,
+ namespace,
+ is_forwarding: info.is_forwarding(),
})
.await
.expect("Should send request to ui");
@@ -81,6 +103,8 @@ impl ssh_agent::Agent for BitwardenDesktopAgent {
cipher_id: None,
process_name: info.process_name().to_string(),
is_list: true,
+ namespace: None,
+ is_forwarding: info.is_forwarding(),
};
self.show_ui_request_tx
.send(message)
@@ -93,6 +117,17 @@ impl ssh_agent::Agent for BitwardenDesktopAgent {
}
false
}
+
+ async fn set_is_forwarding(
+ &self,
+ is_forwarding: bool,
+ connection_info: &peerinfo::models::PeerInfo,
+ ) {
+ // is_forwarding can only be added but never removed from a connection
+ if is_forwarding {
+ connection_info.set_forwarding(is_forwarding);
+ }
+ }
}
impl BitwardenDesktopAgent {
diff --git a/apps/desktop/desktop_native/core/src/ssh_agent/peercred_unix_listener_stream.rs b/apps/desktop/desktop_native/core/src/ssh_agent/peercred_unix_listener_stream.rs
index da9d8a54318..77eec5e35c7 100644
--- a/apps/desktop/desktop_native/core/src/ssh_agent/peercred_unix_listener_stream.rs
+++ b/apps/desktop/desktop_native/core/src/ssh_agent/peercred_unix_listener_stream.rs
@@ -34,9 +34,7 @@ impl Stream for PeercredUnixListenerStream {
return Poll::Ready(Some(Ok((stream, PeerInfo::unknown()))));
}
},
- Err(_) => {
- return Poll::Ready(Some(Ok((stream, PeerInfo::unknown()))));
- }
+ Err(_) => return Poll::Ready(Some(Ok((stream, PeerInfo::unknown())))),
};
let peer_info = peerinfo::gather::get_peer_info(pid as u32);
match peer_info {
diff --git a/apps/desktop/desktop_native/core/src/ssh_agent/peerinfo/models.rs b/apps/desktop/desktop_native/core/src/ssh_agent/peerinfo/models.rs
index 9c2ee363e8f..35a5a508263 100644
--- a/apps/desktop/desktop_native/core/src/ssh_agent/peerinfo/models.rs
+++ b/apps/desktop/desktop_native/core/src/ssh_agent/peerinfo/models.rs
@@ -1,3 +1,5 @@
+use std::sync::{atomic::AtomicBool, Arc};
+
/**
* Peerinfo represents the information of a peer process connecting over a socket.
* This can be later extended to include more information (icon, app name) for the corresponding application.
@@ -7,6 +9,7 @@ pub struct PeerInfo {
uid: u32,
pid: u32,
process_name: String,
+ is_forwarding: Arc,
}
impl PeerInfo {
@@ -15,6 +18,16 @@ impl PeerInfo {
uid,
pid,
process_name,
+ is_forwarding: Arc::new(AtomicBool::new(false)),
+ }
+ }
+
+ pub fn unknown() -> Self {
+ Self {
+ uid: 0,
+ pid: 0,
+ process_name: "Unknown application".to_string(),
+ is_forwarding: Arc::new(AtomicBool::new(false)),
}
}
@@ -30,7 +43,13 @@ impl PeerInfo {
&self.process_name
}
- pub fn unknown() -> Self {
- Self::new(0, 0, "Unknown application".to_string())
+ pub fn is_forwarding(&self) -> bool {
+ self.is_forwarding
+ .load(std::sync::atomic::Ordering::Relaxed)
+ }
+
+ pub fn set_forwarding(&self, value: bool) {
+ self.is_forwarding
+ .store(value, std::sync::atomic::Ordering::Relaxed);
}
}
diff --git a/apps/desktop/desktop_native/core/src/ssh_agent/request_parser.rs b/apps/desktop/desktop_native/core/src/ssh_agent/request_parser.rs
new file mode 100644
index 00000000000..e93aa19869e
--- /dev/null
+++ b/apps/desktop/desktop_native/core/src/ssh_agent/request_parser.rs
@@ -0,0 +1,41 @@
+use bytes::{Buf, Bytes};
+
+#[derive(Debug)]
+pub(crate) struct SshSigRequest {
+ pub namespace: String,
+}
+
+#[derive(Debug)]
+pub(crate) struct SignRequest {}
+
+#[derive(Debug)]
+pub(crate) enum SshAgentSignRequest {
+ SshSigRequest(SshSigRequest),
+ SignRequest(SignRequest),
+}
+
+pub(crate) fn parse_request(data: &[u8]) -> Result {
+ let mut data = Bytes::copy_from_slice(data);
+ let magic_header = "SSHSIG";
+ let header = data.split_to(magic_header.len());
+
+ // sshsig; based on https://github.com/openssh/openssh-portable/blob/master/PROTOCOL.sshsig
+ if header == magic_header.as_bytes() {
+ let _version = data.get_u32();
+
+ // read until null byte
+ let namespace = data
+ .into_iter()
+ .take_while(|&x| x != 0)
+ .collect::>();
+ let namespace =
+ String::from_utf8(namespace).map_err(|_| anyhow::anyhow!("Invalid namespace"))?;
+
+ Ok(SshAgentSignRequest::SshSigRequest(SshSigRequest {
+ namespace,
+ }))
+ } else {
+ // regular sign request
+ Ok(SshAgentSignRequest::SignRequest(SignRequest {}))
+ }
+}
diff --git a/apps/desktop/desktop_native/napi/index.d.ts b/apps/desktop/desktop_native/napi/index.d.ts
index 997e951c89e..c40b7aed487 100644
--- a/apps/desktop/desktop_native/napi/index.d.ts
+++ b/apps/desktop/desktop_native/napi/index.d.ts
@@ -67,7 +67,14 @@ export declare namespace sshagent {
status: SshKeyImportStatus
sshKey?: SshKey
}
- export function serve(callback: (err: Error | null, arg0: string | undefined | null, arg1: boolean, arg2: string) => any): Promise
+ export interface SshUiRequest {
+ cipherId?: string
+ isList: boolean
+ processName: string
+ isForwarding: boolean
+ namespace?: string
+ }
+ export function serve(callback: (err: Error | null, arg: SshUiRequest) => any): Promise
export function stop(agentState: SshAgentState): void
export function isRunning(agentState: SshAgentState): boolean
export function setKeys(agentState: SshAgentState, newKeys: Array): void
diff --git a/apps/desktop/desktop_native/napi/src/lib.rs b/apps/desktop/desktop_native/napi/src/lib.rs
index 35566e16813..7d20bd50699 100644
--- a/apps/desktop/desktop_native/napi/src/lib.rs
+++ b/apps/desktop/desktop_native/napi/src/lib.rs
@@ -243,9 +243,18 @@ pub mod sshagent {
}
}
+ #[napi(object)]
+ pub struct SshUIRequest {
+ pub cipher_id: Option,
+ pub is_list: bool,
+ pub process_name: String,
+ pub is_forwarding: bool,
+ pub namespace: Option,
+ }
+
#[napi]
pub async fn serve(
- callback: ThreadsafeFunction<(Option, bool, String), CalleeHandled>,
+ callback: ThreadsafeFunction,
) -> napi::Result {
let (auth_request_tx, mut auth_request_rx) =
tokio::sync::mpsc::channel::(32);
@@ -262,11 +271,13 @@ pub mod sshagent {
let auth_response_tx_arc = cloned_response_tx_arc;
let callback = cloned_callback;
let promise_result: Result, napi::Error> = callback
- .call_async(Ok((
- request.cipher_id,
- request.is_list,
- request.process_name,
- )))
+ .call_async(Ok(SshUIRequest {
+ cipher_id: request.cipher_id,
+ is_list: request.is_list,
+ process_name: request.process_name,
+ is_forwarding: request.is_forwarding,
+ namespace: request.namespace,
+ }))
.await;
match promise_result {
Ok(promise_result) => match promise_result.await {
diff --git a/apps/desktop/src/locales/en/messages.json b/apps/desktop/src/locales/en/messages.json
index ed55c5fb070..726b5c4b316 100644
--- a/apps/desktop/src/locales/en/messages.json
+++ b/apps/desktop/src/locales/en/messages.json
@@ -3509,9 +3509,27 @@
"sshkeyApprovalTitle": {
"message": "Confirm SSH key usage"
},
+ "agentForwardingWarningTitle": {
+ "message": "Warning: Agent Forwarding"
+ },
+ "agentForwardingWarningText": {
+ "message": "This request comes from a remote device that you are logged into"
+ },
"sshkeyApprovalMessageInfix": {
"message": "is requesting access to"
},
+ "sshkeyApprovalMessageSuffix": {
+ "message": "in order to"
+ },
+ "sshActionLogin": {
+ "message": "authenticate to a server"
+ },
+ "sshActionSign": {
+ "message": "sign a message"
+ },
+ "sshActionGitSign": {
+ "message": "sign a git commit"
+ },
"unknownApplication": {
"message": "An application"
},
diff --git a/apps/desktop/src/platform/components/approve-ssh-request.html b/apps/desktop/src/platform/components/approve-ssh-request.html
index eac451a1fbe..952e3344e9c 100644
--- a/apps/desktop/src/platform/components/approve-ssh-request.html
+++ b/apps/desktop/src/platform/components/approve-ssh-request.html
@@ -2,8 +2,17 @@
{{ "sshkeyApprovalTitle" | i18n }}
+
+ {{ 'agentForwardingWarningText' | i18n }}
+
+
{{params.applicationName}} {{ "sshkeyApprovalMessageInfix" | i18n }}
-
{{params.cipherName}}.
+
{{params.cipherName}}
+ {{ "sshkeyApprovalMessageSuffix" | i18n }} {{ params.action | i18n }}
From 63ba9f6b311cfdbb1d30b3fff21826feaa6bde07 Mon Sep 17 00:00:00 2001
From: Conner Turnbull <133619638+cturnbull-bitwarden@users.noreply.github.com>
Date: Wed, 26 Feb 2025 14:09:29 -0500
Subject: [PATCH 06/47] Fixed typo in tailwind class name (#13586)
---
.../app/billing/organizations/organization-plans.component.html | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/apps/web/src/app/billing/organizations/organization-plans.component.html b/apps/web/src/app/billing/organizations/organization-plans.component.html
index 263ef12ebea..3b765927c3c 100644
--- a/apps/web/src/app/billing/organizations/organization-plans.component.html
+++ b/apps/web/src/app/billing/organizations/organization-plans.component.html
@@ -216,7 +216,7 @@
formControlName="additionalSeats"
placeholder="{{ 'userSeatsDesc' | i18n }}"
/>
-
{{
"userSeatsAdditionalDesc"
| i18n
From cdbbbb365b0b3857fca87b92f0aeb96df5c0b37e Mon Sep 17 00:00:00 2001
From: Conner Turnbull <133619638+cturnbull-bitwarden@users.noreply.github.com>
Date: Wed, 26 Feb 2025 14:09:36 -0500
Subject: [PATCH 07/47] Bootstrap to tailwind updates (#13582)
---
.../change-plan-dialog.component.html | 31 +++++++++----------
1 file changed, 15 insertions(+), 16 deletions(-)
diff --git a/apps/web/src/app/billing/organizations/change-plan-dialog.component.html b/apps/web/src/app/billing/organizations/change-plan-dialog.component.html
index ca1b9245c0b..64a694cdef0 100644
--- a/apps/web/src/app/billing/organizations/change-plan-dialog.component.html
+++ b/apps/web/src/app/billing/organizations/change-plan-dialog.component.html
@@ -55,7 +55,7 @@
class="tw-grid tw-grid-flow-col tw-gap-4 tw-mb-4"
[class]="'tw-grid-cols-' + selectableProducts.length"
>
-
-
+
{{ paymentSource?.description }}
-
+
{{ "changePaymentMethod" | i18n }}
@@ -382,8 +381,8 @@
-
-
+
+
{{ "passwordManager" | i18n }}
@@ -550,7 +549,7 @@
-
+
{{ "passwordManager" | i18n }}
@@ -706,8 +705,8 @@
-
-
+
+
{{ "secretsManager" | i18n }}
@@ -826,7 +825,7 @@
-
+
{{ "secretsManager" | i18n }}
@@ -930,9 +929,9 @@
-
-
+
-
-
+
+
From 9395e9495eac3c7f0624bab792c09d27454b45a0 Mon Sep 17 00:00:00 2001
From: Conner Turnbull <133619638+cturnbull-bitwarden@users.noreply.github.com>
Date: Wed, 26 Feb 2025 14:09:41 -0500
Subject: [PATCH 08/47] Updated trial-billing-step.component.html to use
tw-text-muted instead of text-muted (#13569)
---
.../accounts/trial-initiation/trial-billing-step.component.html | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/apps/web/src/app/billing/accounts/trial-initiation/trial-billing-step.component.html b/apps/web/src/app/billing/accounts/trial-initiation/trial-billing-step.component.html
index b3b8668e90b..212df558d94 100644
--- a/apps/web/src/app/billing/accounts/trial-initiation/trial-billing-step.component.html
+++ b/apps/web/src/app/billing/accounts/trial-initiation/trial-billing-step.component.html
@@ -1,6 +1,6 @@
From 19326609e335859e54421ff0105570e035d46fe7 Mon Sep 17 00:00:00 2001
From: Conner Turnbull <133619638+cturnbull-bitwarden@users.noreply.github.com>
Date: Wed, 26 Feb 2025 14:14:28 -0500
Subject: [PATCH 09/47] Updated to use tailwind's tw-text-muted (#13587)
---
.../organization-subscription-cloud.component.html | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/apps/web/src/app/billing/organizations/organization-subscription-cloud.component.html b/apps/web/src/app/billing/organizations/organization-subscription-cloud.component.html
index 1d8a7846d9d..062b3c05eb8 100644
--- a/apps/web/src/app/billing/organizations/organization-subscription-cloud.component.html
+++ b/apps/web/src/app/billing/organizations/organization-subscription-cloud.component.html
@@ -1,7 +1,7 @@
-
+
{{ "loading" | i18n }}
From 359007ab8d3264262920b33cebc71328958d55a0 Mon Sep 17 00:00:00 2001
From: rr-bw <102181210+rr-bw@users.noreply.github.com>
Date: Wed, 26 Feb 2025 11:44:41 -0800
Subject: [PATCH 10/47] fix(auth): [PM-18639] Resend Admin Auth Request After
Previous Denial (#13574)
---
.../services/auth-request/auth-request-api.service.ts | 8 ++++----
1 file changed, 4 insertions(+), 4 deletions(-)
diff --git a/libs/auth/src/common/services/auth-request/auth-request-api.service.ts b/libs/auth/src/common/services/auth-request/auth-request-api.service.ts
index 180e0079396..b5fc72588a6 100644
--- a/libs/auth/src/common/services/auth-request/auth-request-api.service.ts
+++ b/libs/auth/src/common/services/auth-request/auth-request-api.service.ts
@@ -16,7 +16,7 @@ export class DefaultAuthRequestApiService implements AuthRequestApiService {
const path = `/auth-requests/${requestId}`;
const response = await this.apiService.send("GET", path, null, true, true);
- return response;
+ return new AuthRequestResponse(response);
} catch (e: unknown) {
this.logService.error(e);
throw e;
@@ -28,7 +28,7 @@ export class DefaultAuthRequestApiService implements AuthRequestApiService {
const path = `/auth-requests/${requestId}/response?code=${accessCode}`;
const response = await this.apiService.send("GET", path, null, false, true);
- return response;
+ return new AuthRequestResponse(response);
} catch (e: unknown) {
this.logService.error(e);
throw e;
@@ -45,7 +45,7 @@ export class DefaultAuthRequestApiService implements AuthRequestApiService {
true,
);
- return response;
+ return new AuthRequestResponse(response);
} catch (e: unknown) {
this.logService.error(e);
throw e;
@@ -56,7 +56,7 @@ export class DefaultAuthRequestApiService implements AuthRequestApiService {
try {
const response = await this.apiService.send("POST", "/auth-requests/", request, false, true);
- return response;
+ return new AuthRequestResponse(response);
} catch (e: unknown) {
this.logService.error(e);
throw e;
From d999d91f19f23fe278ae4e00bcd1d2447adef1fc Mon Sep 17 00:00:00 2001
From: Patrick-Pimentel-Bitwarden
Date: Wed, 26 Feb 2025 14:54:06 -0500
Subject: [PATCH 11/47] fix(sso-routing): [Auth/PM-13458] Fixes for routing
flow on TDE login (#13479)
* fix(sso-routing): [PM-13458] Fixes for routing flow on TDE login - Fixed routing flow and added comments.
* fix(sso-routing): [PM-13458] Fixes for routing flow on TDE login - Undid the old sso component flow because we determined it's not worth fixing.
* fix(sso-routing): [PM-13458] Fixes for routing flow on TDE login - Removed flow entirely.
---
libs/auth/src/angular/sso/sso.component.ts | 7 -------
1 file changed, 7 deletions(-)
diff --git a/libs/auth/src/angular/sso/sso.component.ts b/libs/auth/src/angular/sso/sso.component.ts
index 9f81ab2a748..cd3429323b5 100644
--- a/libs/auth/src/angular/sso/sso.component.ts
+++ b/libs/auth/src/angular/sso/sso.component.ts
@@ -445,13 +445,6 @@ export class SsoComponent implements OnInit {
const userId = (await firstValueFrom(this.accountService.activeAccount$))?.id;
await this.ssoLoginService.setActiveUserOrganizationSsoIdentifier(orgSsoIdentifier, userId);
- // Users enrolled in admin acct recovery can be forced to set a new password after
- // having the admin set a temp password for them (affects TDE & standard users)
- if (authResult.forcePasswordReset == ForceSetPasswordReason.AdminForcePasswordReset) {
- // Weak password is not a valid scenario here b/c we cannot have evaluated a MP yet
- return await this.handleForcePasswordReset(orgSsoIdentifier);
- }
-
// must come after 2fa check since user decryption options aren't available if 2fa is required
const userDecryptionOpts = await firstValueFrom(
this.userDecryptionOptionsService.userDecryptionOptions$,
From 9aee5f16c40b0cbde4e1a67bf50976818ff60ae1 Mon Sep 17 00:00:00 2001
From: Daniel Riera
Date: Wed, 26 Feb 2025 16:13:27 -0500
Subject: [PATCH 12/47] PM-18276-Connect confirmation UI (#13498)
* PM-18276-wip
* update typing
* dynamically retrieve messages, resolve theme in function
* five second timeout after save or update
* adjust timeout to five seconds
* negligible performance gain-revert
* sacrifice contorl for to remove event listeners-revert
---
.../notification/confirmation-container.ts | 98 ++++++++++
.../content/components/notification/header.ts | 6 +-
apps/browser/src/autofill/notification/bar.ts | 174 +++++++++++-------
3 files changed, 206 insertions(+), 72 deletions(-)
create mode 100644 apps/browser/src/autofill/content/components/notification/confirmation-container.ts
diff --git a/apps/browser/src/autofill/content/components/notification/confirmation-container.ts b/apps/browser/src/autofill/content/components/notification/confirmation-container.ts
new file mode 100644
index 00000000000..4fda95986db
--- /dev/null
+++ b/apps/browser/src/autofill/content/components/notification/confirmation-container.ts
@@ -0,0 +1,98 @@
+import { css } from "@emotion/css";
+import { html } from "lit";
+
+import { Theme, ThemeTypes } from "@bitwarden/common/platform/enums";
+
+import {
+ NotificationBarIframeInitData,
+ NotificationTypes,
+ NotificationType,
+} from "../../../notification/abstractions/notification-bar";
+import { themes, spacing } from "../constants/styles";
+
+import { NotificationConfirmationBody } from "./confirmation";
+import {
+ NotificationHeader,
+ componentClassPrefix as notificationHeaderClassPrefix,
+} from "./header";
+
+export function NotificationConfirmationContainer({
+ error,
+ handleCloseNotification,
+ i18n,
+ theme = ThemeTypes.Light,
+ type,
+}: NotificationBarIframeInitData & {
+ handleCloseNotification: (e: Event) => void;
+} & {
+ error: string;
+ i18n: { [key: string]: string };
+ type: NotificationType;
+}) {
+ const headerMessage = getHeaderMessage(i18n, type, error);
+ const confirmationMessage = getConfirmationMessage(i18n, type, error);
+ const buttonText = error ? i18n.newItem : i18n.view;
+
+ return html`
+
+ ${NotificationHeader({
+ handleCloseNotification,
+ message: headerMessage,
+ theme,
+ })}
+ ${NotificationConfirmationBody({
+ error: error,
+ buttonText,
+ confirmationMessage,
+ theme,
+ })}
+
+ `;
+}
+
+const notificationContainerStyles = (theme: Theme) => css`
+ position: absolute;
+ right: 20px;
+ border: 1px solid ${themes[theme].secondary["300"]};
+ border-radius: ${spacing["4"]};
+ box-shadow: -2px 4px 6px 0px #0000001a;
+ background-color: ${themes[theme].background.alt};
+ width: 400px;
+ overflow: hidden;
+
+ [class*="${notificationHeaderClassPrefix}-"] {
+ border-radius: ${spacing["4"]} ${spacing["4"]} 0 0;
+ border-bottom: 0.5px solid ${themes[theme].secondary["300"]};
+ }
+`;
+
+function getConfirmationMessage(
+ i18n: { [key: string]: string },
+ type?: NotificationType,
+ error?: string,
+) {
+ if (error) {
+ return i18n.saveFailureDetails;
+ }
+ return type === "add" ? i18n.loginSaveSuccessDetails : i18n.loginUpdateSuccessDetails;
+}
+function getHeaderMessage(
+ i18n: { [key: string]: string },
+ type?: NotificationType,
+ error?: string,
+) {
+ if (error) {
+ return i18n.saveFailure;
+ }
+
+ switch (type) {
+ case NotificationTypes.Add:
+ return i18n.loginSaveSuccess;
+ case NotificationTypes.Change:
+ return i18n.loginUpdateSuccess;
+ case NotificationTypes.Unlock:
+ return "";
+ default:
+ return undefined;
+ }
+}
diff --git a/apps/browser/src/autofill/content/components/notification/header.ts b/apps/browser/src/autofill/content/components/notification/header.ts
index 85f6e48cd5d..50c2c629942 100644
--- a/apps/browser/src/autofill/content/components/notification/header.ts
+++ b/apps/browser/src/autofill/content/components/notification/header.ts
@@ -17,12 +17,12 @@ const { css } = createEmotion({
export function NotificationHeader({
message,
- standalone,
+ standalone = false,
theme = ThemeTypes.Light,
handleCloseNotification,
}: {
message?: string;
- standalone: boolean;
+ standalone?: boolean;
theme: Theme;
handleCloseNotification: (e: Event) => void;
}) {
@@ -49,7 +49,7 @@ const notificationHeaderStyles = ({
display: flex;
align-items: center;
justify-content: flex-start;
- background-color: ${themes[theme].background.alt};
+ background-color: ${themes[theme].background};
padding: 12px 16px 8px 16px;
white-space: nowrap;
diff --git a/apps/browser/src/autofill/notification/bar.ts b/apps/browser/src/autofill/notification/bar.ts
index 5a8d7855bea..8c4c8a3e229 100644
--- a/apps/browser/src/autofill/notification/bar.ts
+++ b/apps/browser/src/autofill/notification/bar.ts
@@ -7,6 +7,7 @@ import { ConsoleLogService } from "@bitwarden/common/platform/services/console-l
import type { FolderView } from "@bitwarden/common/vault/models/view/folder.view";
import { AdjustNotificationBarMessageData } from "../background/abstractions/notification.background";
+import { NotificationConfirmationContainer } from "../content/components/notification/confirmation-container";
import { NotificationContainer } from "../content/components/notification/container";
import { buildSvgDomElement } from "../utils";
import { circleCheckIcon } from "../utils/svg-icons";
@@ -22,12 +23,17 @@ const logService = new ConsoleLogService(false);
let notificationBarIframeInitData: NotificationBarIframeInitData = {};
let windowMessageOrigin: string;
let useComponentBar = false;
+
const notificationBarWindowMessageHandlers: NotificationBarWindowMessageHandlers = {
initNotificationBar: ({ message }) => initNotificationBar(message),
- saveCipherAttemptCompleted: ({ message }) => handleSaveCipherAttemptCompletedMessage(message),
+ saveCipherAttemptCompleted: ({ message }) =>
+ useComponentBar
+ ? handleSaveCipherConfirmation(message)
+ : handleSaveCipherAttemptCompletedMessage(message),
};
globalThis.addEventListener("load", load);
+
function load() {
setupWindowMessageListener();
sendPlatformMessage({ command: "notificationRefreshFlagValue" }, (flagValue) => {
@@ -35,7 +41,6 @@ function load() {
applyNotificationBarStyle();
});
}
-
function applyNotificationBarStyle() {
if (!useComponentBar) {
// eslint-disable-next-line @typescript-eslint/no-require-imports
@@ -44,16 +49,8 @@ function applyNotificationBarStyle() {
postMessageToParent({ command: "initNotificationBar" });
}
-function initNotificationBar(message: NotificationBarWindowMessage) {
- const { initData } = message;
- if (!initData) {
- return;
- }
-
- notificationBarIframeInitData = initData;
- const { isVaultLocked, theme } = notificationBarIframeInitData;
-
- const i18n = {
+function getI18n() {
+ return {
appName: chrome.i18n.getMessage("appName"),
close: chrome.i18n.getMessage("close"),
never: chrome.i18n.getMessage("never"),
@@ -74,20 +71,30 @@ function initNotificationBar(message: NotificationBarWindowMessage) {
updateLoginPrompt: "Update existing login?",
loginSaveSuccess: "Login saved",
loginSaveSuccessDetails: "Login saved to Bitwarden.",
- loginUpdateSuccess: "Login saved",
+ loginUpdateSuccess: "Login updated",
loginUpdateSuccessDetails: "Login updated in Bitwarden.",
saveFailure: "Error saving",
saveFailureDetails: "Oh no! We couldn't save this. Try entering the details as a New item",
+ newItem: "New item",
+ view: "View",
};
+}
+
+function initNotificationBar(message: NotificationBarWindowMessage) {
+ const { initData } = message;
+ if (!initData) {
+ return;
+ }
+
+ notificationBarIframeInitData = initData;
+ const { isVaultLocked, theme } = notificationBarIframeInitData;
+ const i18n = getI18n();
+ const resolvedTheme = getResolvedTheme(theme);
if (useComponentBar) {
document.body.innerHTML = "";
// Current implementations utilize a require for scss files which creates the need to remove the node.
document.head.querySelectorAll('link[rel="stylesheet"]').forEach((node) => node.remove());
- const themeType = getTheme(globalThis, theme);
-
- // There are other possible passed theme values, but for now, resolve to dark or light
- const resolvedTheme: Theme = themeType === ThemeTypes.Dark ? ThemeTypes.Dark : ThemeTypes.Light;
sendPlatformMessage({ command: "bgGetDecryptedCiphers" }, (cipherData) => {
// @TODO use context to avoid prop drilling
@@ -105,77 +112,71 @@ function initNotificationBar(message: NotificationBarWindowMessage) {
document.body,
);
});
- }
+ } else {
+ setNotificationBarTheme();
- setNotificationBarTheme();
+ (document.getElementById("logo") as HTMLImageElement).src = isVaultLocked
+ ? chrome.runtime.getURL("images/icon38_locked.png")
+ : chrome.runtime.getURL("images/icon38.png");
- (document.getElementById("logo") as HTMLImageElement).src = isVaultLocked
- ? chrome.runtime.getURL("images/icon38_locked.png")
- : chrome.runtime.getURL("images/icon38.png");
+ setupLogoLink(i18n);
- setupLogoLink(i18n);
+ // i18n for "Add" template
+ const addTemplate = document.getElementById("template-add") as HTMLTemplateElement;
- // i18n for "Add" template
- const addTemplate = document.getElementById("template-add") as HTMLTemplateElement;
+ const neverButton = addTemplate.content.getElementById("never-save");
+ neverButton.textContent = i18n.never;
- const neverButton = addTemplate.content.getElementById("never-save");
- neverButton.textContent = i18n.never;
+ const selectFolder = addTemplate.content.getElementById("select-folder");
+ selectFolder.hidden = isVaultLocked || removeIndividualVault();
+ selectFolder.setAttribute("aria-label", i18n.folder);
- const selectFolder = addTemplate.content.getElementById("select-folder");
- selectFolder.hidden = isVaultLocked || removeIndividualVault();
- selectFolder.setAttribute("aria-label", i18n.folder);
+ const addButton = addTemplate.content.getElementById("add-save");
+ addButton.textContent = i18n.notificationAddSave;
- const addButton = addTemplate.content.getElementById("add-save");
- addButton.textContent = i18n.notificationAddSave;
+ const addEditButton = addTemplate.content.getElementById("add-edit");
+ // If Remove Individual Vault policy applies, "Add" opens the edit tab, so we hide the Edit button
+ addEditButton.hidden = removeIndividualVault();
+ addEditButton.textContent = i18n.notificationEdit;
- const addEditButton = addTemplate.content.getElementById("add-edit");
- // If Remove Individual Vault policy applies, "Add" opens the edit tab, so we hide the Edit button
- addEditButton.hidden = removeIndividualVault();
- addEditButton.textContent = i18n.notificationEdit;
+ addTemplate.content.getElementById("add-text").textContent = i18n.notificationAddDesc;
- addTemplate.content.getElementById("add-text").textContent = i18n.notificationAddDesc;
+ // i18n for "Change" (update password) template
+ const changeTemplate = document.getElementById("template-change") as HTMLTemplateElement;
- // i18n for "Change" (update password) template
- const changeTemplate = document.getElementById("template-change") as HTMLTemplateElement;
+ const changeButton = changeTemplate.content.getElementById("change-save");
+ changeButton.textContent = i18n.notificationChangeSave;
- const changeButton = changeTemplate.content.getElementById("change-save");
- changeButton.textContent = i18n.notificationChangeSave;
+ const changeEditButton = changeTemplate.content.getElementById("change-edit");
+ changeEditButton.textContent = i18n.notificationEdit;
- const changeEditButton = changeTemplate.content.getElementById("change-edit");
- changeEditButton.textContent = i18n.notificationEdit;
+ changeTemplate.content.getElementById("change-text").textContent = i18n.notificationChangeDesc;
- changeTemplate.content.getElementById("change-text").textContent = i18n.notificationChangeDesc;
+ // i18n for "Unlock" (unlock extension) template
+ const unlockTemplate = document.getElementById("template-unlock") as HTMLTemplateElement;
- // i18n for "Unlock" (unlock extension) template
- const unlockTemplate = document.getElementById("template-unlock") as HTMLTemplateElement;
+ const unlockButton = unlockTemplate.content.getElementById("unlock-vault");
+ unlockButton.textContent = i18n.notificationUnlock;
- const unlockButton = unlockTemplate.content.getElementById("unlock-vault");
- unlockButton.textContent = i18n.notificationUnlock;
+ unlockTemplate.content.getElementById("unlock-text").textContent = i18n.notificationUnlockDesc;
- unlockTemplate.content.getElementById("unlock-text").textContent = i18n.notificationUnlockDesc;
+ // i18n for body content
+ const closeButton = document.getElementById("close-button");
+ closeButton.title = i18n.close;
- // i18n for body content
- const closeButton = document.getElementById("close-button");
- closeButton.title = i18n.close;
+ const notificationType = initData.type;
+ if (notificationType === "add") {
+ handleTypeAdd();
+ } else if (notificationType === "change") {
+ handleTypeChange();
+ } else if (notificationType === "unlock") {
+ handleTypeUnlock();
+ }
- const notificationType = initData.type;
- if (notificationType === "add") {
- handleTypeAdd();
- } else if (notificationType === "change") {
- handleTypeChange();
- } else if (notificationType === "unlock") {
- handleTypeUnlock();
- }
+ closeButton.addEventListener("click", handleCloseNotification);
- closeButton.addEventListener("click", handleCloseNotification);
-
- globalThis.addEventListener("resize", adjustHeight);
- adjustHeight();
- function handleCloseNotification(e: Event) {
- e.preventDefault();
- sendPlatformMessage({
- command: "bgCloseNotificationBar",
- });
+ globalThis.addEventListener("resize", adjustHeight);
+ adjustHeight();
}
function handleEditOrUpdateAction(e: Event) {
const notificationType = initData.type;
@@ -183,6 +184,12 @@ function initNotificationBar(message: NotificationBarWindowMessage) {
notificationType === "add" ? sendSaveCipherMessage(true) : sendSaveCipherMessage(false);
}
}
+function handleCloseNotification(e: Event) {
+ e.preventDefault();
+ sendPlatformMessage({
+ command: "bgCloseNotificationBar",
+ });
+}
function handleSaveAction(e: Event) {
e.preventDefault();
@@ -282,6 +289,27 @@ function handleSaveCipherAttemptCompletedMessage(message: NotificationBarWindowM
);
}
+function handleSaveCipherConfirmation(message: NotificationBarWindowMessage) {
+ const { theme, type } = notificationBarIframeInitData;
+ const { error } = message;
+ const i18n = getI18n();
+ const resolvedTheme = getResolvedTheme(theme);
+
+ globalThis.setTimeout(() => sendPlatformMessage({ command: "bgCloseNotificationBar" }), 5000);
+
+ return render(
+ NotificationConfirmationContainer({
+ ...notificationBarIframeInitData,
+ type: type as NotificationType,
+ theme: resolvedTheme,
+ handleCloseNotification,
+ i18n,
+ error,
+ }),
+ document.body,
+ );
+}
+
function handleTypeUnlock() {
setContent(document.getElementById("template-unlock") as HTMLTemplateElement);
@@ -395,6 +423,14 @@ function getTheme(globalThis: any, theme: NotificationBarIframeInitData["theme"]
return theme;
}
+function getResolvedTheme(theme: Theme) {
+ const themeType = getTheme(globalThis, theme);
+
+ // There are other possible passed theme values, but for now, resolve to dark or light
+ const resolvedTheme: Theme = themeType === ThemeTypes.Dark ? ThemeTypes.Dark : ThemeTypes.Light;
+ return resolvedTheme;
+}
+
function setNotificationBarTheme() {
const theme = getTheme(globalThis, notificationBarIframeInitData.theme);
From b9ebf0704a413a32c9a34b4c37e1e8726f433631 Mon Sep 17 00:00:00 2001
From: Shane Melton
Date: Wed, 26 Feb 2025 13:24:35 -0800
Subject: [PATCH 13/47] [PM-14426] At-risk password Getting Started Carousel
(#13383)
* [PM-14426] Add hideIcon input to simple dialog component
* [PM-14426] Introduce dark-image-source.directive.ts
* [PM-14426] Tweaks to the Vault Carousel component
- Create a Carousel NgModule so that the carousel component and carousel slide component are exported
- Update barrel files
- Adjust min height calculation logic to wait for ;hidden slides to finish rendering before calculating height
* [PM-14426] Introduce at risk password getting started carousel component and images
* [PM-14426] Refactor at-risk-password-page.service.ts to use the same state definition for banner and carousel dismissal
* [PM-14426] Open the getting started carousel on page load
* [PM-14426] Add tests
* [PM-14426] Use booleanAttribute
* [PM-14426] Fix failing type checking
---
apps/browser/src/_locales/en/messages.json | 30 +++++++++
.../generate_password.dark.png | Bin 0 -> 33421 bytes
.../generate_password.light.png | Bin 0 -> 33116 bytes
.../review_at-risk_logins.dark.png | Bin 0 -> 28260 bytes
.../review_at-risk_logins.light.png | Bin 0 -> 27051 bytes
.../update_login.dark.png | Bin 0 -> 25466 bytes
.../update_login.light.png | Bin 0 -> 25759 bytes
.../at-risk-carousel-dialog.component.html | 54 +++++++++++++++
.../at-risk-carousel-dialog.component.ts | 46 +++++++++++++
.../at-risk-password-page.service.ts | 33 ++++++++--
.../at-risk-passwords.component.spec.ts | 34 +++++++++-
.../at-risk-passwords.component.ts | 29 +++++++-
.../src/platform/state/state-definitions.ts | 1 +
.../simple-dialog.component.html | 10 +--
.../simple-dialog/simple-dialog.component.ts | 7 +-
.../simple-dialog/simple-dialog.stories.ts | 20 +++++-
.../carousel/carousel.component.html | 2 +-
.../components/carousel/carousel.component.ts | 22 +++----
.../components/carousel/carousel.module.ts | 10 +++
libs/vault/src/components/carousel/index.ts | 2 +-
.../components/dark-image-source.directive.ts | 62 ++++++++++++++++++
libs/vault/src/index.ts | 2 +
22 files changed, 334 insertions(+), 30 deletions(-)
create mode 100644 apps/browser/src/images/at-risk-password-carousel/generate_password.dark.png
create mode 100644 apps/browser/src/images/at-risk-password-carousel/generate_password.light.png
create mode 100644 apps/browser/src/images/at-risk-password-carousel/review_at-risk_logins.dark.png
create mode 100644 apps/browser/src/images/at-risk-password-carousel/review_at-risk_logins.light.png
create mode 100644 apps/browser/src/images/at-risk-password-carousel/update_login.dark.png
create mode 100644 apps/browser/src/images/at-risk-password-carousel/update_login.light.png
create mode 100644 apps/browser/src/vault/popup/components/at-risk-carousel-dialog/at-risk-carousel-dialog.component.html
create mode 100644 apps/browser/src/vault/popup/components/at-risk-carousel-dialog/at-risk-carousel-dialog.component.ts
create mode 100644 libs/vault/src/components/carousel/carousel.module.ts
create mode 100644 libs/vault/src/components/dark-image-source.directive.ts
diff --git a/apps/browser/src/_locales/en/messages.json b/apps/browser/src/_locales/en/messages.json
index ea9e62916a2..ce73bd99d36 100644
--- a/apps/browser/src/_locales/en/messages.json
+++ b/apps/browser/src/_locales/en/messages.json
@@ -2486,6 +2486,36 @@
"changeAtRiskPasswordsFasterDesc": {
"message": "Update your settings so you can quickly autofill your passwords and generate new ones"
},
+ "reviewAtRiskLogins": {
+ "message": "Review at-risk logins"
+ },
+ "reviewAtRiskPasswords": {
+ "message": "Review at-risk passwords"
+ },
+ "reviewAtRiskLoginsSlideDesc": {
+ "message": "Your organization passwords are at-risk because they are weak, reused, and/or exposed.",
+ "description": "Description of the review at-risk login slide on the at-risk password page carousel"
+ },
+ "reviewAtRiskLoginSlideImgAlt": {
+ "message": "Illustration of a list of logins that are at-risk"
+ },
+ "generatePasswordSlideDesc": {
+ "message": "Quickly generate a strong, unique password with the Bitwarden autofill menu on the at-risk site.",
+ "description": "Description of the generate password slide on the at-risk password page carousel"
+ },
+ "generatePasswordSlideImgAlt": {
+ "message": "Illustration of the Bitwarden autofill menu displaying a generated password"
+ },
+ "updateInBitwarden": {
+ "message": "Update in Bitwarden"
+ },
+ "updateInBitwardenSlideDesc": {
+ "message": "Bitwarden will then prompt you to update the password in the password manager.",
+ "description": "Description of the update in Bitwarden slide on the at-risk password page carousel"
+ },
+ "updateInBitwardenSlideImgAlt": {
+ "message": "Illustration of a Bitwarden’s notification prompting the user to update the login"
+ },
"turnOnAutofill": {
"message": "Turn on autofill"
},
diff --git a/apps/browser/src/images/at-risk-password-carousel/generate_password.dark.png b/apps/browser/src/images/at-risk-password-carousel/generate_password.dark.png
new file mode 100644
index 0000000000000000000000000000000000000000..8d0fbbdec560915a2f816c5c8f57599ca8008cb1
GIT binary patch
literal 33421
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
literal 0
HcmV?d00001
diff --git a/apps/browser/src/images/at-risk-password-carousel/generate_password.light.png b/apps/browser/src/images/at-risk-password-carousel/generate_password.light.png
new file mode 100644
index 0000000000000000000000000000000000000000..4e46ce2adc34b6aa93d3226453961e45056e33ee
GIT binary patch
literal 33116
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