Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

refactor: Expose local storage through access control facility #3299

Open
wants to merge 19 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions packages/zowe-explorer-api/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ All notable changes to the "zowe-explorer-api" extension will be documented in t
### New features and enhancements

- Update Zowe SDKs to `8.2.0` to get the latest enhancements from Imperative.
- Added optional `getLocalStorage` function to the `IApiExplorerExtender` interface to expose local storage access to Zowe Explorer extenders. [#3180](https://github.com/zowe/zowe-explorer-vscode/issues/3180)

### Bug fixes

Expand Down
8 changes: 8 additions & 0 deletions packages/zowe-explorer-api/src/extend/IApiExplorerExtender.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
import * as imperative from "@zowe/imperative";
import { ProfilesCache } from "../profiles/ProfilesCache";
import { ErrorCorrelator } from "../utils/ErrorCorrelator";
import { ILocalStorageAccess } from "./ILocalStorageAccess";

/**
* This interface can be used by other VS Code Extensions to access an alternative
Expand Down Expand Up @@ -53,4 +54,11 @@ export interface IApiExplorerExtender {
* provide tips or additional resources for errors.
*/
getErrorCorrelator?(): ErrorCorrelator;

/**
* Allows extenders to access Zowe Explorer's local storage values. Retrieve a list of
* readable and writable keys by calling the `getReadableKeys, getWritableKeys` functions
* on the returned instance.
*/
getLocalStorage?(): ILocalStorageAccess;
}
38 changes: 38 additions & 0 deletions packages/zowe-explorer-api/src/extend/ILocalStorageAccess.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
/**
* This program and the accompanying materials are made available under the terms of the
* Eclipse Public License v2.0 which accompanies this distribution, and is available at
* https://www.eclipse.org/legal/epl-v20.html
*
* SPDX-License-Identifier: EPL-2.0
*
* Copyright Contributors to the Zowe Project.
*
*/

export interface ILocalStorageAccess {
/**
* @returns The list of readable keys from the access facility
*/
getReadableKeys(): string[];

/**
* @returns The list of writable keys from the access facility
*/
getWritableKeys(): string[];

/**
* Retrieve the value from local storage for the given key.
* @param key A readable key
* @returns The value if it exists in local storage, or `undefined` otherwise
* @throws If the extender does not have appropriate read permissions for the given key
*/
getValue<T>(key: string): T;

/**
* Set a value in local storage for the given key.
* @param key A writable key
* @param value The new value for the given key to set in local storage
* @throws If the extender does not have appropriate write permissions for the given key
*/
setValue<T>(key: string, value: T): Thenable<void>;
}
1 change: 1 addition & 0 deletions packages/zowe-explorer-api/src/extend/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,5 +10,6 @@
*/

