migrate or replicate account service data

This commit is contained in:
Matt Gibson 2024-04-22 18:45:32 -04:00
parent dff58167b8
commit d651193d50
No known key found for this signature in database
GPG Key ID: 963EE038B0581878
4 changed files with 261 additions and 1 deletions

View File

@ -4,6 +4,7 @@ import { ActivatedRoute } from "@angular/router";
import { mock, MockProxy } from "jest-mock-extended";
import { I18nPipe } from "@bitwarden/angular/platform/pipes/i18n.pipe";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
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";
@ -59,6 +60,10 @@ describe("GeneratorComponent", () => {
provide: CipherService,
useValue: mock<CipherService>(),
},
{
provide: AccountService,
useValue: mock<AccountService>(),
},
],
schemas: [NO_ERRORS_SCHEMA],
}).compileComponents();

View File

@ -55,6 +55,7 @@ import { MoveMasterKeyStateToProviderMigrator } from "./migrations/55-move-maste
import { AuthRequestMigrator } from "./migrations/56-move-auth-requests";
import { CipherServiceMigrator } from "./migrations/57-move-cipher-service-to-state-provider";
import { RemoveRefreshTokenMigratedFlagMigrator } from "./migrations/58-remove-refresh-token-migrated-state-provider-flag";
import { ReplicateKnownAccountsMigrator } from "./migrations/59-replicate-known-accounts";
import { RemoveLegacyEtmKeyMigrator } from "./migrations/6-remove-legacy-etm-key";
import { MoveBiometricAutoPromptToAccount } from "./migrations/7-move-biometric-auto-prompt-to-account";
import { MoveStateVersionMigrator } from "./migrations/8-move-state-version";
@ -122,7 +123,8 @@ export function createMigrationBuilder() {
.with(MoveMasterKeyStateToProviderMigrator, 54, 55)
.with(AuthRequestMigrator, 55, 56)
.with(CipherServiceMigrator, 56, 57)
.with(RemoveRefreshTokenMigratedFlagMigrator, 57, CURRENT_VERSION);
.with(RemoveRefreshTokenMigratedFlagMigrator, 57, 58)
.with(ReplicateKnownAccountsMigrator, 58, 59);
}
export async function currentVersion(

View File

@ -0,0 +1,145 @@
import { MockProxy, any } from "jest-mock-extended";
import { MigrationHelper } from "../migration-helper";
import { mockMigrationHelper } from "../migration-helper.spec";
import {
ACCOUNT_ACCOUNTS,
ACCOUNT_ACTIVE_ACCOUNT_ID,
ACCOUNT_ACTIVITY,
ReplicateKnownAccountsMigrator,
} from "./59-replicate-known-accounts";
const migrateJson = () => {
return {
authenticatedAccounts: ["user1", "user2"],
activeUserId: "user1",
user1: {
profile: {
email: "user1",
name: "User 1",
emailVerified: true,
},
},
user2: {
profile: {
email: "",
emailVerified: false,
},
},
accountActivity: {
user1: 1609459200000, // 2021-01-01
user2: 1609545600000, // 2021-01-02
},
};
};
const rollbackJson = () => {
return {
authenticatedAccounts: ["user1", "user2"],
user1: {
profile: {
email: "user1",
name: "User 1",
emailVerified: true,
},
},
user2: {
profile: {
email: "",
emailVerified: false,
},
},
global_accounts_accounts: {
user1: {
profile: {
email: "user1",
name: "User 1",
emailVerified: true,
},
},
user2: {
profile: {
email: "",
emailVerified: false,
},
},
},
global_account_activeAccountId: "user1",
global_account_activity: {
user1: "2021-01-01T00:00:00.000Z",
user2: "2021-01-02T00:00:00.000Z",
},
};
};
describe("ReplicateKnownAccounts", () => {
let helper: MockProxy<MigrationHelper>;
let sut: ReplicateKnownAccountsMigrator;
describe("migrate", () => {
beforeEach(() => {
helper = mockMigrationHelper(migrateJson(), 58);
sut = new ReplicateKnownAccountsMigrator(58, 59);
});
it("replicates accounts", async () => {
await sut.migrate(helper);
expect(helper.setToGlobal).toHaveBeenCalledWith(ACCOUNT_ACCOUNTS, {
user1: {
email: "user1",
name: "User 1",
emailVerified: true,
},
user2: {
email: "",
emailVerified: false,
name: undefined,
},
});
expect(helper.set).not.toHaveBeenCalledWith("authenticatedAccounts", any());
});
it("migrates active account it", async () => {
await sut.migrate(helper);
expect(helper.setToGlobal).toHaveBeenCalledWith(ACCOUNT_ACTIVE_ACCOUNT_ID, "user1");
expect(helper.remove).toHaveBeenCalledWith("activeUserId");
});
it("migrates account activity", async () => {
await sut.migrate(helper);
expect(helper.setToGlobal).toHaveBeenCalledWith(ACCOUNT_ACTIVITY, {
user1: '"2021-01-01T00:00:00.000Z"',
user2: '"2021-01-02T00:00:00.000Z"',
});
expect(helper.remove).toHaveBeenCalledWith("accountActivity");
});
});
describe("rollback", () => {
beforeEach(() => {
helper = mockMigrationHelper(rollbackJson(), 59);
sut = new ReplicateKnownAccountsMigrator(58, 59);
});
it("removes accounts", async () => {
await sut.rollback(helper);
expect(helper.removeFromGlobal).toHaveBeenCalledWith(ACCOUNT_ACCOUNTS);
});
it("rolls back active account id", async () => {
await sut.rollback(helper);
expect(helper.set).toHaveBeenCalledWith("activeUserId", "user1");
expect(helper.removeFromGlobal).toHaveBeenCalledWith(ACCOUNT_ACTIVE_ACCOUNT_ID);
});
it("rolls back account activity", async () => {
await sut.rollback(helper);
expect(helper.set).toHaveBeenCalledWith("accountActivity", {
user1: 1609459200000,
user2: 1609545600000,
});
expect(helper.removeFromGlobal).toHaveBeenCalledWith(ACCOUNT_ACTIVITY);
});
});
});

View File

@ -0,0 +1,108 @@
import { KeyDefinitionLike, MigrationHelper } from "../migration-helper";
import { Migrator } from "../migrator";
export const ACCOUNT_ACCOUNTS: KeyDefinitionLike = {
stateDefinition: {
name: "account",
},
key: "accounts",
};
export const ACCOUNT_ACTIVE_ACCOUNT_ID: KeyDefinitionLike = {
stateDefinition: {
name: "account",
},
key: "activeAccountId",
};
export const ACCOUNT_ACTIVITY: KeyDefinitionLike = {
stateDefinition: {
name: "account",
},
key: "activity",
};
type ExpectedAccountType = {
profile?: {
email?: string;
name?: string;
emailVerified?: boolean;
};
};
export class ReplicateKnownAccountsMigrator extends Migrator<58, 59> {
async migrate(helper: MigrationHelper): Promise<void> {
await this.replicateAuthenticatedAccounts(helper);
await this.migrateActiveAccountId(helper);
await this.migrateAccountActivity(helper);
}
async rollback(helper: MigrationHelper): Promise<void> {
// Known accounts are replicated, not migrated, so we don't need to rollback.
await helper.removeFromGlobal(ACCOUNT_ACCOUNTS);
// Active Account Id
const activeAccountId = await helper.getFromGlobal<string>(ACCOUNT_ACTIVE_ACCOUNT_ID);
if (activeAccountId) {
await helper.set("activeUserId", activeAccountId);
}
await helper.removeFromGlobal(ACCOUNT_ACTIVE_ACCOUNT_ID);
// Account Activity
const accountActivity = await helper.getFromGlobal<Record<string, string>>(ACCOUNT_ACTIVITY);
if (accountActivity) {
const toStore = Object.entries(accountActivity).reduce(
(agg, [userId, dateString]) => {
agg[userId] = new Date(dateString).getTime();
return agg;
},
{} as Record<string, number>,
);
await helper.set("accountActivity", toStore);
}
await helper.removeFromGlobal(ACCOUNT_ACTIVITY);
}
private async replicateAuthenticatedAccounts(helper: MigrationHelper) {
const authenticatedAccounts = (await helper.get<string[]>("authenticatedAccounts")) ?? [];
const accounts = await Promise.all(
authenticatedAccounts.map(async (userId) => {
const account = await helper.get<ExpectedAccountType>(userId);
return { userId, account };
}),
);
const accountsToStore = accounts.reduce(
(agg, { userId, account }) => {
if (account?.profile) {
agg[userId] = {
email: account.profile.email ?? "",
emailVerified: account.profile.emailVerified ?? false,
name: account.profile.name,
};
}
return agg;
},
{} as Record<string, { email: string; emailVerified: boolean; name: string | undefined }>,
);
await helper.setToGlobal(ACCOUNT_ACCOUNTS, accountsToStore);
}
private async migrateAccountActivity(helper: MigrationHelper) {
const stored = await helper.get<Record<string, Date>>("accountActivity");
const accountActivity = Object.entries(stored ?? {}).reduce(
(agg, [userId, dateMs]) => {
agg[userId] = JSON.stringify(new Date(dateMs));
return agg;
},
{} as Record<string, string>,
);
await helper.setToGlobal(ACCOUNT_ACTIVITY, accountActivity);
await helper.remove("accountActivity");
}
private async migrateActiveAccountId(helper: MigrationHelper) {
const activeAccountId = await helper.get<string>("activeUserId");
await helper.setToGlobal(ACCOUNT_ACTIVE_ACCOUNT_ID, activeAccountId);
await helper.remove("activeUserId");
}
}