1
0
mirror of https://github.com/bitwarden/browser synced 2025-12-16 08:13:42 +00:00

[PM-18017] Show key connector domain in remove password page (#14695)

* Passed in userId on RemovePasswordComponent.

* Added userId on other references to KeyConnectorService methods

* remove password component refactor, test coverage, enabled strict

* explicit user id provided to key connector service

* redirect to / instead when user not logged in or not managing organization

* key connector service explicit user id

* key connector service no longer requires account service

* key connector service missing null type

* cli convert to key connector unit tests

* remove unnecessary SyncService

* error toast not showing on ErrorResponse

* bad import due to merge conflict

* bad import due to merge conflict

* missing loading in remove password component for browser extension

* error handling in remove password component

* organization observable race condition in key-connector

* usesKeyConnector always returns boolean

* unit test coverage

* key connector reactive

* reactive key connector service

* introducing convertAccountRequired$

* cli build fix

* moving message sending side effect to sync

* key connector service unit tests

* fix unit tests

* move key connector components to KM team ownership

* new unit tests in wrong place

* key connector domain shown in remove password component

* type safety improvements

* convert to key connector command localization

* key connector domain in convert to key connector command

* convert to key connector command unit tests with prompt assert

* organization name placement change in the remove password component

* unit test update

* key connector url required to be provided when migrating user

* unit tests in wrong place after KM code ownership move

* infinite page reload

* failing unit tests

* failing unit tests

---------

Co-authored-by: Todd Martin <tmartin@bitwarden.com>
This commit is contained in:
Maciej Zieniuk
2025-05-19 14:58:51 +02:00
committed by GitHub
parent ef592bf23a
commit 239556b55f
18 changed files with 201 additions and 140 deletions

View File

@@ -5,13 +5,13 @@ import { IdentityTokenResponse } from "../../../auth/models/response/identity-to
import { UserId } from "../../../types/guid";
export abstract class KeyConnectorService {
abstract setMasterKeyFromUrl(url: string, userId: UserId): Promise<void>;
abstract setMasterKeyFromUrl(keyConnectorUrl: string, userId: UserId): Promise<void>;
abstract getManagingOrganization(userId: UserId): Promise<Organization>;
abstract getUsesKeyConnector(userId: UserId): Promise<boolean>;
abstract migrateUser(userId: UserId): Promise<void>;
abstract migrateUser(keyConnectorUrl: string, userId: UserId): Promise<void>;
abstract convertNewSsoUserToKeyConnector(
tokenResponse: IdentityTokenResponse,

View File

@@ -45,6 +45,8 @@ describe("KeyConnectorService", () => {
key: "eO9nVlVl3I3sU6O+CyK0kEkpGtl/auT84Hig2WTXmZtDTqYtKpDvUPfjhgMOHf+KQzx++TVS2AOLYq856Caa7w==",
});
const keyConnectorUrl = "https://key-connector-url.com";
beforeEach(() => {
jest.clearAllMocks();
@@ -124,27 +126,9 @@ describe("KeyConnectorService", () => {
it("should return the managing organization with key connector enabled", async () => {
// Arrange
const orgs = [
organizationData(
true,
true,
"https://key-connector-url.com",
OrganizationUserType.User,
false,
),
organizationData(
false,
true,
"https://key-connector-url.com",
OrganizationUserType.User,
false,
),
organizationData(
true,
false,
"https://key-connector-url.com",
OrganizationUserType.User,
false,
),
organizationData(true, true, keyConnectorUrl, OrganizationUserType.User, false),
organizationData(false, true, keyConnectorUrl, OrganizationUserType.User, false),
organizationData(true, false, keyConnectorUrl, OrganizationUserType.User, false),
organizationData(true, true, "https://other-url.com", OrganizationUserType.User, false),
];
organizationService.organizations$.mockReturnValue(of(orgs));
@@ -159,20 +143,8 @@ describe("KeyConnectorService", () => {
it("should return undefined if no managing organization with key connector enabled is found", async () => {
// Arrange
const orgs = [
organizationData(
true,
false,
"https://key-connector-url.com",
OrganizationUserType.User,
false,
),
organizationData(
false,
false,
"https://key-connector-url.com",
OrganizationUserType.User,
false,
),
organizationData(true, false, keyConnectorUrl, OrganizationUserType.User, false),
organizationData(false, false, keyConnectorUrl, OrganizationUserType.User, false),
];
organizationService.organizations$.mockReturnValue(of(orgs));
@@ -186,8 +158,8 @@ describe("KeyConnectorService", () => {
it("should return undefined if user is Owner or Admin", async () => {
// Arrange
const orgs = [
organizationData(true, true, "https://key-connector-url.com", 0, false),
organizationData(true, true, "https://key-connector-url.com", 1, false),
organizationData(true, true, keyConnectorUrl, 0, false),
organizationData(true, true, keyConnectorUrl, 1, false),
];
organizationService.organizations$.mockReturnValue(of(orgs));
@@ -201,20 +173,8 @@ describe("KeyConnectorService", () => {
it("should return undefined if user is a Provider", async () => {
// Arrange
const orgs = [
organizationData(
true,
true,
"https://key-connector-url.com",
OrganizationUserType.User,
true,
),
organizationData(
false,
true,
"https://key-connector-url.com",
OrganizationUserType.User,
true,
),
organizationData(true, true, keyConnectorUrl, OrganizationUserType.User, true),
organizationData(false, true, keyConnectorUrl, OrganizationUserType.User, true),
];
organizationService.organizations$.mockReturnValue(of(orgs));
@@ -229,7 +189,7 @@ describe("KeyConnectorService", () => {
describe("setMasterKeyFromUrl", () => {
it("should set the master key from the provided URL", async () => {
// Arrange
const url = "https://key-connector-url.com";
const url = keyConnectorUrl;
apiService.getMasterKeyFromKeyConnector.mockResolvedValue(mockMasterKeyResponse);
@@ -247,7 +207,7 @@ describe("KeyConnectorService", () => {
it("should handle errors thrown during the process", async () => {
// Arrange
const url = "https://key-connector-url.com";
const url = keyConnectorUrl;
const error = new Error("Failed to get master key");
apiService.getMasterKeyFromKeyConnector.mockRejectedValue(error);
@@ -267,29 +227,20 @@ describe("KeyConnectorService", () => {
describe("migrateUser", () => {
it("should migrate the user to the key connector", async () => {
// Arrange
const organization = organizationData(
true,
true,
"https://key-connector-url.com",
OrganizationUserType.User,
false,
);
const masterKey = getMockMasterKey();
masterPasswordService.masterKeySubject.next(masterKey);
const keyConnectorRequest = new KeyConnectorUserKeyRequest(
Utils.fromBufferToB64(masterKey.inner().encryptionKey),
);
jest.spyOn(keyConnectorService, "getManagingOrganization").mockResolvedValue(organization);
jest.spyOn(apiService, "postUserKeyToKeyConnector").mockResolvedValue();
// Act
await keyConnectorService.migrateUser(mockUserId);
await keyConnectorService.migrateUser(keyConnectorUrl, mockUserId);
// Assert
expect(keyConnectorService.getManagingOrganization).toHaveBeenCalled();
expect(apiService.postUserKeyToKeyConnector).toHaveBeenCalledWith(
organization.keyConnectorUrl,
keyConnectorUrl,
keyConnectorRequest,
);
expect(apiService.postConvertToKeyConnector).toHaveBeenCalled();
@@ -297,34 +248,23 @@ describe("KeyConnectorService", () => {
it("should handle errors thrown during migration", async () => {
// Arrange
const organization = organizationData(
true,
true,
"https://key-connector-url.com",
OrganizationUserType.User,
false,
);
const masterKey = getMockMasterKey();
const keyConnectorRequest = new KeyConnectorUserKeyRequest(
Utils.fromBufferToB64(masterKey.inner().encryptionKey),
);
const error = new Error("Failed to post user key to key connector");
organizationService.organizations$.mockReturnValue(of([organization]));
masterPasswordService.masterKeySubject.next(masterKey);
jest.spyOn(keyConnectorService, "getManagingOrganization").mockResolvedValue(organization);
const error = new Error("Failed to post user key to key connector");
jest.spyOn(apiService, "postUserKeyToKeyConnector").mockRejectedValue(error);
jest.spyOn(logService, "error");
try {
// Act
await keyConnectorService.migrateUser(mockUserId);
await keyConnectorService.migrateUser(keyConnectorUrl, mockUserId);
} catch {
// Assert
expect(logService.error).toHaveBeenCalledWith(error);
expect(keyConnectorService.getManagingOrganization).toHaveBeenCalled();
expect(apiService.postUserKeyToKeyConnector).toHaveBeenCalledWith(
organization.keyConnectorUrl,
keyConnectorUrl,
keyConnectorRequest,
);
}
@@ -336,7 +276,7 @@ describe("KeyConnectorService", () => {
const organization = organizationData(
true,
true,
"https://key-connector-url.com",
keyConnectorUrl,
OrganizationUserType.User,
false,
);
@@ -364,7 +304,7 @@ describe("KeyConnectorService", () => {
const organization = organizationData(
true,
false,
"https://key-connector-url.com",
keyConnectorUrl,
OrganizationUserType.User,
false,
);
@@ -379,7 +319,7 @@ describe("KeyConnectorService", () => {
const organization = organizationData(
true,
true,
"https://key-connector-url.com",
keyConnectorUrl,
OrganizationUserType.Admin,
false,
);
@@ -394,7 +334,7 @@ describe("KeyConnectorService", () => {
const organization = organizationData(
true,
true,
"https://key-connector-url.com",
keyConnectorUrl,
OrganizationUserType.Owner,
false,
);
@@ -409,7 +349,7 @@ describe("KeyConnectorService", () => {
const organization = organizationData(
true,
true,
"https://key-connector-url.com",
keyConnectorUrl,
OrganizationUserType.User,
true,
);

View File

@@ -90,18 +90,14 @@ export class KeyConnectorService implements KeyConnectorServiceAbstraction {
);
}
async migrateUser(userId: UserId) {
const organization = await this.getManagingOrganization(userId);
async migrateUser(keyConnectorUrl: string, userId: UserId) {
const masterKey = await firstValueFrom(this.masterPasswordService.masterKey$(userId));
const keyConnectorRequest = new KeyConnectorUserKeyRequest(
Utils.fromBufferToB64(masterKey.inner().encryptionKey),
);
try {
await this.apiService.postUserKeyToKeyConnector(
organization.keyConnectorUrl,
keyConnectorRequest,
);
await this.apiService.postUserKeyToKeyConnector(keyConnectorUrl, keyConnectorRequest);
} catch (e) {
this.handleKeyConnectorError(e);
}
@@ -112,9 +108,9 @@ export class KeyConnectorService implements KeyConnectorServiceAbstraction {
}
// TODO: UserKey should be renamed to MasterKey and typed accordingly
async setMasterKeyFromUrl(url: string, userId: UserId) {
async setMasterKeyFromUrl(keyConnectorUrl: string, userId: UserId) {
try {
const masterKeyResponse = await this.apiService.getMasterKeyFromKeyConnector(url);
const masterKeyResponse = await this.apiService.getMasterKeyFromKeyConnector(keyConnectorUrl);
const keyArr = Utils.fromB64ToArray(masterKeyResponse.key);
const masterKey = new SymmetricCryptoKey(keyArr) as MasterKey;
await this.masterPasswordService.setMasterKey(masterKey, userId);
@@ -192,7 +188,7 @@ export class KeyConnectorService implements KeyConnectorServiceAbstraction {
throw new Error("Key Connector error");
}
private findManagingOrganization(organizations: Organization[]) {
private findManagingOrganization(organizations: Organization[]): Organization | undefined {
return organizations.find(
(o) =>
o.keyConnectorEnabled &&