mirror of
https://github.com/bitwarden/browser
synced 2025-12-12 06:13:38 +00:00
[PM-6172] Run localStorage migrations for web (#7900)
* Create MigrationRunner - Create MigrationRunner Service for running migrations in StateService - Create web override so that migrations also run against `localStorage` * Fix Web StateService * Fix WebMigrationRunner * Fix CLI * Fix ElectronStateService * Update Comment * More Common Scenarios
This commit is contained in:
@@ -73,6 +73,8 @@ import { EncryptServiceImplementation } from "@bitwarden/common/platform/service
|
|||||||
import { MultithreadEncryptServiceImplementation } from "@bitwarden/common/platform/services/cryptography/multithread-encrypt.service.implementation";
|
import { MultithreadEncryptServiceImplementation } from "@bitwarden/common/platform/services/cryptography/multithread-encrypt.service.implementation";
|
||||||
import { FileUploadService } from "@bitwarden/common/platform/services/file-upload/file-upload.service";
|
import { FileUploadService } from "@bitwarden/common/platform/services/file-upload/file-upload.service";
|
||||||
import { MemoryStorageService } from "@bitwarden/common/platform/services/memory-storage.service";
|
import { MemoryStorageService } from "@bitwarden/common/platform/services/memory-storage.service";
|
||||||
|
import { MigrationBuilderService } from "@bitwarden/common/platform/services/migration-builder.service";
|
||||||
|
import { MigrationRunner } from "@bitwarden/common/platform/services/migration-runner";
|
||||||
import { SystemService } from "@bitwarden/common/platform/services/system.service";
|
import { SystemService } from "@bitwarden/common/platform/services/system.service";
|
||||||
import { WebCryptoFunctionService } from "@bitwarden/common/platform/services/web-crypto-function.service";
|
import { WebCryptoFunctionService } from "@bitwarden/common/platform/services/web-crypto-function.service";
|
||||||
import {
|
import {
|
||||||
@@ -381,6 +383,13 @@ export default class MainBackground {
|
|||||||
this.stateProvider,
|
this.stateProvider,
|
||||||
this.accountService,
|
this.accountService,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const migrationRunner = new MigrationRunner(
|
||||||
|
this.storageService,
|
||||||
|
this.logService,
|
||||||
|
new MigrationBuilderService(),
|
||||||
|
);
|
||||||
|
|
||||||
this.stateService = new BrowserStateService(
|
this.stateService = new BrowserStateService(
|
||||||
this.storageService,
|
this.storageService,
|
||||||
this.secureStorageService,
|
this.secureStorageService,
|
||||||
@@ -389,6 +398,7 @@ export default class MainBackground {
|
|||||||
new StateFactory(GlobalState, Account),
|
new StateFactory(GlobalState, Account),
|
||||||
this.accountService,
|
this.accountService,
|
||||||
this.environmentService,
|
this.environmentService,
|
||||||
|
migrationRunner,
|
||||||
);
|
);
|
||||||
this.platformUtilsService = new BrowserPlatformUtilsService(
|
this.platformUtilsService = new BrowserPlatformUtilsService(
|
||||||
this.messagingService,
|
this.messagingService,
|
||||||
|
|||||||
@@ -0,0 +1,32 @@
|
|||||||
|
import { MigrationBuilderService } from "@bitwarden/common/platform/services/migration-builder.service";
|
||||||
|
import { MigrationRunner } from "@bitwarden/common/platform/services/migration-runner";
|
||||||
|
|
||||||
|
import { CachedServices, FactoryOptions, factory } from "./factory-options";
|
||||||
|
import { LogServiceInitOptions, logServiceFactory } from "./log-service.factory";
|
||||||
|
import {
|
||||||
|
DiskStorageServiceInitOptions,
|
||||||
|
diskStorageServiceFactory,
|
||||||
|
} from "./storage-service.factory";
|
||||||
|
|
||||||
|
type MigrationRunnerFactory = FactoryOptions;
|
||||||
|
|
||||||
|
export type MigrationRunnerInitOptions = MigrationRunnerFactory &
|
||||||
|
DiskStorageServiceInitOptions &
|
||||||
|
LogServiceInitOptions;
|
||||||
|
|
||||||
|
export async function migrationRunnerFactory(
|
||||||
|
cache: { migrationRunner?: MigrationRunner } & CachedServices,
|
||||||
|
opts: MigrationRunnerInitOptions,
|
||||||
|
): Promise<MigrationRunner> {
|
||||||
|
return factory(
|
||||||
|
cache,
|
||||||
|
"migrationRunner",
|
||||||
|
opts,
|
||||||
|
async () =>
|
||||||
|
new MigrationRunner(
|
||||||
|
await diskStorageServiceFactory(cache, opts),
|
||||||
|
await logServiceFactory(cache, opts),
|
||||||
|
new MigrationBuilderService(),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -14,6 +14,7 @@ import {
|
|||||||
} from "./environment-service.factory";
|
} from "./environment-service.factory";
|
||||||
import { CachedServices, factory, FactoryOptions } from "./factory-options";
|
import { CachedServices, factory, FactoryOptions } from "./factory-options";
|
||||||
import { logServiceFactory, LogServiceInitOptions } from "./log-service.factory";
|
import { logServiceFactory, LogServiceInitOptions } from "./log-service.factory";
|
||||||
|
import { migrationRunnerFactory, MigrationRunnerInitOptions } from "./migration-runner.factory";
|
||||||
import {
|
import {
|
||||||
diskStorageServiceFactory,
|
diskStorageServiceFactory,
|
||||||
secureStorageServiceFactory,
|
secureStorageServiceFactory,
|
||||||
@@ -36,7 +37,8 @@ export type StateServiceInitOptions = StateServiceFactoryOptions &
|
|||||||
MemoryStorageServiceInitOptions &
|
MemoryStorageServiceInitOptions &
|
||||||
LogServiceInitOptions &
|
LogServiceInitOptions &
|
||||||
AccountServiceInitOptions &
|
AccountServiceInitOptions &
|
||||||
EnvironmentServiceInitOptions;
|
EnvironmentServiceInitOptions &
|
||||||
|
MigrationRunnerInitOptions;
|
||||||
|
|
||||||
export async function stateServiceFactory(
|
export async function stateServiceFactory(
|
||||||
cache: { stateService?: BrowserStateService } & CachedServices,
|
cache: { stateService?: BrowserStateService } & CachedServices,
|
||||||
@@ -55,11 +57,11 @@ export async function stateServiceFactory(
|
|||||||
opts.stateServiceOptions.stateFactory,
|
opts.stateServiceOptions.stateFactory,
|
||||||
await accountServiceFactory(cache, opts),
|
await accountServiceFactory(cache, opts),
|
||||||
await environmentServiceFactory(cache, opts),
|
await environmentServiceFactory(cache, opts),
|
||||||
|
await migrationRunnerFactory(cache, opts),
|
||||||
opts.stateServiceOptions.useAccountCache,
|
opts.stateServiceOptions.useAccountCache,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
|
// TODO: If we run migration through a chrome installed/updated event we can turn off running migrations
|
||||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
await service.init();
|
||||||
service.init();
|
|
||||||
return service;
|
return service;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import {
|
|||||||
import { StateFactory } from "@bitwarden/common/platform/factories/state-factory";
|
import { StateFactory } from "@bitwarden/common/platform/factories/state-factory";
|
||||||
import { GlobalState } from "@bitwarden/common/platform/models/domain/global-state";
|
import { GlobalState } from "@bitwarden/common/platform/models/domain/global-state";
|
||||||
import { State } from "@bitwarden/common/platform/models/domain/state";
|
import { State } from "@bitwarden/common/platform/models/domain/state";
|
||||||
|
import { MigrationRunner } from "@bitwarden/common/platform/services/migration-runner";
|
||||||
import { SendType } from "@bitwarden/common/tools/send/enums/send-type";
|
import { SendType } from "@bitwarden/common/tools/send/enums/send-type";
|
||||||
import { SendView } from "@bitwarden/common/tools/send/models/view/send.view";
|
import { SendView } from "@bitwarden/common/tools/send/models/view/send.view";
|
||||||
|
|
||||||
@@ -31,6 +32,7 @@ describe("Browser State Service", () => {
|
|||||||
let useAccountCache: boolean;
|
let useAccountCache: boolean;
|
||||||
let accountService: MockProxy<AccountService>;
|
let accountService: MockProxy<AccountService>;
|
||||||
let environmentService: MockProxy<EnvironmentService>;
|
let environmentService: MockProxy<EnvironmentService>;
|
||||||
|
let migrationRunner: MockProxy<MigrationRunner>;
|
||||||
|
|
||||||
let state: State<GlobalState, Account>;
|
let state: State<GlobalState, Account>;
|
||||||
const userId = "userId";
|
const userId = "userId";
|
||||||
@@ -44,6 +46,7 @@ describe("Browser State Service", () => {
|
|||||||
stateFactory = mock();
|
stateFactory = mock();
|
||||||
accountService = mock();
|
accountService = mock();
|
||||||
environmentService = mock();
|
environmentService = mock();
|
||||||
|
migrationRunner = mock();
|
||||||
// turn off account cache for tests
|
// turn off account cache for tests
|
||||||
useAccountCache = false;
|
useAccountCache = false;
|
||||||
|
|
||||||
@@ -70,6 +73,7 @@ describe("Browser State Service", () => {
|
|||||||
stateFactory,
|
stateFactory,
|
||||||
accountService,
|
accountService,
|
||||||
environmentService,
|
environmentService,
|
||||||
|
migrationRunner,
|
||||||
useAccountCache,
|
useAccountCache,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import {
|
|||||||
import { StateFactory } from "@bitwarden/common/platform/factories/state-factory";
|
import { StateFactory } from "@bitwarden/common/platform/factories/state-factory";
|
||||||
import { GlobalState } from "@bitwarden/common/platform/models/domain/global-state";
|
import { GlobalState } from "@bitwarden/common/platform/models/domain/global-state";
|
||||||
import { StorageOptions } from "@bitwarden/common/platform/models/domain/storage-options";
|
import { StorageOptions } from "@bitwarden/common/platform/models/domain/storage-options";
|
||||||
|
import { MigrationRunner } from "@bitwarden/common/platform/services/migration-runner";
|
||||||
import { StateService as BaseStateService } from "@bitwarden/common/platform/services/state.service";
|
import { StateService as BaseStateService } from "@bitwarden/common/platform/services/state.service";
|
||||||
|
|
||||||
import { Account } from "../../models/account";
|
import { Account } from "../../models/account";
|
||||||
@@ -46,6 +47,7 @@ export class BrowserStateService
|
|||||||
stateFactory: StateFactory<GlobalState, Account>,
|
stateFactory: StateFactory<GlobalState, Account>,
|
||||||
accountService: AccountService,
|
accountService: AccountService,
|
||||||
environmentService: EnvironmentService,
|
environmentService: EnvironmentService,
|
||||||
|
migrationRunner: MigrationRunner,
|
||||||
useAccountCache = true,
|
useAccountCache = true,
|
||||||
) {
|
) {
|
||||||
super(
|
super(
|
||||||
@@ -56,6 +58,7 @@ export class BrowserStateService
|
|||||||
stateFactory,
|
stateFactory,
|
||||||
accountService,
|
accountService,
|
||||||
environmentService,
|
environmentService,
|
||||||
|
migrationRunner,
|
||||||
useAccountCache,
|
useAccountCache,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -72,6 +72,7 @@ import { GlobalState } from "@bitwarden/common/platform/models/domain/global-sta
|
|||||||
import { ConfigService } from "@bitwarden/common/platform/services/config/config.service";
|
import { ConfigService } from "@bitwarden/common/platform/services/config/config.service";
|
||||||
import { ConsoleLogService } from "@bitwarden/common/platform/services/console-log.service";
|
import { ConsoleLogService } from "@bitwarden/common/platform/services/console-log.service";
|
||||||
import { ContainerService } from "@bitwarden/common/platform/services/container.service";
|
import { ContainerService } from "@bitwarden/common/platform/services/container.service";
|
||||||
|
import { MigrationRunner } from "@bitwarden/common/platform/services/migration-runner";
|
||||||
import { DerivedStateProvider, StateProvider } from "@bitwarden/common/platform/state";
|
import { DerivedStateProvider, StateProvider } from "@bitwarden/common/platform/state";
|
||||||
import { SearchService } from "@bitwarden/common/services/search.service";
|
import { SearchService } from "@bitwarden/common/services/search.service";
|
||||||
import { PasswordGenerationServiceAbstraction } from "@bitwarden/common/tools/generator/password";
|
import { PasswordGenerationServiceAbstraction } from "@bitwarden/common/tools/generator/password";
|
||||||
@@ -482,6 +483,7 @@ function getBgService<T>(service: keyof MainBackground) {
|
|||||||
logService: LogServiceAbstraction,
|
logService: LogServiceAbstraction,
|
||||||
accountService: AccountServiceAbstraction,
|
accountService: AccountServiceAbstraction,
|
||||||
environmentService: EnvironmentService,
|
environmentService: EnvironmentService,
|
||||||
|
migrationRunner: MigrationRunner,
|
||||||
) => {
|
) => {
|
||||||
return new BrowserStateService(
|
return new BrowserStateService(
|
||||||
storageService,
|
storageService,
|
||||||
@@ -491,6 +493,7 @@ function getBgService<T>(service: keyof MainBackground) {
|
|||||||
new StateFactory(GlobalState, Account),
|
new StateFactory(GlobalState, Account),
|
||||||
accountService,
|
accountService,
|
||||||
environmentService,
|
environmentService,
|
||||||
|
migrationRunner,
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
deps: [
|
deps: [
|
||||||
@@ -500,6 +503,7 @@ function getBgService<T>(service: keyof MainBackground) {
|
|||||||
LogServiceAbstraction,
|
LogServiceAbstraction,
|
||||||
AccountServiceAbstraction,
|
AccountServiceAbstraction,
|
||||||
EnvironmentService,
|
EnvironmentService,
|
||||||
|
MigrationRunner,
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -51,6 +51,8 @@ import { EncryptServiceImplementation } from "@bitwarden/common/platform/service
|
|||||||
import { EnvironmentService } from "@bitwarden/common/platform/services/environment.service";
|
import { EnvironmentService } from "@bitwarden/common/platform/services/environment.service";
|
||||||
import { FileUploadService } from "@bitwarden/common/platform/services/file-upload/file-upload.service";
|
import { FileUploadService } from "@bitwarden/common/platform/services/file-upload/file-upload.service";
|
||||||
import { MemoryStorageService } from "@bitwarden/common/platform/services/memory-storage.service";
|
import { MemoryStorageService } from "@bitwarden/common/platform/services/memory-storage.service";
|
||||||
|
import { MigrationBuilderService } from "@bitwarden/common/platform/services/migration-builder.service";
|
||||||
|
import { MigrationRunner } from "@bitwarden/common/platform/services/migration-runner";
|
||||||
import { NoopMessagingService } from "@bitwarden/common/platform/services/noop-messaging.service";
|
import { NoopMessagingService } from "@bitwarden/common/platform/services/noop-messaging.service";
|
||||||
import { StateService } from "@bitwarden/common/platform/services/state.service";
|
import { StateService } from "@bitwarden/common/platform/services/state.service";
|
||||||
import {
|
import {
|
||||||
@@ -276,6 +278,12 @@ export class Main {
|
|||||||
|
|
||||||
this.environmentService = new EnvironmentService(this.stateProvider, this.accountService);
|
this.environmentService = new EnvironmentService(this.stateProvider, this.accountService);
|
||||||
|
|
||||||
|
const migrationRunner = new MigrationRunner(
|
||||||
|
this.storageService,
|
||||||
|
this.logService,
|
||||||
|
new MigrationBuilderService(),
|
||||||
|
);
|
||||||
|
|
||||||
this.stateService = new StateService(
|
this.stateService = new StateService(
|
||||||
this.storageService,
|
this.storageService,
|
||||||
this.secureStorageService,
|
this.secureStorageService,
|
||||||
@@ -284,6 +292,7 @@ export class Main {
|
|||||||
new StateFactory(GlobalState, Account),
|
new StateFactory(GlobalState, Account),
|
||||||
this.accountService,
|
this.accountService,
|
||||||
this.environmentService,
|
this.environmentService,
|
||||||
|
migrationRunner,
|
||||||
);
|
);
|
||||||
|
|
||||||
this.cryptoService = new CryptoService(
|
this.cryptoService = new CryptoService(
|
||||||
|
|||||||
@@ -38,6 +38,7 @@ import { BiometricStateService } from "@bitwarden/common/platform/biometrics/bio
|
|||||||
import { StateFactory } from "@bitwarden/common/platform/factories/state-factory";
|
import { StateFactory } from "@bitwarden/common/platform/factories/state-factory";
|
||||||
import { GlobalState } from "@bitwarden/common/platform/models/domain/global-state";
|
import { GlobalState } from "@bitwarden/common/platform/models/domain/global-state";
|
||||||
import { MemoryStorageService } from "@bitwarden/common/platform/services/memory-storage.service";
|
import { MemoryStorageService } from "@bitwarden/common/platform/services/memory-storage.service";
|
||||||
|
import { MigrationRunner } from "@bitwarden/common/platform/services/migration-runner";
|
||||||
import { SystemService } from "@bitwarden/common/platform/services/system.service";
|
import { SystemService } from "@bitwarden/common/platform/services/system.service";
|
||||||
import { StateProvider } from "@bitwarden/common/platform/state";
|
import { StateProvider } from "@bitwarden/common/platform/state";
|
||||||
// eslint-disable-next-line import/no-restricted-paths -- Implementation for memory storage
|
// eslint-disable-next-line import/no-restricted-paths -- Implementation for memory storage
|
||||||
@@ -134,6 +135,7 @@ const RELOAD_CALLBACK = new InjectionToken<() => any>("RELOAD_CALLBACK");
|
|||||||
STATE_FACTORY,
|
STATE_FACTORY,
|
||||||
AccountServiceAbstraction,
|
AccountServiceAbstraction,
|
||||||
EnvironmentService,
|
EnvironmentService,
|
||||||
|
MigrationRunner,
|
||||||
STATE_SERVICE_USE_CACHE,
|
STATE_SERVICE_USE_CACHE,
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -8,6 +8,8 @@ import { StateFactory } from "@bitwarden/common/platform/factories/state-factory
|
|||||||
import { GlobalState } from "@bitwarden/common/platform/models/domain/global-state";
|
import { GlobalState } from "@bitwarden/common/platform/models/domain/global-state";
|
||||||
import { EnvironmentService } from "@bitwarden/common/platform/services/environment.service";
|
import { EnvironmentService } from "@bitwarden/common/platform/services/environment.service";
|
||||||
import { MemoryStorageService } from "@bitwarden/common/platform/services/memory-storage.service";
|
import { MemoryStorageService } from "@bitwarden/common/platform/services/memory-storage.service";
|
||||||
|
import { MigrationBuilderService } from "@bitwarden/common/platform/services/migration-builder.service";
|
||||||
|
import { MigrationRunner } from "@bitwarden/common/platform/services/migration-runner";
|
||||||
import { NoopMessagingService } from "@bitwarden/common/platform/services/noop-messaging.service";
|
import { NoopMessagingService } from "@bitwarden/common/platform/services/noop-messaging.service";
|
||||||
/* eslint-disable import/no-restricted-paths -- We need the implementation to inject, but generally this should not be accessed */
|
/* eslint-disable import/no-restricted-paths -- We need the implementation to inject, but generally this should not be accessed */
|
||||||
import { DefaultActiveUserStateProvider } from "@bitwarden/common/platform/state/implementations/default-active-user-state.provider";
|
import { DefaultActiveUserStateProvider } from "@bitwarden/common/platform/state/implementations/default-active-user-state.provider";
|
||||||
@@ -17,7 +19,6 @@ import { DefaultSingleUserStateProvider } from "@bitwarden/common/platform/state
|
|||||||
import { DefaultStateProvider } from "@bitwarden/common/platform/state/implementations/default-state.provider";
|
import { DefaultStateProvider } from "@bitwarden/common/platform/state/implementations/default-state.provider";
|
||||||
import { MemoryStorageService as MemoryStorageServiceForStateProviders } from "@bitwarden/common/platform/state/storage/memory-storage.service";
|
import { MemoryStorageService as MemoryStorageServiceForStateProviders } from "@bitwarden/common/platform/state/storage/memory-storage.service";
|
||||||
/* eslint-enable import/no-restricted-paths */
|
/* eslint-enable import/no-restricted-paths */
|
||||||
import { migrate } from "@bitwarden/common/state-migrations";
|
|
||||||
|
|
||||||
import { MenuMain } from "./main/menu/menu.main";
|
import { MenuMain } from "./main/menu/menu.main";
|
||||||
import { MessagingMain } from "./main/messaging.main";
|
import { MessagingMain } from "./main/messaging.main";
|
||||||
@@ -46,6 +47,7 @@ export class Main {
|
|||||||
stateService: ElectronStateService;
|
stateService: ElectronStateService;
|
||||||
environmentService: EnvironmentService;
|
environmentService: EnvironmentService;
|
||||||
desktopCredentialStorageListener: DesktopCredentialStorageListener;
|
desktopCredentialStorageListener: DesktopCredentialStorageListener;
|
||||||
|
migrationRunner: MigrationRunner;
|
||||||
|
|
||||||
windowMain: WindowMain;
|
windowMain: WindowMain;
|
||||||
messagingMain: MessagingMain;
|
messagingMain: MessagingMain;
|
||||||
@@ -123,6 +125,12 @@ export class Main {
|
|||||||
|
|
||||||
this.environmentService = new EnvironmentService(stateProvider, accountService);
|
this.environmentService = new EnvironmentService(stateProvider, accountService);
|
||||||
|
|
||||||
|
this.migrationRunner = new MigrationRunner(
|
||||||
|
this.storageService,
|
||||||
|
this.logService,
|
||||||
|
new MigrationBuilderService(),
|
||||||
|
);
|
||||||
|
|
||||||
// TODO: this state service will have access to on disk storage, but not in memory storage.
|
// TODO: this state service will have access to on disk storage, but not in memory storage.
|
||||||
// If we could get this to work using the stateService singleton that the rest of the app uses we could save
|
// If we could get this to work using the stateService singleton that the rest of the app uses we could save
|
||||||
// ourselves from some hacks, like having to manually update the app menu vs. the menu subscribing to events.
|
// ourselves from some hacks, like having to manually update the app menu vs. the menu subscribing to events.
|
||||||
@@ -134,6 +142,7 @@ export class Main {
|
|||||||
new StateFactory(GlobalState, Account),
|
new StateFactory(GlobalState, Account),
|
||||||
accountService, // will not broadcast logouts. This is a hack until we can remove messaging dependency
|
accountService, // will not broadcast logouts. This is a hack until we can remove messaging dependency
|
||||||
this.environmentService,
|
this.environmentService,
|
||||||
|
this.migrationRunner,
|
||||||
false, // Do not use disk caching because this will get out of sync with the renderer service
|
false, // Do not use disk caching because this will get out of sync with the renderer service
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -192,7 +201,7 @@ export class Main {
|
|||||||
bootstrap() {
|
bootstrap() {
|
||||||
this.desktopCredentialStorageListener.init();
|
this.desktopCredentialStorageListener.init();
|
||||||
// Run migrations first, then other things
|
// Run migrations first, then other things
|
||||||
migrate(this.storageService, this.logService).then(
|
this.migrationRunner.run().then(
|
||||||
async () => {
|
async () => {
|
||||||
await this.windowMain.init();
|
await this.windowMain.init();
|
||||||
const locale = await this.stateService.getLocale();
|
const locale = await this.stateService.getLocale();
|
||||||
|
|||||||
@@ -19,12 +19,15 @@ import { LoginService as LoginServiceAbstraction } from "@bitwarden/common/auth/
|
|||||||
import { LoginService } from "@bitwarden/common/auth/services/login.service";
|
import { LoginService } from "@bitwarden/common/auth/services/login.service";
|
||||||
import { FileDownloadService } from "@bitwarden/common/platform/abstractions/file-download/file-download.service";
|
import { FileDownloadService } from "@bitwarden/common/platform/abstractions/file-download/file-download.service";
|
||||||
import { I18nService as I18nServiceAbstraction } from "@bitwarden/common/platform/abstractions/i18n.service";
|
import { I18nService as I18nServiceAbstraction } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||||
|
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||||
import { MessagingService as MessagingServiceAbstraction } from "@bitwarden/common/platform/abstractions/messaging.service";
|
import { MessagingService as MessagingServiceAbstraction } from "@bitwarden/common/platform/abstractions/messaging.service";
|
||||||
import { PlatformUtilsService as PlatformUtilsServiceAbstraction } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
import { PlatformUtilsService as PlatformUtilsServiceAbstraction } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||||
import { StateService as BaseStateServiceAbstraction } from "@bitwarden/common/platform/abstractions/state.service";
|
import { StateService as BaseStateServiceAbstraction } from "@bitwarden/common/platform/abstractions/state.service";
|
||||||
import { AbstractStorageService } from "@bitwarden/common/platform/abstractions/storage.service";
|
import { AbstractStorageService } from "@bitwarden/common/platform/abstractions/storage.service";
|
||||||
import { StateFactory } from "@bitwarden/common/platform/factories/state-factory";
|
import { StateFactory } from "@bitwarden/common/platform/factories/state-factory";
|
||||||
import { MemoryStorageService } from "@bitwarden/common/platform/services/memory-storage.service";
|
import { MemoryStorageService } from "@bitwarden/common/platform/services/memory-storage.service";
|
||||||
|
import { MigrationBuilderService } from "@bitwarden/common/platform/services/migration-builder.service";
|
||||||
|
import { MigrationRunner } from "@bitwarden/common/platform/services/migration-runner";
|
||||||
import {
|
import {
|
||||||
ActiveUserStateProvider,
|
ActiveUserStateProvider,
|
||||||
GlobalStateProvider,
|
GlobalStateProvider,
|
||||||
@@ -38,6 +41,7 @@ import { HtmlStorageService } from "../core/html-storage.service";
|
|||||||
import { I18nService } from "../core/i18n.service";
|
import { I18nService } from "../core/i18n.service";
|
||||||
import { WebActiveUserStateProvider } from "../platform/web-active-user-state.provider";
|
import { WebActiveUserStateProvider } from "../platform/web-active-user-state.provider";
|
||||||
import { WebGlobalStateProvider } from "../platform/web-global-state.provider";
|
import { WebGlobalStateProvider } from "../platform/web-global-state.provider";
|
||||||
|
import { WebMigrationRunner } from "../platform/web-migration-runner";
|
||||||
import { WebSingleUserStateProvider } from "../platform/web-single-user-state.provider";
|
import { WebSingleUserStateProvider } from "../platform/web-single-user-state.provider";
|
||||||
import { WindowStorageService } from "../platform/window-storage.service";
|
import { WindowStorageService } from "../platform/window-storage.service";
|
||||||
import { CollectionAdminService } from "../vault/core/collection-admin.service";
|
import { CollectionAdminService } from "../vault/core/collection-admin.service";
|
||||||
@@ -139,6 +143,16 @@ import { WebPlatformUtilsService } from "./web-platform-utils.service";
|
|||||||
useClass: WebGlobalStateProvider,
|
useClass: WebGlobalStateProvider,
|
||||||
deps: [OBSERVABLE_MEMORY_STORAGE, OBSERVABLE_DISK_STORAGE, OBSERVABLE_DISK_LOCAL_STORAGE],
|
deps: [OBSERVABLE_MEMORY_STORAGE, OBSERVABLE_DISK_STORAGE, OBSERVABLE_DISK_LOCAL_STORAGE],
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
provide: MigrationRunner,
|
||||||
|
useClass: WebMigrationRunner,
|
||||||
|
deps: [
|
||||||
|
AbstractStorageService,
|
||||||
|
LogService,
|
||||||
|
MigrationBuilderService,
|
||||||
|
OBSERVABLE_DISK_LOCAL_STORAGE,
|
||||||
|
],
|
||||||
|
},
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
export class CoreModule {
|
export class CoreModule {
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import {
|
|||||||
} from "@bitwarden/common/platform/abstractions/storage.service";
|
} from "@bitwarden/common/platform/abstractions/storage.service";
|
||||||
import { StateFactory } from "@bitwarden/common/platform/factories/state-factory";
|
import { StateFactory } from "@bitwarden/common/platform/factories/state-factory";
|
||||||
import { StorageOptions } from "@bitwarden/common/platform/models/domain/storage-options";
|
import { StorageOptions } from "@bitwarden/common/platform/models/domain/storage-options";
|
||||||
|
import { MigrationRunner } from "@bitwarden/common/platform/services/migration-runner";
|
||||||
import { StateService as BaseStateService } from "@bitwarden/common/platform/services/state.service";
|
import { StateService as BaseStateService } from "@bitwarden/common/platform/services/state.service";
|
||||||
import { SendData } from "@bitwarden/common/tools/send/models/data/send.data";
|
import { SendData } from "@bitwarden/common/tools/send/models/data/send.data";
|
||||||
import { CipherData } from "@bitwarden/common/vault/models/data/cipher.data";
|
import { CipherData } from "@bitwarden/common/vault/models/data/cipher.data";
|
||||||
@@ -33,6 +34,7 @@ export class StateService extends BaseStateService<GlobalState, Account> {
|
|||||||
@Inject(STATE_FACTORY) stateFactory: StateFactory<GlobalState, Account>,
|
@Inject(STATE_FACTORY) stateFactory: StateFactory<GlobalState, Account>,
|
||||||
accountService: AccountService,
|
accountService: AccountService,
|
||||||
environmentService: EnvironmentService,
|
environmentService: EnvironmentService,
|
||||||
|
migrationRunner: MigrationRunner,
|
||||||
@Inject(STATE_SERVICE_USE_CACHE) useAccountCache = true,
|
@Inject(STATE_SERVICE_USE_CACHE) useAccountCache = true,
|
||||||
) {
|
) {
|
||||||
super(
|
super(
|
||||||
@@ -43,6 +45,7 @@ export class StateService extends BaseStateService<GlobalState, Account> {
|
|||||||
stateFactory,
|
stateFactory,
|
||||||
accountService,
|
accountService,
|
||||||
environmentService,
|
environmentService,
|
||||||
|
migrationRunner,
|
||||||
useAccountCache,
|
useAccountCache,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
252
apps/web/src/app/platform/web-migration-runner.spec.ts
Normal file
252
apps/web/src/app/platform/web-migration-runner.spec.ts
Normal file
@@ -0,0 +1,252 @@
|
|||||||
|
import { MockProxy, mock } from "jest-mock-extended";
|
||||||
|
|
||||||
|
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||||
|
import { AbstractStorageService } from "@bitwarden/common/platform/abstractions/storage.service";
|
||||||
|
import { MigrationBuilderService } from "@bitwarden/common/platform/services/migration-builder.service";
|
||||||
|
import { MigrationBuilder } from "@bitwarden/common/state-migrations/migration-builder";
|
||||||
|
import { MigrationHelper } from "@bitwarden/common/state-migrations/migration-helper";
|
||||||
|
|
||||||
|
import { WebMigrationRunner } from "./web-migration-runner";
|
||||||
|
import { WindowStorageService } from "./window-storage.service";
|
||||||
|
|
||||||
|
describe("WebMigrationRunner", () => {
|
||||||
|
let logService: MockProxy<LogService>;
|
||||||
|
let sessionStorageService: MockProxy<AbstractStorageService>;
|
||||||
|
let localStorageService: MockProxy<WindowStorageService>;
|
||||||
|
let migrationBuilderService: MockProxy<MigrationBuilderService>;
|
||||||
|
|
||||||
|
let sut: WebMigrationRunner;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
logService = mock();
|
||||||
|
sessionStorageService = mock();
|
||||||
|
localStorageService = mock();
|
||||||
|
migrationBuilderService = mock();
|
||||||
|
|
||||||
|
sut = new WebMigrationRunner(
|
||||||
|
sessionStorageService,
|
||||||
|
logService,
|
||||||
|
migrationBuilderService,
|
||||||
|
localStorageService,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
const mockMigrationBuilder = (migration: (helper: MigrationHelper) => Promise<void>) => {
|
||||||
|
migrationBuilderService.build.mockReturnValue({
|
||||||
|
migrate: async (helper: MigrationHelper) => {
|
||||||
|
await migration(helper);
|
||||||
|
},
|
||||||
|
with: () => {
|
||||||
|
throw new Error("Don't use this in tests.");
|
||||||
|
},
|
||||||
|
rollback: () => {
|
||||||
|
throw new Error("Don't use this in tests.");
|
||||||
|
},
|
||||||
|
} as unknown as MigrationBuilder);
|
||||||
|
};
|
||||||
|
|
||||||
|
const mockGet = (
|
||||||
|
mockStorage: MockProxy<AbstractStorageService>,
|
||||||
|
data: Record<string, unknown>,
|
||||||
|
) => {
|
||||||
|
mockStorage.get.mockImplementation((key) => {
|
||||||
|
return Promise.resolve(data[key]);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
it("should run migration for both storage locations", async () => {
|
||||||
|
mockGet(sessionStorageService, {
|
||||||
|
stateVersion: 4,
|
||||||
|
});
|
||||||
|
mockGet(localStorageService, {});
|
||||||
|
|
||||||
|
mockMigrationBuilder(async (helper) => {
|
||||||
|
await helper.set("something", "something");
|
||||||
|
});
|
||||||
|
|
||||||
|
await sut.run();
|
||||||
|
|
||||||
|
expect(sessionStorageService.save).toHaveBeenCalledWith("something", "something");
|
||||||
|
expect(localStorageService.save).toHaveBeenCalledWith("something", "something");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should only migrate data in one migration if written defensively", async () => {
|
||||||
|
mockGet(sessionStorageService, {
|
||||||
|
stateVersion: 4,
|
||||||
|
});
|
||||||
|
mockGet(localStorageService, {
|
||||||
|
user1: {
|
||||||
|
settings: {
|
||||||
|
myData: "value",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
mockMigrationBuilder(async (helper) => {
|
||||||
|
const account = await helper.get<{ settings?: { myData?: string } }>("user1");
|
||||||
|
const value = account?.settings?.myData;
|
||||||
|
if (value) {
|
||||||
|
await helper.setToUser("user1", { key: "key", stateDefinition: { name: "state" } }, value);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
await sut.run();
|
||||||
|
|
||||||
|
expect(sessionStorageService.save).not.toHaveBeenCalled();
|
||||||
|
expect(localStorageService.save).toHaveBeenCalledWith("user_user1_state_key", "value");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should gather accounts differently", async () => {
|
||||||
|
mockGet(sessionStorageService, {
|
||||||
|
stateVersion: 10,
|
||||||
|
authenticatedAccounts: ["sessionUser1", "sessionUser2"],
|
||||||
|
sessionUser1: {
|
||||||
|
data: 1,
|
||||||
|
},
|
||||||
|
sessionUser2: {
|
||||||
|
data: null,
|
||||||
|
},
|
||||||
|
sessionUser3: {
|
||||||
|
// User does NOT have authenticated accounts entry
|
||||||
|
data: 3,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const localStorageObject = {
|
||||||
|
"8118af89-a621-4b0f-8dd2-4449569e5067": {
|
||||||
|
data: 4,
|
||||||
|
},
|
||||||
|
"cc202dba-55f8-4cbe-8c66-de37e48e7827": {
|
||||||
|
data: <number>null,
|
||||||
|
},
|
||||||
|
otherThing: {
|
||||||
|
data: 6,
|
||||||
|
},
|
||||||
|
"badd2aff-a380-468f-855a-e476557055d5": <object>null,
|
||||||
|
"01f81ccd-fb18-460c-9a6b-811ef5300d4b": 3,
|
||||||
|
};
|
||||||
|
|
||||||
|
mockGet(localStorageService, localStorageObject);
|
||||||
|
localStorageService.getKeys.mockReturnValue(Object.keys(localStorageObject));
|
||||||
|
|
||||||
|
mockMigrationBuilder(async (helper) => {
|
||||||
|
type ExpectedAccountType = {
|
||||||
|
data?: number;
|
||||||
|
};
|
||||||
|
async function migrateAccount(userId: string, account: ExpectedAccountType) {
|
||||||
|
const value = account?.data;
|
||||||
|
if (value != null) {
|
||||||
|
await helper.setToUser(userId, { key: "key", stateDefinition: { name: "state" } }, value);
|
||||||
|
delete account.data;
|
||||||
|
await helper.set(userId, account);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const accounts = await helper.getAccounts();
|
||||||
|
await Promise.all(accounts.map(({ userId, account }) => migrateAccount(userId, account)));
|
||||||
|
});
|
||||||
|
|
||||||
|
await sut.run();
|
||||||
|
|
||||||
|
// Session storage has two users but only one with data
|
||||||
|
expect(sessionStorageService.save).toHaveBeenCalledTimes(2);
|
||||||
|
// Should move the data to the new location first
|
||||||
|
expect(sessionStorageService.save).toHaveBeenNthCalledWith(1, "user_sessionUser1_state_key", 1);
|
||||||
|
// Should then delete the migrated data and resave object
|
||||||
|
expect(sessionStorageService.save).toHaveBeenNthCalledWith(2, "sessionUser1", {});
|
||||||
|
|
||||||
|
expect(sessionStorageService.get).toHaveBeenCalledTimes(4);
|
||||||
|
// Should first get the state version so it knowns which migrations to run (not really used in this test)
|
||||||
|
expect(sessionStorageService.get).toHaveBeenNthCalledWith(1, "stateVersion");
|
||||||
|
// "base" migration runner should trust the authenticatedAccounts stored value for knowing which accounts to migrate
|
||||||
|
expect(sessionStorageService.get).toHaveBeenNthCalledWith(2, "authenticatedAccounts");
|
||||||
|
// Should get the data for each user
|
||||||
|
expect(sessionStorageService.get).toHaveBeenNthCalledWith(3, "sessionUser1");
|
||||||
|
expect(sessionStorageService.get).toHaveBeenNthCalledWith(4, "sessionUser2");
|
||||||
|
|
||||||
|
expect(localStorageService.save).toHaveBeenCalledTimes(2);
|
||||||
|
// Should migrate data for a user in local storage
|
||||||
|
expect(localStorageService.save).toHaveBeenNthCalledWith(
|
||||||
|
1,
|
||||||
|
"user_8118af89-a621-4b0f-8dd2-4449569e5067_state_key",
|
||||||
|
4,
|
||||||
|
);
|
||||||
|
// Should update object with migrated data deleted
|
||||||
|
expect(localStorageService.save).toHaveBeenNthCalledWith(
|
||||||
|
2,
|
||||||
|
"8118af89-a621-4b0f-8dd2-4449569e5067",
|
||||||
|
{},
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(localStorageService.get).toHaveBeenCalledTimes(5);
|
||||||
|
expect(localStorageService.get).toHaveBeenNthCalledWith(1, "stateVersion");
|
||||||
|
expect(localStorageService.get).toHaveBeenNthCalledWith(
|
||||||
|
2,
|
||||||
|
"8118af89-a621-4b0f-8dd2-4449569e5067",
|
||||||
|
);
|
||||||
|
expect(localStorageService.get).toHaveBeenNthCalledWith(
|
||||||
|
3,
|
||||||
|
"cc202dba-55f8-4cbe-8c66-de37e48e7827",
|
||||||
|
);
|
||||||
|
expect(localStorageService.get).toHaveBeenNthCalledWith(
|
||||||
|
4,
|
||||||
|
"badd2aff-a380-468f-855a-e476557055d5",
|
||||||
|
);
|
||||||
|
expect(localStorageService.get).toHaveBeenNthCalledWith(
|
||||||
|
5,
|
||||||
|
"01f81ccd-fb18-460c-9a6b-811ef5300d4b",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should default currentVersion to 12 if no stateVersion exists", async () => {
|
||||||
|
mockGet(sessionStorageService, {
|
||||||
|
stateVersion: 14,
|
||||||
|
});
|
||||||
|
mockGet(localStorageService, {});
|
||||||
|
|
||||||
|
let runCount = 0;
|
||||||
|
|
||||||
|
mockMigrationBuilder(async (helper) => {
|
||||||
|
if (runCount === 0) {
|
||||||
|
// This should be the session storage run
|
||||||
|
expect(helper.currentVersion).toBe(14);
|
||||||
|
} else if (runCount === 1) {
|
||||||
|
// This should be the local storage run, and it should be the default version
|
||||||
|
expect(helper.currentVersion).toBe(12);
|
||||||
|
} else {
|
||||||
|
throw new Error("Should not have been called more than twice");
|
||||||
|
}
|
||||||
|
|
||||||
|
runCount++;
|
||||||
|
});
|
||||||
|
|
||||||
|
await sut.run();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should respect local storage stateVersion", async () => {
|
||||||
|
mockGet(sessionStorageService, {
|
||||||
|
stateVersion: 14,
|
||||||
|
});
|
||||||
|
mockGet(localStorageService, {
|
||||||
|
stateVersion: 18,
|
||||||
|
});
|
||||||
|
|
||||||
|
let runCount = 0;
|
||||||
|
|
||||||
|
mockMigrationBuilder(async (helper) => {
|
||||||
|
if (runCount === 0) {
|
||||||
|
// This should be the session storage run
|
||||||
|
expect(helper.currentVersion).toBe(14);
|
||||||
|
} else if (runCount === 1) {
|
||||||
|
// This should be the local storage run, and it should be the default version
|
||||||
|
expect(helper.currentVersion).toBe(18);
|
||||||
|
} else {
|
||||||
|
throw new Error("Should not have been called more than twice");
|
||||||
|
}
|
||||||
|
|
||||||
|
runCount++;
|
||||||
|
});
|
||||||
|
|
||||||
|
await sut.run();
|
||||||
|
});
|
||||||
|
});
|
||||||
86
apps/web/src/app/platform/web-migration-runner.ts
Normal file
86
apps/web/src/app/platform/web-migration-runner.ts
Normal file
@@ -0,0 +1,86 @@
|
|||||||
|
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||||
|
import { AbstractStorageService } from "@bitwarden/common/platform/abstractions/storage.service";
|
||||||
|
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
||||||
|
import { MigrationBuilderService } from "@bitwarden/common/platform/services/migration-builder.service";
|
||||||
|
import { MigrationRunner } from "@bitwarden/common/platform/services/migration-runner";
|
||||||
|
import { MigrationHelper } from "@bitwarden/common/state-migrations/migration-helper";
|
||||||
|
|
||||||
|
import { WindowStorageService } from "./window-storage.service";
|
||||||
|
|
||||||
|
export class WebMigrationRunner extends MigrationRunner {
|
||||||
|
constructor(
|
||||||
|
diskStorage: AbstractStorageService,
|
||||||
|
logService: LogService,
|
||||||
|
migrationBuilderService: MigrationBuilderService,
|
||||||
|
private diskLocalStorage: WindowStorageService,
|
||||||
|
) {
|
||||||
|
super(diskStorage, logService, migrationBuilderService);
|
||||||
|
}
|
||||||
|
|
||||||
|
override async run(): Promise<void> {
|
||||||
|
// Run the default migration against session storage
|
||||||
|
await super.run();
|
||||||
|
|
||||||
|
// run web disk local specific migrations
|
||||||
|
const migrationBuilder = this.migrationBuilderService.build();
|
||||||
|
|
||||||
|
let stateVersion = await this.diskLocalStorage.get<number | null>("stateVersion");
|
||||||
|
if (stateVersion == null) {
|
||||||
|
// Web has never stored a state version in disk local before
|
||||||
|
// TODO: Is this a good number?
|
||||||
|
stateVersion = 12;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Run migrations again specifically for web `localStorage`.
|
||||||
|
const helper = new WebMigrationHelper(stateVersion, this.diskLocalStorage, this.logService);
|
||||||
|
|
||||||
|
await migrationBuilder.migrate(helper);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class WebMigrationHelper extends MigrationHelper {
|
||||||
|
private readonly diskLocalStorageService: WindowStorageService;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
currentVersion: number,
|
||||||
|
storageService: WindowStorageService,
|
||||||
|
logService: LogService,
|
||||||
|
) {
|
||||||
|
super(currentVersion, storageService, logService);
|
||||||
|
this.diskLocalStorageService = storageService;
|
||||||
|
}
|
||||||
|
|
||||||
|
override async getAccounts<ExpectedAccountType>(): Promise<
|
||||||
|
{ userId: string; account: ExpectedAccountType }[]
|
||||||
|
> {
|
||||||
|
// Get all the keys of things stored in `localStorage`
|
||||||
|
const keys = this.diskLocalStorageService.getKeys();
|
||||||
|
|
||||||
|
const accounts: { userId: string; account: ExpectedAccountType }[] = [];
|
||||||
|
|
||||||
|
for (const key of keys) {
|
||||||
|
// Is this is likely a userid
|
||||||
|
if (!Utils.isGuid(key)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const accountCandidate = await this.diskLocalStorageService.get(key);
|
||||||
|
|
||||||
|
// If there isn't data at that key location, don't bother
|
||||||
|
if (accountCandidate == null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// The legacy account object was always an object, if
|
||||||
|
// it is some other primitive, it's like a false positive.
|
||||||
|
if (typeof accountCandidate !== "object") {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
accounts.push({ userId: key, account: accountCandidate as ExpectedAccountType });
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Cache this for future calls?
|
||||||
|
return accounts;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -50,4 +50,8 @@ export class WindowStorageService implements AbstractStorageService, ObservableS
|
|||||||
this.updatesSubject.next({ key, updateType: "remove" });
|
this.updatesSubject.next({ key, updateType: "remove" });
|
||||||
return Promise.resolve();
|
return Promise.resolve();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getKeys(): string[] {
|
||||||
|
return Object.keys(this.storage);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -125,6 +125,8 @@ import { EncryptServiceImplementation } from "@bitwarden/common/platform/service
|
|||||||
import { MultithreadEncryptServiceImplementation } from "@bitwarden/common/platform/services/cryptography/multithread-encrypt.service.implementation";
|
import { MultithreadEncryptServiceImplementation } from "@bitwarden/common/platform/services/cryptography/multithread-encrypt.service.implementation";
|
||||||
import { EnvironmentService } from "@bitwarden/common/platform/services/environment.service";
|
import { EnvironmentService } from "@bitwarden/common/platform/services/environment.service";
|
||||||
import { FileUploadService } from "@bitwarden/common/platform/services/file-upload/file-upload.service";
|
import { FileUploadService } from "@bitwarden/common/platform/services/file-upload/file-upload.service";
|
||||||
|
import { MigrationBuilderService } from "@bitwarden/common/platform/services/migration-builder.service";
|
||||||
|
import { MigrationRunner } from "@bitwarden/common/platform/services/migration-runner";
|
||||||
import { NoopNotificationsService } from "@bitwarden/common/platform/services/noop-notifications.service";
|
import { NoopNotificationsService } from "@bitwarden/common/platform/services/noop-notifications.service";
|
||||||
import { StateService } from "@bitwarden/common/platform/services/state.service";
|
import { StateService } from "@bitwarden/common/platform/services/state.service";
|
||||||
import { ValidationService } from "@bitwarden/common/platform/services/validation.service";
|
import { ValidationService } from "@bitwarden/common/platform/services/validation.service";
|
||||||
@@ -561,6 +563,7 @@ import { ModalService } from "./modal.service";
|
|||||||
STATE_FACTORY,
|
STATE_FACTORY,
|
||||||
AccountServiceAbstraction,
|
AccountServiceAbstraction,
|
||||||
EnvironmentServiceAbstraction,
|
EnvironmentServiceAbstraction,
|
||||||
|
MigrationRunner,
|
||||||
STATE_SERVICE_USE_CACHE,
|
STATE_SERVICE_USE_CACHE,
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
@@ -929,6 +932,15 @@ import { ModalService } from "./modal.service";
|
|||||||
useClass: VaultSettingsService,
|
useClass: VaultSettingsService,
|
||||||
deps: [StateProvider],
|
deps: [StateProvider],
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
provide: MigrationRunner,
|
||||||
|
useClass: MigrationRunner,
|
||||||
|
deps: [AbstractStorageService, LogService, MigrationBuilderService],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
provide: MigrationBuilderService,
|
||||||
|
useClass: MigrationBuilderService,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
provide: BillingApiServiceAbstraction,
|
provide: BillingApiServiceAbstraction,
|
||||||
useClass: BillingApiService,
|
useClass: BillingApiService,
|
||||||
|
|||||||
@@ -0,0 +1,89 @@
|
|||||||
|
import { mock } from "jest-mock-extended";
|
||||||
|
|
||||||
|
import { FakeStorageService } from "../../../spec/fake-storage.service";
|
||||||
|
import { MigrationHelper } from "../../state-migrations/migration-helper";
|
||||||
|
|
||||||
|
import { MigrationBuilderService } from "./migration-builder.service";
|
||||||
|
|
||||||
|
describe("MigrationBuilderService", () => {
|
||||||
|
// All migrations from 10+ should be capable of having a null account object or null global object
|
||||||
|
const startingStateVersion = 10;
|
||||||
|
|
||||||
|
const noAccounts = {
|
||||||
|
stateVersion: startingStateVersion,
|
||||||
|
authenticatedAccounts: <string[]>[],
|
||||||
|
};
|
||||||
|
|
||||||
|
const nullAndUndefinedAccounts = {
|
||||||
|
stateVersion: startingStateVersion,
|
||||||
|
authenticatedAccounts: ["account1", "account2"],
|
||||||
|
account1: <object>null,
|
||||||
|
account2: <object>undefined,
|
||||||
|
};
|
||||||
|
|
||||||
|
const emptyAccountObject = {
|
||||||
|
stateVersion: startingStateVersion,
|
||||||
|
authenticatedAccounts: ["account1"],
|
||||||
|
account1: {},
|
||||||
|
};
|
||||||
|
|
||||||
|
const nullCommonAccountProperties = {
|
||||||
|
stateVersion: startingStateVersion,
|
||||||
|
authenticatedAccounts: ["account1"],
|
||||||
|
account1: {
|
||||||
|
data: <object>null,
|
||||||
|
keys: <object>null,
|
||||||
|
profile: <object>null,
|
||||||
|
settings: <object>null,
|
||||||
|
tokens: <object>null,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const emptyCommonAccountProperties = {
|
||||||
|
stateVersion: startingStateVersion,
|
||||||
|
authenticatedAccounts: ["account1"],
|
||||||
|
account1: {
|
||||||
|
data: {},
|
||||||
|
keys: {},
|
||||||
|
profile: {},
|
||||||
|
settings: {},
|
||||||
|
tokens: {},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const nullGlobal = {
|
||||||
|
stateVersion: startingStateVersion,
|
||||||
|
global: <object>null,
|
||||||
|
};
|
||||||
|
|
||||||
|
const undefinedGlobal = {
|
||||||
|
stateVersion: startingStateVersion,
|
||||||
|
global: <object>undefined,
|
||||||
|
};
|
||||||
|
|
||||||
|
const emptyGlobalObject = {
|
||||||
|
stateVersion: startingStateVersion,
|
||||||
|
global: {},
|
||||||
|
};
|
||||||
|
|
||||||
|
it.each([
|
||||||
|
noAccounts,
|
||||||
|
nullAndUndefinedAccounts,
|
||||||
|
emptyAccountObject,
|
||||||
|
nullCommonAccountProperties,
|
||||||
|
emptyCommonAccountProperties,
|
||||||
|
nullGlobal,
|
||||||
|
undefinedGlobal,
|
||||||
|
emptyGlobalObject,
|
||||||
|
])("should not produce migrations that throw when given data: %s", async (startingState) => {
|
||||||
|
const sut = new MigrationBuilderService();
|
||||||
|
|
||||||
|
const helper = new MigrationHelper(
|
||||||
|
startingStateVersion,
|
||||||
|
new FakeStorageService(startingState),
|
||||||
|
mock(),
|
||||||
|
);
|
||||||
|
|
||||||
|
await sut.build().migrate(helper);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
import { createMigrationBuilder } from "../../state-migrations";
|
||||||
|
import { MigrationBuilder } from "../../state-migrations/migration-builder";
|
||||||
|
|
||||||
|
export class MigrationBuilderService {
|
||||||
|
private migrationBuilderCache: MigrationBuilder;
|
||||||
|
|
||||||
|
build() {
|
||||||
|
return (this.migrationBuilderCache ??= createMigrationBuilder());
|
||||||
|
}
|
||||||
|
}
|
||||||
102
libs/common/src/platform/services/migration-runner.spec.ts
Normal file
102
libs/common/src/platform/services/migration-runner.spec.ts
Normal file
@@ -0,0 +1,102 @@
|
|||||||
|
import { mock } from "jest-mock-extended";
|
||||||
|
|
||||||
|
import { awaitAsync } from "../../../spec";
|
||||||
|
import { CURRENT_VERSION } from "../../state-migrations";
|
||||||
|
import { MigrationBuilder } from "../../state-migrations/migration-builder";
|
||||||
|
import { LogService } from "../abstractions/log.service";
|
||||||
|
import { AbstractStorageService } from "../abstractions/storage.service";
|
||||||
|
|
||||||
|
import { MigrationBuilderService } from "./migration-builder.service";
|
||||||
|
import { MigrationRunner } from "./migration-runner";
|
||||||
|
|
||||||
|
describe("MigrationRunner", () => {
|
||||||
|
const storage = mock<AbstractStorageService>();
|
||||||
|
const logService = mock<LogService>();
|
||||||
|
const migrationBuilderService = mock<MigrationBuilderService>();
|
||||||
|
const mockMigrationBuilder = mock<MigrationBuilder>();
|
||||||
|
|
||||||
|
migrationBuilderService.build.mockReturnValue(mockMigrationBuilder);
|
||||||
|
|
||||||
|
const sut = new MigrationRunner(storage, logService, migrationBuilderService);
|
||||||
|
|
||||||
|
describe("migrate", () => {
|
||||||
|
it("should not run migrations if state is empty", async () => {
|
||||||
|
storage.get.mockReturnValueOnce(null);
|
||||||
|
await sut.run();
|
||||||
|
expect(migrationBuilderService.build).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should set to current version if state is empty", async () => {
|
||||||
|
storage.get.mockReturnValueOnce(null);
|
||||||
|
await sut.run();
|
||||||
|
expect(storage.save).toHaveBeenCalledWith("stateVersion", CURRENT_VERSION);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should run migration if there is a stateVersion", async () => {
|
||||||
|
storage.get.mockResolvedValueOnce(12);
|
||||||
|
|
||||||
|
await sut.run();
|
||||||
|
|
||||||
|
expect(mockMigrationBuilder.migrate).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("waitForCompletion", () => {
|
||||||
|
it("should wait until stateVersion is current before completing", async () => {
|
||||||
|
let stateVersion: number | null = null;
|
||||||
|
|
||||||
|
storage.get.mockImplementation((key) => {
|
||||||
|
if (key === "stateVersion") {
|
||||||
|
return Promise.resolve(stateVersion);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
let promiseCompleted = false;
|
||||||
|
|
||||||
|
const completionPromise = sut.waitForCompletion().then(() => (promiseCompleted = true));
|
||||||
|
|
||||||
|
await awaitAsync(10);
|
||||||
|
|
||||||
|
expect(promiseCompleted).toBe(false);
|
||||||
|
|
||||||
|
stateVersion = CURRENT_VERSION;
|
||||||
|
await completionPromise;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Skipped for CI since this test takes a while to complete, remove `.skip` to test
|
||||||
|
it.skip(
|
||||||
|
"will complete after 8 second step wait if migrations still aren't complete",
|
||||||
|
async () => {
|
||||||
|
storage.get.mockImplementation((key) => {
|
||||||
|
if (key === "stateVersion") {
|
||||||
|
return Promise.resolve(null);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
let promiseCompleted = false;
|
||||||
|
|
||||||
|
void sut.waitForCompletion().then(() => (promiseCompleted = true));
|
||||||
|
|
||||||
|
await awaitAsync(2 + 4 + 8 + 16);
|
||||||
|
|
||||||
|
expect(promiseCompleted).toBe(false);
|
||||||
|
|
||||||
|
await awaitAsync(32 + 64 + 128 + 256);
|
||||||
|
|
||||||
|
expect(promiseCompleted).toBe(false);
|
||||||
|
|
||||||
|
await awaitAsync(512 + 1024 + 2048 + 4096);
|
||||||
|
|
||||||
|
expect(promiseCompleted).toBe(false);
|
||||||
|
|
||||||
|
const SKEW = 20;
|
||||||
|
|
||||||
|
await awaitAsync(8192 + SKEW);
|
||||||
|
|
||||||
|
expect(promiseCompleted).toBe(true);
|
||||||
|
},
|
||||||
|
// Have to combine all the steps into the timeout to get this to run
|
||||||
|
2 + 4 + 8 + 16 + 32 + 64 + 128 + 256 + 512 + 1024 + 2048 + 4096 + 8192 + 100,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
37
libs/common/src/platform/services/migration-runner.ts
Normal file
37
libs/common/src/platform/services/migration-runner.ts
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
import { waitForMigrations } from "../../state-migrations";
|
||||||
|
import { CURRENT_VERSION, currentVersion } from "../../state-migrations/migrate";
|
||||||
|
import { MigrationHelper } from "../../state-migrations/migration-helper";
|
||||||
|
import { LogService } from "../abstractions/log.service";
|
||||||
|
import { AbstractStorageService } from "../abstractions/storage.service";
|
||||||
|
|
||||||
|
import { MigrationBuilderService } from "./migration-builder.service";
|
||||||
|
|
||||||
|
export class MigrationRunner {
|
||||||
|
constructor(
|
||||||
|
protected diskStorage: AbstractStorageService,
|
||||||
|
protected logService: LogService,
|
||||||
|
protected migrationBuilderService: MigrationBuilderService,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
async run(): Promise<void> {
|
||||||
|
const migrationHelper = new MigrationHelper(
|
||||||
|
await currentVersion(this.diskStorage, this.logService),
|
||||||
|
this.diskStorage,
|
||||||
|
this.logService,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (migrationHelper.currentVersion < 0) {
|
||||||
|
// Cannot determine state, assuming empty so we don't repeatedly apply a migration.
|
||||||
|
await this.diskStorage.save("stateVersion", CURRENT_VERSION);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const migrationBuilder = this.migrationBuilderService.build();
|
||||||
|
|
||||||
|
await migrationBuilder.migrate(migrationHelper);
|
||||||
|
}
|
||||||
|
|
||||||
|
async waitForCompletion(): Promise<void> {
|
||||||
|
await waitForMigrations(this.diskStorage, this.logService);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -14,8 +14,6 @@ import { BiometricKey } from "../../auth/types/biometric-key";
|
|||||||
import { VaultTimeoutAction } from "../../enums/vault-timeout-action.enum";
|
import { VaultTimeoutAction } from "../../enums/vault-timeout-action.enum";
|
||||||
import { EventData } from "../../models/data/event.data";
|
import { EventData } from "../../models/data/event.data";
|
||||||
import { WindowState } from "../../models/domain/window-state";
|
import { WindowState } from "../../models/domain/window-state";
|
||||||
import { migrate } from "../../state-migrations";
|
|
||||||
import { waitForMigrations } from "../../state-migrations/migrate";
|
|
||||||
import { GeneratorOptions } from "../../tools/generator/generator-options";
|
import { GeneratorOptions } from "../../tools/generator/generator-options";
|
||||||
import { GeneratedPasswordHistory, PasswordGeneratorOptions } from "../../tools/generator/password";
|
import { GeneratedPasswordHistory, PasswordGeneratorOptions } from "../../tools/generator/password";
|
||||||
import { UsernameGeneratorOptions } from "../../tools/generator/username";
|
import { UsernameGeneratorOptions } from "../../tools/generator/username";
|
||||||
@@ -57,6 +55,8 @@ import { State } from "../models/domain/state";
|
|||||||
import { StorageOptions } from "../models/domain/storage-options";
|
import { StorageOptions } from "../models/domain/storage-options";
|
||||||
import { SymmetricCryptoKey } from "../models/domain/symmetric-crypto-key";
|
import { SymmetricCryptoKey } from "../models/domain/symmetric-crypto-key";
|
||||||
|
|
||||||
|
import { MigrationRunner } from "./migration-runner";
|
||||||
|
|
||||||
const keys = {
|
const keys = {
|
||||||
state: "state",
|
state: "state",
|
||||||
stateVersion: "stateVersion",
|
stateVersion: "stateVersion",
|
||||||
@@ -108,6 +108,7 @@ export class StateService<
|
|||||||
protected stateFactory: StateFactory<TGlobalState, TAccount>,
|
protected stateFactory: StateFactory<TGlobalState, TAccount>,
|
||||||
protected accountService: AccountService,
|
protected accountService: AccountService,
|
||||||
protected environmentService: EnvironmentService,
|
protected environmentService: EnvironmentService,
|
||||||
|
private migrationRunner: MigrationRunner,
|
||||||
protected useAccountCache: boolean = true,
|
protected useAccountCache: boolean = true,
|
||||||
) {
|
) {
|
||||||
// If the account gets changed, verify the new account is unlocked
|
// If the account gets changed, verify the new account is unlocked
|
||||||
@@ -136,11 +137,11 @@ export class StateService<
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (runMigrations) {
|
if (runMigrations) {
|
||||||
await migrate(this.storageService, this.logService);
|
await this.migrationRunner.run();
|
||||||
} else {
|
} else {
|
||||||
// It may have been requested to not run the migrations but we should defensively not
|
// It may have been requested to not run the migrations but we should defensively not
|
||||||
// continue this method until migrations have a chance to be completed elsewhere.
|
// continue this method until migrations have a chance to be completed elsewhere.
|
||||||
await waitForMigrations(this.storageService, this.logService);
|
await this.migrationRunner.waitForCompletion();
|
||||||
}
|
}
|
||||||
|
|
||||||
await this.state().then(async (state) => {
|
await this.state().then(async (state) => {
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
export { migrate, CURRENT_VERSION } from "./migrate";
|
export { createMigrationBuilder, waitForMigrations, CURRENT_VERSION } from "./migrate";
|
||||||
|
|||||||
@@ -5,34 +5,7 @@ import { LogService } from "../platform/abstractions/log.service";
|
|||||||
// eslint-disable-next-line import/no-restricted-paths -- Needed to interface with storage locations
|
// eslint-disable-next-line import/no-restricted-paths -- Needed to interface with storage locations
|
||||||
import { AbstractStorageService } from "../platform/abstractions/storage.service";
|
import { AbstractStorageService } from "../platform/abstractions/storage.service";
|
||||||
|
|
||||||
import { CURRENT_VERSION, currentVersion, migrate } from "./migrate";
|
import { currentVersion } from "./migrate";
|
||||||
import { MigrationBuilder } from "./migration-builder";
|
|
||||||
|
|
||||||
jest.mock("./migration-builder", () => {
|
|
||||||
return {
|
|
||||||
MigrationBuilder: {
|
|
||||||
create: jest.fn().mockReturnThis(),
|
|
||||||
},
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("migrate", () => {
|
|
||||||
it("should not run migrations if state is empty", async () => {
|
|
||||||
const storage = mock<AbstractStorageService>();
|
|
||||||
const logService = mock<LogService>();
|
|
||||||
storage.get.mockReturnValueOnce(null);
|
|
||||||
await migrate(storage, logService);
|
|
||||||
expect(MigrationBuilder.create).not.toHaveBeenCalled();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should set to current version if state is empty", async () => {
|
|
||||||
const storage = mock<AbstractStorageService>();
|
|
||||||
const logService = mock<LogService>();
|
|
||||||
storage.get.mockReturnValueOnce(null);
|
|
||||||
await migrate(storage, logService);
|
|
||||||
expect(storage.save).toHaveBeenCalledWith("stateVersion", CURRENT_VERSION);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("currentVersion", () => {
|
describe("currentVersion", () => {
|
||||||
let storage: MockProxy<AbstractStorageService>;
|
let storage: MockProxy<AbstractStorageService>;
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import { LogService } from "../platform/abstractions/log.service";
|
|||||||
import { AbstractStorageService } from "../platform/abstractions/storage.service";
|
import { AbstractStorageService } from "../platform/abstractions/storage.service";
|
||||||
|
|
||||||
import { MigrationBuilder } from "./migration-builder";
|
import { MigrationBuilder } from "./migration-builder";
|
||||||
import { MigrationHelper } from "./migration-helper";
|
|
||||||
import { EverHadUserKeyMigrator } from "./migrations/10-move-ever-had-user-key-to-state-providers";
|
import { EverHadUserKeyMigrator } from "./migrations/10-move-ever-had-user-key-to-state-providers";
|
||||||
import { OrganizationKeyMigrator } from "./migrations/11-move-org-keys-to-state-providers";
|
import { OrganizationKeyMigrator } from "./migrations/11-move-org-keys-to-state-providers";
|
||||||
import { MoveEnvironmentStateToProviders } from "./migrations/12-move-environment-state-to-providers";
|
import { MoveEnvironmentStateToProviders } from "./migrations/12-move-environment-state-to-providers";
|
||||||
@@ -27,21 +26,8 @@ export const MIN_VERSION = 2;
|
|||||||
export const CURRENT_VERSION = 18;
|
export const CURRENT_VERSION = 18;
|
||||||
export type MinVersion = typeof MIN_VERSION;
|
export type MinVersion = typeof MIN_VERSION;
|
||||||
|
|
||||||
export async function migrate(
|
export function createMigrationBuilder() {
|
||||||
storageService: AbstractStorageService,
|
return MigrationBuilder.create()
|
||||||
logService: LogService,
|
|
||||||
): Promise<void> {
|
|
||||||
const migrationHelper = new MigrationHelper(
|
|
||||||
await currentVersion(storageService, logService),
|
|
||||||
storageService,
|
|
||||||
logService,
|
|
||||||
);
|
|
||||||
if (migrationHelper.currentVersion < 0) {
|
|
||||||
// Cannot determine state, assuming empty so we don't repeatedly apply a migration.
|
|
||||||
await storageService.save("stateVersion", CURRENT_VERSION);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
await MigrationBuilder.create()
|
|
||||||
.with(MinVersionMigrator)
|
.with(MinVersionMigrator)
|
||||||
.with(FixPremiumMigrator, 2, 3)
|
.with(FixPremiumMigrator, 2, 3)
|
||||||
.with(RemoveEverBeenUnlockedMigrator, 3, 4)
|
.with(RemoveEverBeenUnlockedMigrator, 3, 4)
|
||||||
@@ -58,9 +44,7 @@ export async function migrate(
|
|||||||
.with(FolderMigrator, 14, 15)
|
.with(FolderMigrator, 14, 15)
|
||||||
.with(LastSyncMigrator, 15, 16)
|
.with(LastSyncMigrator, 15, 16)
|
||||||
.with(EnablePasskeysMigrator, 16, 17)
|
.with(EnablePasskeysMigrator, 16, 17)
|
||||||
.with(AutofillSettingsKeyMigrator, 17, CURRENT_VERSION)
|
.with(AutofillSettingsKeyMigrator, 17, CURRENT_VERSION);
|
||||||
|
|
||||||
.migrate(migrationHelper);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function currentVersion(
|
export async function currentVersion(
|
||||||
|
|||||||
Reference in New Issue
Block a user