1
0
mirror of https://github.com/bitwarden/browser.git synced 2025-02-23 02:31:26 +01: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.encryptService,
this.cipherFileUploadService, this.cipherFileUploadService,
this.configService, this.configService,
this.stateProvider,
); );
this.folderService = new FolderService( this.folderService = new FolderService(
this.cryptoService, this.cryptoService,

View File

@ -42,6 +42,7 @@ import {
i18nServiceFactory, i18nServiceFactory,
I18nServiceInitOptions, I18nServiceInitOptions,
} from "../../../platform/background/service-factories/i18n-service.factory"; } from "../../../platform/background/service-factories/i18n-service.factory";
import { stateProviderFactory } from "../../../platform/background/service-factories/state-provider.factory";
import { import {
stateServiceFactory, stateServiceFactory,
StateServiceInitOptions, StateServiceInitOptions,
@ -81,6 +82,7 @@ export function cipherServiceFactory(
await encryptServiceFactory(cache, opts), await encryptServiceFactory(cache, opts),
await cipherFileUploadServiceFactory(cache, opts), await cipherFileUploadServiceFactory(cache, opts),
await configServiceFactory(cache, opts), await configServiceFactory(cache, opts),
await stateProviderFactory(cache, opts),
), ),
); );
} }

View File

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

View File