export * from "./IApiExplorerExtender";
export * from "./ILocalStorageAccess";
export * from "./IRegisterClient";
export * from "./MainframeInteraction";
2 changes: 2 additions & 0 deletions packages/zowe-explorer/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ All notable changes to the "vscode-extension-for-zowe" extension will be documen
- Power users and developers can now build links to efficiently open mainframe resources in Zowe Explorer. Use the **Copy External Link** option in the context menu to get the URL for a data set or USS resource, or create a link in the format `vscode://Zowe.vscode-extension-for-zowe?<ZoweResourceUri>`. For more information on building resource URIs, see the [FileSystemProvider wiki article](https://github.com/zowe/zowe-explorer-vscode/wiki/FileSystemProvider#file-paths-vs-uris). [#3271](https://github.com/zowe/zowe-explorer-vscode/pull/3271)
- Implemented more user-friendly error messages for API or network errors within Zowe Explorer. [#3243](https://github.com/zowe/zowe-explorer-vscode/pull/3243)
- Use the "Troubleshoot" option for certain errors to obtain additional context, tips, and resources for how to resolve the errors. [#3243](https://github.com/zowe/zowe-explorer-vscode/pull/3243)
- Exposed read and write access to local storage keys for Zowe Explorer extenders. [#3180](https://github.com/zowe/zowe-explorer-vscode/issues/3180)

### Bug fixes

Expand All @@ -27,6 +28,7 @@ All notable changes to the "vscode-extension-for-zowe" extension will be documen
- Fixed an issue where opening a PDS member after renaming an expanded PDS resulted in an error. [#3314](https://github.com/zowe/zowe-explorer-vscode/issues/3314)
- Fixed issue where users were not prompted to enter credentials if a 401 error was encountered when opening files, data sets or spools in the editor. [#3197](https://github.com/zowe/zowe-explorer-vscode/issues/3197)
- Fixed issue where profile credential updates or token changes were not reflected within the filesystem. [#3289](https://github.com/zowe/zowe-explorer-vscode/issues/3289)
- Fixed issue where persistent settings defined at the workspace level were migrated into global storage rather than workspace-specific storage. [#3180](https://github.com/zowe/zowe-explorer-vscode/issues/3180)
- Fixed issue to update the success message when changing authentication from token to basic through the 'Change Authentication' option.
- Fixed an issue where fetching a USS file using `UssFSProvider.stat()` with a `fetch=true` query would cause Zowe Explorer to get stuck in an infinite loop.

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ describe("mvsCommandActions unit testing", () => {
};
}),
});
Object.defineProperty(ZoweLocalStorage, "storage", {
Object.defineProperty(ZoweLocalStorage, "globalState", {
value: {
get: () => ({ persistence: true, favorites: [], history: [], sessions: ["zosmf"], searchHistory: [], fileHistory: [] }),
update: jest.fn(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ describe("TsoCommandHandler unit testing", () => {
};
}),
});
Object.defineProperty(ZoweLocalStorage, "storage", {
Object.defineProperty(ZoweLocalStorage, "globalState", {
value: {
get: () => ({ persistence: true, favorites: [], history: [], sessions: ["zosmf"], searchHistory: [], fileHistory: [] }),
update: jest.fn(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ describe("UnixCommand Actions Unit Testing", () => {
}),
});

Object.defineProperty(ZoweLocalStorage, "storage", {
Object.defineProperty(ZoweLocalStorage, "globalState", {
value: {
get: () => ({ persistence: true, favorites: [], history: [], sessions: ["zosmf"], searchHistory: [], fileHistory: [] }),
update: jest.fn(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -146,7 +146,7 @@ function createGlobalMocks(): { [key: string]: any } {
configurable: true,
});

Object.defineProperty(ZoweLocalStorage, "storage", {
Object.defineProperty(ZoweLocalStorage, "globalState", {
value: {
get: jest.fn(() => ({ persistence: true })),
update: jest.fn(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ import { SettingsConfig } from "../../../src/configuration/SettingsConfig";
describe("SettingsConfig Unit Tests", () => {
beforeEach(() => {
Object.defineProperty(ZoweLogger, "trace", { value: jest.fn(), configurable: true });
Object.defineProperty(ZoweLocalStorage, "storage", {
Object.defineProperty(ZoweLocalStorage, "globalState", {
value: {
get: () => ({
persistence: true,
Expand Down Expand Up @@ -209,4 +209,58 @@ describe("SettingsConfig Unit Tests", () => {
expect(migrateToLocalStorageSpy).toHaveBeenCalledTimes(1);
});
});

describe("function migrateSettingsAtLevel", () => {
function getBlockMocks() {
const configurationsMock = jest.spyOn(SettingsConfig as any, "configurations", "get");
const setDirectValueMock = jest.spyOn(SettingsConfig, "setDirectValue").mockImplementation();
const setValueMock = jest.spyOn(ZoweLocalStorage, "setValue").mockImplementation();
jest.spyOn(SettingsConfig, "setMigratedDsTemplates").mockImplementation();

return {
configurationsMock,
setDirectValueMock,
setValueMock,
};
}

it("migrates workspace-level settings from settings config", async () => {
const blockMocks = getBlockMocks();
blockMocks.configurationsMock.mockReturnValue({
inspect: () => ({
globalValue: undefined,
workspaceValue: 123,
}),
});
await (SettingsConfig as any).migrateSettingsAtLevel(vscode.ConfigurationTarget.Workspace);
for (const [_, value, setInWorkspace] of blockMocks.setValueMock.mock.calls) {
expect(value).toBe(123);
expect(setInWorkspace).toBe(true);
}
for (const [_, value, target] of blockMocks.setDirectValueMock.mock.calls) {
expect(value).toEqual(undefined);
expect(target).toBe(vscode.ConfigurationTarget.Workspace);
}
});

it("migrates global-level settings from settings config", async () => {
const blockMocks = getBlockMocks();
blockMocks.configurationsMock.mockReturnValue({
inspect: () => ({
globalValue: 123,
workspaceValue: undefined,
}),
});
await (SettingsConfig as any).migrateSettingsAtLevel(vscode.ConfigurationTarget.Global);

for (const [_, value, setInWorkspace] of blockMocks.setValueMock.mock.calls) {
expect(value).toBe(123);
expect(setInWorkspace).toBe(false);
}
for (const [_, value, target] of blockMocks.setDirectValueMock.mock.calls) {
expect(value).toEqual(undefined);
expect(target).toBe(vscode.ConfigurationTarget.Global);
}
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ import { createUSSSessionNode, createUSSTree } from "../../__mocks__/mockCreator
import { createJobsTree, createIJobObject } from "../../__mocks__/mockCreators/jobs";
import { SettingsConfig } from "../../../src/configuration/SettingsConfig";
import { ZoweExplorerExtender } from "../../../src/extending/ZoweExplorerExtender";
import { ZoweLocalStorage } from "../../../src/tools/ZoweLocalStorage";
import { LocalStorageAccess, ZoweLocalStorage } from "../../../src/tools/ZoweLocalStorage";
import { ZoweLogger } from "../../../src/tools/ZoweLogger";
import { UssFSProvider } from "../../../src/trees/uss/UssFSProvider";
import { ProfilesUtils } from "../../../src/utils/ProfilesUtils";
Expand Down Expand Up @@ -71,7 +71,7 @@ describe("ZoweExplorerExtender unit tests", () => {
value: newMocks.mockTextDocument,
configurable: true,
});
Object.defineProperty(ZoweLocalStorage, "storage", {
Object.defineProperty(ZoweLocalStorage, "globalState", {
value: {
get: () => ({ persistence: true, favorites: [], history: [], sessions: ["zosmf"], searchHistory: [], fileHistory: [] }),
update: jest.fn(),
Expand Down Expand Up @@ -324,4 +324,11 @@ describe("ZoweExplorerExtender unit tests", () => {
expect(blockMocks.instTest.getErrorCorrelator()).toBe(ErrorCorrelator.getInstance());
});
});

describe("getLocalStorage", () => {
it("returns the singleton instance of LocalStorageAccess", () => {
const blockMocks = createBlockMocks();
expect(blockMocks.instTest.getLocalStorage()).toBe(LocalStorageAccess);
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -395,7 +395,7 @@ async function createGlobalMocks() {
value: jest.fn(),
configurable: true,
});
Object.defineProperty(ZoweLocalStorage, "storage", {
Object.defineProperty(ZoweLocalStorage, "globalState", {
value: {
get: () => ({ persistence: true, favorites: [], history: [], sessions: ["zosmf"], searchHistory: [], fileHistory: [] }),
update: jest.fn(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ describe("Checking icon generator's basics", () => {
const createTreeView = jest.fn().mockReturnValue({ onDidCollapseElement: jest.fn() });

Object.defineProperty(vscode.window, "createTreeView", { value: createTreeView });
Object.defineProperty(ZoweLocalStorage, "storage", {
Object.defineProperty(ZoweLocalStorage, "globalState", {
value: {
get: () => ({ persistence: true, favorites: [], history: [], sessions: ["zosmf"], searchHistory: [], fileHistory: [] }),
update: jest.fn(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,15 @@
*/

import * as vscode from "vscode";
import { ZoweLocalStorage } from "../../../src/tools/ZoweLocalStorage";
import { LocalStorageAccess, StorageKeys, ZoweLocalStorage } from "../../../src/tools/ZoweLocalStorage";
import { PersistenceSchemaEnum } from "@zowe/zowe-explorer-api";
import { Definitions } from "../../../src/configuration/Definitions";

describe("ZoweLocalStorage Unit Tests", () => {
it("should initialize successfully", () => {
const mockGlobalState = { get: jest.fn(), update: jest.fn(), keys: () => [] } as vscode.Memento;
ZoweLocalStorage.initializeZoweLocalStorage(mockGlobalState);
expect((ZoweLocalStorage as any).storage).toEqual(mockGlobalState);
expect((ZoweLocalStorage as any).globalState).toEqual(mockGlobalState);
});

it("should get and set values successfully", () => {
Expand All @@ -31,4 +32,102 @@ describe("ZoweLocalStorage Unit Tests", () => {
ZoweLocalStorage.setValue("fruit" as PersistenceSchemaEnum, "banana");
expect(ZoweLocalStorage.getValue("fruit" as PersistenceSchemaEnum)).toEqual("banana");
});

it("should get workspace values with no default and fallback to global if not found", () => {
const globalStorage = {};
const workspaceStorage = {};
const mockGlobalState = {
get: jest.fn().mockImplementation((key, defaultValue) => globalStorage[key] ?? defaultValue),
update: jest.fn().mockImplementation((key, value) => (globalStorage[key] = value)),
keys: () => [],
};
const mockWorkspaceState = {
get: jest.fn().mockImplementation((key, defaultValue) => workspaceStorage[key] ?? defaultValue),
update: jest.fn().mockImplementation((key, value) => (workspaceStorage[key] = value)),
keys: () => [],
};
ZoweLocalStorage.initializeZoweLocalStorage(mockGlobalState, mockWorkspaceState);
// add value into local storage
ZoweLocalStorage.setValue("fruit" as PersistenceSchemaEnum, "banana");

// assert that it can still be retrieved from global storage
expect(ZoweLocalStorage.getValue("fruit" as PersistenceSchemaEnum)).toEqual("banana");

// workspace state access should have default value of undefined
// covers `ZoweLocalStorage.workspaceState?.get<T>(key, undefined) ?? ZoweLocalStorage.globalState.get<T>(key, defaultValue);`
expect(mockWorkspaceState.get).toHaveBeenCalledWith("fruit" as PersistenceSchemaEnum, undefined);
// expect global state to be accessed since key in workspace state was undefined
expect(mockGlobalState.get).toHaveBeenCalledWith("fruit" as PersistenceSchemaEnum, undefined);
});

it("should set workspace values successfully when setInWorkspace is true", () => {
const globalState = { get: jest.fn(), update: jest.fn(), keys: () => [] } as vscode.Memento;
const workspaceState = { get: jest.fn(), update: jest.fn(), keys: () => [] } as vscode.Memento;
ZoweLocalStorage.initializeZoweLocalStorage(globalState, workspaceState);
ZoweLocalStorage.setValue("fruit" as PersistenceSchemaEnum, "banana", true);
expect(workspaceState.update).toHaveBeenCalled();
expect(globalState.update).not.toHaveBeenCalled();
});
});

describe("LocalStorageAccess", () => {
// Read: 1, Write: 2, Read | Write: 3
function omitKeysWithPermission(permBits: number): StorageKeys[] {
return Object.values({ ...Definitions.LocalStorageKey, ...PersistenceSchemaEnum }).filter(
(k) => !(((LocalStorageAccess as any).accessControl[k] & permBits) > 0)
);
}
function keysWithPerm(permBits: number): StorageKeys[] {
return Object.values({ ...Definitions.LocalStorageKey, ...PersistenceSchemaEnum }).filter(
(k) => (LocalStorageAccess as any).accessControl[k] === permBits
);
}

describe("getReadableKeys", () => {
it("returns a list of readable keys to the user", () => {
expect(LocalStorageAccess.getReadableKeys()).toStrictEqual([...keysWithPerm(1), ...keysWithPerm(3)]);
});
});

describe("getWritableKeys", () => {
it("returns a list of writable keys to the user", () => {
expect(LocalStorageAccess.getWritableKeys()).toStrictEqual([...keysWithPerm(2), ...keysWithPerm(3)]);
});
});

describe("getValue", () => {
it("calls ZoweLocalStorage.getValue for all readable keys", () => {
const getValueMock = jest.spyOn(ZoweLocalStorage, "getValue").mockReturnValue(123);
for (const key of keysWithPerm(1)) {
expect(LocalStorageAccess.getValue(key)).toBe(123);
expect(getValueMock).toHaveBeenCalledWith(key);
}
});

it("throws error for all keys that are not readable", () => {
for (const key of omitKeysWithPermission(1)) {
expect(() => LocalStorageAccess.getValue(key)).toThrow(`Insufficient read permissions for ${key as string} in local storage.`);
}
});
});

describe("setValue", () => {
it("calls ZoweLocalStorage.setValue for all writable keys", async () => {
const setValueMock = jest.spyOn(ZoweLocalStorage, "setValue").mockImplementation();
const expectWritableSpy = jest.spyOn(LocalStorageAccess as any, "expectWritable");
for (const key of keysWithPerm(2)) {
await LocalStorageAccess.setValue(key, 123);
expect(setValueMock).toHaveBeenCalledWith(key, 123);
expect(expectWritableSpy).not.toThrow();
}
});

it("throws error for all keys that are not writable", () => {
for (const key of omitKeysWithPermission(2)) {
expect(() => LocalStorageAccess.setValue(key, undefined)).toThrow(
`Insufficient write permissions for ${key as string} in local storage.`
);
}
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import { ZowePersistentFilters } from "../../../src/tools/ZowePersistentFilters"

describe("PersistentFilters Unit Test", () => {
Object.defineProperty(ZoweLogger, "trace", { value: jest.fn(), configurable: true });
Object.defineProperty(ZoweLocalStorage, "storage", {
Object.defineProperty(ZoweLocalStorage, "globalState", {
value: {
get: () => ({
persistence: true,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ jest.mock("../../../src/tools/ZoweLogger");

describe("ZoweSaveQueue - unit tests", () => {
const createGlobalMocks = () => {
Object.defineProperty(ZoweLocalStorage, "storage", {
Object.defineProperty(ZoweLocalStorage, "globalState", {
value: {
get: () => ({ persistence: true, favorites: [], history: [], sessions: ["zosmf"], searchHistory: [], fileHistory: [] }),
update: jest.fn(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ import { AuthUtils } from "../../../src/utils/AuthUtils";
import { IconGenerator } from "../../../src/icons/IconGenerator";

async function createGlobalMocks() {
Object.defineProperty(ZoweLocalStorage, "storage", {
Object.defineProperty(ZoweLocalStorage, "globalState", {
value: {
get: () => ({ persistence: true, favorites: [], history: [], sessions: ["zosmf"], searchHistory: [], fileHistory: [] }),
update: jest.fn(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ function createGlobalMocks() {

globalMocks.mockProfileInstance = createInstanceOfProfile(globalMocks.testProfileLoaded);

Object.defineProperty(ZoweLocalStorage, "storage", {
Object.defineProperty(ZoweLocalStorage, "globalState", {
value: {
get: () => ({ persistence: true, favorites: [], history: [], sessions: ["zosmf"], searchHistory: [], fileHistory: [] }),
update: jest.fn(),
Expand Down
Loading
Loading