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

feat: Expose local storage through access control facility #3299

Merged
merged 24 commits into from
Nov 21, 2024
Merged
Show file tree
Hide file tree
Changes from 19 commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
9effef8
wip: local storage access control
traeok Nov 5, 2024
34421f4
wip: support workspace opts in local storage; move ACL checks
traeok Nov 6, 2024
2ff997b
wip: migrating workspace settings into workspaceState
traeok Nov 6, 2024
d18b7da
add typedoc to LocalStorageAccess, export key type
traeok Nov 6, 2024
43a3d04
resolve failing tests & remove unused import
traeok Nov 6, 2024
53f2abe
tests: cases for SettingsConfig.migrateSettingsAtLevel
traeok Nov 6, 2024
186f375
SettingsConfig.migrateSettingsAtLevel: typedoc, await setters
traeok Nov 6, 2024
88bad94
chore: add changelog entry for workspace fix
traeok Nov 6, 2024
0156a42
Merge branch 'main' into refactor/expose-local-storage
traeok Nov 6, 2024
52b421f
bump patch coverage
traeok Nov 7, 2024
a92e127
make LocalStorageAccess static, run l10n
traeok Nov 7, 2024
0ffcba3
create interface in API to represent access facility
traeok Nov 7, 2024
d0be0fb
chore: update API changelog
traeok Nov 7, 2024
9032a62
pass workspaceState to ZoweLocalStorage init
traeok Nov 7, 2024
6f97044
Merge branch 'main' into refactor/expose-local-storage
traeok Nov 13, 2024
bd10595
fix: use defaultValue undefined for workspaceState access
traeok Nov 13, 2024
e63886d
Merge branch 'main' into refactor/expose-local-storage
traeok Nov 14, 2024
346f63b
run prettier
traeok Nov 14, 2024
0016055
Merge branch 'main' into refactor/expose-local-storage
zFernand0 Nov 18, 2024
267f89a
Merge branch 'main' into refactor/expose-local-storage
traeok Nov 19, 2024
43913cf
Merge branch 'main' into refactor/expose-local-storage
traeok Nov 20, 2024
9968473
Merge branch 'main' into refactor/expose-local-storage
JillieBeanSim Nov 21, 2024
8be39ab
Merge branch 'main' into refactor/expose-local-storage
traeok Nov 21, 2024
6b0674d
Merge branch 'main' into refactor/expose-local-storage
JillieBeanSim Nov 21, 2024
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