@ -344,6 +344,7 @@ import { ModalService } from "./modal.service";
encryptService: EncryptService, encryptService: EncryptService,
fileUploadService: CipherFileUploadServiceAbstraction, fileUploadService: CipherFileUploadServiceAbstraction,
configService: ConfigServiceAbstraction, configService: ConfigServiceAbstraction,
stateProvider: StateProvider,
) => ) =>
new CipherService( new CipherService(
cryptoService, cryptoService,
@ -356,6 +357,7 @@ import { ModalService } from "./modal.service";
encryptService, encryptService,
fileUploadService, fileUploadService,
configService, configService,
stateProvider,
), ),
deps: [ deps: [
CryptoServiceAbstraction, CryptoServiceAbstraction,
@ -368,6 +370,7 @@ import { ModalService } from "./modal.service";
EncryptService, EncryptService,
CipherFileUploadServiceAbstraction, CipherFileUploadServiceAbstraction,
ConfigServiceAbstraction, 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", { export const VAULT_FILTER_DISK = new StateDefinition("vaultFilter", "disk", {
web: "disk-local", 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 { PrivateKeyMigrator } from "./migrations/20-move-private-key-to-state-providers";
import { CollectionMigrator } from "./migrations/21-move-collections-state-to-state-provider"; import { CollectionMigrator } from "./migrations/21-move-collections-state-to-state-provider";
import { CollapsedGroupingsMigrator } from "./migrations/22-move-collapsed-groupings-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 { FixPremiumMigrator } from "./migrations/3-fix-premium";
import { RemoveEverBeenUnlockedMigrator } from "./migrations/4-remove-ever-been-unlocked"; import { RemoveEverBeenUnlockedMigrator } from "./migrations/4-remove-ever-been-unlocked";
import { AddKeyTypeToOrgKeysMigrator } from "./migrations/5-add-key-type-to-org-keys"; 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"; import { MinVersionMigrator } from "./migrations/min-version";
export const MIN_VERSION = 2; export const MIN_VERSION = 2;
export const CURRENT_VERSION = 22; export const CURRENT_VERSION = 23;
export type MinVersion = typeof MIN_VERSION; export type MinVersion = typeof MIN_VERSION;
export function createMigrationBuilder() { export function createMigrationBuilder() {
@ -52,7 +53,8 @@ export function createMigrationBuilder() {
.with(RequirePasswordOnStartMigrator, 18, 19) .with(RequirePasswordOnStartMigrator, 18, 19)
.with(PrivateKeyMigrator, 19, 20) .with(PrivateKeyMigrator, 19, 20)
.with(CollectionMigrator, 20, 21) .with(CollectionMigrator, 20, 21)
.with(CollapsedGroupingsMigrator, 21, CURRENT_VERSION); .with(CollapsedGroupingsMigrator, 21, 22)
.with(LocalDataMigrator, 22, CURRENT_VERSION);
} }
export async function currentVersion( 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 { mock } from "jest-mock-extended";
import { of } from "rxjs"; 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 { makeStaticByteArray } from "../../../spec/utils";
import { ApiService } from "../../abstractions/api.service"; import { ApiService } from "../../abstractions/api.service";
import { SearchService } from "../../abstractions/search.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 { EncryptService } from "../../platform/abstractions/encrypt.service";
import { I18nService } from "../../platform/abstractions/i18n.service"; import { I18nService } from "../../platform/abstractions/i18n.service";
import { StateService } from "../../platform/abstractions/state.service"; import { StateService } from "../../platform/abstractions/state.service";
import { Utils } from "../../platform/misc/utils";
import { EncArrayBuffer } from "../../platform/models/domain/enc-array-buffer"; import { EncArrayBuffer } from "../../platform/models/domain/enc-array-buffer";
import { EncString } from "../../platform/models/domain/enc-string"; import { EncString } from "../../platform/models/domain/enc-string";
import { SymmetricCryptoKey } from "../../platform/models/domain/symmetric-crypto-key"; import { SymmetricCryptoKey } from "../../platform/models/domain/symmetric-crypto-key";
import { ContainerService } from "../../platform/services/container.service"; import { ContainerService } from "../../platform/services/container.service";
import { UserId } from "../../types/guid";
import { CipherKey, OrgKey } from "../../types/key"; import { CipherKey, OrgKey } from "../../types/key";
import { CipherFileUploadService } from "../abstractions/file-upload/cipher-file-upload.service"; import { CipherFileUploadService } from "../abstractions/file-upload/cipher-file-upload.service";
import { UriMatchType, FieldType } from "../enums"; import { UriMatchType, FieldType } from "../enums";
@ -94,6 +98,8 @@ const cipherData: CipherData = {
}, },
], ],
}; };
const mockUserId = Utils.newGuid() as UserId;
let accountService: FakeAccountService;
describe("Cipher Service", () => { describe("Cipher Service", () => {
const cryptoService = mock<CryptoService>(); const cryptoService = mock<CryptoService>();
@ -106,6 +112,8 @@ describe("Cipher Service", () => {
const searchService = mock<SearchService>(); const searchService = mock<SearchService>();
const encryptService = mock<EncryptService>(); const encryptService = mock<EncryptService>();
const configService = mock<ConfigServiceAbstraction>(); const configService = mock<ConfigServiceAbstraction>();
accountService = mockAccountServiceWith(mockUserId);
const stateProvider = new FakeStateProvider(accountService);
let cipherService: CipherService; let cipherService: CipherService;
let cipherObj: Cipher; let cipherObj: Cipher;
@ -127,6 +135,7 @@ describe("Cipher Service", () => {
encryptService, encryptService,
cipherFileUploadService, cipherFileUploadService,
configService, configService,
stateProvider,
); );
cipherObj = new Cipher(cipherData); cipherObj = new Cipher(cipherData);

View File

@ -1,4 +1,4 @@
import { firstValueFrom } from "rxjs"; import { Observable, firstValueFrom } from "rxjs";
import { SemVer } from "semver"; import { SemVer } from "semver";
import { ApiService } from "../../abstractions/api.service"; 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 { EncArrayBuffer } from "../../platform/models/domain/enc-array-buffer";
import { EncString } from "../../platform/models/domain/enc-string"; import { EncString } from "../../platform/models/domain/enc-string";
import { SymmetricCryptoKey } from "../../platform/models/domain/symmetric-crypto-key"; 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 { UserKey, OrgKey } from "../../types/key";
import { CipherService as CipherServiceAbstraction } from "../abstractions/cipher.service"; import { CipherService as CipherServiceAbstraction } from "../abstractions/cipher.service";
import { CipherFileUploadService } from "../abstractions/file-upload/cipher-file-upload.service"; import { CipherFileUploadService } from "../abstractions/file-upload/cipher-file-upload.service";
import { FieldType, UriMatchType } from "../enums"; import { FieldType, UriMatchType } from "../enums";
import { CipherType } from "../enums/cipher-type"; import { CipherType } from "../enums/cipher-type";
import { CipherData } from "../models/data/cipher.data"; import { CipherData } from "../models/data/cipher.data";
import { LocalData } from "../models/data/local.data";
import { Attachment } from "../models/domain/attachment"; import { Attachment } from "../models/domain/attachment";
import { Card } from "../models/domain/card"; import { Card } from "../models/domain/card";
import { Cipher } from "../models/domain/cipher"; 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 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 { export class CipherService implements CipherServiceAbstraction {
private sortedCiphersCache: SortedCiphersCache = new SortedCiphersCache( private sortedCiphersCache: SortedCiphersCache = new SortedCiphersCache(
this.sortCiphersByLastUsed, this.sortCiphersByLastUsed,
); );
localData$: Observable<Record<string, LocalData>>;
private localDataState: ActiveUserState<Record<string, LocalData>>;
private stateProviderFlag: boolean;
constructor( constructor(
private cryptoService: CryptoService, private cryptoService: CryptoService,
private settingsService: SettingsService, private settingsService: SettingsService,
@ -70,7 +82,15 @@ export class CipherService implements CipherServiceAbstraction {
private encryptService: EncryptService, private encryptService: EncryptService,
private cipherFileUploadService: CipherFileUploadService, private cipherFileUploadService: CipherFileUploadService,
private configService: ConfigServiceAbstraction, 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[]> { async getDecryptedCipherCache(): Promise<CipherView[]> {
const decryptedCiphers = await this.stateService.getDecryptedCiphers(); const decryptedCiphers = await this.stateService.getDecryptedCiphers();
@ -266,11 +286,19 @@ export class CipherService implements CipherServiceAbstraction {
return null; 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); return new Cipher(ciphers[id], localData ? localData[id] : null);
} }
async getAll(): Promise<Cipher[]> { async getAll(): Promise<Cipher[]> {
//const localData = await firstValueFrom(this.localData$);
const localData = await this.stateService.getLocalData(); const localData = await this.stateService.getLocalData();
const ciphers = await this.stateService.getEncryptedCiphers(); const ciphers = await this.stateService.getEncryptedCiphers();
const response: Cipher[] = []; const response: Cipher[] = [];
@ -437,7 +465,14 @@ export class CipherService implements CipherServiceAbstraction {
} }
async updateLastUsedDate(id: string): Promise<void> { 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) { if (!ciphersLocalData) {
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(); const decryptedCipherCache = await this.stateService.getDecryptedCiphers();
if (!decryptedCipherCache) { if (!decryptedCipherCache) {
@ -468,7 +508,14 @@ export class CipherService implements CipherServiceAbstraction {
} }
async updateLastLaunchedDate(id: string): Promise<void> { 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) { if (!ciphersLocalData) {
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(); const decryptedCipherCache = await this.stateService.getDecryptedCiphers();
if (!decryptedCipherCache) { if (!decryptedCipherCache) {