1
0
mirror of https://github.com/bitwarden/browser.git synced 2025-01-31 22:51:28 +01:00

Rebase: Bad Commit

This commit is contained in:
Justin Baur 2023-09-25 14:53:27 -04:00
parent d0037bb257
commit d1d12ce61f
No known key found for this signature in database
GPG Key ID: 46438BBD28B69008
9 changed files with 392 additions and 1 deletions

View File

@ -93,6 +93,7 @@ import { EncryptServiceImplementation } from "@bitwarden/common/platform/service
import { MultithreadEncryptServiceImplementation } from "@bitwarden/common/platform/services/cryptography/multithread-encrypt.service.implementation";
import { EnvironmentService } from "@bitwarden/common/platform/services/environment.service";
import { FileUploadService } from "@bitwarden/common/platform/services/file-upload/file-upload.service";
import { ActiveUserStateProviderService, BaseActiveUserStateProviderService } from "@bitwarden/common/platform/services/state-provider.service";
import { StateService } from "@bitwarden/common/platform/services/state.service";
import { ValidationService } from "@bitwarden/common/platform/services/validation.service";
import { WebCryptoFunctionService } from "@bitwarden/common/platform/services/web-crypto-function.service";
@ -174,6 +175,7 @@ import { ModalService } from "./modal.service";
import { ThemingService } from "./theming/theming.service";
import { AbstractThemingService } from "./theming/theming.service.abstraction";
@NgModule({
declarations: [],
providers: [
@ -717,6 +719,15 @@ import { AbstractThemingService } from "./theming/theming.service.abstraction";
useClass: AuthRequestCryptoServiceImplementation,
deps: [CryptoServiceAbstraction],
},
{
provide: ActiveUserStateProviderService,
useClass: BaseActiveUserStateProviderService,
deps: [
// TODO: Do other storage services
StateServiceAbstraction,
AbstractStorageService
]
}
],
})
export class JslibServicesModule {}

View File

@ -0,0 +1,3 @@
export abstract class ActiveUserStateProviderService {
create: <T>(location: StorageLocation, domainToken: DomainToken<T>) => State<T>;
}

View File

@ -0,0 +1,3 @@
export abstract class GlobalStateProviderService {
create: <T>(location: StorageLocation, domainToken: DomainToken<T>) => State<T>;
}

View File

@ -0,0 +1,6 @@
import { Observable } from "rxjs"
export interface State<T> {
update: (configureState: (state: T) => void) => Promise<void>
state$: Observable<T>
}

View File

