From 8f2bf40462e6c965e45144e88c2106989fc95617 Mon Sep 17 00:00:00 2001 From: deanshak Date: Mon, 15 Dec 2025 14:21:54 +0200 Subject: [PATCH 1/3] Fix rotated secrets JSON parsing when parse-json-secrets is enabled - Handle rotatedSecret.value when it's already an object by stringifying it before parsing - Update parseJson function to handle both objects and strings - Improve error messages to include secret name for better debugging - Fixes issue where rotated secrets with parse-json-secrets=true would fail to export individual fields --- src/secrets.js | 41 ++++++++++++++++++++++++++++++----------- 1 file changed, 30 insertions(+), 11 deletions(-) diff --git a/src/secrets.js b/src/secrets.js index a7b2497..7dc9c83 100644 --- a/src/secrets.js +++ b/src/secrets.js @@ -104,18 +104,29 @@ async function exportRotatedSecrets(akeylessToken, rotatedSecrets, apiUrl, expor }); let rotatedSecret = await api.getRotatedSecretValue(param).catch(error => { - core.debug(`getRotatedSecret Failed: ${JSON.stringify(error)}`); - core.setFailed(`get rotated secret failed`); + core.debug(`getRotatedSecretValue Failed for secret '${secretName}': ${JSON.stringify(error)}`); + const errorMessage = error?.body?.error || error?.message || JSON.stringify(error); + core.setFailed(`Failed to get rotated secret '${secretName}': ${errorMessage}`); + throw error; }); if (!rotatedSecret) { - return + core.setFailed(`No response received for rotated secret '${secretName}'`); + return; + } + + let secretValue = rotatedSecret.value; + + if (typeof secretValue === 'object' && secretValue !== null) { + secretValue = JSON.stringify(secretValue); } - setOutput(rotatedSecret.value, rotateParams, exportSecretsToOutputs, exportSecretsToEnvironment, parseJsonSecrets) + + setOutput(secretValue, rotateParams, exportSecretsToOutputs, exportSecretsToEnvironment, parseJsonSecrets) } } catch (error) { - core.debug(`Failed to export rotated secret: ${typeof error === 'object' ? JSON.stringify(error) : error}`); - core.setFailed('Failed to export rotated secret'); + core.debug(`Failed to export rotated secret '${secretName}': ${typeof error === 'object' ? JSON.stringify(error) : error}`); + const errorMessage = error?.body?.error || error?.message || 'Unknown error'; + core.setFailed(`Failed to export rotated secret '${secretName}': ${errorMessage}`); } } @@ -226,12 +237,20 @@ function validateNoDuplicateKeys(parsedJson) { } function parseJson(jsonString) { - try { - const parsedJson = JSON.parse(jsonString); - return parsedJson; - } catch (e) { - return null; + if (typeof jsonString === 'object' && jsonString !== null) { + return jsonString; + } + + if (typeof jsonString === 'string') { + try { + const parsedJson = JSON.parse(jsonString); + return parsedJson; + } catch (e) { + return null; + } } + + return null; } async function handleCreateSecrets(args) { From 731e04a66498450a275064f02b2fbb76e931f0db Mon Sep 17 00:00:00 2001 From: deanshak Date: Mon, 15 Dec 2025 14:44:22 +0200 Subject: [PATCH 2/3] Fix rotated secrets JSON parsing when parse-json-secrets is enabled - Handle rotatedSecret.value when it's already an object by stringifying it before parsing (only when parseJsonSecrets=true) - Update parseJson function to handle both objects and strings - Improve error messages to include secret name for better debugging - Preserve backward compatibility: old behavior maintained when parseJsonSecrets=false - Bump version to v1.1.4 Fixes issue where rotated secrets with parse-json-secrets=true would fail to export individual fields --- src/secrets.js | 2 +- version | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/secrets.js b/src/secrets.js index 7dc9c83..621b72c 100644 --- a/src/secrets.js +++ b/src/secrets.js @@ -117,7 +117,7 @@ async function exportRotatedSecrets(akeylessToken, rotatedSecrets, apiUrl, expor let secretValue = rotatedSecret.value; - if (typeof secretValue === 'object' && secretValue !== null) { + if (parseJsonSecrets && typeof secretValue === 'object' && secretValue !== null) { secretValue = JSON.stringify(secretValue); } diff --git a/version b/version index 48afe85..f386275 100644 --- a/version +++ b/version @@ -1,2 +1,2 @@ # Use Semantic versioning only. Please update the version number before opening a pull request. -v1.1.3 +v1.1.4 From 20708353a3ff604eb39166eda5ddaa95c449cc92 Mon Sep 17 00:00:00 2001 From: deanshak Date: Mon, 15 Dec 2025 19:34:40 +0200 Subject: [PATCH 3/3] Add tests for rotated secrets with parse-json-secrets - Add test for parse-json-secrets=true with prefix (verifies the fix) - Add test for parse-json-secrets=true without prefix (default prefix) - Add test for parseJsonSecrets=false (backward compatibility verification) - All 25 tests pass, covering the fix and backward compatibility --- tests/secrets.test.js | 150 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 150 insertions(+) diff --git a/tests/secrets.test.js b/tests/secrets.test.js index 0bb7dec..9f0c041 100644 --- a/tests/secrets.test.js +++ b/tests/secrets.test.js @@ -219,6 +219,156 @@ describe('testing secret exports', () => { expect(core.exportVariable).toHaveBeenCalledWith('my_second_secret', {"/some2/rotated2/secret2": 'secret-value-2'}); }); + it('should export rotated secret with parse-json-secrets=true (fix for object response)', async function () { + const args = { + akeylessToken: "akeylessToken", + staticSecrets: undefined, + dynamicSecrets: undefined, + rotatedSecrets: [ + { + "name": "/some/rotated/secret", + "prefix-json-secrets": "CREDS" + } + ], + apiUrl: 'https://api.akeyless.io', + exportSecretsToOutputs: true, + exportSecretsToEnvironment: true, + parseJsonSecrets: true, + sshCertificate: undefined, + pkiCertificate: undefined + } + const api = { + getRotatedSecretValue: jest.fn(), + }; + akeylessApi.api.mockReturnValue(api); + + // Simulate the real API response where value is an object (not a string) + api.getRotatedSecretValue.mockResolvedValueOnce({ + value: { + "username": "testuser", + "password": "testpass123", + "host": "db.example.com" + }, + }); + + core.setSecret = jest.fn(); + core.setOutput = jest.fn(); + core.exportVariable = jest.fn(); + + await secrets.handleExportSecrets(args) + + expect(akeylessApi.api).toHaveBeenCalledWith(args.apiUrl); + expect(api.getRotatedSecretValue).toHaveBeenCalledTimes(1); + expect(api.getRotatedSecretValue).toHaveBeenCalledWith({ + token: args.akeylessToken, + names: "/some/rotated/secret", + }); + + // Verify that individual JSON fields are exported with the prefix + expect(core.setSecret).toHaveBeenCalledTimes(4); // 3 fields + token + expect(core.setOutput).toHaveBeenCalledWith('CREDS_USERNAME', 'testuser'); + expect(core.setOutput).toHaveBeenCalledWith('CREDS_PASSWORD', 'testpass123'); + expect(core.setOutput).toHaveBeenCalledWith('CREDS_HOST', 'db.example.com'); + expect(core.exportVariable).toHaveBeenCalledWith('CREDS_USERNAME', 'testuser'); + expect(core.exportVariable).toHaveBeenCalledWith('CREDS_PASSWORD', 'testpass123'); + expect(core.exportVariable).toHaveBeenCalledWith('CREDS_HOST', 'db.example.com'); + + // Verify that the entire object is NOT exported as "undefined" (old bug) + const outputCalls = core.setOutput.mock.calls; + const undefinedOutput = outputCalls.find(call => call[0] === undefined); + expect(undefinedOutput).toBeUndefined(); + }); + + it('should export rotated secret with parse-json-secrets=true and no prefix', async function () { + const args = { + akeylessToken: "akeylessToken", + staticSecrets: undefined, + dynamicSecrets: undefined, + rotatedSecrets: [ + { + "name": "/database/credentials" + } + ], + apiUrl: 'https://api.akeyless.io', + exportSecretsToOutputs: true, + exportSecretsToEnvironment: false, + parseJsonSecrets: true, + sshCertificate: undefined, + pkiCertificate: undefined + } + const api = { + getRotatedSecretValue: jest.fn(), + }; + akeylessApi.api.mockReturnValue(api); + + // Simulate the real API response where value is an object + api.getRotatedSecretValue.mockResolvedValueOnce({ + value: { + "db_user": "admin", + "db_password": "secret123" + }, + }); + + core.setSecret = jest.fn(); + core.setOutput = jest.fn(); + core.exportVariable = jest.fn(); + + await secrets.handleExportSecrets(args) + + expect(api.getRotatedSecretValue).toHaveBeenCalledTimes(1); + + // Verify that individual JSON fields are exported with default prefix (from path) + expect(core.setSecret).toHaveBeenCalledTimes(3); // 2 fields + token + expect(core.setOutput).toHaveBeenCalledWith('DATABASE_CREDENTIALS_DB_USER', 'admin'); + expect(core.setOutput).toHaveBeenCalledWith('DATABASE_CREDENTIALS_DB_PASSWORD', 'secret123'); + }); + + it('should export rotated secret with parseJsonSecrets=false (backward compatibility)', async function () { + const args = { + akeylessToken: "akeylessToken", + staticSecrets: undefined, + dynamicSecrets: undefined, + rotatedSecrets: [{"name":"/some/rotated/secret","output-name":"my_rotated_secret"}], + apiUrl: 'https://api.akeyless.io', + exportSecretsToOutputs: true, + exportSecretsToEnvironment: true, + parseJsonSecrets: false, + sshCertificate: undefined, + pkiCertificate: undefined + } + const api = { + getRotatedSecretValue: jest.fn(), + }; + akeylessApi.api.mockReturnValue(api); + + // API returns object (real-world scenario) + api.getRotatedSecretValue.mockResolvedValueOnce({ + value: { + "username": "testuser", + "password": "testpass123" + }, + }); + + core.setSecret = jest.fn(); + core.setOutput = jest.fn(); + core.exportVariable = jest.fn(); + + await secrets.handleExportSecrets(args) + + expect(api.getRotatedSecretValue).toHaveBeenCalledTimes(1); + + // With parseJsonSecrets=false, the object should be exported as-is (old behavior) + expect(core.setSecret).toHaveBeenCalledTimes(2); // 1 secret + token + expect(core.setOutput).toHaveBeenCalledWith('my_rotated_secret', { + "username": "testuser", + "password": "testpass123" + }); + expect(core.exportVariable).toHaveBeenCalledWith('my_rotated_secret', { + "username": "testuser", + "password": "testpass123" + }); + }); + it('should export ssh certificate secret', async function () { const args = { akeylessToken: "akeylessToken",