[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:
parent
df058ba399
commit
3d19e3489c
|
@ -27,9 +27,9 @@ import {
|
|||
LogServiceInitOptions,
|
||||
} from "../../../platform/background/service-factories/log-service.factory";
|
||||
import {
|
||||
stateServiceFactory,
|
||||
StateServiceInitOptions,
|
||||
} from "../../../platform/background/service-factories/state-service.factory";
|
||||
stateProviderFactory,
|
||||
StateProviderInitOptions,
|
||||
} from "../../../platform/background/service-factories/state-provider.factory";
|
||||
|
||||
import { TokenServiceInitOptions, tokenServiceFactory } from "./token-service.factory";
|
||||
|
||||
|
@ -40,13 +40,13 @@ type KeyConnectorServiceFactoryOptions = FactoryOptions & {
|
|||
};
|
||||
|
||||
export type KeyConnectorServiceInitOptions = KeyConnectorServiceFactoryOptions &
|
||||
StateServiceInitOptions &
|
||||
CryptoServiceInitOptions &
|
||||
ApiServiceInitOptions &
|
||||
TokenServiceInitOptions &
|
||||
LogServiceInitOptions &
|
||||
OrganizationServiceInitOptions &
|
||||
KeyGenerationServiceInitOptions;
|
||||
KeyGenerationServiceInitOptions &
|
||||
StateProviderInitOptions;
|
||||
|
||||
export function keyConnectorServiceFactory(
|
||||
cache: { keyConnectorService?: AbstractKeyConnectorService } & CachedServices,
|
||||
|
@ -58,7 +58,6 @@ export function keyConnectorServiceFactory(
|
|||
opts,
|
||||
async () =>
|
||||
new KeyConnectorService(
|
||||
await stateServiceFactory(cache, opts),
|
||||
await cryptoServiceFactory(cache, opts),
|
||||
await apiServiceFactory(cache, opts),
|
||||
await tokenServiceFactory(cache, opts),
|
||||
|
@ -66,6 +65,7 @@ export function keyConnectorServiceFactory(
|
|||
await organizationServiceFactory(cache, opts),
|
||||
await keyGenerationServiceFactory(cache, opts),
|
||||
opts.keyConnectorServiceOptions.logoutCallback,
|
||||
await stateProviderFactory(cache, opts),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
|
|
@ -30,6 +30,7 @@ import {
|
|||
authServiceFactory,
|
||||
AuthServiceInitOptions,
|
||||
} 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 { openUnlockPopout } from "../../auth/popup/utils/auth-popout-window";
|
||||
import { autofillSettingsServiceFactory } from "../../autofill/background/service_factories/autofill-settings-service.factory";
|
||||
|
@ -78,7 +79,9 @@ export class ContextMenuClickedHandler {
|
|||
|
||||
static async mv3Create(cachedServices: CachedServices) {
|
||||
const stateFactory = new StateFactory(GlobalState, Account);
|
||||
const serviceOptions: AuthServiceInitOptions & CipherServiceInitOptions = {
|
||||
const serviceOptions: AuthServiceInitOptions &
|
||||
CipherServiceInitOptions &
|
||||
KeyConnectorServiceInitOptions = {
|
||||
apiServiceOptions: {
|
||||
logoutCallback: NOT_IMPLEMENTED,
|
||||
},
|
||||
|
|
|
@ -514,7 +514,6 @@ export default class MainBackground {
|
|||
this.badgeSettingsService = new BadgeSettingsService(this.stateProvider);
|
||||
this.policyApiService = new PolicyApiService(this.policyService, this.apiService);
|
||||
this.keyConnectorService = new KeyConnectorService(
|
||||
this.stateService,
|
||||
this.cryptoService,
|
||||
this.apiService,
|
||||
this.tokenService,
|
||||
|
@ -522,6 +521,7 @@ export default class MainBackground {
|
|||
this.organizationService,
|
||||
this.keyGenerationService,
|
||||
logoutCallback,
|
||||
this.stateProvider,
|
||||
);
|
||||
|
||||
this.passwordStrengthService = new PasswordStrengthService();
|
||||
|
@ -1125,7 +1125,6 @@ export default class MainBackground {
|
|||
this.policyService.clear(userId),
|
||||
this.passwordGenerationService.clear(userId),
|
||||
this.vaultTimeoutSettingsService.clear(userId),
|
||||
this.keyConnectorService.clear(),
|
||||
this.vaultFilterService.clear(),
|
||||
this.biometricStateService.logout(userId),
|
||||
this.providerService.save(null, userId),
|
||||
|
|
|
@ -427,7 +427,6 @@ export class Main {
|
|||
this.policyApiService = new PolicyApiService(this.policyService, this.apiService);
|
||||
|
||||
this.keyConnectorService = new KeyConnectorService(
|
||||
this.stateService,
|
||||
this.cryptoService,
|
||||
this.apiService,
|
||||
this.tokenService,
|
||||
|
@ -435,6 +434,7 @@ export class Main {
|
|||
this.organizationService,
|
||||
this.keyGenerationService,
|
||||
async (expired: boolean) => await this.logout(),
|
||||
this.stateProvider,
|
||||
);
|
||||
|
||||
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.vaultTimeoutSettingsService.clear(userBeingLoggedOut);
|
||||
await this.policyService.clear(userBeingLoggedOut);
|
||||
await this.keyConnectorService.clear();
|
||||
await this.biometricStateService.logout(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.policyService.clear(userId),
|
||||
this.passwordGenerationService.clear(),
|
||||
this.keyConnectorService.clear(),
|
||||
this.biometricStateService.logout(userId as UserId),
|
||||
this.paymentMethodWarningService.clear(),
|
||||
]);
|
||||
|
|
|
@ -765,7 +765,6 @@ const safeProviders: SafeProvider[] = [
|
|||
provide: KeyConnectorServiceAbstraction,
|
||||
useClass: KeyConnectorService,
|
||||
deps: [
|
||||
StateServiceAbstraction,
|
||||
CryptoServiceAbstraction,
|
||||
ApiServiceAbstraction,
|
||||
TokenServiceAbstraction,
|
||||
|
@ -773,6 +772,7 @@ const safeProviders: SafeProvider[] = [
|
|||
OrganizationServiceAbstraction,
|
||||
KeyGenerationServiceAbstraction,
|
||||
LOGOUT_CALLBACK,
|
||||
StateProvider,
|
||||
],
|
||||
}),
|
||||
safeProvider({
|
||||
|
|
|
@ -15,5 +15,4 @@ export abstract class KeyConnectorService {
|
|||
setConvertAccountRequired: (status: boolean) => Promise<void>;
|
||||
getConvertAccountRequired: () => Promise<boolean>;
|
||||
removeConvertAccountRequired: () => Promise<void>;
|
||||
clear: () => Promise<void>;
|
||||
}
|
||||
|
|
|
@ -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 { OrganizationService } from "../../admin-console/abstractions/organization/organization.service.abstraction";
|
||||
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 { KeyGenerationService } from "../../platform/abstractions/key-generation.service";
|
||||
import { LogService } from "../../platform/abstractions/log.service";
|
||||
import { StateService } from "../../platform/abstractions/state.service";
|
||||
import { Utils } from "../../platform/misc/utils";
|
||||
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 { KeyConnectorService as KeyConnectorServiceAbstraction } from "../abstractions/key-connector.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 { 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 {
|
||||
private usesKeyConnectorState: ActiveUserState<boolean>;
|
||||
private convertAccountToKeyConnectorState: ActiveUserState<boolean>;
|
||||
constructor(
|
||||
private stateService: StateService,
|
||||
private cryptoService: CryptoService,
|
||||
private apiService: ApiService,
|
||||
private tokenService: TokenService,
|
||||
|
@ -26,14 +52,20 @@ export class KeyConnectorService implements KeyConnectorServiceAbstraction {
|
|||
private organizationService: OrganizationService,
|
||||
private keyGenerationService: KeyGenerationService,
|
||||
private logoutCallback: (expired: boolean, userId?: string) => Promise<void>,
|
||||
) {}
|
||||
|
||||
setUsesKeyConnector(usesKeyConnector: boolean) {
|
||||
return this.stateService.setUsesKeyConnector(usesKeyConnector);
|
||||
private stateProvider: StateProvider,
|
||||
) {
|
||||
this.usesKeyConnectorState = this.stateProvider.getActive(USES_KEY_CONNECTOR);
|
||||
this.convertAccountToKeyConnectorState = this.stateProvider.getActive(
|
||||
CONVERT_ACCOUNT_TO_KEY_CONNECTOR,
|
||||
);
|
||||
}
|
||||
|
||||
async getUsesKeyConnector(): Promise<boolean> {
|
||||
return await this.stateService.getUsesKeyConnector();
|
||||
async setUsesKeyConnector(usesKeyConnector: boolean) {
|
||||
await this.usesKeyConnectorState.update(() => usesKeyConnector);
|
||||
}
|
||||
|
||||
getUsesKeyConnector(): Promise<boolean> {
|
||||
return firstValueFrom(this.usesKeyConnectorState.state$);
|
||||
}
|
||||
|
||||
async userNeedsMigration() {
|
||||
|
@ -132,19 +164,15 @@ export class KeyConnectorService implements KeyConnectorServiceAbstraction {
|
|||
}
|
||||
|
||||
async setConvertAccountRequired(status: boolean) {
|
||||
await this.stateService.setConvertAccountToKeyConnector(status);
|
||||
await this.convertAccountToKeyConnectorState.update(() => status);
|
||||
}
|
||||
|
||||
async getConvertAccountRequired(): Promise<boolean> {
|
||||
return await this.stateService.getConvertAccountToKeyConnector();
|
||||
getConvertAccountRequired(): Promise<boolean> {
|
||||
return firstValueFrom(this.convertAccountToKeyConnectorState.state$);
|
||||
}
|
||||
|
||||
async removeConvertAccountRequired() {
|
||||
await this.stateService.setConvertAccountToKeyConnector(null);
|
||||
}
|
||||
|
||||
async clear() {
|
||||
await this.removeConvertAccountRequired();
|
||||
await this.setConvertAccountRequired(null);
|
||||
}
|
||||
|
||||
private handleKeyConnectorError(e: any) {
|
||||
|
|
|
@ -52,8 +52,6 @@ export abstract class StateService<T extends Account = Account> {
|
|||
setAddEditCipherInfo: (value: AddEditCipherInfo, options?: StorageOptions) => Promise<void>;
|
||||
getBiometricFingerprintValidated: (options?: StorageOptions) => Promise<boolean>;
|
||||
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
|
||||
*/
|
||||
|
@ -269,8 +267,6 @@ export abstract class StateService<T extends Account = Account> {
|
|||
getSecurityStamp: (options?: StorageOptions) => Promise<string>;
|
||||
setSecurityStamp: (value: string, options?: StorageOptions) => Promise<void>;
|
||||
getUserId: (options?: StorageOptions) => Promise<string>;
|
||||
getUsesKeyConnector: (options?: StorageOptions) => Promise<boolean>;
|
||||
setUsesKeyConnector: (value: boolean, options?: StorageOptions) => Promise<void>;
|
||||
getVaultTimeout: (options?: StorageOptions) => Promise<number>;
|
||||
setVaultTimeout: (value: number, options?: StorageOptions) => Promise<void>;
|
||||
getVaultTimeoutAction: (options?: StorageOptions) => Promise<string>;
|
||||
|
|
|
@ -158,7 +158,6 @@ export class AccountKeys {
|
|||
}
|
||||
|
||||
export class AccountProfile {
|
||||
convertAccountToKeyConnector?: boolean;
|
||||
name?: string;
|
||||
email?: string;
|
||||
emailVerified?: boolean;
|
||||
|
@ -166,7 +165,6 @@ export class AccountProfile {
|
|||
forceSetPasswordReason?: ForceSetPasswordReason;
|
||||
lastSync?: string;
|
||||
userId?: string;
|
||||
usesKeyConnector?: boolean;
|
||||
keyHash?: string;
|
||||
kdfIterations?: 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
|
||||
*/
|
||||
|
@ -1298,23 +1281,6 @@ export class StateService<
|
|||
)?.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> {
|
||||
const accountVaultTimeout = (
|
||||
await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskLocalOptions()))
|
||||
|
|
|
@ -35,6 +35,7 @@ export const BILLING_DISK = new StateDefinition("billing", "disk");
|
|||
|
||||
// Auth
|
||||
|
||||
export const KEY_CONNECTOR_DISK = new StateDefinition("keyConnector", "disk");
|
||||
export const ACCOUNT_MEMORY = new StateDefinition("account", "memory");
|
||||
export const AVATAR_DISK = new StateDefinition("avatar", "disk", { web: "disk-local" });
|
||||
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 { AccountServerConfigMigrator } from "./migrations/49-move-account-server-configs";
|
||||
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 { MoveBiometricAutoPromptToAccount } from "./migrations/7-move-biometric-auto-prompt-to-account";
|
||||
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";
|
||||
|
||||
export const MIN_VERSION = 3;
|
||||
export const CURRENT_VERSION = 49;
|
||||
export const CURRENT_VERSION = 50;
|
||||
|
||||
export type MinVersion = typeof MIN_VERSION;
|
||||
|
||||
export function createMigrationBuilder() {
|
||||
|
@ -104,7 +106,8 @@ export function createMigrationBuilder() {
|
|||
.with(DeleteBiometricPromptCancelledData, 45, 46)
|
||||
.with(MoveDesktopSettingsMigrator, 46, 47)
|
||||
.with(MoveDdgToStateProviderMigrator, 47, 48)
|
||||
.with(AccountServerConfigMigrator, 48, CURRENT_VERSION);
|
||||
.with(AccountServerConfigMigrator, 48, 49)
|
||||
.with(KeyConnectorMigrator, 49, CURRENT_VERSION);
|
||||
}
|
||||
|
||||
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))]);
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue