Skip to content
Open
Show file tree
Hide file tree
Changes from 26 commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
1e36c1c
wip: setup credential manager options
traeok Oct 20, 2025
ed2c8b9
refactor: update cred mgr impls
traeok Oct 20, 2025
a60a169
wip: map persist levels and make default cred mgr opts
traeok Oct 21, 2025
b6444be
feat(secrets_core): implement support for win32 persist flag
traeok Oct 21, 2025
02e017f
refactor: update setPassword w/ persist_win32 option
traeok Oct 21, 2025
f48b15f
fix: adjust use statement in keyring crate
traeok Oct 21, 2025
8023319
feat: support r/w options in imperative.json file
traeok Oct 22, 2025
31ae2fb
tests: cases for cred mgr options
traeok Oct 22, 2025
bca0b45
chore: update changelogs
traeok Oct 22, 2025
ba1b7ea
fix: build errors for SetPassword task
traeok Oct 22, 2025
c23088e
lint: fix switch statement & remove spaces
traeok Oct 22, 2025
b25b261
doc: Add info for cred mgr options
traeok Oct 22, 2025
f479b91
fix failing unit tests
traeok Oct 23, 2025
9aff17f
refactor: move cred mgr options out of overrides obj
traeok Oct 24, 2025
867b569
refactor: move opts out of overrides for backward compatibility
traeok Oct 24, 2025
c101ba2
refactor: CredentialManagerOptions -> credentialManagerOptions
traeok Oct 24, 2025
612f8c0
lint: remove unused import
traeok Oct 24, 2025
58ca777
tests: cases for default cred mgr and AppSettings
traeok Oct 24, 2025
8e983f8
chore: update secrets changelog
traeok Oct 24, 2025
9b469b8
add secrets SDK tests, imperative typedoc
traeok Oct 27, 2025
f8c8059
refactor: simplify empty object assignment
traeok Oct 27, 2025
7e0ee5e
refactor: simplify CredentialManagerOverride.syncCachedSettings
traeok Oct 27, 2025
26563bb
fix: address SonarCloud issues
traeok Oct 27, 2025
0261bee
refactor: only log persistence level on Win32
traeok Oct 27, 2025
af7581a
refactor: only run persist logic when platform is win32
traeok Oct 27, 2025
46b5ef3
test: skip persistence flag tests on linux/mac
traeok Oct 27, 2025
3ca7d34
doc: update comment in OverridesLoader
traeok Oct 28, 2025
fdfdf62
refactor: use interface instead of redeclaring type
traeok Oct 28, 2025
3ef675c
doc: pass correct value for persist option example
traeok Oct 28, 2025
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
Original file line number Diff line number Diff line change
Expand Up @@ -62,4 +62,94 @@ Any plugin *CAN* supply a lifecycle class. However, credential manager override

