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:
@@ -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,
|
||||
);
|
||||
|
||||
@@ -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,
|
||||
);
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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();
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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(),
|
||||
);
|
||||
|
||||
@@ -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(),
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user