Skip to content
Merged
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
17 changes: 17 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,23 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Changed

- Updated to Frodo Lib 4.0.0

### Added

- Added `frodo config-manager` (`fr-config-manager`) commands.
- Added support for Node Designer Nodes through several API and Ops functions to allow for doing exports, imports, deletes, etc. with custom node configurations.<br><br>
Just like with journeys, custom nodes get exported and imported in the same way as they do from AIC/AM, so you can import Frodo exported custom nodes into AIC/AM and vice versa.<br><br>
Additionally, journeys were updated to include custom node dependencies during exports. Even if a journey is exported with Frodo and contains these dependencies in the export JSON, they can still be imported into AIC/AM using the admin UI as it should ignore the custom node dependencies (since AIC/AM doesn't support exporting them yet).
- Added `--retry <strategy>` option to all commands.
- Added the ability to authenticate to an AM classic deployment using Amster credentials (i.e. a public/private key pair). The private key can be in a variety of formats such as PKCS, JWK, and OpenSSH, but is ultimately stored in PKCS#8 format. You can also use encrypted private keys by providing the passphrase when creating the connection profile.

### Fixed

- \#XXX:

## [4.0.0-2] - 2026-01-27

## [4.0.0-1] - 2026-01-26
Expand Down
19 changes: 4 additions & 15 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,7 @@
]
},
"devDependencies": {
"@rockcarver/frodo-lib": "4.0.0-2",
"@rockcarver/frodo-lib": "4.0.0-3",
"@types/colors": "^1.2.1",
"@types/fs-extra": "^11.0.1",
"@types/jest": "^29.2.3",
Expand Down
48 changes: 46 additions & 2 deletions src/cli/FrodoCommand.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ const {
RETRY_STRATEGIES,
RETRY_NOTHING_KEY,
} = frodo.utils.constants;
const { convertPrivateKeyToPem } = frodo.utils.crypto;

const hostArgument = new Argument(
'[host]',
Expand Down Expand Up @@ -68,6 +69,16 @@ const serviceAccountJwkFileOption = new Option(
'File containing the JSON Web Key (JWK) associated with the the service account.'
);

const amsterPrivateKeyPassphraseOption = new Option(
'--passphrase <passphrase>',
'The passphrase for the Amster private key if it is encrypted.'
);

const amsterPrivateKeyFileOption = new Option(
'--private-key <file>',
'File containing the private key for authenticating with Amster. Supported formats include PEM (both PKCS#1 and PKCS#8 variants), OpenSSH, DNSSEC, and JWK.'
);

const deploymentOption = new Option(
'-m, --type <type>',
'Override auto-detected deployment type. Valid values for type: \n\
Expand Down Expand Up @@ -135,6 +146,8 @@ const defaultOpts = [
loginRedirectUri,
serviceAccountIdOption,
serviceAccountJwkFileOption,
amsterPrivateKeyPassphraseOption,
amsterPrivateKeyFileOption,
deploymentOption,
directoryOption,
insecureOption,
Expand Down Expand Up @@ -171,6 +184,35 @@ const stateMap = {
);
}
},
[amsterPrivateKeyPassphraseOption.attributeName()]: (passphrase: string) => {
// This is needed in the case the passphrase is an option, but the private key is an environment variable.
process.env.FRODO_AMSTER_PASSPHRASE = passphrase;
},
[amsterPrivateKeyFileOption.attributeName()]: (
file: string,
options: Record<string, string | boolean>
) => {
const passphrase =
(options[amsterPrivateKeyPassphraseOption.attributeName()] as string) ||
process.env.FRODO_AMSTER_PASSPHRASE;
try {
// Store as PEM format (PKCS#8 variant specifically) since Jose supports PEM and since PKCS#8 supports more algorithms than PKCS#1
state.setAmsterPrivateKey(
convertPrivateKeyToPem(
fs.readFileSync(file, 'utf8'),
passphrase,
file
.replaceAll('\\', '/')
.substring(file.replaceAll('\\', '/').lastIndexOf('/') + 1)
)
);
} catch (error) {
printMessage(
`Error parsing private key from file ${file}: ${error.message}`,
'error'
);
}
},
[deploymentOption.attributeName()]: (type: string) =>
state.setDeploymentType(type),
[directoryOption.attributeName()]: (directory: string) =>
Expand Down Expand Up @@ -298,6 +340,8 @@ export class FrodoCommand extends FrodoStubCommand {
` FRODO_LOGIN_REDIRECT_URI: Redirect Uri for custom OAuth2 client id. Overridden by '--login-redirect-uri' option.\n` +
` FRODO_SA_ID: Service account uuid. Overridden by '--sa-id' option.\n` +
` FRODO_SA_JWK: Service account JWK. Overridden by '--sa-jwk-file' option but takes the actual JWK as a value, not a file name.\n` +
` FRODO_AMSTER_PASSPHRASE: Passphrase for the Amster private key if it is encrypted. Overridden by '--passphrase' option.\n` +
` FRODO_AMSTER_PRIVATE_KEY: Amster private key. Overridden by '--private-key' option but takes the actual private key as a value (i.e. the file contents), not a file name. Supported formats include PEM (both PKCS#1 and PKCS#8 variants), OpenSSH, DNSSEC, and JWK.\n` +
` FRODO_NO_CACHE: Disable token cache. Same as '--no-cache' option.\n` +
` FRODO_TOKEN_CACHE_PATH: Use this token cache file instead of '~/.frodo/TokenCache.json'.\n` +
('frodo conn save' === this.name()
Expand All @@ -309,7 +353,7 @@ export class FrodoCommand extends FrodoStubCommand {
` FRODO_LOG_SECRET: Log API secret. Overridden by 'password' argument.\n`
: ``) +
` FRODO_CONNECTION_PROFILES_PATH: Use this connection profiles file instead of '~/.frodo/Connections.json'.\n` +
` FRODO_AUTHENTICATION_SERVICE: Name of a login journey to use.\n` +
` FRODO_AUTHENTICATION_SERVICE: Name of a login journey to use. When using an Amster private key, specifies which journey to use for Amster authentication as opposed to the default 'amsterService' journey.\n` +
` FRODO_DEBUG: Set to any value to enable debug output. Same as '--debug'.\n` +
` FRODO_MASTER_KEY_PATH: Use this master key file instead of '~/.frodo/masterkey.key' file.\n` +
` FRODO_MASTER_KEY: Use this master key instead of what's in '~/.frodo/masterkey.key'. Takes precedence over FRODO_MASTER_KEY_PATH.\n`
Expand Down Expand Up @@ -359,7 +403,7 @@ export class FrodoCommand extends FrodoStubCommand {
);
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const handler: any = stateMap[k];
handler(v);
handler(v, options);
} else {
debugMessage(
`FrodoCommand.handleDefaultArgsAndOpts: Ignoring non-default option '${k}'.`
Expand Down
11 changes: 10 additions & 1 deletion src/cli/conn/conn-save.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,10 @@ export default function setup() {
` $ frodo conn save ${s.amBaseUrl} ${s.username} '${s.password}'\n`[
'brightCyan'
] +
` Create a connection profile using Amster private key credentials (PingAM classic deployments only):\n` +
` $ frodo conn save --private-key ${s.amsterPrivateKey} ${s.amClassicBaseUrl}\n`[
'brightCyan'
] +
` Save an existing service account to an existing or new connection profile:\n` +
` $ frodo conn save --sa-id ${s.saId} --sa-jwk-file ${s.saJwkFile} ${s.amBaseUrl}\n`[
'brightCyan'
Expand All @@ -72,6 +76,10 @@ export default function setup() {
` Update an existing connection profile with a custom header override for a freshly Proxy Connect-protected PingOne Advanced Identity Cloud environment:\n` +
` $ frodo conn save --authentication-header-overrides '{"MY-SECRET-HEADER": "proxyconnect secret header value"}' ${s.connId}\n`[
'brightCyan'
] +
` Update an existing connection profile to use Amster private key credentials with a custom Amster journey (PingAM classic deployments only):\n` +
` $ frodo conn save --private-key ${s.amsterPrivateKey} --authentication-service ${s.customAmsterService} ${s.classicConnId}\n`[
'brightCyan'
]
)
.action(
Expand All @@ -94,6 +102,7 @@ export default function setup() {
JSON.parse(options.authenticationHeaderOverrides)
);
}
const needAmsterLogin = !!options.privateKey;
const needSa =
options.sa &&
!state.getServiceAccountId() &&
Expand All @@ -103,7 +112,7 @@ export default function setup() {
!state.getLogApiKey() &&
!state.getLogApiSecret() &&
needSa;
const forceLoginAsUser = needSa || needLogApiKey;
const forceLoginAsUser = !needAmsterLogin && (needSa || needLogApiKey);
if (
(options.validate && (await getTokens(forceLoginAsUser))) ||
!options.validate
Expand Down
5 changes: 5 additions & 0 deletions src/help/SampleData.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,8 @@ export const connId2 = 'zion';
export const username2 = 'neo@nebuchadnezzar.zion.com';
export const password2 = 'R3dP!ll3d';
export const realm = '/alpha';
export const amClassicBaseUrl = 'https://am.example.com:8443/am';
export const classicConnId = 'am.example';
export const amsterPrivateKey =
'/home/trinity/am/security/keys/amster/amster_rsa';
export const customAmsterService = 'AmsterLogin';
14 changes: 14 additions & 0 deletions src/ops/ConnectionProfileOps.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,13 +34,15 @@ export function listConnectionProfiles(long: boolean = false): void {
'Service Account',
'Username',
'Log API Key',
'Authentication Service',
]);
Object.keys(connectionsData).forEach((c) => {
table.push([
c,
connectionsData[c].svcacctName || connectionsData[c].svcacctId,
connectionsData[c].username,
connectionsData[c].logApiKey,
connectionsData[c].authenticationService,
]);
});
printMessage(table.toString(), 'data');
Expand Down Expand Up @@ -76,10 +78,12 @@ export async function describeConnectionProfile(
debugMessage(profile);
const present = '[present]';
const jwk = profile.svcacctJwk;
const privateKey = profile.amsterPrivateKey;
if (!showSecrets) {
if (profile.password) profile.password = present;
if (profile.logApiSecret) profile.logApiSecret = present;
if (profile.svcacctJwk) (profile as unknown)['svcacctJwk'] = present;
if (profile.amsterPrivateKey) profile.amsterPrivateKey = present;
}
if (!profile.idmHost) {
delete profile.idmHost;
Expand Down Expand Up @@ -122,9 +126,15 @@ export async function describeConnectionProfile(
// do nothing
}
}
if (!profile.amsterPrivateKey) {
delete profile.amsterPrivateKey;
}
if (showSecrets && jwk) {
(profile as unknown)['svcacctJwk'] = 'see below';
}
if (showSecrets && privateKey) {
profile.amsterPrivateKey = 'see below';
}
if (!profile.authenticationService) {
delete profile.authenticationService;
}
Expand All @@ -141,12 +151,16 @@ export async function describeConnectionProfile(
svcacctId: 'Service Account Id',
svcacctJwk: 'Service Account JWK',
svcacctScope: 'Service Account Scope',
amsterPrivateKey: 'Amster Private Key',
};
const table = createObjectTable(profile, keyMap);
printMessage(table.toString(), 'data');
if (showSecrets && jwk) {
printMessage(JSON.stringify(jwk), 'data');
}
if (showSecrets && privateKey) {
printMessage(privateKey, 'data');
}
} else {
printMessage(`No connection profile ${host} found`);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,13 @@ Options:
(choices: "classic", "cloud",
"forgeops")
--no-cache Disable token cache for this operation.
--passphrase <passphrase> The passphrase for the Amster private
key if it is encrypted.
--private-key <file> File containing the private key for
authenticating with Amster. Supported
formats include PEM (both PKCS#1 and
PKCS#8 variants), OpenSSH, DNSSEC, and
JWK.
--retry <strategy> Retry failed operations. Valid values
for strategy:
everything: Retry all failed operations.
Expand Down Expand Up @@ -101,10 +108,12 @@ Environment Variables:
FRODO_LOGIN_REDIRECT_URI: Redirect Uri for custom OAuth2 client id. Overridden by '--login-redirect-uri' option.
FRODO_SA_ID: Service account uuid. Overridden by '--sa-id' option.
FRODO_SA_JWK: Service account JWK. Overridden by '--sa-jwk-file' option but takes the actual JWK as a value, not a file name.
FRODO_AMSTER_PASSPHRASE: Passphrase for the Amster private key if it is encrypted. Overridden by '--passphrase' option.
FRODO_AMSTER_PRIVATE_KEY: Amster private key. Overridden by '--private-key' option but takes the actual private key as a value (i.e. the file contents), not a file name. Supported formats include PEM (both PKCS#1 and PKCS#8 variants), OpenSSH, DNSSEC, and JWK.
FRODO_NO_CACHE: Disable token cache. Same as '--no-cache' option.
FRODO_TOKEN_CACHE_PATH: Use this token cache file instead of '~/.frodo/TokenCache.json'.
FRODO_CONNECTION_PROFILES_PATH: Use this connection profiles file instead of '~/.frodo/Connections.json'.
FRODO_AUTHENTICATION_SERVICE: Name of a login journey to use.
FRODO_AUTHENTICATION_SERVICE: Name of a login journey to use. When using an Amster private key, specifies which journey to use for Amster authentication as opposed to the default 'amsterService' journey.
FRODO_DEBUG: Set to any value to enable debug output. Same as '--debug'.
FRODO_MASTER_KEY_PATH: Use this master key file instead of '~/.frodo/masterkey.key' file.
FRODO_MASTER_KEY: Use this master key instead of what's in '~/.frodo/masterkey.key'. Takes precedence over FRODO_MASTER_KEY_PATH.
Expand Down
Loading