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
41 changes: 30 additions & 11 deletions src/secrets.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 (parseJsonSecrets && 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}`);
}
}

Expand Down Expand Up @@ -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) {
Expand Down
150 changes: 150 additions & 0 deletions tests/secrets.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
2 changes: 1 addition & 1 deletion version
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
# Use Semantic versioning only. Please update the version number before opening a pull request.
v1.1.3
v1.1.4