From bf2d2cfbed3954efffa1d88f07af5e24da659078 Mon Sep 17 00:00:00 2001 From: Addison Beck Date: Tue, 19 Mar 2024 16:37:35 -0500 Subject: [PATCH] Migrate `autoConfirmFingerPrints` to `StateProvider` (#8337) * Fix a typo in the `StateDefinition` description * Introduce `OrganizationManagementPreferencesService` * Declare `OrganizationManagementPreferencesService` in DI * Update `autoConfirmFingerPrints` logic in emergency access files * Update `autoConfirmFingerPrints` logic in `people` files * Remove `autoConfirmFingerPrints` from `StateService` and `Account` * Migrate existing client data for `autoConfirmFingerPrints` * Update apps/web/src/app/admin-console/organizations/manage/user-confirm.component.ts Co-authored-by: Matt Gibson * Update apps/web/src/app/admin-console/organizations/manage/user-confirm.component.ts Co-authored-by: Matt Gibson * Use `set` instead of `update` for function names --------- Co-authored-by: Matt Gibson --- .../common/base.people.component.ts | 9 +- .../manage/user-confirm.component.ts | 6 +- .../organizations/members/people.component.ts | 6 +- .../emergency-access-confirm.component.ts | 6 +- .../emergency-access.component.ts | 6 +- .../providers/manage/people.component.ts | 6 +- .../src/services/jslib-services.module.ts | 7 ++ ...nization-management-preferences.service.ts | 22 ++++ ...ion-management-preferences.service.spec.ts | 44 ++++++++ ...nization-management-preferences.service.ts | 71 ++++++++++++ .../platform/abstractions/state.service.ts | 3 - .../src/platform/models/domain/account.ts | 1 - .../src/platform/services/state.service.ts | 18 ---- .../src/platform/state/state-definition.ts | 6 +- .../src/platform/state/state-definitions.ts | 7 ++ libs/common/src/state-migrations/migrate.ts | 6 +- ...rm-finger-prints-to-state-provider.spec.ts | 102 ++++++++++++++++++ ...confirm-finger-prints-to-state-provider.ts | 63 +++++++++++ 18 files changed, 346 insertions(+), 43 deletions(-) create mode 100644 libs/common/src/admin-console/abstractions/organization-management-preferences/organization-management-preferences.service.ts create mode 100644 libs/common/src/admin-console/services/organization-management-preferences/default-organization-management-preferences.service.spec.ts create mode 100644 libs/common/src/admin-console/services/organization-management-preferences/default-organization-management-preferences.service.ts create mode 100644 libs/common/src/state-migrations/migrations/43-move-auto-confirm-finger-prints-to-state-provider.spec.ts create mode 100644 libs/common/src/state-migrations/migrations/43-move-auto-confirm-finger-prints-to-state-provider.ts diff --git a/apps/web/src/app/admin-console/common/base.people.component.ts b/apps/web/src/app/admin-console/common/base.people.component.ts index 29303f4ccc2..0a1f4338ff8 100644 --- a/apps/web/src/app/admin-console/common/base.people.component.ts +++ b/apps/web/src/app/admin-console/common/base.people.component.ts @@ -1,10 +1,12 @@ import { Directive, ViewChild, ViewContainerRef } from "@angular/core"; +import { firstValueFrom } from "rxjs"; import { SearchPipe } from "@bitwarden/angular/pipes/search.pipe"; import { UserNamePipe } from "@bitwarden/angular/pipes/user-name.pipe"; import { ModalService } from "@bitwarden/angular/services/modal.service"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { SearchService } from "@bitwarden/common/abstractions/search.service"; +import { OrganizationManagementPreferencesService } from "@bitwarden/common/admin-console/abstractions/organization-management-preferences/organization-management-preferences.service"; import { OrganizationUserStatusType, OrganizationUserType, @@ -17,7 +19,6 @@ import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.se import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; -import { StateService } from "@bitwarden/common/platform/abstractions/state.service"; import { ValidationService } from "@bitwarden/common/platform/abstractions/validation.service"; import { Utils } from "@bitwarden/common/platform/misc/utils"; import { DialogService } from "@bitwarden/components"; @@ -109,8 +110,8 @@ export abstract class BasePeopleComponent< private logService: LogService, private searchPipe: SearchPipe, protected userNamePipe: UserNamePipe, - protected stateService: StateService, protected dialogService: DialogService, + protected organizationManagementPreferencesService: OrganizationManagementPreferencesService, ) {} abstract edit(user: UserType): void; @@ -351,7 +352,9 @@ export abstract class BasePeopleComponent< const publicKeyResponse = await this.apiService.getUserPublicKey(user.userId); const publicKey = Utils.fromB64ToArray(publicKeyResponse.publicKey); - const autoConfirm = await this.stateService.getAutoConfirmFingerPrints(); + const autoConfirm = await firstValueFrom( + this.organizationManagementPreferencesService.autoConfirmFingerPrints.state$, + ); if (autoConfirm == null || !autoConfirm) { const [modal] = await this.modalService.openViewRef( UserConfirmComponent, diff --git a/apps/web/src/app/admin-console/organizations/manage/user-confirm.component.ts b/apps/web/src/app/admin-console/organizations/manage/user-confirm.component.ts index 25f16aff65d..4f712f30a81 100644 --- a/apps/web/src/app/admin-console/organizations/manage/user-confirm.component.ts +++ b/apps/web/src/app/admin-console/organizations/manage/user-confirm.component.ts @@ -1,8 +1,8 @@ import { Component, EventEmitter, Input, OnInit, Output } from "@angular/core"; +import { OrganizationManagementPreferencesService } from "@bitwarden/common/admin-console/abstractions/organization-management-preferences/organization-management-preferences.service"; import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; -import { StateService } from "@bitwarden/common/platform/abstractions/state.service"; @Component({ selector: "app-user-confirm", @@ -22,7 +22,7 @@ export class UserConfirmComponent implements OnInit { constructor( private cryptoService: CryptoService, private logService: LogService, - private stateService: StateService, + private organizationManagementPreferencesService: OrganizationManagementPreferencesService, ) {} async ngOnInit() { @@ -45,7 +45,7 @@ export class UserConfirmComponent implements OnInit { } if (this.dontAskAgain) { - await this.stateService.setAutoConfirmFingerprints(true); + await this.organizationManagementPreferencesService.autoConfirmFingerPrints.set(true); } this.onConfirmedUser.emit(); diff --git a/apps/web/src/app/admin-console/organizations/members/people.component.ts b/apps/web/src/app/admin-console/organizations/members/people.component.ts index b3142125dfe..b2aedacc800 100644 --- a/apps/web/src/app/admin-console/organizations/members/people.component.ts +++ b/apps/web/src/app/admin-console/organizations/members/people.component.ts @@ -21,6 +21,7 @@ import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { SearchService } from "@bitwarden/common/abstractions/search.service"; import { OrganizationApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization/organization-api.service.abstraction"; import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; +import { OrganizationManagementPreferencesService } from "@bitwarden/common/admin-console/abstractions/organization-management-preferences/organization-management-preferences.service"; import { OrganizationUserService } from "@bitwarden/common/admin-console/abstractions/organization-user/organization-user.service"; import { OrganizationUserConfirmRequest } from "@bitwarden/common/admin-console/abstractions/organization-user/requests"; import { @@ -43,7 +44,6 @@ import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.se import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; -import { StateService } from "@bitwarden/common/platform/abstractions/state.service"; import { ValidationService } from "@bitwarden/common/platform/abstractions/validation.service"; import { CollectionService } from "@bitwarden/common/vault/abstractions/collection.service"; import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction"; @@ -117,7 +117,6 @@ export class PeopleComponent searchPipe: SearchPipe, userNamePipe: UserNamePipe, private syncService: SyncService, - stateService: StateService, private organizationService: OrganizationService, private organizationApiService: OrganizationApiServiceAbstraction, private organizationUserService: OrganizationUserService, @@ -125,6 +124,7 @@ export class PeopleComponent private router: Router, private groupService: GroupService, private collectionService: CollectionService, + organizationManagementPreferencesService: OrganizationManagementPreferencesService, ) { super( apiService, @@ -137,8 +137,8 @@ export class PeopleComponent logService, searchPipe, userNamePipe, - stateService, dialogService, + organizationManagementPreferencesService, ); } diff --git a/apps/web/src/app/auth/settings/emergency-access/confirm/emergency-access-confirm.component.ts b/apps/web/src/app/auth/settings/emergency-access/confirm/emergency-access-confirm.component.ts index 4afc60c9be3..3bfe90d48ed 100644 --- a/apps/web/src/app/auth/settings/emergency-access/confirm/emergency-access-confirm.component.ts +++ b/apps/web/src/app/auth/settings/emergency-access/confirm/emergency-access-confirm.component.ts @@ -3,9 +3,9 @@ import { Component, OnInit, Inject } from "@angular/core"; import { FormBuilder } from "@angular/forms"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; +import { OrganizationManagementPreferencesService } from "@bitwarden/common/admin-console/abstractions/organization-management-preferences/organization-management-preferences.service"; import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; -import { StateService } from "@bitwarden/common/platform/abstractions/state.service"; import { Utils } from "@bitwarden/common/platform/misc/utils"; import { DialogService } from "@bitwarden/components"; @@ -36,7 +36,7 @@ export class EmergencyAccessConfirmComponent implements OnInit { private formBuilder: FormBuilder, private apiService: ApiService, private cryptoService: CryptoService, - private stateService: StateService, + protected organizationManagementPreferencesService: OrganizationManagementPreferencesService, private logService: LogService, private dialogRef: DialogRef, ) {} @@ -63,7 +63,7 @@ export class EmergencyAccessConfirmComponent implements OnInit { } if (this.confirmForm.get("dontAskAgain").value) { - await this.stateService.setAutoConfirmFingerprints(true); + await this.organizationManagementPreferencesService.autoConfirmFingerPrints.set(true); } try { diff --git a/apps/web/src/app/auth/settings/emergency-access/emergency-access.component.ts b/apps/web/src/app/auth/settings/emergency-access/emergency-access.component.ts index ce8db4e9313..05e65405fb7 100644 --- a/apps/web/src/app/auth/settings/emergency-access/emergency-access.component.ts +++ b/apps/web/src/app/auth/settings/emergency-access/emergency-access.component.ts @@ -3,6 +3,7 @@ import { lastValueFrom, Observable, firstValueFrom } from "rxjs"; import { UserNamePipe } from "@bitwarden/angular/pipes/user-name.pipe"; import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; +import { OrganizationManagementPreferencesService } from "@bitwarden/common/admin-console/abstractions/organization-management-preferences/organization-management-preferences.service"; import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; @@ -64,6 +65,7 @@ export class EmergencyAccessComponent implements OnInit { private organizationService: OrganizationService, protected dialogService: DialogService, billingAccountProfileStateService: BillingAccountProfileStateService, + protected organizationManagementPreferencesService: OrganizationManagementPreferencesService, ) { this.canAccessPremium$ = billingAccountProfileStateService.hasPremiumFromAnySource$; } @@ -136,7 +138,9 @@ export class EmergencyAccessComponent implements OnInit { return; } - const autoConfirm = await this.stateService.getAutoConfirmFingerPrints(); + const autoConfirm = await firstValueFrom( + this.organizationManagementPreferencesService.autoConfirmFingerPrints.state$, + ); if (autoConfirm == null || !autoConfirm) { const dialogRef = EmergencyAccessConfirmComponent.open(this.dialogService, { data: { diff --git a/bitwarden_license/bit-web/src/app/admin-console/providers/manage/people.component.ts b/bitwarden_license/bit-web/src/app/admin-console/providers/manage/people.component.ts index 227e37984fd..b83daf24b52 100644 --- a/bitwarden_license/bit-web/src/app/admin-console/providers/manage/people.component.ts +++ b/bitwarden_license/bit-web/src/app/admin-console/providers/manage/people.component.ts @@ -7,6 +7,7 @@ import { UserNamePipe } from "@bitwarden/angular/pipes/user-name.pipe"; import { ModalService } from "@bitwarden/angular/services/modal.service"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { SearchService } from "@bitwarden/common/abstractions/search.service"; +import { OrganizationManagementPreferencesService } from "@bitwarden/common/admin-console/abstractions/organization-management-preferences/organization-management-preferences.service"; import { ProviderService } from "@bitwarden/common/admin-console/abstractions/provider.service"; import { ProviderUserStatusType, ProviderUserType } from "@bitwarden/common/admin-console/enums"; import { ProviderUserBulkRequest } from "@bitwarden/common/admin-console/models/request/provider/provider-user-bulk.request"; @@ -18,7 +19,6 @@ import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.se import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; -import { StateService } from "@bitwarden/common/platform/abstractions/state.service"; import { ValidationService } from "@bitwarden/common/platform/abstractions/validation.service"; import { DialogService } from "@bitwarden/components"; import { BasePeopleComponent } from "@bitwarden/web-vault/app/admin-console/common/base.people.component"; @@ -67,9 +67,9 @@ export class PeopleComponent logService: LogService, searchPipe: SearchPipe, userNamePipe: UserNamePipe, - stateService: StateService, private providerService: ProviderService, dialogService: DialogService, + organizationManagementPreferencesService: OrganizationManagementPreferencesService, ) { super( apiService, @@ -82,8 +82,8 @@ export class PeopleComponent logService, searchPipe, userNamePipe, - stateService, dialogService, + organizationManagementPreferencesService, ); } diff --git a/libs/angular/src/services/jslib-services.module.ts b/libs/angular/src/services/jslib-services.module.ts index a509897fd3a..498c9171b3e 100644 --- a/libs/angular/src/services/jslib-services.module.ts +++ b/libs/angular/src/services/jslib-services.module.ts @@ -27,6 +27,7 @@ import { OrgDomainInternalServiceAbstraction, OrgDomainServiceAbstraction, } from "@bitwarden/common/admin-console/abstractions/organization-domain/org-domain.service.abstraction"; +import { OrganizationManagementPreferencesService } from "@bitwarden/common/admin-console/abstractions/organization-management-preferences/organization-management-preferences.service"; import { OrganizationUserService } from "@bitwarden/common/admin-console/abstractions/organization-user/organization-user.service"; import { PolicyApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/policy/policy-api.service.abstraction"; import { @@ -38,6 +39,7 @@ import { OrganizationApiService } from "@bitwarden/common/admin-console/services import { OrganizationService } from "@bitwarden/common/admin-console/services/organization/organization.service"; import { OrgDomainApiService } from "@bitwarden/common/admin-console/services/organization-domain/org-domain-api.service"; import { OrgDomainService } from "@bitwarden/common/admin-console/services/organization-domain/org-domain.service"; +import { DefaultOrganizationManagementPreferencesService } from "@bitwarden/common/admin-console/services/organization-management-preferences/default-organization-management-preferences.service"; import { OrganizationUserServiceImplementation } from "@bitwarden/common/admin-console/services/organization-user/organization-user.service.implementation"; import { PolicyApiService } from "@bitwarden/common/admin-console/services/policy/policy-api.service"; import { PolicyService } from "@bitwarden/common/admin-console/services/policy/policy.service"; @@ -1048,6 +1050,11 @@ const typesafeProviders: Array = [ useClass: DefaultBillingAccountProfileStateService, deps: [ActiveUserStateProvider], }), + safeProvider({ + provide: OrganizationManagementPreferencesService, + useClass: DefaultOrganizationManagementPreferencesService, + deps: [StateProvider], + }), ]; function encryptServiceFactory( diff --git a/libs/common/src/admin-console/abstractions/organization-management-preferences/organization-management-preferences.service.ts b/libs/common/src/admin-console/abstractions/organization-management-preferences/organization-management-preferences.service.ts new file mode 100644 index 00000000000..2328165e4b2 --- /dev/null +++ b/libs/common/src/admin-console/abstractions/organization-management-preferences/organization-management-preferences.service.ts @@ -0,0 +1,22 @@ +import { Observable } from "rxjs"; + +/** + * Manages the state of a single organization management preference. + * Can be used to subscribe to or update a given property. + */ +export class OrganizationManagementPreference { + state$: Observable; + set: (value: T) => Promise; + + constructor(state$: Observable, setFn: (value: T) => Promise) { + this.state$ = state$; + this.set = setFn; + } +} + +/** + * Publishes state of a given user's personal settings relating to the user experience of managing an organization. + */ +export abstract class OrganizationManagementPreferencesService { + autoConfirmFingerPrints: OrganizationManagementPreference; +} diff --git a/libs/common/src/admin-console/services/organization-management-preferences/default-organization-management-preferences.service.spec.ts b/libs/common/src/admin-console/services/organization-management-preferences/default-organization-management-preferences.service.spec.ts new file mode 100644 index 00000000000..0d16e770eae --- /dev/null +++ b/libs/common/src/admin-console/services/organization-management-preferences/default-organization-management-preferences.service.spec.ts @@ -0,0 +1,44 @@ +import { MockProxy } from "jest-mock-extended"; +import { firstValueFrom } from "rxjs"; + +import { FakeStateProvider, mockAccountServiceWith } from "../../../../spec"; +import { UserId } from "../../../types/guid"; + +import { DefaultOrganizationManagementPreferencesService } from "./default-organization-management-preferences.service"; + +describe("OrganizationManagementPreferencesService", () => { + let stateProvider: FakeStateProvider; + let organizationManagementPreferencesService: MockProxy; + + beforeEach(() => { + const accountService = mockAccountServiceWith("userId" as UserId); + stateProvider = new FakeStateProvider(accountService); + organizationManagementPreferencesService = new DefaultOrganizationManagementPreferencesService( + stateProvider, + ); + }); + + describe("autoConfirmFingerPrints", () => { + it("returns false by default", async () => { + const value = await firstValueFrom( + organizationManagementPreferencesService.autoConfirmFingerPrints.state$, + ); + expect(value).toEqual(false); + }); + it("returns true if set", async () => { + await organizationManagementPreferencesService.autoConfirmFingerPrints.set(true); + const value = await firstValueFrom( + organizationManagementPreferencesService.autoConfirmFingerPrints.state$, + ); + expect(value).toEqual(true); + }); + it("can be unset", async () => { + await organizationManagementPreferencesService.autoConfirmFingerPrints.set(true); + await organizationManagementPreferencesService.autoConfirmFingerPrints.set(false); + const value = await firstValueFrom( + organizationManagementPreferencesService.autoConfirmFingerPrints.state$, + ); + expect(value).toEqual(false); + }); + }); +}); diff --git a/libs/common/src/admin-console/services/organization-management-preferences/default-organization-management-preferences.service.ts b/libs/common/src/admin-console/services/organization-management-preferences/default-organization-management-preferences.service.ts new file mode 100644 index 00000000000..e257b691638 --- /dev/null +++ b/libs/common/src/admin-console/services/organization-management-preferences/default-organization-management-preferences.service.ts @@ -0,0 +1,71 @@ +import { map } from "rxjs"; +import { Jsonify } from "type-fest"; + +import { + ORGANIZATION_MANAGEMENT_PREFERENCES_DISK, + StateProvider, + UserKeyDefinition, +} from "../../../platform/state"; +import { + OrganizationManagementPreference, + OrganizationManagementPreferencesService, +} from "../../abstractions/organization-management-preferences/organization-management-preferences.service"; + +/** + * This helper function can be used to quickly create `KeyDefinitions` that + * target the `ORGANIZATION_MANAGEMENT_PREFERENCES_DISK` `StateDefinition` + * and that have the default deserializer and `clearOn` options. Any + * contenders for options to add to this service will likely use these same + * options. + */ +function buildKeyDefinition(key: string): UserKeyDefinition { + return new UserKeyDefinition(ORGANIZATION_MANAGEMENT_PREFERENCES_DISK, key, { + deserializer: (obj: Jsonify) => obj as T, + clearOn: ["logout"], + }); +} + +export const AUTO_CONFIRM_FINGERPRINTS = buildKeyDefinition("autoConfirmFingerPrints"); + +export class DefaultOrganizationManagementPreferencesService + implements OrganizationManagementPreferencesService +{ + constructor(private stateProvider: StateProvider) {} + + autoConfirmFingerPrints = this.buildOrganizationManagementPreference( + AUTO_CONFIRM_FINGERPRINTS, + false, + ); + + /** + * Returns an `OrganizationManagementPreference` object for the provided + * `KeyDefinition`. This object can then be used by callers to subscribe to + * a given key, or set its value in state. + */ + private buildOrganizationManagementPreference( + keyDefinition: UserKeyDefinition, + defaultValue: T, + ) { + return new OrganizationManagementPreference( + this.getKeyFromState(keyDefinition).state$.pipe(map((x) => x ?? defaultValue)), + this.setKeyInStateFn(keyDefinition), + ); + } + + /** + * Returns the full `ActiveUserState` value for a given `keyDefinition` + * The returned value can then be called for subscription || set operations + */ + private getKeyFromState(keyDefinition: UserKeyDefinition) { + return this.stateProvider.getActive(keyDefinition); + } + + /** + * Returns a function that can be called to set the given `keyDefinition` in state + */ + private setKeyInStateFn(keyDefinition: UserKeyDefinition) { + return async (value: T) => { + await this.getKeyFromState(keyDefinition).update(() => value); + }; + } +} diff --git a/libs/common/src/platform/abstractions/state.service.ts b/libs/common/src/platform/abstractions/state.service.ts index 3413afe1825..3bed46e769a 100644 --- a/libs/common/src/platform/abstractions/state.service.ts +++ b/libs/common/src/platform/abstractions/state.service.ts @@ -54,9 +54,6 @@ export abstract class StateService { setAddEditCipherInfo: (value: AddEditCipherInfo, options?: StorageOptions) => Promise; getAlwaysShowDock: (options?: StorageOptions) => Promise; setAlwaysShowDock: (value: boolean, options?: StorageOptions) => Promise; - - getAutoConfirmFingerPrints: (options?: StorageOptions) => Promise; - setAutoConfirmFingerprints: (value: boolean, options?: StorageOptions) => Promise; getBiometricFingerprintValidated: (options?: StorageOptions) => Promise; setBiometricFingerprintValidated: (value: boolean, options?: StorageOptions) => Promise; getConvertAccountToKeyConnector: (options?: StorageOptions) => Promise; diff --git a/libs/common/src/platform/models/domain/account.ts b/libs/common/src/platform/models/domain/account.ts index 07efb505a5e..460ce25c15e 100644 --- a/libs/common/src/platform/models/domain/account.ts +++ b/libs/common/src/platform/models/domain/account.ts @@ -187,7 +187,6 @@ export class AccountProfile { } export class AccountSettings { - autoConfirmFingerPrints?: boolean; defaultUriMatch?: UriMatchStrategySetting; disableGa?: boolean; enableAlwaysOnTop?: boolean; diff --git a/libs/common/src/platform/services/state.service.ts b/libs/common/src/platform/services/state.service.ts index 31d69e868bf..0985c9949a5 100644 --- a/libs/common/src/platform/services/state.service.ts +++ b/libs/common/src/platform/services/state.service.ts @@ -300,24 +300,6 @@ export class StateService< ); } - async getAutoConfirmFingerPrints(options?: StorageOptions): Promise { - return ( - (await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskOptions()))) - ?.settings?.autoConfirmFingerPrints ?? false - ); - } - - async setAutoConfirmFingerprints(value: boolean, options?: StorageOptions): Promise { - const account = await this.getAccount( - this.reconcileOptions(options, await this.defaultOnDiskOptions()), - ); - account.settings.autoConfirmFingerPrints = value; - await this.saveAccount( - account, - this.reconcileOptions(options, await this.defaultOnDiskOptions()), - ); - } - async getBiometricFingerprintValidated(options?: StorageOptions): Promise { return ( (await this.getGlobals(this.reconcileOptions(options, await this.defaultOnDiskOptions()))) diff --git a/libs/common/src/platform/state/state-definition.ts b/libs/common/src/platform/state/state-definition.ts index 858be39855a..15dc9ff7574 100644 --- a/libs/common/src/platform/state/state-definition.ts +++ b/libs/common/src/platform/state/state-definition.ts @@ -2,9 +2,9 @@ * Default storage location options. * * `disk` generally means state that is accessible between restarts of the application, - * with the exception of the web client. In web this means `sessionStorage`. The data is - * through refreshes of the page but not available once that tab is closed or from any - * other tabs. + * with the exception of the web client. In web this means `sessionStorage`. The data + * persists through refreshes of the page but not available once that tab is closed or + * from any other tabs. * * `memory` means that the information stored there goes away during application * restarts. diff --git a/libs/common/src/platform/state/state-definitions.ts b/libs/common/src/platform/state/state-definitions.ts index 8115555b2ed..122578d2325 100644 --- a/libs/common/src/platform/state/state-definitions.ts +++ b/libs/common/src/platform/state/state-definitions.ts @@ -22,6 +22,13 @@ import { StateDefinition } from "./state-definition"; export const ORGANIZATIONS_DISK = new StateDefinition("organizations", "disk"); export const POLICIES_DISK = new StateDefinition("policies", "disk"); export const PROVIDERS_DISK = new StateDefinition("providers", "disk"); +export const ORGANIZATION_MANAGEMENT_PREFERENCES_DISK = new StateDefinition( + "organizationManagementPreferences", + "disk", + { + web: "disk-local", + }, +); // Billing export const BILLING_DISK = new StateDefinition("billing", "disk"); diff --git a/libs/common/src/state-migrations/migrate.ts b/libs/common/src/state-migrations/migrate.ts index 000f08b392b..1e3b925c2b2 100644 --- a/libs/common/src/state-migrations/migrate.ts +++ b/libs/common/src/state-migrations/migrate.ts @@ -38,6 +38,7 @@ import { RemoveEverBeenUnlockedMigrator } from "./migrations/4-remove-ever-been- import { OrganizationMigrator } from "./migrations/40-move-organization-state-to-state-provider"; import { EventCollectionMigrator } from "./migrations/41-move-event-collection-to-state-provider"; import { EnableFaviconMigrator } from "./migrations/42-move-enable-favicon-to-domain-settings-state-provider"; +import { AutoConfirmFingerPrintsMigrator } from "./migrations/43-move-auto-confirm-finger-prints-to-state-provider"; import { AddKeyTypeToOrgKeysMigrator } from "./migrations/5-add-key-type-to-org-keys"; import { RemoveLegacyEtmKeyMigrator } from "./migrations/6-remove-legacy-etm-key"; import { MoveBiometricAutoPromptToAccount } from "./migrations/7-move-biometric-auto-prompt-to-account"; @@ -46,7 +47,7 @@ import { MoveBrowserSettingsToGlobal } from "./migrations/9-move-browser-setting import { MinVersionMigrator } from "./migrations/min-version"; export const MIN_VERSION = 3; -export const CURRENT_VERSION = 42; +export const CURRENT_VERSION = 43; export type MinVersion = typeof MIN_VERSION; export function createMigrationBuilder() { @@ -90,7 +91,8 @@ export function createMigrationBuilder() { .with(MoveBillingAccountProfileMigrator, 38, 39) .with(OrganizationMigrator, 39, 40) .with(EventCollectionMigrator, 40, 41) - .with(EnableFaviconMigrator, 41, 42); + .with(EnableFaviconMigrator, 41, 42) + .with(AutoConfirmFingerPrintsMigrator, 42, CURRENT_VERSION); } export async function currentVersion( diff --git a/libs/common/src/state-migrations/migrations/43-move-auto-confirm-finger-prints-to-state-provider.spec.ts b/libs/common/src/state-migrations/migrations/43-move-auto-confirm-finger-prints-to-state-provider.spec.ts new file mode 100644 index 00000000000..359f582b8c0 --- /dev/null +++ b/libs/common/src/state-migrations/migrations/43-move-auto-confirm-finger-prints-to-state-provider.spec.ts @@ -0,0 +1,102 @@ +import { MockProxy } from "jest-mock-extended"; + +import { MigrationHelper } from "../migration-helper"; +import { mockMigrationHelper, runMigrator } from "../migration-helper.spec"; + +import { AutoConfirmFingerPrintsMigrator } from "./43-move-auto-confirm-finger-prints-to-state-provider"; + +function rollbackJSON() { + return { + authenticatedAccounts: ["user-1", "user-2"], + "user_user-1_organizationManagementPreferences_autoConfirmFingerPrints": true, + "user_user-2_organizationManagementPreferences_autoConfirmFingerPrints": false, + "user-1": { + settings: { + extra: "data", + }, + extra: "data", + }, + "user-2": { + settings: { + extra: "data", + }, + extra: "data", + }, + }; +} + +describe("AutoConfirmFingerPrintsMigrator", () => { + const migrator = new AutoConfirmFingerPrintsMigrator(42, 43); + + it("should migrate the autoConfirmFingerPrints property from the account settings object to a user StorageKey", async () => { + const output = await runMigrator(migrator, { + authenticatedAccounts: ["user-1", "user-2"] as const, + "user-1": { + settings: { + autoConfirmFingerPrints: true, + extra: "data", + }, + extra: "data", + }, + "user-2": { + settings: { + autoConfirmFingerPrints: false, + extra: "data", + }, + extra: "data", + }, + }); + + expect(output).toEqual({ + authenticatedAccounts: ["user-1", "user-2"], + "user_user-1_organizationManagementPreferences_autoConfirmFingerPrints": true, + "user_user-2_organizationManagementPreferences_autoConfirmFingerPrints": false, + "user-1": { + settings: { + extra: "data", + }, + extra: "data", + }, + "user-2": { + settings: { + extra: "data", + }, + extra: "data", + }, + }); + }); + + describe("rollback", () => { + let helper: MockProxy; + let sut: AutoConfirmFingerPrintsMigrator; + + const keyDefinitionLike = { + key: "autoConfirmFingerPrints", + stateDefinition: { + name: "organizationManagementPreferences", + }, + }; + + beforeEach(() => { + helper = mockMigrationHelper(rollbackJSON(), 43); + sut = new AutoConfirmFingerPrintsMigrator(42, 43); + }); + + it("should null the autoConfirmFingerPrints user StorageKey for each account", async () => { + await sut.rollback(helper); + expect(helper.setToUser).toHaveBeenCalledWith("user-1", keyDefinitionLike, null); + }); + + it("should add the autoConfirmFingerPrints property back to the account settings object", async () => { + await sut.rollback(helper); + + expect(helper.set).toHaveBeenCalledWith("user-1", { + settings: { + autoConfirmFingerPrints: true, + extra: "data", + }, + extra: "data", + }); + }); + }); +}); diff --git a/libs/common/src/state-migrations/migrations/43-move-auto-confirm-finger-prints-to-state-provider.ts b/libs/common/src/state-migrations/migrations/43-move-auto-confirm-finger-prints-to-state-provider.ts new file mode 100644 index 00000000000..246e3cf4365 --- /dev/null +++ b/libs/common/src/state-migrations/migrations/43-move-auto-confirm-finger-prints-to-state-provider.ts @@ -0,0 +1,63 @@ +import { KeyDefinitionLike, MigrationHelper, StateDefinitionLike } from "../migration-helper"; +import { Migrator } from "../migrator"; + +type ExpectedAccountState = { + settings?: { autoConfirmFingerPrints?: boolean }; +}; + +const ORGANIZATION_MANAGEMENT_PREFERENCES: StateDefinitionLike = { + name: "organizationManagementPreferences", +}; + +const AUTO_CONFIRM_FINGERPRINTS: KeyDefinitionLike = { + key: "autoConfirmFingerPrints", + stateDefinition: ORGANIZATION_MANAGEMENT_PREFERENCES, +}; + +export class AutoConfirmFingerPrintsMigrator extends Migrator<42, 43> { + async migrate(helper: MigrationHelper): Promise { + const legacyAccounts = await helper.getAccounts(); + + await Promise.all( + legacyAccounts.map(async ({ userId, account }) => { + if (account?.settings?.autoConfirmFingerPrints != null) { + await helper.setToUser( + userId, + AUTO_CONFIRM_FINGERPRINTS, + account.settings.autoConfirmFingerPrints, + ); + delete account?.settings?.autoConfirmFingerPrints; + await helper.set(userId, account); + } + }), + ); + } + + async rollback(helper: MigrationHelper): Promise { + async function rollbackUser(userId: string, account: ExpectedAccountState) { + let updatedAccount = false; + const autoConfirmFingerPrints = await helper.getFromUser( + userId, + AUTO_CONFIRM_FINGERPRINTS, + ); + + if (autoConfirmFingerPrints) { + if (!account) { + account = {}; + } + + updatedAccount = true; + account.settings.autoConfirmFingerPrints = autoConfirmFingerPrints; + await helper.setToUser(userId, AUTO_CONFIRM_FINGERPRINTS, null); + } + + if (updatedAccount) { + await helper.set(userId, account); + } + } + + const accounts = await helper.getAccounts(); + + await Promise.all(accounts.map(({ userId, account }) => rollbackUser(userId, account))); + } +}