1
0
mirror of https://github.com/bitwarden/browser synced 2025-12-18 09:13:33 +00:00

[PM-18576] Fix missing user id on remove password (#13777)

* 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

* 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 08:51:46 +02:00
committed by GitHub
parent a02c230e4d
commit e73f902aee
17 changed files with 782 additions and 245 deletions

View File

@@ -14,7 +14,6 @@ import { EnvironmentService } from "@bitwarden/common/platform/abstractions/envi
import { Utils } from "@bitwarden/common/platform/misc/utils";
import { ConsoleLogService } from "@bitwarden/common/platform/services/console-log.service";
import { MasterKey } from "@bitwarden/common/types/key";
import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction";
import { KeyService } from "@bitwarden/key-management";
import { ConvertToKeyConnectorCommand } from "../../key-management/convert-to-key-connector.command";
@@ -32,7 +31,6 @@ export class UnlockCommand {
private logService: ConsoleLogService,
private keyConnectorService: KeyConnectorService,
private environmentService: EnvironmentService,
private syncService: SyncService,
private organizationApiService: OrganizationApiServiceAbstraction,
private logout: () => Promise<void>,
) {}
@@ -73,12 +71,11 @@ export class UnlockCommand {
const userKey = await this.masterPasswordService.decryptUserKeyWithMasterKey(masterKey, userId);
await this.keyService.setUserKey(userKey, userId);
if (await this.keyConnectorService.getConvertAccountRequired()) {
if (await firstValueFrom(this.keyConnectorService.convertAccountRequired$)) {
const convertToKeyConnectorCommand = new ConvertToKeyConnectorCommand(
userId,
this.keyConnectorService,
this.environmentService,
this.syncService,
this.organizationApiService,
this.logout,
);

View File

@@ -179,7 +179,6 @@ export abstract class BaseProgram {
this.serviceContainer.logService,
this.serviceContainer.keyConnectorService,
this.serviceContainer.environmentService,
this.serviceContainer.syncService,
this.serviceContainer.organizationApiService,
this.serviceContainer.logout,
);

View File

@@ -0,0 +1,140 @@
import { createPromptModule } from "inquirer";
import { mock } from "jest-mock-extended";
import { of } from "rxjs";
import { OrganizationApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization/organization-api.service.abstraction";
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
import { KeyConnectorService } from "@bitwarden/common/key-management/key-connector/abstractions/key-connector.service";
import {
Environment,
EnvironmentService,
Region,
Urls,
} from "@bitwarden/common/platform/abstractions/environment.service";
import { UserId } from "@bitwarden/common/types/guid";
import { Response } from "../models/response";
import { MessageResponse } from "../models/response/message.response";
import { ConvertToKeyConnectorCommand } from "./convert-to-key-connector.command";
jest.mock("inquirer", () => {
return {
createPromptModule: jest.fn(() => jest.fn(() => Promise.resolve({ convert: "" }))),
};
});
describe("ConvertToKeyConnectorCommand", () => {
let command: ConvertToKeyConnectorCommand;
const userId = "test-user-id" as UserId;
const organization = {
id: "test-organization-id",
name: "Test Organization",
keyConnectorUrl: "https://keyconnector.example.com",
} as Organization;
const keyConnectorService = mock<KeyConnectorService>();
const environmentService = mock<EnvironmentService>();
const organizationApiService = mock<OrganizationApiServiceAbstraction>();
const logout = jest.fn();
beforeEach(async () => {
command = new ConvertToKeyConnectorCommand(
userId,
keyConnectorService,
environmentService,
organizationApiService,
logout,
);
});
describe("run", () => {
it("should logout and return error response if no interaction available", async () => {
process.env.BW_NOINTERACTION = "true";
const response = await command.run();
expect(response).not.toBeNull();
expect(response.success).toEqual(false);
expect(response).toEqual(
Response.error(
new MessageResponse(
"An organization you are a member of is using Key Connector. In order to access the vault, you must opt-in to Key Connector now via the web vault. You have been logged out.",
null,
),
),
);
expect(logout).toHaveBeenCalled();
});
it("should logout and return error response if interaction answer is exit", async () => {
process.env.BW_NOINTERACTION = "false";
keyConnectorService.getManagingOrganization.mockResolvedValue(organization);
(createPromptModule as jest.Mock).mockImplementation(() =>
jest.fn(() => Promise.resolve({ convert: "exit" })),
);
const response = await command.run();
expect(response).not.toBeNull();
expect(response.success).toEqual(false);
expect(response).toEqual(Response.error("You have been logged out."));
expect(logout).toHaveBeenCalled();
});
it("should key connector migrate user and return success response if answer is remove", async () => {
process.env.BW_NOINTERACTION = "false";
keyConnectorService.getManagingOrganization.mockResolvedValue(organization);
environmentService.environment$ = of({
getUrls: () =>
({
keyConnector: "old-key-connector-url",
}) as Urls,
} as Environment);
(createPromptModule as jest.Mock).mockImplementation(() =>
jest.fn(() => Promise.resolve({ convert: "remove" })),
);
const response = await command.run();
expect(response).not.toBeNull();
expect(response.success).toEqual(true);
expect(keyConnectorService.migrateUser).toHaveBeenCalledWith(userId);
expect(environmentService.setEnvironment).toHaveBeenCalledWith(Region.SelfHosted, {
keyConnector: organization.keyConnectorUrl,
} as Urls);
});
it("should logout and throw error if key connector migrate user fails", async () => {
process.env.BW_NOINTERACTION = "false";
keyConnectorService.getManagingOrganization.mockResolvedValue(organization);
(createPromptModule as jest.Mock).mockImplementation(() =>
jest.fn(() => Promise.resolve({ convert: "remove" })),
);
keyConnectorService.migrateUser.mockRejectedValue(new Error("Migration failed"));
await expect(command.run()).rejects.toThrow("Migration failed");
expect(logout).toHaveBeenCalled();
});
it("should leave organization and return success response if answer is leave", async () => {
process.env.BW_NOINTERACTION = "false";
keyConnectorService.getManagingOrganization.mockResolvedValue(organization);
(createPromptModule as jest.Mock).mockImplementation(() =>
jest.fn(() => Promise.resolve({ convert: "leave" })),
);
const response = await command.run();
expect(response).not.toBeNull();
expect(response.success).toEqual(true);
expect(organizationApiService.leave).toHaveBeenCalledWith(organization.id);
});
});
});

View File

@@ -1,5 +1,3 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import * as inquirer from "inquirer";
import { firstValueFrom } from "rxjs";
@@ -10,7 +8,6 @@ import {
Region,
} from "@bitwarden/common/platform/abstractions/environment.service";
import { UserId } from "@bitwarden/common/types/guid";
import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction";
import { Response } from "../models/response";
import { MessageResponse } from "../models/response/message.response";
@@ -20,7 +17,6 @@ export class ConvertToKeyConnectorCommand {
private readonly userId: UserId,
private keyConnectorService: KeyConnectorService,
private environmentService: EnvironmentService,
private syncService: SyncService,
private organizationApiService: OrganizationApiServiceAbstraction,
private logout: () => Promise<void>,
) {}
@@ -39,7 +35,7 @@ export class ConvertToKeyConnectorCommand {
);
}
const organization = await this.keyConnectorService.getManagingOrganization();
const organization = await this.keyConnectorService.getManagingOrganization(this.userId);
const answer: inquirer.Answers = await inquirer.createPromptModule({ output: process.stderr })({
type: "list",
@@ -65,15 +61,12 @@ export class ConvertToKeyConnectorCommand {
if (answer.convert === "remove") {
try {
await this.keyConnectorService.migrateUser();
await this.keyConnectorService.migrateUser(this.userId);
} catch (e) {
await this.logout();
throw e;
}
await this.keyConnectorService.removeConvertAccountRequired();
await this.keyConnectorService.setUsesKeyConnector(true, this.userId);
// Update environment URL - required for api key login
const env = await firstValueFrom(this.environmentService.environment$);
const urls = env.getUrls();
@@ -83,7 +76,6 @@ export class ConvertToKeyConnectorCommand {
return Response.success();
} else if (answer.convert === "leave") {
await this.organizationApiService.leave(organization.id);
await this.keyConnectorService.removeConvertAccountRequired();
return Response.success();
} else {
await this.logout();

View File

@@ -5,11 +5,11 @@ import { BaseResponse } from "./base.response";
export class MessageResponse implements BaseResponse {
object: string;
title: string;
message: string;
message: string | null;
raw: string;
noColor = false;
constructor(title: string, message: string) {
constructor(title: string, message: string | null) {
this.object = "message";
this.title = title;
this.message = message;

View File

@@ -144,7 +144,6 @@ export class OssServeConfigurator {
this.serviceContainer.logService,
this.serviceContainer.keyConnectorService,
this.serviceContainer.environmentService,
this.serviceContainer.syncService,
this.serviceContainer.organizationApiService,
async () => await this.serviceContainer.logout(),
);

View File

@@ -281,7 +281,6 @@ export class Program extends BaseProgram {
this.serviceContainer.logService,
this.serviceContainer.keyConnectorService,
this.serviceContainer.environmentService,
this.serviceContainer.syncService,
this.serviceContainer.organizationApiService,
async () => await this.serviceContainer.logout(),
);