mirror of
https://github.com/bitwarden/browser.git
synced 2025-02-22 02:21:34 +01:00
PM-5273 Initial migration work for localData
This commit is contained in:
parent
b17239595d
commit
60ac34182d
@ -571,6 +571,7 @@ export default class MainBackground {
|
||||
this.encryptService,
|
||||
this.cipherFileUploadService,
|
||||
this.configService,
|
||||
this.stateProvider,
|
||||
);
|
||||
this.folderService = new FolderService(
|
||||
this.cryptoService,
|
||||
|
@ -42,6 +42,7 @@ import {
|
||||
i18nServiceFactory,
|
||||
I18nServiceInitOptions,
|
||||
} from "../../../platform/background/service-factories/i18n-service.factory";
|
||||
import { stateProviderFactory } from "../../../platform/background/service-factories/state-provider.factory";
|
||||
import {
|
||||
stateServiceFactory,
|
||||
StateServiceInitOptions,
|
||||
@ -81,6 +82,7 @@ export function cipherServiceFactory(
|
||||
await encryptServiceFactory(cache, opts),
|
||||
await cipherFileUploadServiceFactory(cache, opts),
|
||||
await configServiceFactory(cache, opts),
|
||||
await stateProviderFactory(cache, opts),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
@ -460,6 +460,7 @@ export class Main {
|
||||
this.encryptService,
|
||||
this.cipherFileUploadService,
|
||||
this.configService,
|
||||
this.stateProvider,
|
||||
);
|
||||
|
||||
this.folderService = new FolderService(
|
||||
|
@ -344,6 +344,7 @@ import { ModalService } from "./modal.service";
|
||||
encryptService: EncryptService,
|
||||
fileUploadService: CipherFileUploadServiceAbstraction,
|
||||
configService: ConfigServiceAbstraction,
|
||||
stateProvider: StateProvider,
|
||||
) =>
|
||||
new CipherService(
|
||||
cryptoService,
|
||||
@ -356,6 +357,7 @@ import { ModalService } from "./modal.service";
|
||||
encryptService,
|
||||
fileUploadService,
|
||||
configService,
|
||||
stateProvider,
|
||||
),
|
||||
deps: [
|
||||
CryptoServiceAbstraction,
|
||||
@ -368,6 +370,7 @@ import { ModalService } from "./modal.service";
|
||||
EncryptService,
|
||||
CipherFileUploadServiceAbstraction,
|
||||
ConfigServiceAbstraction,
|
||||
StateProvider,
|
||||
],
|
||||
},
|
||||
{
|
||||
|
@ -62,3 +62,5 @@ export const AUTOFILL_SETTINGS_DISK_LOCAL = new StateDefinition("autofillSetting
|
||||
export const VAULT_FILTER_DISK = new StateDefinition("vaultFilter", "disk", {
|
||||
web: "disk-local",
|
||||
});
|
||||
|
||||
export const LOCAL_DATA = new StateDefinition("localData", "disk", { web: "disk-local" });
|
||||
|
@ -17,6 +17,7 @@ import { RequirePasswordOnStartMigrator } from "./migrations/19-migrate-require-
|
||||
import { PrivateKeyMigrator } from "./migrations/20-move-private-key-to-state-providers";
|
||||
import { CollectionMigrator } from "./migrations/21-move-collections-state-to-state-provider";
|
||||
import { CollapsedGroupingsMigrator } from "./migrations/22-move-collapsed-groupings-to-state-provider";
|
||||
import { LocalDataMigrator } from "./migrations/23-move-local-data-to-state-provider";
|
||||
import { FixPremiumMigrator } from "./migrations/3-fix-premium";
|
||||
import { RemoveEverBeenUnlockedMigrator } from "./migrations/4-remove-ever-been-unlocked";
|
||||
import { AddKeyTypeToOrgKeysMigrator } from "./migrations/5-add-key-type-to-org-keys";
|
||||
@ -27,7 +28,7 @@ import { MoveBrowserSettingsToGlobal } from "./migrations/9-move-browser-setting
|
||||
import { MinVersionMigrator } from "./migrations/min-version";
|
||||
|
||||
export const MIN_VERSION = 2;
|
||||
export const CURRENT_VERSION = 22;
|
||||
export const CURRENT_VERSION = 23;
|
||||
export type MinVersion = typeof MIN_VERSION;
|
||||
|
||||
export function createMigrationBuilder() {
|
||||
@ -52,7 +53,8 @@ export function createMigrationBuilder() {
|
||||
.with(RequirePasswordOnStartMigrator, 18, 19)
|
||||
.with(PrivateKeyMigrator, 19, 20)
|
||||
.with(CollectionMigrator, 20, 21)
|
||||
.with(CollapsedGroupingsMigrator, 21, CURRENT_VERSION);
|
||||
.with(CollapsedGroupingsMigrator, 21, 22)
|
||||
.with(LocalDataMigrator, 22, CURRENT_VERSION);
|
||||
}
|
||||
|
||||
export async function currentVersion(
|
||||
|
@ -0,0 +1,117 @@
|
||||
import { MockProxy } from "jest-mock-extended";
|
||||
|
||||
import { MigrationHelper } from "../migration-helper";
|
||||
import { mockMigrationHelper } from "../migration-helper.spec";
|
||||
|
||||
import { LocalDataMigrator } from "./23-move-local-data-to-state-provider";
|
||||
|
||||
function exampleJSON() {
|
||||
return {
|
||||
global: {
|
||||
otherStuff: "otherStuff1",
|
||||
},
|
||||
authenticatedAccounts: ["user-1", "user-2"],
|
||||
"user-1": {
|
||||
localData: [
|
||||
{
|
||||
"6865ba55-7966-4d63-b743-b12000d49631": {
|
||||
lastUsedDate: 1708950970632,
|
||||
},
|
||||
},
|
||||
{
|
||||
"f895f099-6739-4cca-9d61-b12200d04bfa": {
|
||||
lastUsedDate: 1709031916943,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
"user-2": {
|
||||
localData: {
|
||||
"fce9e7bf-bb3d-4650-897f-b12300f43541": {
|
||||
lastUsedDate: 1708950970632,
|
||||
},
|
||||
"ffb90bc2-a4ff-4571-b954-b12300f4207e": {
|
||||
lastUsedDate: 1709031916943,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function rollbackJSON() {
|
||||
return {
|
||||
authenticatedAccounts: ["user-1", "user-2"],
|
||||
"user-1": {
|
||||
localdata: [
|
||||
{
|
||||
"6865ba55-7966-4d63-b743-b12000d49631": {
|
||||
lastUsedDate: 1708950970632,
|
||||
},
|
||||
"f895f099-6739-4cca-9d61-b12200d04bfa": {
|
||||
lastUsedDate: 1709031916943,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
"user-2": {
|
||||
localdata: [
|
||||
{
|
||||
"fce9e7bf-bb3d-4650-897f-b12300f43541": {
|
||||
lastUsedDate: 1708950970632,
|
||||
},
|
||||
"ffb90bc2-a4ff-4571-b954-b12300f4207e": {
|
||||
lastUsedDate: 1709031916943,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
describe("LocalDataMigrator", () => {
|
||||
let helper: MockProxy<MigrationHelper>;
|
||||
let sut: LocalDataMigrator;
|
||||
const keyDefinitionLike = {
|
||||
key: "local_data",
|
||||
stateDefinition: {
|
||||
name: "localData",
|
||||
},
|
||||
};
|
||||
|
||||
describe("migrate", () => {
|
||||
beforeEach(() => {
|
||||
helper = mockMigrationHelper(exampleJSON(), 23);
|
||||
sut = new LocalDataMigrator(22, 23);
|
||||
});
|
||||
|
||||
it("should remove local data from all accounts", async () => {
|
||||
await sut.migrate(helper);
|
||||
expect(helper.set).toHaveBeenCalledWith("user-1", {
|
||||
localData: [
|
||||
{
|
||||
"6865ba55-7966-4d63-b743-b12000d49631": {
|
||||
lastUsedDate: 1708950970632,
|
||||
},
|
||||
},
|
||||
{
|
||||
"f895f099-6739-4cca-9d61-b12200d04bfa": {
|
||||
lastUsedDate: 1709031916943,
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("rollback", () => {
|
||||
beforeEach(() => {
|
||||
helper = mockMigrationHelper(rollbackJSON(), 23);
|
||||
sut = new LocalDataMigrator(22, 23);
|
||||
});
|
||||
|
||||
it.each(["user-1", "user-2"])("should null out new values", async (userId) => {
|
||||
await sut.rollback(helper);
|
||||
expect(helper.setToUser).toHaveBeenCalledWith(userId, keyDefinitionLike, null);
|
||||
});
|
||||
});
|
||||
});
|
@ -0,0 +1,50 @@
|
||||
import { KeyDefinitionLike, MigrationHelper } from "../migration-helper";
|
||||
import { Migrator } from "../migrator";
|
||||
|
||||
type ExpectedAccountType = {
|
||||
[cipherId: string]: LocalData;
|
||||
};
|
||||
|
||||
type LocalData = {
|
||||
lastUsedDate?: number;
|
||||
lastLaunched?: number;
|
||||
};
|
||||
|
||||
const LOCAL_DATA: KeyDefinitionLike = {
|
||||
key: "local_data",
|
||||
stateDefinition: {
|
||||
name: "localData",
|
||||
},
|
||||
};
|
||||
|
||||
export class LocalDataMigrator extends Migrator<22, 23> {
|
||||
async migrate(helper: MigrationHelper): Promise<void> {
|
||||
const accounts = await helper.getAccounts<ExpectedAccountType>();
|
||||
async function migrateAccount(userId: string, account: ExpectedAccountType): Promise<void> {
|
||||
const value = account?.localData;
|
||||
if (value != null) {
|
||||
await helper.setToUser(userId, LOCAL_DATA, value);
|
||||
delete account.LocalData;
|
||||
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 value = await helper.getFromUser(userId, LOCAL_DATA);
|
||||
if (account) {
|
||||
account.localData = Object.assign(account.localData ?? {}, {
|
||||
localData: value,
|
||||
});
|
||||
await helper.set(userId, account);
|
||||
}
|
||||
await helper.setToUser(userId, LOCAL_DATA, null);
|
||||
}
|
||||
|
||||
await Promise.all([...accounts.map(({ userId, account }) => rollbackAccount(userId, account))]);
|
||||
}
|
||||
}
|
@ -1,6 +1,8 @@
|
||||
import { mock } from "jest-mock-extended";
|
||||
import { of } from "rxjs";
|
||||
|
||||
import { FakeAccountService, mockAccountServiceWith } from "../../../spec/fake-account-service";
|
||||
import { FakeStateProvider } from "../../../spec/fake-state-provider";
|
||||
import { makeStaticByteArray } from "../../../spec/utils";
|
||||
import { ApiService } from "../../abstractions/api.service";
|
||||
import { SearchService } from "../../abstractions/search.service";
|
||||
@ -11,10 +13,12 @@ import { CryptoService } from "../../platform/abstractions/crypto.service";
|
||||
import { EncryptService } from "../../platform/abstractions/encrypt.service";
|
||||
import { I18nService } from "../../platform/abstractions/i18n.service";
|
||||
import { StateService } from "../../platform/abstractions/state.service";
|
||||
import { Utils } from "../../platform/misc/utils";
|
||||
import { EncArrayBuffer } from "../../platform/models/domain/enc-array-buffer";
|
||||
import { EncString } from "../../platform/models/domain/enc-string";
|
||||
import { SymmetricCryptoKey } from "../../platform/models/domain/symmetric-crypto-key";
|
||||
import { ContainerService } from "../../platform/services/container.service";
|
||||
import { UserId } from "../../types/guid";
|
||||
import { CipherKey, OrgKey } from "../../types/key";
|
||||
import { CipherFileUploadService } from "../abstractions/file-upload/cipher-file-upload.service";
|
||||
import { UriMatchType, FieldType } from "../enums";
|
||||
@ -94,6 +98,8 @@ const cipherData: CipherData = {
|
||||
},
|
||||
],
|
||||
};
|
||||
const mockUserId = Utils.newGuid() as UserId;
|
||||
let accountService: FakeAccountService;
|
||||
|
||||
describe("Cipher Service", () => {
|
||||
const cryptoService = mock<CryptoService>();
|
||||
@ -106,6 +112,8 @@ describe("Cipher Service", () => {
|
||||
const searchService = mock<SearchService>();
|
||||
const encryptService = mock<EncryptService>();
|
||||
const configService = mock<ConfigServiceAbstraction>();
|
||||
accountService = mockAccountServiceWith(mockUserId);
|
||||
const stateProvider = new FakeStateProvider(accountService);
|
||||
|
||||
let cipherService: CipherService;
|
||||
let cipherObj: Cipher;
|
||||
@ -127,6 +135,7 @@ describe("Cipher Service", () => {
|
||||
encryptService,
|
||||
cipherFileUploadService,
|
||||
configService,
|
||||
stateProvider,
|
||||
);
|
||||
|
||||
cipherObj = new Cipher(cipherData);
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { firstValueFrom } from "rxjs";
|
||||
import { Observable, firstValueFrom } from "rxjs";
|
||||
import { SemVer } from "semver";
|
||||
|
||||
import { ApiService } from "../../abstractions/api.service";
|
||||
@ -20,12 +20,14 @@ import Domain from "../../platform/models/domain/domain-base";
|
||||
import { EncArrayBuffer } from "../../platform/models/domain/enc-array-buffer";
|
||||
import { EncString } from "../../platform/models/domain/enc-string";
|
||||
import { SymmetricCryptoKey } from "../../platform/models/domain/symmetric-crypto-key";
|
||||
import { ActiveUserState, KeyDefinition, LOCAL_DATA, StateProvider } from "../../platform/state";
|
||||
import { UserKey, OrgKey } from "../../types/key";
|
||||
import { CipherService as CipherServiceAbstraction } from "../abstractions/cipher.service";
|
||||
import { CipherFileUploadService } from "../abstractions/file-upload/cipher-file-upload.service";
|
||||
import { FieldType, UriMatchType } from "../enums";
|
||||
import { CipherType } from "../enums/cipher-type";
|
||||
import { CipherData } from "../models/data/cipher.data";
|
||||
import { LocalData } from "../models/data/local.data";
|
||||
import { Attachment } from "../models/domain/attachment";
|
||||
import { Card } from "../models/domain/card";
|
||||
import { Cipher } from "../models/domain/cipher";
|
||||
@ -54,11 +56,21 @@ import { PasswordHistoryView } from "../models/view/password-history.view";
|
||||
|
||||
const CIPHER_KEY_ENC_MIN_SERVER_VER = new SemVer("2024.2.0");
|
||||
|
||||
const LOCAL_DATA_KEY = new KeyDefinition<Record<string, LocalData>>(LOCAL_DATA, "local_data", {
|
||||
deserializer: (obj) => obj,
|
||||
});
|
||||
|
||||
export class CipherService implements CipherServiceAbstraction {
|
||||
private sortedCiphersCache: SortedCiphersCache = new SortedCiphersCache(
|
||||
this.sortCiphersByLastUsed,
|
||||
);
|
||||
|
||||
localData$: Observable<Record<string, LocalData>>;
|
||||
|
||||
private localDataState: ActiveUserState<Record<string, LocalData>>;
|
||||
|
||||
private stateProviderFlag: boolean;
|
||||
|
||||
constructor(
|
||||
private cryptoService: CryptoService,
|
||||
private settingsService: SettingsService,
|
||||
@ -70,7 +82,15 @@ export class CipherService implements CipherServiceAbstraction {
|
||||
private encryptService: EncryptService,
|
||||
private cipherFileUploadService: CipherFileUploadService,
|
||||
private configService: ConfigServiceAbstraction,
|
||||
) {}
|
||||
private stateProvider: StateProvider,
|
||||
) {
|
||||
this.localDataState = this.stateProvider.getActive(LOCAL_DATA_KEY);
|
||||
|
||||
this.localData$ = this.localDataState.state$;
|
||||
|
||||
//TODO remove this before opening the PR and references to 5273
|
||||
this.stateProviderFlag = false;
|
||||
}
|
||||
|
||||
async getDecryptedCipherCache(): Promise<CipherView[]> {
|
||||
const decryptedCiphers = await this.stateService.getDecryptedCiphers();
|
||||
@ -266,11 +286,19 @@ export class CipherService implements CipherServiceAbstraction {
|
||||
return null;
|
||||
}
|
||||
|
||||
const localData = await this.stateService.getLocalData();
|
||||
//5273
|
||||
let localData;
|
||||
if (this.stateProviderFlag) {
|
||||
localData = await firstValueFrom(this.localData$);
|
||||
} else {
|
||||
localData = await this.stateService.getLocalData();
|
||||
}
|
||||
|
||||
return new Cipher(ciphers[id], localData ? localData[id] : null);
|
||||
}
|
||||
|
||||
async getAll(): Promise<Cipher[]> {
|
||||
//const localData = await firstValueFrom(this.localData$);
|
||||
const localData = await this.stateService.getLocalData();
|
||||
const ciphers = await this.stateService.getEncryptedCiphers();
|
||||
const response: Cipher[] = [];
|
||||
@ -437,7 +465,14 @@ export class CipherService implements CipherServiceAbstraction {
|
||||
}
|
||||
|
||||
async updateLastUsedDate(id: string): Promise<void> {
|
||||
let ciphersLocalData = await this.stateService.getLocalData();
|
||||
//5273
|
||||
let ciphersLocalData: { [cipherId: string]: LocalData } | Record<string, LocalData>;
|
||||
if (this.stateProviderFlag) {
|
||||
ciphersLocalData = await firstValueFrom(this.localData$);
|
||||
} else {
|
||||
ciphersLocalData = await this.stateService.getLocalData();
|
||||
}
|
||||
|
||||
if (!ciphersLocalData) {
|
||||
ciphersLocalData = {};
|
||||
}
|
||||
@ -450,7 +485,12 @@ export class CipherService implements CipherServiceAbstraction {
|
||||
};
|
||||
}
|
||||
|
||||
await this.stateService.setLocalData(ciphersLocalData);
|
||||
//5273
|
||||
if (this.stateProviderFlag) {
|
||||
await this.localDataState.update(() => ciphersLocalData);
|
||||
} else {
|
||||
await this.stateService.setLocalData(ciphersLocalData);
|
||||
}
|
||||
|
||||
const decryptedCipherCache = await this.stateService.getDecryptedCiphers();
|
||||
if (!decryptedCipherCache) {
|
||||
@ -468,7 +508,14 @@ export class CipherService implements CipherServiceAbstraction {
|
||||
}
|
||||
|
||||
async updateLastLaunchedDate(id: string): Promise<void> {
|
||||
let ciphersLocalData = await this.stateService.getLocalData();
|
||||
//5273
|
||||
let ciphersLocalData: { [cipherId: string]: LocalData } | Record<string, LocalData>;
|
||||
if (this.stateProviderFlag) {
|
||||
ciphersLocalData = await firstValueFrom(this.localData$);
|
||||
} else {
|
||||
ciphersLocalData = await this.stateService.getLocalData();
|
||||
}
|
||||
|
||||
if (!ciphersLocalData) {
|
||||
ciphersLocalData = {};
|
||||
}
|
||||
@ -481,7 +528,12 @@ export class CipherService implements CipherServiceAbstraction {
|
||||
};
|
||||
}
|
||||
|
||||
await this.stateService.setLocalData(ciphersLocalData);
|
||||
//5273
|
||||
if (this.stateProviderFlag) {
|
||||
await this.localDataState.update(() => ciphersLocalData);
|
||||
} else {
|
||||
await this.stateService.setLocalData(ciphersLocalData);
|
||||
}
|
||||
|
||||
const decryptedCipherCache = await this.stateService.getDecryptedCiphers();
|
||||
if (!decryptedCipherCache) {
|
||||
|
Loading…
Reference in New Issue
Block a user