1
0
mirror of https://github.com/bitwarden/browser.git synced 2024-11-22 11:45:59 +01:00

[PM-6404] Fully Integrate clearOn Events (#8134)

* Add New KeyDefinitionOption

* Add New Services

* Add WebStorageServiceProvider Tests

* Update Error Message

* Add `UserKeyDefinition`

* Fix Deserialization Helpers

* Fix KeyDefinition

* Add `UserKeyDefinition`

* Fix Deserialization Helpers

* Fix KeyDefinition

* Move `ClearEvent`

* Cleanup

* Fix Imports

* Integrate onClear Events

* Remove Accidental Addition

* Fix Test

* Add VaultTimeoutService Tests

* Only Register When Current State is Null

* Address Feedback
This commit is contained in:
Justin Baur 2024-03-04 14:33:25 -06:00 committed by GitHub
parent 4ba2717eb4
commit c3eba7f2c8
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
28 changed files with 471 additions and 345 deletions

View File

@ -93,6 +93,7 @@ import { KeyGenerationService } from "@bitwarden/common/platform/services/key-ge
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 { StorageServiceProvider } from "@bitwarden/common/platform/services/storage-service.provider";
import { SystemService } from "@bitwarden/common/platform/services/system.service";
import { WebCryptoFunctionService } from "@bitwarden/common/platform/services/web-crypto-function.service";
import {
@ -100,6 +101,7 @@ import {
DerivedStateProvider,
GlobalStateProvider,
SingleUserStateProvider,
StateEventRunnerService,
StateProvider,
} from "@bitwarden/common/platform/state";
/* eslint-disable import/no-restricted-paths -- We need the implementation to inject, but generally these should not be accessed */
@ -107,6 +109,7 @@ import { DefaultActiveUserStateProvider } from "@bitwarden/common/platform/state
import { DefaultGlobalStateProvider } from "@bitwarden/common/platform/state/implementations/default-global-state.provider";
import { DefaultSingleUserStateProvider } from "@bitwarden/common/platform/state/implementations/default-single-user-state.provider";
import { DefaultStateProvider } from "@bitwarden/common/platform/state/implementations/default-state.provider";
import { StateEventRegistrarService } from "@bitwarden/common/platform/state/state-event-registrar.service";
/* eslint-enable import/no-restricted-paths */
import { AvatarUpdateService } from "@bitwarden/common/services/account/avatar-update.service";
import { ApiService } from "@bitwarden/common/services/api.service";
@ -299,6 +302,7 @@ export default class MainBackground {
organizationVaultExportService: OrganizationVaultExportServiceAbstraction;
vaultSettingsService: VaultSettingsServiceAbstraction;
biometricStateService: BiometricStateService;
stateEventRunnerService: StateEventRunnerService;
ssoLoginService: SsoLoginServiceAbstraction;
// Passed to the popup for Safari to workaround issues with theming, downloading, etc.
@ -366,10 +370,24 @@ export default class MainBackground {
this.keyGenerationService,
)
: new BackgroundMemoryStorageService();
this.globalStateProvider = new DefaultGlobalStateProvider(
this.memoryStorageForStateProviders,
const storageServiceProvider = new StorageServiceProvider(
this.storageService as BrowserLocalStorageService,
this.memoryStorageForStateProviders,
);
this.globalStateProvider = new DefaultGlobalStateProvider(storageServiceProvider);
const stateEventRegistrarService = new StateEventRegistrarService(
this.globalStateProvider,
storageServiceProvider,
);
this.stateEventRunnerService = new StateEventRunnerService(
this.globalStateProvider,
storageServiceProvider,
);
this.encryptService = flagEnabled("multithreadDecryption")
? new MultithreadEncryptServiceImplementation(
this.cryptoFunctionService,
@ -379,8 +397,8 @@ export default class MainBackground {
: new EncryptServiceImplementation(this.cryptoFunctionService, this.logService, true);
this.singleUserStateProvider = new DefaultSingleUserStateProvider(
this.memoryStorageForStateProviders,
this.storageService as BrowserLocalStorageService,
storageServiceProvider,
stateEventRegistrarService,
);
this.accountService = new AccountServiceImplementation(
this.messagingService,
@ -389,8 +407,8 @@ export default class MainBackground {
);
this.activeUserStateProvider = new DefaultActiveUserStateProvider(
this.accountService,
this.memoryStorageForStateProviders,
this.storageService as BrowserLocalStorageService,
storageServiceProvider,
stateEventRegistrarService,
);
this.derivedStateProvider = new BackgroundDerivedStateProvider(
this.memoryStorageForStateProviders,
@ -666,6 +684,7 @@ export default class MainBackground {
this.stateService,
this.authService,
this.vaultTimeoutSettingsService,
this.stateEventRunnerService,
lockedCallback,
logoutCallback,
);
@ -1113,6 +1132,8 @@ export default class MainBackground {
this.searchService.clearIndex();
}
await this.stateEventRunnerService.handleEvent("logout", currentUserId as UserId);
if (newActiveUser != null) {
// we have a new active user, do not continue tearing down application
await this.switchAccount(newActiveUser as UserId);

View File

@ -21,6 +21,10 @@ import {
platformUtilsServiceFactory,
PlatformUtilsServiceInitOptions,
} from "../../platform/background/service-factories/platform-utils-service.factory";
import {
stateEventRunnerServiceFactory,
StateEventRunnerServiceInitOptions,
} from "../../platform/background/service-factories/state-event-runner-service.factory";
import {
StateServiceInitOptions,
stateServiceFactory,
@ -62,7 +66,8 @@ export type VaultTimeoutServiceInitOptions = VaultTimeoutServiceFactoryOptions &
SearchServiceInitOptions &
StateServiceInitOptions &
AuthServiceInitOptions &
VaultTimeoutSettingsServiceInitOptions;
VaultTimeoutSettingsServiceInitOptions &
StateEventRunnerServiceInitOptions;
export function vaultTimeoutServiceFactory(
cache: { vaultTimeoutService?: AbstractVaultTimeoutService } & CachedServices,
@ -84,6 +89,7 @@ export function vaultTimeoutServiceFactory(
await stateServiceFactory(cache, opts),
await authServiceFactory(cache, opts),
await vaultTimeoutSettingsServiceFactory(cache, opts),
await stateEventRunnerServiceFactory(cache, opts),
opts.vaultTimeoutServiceOptions.lockedCallback,
opts.vaultTimeoutServiceOptions.loggedOutCallback,
),

View File

@ -9,18 +9,20 @@ import {
import { CachedServices, FactoryOptions, factory } from "./factory-options";
import {
DiskStorageServiceInitOptions,
MemoryStorageServiceInitOptions,
observableDiskStorageServiceFactory,
observableMemoryStorageServiceFactory,
} from "./storage-service.factory";
StateEventRegistrarServiceInitOptions,
stateEventRegistrarServiceFactory,
} from "./state-event-registrar-service.factory";
import {
StorageServiceProviderInitOptions,
storageServiceProviderFactory,
} from "./storage-service-provider.factory";
type ActiveUserStateProviderFactory = FactoryOptions;
export type ActiveUserStateProviderInitOptions = ActiveUserStateProviderFactory &
AccountServiceInitOptions &
MemoryStorageServiceInitOptions &
DiskStorageServiceInitOptions;
StorageServiceProviderInitOptions &
StateEventRegistrarServiceInitOptions;
export async function activeUserStateProviderFactory(
cache: { activeUserStateProvider?: ActiveUserStateProvider } & CachedServices,
@ -33,8 +35,8 @@ export async function activeUserStateProviderFactory(
async () =>
new DefaultActiveUserStateProvider(
await accountServiceFactory(cache, opts),
await observableMemoryStorageServiceFactory(cache, opts),
await observableDiskStorageServiceFactory(cache, opts),
await storageServiceProviderFactory(cache, opts),
await stateEventRegistrarServiceFactory(cache, opts),
),
);
}

View File

@ -4,17 +4,14 @@ import { DefaultGlobalStateProvider } from "@bitwarden/common/platform/state/imp
import { CachedServices, FactoryOptions, factory } from "./factory-options";
import {
DiskStorageServiceInitOptions,
MemoryStorageServiceInitOptions,
observableDiskStorageServiceFactory,
observableMemoryStorageServiceFactory,
} from "./storage-service.factory";
StorageServiceProviderInitOptions,
storageServiceProviderFactory,
} from "./storage-service-provider.factory";
type GlobalStateProviderFactoryOptions = FactoryOptions;
export type GlobalStateProviderInitOptions = GlobalStateProviderFactoryOptions &
MemoryStorageServiceInitOptions &
DiskStorageServiceInitOptions;
StorageServiceProviderInitOptions;
export async function globalStateProviderFactory(
cache: { globalStateProvider?: GlobalStateProvider } & CachedServices,
@ -24,10 +21,6 @@ export async function globalStateProviderFactory(
cache,
"globalStateProvider",
opts,
async () =>
new DefaultGlobalStateProvider(
await observableMemoryStorageServiceFactory(cache, opts),
await observableDiskStorageServiceFactory(cache, opts),
),
async () => new DefaultGlobalStateProvider(await storageServiceProviderFactory(cache, opts)),
);
}

View File

@ -4,17 +4,19 @@ import { DefaultSingleUserStateProvider } from "@bitwarden/common/platform/state
import { CachedServices, FactoryOptions, factory } from "./factory-options";
import {
DiskStorageServiceInitOptions,
MemoryStorageServiceInitOptions,
observableDiskStorageServiceFactory,
observableMemoryStorageServiceFactory,
} from "./storage-service.factory";
StateEventRegistrarServiceInitOptions,
stateEventRegistrarServiceFactory,
} from "./state-event-registrar-service.factory";
import {
StorageServiceProviderInitOptions,
storageServiceProviderFactory,
} from "./storage-service-provider.factory";
type SingleUserStateProviderFactoryOptions = FactoryOptions;
export type SingleUserStateProviderInitOptions = SingleUserStateProviderFactoryOptions &
MemoryStorageServiceInitOptions &
DiskStorageServiceInitOptions;
StorageServiceProviderInitOptions &
StateEventRegistrarServiceInitOptions;
export async function singleUserStateProviderFactory(
cache: { singleUserStateProvider?: SingleUserStateProvider } & CachedServices,
@ -26,8 +28,8 @@ export async function singleUserStateProviderFactory(
opts,
async () =>
new DefaultSingleUserStateProvider(
await observableMemoryStorageServiceFactory(cache, opts),
await observableDiskStorageServiceFactory(cache, opts),
await storageServiceProviderFactory(cache, opts),
await stateEventRegistrarServiceFactory(cache, opts),
),
);
}

View File

@ -0,0 +1,33 @@
import { StateEventRunnerService } from "@bitwarden/common/platform/state";
import { CachedServices, FactoryOptions, factory } from "./factory-options";
import {
GlobalStateProviderInitOptions,
globalStateProviderFactory,
} from "./global-state-provider.factory";
import {
StorageServiceProviderInitOptions,
storageServiceProviderFactory,
} from "./storage-service-provider.factory";
type StateEventRunnerServiceFactoryOptions = FactoryOptions;
export type StateEventRunnerServiceInitOptions = StateEventRunnerServiceFactoryOptions &
GlobalStateProviderInitOptions &
StorageServiceProviderInitOptions;
export function stateEventRunnerServiceFactory(
cache: { stateEventRunnerService?: StateEventRunnerService } & CachedServices,
opts: StateEventRunnerServiceInitOptions,
): Promise<StateEventRunnerService> {
return factory(
cache,
"stateEventRunnerService",
opts,
async () =>
new StateEventRunnerService(
await globalStateProviderFactory(cache, opts),
await storageServiceProviderFactory(cache, opts),
),
);
}

View File

@ -60,11 +60,13 @@ import { MigrationBuilderService } from "@bitwarden/common/platform/services/mig
import { MigrationRunner } from "@bitwarden/common/platform/services/migration-runner";
import { NoopMessagingService } from "@bitwarden/common/platform/services/noop-messaging.service";
import { StateService } from "@bitwarden/common/platform/services/state.service";
import { StorageServiceProvider } from "@bitwarden/common/platform/services/storage-service.provider";
import {
ActiveUserStateProvider,
DerivedStateProvider,
GlobalStateProvider,
SingleUserStateProvider,
StateEventRunnerService,
StateProvider,
} from "@bitwarden/common/platform/state";
/* eslint-disable import/no-restricted-paths -- We need the implementation to inject, but generally these should not be accessed */
@ -73,6 +75,7 @@ import { DefaultDerivedStateProvider } from "@bitwarden/common/platform/state/im
import { DefaultGlobalStateProvider } from "@bitwarden/common/platform/state/implementations/default-global-state.provider";
import { DefaultSingleUserStateProvider } from "@bitwarden/common/platform/state/implementations/default-single-user-state.provider";
import { DefaultStateProvider } from "@bitwarden/common/platform/state/implementations/default-state.provider";
import { StateEventRegistrarService } from "@bitwarden/common/platform/state/state-event-registrar.service";
import { MemoryStorageService as MemoryStorageServiceForStateProviders } from "@bitwarden/common/platform/state/storage/memory-storage.service";
/* eslint-enable import/no-restricted-paths */
import { AuditService } from "@bitwarden/common/services/audit.service";
@ -208,6 +211,7 @@ export class Main {
derivedStateProvider: DerivedStateProvider;
stateProvider: StateProvider;
loginStrategyService: LoginStrategyServiceAbstraction;
stateEventRunnerService: StateEventRunnerService;
biometricStateService: BiometricStateService;
constructor() {
@ -249,14 +253,26 @@ export class Main {
this.memoryStorageService = new MemoryStorageService();
this.memoryStorageForStateProviders = new MemoryStorageServiceForStateProviders();
this.globalStateProvider = new DefaultGlobalStateProvider(
this.memoryStorageForStateProviders,
const storageServiceProvider = new StorageServiceProvider(
this.storageService,
this.memoryStorageForStateProviders,
);
this.globalStateProvider = new DefaultGlobalStateProvider(storageServiceProvider);
const stateEventRegistrarService = new StateEventRegistrarService(
this.globalStateProvider,
storageServiceProvider,
);
this.stateEventRunnerService = new StateEventRunnerService(
this.globalStateProvider,
storageServiceProvider,
);
this.singleUserStateProvider = new DefaultSingleUserStateProvider(
this.memoryStorageForStateProviders,
this.storageService,
storageServiceProvider,
stateEventRegistrarService,
);
this.messagingService = new NoopMessagingService();
@ -269,8 +285,8 @@ export class Main {
this.activeUserStateProvider = new DefaultActiveUserStateProvider(
this.accountService,
this.memoryStorageForStateProviders,
this.storageService,
storageServiceProvider,
stateEventRegistrarService,
);
this.derivedStateProvider = new DefaultDerivedStateProvider(
@ -530,6 +546,7 @@ export class Main {
this.stateService,
this.authService,
this.vaultTimeoutSettingsService,
this.stateEventRunnerService,
lockedCallback,
null,
);
@ -639,6 +656,9 @@ export class Main {
this.policyService.clear(userId),
this.passwordGenerationService.clear(),
]);
await this.stateEventRunnerService.handleEvent("logout", userId as UserId);
await this.stateService.clean();
process.env.BW_SESSION = null;
}

View File

@ -39,6 +39,7 @@ import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/pl
import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
import { SystemService } from "@bitwarden/common/platform/abstractions/system.service";
import { BiometricStateService } from "@bitwarden/common/platform/biometrics/biometric-state.service";
import { StateEventRunnerService } from "@bitwarden/common/platform/state";
import { PasswordGenerationServiceAbstraction } from "@bitwarden/common/tools/generator/password";
import { UserId } from "@bitwarden/common/types/guid";
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
@ -149,6 +150,7 @@ export class AppComponent implements OnInit, OnDestroy {
private configService: ConfigServiceAbstraction,
private dialogService: DialogService,
private biometricStateService: BiometricStateService,
private stateEventRunnerService: StateEventRunnerService,
) {}
ngOnInit() {
@ -219,13 +221,13 @@ export class AppComponent implements OnInit, OnDestroy {
const currentUser = await this.stateService.getUserId();
const accounts = await firstValueFrom(this.stateService.accounts$);
await this.vaultTimeoutService.lock(currentUser);
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
// eslint-disable-next-line @typescript-eslint/no-floating-promises
Promise.all(
Object.keys(accounts)
.filter((u) => u !== currentUser)
.map((u) => this.vaultTimeoutService.lock(u)),
);
for (const account of Object.keys(accounts)) {
if (account === currentUser) {
continue;
}
await this.vaultTimeoutService.lock(account);
}
break;
}
case "locked":
@ -583,6 +585,8 @@ export class AppComponent implements OnInit, OnDestroy {
await this.keyConnectorService.clear();
await this.biometricStateService.logout(userBeingLoggedOut as UserId);
await this.stateEventRunnerService.handleEvent("logout", userBeingLoggedOut as UserId);
preLogoutActiveUserId = this.activeUserId;
await this.stateService.clean({ userId: userBeingLoggedOut });
} finally {

View File

@ -13,11 +13,13 @@ import { MigrationBuilderService } from "@bitwarden/common/platform/services/mig
import { MigrationRunner } from "@bitwarden/common/platform/services/migration-runner";
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 */
import { StorageServiceProvider } from "@bitwarden/common/platform/services/storage-service.provider";
import { DefaultActiveUserStateProvider } from "@bitwarden/common/platform/state/implementations/default-active-user-state.provider";
import { DefaultDerivedStateProvider } from "@bitwarden/common/platform/state/implementations/default-derived-state.provider";
import { DefaultGlobalStateProvider } from "@bitwarden/common/platform/state/implementations/default-global-state.provider";
import { DefaultSingleUserStateProvider } from "@bitwarden/common/platform/state/implementations/default-single-user-state.provider";
import { DefaultStateProvider } from "@bitwarden/common/platform/state/implementations/default-state.provider";
import { StateEventRegistrarService } from "@bitwarden/common/platform/state/state-event-registrar.service";
import { MemoryStorageService as MemoryStorageServiceForStateProviders } from "@bitwarden/common/platform/state/storage/memory-storage.service";
/* eslint-enable import/no-restricted-paths */
@ -104,10 +106,11 @@ export class Main {
this.storageService = new ElectronStorageService(app.getPath("userData"), storageDefaults);
this.memoryStorageService = new MemoryStorageService();
this.memoryStorageForStateProviders = new MemoryStorageServiceForStateProviders();
const globalStateProvider = new DefaultGlobalStateProvider(
this.memoryStorageForStateProviders,
const storageServiceProvider = new StorageServiceProvider(
this.storageService,
this.memoryStorageForStateProviders,
);
const globalStateProvider = new DefaultGlobalStateProvider(storageServiceProvider);
const accountService = new AccountServiceImplementation(
new NoopMessagingService(),
@ -115,13 +118,18 @@ export class Main {
globalStateProvider,
);
const stateEventRegistrarService = new StateEventRegistrarService(
globalStateProvider,
storageServiceProvider,
);
const stateProvider = new DefaultStateProvider(
new DefaultActiveUserStateProvider(
accountService,
this.memoryStorageForStateProviders,
this.storageService,
storageServiceProvider,
stateEventRegistrarService,
),
new DefaultSingleUserStateProvider(this.memoryStorageForStateProviders, this.storageService),
new DefaultSingleUserStateProvider(storageServiceProvider, stateEventRegistrarService),
globalStateProvider,
new DefaultDerivedStateProvider(this.memoryStorageForStateProviders),
);

View File

@ -23,6 +23,7 @@ import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.servic
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
import { BiometricStateService } from "@bitwarden/common/platform/biometrics/biometric-state.service";
import { StateEventRunnerService } from "@bitwarden/common/platform/state";
import { PasswordGenerationServiceAbstraction } from "@bitwarden/common/tools/generator/password";
import { UserId } from "@bitwarden/common/types/guid";
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
@ -89,6 +90,7 @@ export class AppComponent implements OnDestroy, OnInit {
private configService: ConfigServiceAbstraction,
private dialogService: DialogService,
private biometricStateService: BiometricStateService,
private stateEventRunnerService: StateEventRunnerService,
private paymentMethodWarningService: PaymentMethodWarningService,
private organizationService: OrganizationService,
) {}
@ -284,6 +286,8 @@ export class AppComponent implements OnDestroy, OnInit {
this.paymentMethodWarningService.clear(),
]);
await this.stateEventRunnerService.handleEvent("logout", userId as UserId);
this.searchService.clearIndex();
this.authService.logOut(async () => {
if (expired) {

View File

@ -14,7 +14,6 @@ import {
} from "@bitwarden/angular/services/injection-tokens";
import { JslibServicesModule } from "@bitwarden/angular/services/jslib-services.module";
import { ModalService as ModalServiceAbstraction } from "@bitwarden/angular/services/modal.service";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { LoginService as LoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/login.service";
import { LoginService } from "@bitwarden/common/auth/services/login.service";
import { FileDownloadService } from "@bitwarden/common/platform/abstractions/file-download/file-download.service";
@ -28,21 +27,16 @@ import { StateFactory } from "@bitwarden/common/platform/factories/state-factory
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 {
ActiveUserStateProvider,
GlobalStateProvider,
SingleUserStateProvider,
} from "@bitwarden/common/platform/state";
// eslint-disable-next-line import/no-restricted-paths -- Implementation for memory storage
/* eslint-disable import/no-restricted-paths -- Implementation for memory storage */
import { StorageServiceProvider } from "@bitwarden/common/platform/services/storage-service.provider";
import { MemoryStorageService as MemoryStorageServiceForStateProviders } from "@bitwarden/common/platform/state/storage/memory-storage.service";
/* eslint-enable import/no-restricted-paths -- Implementation for memory storage */
import { PolicyListService } from "../admin-console/core/policy-list.service";
import { HtmlStorageService } from "../core/html-storage.service";
import { I18nService } from "../core/i18n.service";
import { WebActiveUserStateProvider } from "../platform/web-active-user-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 { WebStorageServiceProvider } from "../platform/web-storage-service.provider";
import { WindowStorageService } from "../platform/window-storage.service";
import { CollectionAdminService } from "../vault/core/collection-admin.service";
@ -124,24 +118,9 @@ import { WebPlatformUtilsService } from "./web-platform-utils.service";
useFactory: () => new WindowStorageService(window.localStorage),
},
{
provide: SingleUserStateProvider,
useClass: WebSingleUserStateProvider,
deps: [OBSERVABLE_MEMORY_STORAGE, OBSERVABLE_DISK_STORAGE, OBSERVABLE_DISK_LOCAL_STORAGE],
},
{
provide: ActiveUserStateProvider,
useClass: WebActiveUserStateProvider,
deps: [
AccountService,
OBSERVABLE_MEMORY_STORAGE,
OBSERVABLE_DISK_STORAGE,
OBSERVABLE_DISK_LOCAL_STORAGE,
],
},
{
provide: GlobalStateProvider,
useClass: WebGlobalStateProvider,
deps: [OBSERVABLE_MEMORY_STORAGE, OBSERVABLE_DISK_STORAGE, OBSERVABLE_DISK_LOCAL_STORAGE],
provide: StorageServiceProvider,
useClass: WebStorageServiceProvider,
deps: [OBSERVABLE_DISK_STORAGE, OBSERVABLE_MEMORY_STORAGE, OBSERVABLE_DISK_LOCAL_STORAGE],
},
{
provide: MigrationRunner,

View File

@ -1,44 +0,0 @@
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import {
AbstractMemoryStorageService,
AbstractStorageService,
ObservableStorageService,
} from "@bitwarden/common/platform/abstractions/storage.service";
import { UserKeyDefinition } from "@bitwarden/common/platform/state";
/* eslint-disable import/no-restricted-paths -- Needed to extend class & in platform owned code */
import { DefaultActiveUserStateProvider } from "@bitwarden/common/platform/state/implementations/default-active-user-state.provider";
import { StateDefinition } from "@bitwarden/common/platform/state/state-definition";
/* eslint-enable import/no-restricted-paths */
export class WebActiveUserStateProvider extends DefaultActiveUserStateProvider {
constructor(
accountService: AccountService,
memoryStorage: AbstractMemoryStorageService & ObservableStorageService,
sessionStorage: AbstractStorageService & ObservableStorageService,
private readonly diskLocalStorage: AbstractStorageService & ObservableStorageService,
) {
super(accountService, memoryStorage, sessionStorage);
}
protected override getLocationString(keyDefinition: UserKeyDefinition<unknown>): string {
return (
keyDefinition.stateDefinition.storageLocationOverrides["web"] ??
keyDefinition.stateDefinition.defaultStorageLocation
);
}
protected override getLocation(
stateDefinition: StateDefinition,
): AbstractStorageService & ObservableStorageService {
const location =
stateDefinition.storageLocationOverrides["web"] ?? stateDefinition.defaultStorageLocation;
switch (location) {
case "disk":
return this.diskStorage;
case "memory":
return this.memoryStorage;
case "disk-local":
return this.diskLocalStorage;
}
}
}

View File

@ -1,42 +0,0 @@
import {
AbstractMemoryStorageService,
AbstractStorageService,
ObservableStorageService,
} from "@bitwarden/common/platform/abstractions/storage.service";
import { KeyDefinition } from "@bitwarden/common/platform/state";
/* eslint-disable import/no-restricted-paths -- Needed to extend class & in platform owned code*/
import { DefaultGlobalStateProvider } from "@bitwarden/common/platform/state/implementations/default-global-state.provider";
import { StateDefinition } from "@bitwarden/common/platform/state/state-definition";
/* eslint-enable import/no-restricted-paths */
export class WebGlobalStateProvider extends DefaultGlobalStateProvider {
constructor(
memoryStorage: AbstractMemoryStorageService & ObservableStorageService,
sessionStorage: AbstractStorageService & ObservableStorageService,
private readonly diskLocalStorage: AbstractStorageService & ObservableStorageService,
) {
super(memoryStorage, sessionStorage);
}
protected getLocationString(keyDefinition: KeyDefinition<unknown>): string {
return (
keyDefinition.stateDefinition.storageLocationOverrides["web"] ??
keyDefinition.stateDefinition.defaultStorageLocation
);
}
protected override getLocation(
stateDefinition: StateDefinition,
): AbstractStorageService & ObservableStorageService {
const location =
stateDefinition.storageLocationOverrides["web"] ?? stateDefinition.defaultStorageLocation;
switch (location) {
case "disk":
return this.diskStorage;
case "memory":
return this.memoryStorage;
case "disk-local":
return this.diskLocalStorage;
}
}
}

View File

@ -1,43 +0,0 @@
import {
AbstractMemoryStorageService,
AbstractStorageService,
ObservableStorageService,
} from "@bitwarden/common/platform/abstractions/storage.service";
import { UserKeyDefinition } from "@bitwarden/common/platform/state";
/* eslint-disable import/no-restricted-paths -- Needed to extend service & and in platform owned file */
import { DefaultSingleUserStateProvider } from "@bitwarden/common/platform/state/implementations/default-single-user-state.provider";
import { StateDefinition } from "@bitwarden/common/platform/state/state-definition";
/* eslint-enable import/no-restricted-paths */
export class WebSingleUserStateProvider extends DefaultSingleUserStateProvider {
constructor(
memoryStorageService: AbstractMemoryStorageService & ObservableStorageService,
sessionStorageService: AbstractStorageService & ObservableStorageService,
private readonly diskLocalStorageService: AbstractStorageService & ObservableStorageService,
) {
super(memoryStorageService, sessionStorageService);
}
protected override getLocationString(keyDefinition: UserKeyDefinition<unknown>): string {
return (
keyDefinition.stateDefinition.storageLocationOverrides["web"] ??
keyDefinition.stateDefinition.defaultStorageLocation
);
}
protected override getLocation(
stateDefinition: StateDefinition,
): AbstractStorageService & ObservableStorageService {
const location =
stateDefinition.storageLocationOverrides["web"] ?? stateDefinition.defaultStorageLocation;
switch (location) {
case "disk":
return this.diskStorage;
case "memory":
return this.memoryStorage;
case "disk-local":
return this.diskLocalStorageService;
}
}
}

View File

@ -135,6 +135,7 @@ import { MigrationBuilderService } from "@bitwarden/common/platform/services/mig
import { MigrationRunner } from "@bitwarden/common/platform/services/migration-runner";
import { NoopNotificationsService } from "@bitwarden/common/platform/services/noop-notifications.service";
import { StateService } from "@bitwarden/common/platform/services/state.service";
import { StorageServiceProvider } from "@bitwarden/common/platform/services/storage-service.provider";
import { ValidationService } from "@bitwarden/common/platform/services/validation.service";
import { WebCryptoFunctionService } from "@bitwarden/common/platform/services/web-crypto-function.service";
import {
@ -150,6 +151,8 @@ import { DefaultDerivedStateProvider } from "@bitwarden/common/platform/state/im
import { DefaultGlobalStateProvider } from "@bitwarden/common/platform/state/implementations/default-global-state.provider";
import { DefaultSingleUserStateProvider } from "@bitwarden/common/platform/state/implementations/default-single-user-state.provider";
import { DefaultStateProvider } from "@bitwarden/common/platform/state/implementations/default-state.provider";
import { StateEventRegistrarService } from "@bitwarden/common/platform/state/state-event-registrar.service";
import { StateEventRunnerService } from "@bitwarden/common/platform/state/state-event-runner.service";
/* eslint-enable import/no-restricted-paths */
import { AvatarUpdateService } from "@bitwarden/common/services/account/avatar-update.service";
import { ApiService } from "@bitwarden/common/services/api.service";
@ -551,6 +554,7 @@ import { ModalService } from "./modal.service";
StateServiceAbstraction,
AuthServiceAbstraction,
VaultTimeoutSettingsServiceAbstraction,
StateEventRunnerService,
LOCKED_CALLBACK,
LOGOUT_CALLBACK,
],
@ -890,20 +894,35 @@ import { ModalService } from "./modal.service";
LogService,
],
},
{
provide: StorageServiceProvider,
useClass: StorageServiceProvider,
deps: [OBSERVABLE_DISK_STORAGE, OBSERVABLE_MEMORY_STORAGE],
},
{
provide: StateEventRegistrarService,
useClass: StateEventRegistrarService,
deps: [GlobalStateProvider, StorageServiceProvider],
},
{
provide: StateEventRunnerService,
useClass: StateEventRunnerService,
deps: [GlobalStateProvider, StorageServiceProvider],
},
{
provide: GlobalStateProvider,
useClass: DefaultGlobalStateProvider,
deps: [OBSERVABLE_MEMORY_STORAGE, OBSERVABLE_DISK_STORAGE],
deps: [StorageServiceProvider],
},
{
provide: ActiveUserStateProvider,
useClass: DefaultActiveUserStateProvider,
deps: [AccountServiceAbstraction, OBSERVABLE_MEMORY_STORAGE, OBSERVABLE_DISK_STORAGE],
deps: [AccountServiceAbstraction, StorageServiceProvider, StateEventRegistrarService],
},
{
provide: SingleUserStateProvider,
useClass: DefaultSingleUserStateProvider,
deps: [OBSERVABLE_MEMORY_STORAGE, OBSERVABLE_DISK_STORAGE],
deps: [StorageServiceProvider, StateEventRegistrarService],
},
{
provide: DerivedStateProvider,

View File

@ -65,6 +65,7 @@ export class FakeGlobalState<T> implements GlobalState<T> {
this.nextMock(newState);
return newState;
}
/** Tracks update values resolved by `FakeState.update` */
nextMock = jest.fn<void, [T]>();

View File

@ -1,3 +1,4 @@
import { mock } from "jest-mock-extended";
import { firstValueFrom, timeout } from "rxjs";
import { awaitAsync } from "../../../spec";
@ -14,9 +15,11 @@ import { DefaultDerivedStateProvider } from "../state/implementations/default-de
import { DefaultGlobalStateProvider } from "../state/implementations/default-global-state.provider";
import { DefaultSingleUserStateProvider } from "../state/implementations/default-single-user-state.provider";
import { DefaultStateProvider } from "../state/implementations/default-state.provider";
/* eslint-disable import/no-restricted-paths */
import { StateEventRegistrarService } from "../state/state-event-registrar.service";
/* eslint-enable import/no-restricted-paths */
import { EnvironmentService } from "./environment.service";
import { StorageServiceProvider } from "./storage-service.provider";
// There are a few main states EnvironmentService could be in when first used
// 1. Not initialized, no active user. Hopefully not to likely but possible
@ -26,6 +29,8 @@ import { EnvironmentService } from "./environment.service";
describe("EnvironmentService", () => {
let diskStorageService: FakeStorageService;
let memoryStorageService: FakeStorageService;
let storageServiceProvider: StorageServiceProvider;
const stateEventRegistrarService = mock<StateEventRegistrarService>();
let accountService: FakeAccountService;
let stateProvider: StateProvider;
@ -37,16 +42,17 @@ describe("EnvironmentService", () => {
beforeEach(async () => {
diskStorageService = new FakeStorageService();
memoryStorageService = new FakeStorageService();
storageServiceProvider = new StorageServiceProvider(diskStorageService, memoryStorageService);
accountService = mockAccountServiceWith(undefined);
stateProvider = new DefaultStateProvider(
new DefaultActiveUserStateProvider(
accountService,
memoryStorageService as any,
diskStorageService,
storageServiceProvider,
stateEventRegistrarService,
),
new DefaultSingleUserStateProvider(memoryStorageService as any, diskStorageService),
new DefaultGlobalStateProvider(memoryStorageService as any, diskStorageService),
new DefaultSingleUserStateProvider(storageServiceProvider, stateEventRegistrarService),
new DefaultGlobalStateProvider(storageServiceProvider),
new DefaultDerivedStateProvider(memoryStorageService),
);

View File

@ -3,17 +3,14 @@ import { mock } from "jest-mock-extended";
import { mockAccountServiceWith, trackEmissions } from "../../../../spec";
import { AuthenticationStatus } from "../../../auth/enums/authentication-status";
import { UserId } from "../../../types/guid";
import {
AbstractMemoryStorageService,
AbstractStorageService,
ObservableStorageService,
} from "../../abstractions/storage.service";
import { StorageServiceProvider } from "../../services/storage-service.provider";
import { StateEventRegistrarService } from "../state-event-registrar.service";
import { DefaultActiveUserStateProvider } from "./default-active-user-state.provider";
describe("DefaultActiveUserStateProvider", () => {
const memoryStorage = mock<AbstractMemoryStorageService & ObservableStorageService>();
const diskStorage = mock<AbstractStorageService & ObservableStorageService>();
const storageServiceProvider = mock<StorageServiceProvider>();
const stateEventRegistrarService = mock<StateEventRegistrarService>();
const userId = "userId" as UserId;
const accountInfo = {
id: userId,
@ -25,7 +22,11 @@ describe("DefaultActiveUserStateProvider", () => {
let sut: DefaultActiveUserStateProvider;
beforeEach(() => {
sut = new DefaultActiveUserStateProvider(accountService, memoryStorage, diskStorage);
sut = new DefaultActiveUserStateProvider(
accountService,
storageServiceProvider,
stateEventRegistrarService,
);
});
afterEach(() => {

View File

@ -2,13 +2,9 @@ import { Observable, map } from "rxjs";
import { AccountService } from "../../../auth/abstractions/account.service";
import { UserId } from "../../../types/guid";
import {
AbstractMemoryStorageService,
AbstractStorageService,
ObservableStorageService,
} from "../../abstractions/storage.service";
import { StorageServiceProvider } from "../../services/storage-service.provider";
import { KeyDefinition } from "../key-definition";
import { StateDefinition } from "../state-definition";
import { StateEventRegistrarService } from "../state-event-registrar.service";
import { UserKeyDefinition, isUserKeyDefinition } from "../user-key-definition";
import { ActiveUserState } from "../user-state";
import { ActiveUserStateProvider } from "../user-state.provider";
@ -21,9 +17,9 @@ export class DefaultActiveUserStateProvider implements ActiveUserStateProvider {
activeUserId$: Observable<UserId | undefined>;
constructor(
protected readonly accountService: AccountService,
protected readonly memoryStorage: AbstractMemoryStorageService & ObservableStorageService,
protected readonly diskStorage: AbstractStorageService & ObservableStorageService,
private readonly accountService: AccountService,
private readonly storageServiceProvider: StorageServiceProvider,
private readonly stateEventRegistrarService: StateEventRegistrarService,
) {
this.activeUserId$ = this.accountService.activeAccount$.pipe(map((account) => account?.id));
}
@ -32,7 +28,11 @@ export class DefaultActiveUserStateProvider implements ActiveUserStateProvider {
if (!isUserKeyDefinition(keyDefinition)) {
keyDefinition = UserKeyDefinition.fromBaseKeyDefinition(keyDefinition);
}
const cacheKey = this.buildCacheKey(keyDefinition);
const [location, storageService] = this.storageServiceProvider.get(
keyDefinition.stateDefinition.defaultStorageLocation,
keyDefinition.stateDefinition.storageLocationOverrides,
);
const cacheKey = this.buildCacheKey(location, keyDefinition);
const existingUserState = this.cache[cacheKey];
if (existingUserState != null) {
// I have to cast out of the unknown generic but this should be safe if rules
@ -40,36 +40,17 @@ export class DefaultActiveUserStateProvider implements ActiveUserStateProvider {
return existingUserState as ActiveUserState<T>;
}
const newUserState = this.buildActiveUserState(keyDefinition);
const newUserState = new DefaultActiveUserState<T>(
keyDefinition,
this.accountService,
storageService,
this.stateEventRegistrarService,
);
this.cache[cacheKey] = newUserState;
return newUserState;
}
private buildCacheKey(keyDefinition: UserKeyDefinition<unknown>) {
return `${this.getLocationString(keyDefinition)}_${keyDefinition.fullName}`;
}
protected buildActiveUserState<T>(keyDefinition: UserKeyDefinition<T>): ActiveUserState<T> {
return new DefaultActiveUserState<T>(
keyDefinition,
this.accountService,
this.getLocation(keyDefinition.stateDefinition),
);
}
protected getLocationString(keyDefinition: UserKeyDefinition<unknown>): string {
return keyDefinition.stateDefinition.defaultStorageLocation;
}
protected getLocation(stateDefinition: StateDefinition) {
// The default implementations don't support the client overrides
// it is up to the client to extend this class and add that support
const location = stateDefinition.defaultStorageLocation;
switch (location) {
case "disk":
return this.diskStorage;
case "memory":
return this.memoryStorage;
}
private buildCacheKey(location: string, keyDefinition: UserKeyDefinition<unknown>) {
return `${location}_${keyDefinition.fullName}`;
}
}

View File

@ -12,6 +12,7 @@ import { AccountInfo, AccountService } from "../../../auth/abstractions/account.
import { AuthenticationStatus } from "../../../auth/enums/authentication-status";
import { UserId } from "../../../types/guid";
import { StateDefinition } from "../state-definition";
import { StateEventRegistrarService } from "../state-event-registrar.service";
import { UserKeyDefinition } from "../user-key-definition";
import { DefaultActiveUserState } from "./default-active-user-state";
@ -42,6 +43,7 @@ const testKeyDefinition = new UserKeyDefinition<TestState>(testStateDefinition,
describe("DefaultActiveUserState", () => {
const accountService = mock<AccountService>();
let diskStorageService: FakeStorageService;
const stateEventRegistrarService = mock<StateEventRegistrarService>();
let activeAccountSubject: BehaviorSubject<{ id: UserId } & AccountInfo>;
let userState: DefaultActiveUserState<TestState>;
@ -50,7 +52,12 @@ describe("DefaultActiveUserState", () => {
accountService.activeAccount$ = activeAccountSubject;
diskStorageService = new FakeStorageService();
userState = new DefaultActiveUserState(testKeyDefinition, accountService, diskStorageService);
userState = new DefaultActiveUserState(
testKeyDefinition,
accountService,
diskStorageService,
stateEventRegistrarService,
);
});
const makeUserId = (id: string) => {
@ -391,6 +398,48 @@ describe("DefaultActiveUserState", () => {
"No active user at this time.",
);
});
it.each([null, undefined])(
"should register user key definition when state transitions from null-ish (%s) to non-null",
async (startingValue: TestState | null) => {
diskStorageService.internalUpdateStore({
"user_00000000-0000-1000-a000-000000000001_fake_fake": startingValue,
});
await userState.update(() => ({ array: ["one"], date: new Date() }));
expect(stateEventRegistrarService.registerEvents).toHaveBeenCalledWith(testKeyDefinition);
},
);
it("should not register user key definition when state has preexisting value", async () => {
diskStorageService.internalUpdateStore({
"user_00000000-0000-1000-a000-000000000001_fake_fake": {
date: new Date(2019, 1),
array: [],
},
});
await userState.update(() => ({ array: ["one"], date: new Date() }));
expect(stateEventRegistrarService.registerEvents).not.toHaveBeenCalled();
});
it.each([null, undefined])(
"should not register user key definition when setting value to null-ish (%s) value",
async (updatedValue: TestState | null) => {
diskStorageService.internalUpdateStore({
"user_00000000-0000-1000-a000-000000000001_fake_fake": {
date: new Date(2019, 1),
array: [],
},
});
await userState.update(() => updatedValue);
expect(stateEventRegistrarService.registerEvents).not.toHaveBeenCalled();
},
);
});
describe("update races", () => {

View File

@ -21,6 +21,7 @@ import {
AbstractStorageService,
ObservableStorageService,
} from "../../abstractions/storage.service";
import { StateEventRegistrarService } from "../state-event-registrar.service";
import { StateUpdateOptions, populateOptionsWithDefault } from "../state-update-options";
import { UserKeyDefinition } from "../user-key-definition";
import { ActiveUserState, CombinedState, activeMarker } from "../user-state";
@ -42,6 +43,7 @@ export class DefaultActiveUserState<T> implements ActiveUserState<T> {
protected keyDefinition: UserKeyDefinition<T>,
private accountService: AccountService,
private chosenStorageLocation: AbstractStorageService & ObservableStorageService,
private stateEventRegistrarService: StateEventRegistrarService,
) {
this.activeUserId$ = this.accountService.activeAccount$.pipe(
// We only care about the UserId but we do want to know about no user as well.
@ -150,6 +152,11 @@ export class DefaultActiveUserState<T> implements ActiveUserState<T> {
const newState = configureState(currentState, combinedDependencies);
await this.saveToStorage(key, newState);
if (newState != null && currentState == null) {
// Only register this state as something clearable on the first time it saves something
// worth deleting. This is helpful in making sure there is less of a race to adding events.
await this.stateEventRegistrarService.registerEvents(this.keyDefinition);
}
return [userId, newState];
}

View File

@ -1,25 +1,21 @@
import {
AbstractMemoryStorageService,
AbstractStorageService,
ObservableStorageService,
} from "../../abstractions/storage.service";
import { StorageServiceProvider } from "../../services/storage-service.provider";
import { GlobalState } from "../global-state";
import { GlobalStateProvider } from "../global-state.provider";
import { KeyDefinition } from "../key-definition";
import { StateDefinition } from "../state-definition";
import { DefaultGlobalState } from "./default-global-state";
export class DefaultGlobalStateProvider implements GlobalStateProvider {
private globalStateCache: Record<string, GlobalState<unknown>> = {};
constructor(
protected readonly memoryStorage: AbstractMemoryStorageService & ObservableStorageService,
protected readonly diskStorage: AbstractStorageService & ObservableStorageService,
) {}
constructor(private storageServiceProvider: StorageServiceProvider) {}
get<T>(keyDefinition: KeyDefinition<T>): GlobalState<T> {
const cacheKey = this.buildCacheKey(keyDefinition);
const [location, storageService] = this.storageServiceProvider.get(
keyDefinition.stateDefinition.defaultStorageLocation,
keyDefinition.stateDefinition.storageLocationOverrides,
);
const cacheKey = this.buildCacheKey(location, keyDefinition);
const existingGlobalState = this.globalStateCache[cacheKey];
if (existingGlobalState != null) {
// The cast into the actual generic is safe because of rules around key definitions
@ -27,30 +23,13 @@ export class DefaultGlobalStateProvider implements GlobalStateProvider {
return existingGlobalState as DefaultGlobalState<T>;
}
const newGlobalState = new DefaultGlobalState<T>(
keyDefinition,
this.getLocation(keyDefinition.stateDefinition),
);
const newGlobalState = new DefaultGlobalState<T>(keyDefinition, storageService);
this.globalStateCache[cacheKey] = newGlobalState;
return newGlobalState;
}
private buildCacheKey(keyDefinition: KeyDefinition<unknown>) {
return `${this.getLocationString(keyDefinition)}_${keyDefinition.fullName}`;
}
protected getLocationString(keyDefinition: KeyDefinition<unknown>): string {
return keyDefinition.stateDefinition.defaultStorageLocation;
}
protected getLocation(stateDefinition: StateDefinition) {
const location = stateDefinition.defaultStorageLocation;
switch (location) {
case "disk":
return this.diskStorage;
case "memory":
return this.memoryStorage;
}
private buildCacheKey(location: string, keyDefinition: KeyDefinition<unknown>) {
return `${location}_${keyDefinition.fullName}`;
}
}

View File

@ -1,11 +1,7 @@
import { UserId } from "../../../types/guid";
import {
AbstractMemoryStorageService,
AbstractStorageService,
ObservableStorageService,
} from "../../abstractions/storage.service";
import { StorageServiceProvider } from "../../services/storage-service.provider";
import { KeyDefinition } from "../key-definition";
import { StateDefinition } from "../state-definition";
import { StateEventRegistrarService } from "../state-event-registrar.service";
import { UserKeyDefinition, isUserKeyDefinition } from "../user-key-definition";
import { SingleUserState } from "../user-state";
import { SingleUserStateProvider } from "../user-state.provider";
@ -16,8 +12,8 @@ export class DefaultSingleUserStateProvider implements SingleUserStateProvider {
private cache: Record<string, SingleUserState<unknown>> = {};
constructor(
protected readonly memoryStorage: AbstractMemoryStorageService & ObservableStorageService,
protected readonly diskStorage: AbstractStorageService & ObservableStorageService,
private readonly storageServiceProvider: StorageServiceProvider,
private readonly stateEventRegistrarService: StateEventRegistrarService,
) {}
get<T>(
@ -27,7 +23,11 @@ export class DefaultSingleUserStateProvider implements SingleUserStateProvider {
if (!isUserKeyDefinition(keyDefinition)) {
keyDefinition = UserKeyDefinition.fromBaseKeyDefinition(keyDefinition);
}
const cacheKey = this.buildCacheKey(userId, keyDefinition);
const [location, storageService] = this.storageServiceProvider.get(
keyDefinition.stateDefinition.defaultStorageLocation,
keyDefinition.stateDefinition.storageLocationOverrides,
);
const cacheKey = this.buildCacheKey(location, userId, keyDefinition);
const existingUserState = this.cache[cacheKey];
if (existingUserState != null) {
// I have to cast out of the unknown generic but this should be safe if rules
@ -35,38 +35,21 @@ export class DefaultSingleUserStateProvider implements SingleUserStateProvider {
return existingUserState as SingleUserState<T>;
}
const newUserState = this.buildSingleUserState(userId, keyDefinition);
const newUserState = new DefaultSingleUserState<T>(
userId,
keyDefinition,
storageService,
this.stateEventRegistrarService,
);
this.cache[cacheKey] = newUserState;
return newUserState;
}
private buildCacheKey(userId: UserId, keyDefinition: UserKeyDefinition<unknown>) {
return `${this.getLocationString(keyDefinition)}_${keyDefinition.fullName}_${userId}`;
}
protected buildSingleUserState<T>(
private buildCacheKey(
location: string,
userId: UserId,
keyDefinition: UserKeyDefinition<T>,
): SingleUserState<T> {
return new DefaultSingleUserState<T>(
userId,
keyDefinition,
this.getLocation(keyDefinition.stateDefinition),
);
}
protected getLocationString(keyDefinition: UserKeyDefinition<unknown>): string {
return keyDefinition.stateDefinition.defaultStorageLocation;
}
protected getLocation(stateDefinition: StateDefinition) {
// The default implementations don't support the client overrides
// it is up to the client to extend this class and add that support
switch (stateDefinition.defaultStorageLocation) {
case "disk":
return this.diskStorage;
case "memory":
return this.memoryStorage;
}
keyDefinition: UserKeyDefinition<unknown>,
) {
return `${location}_${keyDefinition.fullName}_${userId}`;
}
}

View File

@ -3,6 +3,7 @@
* @jest-environment ../shared/test.environment.ts
*/
import { mock } from "jest-mock-extended";
import { firstValueFrom, of } from "rxjs";
import { Jsonify } from "type-fest";
@ -11,6 +12,7 @@ import { FakeStorageService } from "../../../../spec/fake-storage.service";
import { UserId } from "../../../types/guid";
import { Utils } from "../../misc/utils";
import { StateDefinition } from "../state-definition";
import { StateEventRegistrarService } from "../state-event-registrar.service";
import { UserKeyDefinition } from "../user-key-definition";
import { DefaultSingleUserState } from "./default-single-user-state";
@ -42,11 +44,17 @@ const userKey = testKeyDefinition.buildKey(userId);
describe("DefaultSingleUserState", () => {
let diskStorageService: FakeStorageService;
let userState: DefaultSingleUserState<TestState>;
const stateEventRegistrarService = mock<StateEventRegistrarService>();
const newData = { date: new Date() };
beforeEach(() => {
diskStorageService = new FakeStorageService();
userState = new DefaultSingleUserState(userId, testKeyDefinition, diskStorageService);
userState = new DefaultSingleUserState(
userId,
testKeyDefinition,
diskStorageService,
stateEventRegistrarService,
);
});
afterEach(() => {
@ -255,6 +263,49 @@ describe("DefaultSingleUserState", () => {
expect(emissions).toHaveLength(2);
expect(emissions).toEqual(expect.arrayContaining([initialState, newState]));
});
it.each([null, undefined])(
"should register user key definition when state transitions from null-ish (%s) to non-null",
async (startingValue: TestState | null) => {
const initialState: Record<string, TestState> = {};
initialState[userKey] = startingValue;
diskStorageService.internalUpdateStore(initialState);
await userState.update(() => ({ array: ["one"], date: new Date() }));
expect(stateEventRegistrarService.registerEvents).toHaveBeenCalledWith(testKeyDefinition);
},
);
it("should not register user key definition when state has preexisting value", async () => {
const initialState: Record<string, TestState> = {};
initialState[userKey] = {
date: new Date(2019, 1),
};
diskStorageService.internalUpdateStore(initialState);
await userState.update(() => ({ array: ["one"], date: new Date() }));
expect(stateEventRegistrarService.registerEvents).not.toHaveBeenCalled();
});
it.each([null, undefined])(
"should not register user key definition when setting value to null-ish (%s) value",
async (updatedValue: TestState | null) => {
const initialState: Record<string, TestState> = {};
initialState[userKey] = {
date: new Date(2019, 1),
};
diskStorageService.internalUpdateStore(initialState);
await userState.update(() => updatedValue);
expect(stateEventRegistrarService.registerEvents).not.toHaveBeenCalled();
},
);
});
describe("update races", () => {

View File

@ -18,6 +18,7 @@ import {
AbstractStorageService,
ObservableStorageService,
} from "../../abstractions/storage.service";
import { StateEventRegistrarService } from "../state-event-registrar.service";
import { StateUpdateOptions, populateOptionsWithDefault } from "../state-update-options";
import { UserKeyDefinition } from "../user-key-definition";
import { CombinedState, SingleUserState } from "../user-state";
@ -35,6 +36,7 @@ export class DefaultSingleUserState<T> implements SingleUserState<T> {
readonly userId: UserId,
private keyDefinition: UserKeyDefinition<T>,
private chosenLocation: AbstractStorageService & ObservableStorageService,
private stateEventRegistrarService: StateEventRegistrarService,
) {
this.storageKey = this.keyDefinition.buildKey(this.userId);
const initialStorageGet$ = defer(() => {
@ -100,6 +102,11 @@ export class DefaultSingleUserState<T> implements SingleUserState<T> {
const newState = configureState(currentState, combinedDependencies);
await this.chosenLocation.save(this.storageKey, newState);
if (newState != null && currentState == null) {
// Only register this state as something clearable on the first time it saves something
// worth deleting. This is helpful in making sure there is less of a race to adding events.
await this.stateEventRegistrarService.registerEvents(this.keyDefinition);
}
return newState;
}

View File

@ -1,8 +1,12 @@
import { mock } from "jest-mock-extended";
import { mockAccountServiceWith } from "../../../../spec/fake-account-service";
import { FakeStorageService } from "../../../../spec/fake-storage.service";
import { UserId } from "../../../types/guid";
import { StorageServiceProvider } from "../../services/storage-service.provider";
import { KeyDefinition } from "../key-definition";
import { StateDefinition } from "../state-definition";
import { StateEventRegistrarService } from "../state-event-registrar.service";
import { DefaultActiveUserState } from "./default-active-user-state";
import { DefaultActiveUserStateProvider } from "./default-active-user-state.provider";
@ -12,6 +16,9 @@ import { DefaultSingleUserState } from "./default-single-user-state";
import { DefaultSingleUserStateProvider } from "./default-single-user-state.provider";
describe("Specific State Providers", () => {
const storageServiceProvider = mock<StorageServiceProvider>();
const stateEventRegistrarService = mock<StateEventRegistrarService>();
let singleSut: DefaultSingleUserStateProvider;
let activeSut: DefaultActiveUserStateProvider;
let globalSut: DefaultGlobalStateProvider;
@ -19,19 +26,20 @@ describe("Specific State Providers", () => {
const fakeUser1 = "00000000-0000-1000-a000-000000000001" as UserId;
beforeEach(() => {
storageServiceProvider.get.mockImplementation((location) => {
return [location, new FakeStorageService()];
});
singleSut = new DefaultSingleUserStateProvider(
new FakeStorageService() as any,
new FakeStorageService(),
storageServiceProvider,
stateEventRegistrarService,
);
activeSut = new DefaultActiveUserStateProvider(
mockAccountServiceWith(null),
new FakeStorageService() as any,
new FakeStorageService(),
);
globalSut = new DefaultGlobalStateProvider(
new FakeStorageService() as any,
new FakeStorageService(),
storageServiceProvider,
stateEventRegistrarService,
);
globalSut = new DefaultGlobalStateProvider(storageServiceProvider);
});
const fakeDiskStateDefinition = new StateDefinition("fake", "disk");

View File

@ -11,6 +11,7 @@ import { MessagingService } from "../../platform/abstractions/messaging.service"
import { PlatformUtilsService } from "../../platform/abstractions/platform-utils.service";
import { StateService } from "../../platform/abstractions/state.service";
import { Account } from "../../platform/models/domain/account";
import { StateEventRunnerService } from "../../platform/state";
import { CipherService } from "../../vault/abstractions/cipher.service";
import { CollectionService } from "../../vault/abstractions/collection.service";
import { FolderService } from "../../vault/abstractions/folder/folder.service.abstraction";
@ -28,6 +29,7 @@ describe("VaultTimeoutService", () => {
let stateService: MockProxy<StateService>;
let authService: MockProxy<AuthService>;
let vaultTimeoutSettingsService: MockProxy<VaultTimeoutSettingsService>;
let stateEventRunnerService: MockProxy<StateEventRunnerService>;
let lockedCallback: jest.Mock<Promise<void>, [userId: string]>;
let loggedOutCallback: jest.Mock<Promise<void>, [expired: boolean, userId?: string]>;
@ -48,6 +50,7 @@ describe("VaultTimeoutService", () => {
stateService = mock();
authService = mock();
vaultTimeoutSettingsService = mock();
stateEventRunnerService = mock();
lockedCallback = jest.fn();
loggedOutCallback = jest.fn();
@ -73,6 +76,7 @@ describe("VaultTimeoutService", () => {
stateService,
authService,
vaultTimeoutSettingsService,
stateEventRunnerService,
lockedCallback,
loggedOutCallback,
);
@ -103,7 +107,8 @@ describe("VaultTimeoutService", () => {
return Promise.resolve(accounts[userId]?.authStatus);
});
stateService.getIsAuthenticated.mockImplementation((options) => {
return Promise.resolve(accounts[options.userId]?.isAuthenticated);
// Just like actual state service, if no userId is given fallback to active userId
return Promise.resolve(accounts[options.userId ?? globalSetups?.userId]?.isAuthenticated);
});
vaultTimeoutSettingsService.getVaultTimeout.mockImplementation((userId) => {
@ -337,4 +342,80 @@ describe("VaultTimeoutService", () => {
expectNoAction("1");
});
});
describe("lock", () => {
const setupLock = () => {
setupAccounts(
{
user1: {
authStatus: AuthenticationStatus.Unlocked,
isAuthenticated: true,
},
user2: {
authStatus: AuthenticationStatus.Unlocked,
isAuthenticated: true,
},
},
{
userId: "user1",
},
);
};
it("should call state event runner with currently active user if no user passed into lock", async () => {
setupLock();
await vaultTimeoutService.lock();
expect(stateEventRunnerService.handleEvent).toHaveBeenCalledWith("lock", "user1");
});
it("should call messaging service locked message if no user passed into lock", async () => {
setupLock();
await vaultTimeoutService.lock();
// Currently these pass `undefined` (or what they were given) as the userId back
// but we could change this to give the user that was locked (active) to these methods
// so they don't have to get it their own way, but that is a behavioral change that needs
// to be tested.
expect(messagingService.send).toHaveBeenCalledWith("locked", { userId: undefined });
});
it("should call locked callback if no user passed into lock", async () => {
setupLock();
await vaultTimeoutService.lock();
// Currently these pass `undefined` (or what they were given) as the userId back
// but we could change this to give the user that was locked (active) to these methods
// so they don't have to get it their own way, but that is a behavioral change that needs
// to be tested.
expect(lockedCallback).toHaveBeenCalledWith(undefined);
});
it("should call state event runner with user passed into lock", async () => {
setupLock();
await vaultTimeoutService.lock("user2");
expect(stateEventRunnerService.handleEvent).toHaveBeenCalledWith("lock", "user2");
});
it("should call messaging service locked message with user passed into lock", async () => {
setupLock();
await vaultTimeoutService.lock("user2");
expect(messagingService.send).toHaveBeenCalledWith("locked", { userId: "user2" });
});
it("should call locked callback with user passed into lock", async () => {
setupLock();
await vaultTimeoutService.lock("user2");
expect(lockedCallback).toHaveBeenCalledWith("user2");
});
});
});

View File

@ -11,6 +11,8 @@ import { CryptoService } from "../../platform/abstractions/crypto.service";
import { MessagingService } from "../../platform/abstractions/messaging.service";
import { PlatformUtilsService } from "../../platform/abstractions/platform-utils.service";
import { StateService } from "../../platform/abstractions/state.service";
import { StateEventRunnerService } from "../../platform/state";
import { UserId } from "../../types/guid";
import { CipherService } from "../../vault/abstractions/cipher.service";
import { CollectionService } from "../../vault/abstractions/collection.service";
import { FolderService } from "../../vault/abstractions/folder/folder.service.abstraction";
@ -29,6 +31,7 @@ export class VaultTimeoutService implements VaultTimeoutServiceAbstraction {
private stateService: StateService,
private authService: AuthService,
private vaultTimeoutSettingsService: VaultTimeoutSettingsService,
private stateEventRunnerService: StateEventRunnerService,
private lockedCallback: (userId?: string) => Promise<void> = null,
private loggedOutCallback: (expired: boolean, userId?: string) => Promise<void> = null,
) {}
@ -81,7 +84,9 @@ export class VaultTimeoutService implements VaultTimeoutServiceAbstraction {
await this.logOut(userId);
}
if (userId == null || userId === (await this.stateService.getUserId())) {
const currentUserId = await this.stateService.getUserId();
if (userId == null || userId === currentUserId) {
this.searchService.clearIndex();
await this.folderService.clearCache();
await this.collectionService.clearActiveUserCache();
@ -98,6 +103,11 @@ export class VaultTimeoutService implements VaultTimeoutServiceAbstraction {
await this.cipherService.clearCache(userId);
await this.stateEventRunnerService.handleEvent("lock", (userId ?? currentUserId) as UserId);
// FIXME: We should send the userId of the user that was locked, in the case of this method being passed
// undefined then it should give back the currentUserId. Better yet, this method shouldn't take
// an undefined userId at all. All receivers need to be checked for how they handle getting undefined.
this.messagingService.send("locked", { userId: userId });
if (this.lockedCallback != null) {