The Imperative class [CredentialManagerOverride ](https://github.com/zowe/imperative/blob/master/packages/security/src/CredentialManagerOverride.ts) provides a number of useful utilities related to the configuration of known credential mangers. Two functions of particular value to a credential manger override plugin are recordCredMgrInConfig and recordDefaultCredMgrInConfig.

A credential manager override plugin must call recordCredMgrInConfig() during its postInstall() function to configure that plugin for use during CLI commands. During its preUninstall() function, the plugin must call recordDefaultCredMgrInConfig() to restore the default credential manager. This leaves the CLI in an operational state after the plugin has been uninstalled.
A credential manager override plugin must call recordCredMgrInConfig() during its postInstall() function to configure that plugin for use during CLI commands. During its preUninstall() function, the plugin must call recordDefaultCredMgrInConfig() to restore the default credential manager. This leaves the CLI in an operational state after the plugin has been uninstalled.

## Passing options to credential managers

Credential managers can accept configuration options to customize their behavior. These options are passed during initialization.

### Configuring options in imperative.json (settings file)

Credential manager options can be configured in the `imperative.json` settings file located in the `~/.zowe/settings` directory:

```json
{
"overrides": {
"CredentialManager": "my-custom-plugin"
},
"credentialManagerOptions": {
"timeout": 5000,
"retryAttempts": 3
}
}
```

### Default credential manager options

The default credential manager supports the following options:

#### Windows Persistence Level

On Windows systems, the `persist` option controls where and how long credentials are stored in the Windows Credential Manager. The available persistence levels are:

- **`"session"`** (CRED_PERSIST_SESSION): Credentials are stored only for the current logon session and are deleted when the user logs off.
- **`"local_machine"`** (CRED_PERSIST_LOCAL_MACHINE): Credentials persist on the local machine and are available across logon sessions for the current user on this computer only.
- **`"enterprise"`** (CRED_PERSIST_ENTERPRISE): Credentials are stored in the user's roaming profile and are available across all computers in the domain (default for backward compatibility).

**Note:** The `persist` option only affects Windows systems. On macOS and Linux, this option is ignored as those platforms use different credential storage mechanisms (Keychain on macOS and Secret Service API/libsecret on Linux).

### Custom credential manager options

When developing a custom credential manager plugin, you can define your own options structure. The options are passed to your credential manager's constructor through the `ICredentialManagerInit` parameter.

The options should extend the `ICredentialManagerOptions` interface, which is defined as:

```typescript
export type ICredentialManagerOptions = { [key: string]: any };
```

Example of custom credential manager options:

```typescript
import { AbstractCredentialManager, ICredentialManagerOptions } from "@zowe/imperative";

interface MyCredentialManagerOptions extends ICredentialManagerOptions {
serverUrl?: string;
timeout?: number;
retryAttempts?: number;
enableLogging?: boolean;
}

export default class MyCredentialManager extends AbstractCredentialManager {
constructor(service: string, displayName: string) {
super(service, displayName);
}

protected async initializeImplementation(options?: MyCredentialManagerOptions): Promise<void> {
// Access options passed from configuration
const serverUrl = options?.serverUrl || "https://default.example.com";
const timeout = options?.timeout || 3000;
const retryAttempts = options?.retryAttempts || 1;

// Use these options to configure your credential manager
// ...
}

// Implement other required methods...
}
```

Users of your custom credential manager can then configure it in their `imperative.json` settings file:

```json
{
"overrides": {
"CredentialManager": "my-custom-plugin"
},
"credentialManagerOptions": {
"serverUrl": "https://vault.example.com",
"timeout": 5000,
"retryAttempts": 3
}
}
```
4 changes: 4 additions & 0 deletions packages/cli/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
# Change Log
All notable changes to the Zowe CLI package will be documented in this file.

## Recent Changes

- Enhancement: Added support for providing options to both default and custom credential managers. [#2601](https://github.com/zowe/zowe-cli/issues/2601)

## `8.27.4`

- BugFix: Updated minimum supported version of Node from 18 to 20. Added Node 24 support. [#2616](https://github.com/zowe/zowe-cli/pull/2616)
Expand Down
4 changes: 4 additions & 0 deletions packages/imperative/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@

All notable changes to the Imperative package will be documented in this file.

## Recent Changes

- Enhancement: Added support for providing options to both default and custom credential managers. [#2601](https://github.com/zowe/zowe-cli/issues/2601)

## `8.27.4`

- BugFix: Updated minimum supported version of Node from 18 to 20. Added Node 24 support. [#2616](https://github.com/zowe/zowe-cli/pull/2616)
Expand Down
8 changes: 8 additions & 0 deletions packages/imperative/src/config/src/doc/IConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,19 @@
*/

import { IConfigProfile } from "./IConfigProfile";
import { ICredentialManagerOptions } from "../../../security/src/doc/ICredentialManagerOptions";

export interface IConfig {
$schema?: string;
defaults: { [key: string]: string };
profiles: { [key: string]: IConfigProfile };
autoStore?: boolean;
plugins?: string[];
/**
* Options to pass to the credential manager when it is initialized.
* Allows configuration of credential manager behavior without relying on environment variables.
* The structure of options depends on the specific credential manager being used.
* @type {ICredentialManagerOptions}
*/
credentialManagerOptions?: ICredentialManagerOptions;
}
Original file line number Diff line number Diff line change
Expand Up @@ -179,7 +179,7 @@ describe("Imperative", () => {
});

describe("AppSettings", () => {
const defaultSettings = { overrides: { CredentialManager: "host-package" } };
const defaultSettings = { credentialManagerOptions: {}, overrides: { CredentialManager: "host-package" } };
it("should initialize an app settings instance", async () => {
await Imperative.init();

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import { CredentialManagerFactory, AbstractCredentialManager } from "../../secur
import * as path from "path";
import { ImperativeConfig, Logger } from "../..";
import { AppSettings } from "../../settings";
import { PersistenceLevel } from "../../security/src/doc/IDefaultCredentialManagerOptions";

const TEST_MANAGER_NAME = "test manager";

Expand Down Expand Up @@ -122,7 +123,13 @@ describe("OverridesLoader", () => {

jest.spyOn(AppSettings, "initialized", "get").mockReturnValue(true);
jest.spyOn(AppSettings, "instance", "get").mockReturnValue({
getNamespace: () => ({ CredentialManager: "host-package" })
getNamespace: (namespace: string) => {
if (namespace === "overrides") {
return { CredentialManager: "host-package" };
}

return undefined;
}
} as any);
await OverridesLoader.load(config, packageJson);

Expand All @@ -131,18 +138,61 @@ describe("OverridesLoader", () => {
Manager: undefined,
displayName: config.productDisplayName,
invalidOnFailure: false,
service: config.name
service: config.name,
});
});

it("should pass credential manager options from settings to the credential manager", async () => {
const config: IImperativeConfig = {
name: "ABCD",
overrides: {},
productDisplayName: "a fake CLI"
};

const packageJson = {
name: "host-package",
dependencies: {
"@zowe/secrets-for-zowe-sdk": "1.0"
}
};

const settingsOptions = {
persist: PersistenceLevel.Enterprise,
customOption: "test"
};

jest.spyOn(AppSettings, "initialized", "get").mockReturnValue(true);
const appSettingsMock = jest.spyOn(AppSettings, "instance", "get").mockReturnValue({
getNamespace: (namespace: string) => {
if (namespace === "overrides") {
return {
CredentialManager: "host-package"
};
}
if (namespace === "credentialManagerOptions") {
return settingsOptions;
}
return undefined;
}
} as any);

await OverridesLoader.load(config, packageJson);

expect(CredentialManagerFactory.initialize).toHaveBeenCalledTimes(1);
expect(CredentialManagerFactory.initialize).toHaveBeenCalledWith(expect.objectContaining({
options: expect.objectContaining(settingsOptions)
}));
appSettingsMock.mockRestore();
});

describe("should load a credential manager specified by the user", () => {
it("was passed a class", async () => {
const config: IImperativeConfig = {
name: "EFGH",
overrides: {
CredentialManager: class extends AbstractCredentialManager {
constructor(service: string) {
super(service, TEST_MANAGER_NAME);
constructor(service: string, displayName?: string, options?: import("../../security").ICredentialManagerOptions) {
super(service, displayName || TEST_MANAGER_NAME, options);
}

protected async deleteCredentials(_account: string): Promise<void> {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ import { SessConstants } from "../../../../../rest";
import { setupConfigToLoad } from "../../../../../../__tests__/src/TestUtil";
import { IHandlerParameters } from "../../../../../cmd";
import { EventOperator, EventUtils } from "../../../../../events";
import { PersistenceValue } from "../../../../../security/src/doc/IDefaultCredentialManagerOptions";

let readPromptSpy: any;
const getIHandlerParametersObject = (): IHandlerParameters => {
Expand Down Expand Up @@ -219,7 +220,8 @@ describe("Configuration Secure command handler", () => {
expect(keytarSetPasswordSpy).toHaveBeenCalledWith(
"Zowe",
"secure_config_props",
fakeSecureDataExpected
fakeSecureDataExpected,
PersistenceValue.Enterprise
);
expect(writeFileSyncSpy).toHaveBeenCalledTimes(1);
expect(writeFileSyncSpy).toHaveBeenNthCalledWith(
Expand Down Expand Up @@ -292,7 +294,8 @@ describe("Configuration Secure command handler", () => {
expect(keytarSetPasswordSpy).toHaveBeenCalledWith(
"Zowe",
"secure_config_props",
fakeSecureDataExpected
fakeSecureDataExpected,
PersistenceValue.Enterprise
);
expect(writeFileSyncSpy).toHaveBeenCalledTimes(1);
expect(writeFileSyncSpy).toHaveBeenNthCalledWith(
Expand Down Expand Up @@ -371,7 +374,8 @@ describe("Configuration Secure command handler", () => {
expect(keytarSetPasswordSpy).toHaveBeenCalledWith(
"Zowe",
"secure_config_props",
fakeSecureDataExpected
fakeSecureDataExpected,
PersistenceValue.Enterprise
);
expect(writeFileSyncSpy).toHaveBeenCalledTimes(1);
expect(writeFileSyncSpy).toHaveBeenNthCalledWith(
Expand Down Expand Up @@ -449,7 +453,8 @@ describe("Configuration Secure command handler", () => {
expect(keytarSetPasswordSpy).toHaveBeenCalledWith(
"Zowe",
"secure_config_props",
fakeSecureDataExpected
fakeSecureDataExpected,
PersistenceValue.Enterprise
);
expect(writeFileSyncSpy).toHaveBeenCalledTimes(1);
expect(writeFileSyncSpy).toHaveBeenNthCalledWith(
Expand Down Expand Up @@ -575,7 +580,8 @@ describe("Configuration Secure command handler", () => {
expect(keytarSetPasswordSpy).toHaveBeenCalledWith(
"Zowe",
"secure_config_props",
fakeSecureDataExpected
fakeSecureDataExpected,
PersistenceValue.Enterprise
);
expect(writeFileSyncSpy).toHaveBeenCalledTimes(1);
expect(writeFileSyncSpy).toHaveBeenNthCalledWith(
Expand Down Expand Up @@ -727,7 +733,8 @@ describe("Configuration Secure command handler", () => {
expect(keytarSetPasswordSpy).toHaveBeenCalledWith(
"Zowe",
"secure_config_props",
fakeSecureDataExpected
fakeSecureDataExpected,
PersistenceValue.Enterprise
);
expect(writeFileSyncSpy).toHaveBeenCalledTimes(1);
expect(writeFileSyncSpy).toHaveBeenNthCalledWith(
Expand Down Expand Up @@ -780,7 +787,8 @@ describe("Configuration Secure command handler", () => {
expect(keytarSetPasswordSpy).toHaveBeenCalledWith(
"Zowe",
"secure_config_props",
fakeSecureDataExpected
fakeSecureDataExpected,
PersistenceValue.Enterprise
);
expect(writeFileSyncSpy).toHaveBeenCalledTimes(1);
expect(writeFileSyncSpy).toHaveBeenNthCalledWith(
Expand Down Expand Up @@ -834,7 +842,8 @@ describe("Configuration Secure command handler", () => {
expect(keytarSetPasswordSpy).toHaveBeenCalledWith(
"Zowe",
"secure_config_props",
fakeSecureDataExpected
fakeSecureDataExpected,
PersistenceValue.Enterprise
);
expect(writeFileSyncSpy).toHaveBeenCalledTimes(1);
expect(writeFileSyncSpy).toHaveBeenNthCalledWith(
Expand Down Expand Up @@ -901,7 +910,8 @@ describe("Configuration Secure command handler", () => {
expect(keytarSetPasswordSpy).toHaveBeenCalledWith(
"Zowe",
"secure_config_props",
fakeSecureDataExpected
fakeSecureDataExpected,
PersistenceValue.Enterprise
);
expect(writeFileSyncSpy).toHaveBeenCalledTimes(1);
expect(writeFileSyncSpy).toHaveBeenNthCalledWith(
Expand Down Expand Up @@ -972,7 +982,8 @@ describe("Configuration Secure command handler", () => {
expect(keytarSetPasswordSpy).toHaveBeenCalledWith(
"Zowe",
"secure_config_props",
fakeSecureDataExpected
fakeSecureDataExpected,
PersistenceValue.Enterprise
);
expect(writeFileSyncSpy).toHaveBeenCalledTimes(1);
expect(writeFileSyncSpy).toHaveBeenNthCalledWith(
Expand Down
Loading