@ -0,0 +1,125 @@
import { BehaviorSubject, Observable, defer, distinctUntilChanged, filter, firstValueFrom, map, share, switchMap, tap } from "rxjs";
import { Jsonify } from "type-fest";
import { ActiveUserStateProviderService } from "../abstractions/active-user-state-provider.service";
import { StateService } from "../abstractions/state.service";
import { AbstractMemoryStorageService, AbstractStorageService } from "../abstractions/storage.service";
import { State } from "../interfaces/state";
import { DomainToken, StorageLocation } from "./default-global-state-provider.service";
class DefaultActiveUserState<T> implements State<T> {
private formattedKey$: Observable<string>;
// TODO: Use BitSubject
protected stateSubject: BehaviorSubject<T | null> = new BehaviorSubject<T | null>(null);
private stateSubject$ = this.stateSubject.asObservable();
state$: Observable<T>;
// Global:
// FolderService = <data>
// User (super flat)
// FolderService_{userId}_someData = <data>
// FolderService_{userId}_moreData = <data>
// -- or --
// User (not as flat)
// FolderService_{userId} = <data>
constructor(
private stateService: StateService,
private storageLocation: AbstractStorageService,
domainToken: DomainToken<T>) {
const unformattedKey = `${domainToken.domainName}_{userId}`;
// startWith?
this.formattedKey$ = this.stateService.activeAccount$
.pipe(
distinctUntilChanged(),
filter(account => account != null),
map(accountId => unformattedKey.replace("{userId}", accountId))
);
// TODO: Don't use async if possible
const activeAccountData$ = this.formattedKey$
.pipe(switchMap(async key => {
// TODO: Force this in the storages so I don't have to `as`
const jsonData = await this.storageLocation.get<T>(key) as Jsonify<T>;
const data = domainToken.serializer(jsonData);
return data;
}),
tap(data => this.stateSubject.next(data)),
// Share the execution
share()
);
// Whomever subscribes to this data, should be notified of updated data
// if someone calls my update() method, or the active user changes.
this.state$ = defer(() => {
const subscription = activeAccountData$.subscribe();
return this.stateSubject$
.pipe(tap({
complete: () => subscription.unsubscribe(),
}));
});
}
async update(configureState: (state: T) => void): Promise<void> {
// wait for lock
try {
const currentState = this.stateSubject.getValue();
configureState(currentState);
await this.storageLocation.save(await this.createKey(), currentState);
this.stateSubject.next(currentState);
}
finally {
// TODO: Free lock
}
}
private async createKey(): Promise<string> {
return await firstValueFrom(this.formattedKey$);
}
}
export class DefaultActiveUserStateProviderService implements ActiveUserStateProviderService {
private userStateCache: Record<string, DefaultActiveUserState<unknown>> = {};
constructor(
private stateService: StateService, // Inject the lightest weight service that provides accountUserId$
private memoryStorage: AbstractMemoryStorageService,
private diskStorage: AbstractStorageService,
private secureStorage: AbstractStorageService) {
}
create<T>(location: StorageLocation, domainToken: DomainToken<T>): DefaultActiveUserState<T> {
const locationDomainKey = `${location}_${domainToken.domainName}`;
const existingActiveUserState = this.userStateCache[locationDomainKey];
if (existingActiveUserState != null) {
// I have to cast out of the unknown generic but this should be safe if rules
// around domain token are made
return existingActiveUserState as DefaultActiveUserState<T>;
}
const newActiveUserState = new DefaultActiveUserState<T>(
this.stateService,
this.getLocation(location),
domainToken);
this.userStateCache[locationDomainKey] = newActiveUserState;
return newActiveUserState;
}
private getLocation(location: StorageLocation) {
switch (location) {
case "disk":
return this.diskStorage;
case "secure":
return this.secureStorage;
case "memory":
return this.memoryStorage;
}
}
}

View File

@ -0,0 +1,81 @@
import { BehaviorSubject, Observable, defer, distinctUntilChanged, filter, firstValueFrom, forkJoin, map, merge, share, switchMap, tap } from "rxjs";
import { Jsonify } from "type-fest";
import { ActiveUserStateProviderService } from "../abstractions/active-user-state-provider.service";
import { GlobalStateProviderService } from "../abstractions/global-state-provider.service";
import { StateService } from "../abstractions/state.service";
import { AbstractMemoryStorageService, AbstractStorageService } from "../abstractions/storage.service";
import { State } from "../interfaces/state";
// TODO: Move type
// TODO: How can we protect the creation of these so that platform can maintain the allowed creations?
export class DomainToken<T> {
constructor(
public domainName: string,
public serializer: (jsonData: Jsonify<T>) => T) {
}
}
// TODO: Move type
export type StorageLocation = "memory" | "disk" | "secure";
class DefaultGlobalState<T> implements State<T> {
protected stateSubject: BehaviorSubject<T | null> = new BehaviorSubject<T | null>(null);
state$: Observable<T>;
constructor(private storageLocation: AbstractStorageService, private domainToken: DomainToken<T>) {
this.state$ = this.stateSubject.asObservable();
}
async update(configureState: (state: T) => void): Promise<void> {
// wait for lock
try {
const currentState = this.stateSubject.getValue();
configureState(currentState);
await this.storageLocation.save(this.domainToken.domainName, currentState);
this.stateSubject.next(currentState);
}
finally {
// TODO: Free lock
}
}
}
export class DefaultGlobalStateProviderService implements GlobalStateProviderService {
private globalStateCache: Record<string, DefaultGlobalState<unknown>> = {};
constructor(
private memoryStorage: AbstractMemoryStorageService,
private diskStorage: AbstractStorageService,
private secureStorage: AbstractStorageService) {
}
create<T>(location: StorageLocation, domainToken: DomainToken<T>): DefaultGlobalState<T> {
const locationDomainKey = `${location}_${domainToken.domainName}`;
const existingGlobalState = this.globalStateCache[locationDomainKey];
if (existingGlobalState != null) {
// I have to cast out of the unknown generic but this should be safe if rules
// around domain token are made
return existingGlobalState as DefaultGlobalState<T>;
}
const newGlobalState = new DefaultGlobalState<T>(this.getLocation(location), domainToken);
this.globalStateCache[locationDomainKey] = newGlobalState;
return newGlobalState;
}
private getLocation(location: StorageLocation) {
switch (location) {
case "disk":
return this.diskStorage;
case "secure":
return this.secureStorage;
case "memory":
return this.memoryStorage;
}
}
}

