mirror of
https://github.com/bitwarden/browser
synced 2025-12-06 00:13:28 +00:00
[PM-5269] Key Connector state migration (#8327)
* key connector migration initial * migrator complete * fix dependencies * finalized tests * fix deps and sync main * clean up definition file * fixing tests * fixed tests * fixing CLI, Browser, Desktop builds * fixed factory options * reverting exports * implemented UserKeyDefinition clearOn * Update KeyConnector MIgration * updated migrator and tests to match profile object * removed unused service and updated clear * dep fix * dep fixes * clear usesKeyConnector on logout
This commit is contained in:
@@ -27,9 +27,9 @@ import {
|
|||||||
LogServiceInitOptions,
|
LogServiceInitOptions,
|
||||||
} from "../../../platform/background/service-factories/log-service.factory";
|
} from "../../../platform/background/service-factories/log-service.factory";
|
||||||
import {
|
import {
|
||||||
stateServiceFactory,
|
stateProviderFactory,
|
||||||
StateServiceInitOptions,
|
StateProviderInitOptions,
|
||||||
} from "../../../platform/background/service-factories/state-service.factory";
|
} from "../../../platform/background/service-factories/state-provider.factory";
|
||||||
|
|
||||||
import { TokenServiceInitOptions, tokenServiceFactory } from "./token-service.factory";
|
import { TokenServiceInitOptions, tokenServiceFactory } from "./token-service.factory";
|
||||||
|
|
||||||
@@ -40,13 +40,13 @@ type KeyConnectorServiceFactoryOptions = FactoryOptions & {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export type KeyConnectorServiceInitOptions = KeyConnectorServiceFactoryOptions &
|
export type KeyConnectorServiceInitOptions = KeyConnectorServiceFactoryOptions &
|
||||||
StateServiceInitOptions &
|
|
||||||
CryptoServiceInitOptions &
|
CryptoServiceInitOptions &
|
||||||
ApiServiceInitOptions &
|
ApiServiceInitOptions &
|
||||||
TokenServiceInitOptions &
|
TokenServiceInitOptions &
|
||||||
LogServiceInitOptions &
|
LogServiceInitOptions &
|
||||||
OrganizationServiceInitOptions &
|
OrganizationServiceInitOptions &
|
||||||
KeyGenerationServiceInitOptions;
|
KeyGenerationServiceInitOptions &
|
||||||
|
StateProviderInitOptions;
|
||||||
|
|
||||||
export function keyConnectorServiceFactory(
|
export function keyConnectorServiceFactory(
|
||||||
cache: { keyConnectorService?: AbstractKeyConnectorService } & CachedServices,
|
cache: { keyConnectorService?: AbstractKeyConnectorService } & CachedServices,
|
||||||
@@ -58,7 +58,6 @@ export function keyConnectorServiceFactory(
|
|||||||
opts,
|
opts,
|
||||||
async () =>
|
async () =>
|
||||||
new KeyConnectorService(
|
new KeyConnectorService(
|
||||||
await stateServiceFactory(cache, opts),
|
|
||||||
await cryptoServiceFactory(cache, opts),
|
await cryptoServiceFactory(cache, opts),
|
||||||
await apiServiceFactory(cache, opts),
|
await apiServiceFactory(cache, opts),
|
||||||
await tokenServiceFactory(cache, opts),
|
await tokenServiceFactory(cache, opts),
|
||||||
@@ -66,6 +65,7 @@ export function keyConnectorServiceFactory(
|
|||||||
await organizationServiceFactory(cache, opts),
|
await organizationServiceFactory(cache, opts),
|
||||||
await keyGenerationServiceFactory(cache, opts),
|
await keyGenerationServiceFactory(cache, opts),
|
||||||
opts.keyConnectorServiceOptions.logoutCallback,
|
opts.keyConnectorServiceOptions.logoutCallback,
|
||||||
|
await stateProviderFactory(cache, opts),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ import {
|
|||||||
authServiceFactory,
|
authServiceFactory,
|
||||||
AuthServiceInitOptions,
|
AuthServiceInitOptions,
|
||||||
} from "../../auth/background/service-factories/auth-service.factory";
|
} from "../../auth/background/service-factories/auth-service.factory";
|
||||||
|
import { KeyConnectorServiceInitOptions } from "../../auth/background/service-factories/key-connector-service.factory";
|
||||||
import { userVerificationServiceFactory } from "../../auth/background/service-factories/user-verification-service.factory";
|
import { userVerificationServiceFactory } from "../../auth/background/service-factories/user-verification-service.factory";
|
||||||
import { openUnlockPopout } from "../../auth/popup/utils/auth-popout-window";
|
import { openUnlockPopout } from "../../auth/popup/utils/auth-popout-window";
|
||||||
import { autofillSettingsServiceFactory } from "../../autofill/background/service_factories/autofill-settings-service.factory";
|
import { autofillSettingsServiceFactory } from "../../autofill/background/service_factories/autofill-settings-service.factory";
|
||||||
@@ -78,7 +79,9 @@ export class ContextMenuClickedHandler {
|
|||||||
|
|
||||||
static async mv3Create(cachedServices: CachedServices) {
|
static async mv3Create(cachedServices: CachedServices) {
|
||||||
const stateFactory = new StateFactory(GlobalState, Account);
|
const stateFactory = new StateFactory(GlobalState, Account);
|
||||||
const serviceOptions: AuthServiceInitOptions & CipherServiceInitOptions = {
|
const serviceOptions: AuthServiceInitOptions &
|
||||||
|
CipherServiceInitOptions &
|
||||||
|
KeyConnectorServiceInitOptions = {
|
||||||
apiServiceOptions: {
|
apiServiceOptions: {
|
||||||
logoutCallback: NOT_IMPLEMENTED,
|
logoutCallback: NOT_IMPLEMENTED,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -514,7 +514,6 @@ export default class MainBackground {
|
|||||||
this.badgeSettingsService = new BadgeSettingsService(this.stateProvider);
|
this.badgeSettingsService = new BadgeSettingsService(this.stateProvider);
|
||||||
this.policyApiService = new PolicyApiService(this.policyService, this.apiService);
|
this.policyApiService = new PolicyApiService(this.policyService, this.apiService);
|
||||||
this.keyConnectorService = new KeyConnectorService(
|
this.keyConnectorService = new KeyConnectorService(
|
||||||
this.stateService,
|
|
||||||
this.cryptoService,
|
this.cryptoService,
|
||||||
this.apiService,
|
this.apiService,
|
||||||
this.tokenService,
|
this.tokenService,
|
||||||
@@ -522,6 +521,7 @@ export default class MainBackground {
|
|||||||
this.organizationService,
|
this.organizationService,
|
||||||
this.keyGenerationService,
|
this.keyGenerationService,
|
||||||
logoutCallback,
|
logoutCallback,
|
||||||
|
this.stateProvider,
|
||||||
);
|
);
|
||||||
|
|
||||||
this.passwordStrengthService = new PasswordStrengthService();
|
this.passwordStrengthService = new PasswordStrengthService();
|
||||||
@@ -1125,7 +1125,6 @@ export default class MainBackground {
|
|||||||
this.policyService.clear(userId),
|
this.policyService.clear(userId),
|
||||||
this.passwordGenerationService.clear(userId),
|
this.passwordGenerationService.clear(userId),
|
||||||
this.vaultTimeoutSettingsService.clear(userId),
|
this.vaultTimeoutSettingsService.clear(userId),
|
||||||
this.keyConnectorService.clear(),
|
|
||||||
this.vaultFilterService.clear(),
|
this.vaultFilterService.clear(),
|
||||||
this.biometricStateService.logout(userId),
|
this.biometricStateService.logout(userId),
|
||||||
this.providerService.save(null, userId),
|
this.providerService.save(null, userId),
|
||||||
|
|||||||
@@ -427,7 +427,6 @@ export class Main {
|
|||||||
this.policyApiService = new PolicyApiService(this.policyService, this.apiService);
|
this.policyApiService = new PolicyApiService(this.policyService, this.apiService);
|
||||||
|
|
||||||
this.keyConnectorService = new KeyConnectorService(
|
this.keyConnectorService = new KeyConnectorService(
|
||||||
this.stateService,
|
|
||||||
this.cryptoService,
|
this.cryptoService,
|
||||||
this.apiService,
|
this.apiService,
|
||||||
this.tokenService,
|
this.tokenService,
|
||||||
@@ -435,6 +434,7 @@ export class Main {
|
|||||||
this.organizationService,
|
this.organizationService,
|
||||||
this.keyGenerationService,
|
this.keyGenerationService,
|
||||||
async (expired: boolean) => await this.logout(),
|
async (expired: boolean) => await this.logout(),
|
||||||
|
this.stateProvider,
|
||||||
);
|
);
|
||||||
|
|
||||||
this.twoFactorService = new TwoFactorService(this.i18nService, this.platformUtilsService);
|
this.twoFactorService = new TwoFactorService(this.i18nService, this.platformUtilsService);
|
||||||
|
|||||||
@@ -584,7 +584,6 @@ export class AppComponent implements OnInit, OnDestroy {
|
|||||||
await this.passwordGenerationService.clear(userBeingLoggedOut);
|
await this.passwordGenerationService.clear(userBeingLoggedOut);
|
||||||
await this.vaultTimeoutSettingsService.clear(userBeingLoggedOut);
|
await this.vaultTimeoutSettingsService.clear(userBeingLoggedOut);
|
||||||
await this.policyService.clear(userBeingLoggedOut);
|
await this.policyService.clear(userBeingLoggedOut);
|
||||||
await this.keyConnectorService.clear();
|
|
||||||
await this.biometricStateService.logout(userBeingLoggedOut as UserId);
|
await this.biometricStateService.logout(userBeingLoggedOut as UserId);
|
||||||
await this.providerService.save(null, userBeingLoggedOut as UserId);
|
await this.providerService.save(null, userBeingLoggedOut as UserId);
|
||||||
|
|
||||||
|
|||||||
@@ -276,7 +276,6 @@ export class AppComponent implements OnDestroy, OnInit {
|
|||||||
this.collectionService.clear(userId),
|
this.collectionService.clear(userId),
|
||||||
this.policyService.clear(userId),
|
this.policyService.clear(userId),
|
||||||
this.passwordGenerationService.clear(),
|
this.passwordGenerationService.clear(),
|
||||||
this.keyConnectorService.clear(),
|
|
||||||
this.biometricStateService.logout(userId as UserId),
|
this.biometricStateService.logout(userId as UserId),
|
||||||
this.paymentMethodWarningService.clear(),
|
this.paymentMethodWarningService.clear(),
|
||||||
]);
|
]);
|
||||||
|
|||||||
@@ -765,7 +765,6 @@ const safeProviders: SafeProvider[] = [
|
|||||||
provide: KeyConnectorServiceAbstraction,
|
provide: KeyConnectorServiceAbstraction,
|
||||||
useClass: KeyConnectorService,
|
useClass: KeyConnectorService,
|
||||||
deps: [
|
deps: [
|
||||||
StateServiceAbstraction,
|
|
||||||
CryptoServiceAbstraction,
|
CryptoServiceAbstraction,
|
||||||
ApiServiceAbstraction,
|
ApiServiceAbstraction,
|
||||||
TokenServiceAbstraction,
|
TokenServiceAbstraction,
|
||||||
@@ -773,6 +772,7 @@ const safeProviders: SafeProvider[] = [
|
|||||||
OrganizationServiceAbstraction,
|
OrganizationServiceAbstraction,
|
||||||
KeyGenerationServiceAbstraction,
|
KeyGenerationServiceAbstraction,
|
||||||
LOGOUT_CALLBACK,
|
LOGOUT_CALLBACK,
|
||||||
|
StateProvider,
|
||||||
],
|
],
|
||||||
}),
|
}),
|
||||||
safeProvider({
|
safeProvider({
|
||||||
|
|||||||
@@ -15,5 +15,4 @@ export abstract class KeyConnectorService {
|
|||||||
setConvertAccountRequired: (status: boolean) => Promise<void>;
|
setConvertAccountRequired: (status: boolean) => Promise<void>;
|
||||||
getConvertAccountRequired: () => Promise<boolean>;
|
getConvertAccountRequired: () => Promise<boolean>;
|
||||||
removeConvertAccountRequired: () => Promise<void>;
|
removeConvertAccountRequired: () => Promise<void>;
|
||||||
clear: () => Promise<void>;
|
|
||||||
}
|
}
|
||||||
|
|||||||
376
libs/common/src/auth/services/key-connector.service.spec.ts
Normal file
376
libs/common/src/auth/services/key-connector.service.spec.ts
Normal file
@@ -0,0 +1,376 @@
|
|||||||
|
import { mock } from "jest-mock-extended";
|
||||||
|
|
||||||
|
import { FakeAccountService, FakeStateProvider, mockAccountServiceWith } from "../../../spec";
|
||||||
|
import { ApiService } from "../../abstractions/api.service";
|
||||||
|
import { OrganizationService } from "../../admin-console/abstractions/organization/organization.service.abstraction";
|
||||||
|
import { OrganizationData } from "../../admin-console/models/data/organization.data";
|
||||||
|
import { Organization } from "../../admin-console/models/domain/organization";
|
||||||
|
import { ProfileOrganizationResponse } from "../../admin-console/models/response/profile-organization.response";
|
||||||
|
import { CryptoService } from "../../platform/abstractions/crypto.service";
|
||||||
|
import { LogService } from "../../platform/abstractions/log.service";
|
||||||
|
import { Utils } from "../../platform/misc/utils";
|
||||||
|
import { SymmetricCryptoKey } from "../../platform/models/domain/symmetric-crypto-key";
|
||||||
|
import { KeyGenerationService } from "../../platform/services/key-generation.service";
|
||||||
|
import { OrganizationId, UserId } from "../../types/guid";
|
||||||
|
import { MasterKey } from "../../types/key";
|
||||||
|
import { KeyConnectorUserKeyRequest } from "../models/request/key-connector-user-key.request";
|
||||||
|
import { KeyConnectorUserKeyResponse } from "../models/response/key-connector-user-key.response";
|
||||||
|
|
||||||
|
import {
|
||||||
|
USES_KEY_CONNECTOR,
|
||||||
|
CONVERT_ACCOUNT_TO_KEY_CONNECTOR,
|
||||||
|
KeyConnectorService,
|
||||||
|
} from "./key-connector.service";
|
||||||
|
import { TokenService } from "./token.service";
|
||||||
|
|
||||||
|
describe("KeyConnectorService", () => {
|
||||||
|
let keyConnectorService: KeyConnectorService;
|
||||||
|
|
||||||
|
const cryptoService = mock<CryptoService>();
|
||||||
|
const apiService = mock<ApiService>();
|
||||||
|
const tokenService = mock<TokenService>();
|
||||||
|
const logService = mock<LogService>();
|
||||||
|
const organizationService = mock<OrganizationService>();
|
||||||
|
const keyGenerationService = mock<KeyGenerationService>();
|
||||||
|
|
||||||
|
let stateProvider: FakeStateProvider;
|
||||||
|
|
||||||
|
let accountService: FakeAccountService;
|
||||||
|
|
||||||
|
const mockUserId = Utils.newGuid() as UserId;
|
||||||
|
const mockOrgId = Utils.newGuid() as OrganizationId;
|
||||||
|
|
||||||
|
const mockMasterKeyResponse: KeyConnectorUserKeyResponse = new KeyConnectorUserKeyResponse({
|
||||||
|
key: "eO9nVlVl3I3sU6O+CyK0kEkpGtl/auT84Hig2WTXmZtDTqYtKpDvUPfjhgMOHf+KQzx++TVS2AOLYq856Caa7w==",
|
||||||
|
});
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
jest.clearAllMocks();
|
||||||
|
|
||||||
|
accountService = mockAccountServiceWith(mockUserId);
|
||||||
|
stateProvider = new FakeStateProvider(accountService);
|
||||||
|
|
||||||
|
keyConnectorService = new KeyConnectorService(
|
||||||
|
cryptoService,
|
||||||
|
apiService,
|
||||||
|
tokenService,
|
||||||
|
logService,
|
||||||
|
organizationService,
|
||||||
|
keyGenerationService,
|
||||||
|
async () => {},
|
||||||
|
stateProvider,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("instantiates", () => {
|
||||||
|
expect(keyConnectorService).not.toBeFalsy();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("setUsesKeyConnector()", () => {
|
||||||
|
it("should update the usesKeyConnectorState with the provided value", async () => {
|
||||||
|
const state = stateProvider.activeUser.getFake(USES_KEY_CONNECTOR);
|
||||||
|
state.nextState(false);
|
||||||
|
|
||||||
|
const newValue = true;
|
||||||
|
|
||||||
|
await keyConnectorService.setUsesKeyConnector(newValue);
|
||||||
|
|
||||||
|
expect(await keyConnectorService.getUsesKeyConnector()).toBe(newValue);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("getManagingOrganization()", () => {
|
||||||
|
it("should return the managing organization with key connector enabled", async () => {
|
||||||
|
// Arrange
|
||||||
|
const orgs = [
|
||||||
|
organizationData(true, true, "https://key-connector-url.com", 2, false),
|
||||||
|
organizationData(false, true, "https://key-connector-url.com", 2, false),
|
||||||
|
organizationData(true, false, "https://key-connector-url.com", 2, false),
|
||||||
|
organizationData(true, true, "https://other-url.com", 2, false),
|
||||||
|
];
|
||||||
|
organizationService.getAll.mockResolvedValue(orgs);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const result = await keyConnectorService.getManagingOrganization();
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(result).toEqual(orgs[0]);
|
||||||
|
});
|
||||||
|
|
||||||
|
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", 2, false),
|
||||||
|
organizationData(false, false, "https://key-connector-url.com", 2, false),
|
||||||
|
];
|
||||||
|
organizationService.getAll.mockResolvedValue(orgs);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const result = await keyConnectorService.getManagingOrganization();
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(result).toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
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),
|
||||||
|
];
|
||||||
|
organizationService.getAll.mockResolvedValue(orgs);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const result = await keyConnectorService.getManagingOrganization();
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(result).toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return undefined if user is a Provider", async () => {
|
||||||
|
// Arrange
|
||||||
|
const orgs = [
|
||||||
|
organizationData(true, true, "https://key-connector-url.com", 2, true),
|
||||||
|
organizationData(false, true, "https://key-connector-url.com", 2, true),
|
||||||
|
];
|
||||||
|
organizationService.getAll.mockResolvedValue(orgs);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const result = await keyConnectorService.getManagingOrganization();
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(result).toBeUndefined();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("setConvertAccountRequired()", () => {
|
||||||
|
it("should update the convertAccountToKeyConnectorState with the provided value", async () => {
|
||||||
|
const state = stateProvider.activeUser.getFake(CONVERT_ACCOUNT_TO_KEY_CONNECTOR);
|
||||||
|
state.nextState(false);
|
||||||
|
|
||||||
|
const newValue = true;
|
||||||
|
|
||||||
|
await keyConnectorService.setConvertAccountRequired(newValue);
|
||||||
|
|
||||||
|
expect(await keyConnectorService.getConvertAccountRequired()).toBe(newValue);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should remove the convertAccountToKeyConnectorState", async () => {
|
||||||
|
const state = stateProvider.activeUser.getFake(CONVERT_ACCOUNT_TO_KEY_CONNECTOR);
|
||||||
|
state.nextState(false);
|
||||||
|
|
||||||
|
const newValue: boolean = null;
|
||||||
|
|
||||||
|
await keyConnectorService.setConvertAccountRequired(newValue);
|
||||||
|
|
||||||
|
expect(await keyConnectorService.getConvertAccountRequired()).toBe(newValue);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("userNeedsMigration()", () => {
|
||||||
|
it("should return true if the user needs migration", async () => {
|
||||||
|
// token
|
||||||
|
tokenService.getIsExternal.mockResolvedValue(true);
|
||||||
|
|
||||||
|
// create organization object
|
||||||
|
const data = organizationData(true, true, "https://key-connector-url.com", 2, false);
|
||||||
|
organizationService.getAll.mockResolvedValue([data]);
|
||||||
|
|
||||||
|
// uses KeyConnector
|
||||||
|
const state = stateProvider.activeUser.getFake(USES_KEY_CONNECTOR);
|
||||||
|
state.nextState(false);
|
||||||
|
|
||||||
|
const result = await keyConnectorService.userNeedsMigration();
|
||||||
|
|
||||||
|
expect(result).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return false if the user does not need migration", async () => {
|
||||||
|
tokenService.getIsExternal.mockResolvedValue(false);
|
||||||
|
const data = organizationData(false, false, "https://key-connector-url.com", 2, false);
|
||||||
|
organizationService.getAll.mockResolvedValue([data]);
|
||||||
|
|
||||||
|
const state = stateProvider.activeUser.getFake(USES_KEY_CONNECTOR);
|
||||||
|
state.nextState(true);
|
||||||
|
const result = await keyConnectorService.userNeedsMigration();
|
||||||
|
|
||||||
|
expect(result).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("setMasterKeyFromUrl", () => {
|
||||||
|
it("should set the master key from the provided URL", async () => {
|
||||||
|
// Arrange
|
||||||
|
const url = "https://key-connector-url.com";
|
||||||
|
|
||||||
|
apiService.getMasterKeyFromKeyConnector.mockResolvedValue(mockMasterKeyResponse);
|
||||||
|
|
||||||
|
// Hard to mock these, but we can generate the same keys
|
||||||
|
const keyArr = Utils.fromB64ToArray(mockMasterKeyResponse.key);
|
||||||
|
const masterKey = new SymmetricCryptoKey(keyArr) as MasterKey;
|
||||||
|
|
||||||
|
// Act
|
||||||
|
await keyConnectorService.setMasterKeyFromUrl(url);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(apiService.getMasterKeyFromKeyConnector).toHaveBeenCalledWith(url);
|
||||||
|
expect(cryptoService.setMasterKey).toHaveBeenCalledWith(masterKey);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should handle errors thrown during the process", async () => {
|
||||||
|
// Arrange
|
||||||
|
const url = "https://key-connector-url.com";
|
||||||
|
|
||||||
|
const error = new Error("Failed to get master key");
|
||||||
|
apiService.getMasterKeyFromKeyConnector.mockRejectedValue(error);
|
||||||
|
jest.spyOn(logService, "error");
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Act
|
||||||
|
await keyConnectorService.setMasterKeyFromUrl(url);
|
||||||
|
} catch {
|
||||||
|
// Assert
|
||||||
|
expect(logService.error).toHaveBeenCalledWith(error);
|
||||||
|
expect(apiService.getMasterKeyFromKeyConnector).toHaveBeenCalledWith(url);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("migrateUser()", () => {
|
||||||
|
it("should migrate the user to the key connector", async () => {
|
||||||
|
// Arrange
|
||||||
|
const organization = organizationData(true, true, "https://key-connector-url.com", 2, false);
|
||||||
|
const masterKey = getMockMasterKey();
|
||||||
|
const keyConnectorRequest = new KeyConnectorUserKeyRequest(masterKey.encKeyB64);
|
||||||
|
|
||||||
|
jest.spyOn(keyConnectorService, "getManagingOrganization").mockResolvedValue(organization);
|
||||||
|
jest.spyOn(cryptoService, "getMasterKey").mockResolvedValue(masterKey);
|
||||||
|
jest.spyOn(apiService, "postUserKeyToKeyConnector").mockResolvedValue();
|
||||||
|
|
||||||
|
// Act
|
||||||
|
await keyConnectorService.migrateUser();
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(keyConnectorService.getManagingOrganization).toHaveBeenCalled();
|
||||||
|
expect(cryptoService.getMasterKey).toHaveBeenCalled();
|
||||||
|
expect(apiService.postUserKeyToKeyConnector).toHaveBeenCalledWith(
|
||||||
|
organization.keyConnectorUrl,
|
||||||
|
keyConnectorRequest,
|
||||||
|
);
|
||||||
|
expect(apiService.postConvertToKeyConnector).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should handle errors thrown during migration", async () => {
|
||||||
|
// Arrange
|
||||||
|
const organization = organizationData(true, true, "https://key-connector-url.com", 2, false);
|
||||||
|
const masterKey = getMockMasterKey();
|
||||||
|
const keyConnectorRequest = new KeyConnectorUserKeyRequest(masterKey.encKeyB64);
|
||||||
|
const error = new Error("Failed to post user key to key connector");
|
||||||
|
organizationService.getAll.mockResolvedValue([organization]);
|
||||||
|
|
||||||
|
jest.spyOn(keyConnectorService, "getManagingOrganization").mockResolvedValue(organization);
|
||||||
|
jest.spyOn(cryptoService, "getMasterKey").mockResolvedValue(masterKey);
|
||||||
|
jest.spyOn(apiService, "postUserKeyToKeyConnector").mockRejectedValue(error);
|
||||||
|
jest.spyOn(logService, "error");
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Act
|
||||||
|
await keyConnectorService.migrateUser();
|
||||||
|
} catch {
|
||||||
|
// Assert
|
||||||
|
expect(logService.error).toHaveBeenCalledWith(error);
|
||||||
|
expect(keyConnectorService.getManagingOrganization).toHaveBeenCalled();
|
||||||
|
expect(cryptoService.getMasterKey).toHaveBeenCalled();
|
||||||
|
expect(apiService.postUserKeyToKeyConnector).toHaveBeenCalledWith(
|
||||||
|
organization.keyConnectorUrl,
|
||||||
|
keyConnectorRequest,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
function organizationData(
|
||||||
|
usesKeyConnector: boolean,
|
||||||
|
keyConnectorEnabled: boolean,
|
||||||
|
keyConnectorUrl: string,
|
||||||
|
userType: number,
|
||||||
|
isProviderUser: boolean,
|
||||||
|
): Organization {
|
||||||
|
return new Organization(
|
||||||
|
new OrganizationData(
|
||||||
|
new ProfileOrganizationResponse({
|
||||||
|
id: mockOrgId,
|
||||||
|
name: "TEST_KEY_CONNECTOR_ORG",
|
||||||
|
usePolicies: true,
|
||||||
|
useSso: true,
|
||||||
|
useKeyConnector: usesKeyConnector,
|
||||||
|
useScim: true,
|
||||||
|
useGroups: true,
|
||||||
|
useDirectory: true,
|
||||||
|
useEvents: true,
|
||||||
|
useTotp: true,
|
||||||
|
use2fa: true,
|
||||||
|
useApi: true,
|
||||||
|
useResetPassword: true,
|
||||||
|
useSecretsManager: true,
|
||||||
|
usePasswordManager: true,
|
||||||
|
usersGetPremium: true,
|
||||||
|
useCustomPermissions: true,
|
||||||
|
useActivateAutofillPolicy: true,
|
||||||
|
selfHost: true,
|
||||||
|
seats: 5,
|
||||||
|
maxCollections: null,
|
||||||
|
maxStorageGb: 1,
|
||||||
|
key: "super-secret-key",
|
||||||
|
status: 2,
|
||||||
|
type: userType,
|
||||||
|
enabled: true,
|
||||||
|
ssoBound: true,
|
||||||
|
identifier: "TEST_KEY_CONNECTOR_ORG",
|
||||||
|
permissions: {
|
||||||
|
accessEventLogs: false,
|
||||||
|
accessImportExport: false,
|
||||||
|
accessReports: false,
|
||||||
|
createNewCollections: false,
|
||||||
|
editAnyCollection: false,
|
||||||
|
deleteAnyCollection: false,
|
||||||
|
editAssignedCollections: false,
|
||||||
|
deleteAssignedCollections: false,
|
||||||
|
manageGroups: false,
|
||||||
|
managePolicies: false,
|
||||||
|
manageSso: false,
|
||||||
|
manageUsers: false,
|
||||||
|
manageResetPassword: false,
|
||||||
|
manageScim: false,
|
||||||
|
},
|
||||||
|
resetPasswordEnrolled: true,
|
||||||
|
userId: mockUserId,
|
||||||
|
hasPublicAndPrivateKeys: true,
|
||||||
|
providerId: null,
|
||||||
|
providerName: null,
|
||||||
|
providerType: null,
|
||||||
|
familySponsorshipFriendlyName: null,
|
||||||
|
familySponsorshipAvailable: true,
|
||||||
|
planProductType: 3,
|
||||||
|
KeyConnectorEnabled: keyConnectorEnabled,
|
||||||
|
KeyConnectorUrl: keyConnectorUrl,
|
||||||
|
familySponsorshipLastSyncDate: null,
|
||||||
|
familySponsorshipValidUntil: null,
|
||||||
|
familySponsorshipToDelete: null,
|
||||||
|
accessSecretsManager: false,
|
||||||
|
limitCollectionCreationDeletion: true,
|
||||||
|
allowAdminAccessToAllCollectionItems: true,
|
||||||
|
flexibleCollections: false,
|
||||||
|
object: "profileOrganization",
|
||||||
|
}),
|
||||||
|
{ isMember: true, isProviderUser: isProviderUser },
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getMockMasterKey(): MasterKey {
|
||||||
|
const keyArr = Utils.fromB64ToArray(mockMasterKeyResponse.key);
|
||||||
|
const masterKey = new SymmetricCryptoKey(keyArr) as MasterKey;
|
||||||
|
return masterKey;
|
||||||
|
}
|
||||||
|
});
|
||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import { firstValueFrom } from "rxjs";
|
||||||
|
|
||||||
import { ApiService } from "../../abstractions/api.service";
|
import { ApiService } from "../../abstractions/api.service";
|
||||||
import { OrganizationService } from "../../admin-console/abstractions/organization/organization.service.abstraction";
|
import { OrganizationService } from "../../admin-console/abstractions/organization/organization.service.abstraction";
|
||||||
import { OrganizationUserType } from "../../admin-console/enums";
|
import { OrganizationUserType } from "../../admin-console/enums";
|
||||||
@@ -5,9 +7,14 @@ import { KeysRequest } from "../../models/request/keys.request";
|
|||||||
import { CryptoService } from "../../platform/abstractions/crypto.service";
|
import { CryptoService } from "../../platform/abstractions/crypto.service";
|
||||||
import { KeyGenerationService } from "../../platform/abstractions/key-generation.service";
|
import { KeyGenerationService } from "../../platform/abstractions/key-generation.service";
|
||||||
import { LogService } from "../../platform/abstractions/log.service";
|
import { LogService } from "../../platform/abstractions/log.service";
|
||||||
import { StateService } from "../../platform/abstractions/state.service";
|
|
||||||
import { Utils } from "../../platform/misc/utils";
|
import { Utils } from "../../platform/misc/utils";
|
||||||
import { SymmetricCryptoKey } from "../../platform/models/domain/symmetric-crypto-key";
|
import { SymmetricCryptoKey } from "../../platform/models/domain/symmetric-crypto-key";
|
||||||
|
import {
|
||||||
|
ActiveUserState,
|
||||||
|
KEY_CONNECTOR_DISK,
|
||||||
|
StateProvider,
|
||||||
|
UserKeyDefinition,
|
||||||
|
} from "../../platform/state";
|
||||||
import { MasterKey } from "../../types/key";
|
import { MasterKey } from "../../types/key";
|
||||||
import { KeyConnectorService as KeyConnectorServiceAbstraction } from "../abstractions/key-connector.service";
|
import { KeyConnectorService as KeyConnectorServiceAbstraction } from "../abstractions/key-connector.service";
|
||||||
import { TokenService } from "../abstractions/token.service";
|
import { TokenService } from "../abstractions/token.service";
|
||||||
@@ -16,9 +23,28 @@ import { KeyConnectorUserKeyRequest } from "../models/request/key-connector-user
|
|||||||
import { SetKeyConnectorKeyRequest } from "../models/request/set-key-connector-key.request";
|
import { SetKeyConnectorKeyRequest } from "../models/request/set-key-connector-key.request";
|
||||||
import { IdentityTokenResponse } from "../models/response/identity-token.response";
|
import { IdentityTokenResponse } from "../models/response/identity-token.response";
|
||||||
|
|
||||||
|
export const USES_KEY_CONNECTOR = new UserKeyDefinition<boolean>(
|
||||||
|
KEY_CONNECTOR_DISK,
|
||||||
|
"usesKeyConnector",
|
||||||
|
{
|
||||||
|
deserializer: (usesKeyConnector) => usesKeyConnector,
|
||||||
|
clearOn: ["logout"],
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
export const CONVERT_ACCOUNT_TO_KEY_CONNECTOR = new UserKeyDefinition<boolean>(
|
||||||
|
KEY_CONNECTOR_DISK,
|
||||||
|
"convertAccountToKeyConnector",
|
||||||
|
{
|
||||||
|
deserializer: (convertAccountToKeyConnector) => convertAccountToKeyConnector,
|
||||||
|
clearOn: ["logout"],
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
export class KeyConnectorService implements KeyConnectorServiceAbstraction {
|
export class KeyConnectorService implements KeyConnectorServiceAbstraction {
|
||||||
|
private usesKeyConnectorState: ActiveUserState<boolean>;
|
||||||
|
private convertAccountToKeyConnectorState: ActiveUserState<boolean>;
|
||||||
constructor(
|
constructor(
|
||||||
private stateService: StateService,
|
|
||||||
private cryptoService: CryptoService,
|
private cryptoService: CryptoService,
|
||||||
private apiService: ApiService,
|
private apiService: ApiService,
|
||||||
private tokenService: TokenService,
|
private tokenService: TokenService,
|
||||||
@@ -26,14 +52,20 @@ export class KeyConnectorService implements KeyConnectorServiceAbstraction {
|
|||||||
private organizationService: OrganizationService,
|
private organizationService: OrganizationService,
|
||||||
private keyGenerationService: KeyGenerationService,
|
private keyGenerationService: KeyGenerationService,
|
||||||
private logoutCallback: (expired: boolean, userId?: string) => Promise<void>,
|
private logoutCallback: (expired: boolean, userId?: string) => Promise<void>,
|
||||||
) {}
|
private stateProvider: StateProvider,
|
||||||
|
) {
|
||||||
setUsesKeyConnector(usesKeyConnector: boolean) {
|
this.usesKeyConnectorState = this.stateProvider.getActive(USES_KEY_CONNECTOR);
|
||||||
return this.stateService.setUsesKeyConnector(usesKeyConnector);
|
this.convertAccountToKeyConnectorState = this.stateProvider.getActive(
|
||||||
|
CONVERT_ACCOUNT_TO_KEY_CONNECTOR,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
async getUsesKeyConnector(): Promise<boolean> {
|
async setUsesKeyConnector(usesKeyConnector: boolean) {
|
||||||
return await this.stateService.getUsesKeyConnector();
|
await this.usesKeyConnectorState.update(() => usesKeyConnector);
|
||||||
|
}
|
||||||
|
|
||||||
|
getUsesKeyConnector(): Promise<boolean> {
|
||||||
|
return firstValueFrom(this.usesKeyConnectorState.state$);
|
||||||
}
|
}
|
||||||
|
|
||||||
async userNeedsMigration() {
|
async userNeedsMigration() {
|
||||||
@@ -132,19 +164,15 @@ export class KeyConnectorService implements KeyConnectorServiceAbstraction {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async setConvertAccountRequired(status: boolean) {
|
async setConvertAccountRequired(status: boolean) {
|
||||||
await this.stateService.setConvertAccountToKeyConnector(status);
|
await this.convertAccountToKeyConnectorState.update(() => status);
|
||||||
}
|
}
|
||||||
|
|
||||||
async getConvertAccountRequired(): Promise<boolean> {
|
getConvertAccountRequired(): Promise<boolean> {
|
||||||
return await this.stateService.getConvertAccountToKeyConnector();
|
return firstValueFrom(this.convertAccountToKeyConnectorState.state$);
|
||||||
}
|
}
|
||||||
|
|
||||||
async removeConvertAccountRequired() {
|
async removeConvertAccountRequired() {
|
||||||
await this.stateService.setConvertAccountToKeyConnector(null);
|
await this.setConvertAccountRequired(null);
|
||||||
}
|
|
||||||
|
|
||||||
async clear() {
|
|
||||||
await this.removeConvertAccountRequired();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private handleKeyConnectorError(e: any) {
|
private handleKeyConnectorError(e: any) {
|
||||||
|
|||||||
@@ -52,8 +52,6 @@ export abstract class StateService<T extends Account = Account> {
|
|||||||
setAddEditCipherInfo: (value: AddEditCipherInfo, options?: StorageOptions) => Promise<void>;
|
setAddEditCipherInfo: (value: AddEditCipherInfo, options?: StorageOptions) => Promise<void>;
|
||||||
getBiometricFingerprintValidated: (options?: StorageOptions) => Promise<boolean>;
|
getBiometricFingerprintValidated: (options?: StorageOptions) => Promise<boolean>;
|
||||||
setBiometricFingerprintValidated: (value: boolean, options?: StorageOptions) => Promise<void>;
|
setBiometricFingerprintValidated: (value: boolean, options?: StorageOptions) => Promise<void>;
|
||||||
getConvertAccountToKeyConnector: (options?: StorageOptions) => Promise<boolean>;
|
|
||||||
setConvertAccountToKeyConnector: (value: boolean, options?: StorageOptions) => Promise<void>;
|
|
||||||
/**
|
/**
|
||||||
* Gets the user's master key
|
* Gets the user's master key
|
||||||
*/
|
*/
|
||||||
@@ -269,8 +267,6 @@ export abstract class StateService<T extends Account = Account> {
|
|||||||
getSecurityStamp: (options?: StorageOptions) => Promise<string>;
|
getSecurityStamp: (options?: StorageOptions) => Promise<string>;
|
||||||
setSecurityStamp: (value: string, options?: StorageOptions) => Promise<void>;
|
setSecurityStamp: (value: string, options?: StorageOptions) => Promise<void>;
|
||||||
getUserId: (options?: StorageOptions) => Promise<string>;
|
getUserId: (options?: StorageOptions) => Promise<string>;
|
||||||
getUsesKeyConnector: (options?: StorageOptions) => Promise<boolean>;
|
|
||||||
setUsesKeyConnector: (value: boolean, options?: StorageOptions) => Promise<void>;
|
|
||||||
getVaultTimeout: (options?: StorageOptions) => Promise<number>;
|
getVaultTimeout: (options?: StorageOptions) => Promise<number>;
|
||||||
setVaultTimeout: (value: number, options?: StorageOptions) => Promise<void>;
|
setVaultTimeout: (value: number, options?: StorageOptions) => Promise<void>;
|
||||||
getVaultTimeoutAction: (options?: StorageOptions) => Promise<string>;
|
getVaultTimeoutAction: (options?: StorageOptions) => Promise<string>;
|
||||||
|
|||||||
@@ -158,7 +158,6 @@ export class AccountKeys {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export class AccountProfile {
|
export class AccountProfile {
|
||||||
convertAccountToKeyConnector?: boolean;
|
|
||||||
name?: string;
|
name?: string;
|
||||||
email?: string;
|
email?: string;
|
||||||
emailVerified?: boolean;
|
emailVerified?: boolean;
|
||||||
@@ -166,7 +165,6 @@ export class AccountProfile {
|
|||||||
forceSetPasswordReason?: ForceSetPasswordReason;
|
forceSetPasswordReason?: ForceSetPasswordReason;
|
||||||
lastSync?: string;
|
lastSync?: string;
|
||||||
userId?: string;
|
userId?: string;
|
||||||
usesKeyConnector?: boolean;
|
|
||||||
keyHash?: string;
|
keyHash?: string;
|
||||||
kdfIterations?: number;
|
kdfIterations?: number;
|
||||||
kdfMemory?: number;
|
kdfMemory?: number;
|
||||||
|
|||||||
@@ -293,23 +293,6 @@ export class StateService<
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
async getConvertAccountToKeyConnector(options?: StorageOptions): Promise<boolean> {
|
|
||||||
return (
|
|
||||||
await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskOptions()))
|
|
||||||
)?.profile?.convertAccountToKeyConnector;
|
|
||||||
}
|
|
||||||
|
|
||||||
async setConvertAccountToKeyConnector(value: boolean, options?: StorageOptions): Promise<void> {
|
|
||||||
const account = await this.getAccount(
|
|
||||||
this.reconcileOptions(options, await this.defaultOnDiskOptions()),
|
|
||||||
);
|
|
||||||
account.profile.convertAccountToKeyConnector = value;
|
|
||||||
await this.saveAccount(
|
|
||||||
account,
|
|
||||||
this.reconcileOptions(options, await this.defaultOnDiskOptions()),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @deprecated Do not save the Master Key. Use the User Symmetric Key instead
|
* @deprecated Do not save the Master Key. Use the User Symmetric Key instead
|
||||||
*/
|
*/
|
||||||
@@ -1298,23 +1281,6 @@ export class StateService<
|
|||||||
)?.profile?.userId;
|
)?.profile?.userId;
|
||||||
}
|
}
|
||||||
|
|
||||||
async getUsesKeyConnector(options?: StorageOptions): Promise<boolean> {
|
|
||||||
return (
|
|
||||||
await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskOptions()))
|
|
||||||
)?.profile?.usesKeyConnector;
|
|
||||||
}
|
|
||||||
|
|
||||||
async setUsesKeyConnector(value: boolean, options?: StorageOptions): Promise<void> {
|
|
||||||
const account = await this.getAccount(
|
|
||||||
this.reconcileOptions(options, await this.defaultOnDiskOptions()),
|
|
||||||
);
|
|
||||||
account.profile.usesKeyConnector = value;
|
|
||||||
await this.saveAccount(
|
|
||||||
account,
|
|
||||||
this.reconcileOptions(options, await this.defaultOnDiskOptions()),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
async getVaultTimeout(options?: StorageOptions): Promise<number> {
|
async getVaultTimeout(options?: StorageOptions): Promise<number> {
|
||||||
const accountVaultTimeout = (
|
const accountVaultTimeout = (
|
||||||
await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskLocalOptions()))
|
await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskLocalOptions()))
|
||||||
|
|||||||
@@ -35,6 +35,7 @@ export const BILLING_DISK = new StateDefinition("billing", "disk");
|
|||||||
|
|
||||||
// Auth
|
// Auth
|
||||||
|
|
||||||
|
export const KEY_CONNECTOR_DISK = new StateDefinition("keyConnector", "disk");
|
||||||
export const ACCOUNT_MEMORY = new StateDefinition("account", "memory");
|
export const ACCOUNT_MEMORY = new StateDefinition("account", "memory");
|
||||||
export const AVATAR_DISK = new StateDefinition("avatar", "disk", { web: "disk-local" });
|
export const AVATAR_DISK = new StateDefinition("avatar", "disk", { web: "disk-local" });
|
||||||
export const SSO_DISK = new StateDefinition("ssoLogin", "disk");
|
export const SSO_DISK = new StateDefinition("ssoLogin", "disk");
|
||||||
|
|||||||
@@ -46,6 +46,7 @@ import { MoveDesktopSettingsMigrator } from "./migrations/47-move-desktop-settin
|
|||||||
import { MoveDdgToStateProviderMigrator } from "./migrations/48-move-ddg-to-state-provider";
|
import { MoveDdgToStateProviderMigrator } from "./migrations/48-move-ddg-to-state-provider";
|
||||||
import { AccountServerConfigMigrator } from "./migrations/49-move-account-server-configs";
|
import { AccountServerConfigMigrator } from "./migrations/49-move-account-server-configs";
|
||||||
import { AddKeyTypeToOrgKeysMigrator } from "./migrations/5-add-key-type-to-org-keys";
|
import { AddKeyTypeToOrgKeysMigrator } from "./migrations/5-add-key-type-to-org-keys";
|
||||||
|
import { KeyConnectorMigrator } from "./migrations/50-move-key-connector-to-state-provider";
|
||||||
import { RemoveLegacyEtmKeyMigrator } from "./migrations/6-remove-legacy-etm-key";
|
import { RemoveLegacyEtmKeyMigrator } from "./migrations/6-remove-legacy-etm-key";
|
||||||
import { MoveBiometricAutoPromptToAccount } from "./migrations/7-move-biometric-auto-prompt-to-account";
|
import { MoveBiometricAutoPromptToAccount } from "./migrations/7-move-biometric-auto-prompt-to-account";
|
||||||
import { MoveStateVersionMigrator } from "./migrations/8-move-state-version";
|
import { MoveStateVersionMigrator } from "./migrations/8-move-state-version";
|
||||||
@@ -53,7 +54,8 @@ import { MoveBrowserSettingsToGlobal } from "./migrations/9-move-browser-setting
|
|||||||
import { MinVersionMigrator } from "./migrations/min-version";
|
import { MinVersionMigrator } from "./migrations/min-version";
|
||||||
|
|
||||||
export const MIN_VERSION = 3;
|
export const MIN_VERSION = 3;
|
||||||
export const CURRENT_VERSION = 49;
|
export const CURRENT_VERSION = 50;
|
||||||
|
|
||||||
export type MinVersion = typeof MIN_VERSION;
|
export type MinVersion = typeof MIN_VERSION;
|
||||||
|
|
||||||
export function createMigrationBuilder() {
|
export function createMigrationBuilder() {
|
||||||
@@ -104,7 +106,8 @@ export function createMigrationBuilder() {
|
|||||||
.with(DeleteBiometricPromptCancelledData, 45, 46)
|
.with(DeleteBiometricPromptCancelledData, 45, 46)
|
||||||
.with(MoveDesktopSettingsMigrator, 46, 47)
|
.with(MoveDesktopSettingsMigrator, 46, 47)
|
||||||
.with(MoveDdgToStateProviderMigrator, 47, 48)
|
.with(MoveDdgToStateProviderMigrator, 47, 48)
|
||||||
.with(AccountServerConfigMigrator, 48, CURRENT_VERSION);
|
.with(AccountServerConfigMigrator, 48, 49)
|
||||||
|
.with(KeyConnectorMigrator, 49, CURRENT_VERSION);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function currentVersion(
|
export async function currentVersion(
|
||||||
|
|||||||
@@ -0,0 +1,174 @@
|
|||||||
|
import { MockProxy } from "jest-mock-extended";
|
||||||
|
|
||||||
|
import { KeyDefinitionLike, MigrationHelper } from "../migration-helper";
|
||||||
|
import { mockMigrationHelper } from "../migration-helper.spec";
|
||||||
|
|
||||||
|
import { KeyConnectorMigrator } from "./50-move-key-connector-to-state-provider";
|
||||||
|
|
||||||
|
function exampleJSON() {
|
||||||
|
return {
|
||||||
|
global: {
|
||||||
|
otherStuff: "otherStuff1",
|
||||||
|
},
|
||||||
|
authenticatedAccounts: ["FirstAccount", "SecondAccount", "ThirdAccount"],
|
||||||
|
FirstAccount: {
|
||||||
|
profile: {
|
||||||
|
usesKeyConnector: true,
|
||||||
|
convertAccountToKeyConnector: false,
|
||||||
|
otherStuff: "otherStuff2",
|
||||||
|
},
|
||||||
|
otherStuff: "otherStuff3",
|
||||||
|
},
|
||||||
|
SecondAccount: {
|
||||||
|
profile: {
|
||||||
|
usesKeyConnector: true,
|
||||||
|
convertAccountToKeyConnector: true,
|
||||||
|
otherStuff: "otherStuff4",
|
||||||
|
},
|
||||||
|
otherStuff: "otherStuff5",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function rollbackJSON() {
|
||||||
|
return {
|
||||||
|
user_FirstAccount_keyConnector_usesKeyConnector: true,
|
||||||
|
user_FirstAccount_keyConnector_convertAccountToKeyConnector: false,
|
||||||
|
user_SecondAccount_keyConnector_usesKeyConnector: true,
|
||||||
|
user_SecondAccount_keyConnector_convertAccountToKeyConnector: true,
|
||||||
|
global: {
|
||||||
|
otherStuff: "otherStuff1",
|
||||||
|
},
|
||||||
|
authenticatedAccounts: ["FirstAccount", "SecondAccount", "ThirdAccount"],
|
||||||
|
FirstAccount: {
|
||||||
|
profile: {
|
||||||
|
otherStuff: "otherStuff2",
|
||||||
|
},
|
||||||
|
otherStuff: "otherStuff3",
|
||||||
|
},
|
||||||
|
SecondAccount: {
|
||||||
|
profile: {
|
||||||
|
otherStuff: "otherStuff4",
|
||||||
|
},
|
||||||
|
otherStuff: "otherStuff5",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const usesKeyConnectorKeyDefinition: KeyDefinitionLike = {
|
||||||
|
key: "usesKeyConnector",
|
||||||
|
stateDefinition: {
|
||||||
|
name: "keyConnector",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const convertAccountToKeyConnectorKeyDefinition: KeyDefinitionLike = {
|
||||||
|
key: "convertAccountToKeyConnector",
|
||||||
|
stateDefinition: {
|
||||||
|
name: "keyConnector",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
describe("KeyConnectorMigrator", () => {
|
||||||
|
let helper: MockProxy<MigrationHelper>;
|
||||||
|
let sut: KeyConnectorMigrator;
|
||||||
|
|
||||||
|
describe("migrate", () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
helper = mockMigrationHelper(exampleJSON(), 50);
|
||||||
|
sut = new KeyConnectorMigrator(49, 50);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should remove usesKeyConnector and convertAccountToKeyConnector from Profile", async () => {
|
||||||
|
await sut.migrate(helper);
|
||||||
|
|
||||||
|
// Set is called 2 times even though there are 3 accounts. Since the target properties don't exist in ThirdAccount, they are not set.
|
||||||
|
expect(helper.set).toHaveBeenCalledTimes(2);
|
||||||
|
expect(helper.set).toHaveBeenCalledWith("FirstAccount", {
|
||||||
|
profile: {
|
||||||
|
otherStuff: "otherStuff2",
|
||||||
|
},
|
||||||
|
otherStuff: "otherStuff3",
|
||||||
|
});
|
||||||
|
expect(helper.setToUser).toHaveBeenCalledWith(
|
||||||
|
"FirstAccount",
|
||||||
|
usesKeyConnectorKeyDefinition,
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
expect(helper.setToUser).toHaveBeenCalledWith(
|
||||||
|
"FirstAccount",
|
||||||
|
convertAccountToKeyConnectorKeyDefinition,
|
||||||
|
false,
|
||||||
|
);
|
||||||
|
expect(helper.set).toHaveBeenCalledWith("SecondAccount", {
|
||||||
|
profile: {
|
||||||
|
otherStuff: "otherStuff4",
|
||||||
|
},
|
||||||
|
otherStuff: "otherStuff5",
|
||||||
|
});
|
||||||
|
expect(helper.setToUser).toHaveBeenCalledWith(
|
||||||
|
"SecondAccount",
|
||||||
|
usesKeyConnectorKeyDefinition,
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
expect(helper.setToUser).toHaveBeenCalledWith(
|
||||||
|
"SecondAccount",
|
||||||
|
convertAccountToKeyConnectorKeyDefinition,
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
expect(helper.setToUser).not.toHaveBeenCalledWith("ThirdAccount");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("rollback", () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
helper = mockMigrationHelper(rollbackJSON(), 50);
|
||||||
|
sut = new KeyConnectorMigrator(49, 50);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should null out new usesKeyConnector global value", async () => {
|
||||||
|
await sut.rollback(helper);
|
||||||
|
|
||||||
|
expect(helper.setToUser).toHaveBeenCalledTimes(4);
|
||||||
|
expect(helper.set).toHaveBeenCalledTimes(2);
|
||||||
|
expect(helper.setToUser).toHaveBeenCalledWith(
|
||||||
|
"FirstAccount",
|
||||||
|
usesKeyConnectorKeyDefinition,
|
||||||
|
null,
|
||||||
|
);
|
||||||
|
expect(helper.setToUser).toHaveBeenCalledWith(
|
||||||
|
"FirstAccount",
|
||||||
|
convertAccountToKeyConnectorKeyDefinition,
|
||||||
|
null,
|
||||||
|
);
|
||||||
|
expect(helper.set).toHaveBeenCalledWith("FirstAccount", {
|
||||||
|
profile: {
|
||||||
|
usesKeyConnector: true,
|
||||||
|
convertAccountToKeyConnector: false,
|
||||||
|
otherStuff: "otherStuff2",
|
||||||
|
},
|
||||||
|
otherStuff: "otherStuff3",
|
||||||
|
});
|
||||||
|
expect(helper.setToUser).toHaveBeenCalledWith(
|
||||||
|
"SecondAccount",
|
||||||
|
usesKeyConnectorKeyDefinition,
|
||||||
|
null,
|
||||||
|
);
|
||||||
|
expect(helper.setToUser).toHaveBeenCalledWith(
|
||||||
|
"SecondAccount",
|
||||||
|
convertAccountToKeyConnectorKeyDefinition,
|
||||||
|
null,
|
||||||
|
);
|
||||||
|
expect(helper.set).toHaveBeenCalledWith("SecondAccount", {
|
||||||
|
profile: {
|
||||||
|
usesKeyConnector: true,
|
||||||
|
convertAccountToKeyConnector: true,
|
||||||
|
otherStuff: "otherStuff4",
|
||||||
|
},
|
||||||
|
otherStuff: "otherStuff5",
|
||||||
|
});
|
||||||
|
expect(helper.setToUser).not.toHaveBeenCalledWith("ThirdAccount");
|
||||||
|
expect(helper.set).not.toHaveBeenCalledWith("ThirdAccount");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,78 @@
|
|||||||
|
import { KeyDefinitionLike, MigrationHelper } from "../migration-helper";
|
||||||
|
import { Migrator } from "../migrator";
|
||||||
|
|
||||||
|
type ExpectedAccountType = {
|
||||||
|
profile?: {
|
||||||
|
usesKeyConnector?: boolean;
|
||||||
|
convertAccountToKeyConnector?: boolean;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const usesKeyConnectorKeyDefinition: KeyDefinitionLike = {
|
||||||
|
key: "usesKeyConnector",
|
||||||
|
stateDefinition: {
|
||||||
|
name: "keyConnector",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const convertAccountToKeyConnectorKeyDefinition: KeyDefinitionLike = {
|
||||||
|
key: "convertAccountToKeyConnector",
|
||||||
|
stateDefinition: {
|
||||||
|
name: "keyConnector",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export class KeyConnectorMigrator extends Migrator<49, 50> {
|
||||||
|
async migrate(helper: MigrationHelper): Promise<void> {
|
||||||
|
const accounts = await helper.getAccounts<ExpectedAccountType>();
|
||||||
|
async function migrateAccount(userId: string, account: ExpectedAccountType): Promise<void> {
|
||||||
|
const usesKeyConnector = account?.profile?.usesKeyConnector;
|
||||||
|
const convertAccountToKeyConnector = account?.profile?.convertAccountToKeyConnector;
|
||||||
|
if (usesKeyConnector == null && convertAccountToKeyConnector == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (usesKeyConnector != null) {
|
||||||
|
await helper.setToUser(userId, usesKeyConnectorKeyDefinition, usesKeyConnector);
|
||||||
|
delete account.profile.usesKeyConnector;
|
||||||
|
}
|
||||||
|
if (convertAccountToKeyConnector != null) {
|
||||||
|
await helper.setToUser(
|
||||||
|
userId,
|
||||||
|
convertAccountToKeyConnectorKeyDefinition,
|
||||||
|
convertAccountToKeyConnector,
|
||||||
|
);
|
||||||
|
delete account.profile.convertAccountToKeyConnector;
|
||||||
|
}
|
||||||
|
await helper.set(userId, account);
|
||||||
|
}
|
||||||
|
await Promise.all([...accounts.map(({ userId, account }) => migrateAccount(userId, account))]);
|
||||||
|
}
|
||||||
|
|
||||||
|
async rollback(helper: MigrationHelper): Promise<void> {
|
||||||
|
const accounts = await helper.getAccounts<ExpectedAccountType>();
|
||||||
|
async function rollbackAccount(userId: string, account: ExpectedAccountType): Promise<void> {
|
||||||
|
const usesKeyConnector: boolean = await helper.getFromUser(
|
||||||
|
userId,
|
||||||
|
usesKeyConnectorKeyDefinition,
|
||||||
|
);
|
||||||
|
const convertAccountToKeyConnector: boolean = await helper.getFromUser(
|
||||||
|
userId,
|
||||||
|
convertAccountToKeyConnectorKeyDefinition,
|
||||||
|
);
|
||||||
|
if (usesKeyConnector == null && convertAccountToKeyConnector == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (usesKeyConnector != null) {
|
||||||
|
account.profile.usesKeyConnector = usesKeyConnector;
|
||||||
|
await helper.setToUser(userId, usesKeyConnectorKeyDefinition, null);
|
||||||
|
}
|
||||||
|
if (convertAccountToKeyConnector != null) {
|
||||||
|
account.profile.convertAccountToKeyConnector = convertAccountToKeyConnector;
|
||||||
|
await helper.setToUser(userId, convertAccountToKeyConnectorKeyDefinition, null);
|
||||||
|
}
|
||||||
|
await helper.set(userId, account);
|
||||||
|
}
|
||||||
|
|
||||||
|
await Promise.all([...accounts.map(({ userId, account }) => rollbackAccount(userId, account))]);
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user