From d8acff67d16f4e3f89d127126c15e6ca87053fca Mon Sep 17 00:00:00 2001 From: Juho Heikka Date: Tue, 21 Feb 2023 16:09:03 +0200 Subject: [PATCH] Fix ensure hashed directory for extension (#7188) * Fix ensure hashed directory for extension Earlier open lens versions stored extension_data directory based on path of extension's package.json. This causes problems because extensions have moved to new location. This ensures backward compatibility that extension will get the same directory than before the change. Signed-off-by: Juho Heikka * Reconstruct the key for old hashed directories Signed-off-by: Juho Heikka * Remove unnecessary return Signed-off-by: Juho Heikka * Use sync version of random bytes Signed-off-by: Juho Heikka * Add migration to new type of key Signed-off-by: Juho Heikka * Fix global override for random bytes to not return a promise Signed-off-by: Juho Heikka * Make registeredExtensions a dependency Signed-off-by: Juho Heikka --------- Signed-off-by: Juho Heikka --- ...om-bytes.global-override-for-injectable.ts | 2 +- .../common/utils/random-bytes.injectable.ts | 5 +- ...shed-directory-for-extension.injectable.ts | 30 ++++-- ...ure-hashed-directory-for-extension.test.ts | 96 +++++++++++++++++++ ...ystem-provisioner-store-injection-token.ts | 10 ++ ...ile-system-provisioner-store.injectable.ts | 2 + .../file-system-provisioner-store.ts | 12 +-- ...-path-to-legacy-package-json.injectable.ts | 22 +++++ .../registered-extensions.injectable.ts | 12 +++ 9 files changed, 173 insertions(+), 18 deletions(-) create mode 100644 packages/core/src/extensions/extension-loader/file-system-provisioner-store/ensure-hashed-directory-for-extension.test.ts create mode 100644 packages/core/src/extensions/extension-loader/file-system-provisioner-store/file-system-provisioner-store-injection-token.ts create mode 100644 packages/core/src/extensions/extension-loader/file-system-provisioner-store/get-path-to-legacy-package-json.injectable.ts create mode 100644 packages/core/src/extensions/extension-loader/file-system-provisioner-store/registered-extensions.injectable.ts diff --git a/packages/core/src/common/utils/random-bytes.global-override-for-injectable.ts b/packages/core/src/common/utils/random-bytes.global-override-for-injectable.ts index 6f83a264e4..9fce1baebd 100644 --- a/packages/core/src/common/utils/random-bytes.global-override-for-injectable.ts +++ b/packages/core/src/common/utils/random-bytes.global-override-for-injectable.ts @@ -6,7 +6,7 @@ import { getGlobalOverride } from "../test-utils/get-global-override"; import randomBytesInjectable from "./random-bytes.injectable"; -export default getGlobalOverride(randomBytesInjectable, () => async (size) => { +export default getGlobalOverride(randomBytesInjectable, () => (size) => { const res = Buffer.alloc(size); for (let i = 0; i < size; i += 1) { diff --git a/packages/core/src/common/utils/random-bytes.injectable.ts b/packages/core/src/common/utils/random-bytes.injectable.ts index 9f00961824..fce3a0d523 100644 --- a/packages/core/src/common/utils/random-bytes.injectable.ts +++ b/packages/core/src/common/utils/random-bytes.injectable.ts @@ -4,13 +4,12 @@ */ import { getInjectable } from "@ogre-tools/injectable"; import { randomBytes } from "crypto"; -import { promisify } from "util"; -export type RandomBytes = (size: number) => Promise; +export type RandomBytes = (size: number) => Buffer; const randomBytesInjectable = getInjectable({ id: "random-bytes", - instantiate: (): RandomBytes => promisify(randomBytes), + instantiate: (): RandomBytes => randomBytes, causesSideEffects: true, }); diff --git a/packages/core/src/extensions/extension-loader/file-system-provisioner-store/ensure-hashed-directory-for-extension.injectable.ts b/packages/core/src/extensions/extension-loader/file-system-provisioner-store/ensure-hashed-directory-for-extension.injectable.ts index 6b696f663d..50051bf094 100644 --- a/packages/core/src/extensions/extension-loader/file-system-provisioner-store/ensure-hashed-directory-for-extension.injectable.ts +++ b/packages/core/src/extensions/extension-loader/file-system-provisioner-store/ensure-hashed-directory-for-extension.injectable.ts @@ -3,17 +3,18 @@ * Licensed under MIT License. See LICENSE in root directory for more information. */ -import type { ObservableMap } from "mobx"; import { getInjectable } from "@ogre-tools/injectable"; -import { getOrInsertWithAsync } from "../../../common/utils"; +import { getOrInsert } from "../../../common/utils"; import randomBytesInjectable from "../../../common/utils/random-bytes.injectable"; import joinPathsInjectable from "../../../common/path/join-paths.injectable"; import directoryForExtensionDataInjectable from "./directory-for-extension-data.injectable"; import ensureDirInjectable from "../../../common/fs/ensure-dir.injectable"; import getHashInjectable from "./get-hash.injectable"; +import getPathToLegacyPackageJsonInjectable from "./get-path-to-legacy-package-json.injectable"; +import { registeredExtensionsInjectable } from "./registered-extensions.injectable"; -export type EnsureHashedDirectoryForExtension = (extensionName: string, registeredExtensions: ObservableMap) => Promise; +export type EnsureHashedDirectoryForExtension = (extensionName: string) => Promise; const ensureHashedDirectoryForExtensionInjectable = getInjectable({ id: "ensure-hashed-directory-for-extension", @@ -24,14 +25,27 @@ const ensureHashedDirectoryForExtensionInjectable = getInjectable({ const directoryForExtensionData = di.inject(directoryForExtensionDataInjectable); const ensureDirectory = di.inject(ensureDirInjectable); const getHash = di.inject(getHashInjectable); + const getPathToLegacyPackageJson = di.inject(getPathToLegacyPackageJsonInjectable); + const registeredExtensions = di.inject(registeredExtensionsInjectable); - return async (extensionName, registeredExtensions) => { - const dirPath = await getOrInsertWithAsync(registeredExtensions, extensionName, async () => { - const salt = (await randomBytes(32)).toString("hex"); + return async (extensionName) => { + let dirPath: string; + + const legacyDirPath = getPathToLegacyPackageJson(extensionName); + const hashedDirectoryForLegacyDirPath = registeredExtensions.get(legacyDirPath); + + if (hashedDirectoryForLegacyDirPath) { + registeredExtensions.set(extensionName, hashedDirectoryForLegacyDirPath); + registeredExtensions.delete(legacyDirPath); + dirPath = hashedDirectoryForLegacyDirPath; + } else { + const salt = randomBytes(32).toString("hex"); const hashedName = getHash(`${extensionName}/${salt}`); - return joinPaths(directoryForExtensionData, hashedName); - }); + const hashedExtensionDirectory = joinPaths(directoryForExtensionData, hashedName); + + dirPath = getOrInsert(registeredExtensions, extensionName, hashedExtensionDirectory); + } await ensureDirectory(dirPath); diff --git a/packages/core/src/extensions/extension-loader/file-system-provisioner-store/ensure-hashed-directory-for-extension.test.ts b/packages/core/src/extensions/extension-loader/file-system-provisioner-store/ensure-hashed-directory-for-extension.test.ts new file mode 100644 index 0000000000..dab86f68b2 --- /dev/null +++ b/packages/core/src/extensions/extension-loader/file-system-provisioner-store/ensure-hashed-directory-for-extension.test.ts @@ -0,0 +1,96 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import type { ObservableMap } from "mobx"; +import { runInAction } from "mobx"; +import { getDiForUnitTesting } from "../../../main/getDiForUnitTesting"; +import type { EnsureHashedDirectoryForExtension } from "./ensure-hashed-directory-for-extension.injectable"; +import ensureHashedDirectoryForExtensionInjectable from "./ensure-hashed-directory-for-extension.injectable"; +import ensureDirInjectable from "../../../common/fs/ensure-dir.injectable"; +import directoryForExtensionDataInjectable from "./directory-for-extension-data.injectable"; +import directoryForUserDataInjectable from "../../../common/app-paths/directory-for-user-data/directory-for-user-data.injectable"; +import { registeredExtensionsInjectable } from "./registered-extensions.injectable"; + +describe("ensure-hashed-directory-for-extension", () => { + let ensureHashedDirectoryForExtension: EnsureHashedDirectoryForExtension; + let ensureDirMock: jest.Mock; + let registeredExtensions: ObservableMap; + + beforeEach(() => { + const di = getDiForUnitTesting({ doGeneralOverrides: true }); + + ensureDirMock = jest.fn(); + + di.override(ensureDirInjectable, () => ensureDirMock); + di.override(directoryForExtensionDataInjectable, () => "some-directory-for-extension-data"); + di.override(directoryForUserDataInjectable, () => "/some-directory-for-user-data"); + + ensureHashedDirectoryForExtension = di.inject( + ensureHashedDirectoryForExtensionInjectable, + ); + + registeredExtensions = di.inject(registeredExtensionsInjectable); + }); + + it("given registered extension exists, returns existing directory", async () => { + runInAction(() => { + registeredExtensions.set("some-extension-name", "some-directory"); + }); + + const actual = await ensureHashedDirectoryForExtension( + "some-extension-name", + ); + + expect(actual).toBe("some-directory"); + }); + + it("given registered extension does not exist, returns random directory", async () => { + const actual = await ensureHashedDirectoryForExtension( + "some-extension-name", + ); + + expect(actual).toBe("some-directory-for-extension-data/a37a1cfefc0391af3733f23cb6b29443f596a2b8ffe6d116c35df7bc3cd99ef6"); + }); + + describe("given extension directory was saved based on extension's package.json path", () => { + beforeEach(() => { + runInAction(() => { + registeredExtensions.set("/some-directory-for-user-data/node_modules/some-extension-name/package.json", "some-directory"); + }); + ensureDirMock.mockClear(); + }); + + it("returns existing directory", async () => { + const actual = await ensureHashedDirectoryForExtension( + "some-extension-name", + ); + + expect(actual).toBe("some-directory"); + }); + + it("ensure dir is called with some directory", async () => { + await ensureHashedDirectoryForExtension( + "some-extension-name", + ); + + expect(ensureDirMock).toHaveBeenCalledWith("some-directory"); + }); + + it("is migrated to use the extension name as key", async () => { + await ensureHashedDirectoryForExtension( + "some-extension-name", + ); + + expect(registeredExtensions.get("some-extension-name")).toEqual("some-directory"); + }); + + it("old key is removed", async () => { + await ensureHashedDirectoryForExtension( + "some-extension-name", + ); + + expect(registeredExtensions.has("/some-directory-for-user-data/node_modules/some-extension-name/package.json")).toEqual(false); + }); + }); +}); diff --git a/packages/core/src/extensions/extension-loader/file-system-provisioner-store/file-system-provisioner-store-injection-token.ts b/packages/core/src/extensions/extension-loader/file-system-provisioner-store/file-system-provisioner-store-injection-token.ts new file mode 100644 index 0000000000..d7635a71b2 --- /dev/null +++ b/packages/core/src/extensions/extension-loader/file-system-provisioner-store/file-system-provisioner-store-injection-token.ts @@ -0,0 +1,10 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import { getInjectionToken } from "@ogre-tools/injectable"; +import type { MigrationDeclaration } from "../../../common/base-store/migrations.injectable"; + +export const fileSystemProvisionerStoreInjectionToken = getInjectionToken({ + id: "file-system-provisioner-store-injection-token", +}); diff --git a/packages/core/src/extensions/extension-loader/file-system-provisioner-store/file-system-provisioner-store.injectable.ts b/packages/core/src/extensions/extension-loader/file-system-provisioner-store/file-system-provisioner-store.injectable.ts index 7a9d3c4aa1..847ae37f03 100644 --- a/packages/core/src/extensions/extension-loader/file-system-provisioner-store/file-system-provisioner-store.injectable.ts +++ b/packages/core/src/extensions/extension-loader/file-system-provisioner-store/file-system-provisioner-store.injectable.ts @@ -14,6 +14,7 @@ import { persistStateToConfigInjectionToken } from "../../../common/base-store/s import getBasenameOfPathInjectable from "../../../common/path/get-basename.injectable"; import { enlistMessageChannelListenerInjectionToken } from "../../../common/utils/channel/enlist-message-channel-listener-injection-token"; import ensureHashedDirectoryForExtensionInjectable from "./ensure-hashed-directory-for-extension.injectable"; +import { registeredExtensionsInjectable } from "./registered-extensions.injectable"; const fileSystemProvisionerStoreInjectable = getInjectable({ id: "file-system-provisioner-store", @@ -30,6 +31,7 @@ const fileSystemProvisionerStoreInjectable = getInjectable({ enlistMessageChannelListener: di.inject(enlistMessageChannelListenerInjectionToken), shouldDisableSyncInListener: di.inject(shouldBaseStoreDisableSyncInIpcListenerInjectionToken), ensureHashedDirectoryForExtension: di.inject(ensureHashedDirectoryForExtensionInjectable), + registeredExtensions: di.inject(registeredExtensionsInjectable), }), }); diff --git a/packages/core/src/extensions/extension-loader/file-system-provisioner-store/file-system-provisioner-store.ts b/packages/core/src/extensions/extension-loader/file-system-provisioner-store/file-system-provisioner-store.ts index 7c4fb87361..37bff649ad 100644 --- a/packages/core/src/extensions/extension-loader/file-system-provisioner-store/file-system-provisioner-store.ts +++ b/packages/core/src/extensions/extension-loader/file-system-provisioner-store/file-system-provisioner-store.ts @@ -3,7 +3,8 @@ * Licensed under MIT License. See LICENSE in root directory for more information. */ -import { action, makeObservable, observable } from "mobx"; +import type { ObservableMap } from "mobx"; +import { action, makeObservable } from "mobx"; import type { BaseStoreDependencies } from "../../../common/base-store/base-store"; import { BaseStore } from "../../../common/base-store/base-store"; import type { LensExtensionId } from "../../lens-extension"; @@ -16,11 +17,10 @@ interface FSProvisionModel { interface Dependencies extends BaseStoreDependencies { ensureHashedDirectoryForExtension: EnsureHashedDirectoryForExtension; + registeredExtensions: ObservableMap; } export class FileSystemProvisionerStore extends BaseStore { - readonly registeredExtensions = observable.map(); - constructor(protected readonly dependencies: Dependencies) { super(dependencies, { configName: "lens-filesystem-provisioner-store", @@ -37,17 +37,17 @@ export class FileSystemProvisionerStore extends BaseStore { * @returns path to the folder that the extension can safely write files to. */ async requestDirectory(extensionName: string): Promise { - return this.dependencies.ensureHashedDirectoryForExtension(extensionName, this.registeredExtensions); + return this.dependencies.ensureHashedDirectoryForExtension(extensionName); } @action protected fromStore({ extensions }: FSProvisionModel = { extensions: {}}): void { - this.registeredExtensions.merge(extensions); + this.dependencies.registeredExtensions.merge(extensions); } toJSON(): FSProvisionModel { return toJS({ - extensions: Object.fromEntries(this.registeredExtensions), + extensions: Object.fromEntries(this.dependencies.registeredExtensions), }); } } diff --git a/packages/core/src/extensions/extension-loader/file-system-provisioner-store/get-path-to-legacy-package-json.injectable.ts b/packages/core/src/extensions/extension-loader/file-system-provisioner-store/get-path-to-legacy-package-json.injectable.ts new file mode 100644 index 0000000000..668d010afa --- /dev/null +++ b/packages/core/src/extensions/extension-loader/file-system-provisioner-store/get-path-to-legacy-package-json.injectable.ts @@ -0,0 +1,22 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import { getInjectable } from "@ogre-tools/injectable"; +import directoryForUserDataInjectable from "../../../common/app-paths/directory-for-user-data/directory-for-user-data.injectable"; +import joinPathsInjectable from "../../../common/path/join-paths.injectable"; + +export type GetPathToLegacyPackageJson = (extensionName: string) => string; + +const getPathToLegacyPackageJson = getInjectable({ + id: "get-path-to-legacy-package-json", + + instantiate: (di): GetPathToLegacyPackageJson => { + const directoryForUserData = di.inject(directoryForUserDataInjectable); + const joinPaths = di.inject(joinPathsInjectable); + + return (extensionName: string) => joinPaths(directoryForUserData, "node_modules", extensionName, "package.json"); + }, +}); + +export default getPathToLegacyPackageJson; diff --git a/packages/core/src/extensions/extension-loader/file-system-provisioner-store/registered-extensions.injectable.ts b/packages/core/src/extensions/extension-loader/file-system-provisioner-store/registered-extensions.injectable.ts new file mode 100644 index 0000000000..93fb7a342c --- /dev/null +++ b/packages/core/src/extensions/extension-loader/file-system-provisioner-store/registered-extensions.injectable.ts @@ -0,0 +1,12 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import { getInjectable } from "@ogre-tools/injectable"; +import { observable } from "mobx"; +import type { LensExtensionId } from "../../lens-extension"; + +export const registeredExtensionsInjectable = getInjectable({ + id: "registered-extensions", + instantiate: () => observable.map(), +});