View File

@ -0,0 +1,90 @@
import { matches, mock, mockReset, notNull } from "jest-mock-extended";
import { BehaviorSubject } from "rxjs";
import { Jsonify } from "type-fest";
import { StateService } from "../abstractions/state.service"
import { AbstractMemoryStorageService } from "../abstractions/storage.service";
import { DomainToken, BaseActiveUserStateProviderService } from "./default-global-state-provider.service"
class TestState {
constructor() {
}
date: Date;
array: string[]
// TODO: More complex data types
static fromJSON(jsonState: Jsonify<TestState>) {
const state = new TestState();
state.date = new Date(jsonState.date);
state.array = jsonState.array;
return state;
}
}
const fakeDomainToken = new DomainToken<TestState>("fake", TestState.fromJSON);
describe("BaseStateProviderService", () => {
const stateService = mock<StateService>();
const memoryStorageService = mock<AbstractMemoryStorageService>();
const diskStorageService = mock<AbstractMemoryStorageService>();
const activeAccountSubject = new BehaviorSubject<string>(undefined);
let stateProviderService: BaseActiveUserStateProviderService;
beforeEach(() => {
mockReset(stateService);
mockReset(memoryStorageService);
mockReset(diskStorageService);
stateService.activeAccount$ = activeAccountSubject;
stateProviderService = new BaseActiveUserStateProviderService(stateService, diskStorageService);
});
it("createUserState", async () => {
diskStorageService.get
.mockImplementation(async (key, options) => {
if (key == "fake_1") {
return {date: "2023-09-21T13:14:17.648Z", array: ["value1", "value2"]}
}
return undefined;
});
const fakeDomainState = stateProviderService.create(fakeDomainToken);
const subscribeCallback = jest.fn<void, [TestState]>();
const subscription = fakeDomainState.state$.subscribe(subscribeCallback);
// User signs in
activeAccountSubject.next("1");
await new Promise<void>(resolve => setTimeout(resolve, 10));
// Service does an update
fakeDomainState.update(state => state.array.push("value3"));
await new Promise<void>(resolve => setTimeout(resolve, 1));
subscription.unsubscribe();
// No user at the start, no data
expect(subscribeCallback).toHaveBeenNthCalledWith(1, null);
// Gotten starter user data
expect(subscribeCallback).toHaveBeenNthCalledWith(2, matches<TestState>(value => {
console.log("Called", value);
return true;
}));
// Gotten update callback data
expect(subscribeCallback).toHaveBeenNthCalledWith(3, matches<TestState>((value) => {
return typeof value.date == "object" &&
value.date.getFullYear() == 2023 &&
value.array.length == 3
}));
});
});

View File

