1
0
mirror of https://github.com/bitwarden/browser synced 2025-12-11 13:53:34 +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

@@ -3014,14 +3014,14 @@
"copyCustomFieldNameNotUnique": { "copyCustomFieldNameNotUnique": {
"message": "No unique identifier found." "message": "No unique identifier found."
}, },
"convertOrganizationEncryptionDesc": { "removeMasterPasswordForOrganizationUserKeyConnector": {
"message": "$ORGANIZATION$ is using SSO with a self-hosted key server. A master password is no longer required to log in for members of this organization.", "message": "A master password is no longer required for members of the following organization. Please confirm the domain below with your organization administrator."
"placeholders": { },
"organization": { "organizationName": {
"content": "$1", "message": "Organization name"
"example": "My Org Name" },
} "keyConnectorDomain": {
} "message": "Key Connector domain"
}, },
"leaveOrganization": { "leaveOrganization": {
"message": "Leave organization" "message": "Leave organization"

View File

@@ -15,7 +15,11 @@
</div> </div>
<div class="box-content" *ngIf="!loading"> <div class="box-content" *ngIf="!loading">
<div class="box-content-row" appBoxRow> <div class="box-content-row" appBoxRow>
<p>{{ "convertOrganizationEncryptionDesc" | i18n: organization.name }}</p> <p>{{ "removeMasterPasswordForOrganizationUserKeyConnector" | i18n }}</p>
<p class="tw-mb-0">{{ "organizationName" | i18n }}:</p>
<p class="tw-text-muted tw-mb-6">{{ organization.name }}</p>
<p class="tw-mb-0">{{ "keyConnectorDomain" | i18n }}:</p>
<p class="tw-text-muted tw-mb-6">{{ organization.keyConnectorUrl }}</p>
</div> </div>
<div class="box-content-row"> <div class="box-content-row">
<button type="button" class="btn block primary" (click)="convert()" [disabled]="action"> <button type="button" class="btn block primary" (click)="convert()" [disabled]="action">

View File

@@ -19,6 +19,7 @@ import { KeyService } from "@bitwarden/key-management";
import { ConvertToKeyConnectorCommand } from "../../key-management/convert-to-key-connector.command"; import { ConvertToKeyConnectorCommand } from "../../key-management/convert-to-key-connector.command";
import { Response } from "../../models/response"; import { Response } from "../../models/response";
import { MessageResponse } from "../../models/response/message.response"; import { MessageResponse } from "../../models/response/message.response";
import { I18nService } from "../../platform/services/i18n.service";
import { CliUtils } from "../../utils"; import { CliUtils } from "../../utils";
export class UnlockCommand { export class UnlockCommand {
@@ -33,6 +34,7 @@ export class UnlockCommand {
private environmentService: EnvironmentService, private environmentService: EnvironmentService,
private organizationApiService: OrganizationApiServiceAbstraction, private organizationApiService: OrganizationApiServiceAbstraction,
private logout: () => Promise<void>, private logout: () => Promise<void>,
private i18nService: I18nService,
) {} ) {}
async run(password: string, cmdOptions: Record<string, any>) { async run(password: string, cmdOptions: Record<string, any>) {
@@ -78,6 +80,7 @@ export class UnlockCommand {
this.environmentService, this.environmentService,
this.organizationApiService, this.organizationApiService,
this.logout, this.logout,
this.i18nService,
); );
const convertResponse = await convertToKeyConnectorCommand.run(); const convertResponse = await convertToKeyConnectorCommand.run();
if (!convertResponse.success) { if (!convertResponse.success) {

View File

@@ -181,6 +181,7 @@ export abstract class BaseProgram {
this.serviceContainer.environmentService, this.serviceContainer.environmentService,
this.serviceContainer.organizationApiService, this.serviceContainer.organizationApiService,
this.serviceContainer.logout, this.serviceContainer.logout,
this.serviceContainer.i18nService,
); );
const response = await command.run(null, null); const response = await command.run(null, null);
if (!response.success) { if (!response.success) {

View File

@@ -15,6 +15,7 @@ import { UserId } from "@bitwarden/common/types/guid";
import { Response } from "../models/response"; import { Response } from "../models/response";
import { MessageResponse } from "../models/response/message.response"; import { MessageResponse } from "../models/response/message.response";
import { I18nService } from "../platform/services/i18n.service";
import { ConvertToKeyConnectorCommand } from "./convert-to-key-connector.command"; import { ConvertToKeyConnectorCommand } from "./convert-to-key-connector.command";
@@ -38,6 +39,7 @@ describe("ConvertToKeyConnectorCommand", () => {
const environmentService = mock<EnvironmentService>(); const environmentService = mock<EnvironmentService>();
const organizationApiService = mock<OrganizationApiServiceAbstraction>(); const organizationApiService = mock<OrganizationApiServiceAbstraction>();
const logout = jest.fn(); const logout = jest.fn();
const i18nService = mock<I18nService>();
beforeEach(async () => { beforeEach(async () => {
command = new ConvertToKeyConnectorCommand( command = new ConvertToKeyConnectorCommand(
@@ -46,7 +48,27 @@ describe("ConvertToKeyConnectorCommand", () => {
environmentService, environmentService,
organizationApiService, organizationApiService,
logout, logout,
i18nService,
); );
i18nService.t.mockImplementation((key: string) => {
switch (key) {
case "removeMasterPasswordForOrganizationUserKeyConnector":
return "A master password is no longer required for members of the following organization. Please confirm the domain below with your organization administrator. Organization name: Test Organization. Key Connector domain: https://keyconnector.example.com";
case "removeMasterPasswordAndUnlock":
return "Remove master password and unlock";
case "leaveOrganizationAndUnlock":
return "Leave organization and unlock";
case "logOut":
return "Log out";
case "youHaveBeenLoggedOut":
return "You have been logged out.";
case "organizationUsingKeyConnectorOptInLoggedOut":
return "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.";
default:
return "";
}
});
}); });
describe("run", () => { describe("run", () => {
@@ -73,7 +95,10 @@ describe("ConvertToKeyConnectorCommand", () => {
keyConnectorService.getManagingOrganization.mockResolvedValue(organization); keyConnectorService.getManagingOrganization.mockResolvedValue(organization);
(createPromptModule as jest.Mock).mockImplementation(() => (createPromptModule as jest.Mock).mockImplementation(() =>
jest.fn(() => Promise.resolve({ convert: "exit" })), jest.fn((prompt) => {
assertPrompt(prompt);
return Promise.resolve({ convert: "exit" });
}),
); );
const response = await command.run(); const response = await command.run();
@@ -95,14 +120,20 @@ describe("ConvertToKeyConnectorCommand", () => {
} as Environment); } as Environment);
(createPromptModule as jest.Mock).mockImplementation(() => (createPromptModule as jest.Mock).mockImplementation(() =>
jest.fn(() => Promise.resolve({ convert: "remove" })), jest.fn((prompt) => {
assertPrompt(prompt);
return Promise.resolve({ convert: "remove" });
}),
); );
const response = await command.run(); const response = await command.run();
expect(response).not.toBeNull(); expect(response).not.toBeNull();
expect(response.success).toEqual(true); expect(response.success).toEqual(true);
expect(keyConnectorService.migrateUser).toHaveBeenCalledWith(userId); expect(keyConnectorService.migrateUser).toHaveBeenCalledWith(
organization.keyConnectorUrl,
userId,
);
expect(environmentService.setEnvironment).toHaveBeenCalledWith(Region.SelfHosted, { expect(environmentService.setEnvironment).toHaveBeenCalledWith(Region.SelfHosted, {
keyConnector: organization.keyConnectorUrl, keyConnector: organization.keyConnectorUrl,
} as Urls); } as Urls);
@@ -113,7 +144,10 @@ describe("ConvertToKeyConnectorCommand", () => {
keyConnectorService.getManagingOrganization.mockResolvedValue(organization); keyConnectorService.getManagingOrganization.mockResolvedValue(organization);
(createPromptModule as jest.Mock).mockImplementation(() => (createPromptModule as jest.Mock).mockImplementation(() =>
jest.fn(() => Promise.resolve({ convert: "remove" })), jest.fn((prompt) => {
assertPrompt(prompt);
return Promise.resolve({ convert: "remove" });
}),
); );
keyConnectorService.migrateUser.mockRejectedValue(new Error("Migration failed")); keyConnectorService.migrateUser.mockRejectedValue(new Error("Migration failed"));
@@ -127,7 +161,10 @@ describe("ConvertToKeyConnectorCommand", () => {
keyConnectorService.getManagingOrganization.mockResolvedValue(organization); keyConnectorService.getManagingOrganization.mockResolvedValue(organization);
(createPromptModule as jest.Mock).mockImplementation(() => (createPromptModule as jest.Mock).mockImplementation(() =>
jest.fn(() => Promise.resolve({ convert: "leave" })), jest.fn((prompt) => {
assertPrompt(prompt);
return Promise.resolve({ convert: "leave" });
}),
); );
const response = await command.run(); const response = await command.run();
@@ -136,5 +173,34 @@ describe("ConvertToKeyConnectorCommand", () => {
expect(response.success).toEqual(true); expect(response.success).toEqual(true);
expect(organizationApiService.leave).toHaveBeenCalledWith(organization.id); expect(organizationApiService.leave).toHaveBeenCalledWith(organization.id);
}); });
function assertPrompt(prompt: unknown) {
expect(typeof prompt).toEqual("object");
expect(prompt).toHaveProperty("type");
expect(prompt).toHaveProperty("name");
expect(prompt).toHaveProperty("message");
expect(prompt).toHaveProperty("choices");
const promptObj = prompt as Record<string, unknown>;
expect(promptObj["type"]).toEqual("list");
expect(promptObj["name"]).toEqual("convert");
expect(promptObj["message"]).toEqual(
`A master password is no longer required for members of the following organization. Please confirm the domain below with your organization administrator. Organization name: ${organization.name}. Key Connector domain: ${organization.keyConnectorUrl}`,
);
expect(promptObj["choices"]).toBeInstanceOf(Array);
const choices = promptObj["choices"] as Array<Record<string, unknown>>;
expect(choices).toHaveLength(3);
expect(choices[0]).toEqual({
name: "Remove master password and unlock",
value: "remove",
});
expect(choices[1]).toEqual({
name: "Leave organization and unlock",
value: "leave",
});
expect(choices[2]).toEqual({
name: "Log out",
value: "exit",
});
}
}); });
}); });

View File

@@ -11,6 +11,7 @@ import { UserId } from "@bitwarden/common/types/guid";
import { Response } from "../models/response"; import { Response } from "../models/response";
import { MessageResponse } from "../models/response/message.response"; import { MessageResponse } from "../models/response/message.response";
import { I18nService } from "../platform/services/i18n.service";
export class ConvertToKeyConnectorCommand { export class ConvertToKeyConnectorCommand {
constructor( constructor(
@@ -19,6 +20,7 @@ export class ConvertToKeyConnectorCommand {
private environmentService: EnvironmentService, private environmentService: EnvironmentService,
private organizationApiService: OrganizationApiServiceAbstraction, private organizationApiService: OrganizationApiServiceAbstraction,
private logout: () => Promise<void>, private logout: () => Promise<void>,
private i18nService: I18nService,
) {} ) {}
async run(): Promise<Response> { async run(): Promise<Response> {
@@ -28,8 +30,7 @@ export class ConvertToKeyConnectorCommand {
await this.logout(); await this.logout();
return Response.error( return Response.error(
new MessageResponse( new MessageResponse(
"An organization you are a member of is using Key Connector. " + this.i18nService.t("organizationUsingKeyConnectorOptInLoggedOut"),
"In order to access the vault, you must opt-in to Key Connector now via the web vault. You have been logged out.",
null, null,
), ),
); );
@@ -40,20 +41,22 @@ export class ConvertToKeyConnectorCommand {
const answer: inquirer.Answers = await inquirer.createPromptModule({ output: process.stderr })({ const answer: inquirer.Answers = await inquirer.createPromptModule({ output: process.stderr })({
type: "list", type: "list",
name: "convert", name: "convert",
message: message: this.i18nService.t(
organization.name + "removeMasterPasswordForOrganizationUserKeyConnector",
" is using a self-hosted key server. A master password is no longer required to log in for members of this organization. ", organization.name,
organization.keyConnectorUrl,
),
choices: [ choices: [
{ {
name: "Remove master password and unlock", name: this.i18nService.t("removeMasterPasswordAndUnlock"),
value: "remove", value: "remove",
}, },
{ {
name: "Leave organization and unlock", name: this.i18nService.t("leaveOrganizationAndUnlock"),
value: "leave", value: "leave",
}, },
{ {
name: "Log out", name: this.i18nService.t("logOut"),
value: "exit", value: "exit",
}, },
], ],
@@ -61,7 +64,7 @@ export class ConvertToKeyConnectorCommand {
if (answer.convert === "remove") { if (answer.convert === "remove") {
try { try {
await this.keyConnectorService.migrateUser(this.userId); await this.keyConnectorService.migrateUser(organization.keyConnectorUrl, this.userId);
} catch (e) { } catch (e) {
await this.logout(); await this.logout();
throw e; throw e;
@@ -79,7 +82,7 @@ export class ConvertToKeyConnectorCommand {
return Response.success(); return Response.success();
} else { } else {
await this.logout(); await this.logout();
return Response.error("You have been logged out."); return Response.error(this.i18nService.t("youHaveBeenLoggedOut"));
} }
} }
} }

View File

@@ -184,5 +184,33 @@
"example": "JustTrust.us" "example": "JustTrust.us"
} }
} }
},
"organizationUsingKeyConnectorOptInLoggedOut": {
"message": "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."
},
"removeMasterPasswordForOrganizationUserKeyConnector": {
"message": "A master password is no longer required for members of the following organization. Please confirm the domain below with your organization administrator. Organization name: $ORGANIZATION$. Key Connector domain: $KEYCONNECTORDOMAIN$",
"placeholders": {
"organization": {
"content": "$1",
"example": "My Org Name"
},
"keyConnectorDomain": {
"content": "$2",
"example": "Key Connector domain"
}
}
},
"removeMasterPasswordAndUnlock": {
"message": "Remove master password and unlock"
},
"leaveOrganizationAndUnlock": {
"message": "Leave organization and unlock"
},
"logOut": {
"message": "Log out"
},
"youHaveBeenLoggedOut": {
"message": "You have been logged out."
} }
} }

View File

@@ -146,6 +146,7 @@ export class OssServeConfigurator {
this.serviceContainer.environmentService, this.serviceContainer.environmentService,
this.serviceContainer.organizationApiService, this.serviceContainer.organizationApiService,
async () => await this.serviceContainer.logout(), async () => await this.serviceContainer.logout(),
this.serviceContainer.i18nService,
); );
this.sendCreateCommand = new SendCreateCommand( this.sendCreateCommand = new SendCreateCommand(

View File

@@ -283,6 +283,7 @@ export class Program extends BaseProgram {
this.serviceContainer.environmentService, this.serviceContainer.environmentService,
this.serviceContainer.organizationApiService, this.serviceContainer.organizationApiService,
async () => await this.serviceContainer.logout(), async () => await this.serviceContainer.logout(),
this.serviceContainer.i18nService,
); );
const response = await command.run(password, cmd); const response = await command.run(password, cmd);
this.processResponse(response); this.processResponse(response);

View File

@@ -1,7 +1,11 @@
<div id="remove-password-page" *ngIf="!loading"> <div id="remove-password-page" *ngIf="!loading">
<div class="content"> <div class="content">
<h1>{{ "removeMasterPassword" | i18n }}</h1> <h1>{{ "removeMasterPassword" | i18n }}</h1>
<p>{{ "convertOrganizationEncryptionDesc" | i18n: organization.name }}</p> <p>{{ "removeMasterPasswordForOrganizationUserKeyConnector" | i18n }}</p>
<p class="tw-mb-0">{{ "organizationName" | i18n }}:</p>
<p class="tw-text-muted tw-mb-6">{{ organization.name }}</p>
<p class="tw-mb-0">{{ "keyConnectorDomain" | i18n }}:</p>
<p class="tw-text-muted tw-mb-6">{{ organization.keyConnectorUrl }}</p>
<div class="buttons"> <div class="buttons">
<button type="submit" class="btn primary block" [disabled]="action" (click)="convert()"> <button type="submit" class="btn primary block" [disabled]="action" (click)="convert()">
<b [hidden]="continuing">{{ "removeMasterPassword" | i18n }}</b> <b [hidden]="continuing">{{ "removeMasterPassword" | i18n }}</b>

View File

@@ -2512,14 +2512,14 @@
"removedMasterPassword": { "removedMasterPassword": {
"message": "Master password removed" "message": "Master password removed"
}, },
"convertOrganizationEncryptionDesc": { "removeMasterPasswordForOrganizationUserKeyConnector": {
"message": "$ORGANIZATION$ is using SSO with a self-hosted key server. A master password is no longer required to log in for members of this organization.", "message": "A master password is no longer required for members of the following organization. Please confirm the domain below with your organization administrator."
"placeholders": { },
"organization": { "organizationName": {
"content": "$1", "message": "Organization name"
"example": "My Org Name" },
} "keyConnectorDomain": {
} "message": "Key Connector domain"
}, },
"leaveOrganization": { "leaveOrganization": {
"message": "Leave organization" "message": "Leave organization"

View File

@@ -8,7 +8,11 @@
</div> </div>
<div *ngIf="!loading"> <div *ngIf="!loading">
<p>{{ "convertOrganizationEncryptionDesc" | i18n: organization.name }}</p> <p>{{ "removeMasterPasswordForOrganizationUserKeyConnector" | i18n }}</p>
<p class="tw-mb-0">{{ "organizationName" | i18n }}:</p>
<p class="tw-text-muted tw-mb-6">{{ organization.name }}</p>
<p class="tw-mb-0">{{ "keyConnectorDomain" | i18n }}:</p>
<p class="tw-text-muted tw-mb-6">{{ organization.keyConnectorUrl }}</p>
<button <button
bitButton bitButton

View File

@@ -6477,14 +6477,11 @@
"invalidVerificationCode": { "invalidVerificationCode": {
"message": "Invalid verification code" "message": "Invalid verification code"
}, },
"convertOrganizationEncryptionDesc": { "removeMasterPasswordForOrganizationUserKeyConnector": {
"message": "$ORGANIZATION$ is using SSO with a self-hosted key server. A master password is no longer required to log in for members of this organization.", "message": "A master password is no longer required for members of the following organization. Please confirm the domain below with your organization administrator."
"placeholders": { },
"organization": { "keyConnectorDomain": {
"content": "$1", "message": "Key Connector domain"
"example": "My Org Name"
}
}
}, },
"leaveOrganization": { "leaveOrganization": {
"message": "Leave organization" "message": "Leave organization"

View File

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

View File

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

View File

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

View File

@@ -22,6 +22,7 @@ describe("RemovePasswordComponent", () => {
const organization = { const organization = {
id: "test-organization-id", id: "test-organization-id",
name: "test-organization-name", name: "test-organization-name",
keyConnectorUrl: "https://key-connector-url.com",
} as Organization; } as Organization;
const accountService = mockAccountServiceWith(userId); const accountService = mockAccountServiceWith(userId);
@@ -124,7 +125,10 @@ describe("RemovePasswordComponent", () => {
await component.convert(); await component.convert();
expect(component.continuing).toBe(true); expect(component.continuing).toBe(true);
expect(mockKeyConnectorService.migrateUser).toHaveBeenCalledWith(userId); expect(mockKeyConnectorService.migrateUser).toHaveBeenCalledWith(
organization.keyConnectorUrl,
userId,
);
expect(mockToastService.showToast).toHaveBeenCalledWith({ expect(mockToastService.showToast).toHaveBeenCalledWith({
variant: "success", variant: "success",
message: "removed master password", message: "removed master password",
@@ -140,7 +144,10 @@ describe("RemovePasswordComponent", () => {
await component.convert(); await component.convert();
expect(component.continuing).toBe(false); expect(component.continuing).toBe(false);
expect(mockKeyConnectorService.migrateUser).toHaveBeenCalledWith(userId); expect(mockKeyConnectorService.migrateUser).toHaveBeenCalledWith(
organization.keyConnectorUrl,
userId,
);
expect(mockToastService.showToast).toHaveBeenCalledWith({ expect(mockToastService.showToast).toHaveBeenCalledWith({
variant: "error", variant: "error",
title: "error occurred", title: "error occurred",
@@ -164,7 +171,10 @@ describe("RemovePasswordComponent", () => {
await component.convert(); await component.convert();
expect(component.continuing).toBe(false); expect(component.continuing).toBe(false);
expect(mockKeyConnectorService.migrateUser).toHaveBeenCalledWith(userId); expect(mockKeyConnectorService.migrateUser).toHaveBeenCalledWith(
organization.keyConnectorUrl,
userId,
);
expect(mockToastService.showToast).toHaveBeenCalledWith({ expect(mockToastService.showToast).toHaveBeenCalledWith({
variant: "error", variant: "error",
title: "error occurred", title: "error occurred",

View File

@@ -66,7 +66,10 @@ export class RemovePasswordComponent implements OnInit {
this.continuing = true; this.continuing = true;
try { try {
await this.keyConnectorService.migrateUser(this.activeUserId); await this.keyConnectorService.migrateUser(
this.organization.keyConnectorUrl,
this.activeUserId,
);
this.toastService.showToast({ this.toastService.showToast({
variant: "success", variant: "success",