1
0
mirror of https://github.com/bitwarden/browser.git synced 2024-10-01 04:37:40 +02:00

PM-5273 Initial migration work for localData

This commit is contained in:
Carlos Gonçalves 2024-02-29 18:01:20 +00:00
parent b17239595d
commit 60ac34182d
No known key found for this signature in database
GPG Key ID: 8147F618E732EF25
10 changed files with 248 additions and 9 deletions

View File

@ -571,6 +571,7 @@ export default class MainBackground {
this.encryptService,
this.cipherFileUploadService,
this.configService,
this.stateProvider,
);
this.folderService = new FolderService(
this.cryptoService,

View File

@ -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),
),
);
}

View File

@ -460,6 +460,7 @@ export class Main {
this.encryptService,
this.cipherFileUploadService,
this.configService,
this.stateProvider,
);
this.folderService = new FolderService(

View File

@ -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,
],
},
{

View File

@ -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" });

View File

@ -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(

View File

@ -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);
});
});
});

View File

@ -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))]);
}
}

View File

@ -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);

View File

@ -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) {