@ -0,0 +1,59 @@
import { LogService } from "../platform/abstractions/log.service";
import { DomainToken } from "../platform/services/default-global-state-provider.service";
import { MigrationHelper } from "./migration-helper";
import { Migrator } from "./migrator";
export class OwnedMigrationHelper {
currentVersion: number;
logService: LogService;
constructor(
public domainToken: DomainToken<unknown>,
private migrationHelper: MigrationHelper) {
this.currentVersion = migrationHelper.currentVersion;
this.logService = migrationHelper.logService;
}
get<T>(key: string): Promise<T> {
return this.migrationHelper.get(key);
}
getFromOwned<T>(key: string): Promise<T> {
}
set<T>(key: string, value: T): Promise<void> {
// Create the key
return undefined;
}
info(message: string): void {
// Add domain info?
this.migrationHelper.info("$OwnedMigratonHelper: {message}");
}
getAccounts<ExpectedAccountType>(): Promise<{ userId: string; account: ExpectedAccountType; }[]> {
return this.migrationHelper.getAccounts();
}
}
export abstract class OwnedMigrator<TFrom extends number, TTo extends number, TState> extends Migrator<TFrom, TTo> {
constructor(public domainToken: DomainToken<TState>,
fromVersion: TFrom, toVersion: TTo) {
super(fromVersion, toVersion)
}
override async migrate(helper: MigrationHelper): Promise<void> {
// Create our custom helper
const ownedHelper = new OwnedMigrationHelper(this.domainToken, helper);
await this.migrateOwned(ownedHelper);
const accounts = await ownedHelper.getAccounts();
for (const account of accounts) {
await this.migrateUserOwned(account.userId, ownedHelper);
}
}
abstract migrateOwned(helper: OwnedMigrationHelper): Promise<void>;
abstract migrateUserOwned(userId: string, helper: OwnedMigrationHelper): Promise<void>;
}

View File

@ -1,10 +1,12 @@
import { BehaviorSubject, concatMap } from "rxjs";
import { Jsonify } from "type-fest";
import { CryptoService } from "../../../platform/abstractions/crypto.service";
import { I18nService } from "../../../platform/abstractions/i18n.service";
import { StateService } from "../../../platform/abstractions/state.service";
import { Utils } from "../../../platform/misc/utils";
import { SymmetricCryptoKey } from "../../../platform/models/domain/symmetric-crypto-key";
import { ActiveUserStateProviderService, DomainToken, State } from "../../../platform/services/default-global-state-provider.service";
import { CipherService } from "../../../vault/abstractions/cipher.service";
import { InternalFolderService as InternalFolderServiceAbstraction } from "../../../vault/abstractions/folder/folder.service.abstraction";
import { CipherData } from "../../../vault/models/data/cipher.data";
@ -12,6 +14,13 @@ import { FolderData } from "../../../vault/models/data/folder.data";
import { Folder } from "../../../vault/models/domain/folder";
import { FolderView } from "../../../vault/models/view/folder.view";
class UserFolderState {
static fromJSON(jsonState: Jsonify<UserFolderState>) {
const state = new UserFolderState();
return state;
}
}
export class FolderService implements InternalFolderServiceAbstraction {
protected _folders: BehaviorSubject<Folder[]> = new BehaviorSubject([]);
protected _folderViews: BehaviorSubject<FolderView[]> = new BehaviorSubject([]);
@ -19,12 +28,16 @@ export class FolderService implements InternalFolderServiceAbstraction {
folders$ = this._folders.asObservable();
folderViews$ = this._folderViews.asObservable();
private folderState: State<UserFolderState>;
constructor(
private cryptoService: CryptoService,
private i18nService: I18nService,
private cipherService: CipherService,
private stateService: StateService
private stateService: StateService,
private activeUserStateProviderService: ActiveUserStateProviderService
) {
this.folderState = activeUserStateProviderService.create<UserFolderState>(new DomainToken("folder", UserFolderState.fromJSON));
this.stateService.activeAccountUnlocked$
.pipe(
concatMap(async (unlocked) => {