mirror of
https://github.com/bitwarden/browser
synced 2025-12-16 00:03:56 +00:00
[PM-5537] Migrate Biometric Prompts (#7771)
* Fix nextMock arguments
* Add state for biometric prompts
* Use biometric state for prompts
* Migrate biometric prompt data
* wire up biometric state to logouts
* Add migrator to migrate list
* Remove usages of prompt automatically
Explicitly list non-nulled state as intentional
* `npm run prettier` 🤖
* Fix web lock component
This commit is contained in:
@@ -17,6 +17,7 @@ import { RequirePasswordOnStartMigrator } from "./migrations/19-migrate-require-
|
||||
import { PrivateKeyMigrator } from "./migrations/20-move-private-key-to-state-providers";
|
||||
import { CollectionMigrator } from "./migrations/21-move-collections-state-to-state-provider";
|
||||
import { CollapsedGroupingsMigrator } from "./migrations/22-move-collapsed-groupings-to-state-provider";
|
||||
import { MoveBiometricPromptsToStateProviders } from "./migrations/23-move-biometric-prompts-to-state-providers";
|
||||
import { FixPremiumMigrator } from "./migrations/3-fix-premium";
|
||||
import { RemoveEverBeenUnlockedMigrator } from "./migrations/4-remove-ever-been-unlocked";
|
||||
import { AddKeyTypeToOrgKeysMigrator } from "./migrations/5-add-key-type-to-org-keys";
|
||||
@@ -27,7 +28,7 @@ import { MoveBrowserSettingsToGlobal } from "./migrations/9-move-browser-setting
|
||||
import { MinVersionMigrator } from "./migrations/min-version";
|
||||
|
||||
export const MIN_VERSION = 2;
|
||||
export const CURRENT_VERSION = 22;
|
||||
export const CURRENT_VERSION = 23;
|
||||
export type MinVersion = typeof MIN_VERSION;
|
||||
|
||||
export function createMigrationBuilder() {
|
||||
@@ -52,7 +53,8 @@ export function createMigrationBuilder() {
|
||||
.with(RequirePasswordOnStartMigrator, 18, 19)
|
||||
.with(PrivateKeyMigrator, 19, 20)
|
||||
.with(CollectionMigrator, 20, 21)
|
||||
.with(CollapsedGroupingsMigrator, 21, CURRENT_VERSION);
|
||||
.with(CollapsedGroupingsMigrator, 21, 22)
|
||||
.with(MoveBiometricPromptsToStateProviders, 22, CURRENT_VERSION);
|
||||
}
|
||||
|
||||
export async function currentVersion(
|
||||
|
||||
@@ -0,0 +1,131 @@
|
||||
import { MockProxy, any } from "jest-mock-extended";
|
||||
|
||||
import { MigrationHelper } from "../migration-helper";
|
||||
import { mockMigrationHelper } from "../migration-helper.spec";
|
||||
|
||||
import {
|
||||
MoveBiometricPromptsToStateProviders,
|
||||
DISMISSED_BIOMETRIC_REQUIRE_PASSWORD_ON_START_CALLOUT,
|
||||
PROMPT_AUTOMATICALLY,
|
||||
} from "./23-move-biometric-prompts-to-state-providers";
|
||||
|
||||
function exampleJSON() {
|
||||
return {
|
||||
global: {
|
||||
otherStuff: "otherStuff1",
|
||||
},
|
||||
authenticatedAccounts: ["user-1", "user-2", "user-3"],
|
||||
"user-1": {
|
||||
settings: {
|
||||
disableAutoBiometricsPrompt: false,
|
||||
dismissedBiometricRequirePasswordOnStartCallout: true,
|
||||
otherStuff: "otherStuff2",
|
||||
},
|
||||
otherStuff: "otherStuff3",
|
||||
},
|
||||
"user-2": {
|
||||
otherStuff: "otherStuff4",
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function rollbackJSON() {
|
||||
return {
|
||||
"user_user-1_biometricSettings_dismissedBiometricRequirePasswordOnStartCallout": true,
|
||||
"user_user-1_biometricSettings_promptAutomatically": "false",
|
||||
global: {
|
||||
otherStuff: "otherStuff1",
|
||||
},
|
||||
authenticatedAccounts: ["user-1", "user-2", "user-3"],
|
||||
"user-1": {
|
||||
settings: {
|
||||
otherStuff: "otherStuff2",
|
||||
},
|
||||
otherStuff: "otherStuff3",
|
||||
},
|
||||
"user-2": {
|
||||
otherStuff: "otherStuff4",
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
describe("MoveBiometricPromptsToStateProviders migrator", () => {
|
||||
let helper: MockProxy<MigrationHelper>;
|
||||
let sut: MoveBiometricPromptsToStateProviders;
|
||||
|
||||
describe("migrate", () => {
|
||||
beforeEach(() => {
|
||||
helper = mockMigrationHelper(exampleJSON(), 22);
|
||||
sut = new MoveBiometricPromptsToStateProviders(22, 23);
|
||||
});
|
||||
|
||||
it("should remove biometricUnlock, dismissedBiometricRequirePasswordOnStartCallout, and biometricEncryptionClientKeyHalf from all accounts", async () => {
|
||||
await sut.migrate(helper);
|
||||
expect(helper.set).toHaveBeenCalledTimes(2);
|
||||
expect(helper.set).toHaveBeenCalledWith("user-1", {
|
||||
settings: {
|
||||
otherStuff: "otherStuff2",
|
||||
},
|
||||
otherStuff: "otherStuff3",
|
||||
});
|
||||
expect(helper.set).toHaveBeenCalledWith("user-2", {
|
||||
otherStuff: "otherStuff4",
|
||||
});
|
||||
});
|
||||
|
||||
it("should set dismissedBiometricRequirePasswordOnStartCallout value for account that have it", async () => {
|
||||
await sut.migrate(helper);
|
||||
|
||||
expect(helper.setToUser).toHaveBeenCalledWith(
|
||||
"user-1",
|
||||
DISMISSED_BIOMETRIC_REQUIRE_PASSWORD_ON_START_CALLOUT,
|
||||
true,
|
||||
);
|
||||
});
|
||||
|
||||
it("should not call extra setToUser", async () => {
|
||||
await sut.migrate(helper);
|
||||
|
||||
expect(helper.setToUser).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
});
|
||||
|
||||
describe("rollback", () => {
|
||||
beforeEach(() => {
|
||||
helper = mockMigrationHelper(rollbackJSON(), 23);
|
||||
sut = new MoveBiometricPromptsToStateProviders(22, 23);
|
||||
});
|
||||
|
||||
it.each([DISMISSED_BIOMETRIC_REQUIRE_PASSWORD_ON_START_CALLOUT, PROMPT_AUTOMATICALLY])(
|
||||
"should null out new values %s",
|
||||
async (keyDefinition) => {
|
||||
await sut.rollback(helper);
|
||||
|
||||
expect(helper.setToUser).toHaveBeenCalledWith("user-1", keyDefinition, null);
|
||||
},
|
||||
);
|
||||
|
||||
it("should add explicit value back to accounts", async () => {
|
||||
await sut.rollback(helper);
|
||||
|
||||
expect(helper.set).toHaveBeenCalledTimes(1);
|
||||
expect(helper.set).toHaveBeenCalledWith("user-1", {
|
||||
settings: {
|
||||
disableAutoBiometricsPrompt: false,
|
||||
dismissedBiometricRequirePasswordOnStartCallout: true,
|
||||
otherStuff: "otherStuff2",
|
||||
},
|
||||
otherStuff: "otherStuff3",
|
||||
});
|
||||
});
|
||||
|
||||
it.each(["user-2", "user-3"])(
|
||||
"should not try to restore values to missing accounts",
|
||||
async (userId) => {
|
||||
await sut.rollback(helper);
|
||||
|
||||
expect(helper.set).not.toHaveBeenCalledWith(userId, any());
|
||||
},
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,99 @@
|
||||
import { KeyDefinitionLike, MigrationHelper } from "../migration-helper";
|
||||
import { Migrator } from "../migrator";
|
||||
|
||||
type ExpectedAccountType = {
|
||||
settings?: {
|
||||
disableAutoBiometricsPrompt?: boolean;
|
||||
dismissedBiometricRequirePasswordOnStartCallout?: boolean;
|
||||
};
|
||||
};
|
||||
|
||||
// prompt cancelled is refreshed on every app start/quit/unlock, so we don't need to migrate it
|
||||
|
||||
export const DISMISSED_BIOMETRIC_REQUIRE_PASSWORD_ON_START_CALLOUT: KeyDefinitionLike = {
|
||||
key: "dismissedBiometricRequirePasswordOnStartCallout",
|
||||
stateDefinition: { name: "biometricSettings" },
|
||||
};
|
||||
|
||||
export const PROMPT_AUTOMATICALLY: KeyDefinitionLike = {
|
||||
key: "promptAutomatically",
|
||||
stateDefinition: { name: "biometricSettings" },
|
||||
};
|
||||
|
||||
export class MoveBiometricPromptsToStateProviders extends Migrator<22, 23> {
|
||||
async migrate(helper: MigrationHelper): Promise<void> {
|
||||
const legacyAccounts = await helper.getAccounts<ExpectedAccountType>();
|
||||
|
||||
await Promise.all(
|
||||
legacyAccounts.map(async ({ userId, account }) => {
|
||||
if (account == null) {
|
||||
return;
|
||||
}
|
||||
// Move account data
|
||||
|
||||
if (account?.settings?.dismissedBiometricRequirePasswordOnStartCallout != null) {
|
||||
await helper.setToUser(
|
||||
userId,
|
||||
DISMISSED_BIOMETRIC_REQUIRE_PASSWORD_ON_START_CALLOUT,
|
||||
account.settings.dismissedBiometricRequirePasswordOnStartCallout,
|
||||
);
|
||||
}
|
||||
|
||||
if (account?.settings?.disableAutoBiometricsPrompt != null) {
|
||||
await helper.setToUser(
|
||||
userId,
|
||||
PROMPT_AUTOMATICALLY,
|
||||
!account.settings.disableAutoBiometricsPrompt,
|
||||
);
|
||||
}
|
||||
|
||||
// Delete old account data
|
||||
delete account?.settings?.dismissedBiometricRequirePasswordOnStartCallout;
|
||||
delete account?.settings?.disableAutoBiometricsPrompt;
|
||||
await helper.set(userId, account);
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
async rollback(helper: MigrationHelper): Promise<void> {
|
||||
async function rollbackUser(userId: string, account: ExpectedAccountType) {
|
||||
let updatedAccount = false;
|
||||
|
||||
const userDismissed = await helper.getFromUser<boolean>(
|
||||
userId,
|
||||
DISMISSED_BIOMETRIC_REQUIRE_PASSWORD_ON_START_CALLOUT,
|
||||
);
|
||||
|
||||
if (userDismissed) {
|
||||
account ??= {};
|
||||
account.settings ??= {};
|
||||
|
||||
updatedAccount = true;
|
||||
account.settings.dismissedBiometricRequirePasswordOnStartCallout = userDismissed;
|
||||
await helper.setToUser(userId, DISMISSED_BIOMETRIC_REQUIRE_PASSWORD_ON_START_CALLOUT, null);
|
||||
}
|
||||
|
||||
const userPromptAutomatically = await helper.getFromUser<boolean>(
|
||||
userId,
|
||||
PROMPT_AUTOMATICALLY,
|
||||
);
|
||||
|
||||
if (userPromptAutomatically != null) {
|
||||
account ??= {};
|
||||
account.settings ??= {};
|
||||
|
||||
updatedAccount = true;
|
||||
account.settings.disableAutoBiometricsPrompt = !userPromptAutomatically;
|
||||
await helper.setToUser(userId, PROMPT_AUTOMATICALLY, null);
|
||||
}
|
||||
|
||||
if (updatedAccount) {
|
||||
await helper.set(userId, account);
|
||||
}
|
||||
}
|
||||
|
||||
const accounts = await helper.getAccounts<ExpectedAccountType>();
|
||||
|
||||
await Promise.all(accounts.map(({ userId, account }) => rollbackUser(userId, account)));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user