Skip to content
Open
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
1,237 changes: 549 additions & 688 deletions dist/index.js

Large diffs are not rendered by default.

86 changes: 7 additions & 79 deletions package-lock.json

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

4 changes: 1 addition & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,11 @@
"license": "ISC",
"dependencies": {
"@actions/core": "^1.11.1",
"@aws-crypto/sha256-js": "^5.2.0",
"@aws-sdk/credential-providers": "^3.1090.0",
"@azure/identity": "^4.8.0",
"@google-cloud/iam-credentials": "^3.3.0",
"@smithy/protocol-http": "^5.3.8",
"@smithy/signature-v4": "^5.3.8",
"akeyless": "^3.3.16",
"aws4": "^1.13.2",
"google-auth-library": "^9.15.1",
"js-yaml": "^4.1.0"
},
Expand Down
10 changes: 9 additions & 1 deletion src/akeyless_api.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,12 @@ const akeyless = require('akeyless');
const https = require('https');
const core = require("@actions/core");

// superagent@10 (forced via package.json overrides for DEP0169) no longer sets a
// default User-Agent. superagent@3 always sent "node-superagent/3.7.0". Some
// Gateways / ingress / WAF policies reject requests with no User-Agent, which
// surfaced as "Failed to login to Akeyless" after v1.1.7.
const DEFAULT_USER_AGENT = 'node-superagent/3.7.0 akeyless-github-action';

function api(url) {
const client = new akeyless.ApiClient();

Expand All @@ -14,10 +20,12 @@ function api(url) {
}

client.defaultHeaders = {
'akeylessclienttype': 'github_action'
'akeylessclienttype': 'github_action',
'User-Agent': DEFAULT_USER_AGENT,
}
client.basePath = url;
return new akeyless.V2Api(client);
}

exports.api = api;
exports.DEFAULT_USER_AGENT = DEFAULT_USER_AGENT;
27 changes: 20 additions & 7 deletions src/auth.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,10 @@ const path = require('path');
const akeylessCloud = require('./cloud_id');

function handleActionFail(message, debugMessage) {
core.debug(debugMessage); // Only visible with ACTIONS_RUNNER_DEBUG=true
core.setFailed(message); // Always visible
throw new Error(message);
const detail = debugMessage || message;
core.error(detail);
core.setFailed(detail);
throw new Error(detail);
}


Expand All @@ -35,7 +36,15 @@ async function jwtLogin(apiUrl, accessId) {

async function awsIamLogin(apiUrl, accessId) {
core.debug('getting aws cloud id');
const awsCloudId = await akeylessCloud.getCloudId('aws_iam')
let awsCloudId;
try {
awsCloudId = await akeylessCloud.getCloudId('aws_iam');
} catch (error) {
handleActionFail(
'Failed to login to Akeyless',
`Failed to obtain AWS cloud id (check runner IAM role / credentials): ${error && error.message ? error.message : error}`
);
}
const opts = {
"access-type": 'aws_iam',
'access-id': accessId,
Expand Down Expand Up @@ -95,7 +104,10 @@ async function loginHelper(opts, apiUrl) {
const authResult = await api.auth(authBody)
return authResult
} catch (error) {
handleActionFail('Failed to login to Akeyless', `Failed to login to AKeyless: ${typeof error === 'object' ? JSON.stringify(error) : error}`)
const detail = typeof error === 'object'
? (error.body ? JSON.stringify(error.body) : JSON.stringify(error))
: String(error);
handleActionFail('Failed to login to Akeyless', `Failed to login to Akeyless: ${detail}`)
}
}

Expand All @@ -114,9 +126,10 @@ const allowedAccessTypes = Object.keys(login);
async function akeylessLogin(accessId, accessType, apiUrl) {
try {
core.debug('fetch token');
return login[accessType](apiUrl, accessId);
return await login[accessType](apiUrl, accessId);
} catch (error) {
handleActionFail('failed to fetch token', error.message);
// login helpers already call setFailed; rethrow so callers stop
throw error;
}
}

Expand Down
69 changes: 26 additions & 43 deletions src/cloud_id.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
/**
* Cloud identity helpers for Akeyless cloud auth (AWS IAM / Azure AD / GCP).
* AWS uses AWS SDK for JavaScript v3 credential providers + Smithy SigV4
* (no aws-sdk v2 / aws4 url.parse).
*
* AWS credentials: AWS SDK v3 provider chain (no aws-sdk v2).
* AWS STS signing: aws4 — same algorithm/header layout as akeyless-cloud-id
* (Smithy SigV4 produced SignatureDoesNotMatch when Akeyless replayed the request).
*
* Provider SDKs are required lazily so jwt/access_key auth does not load them.
*/
Expand Down Expand Up @@ -43,8 +45,6 @@ async function getGcpCloudId(audience) {
if (typeof client.fetchIdToken === 'function') {
token = await client.fetchIdToken(audience);
} else if (client.serviceAccountImpersonationUrl) {
// WIF: get ID token via IAM Credentials API.
// URL format: https://iamcredentials.googleapis.com/v1/{name=projects/*/serviceAccounts/*}:generateAccessToken
const { IAMCredentialsClient } = require('@google-cloud/iam-credentials');
const url = client.serviceAccountImpersonationUrl;
const name = url.match(/projects\/[^:]+/)?.[0];
Expand Down Expand Up @@ -75,57 +75,40 @@ async function getAwsCloudId() {
});
}

async function stsGetCallerIdentity(creds) {
const { SignatureV4 } = require('@smithy/signature-v4');
const { HttpRequest } = require('@smithy/protocol-http');
const { Sha256 } = require('@aws-crypto/sha256-js');

const body = 'Action=GetCallerIdentity&Version=2011-06-15';
const host = 'sts.amazonaws.com';
const region = 'us-east-1';

const signer = new SignatureV4({
credentials: {
accessKeyId: creds.accessKeyId,
secretAccessKey: creds.secretAccessKey,
sessionToken: creds.sessionToken,
},
region,
service: 'sts',
sha256: Sha256,
});

const request = new HttpRequest({
/**
* Must match akeyless-cloud-id's aws4 signing layout so Akeyless can replay
* the STS GetCallerIdentity request without SignatureDoesNotMatch.
*/
function stsGetCallerIdentity(creds) {
const aws4 = require('aws4');
const opts = {
method: 'POST',
protocol: 'https:',
hostname: host,
path: '/',
service: 'sts',
body: 'Action=GetCallerIdentity&Version=2011-06-15',
region: 'us-east-1',
headers: {
host,
'content-type': 'application/x-www-form-urlencoded; charset=utf-8',
'content-length': String(Buffer.byteLength(body)),
'Content-Length': 'Action=GetCallerIdentity&Version=2011-06-15'.length,
'Content-Type': 'application/x-www-form-urlencoded; charset=utf-8',
},
body,
});
};

const signed = await signer.sign(request);
aws4.sign(opts, creds);

const h = {
Authorization: [signed.headers.authorization || signed.headers.Authorization],
'Content-Length': [String(Buffer.byteLength(body))],
Host: [host],
'Content-Type': ['application/x-www-form-urlencoded; charset=utf-8'],
'X-Amz-Date': [signed.headers['x-amz-date'] || signed.headers['X-Amz-Date']],
Authorization: [opts.headers.Authorization],
'Content-Length': [opts.body.length.toString()],
Host: [opts.headers.Host],
'Content-Type': [opts.headers['Content-Type']],
'X-Amz-Date': [opts.headers['X-Amz-Date']],
};
const securityToken = signed.headers['x-amz-security-token'] || signed.headers['X-Amz-Security-Token'] || creds.sessionToken;
if (securityToken) {
h['X-Amz-Security-Token'] = [securityToken];
if (creds.sessionToken) {
h['X-Amz-Security-Token'] = [creds.sessionToken];
}

const obj = {
sts_request_method: 'POST',
sts_request_url: Buffer.from('https://sts.amazonaws.com/').toString('base64'),
sts_request_body: Buffer.from(body).toString('base64'),
sts_request_body: Buffer.from('Action=GetCallerIdentity&Version=2011-06-15').toString('base64'),
sts_request_headers: Buffer.from(JSON.stringify(h)).toString('base64'),
};
return Buffer.from(JSON.stringify(obj)).toString('base64');
Expand Down
2 changes: 1 addition & 1 deletion src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ async function run() {
}
} catch (error) {
core.debug(`Failed to login to Akeyless: ${error}`);
core.setFailed(`Failed to login to Akeyless`);
core.setFailed(`Failed to login to Akeyless: ${error && error.message ? error.message : error}`);
return;
}

Expand Down
47 changes: 47 additions & 0 deletions tests/akeyless_api.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
jest.mock('@actions/core');

const http = require('http');
const akeyless = require('akeyless');
const core = require('@actions/core');
const { api, DEFAULT_USER_AGENT } = require('../src/akeyless_api');

describe('akeyless_api', () => {
beforeEach(() => {
core.getInput = jest.fn(() => '');
});

it('sets User-Agent on ApiClient defaultHeaders for superagent@10 compatibility', () => {
const clientApi = api('https://api.akeyless.io');
// V2Api keeps a reference to the ApiClient
expect(clientApi.apiClient.defaultHeaders['User-Agent']).toBe(DEFAULT_USER_AGENT);
expect(clientApi.apiClient.defaultHeaders['akeylessclienttype']).toBe('github_action');
});

it('sends User-Agent on outbound auth requests', async () => {
const seen = await new Promise((resolve, reject) => {
const server = http.createServer((req, res) => {
const headers = { ...req.headers };
res.writeHead(401, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ error: 'unauthorized' }));
server.close(() => resolve(headers));
});
server.on('error', reject);
server.listen(0, async () => {
const port = server.address().port;
const clientApi = api(`http://127.0.0.1:${port}`);
try {
await clientApi.auth(akeyless.Auth.constructFromObject({
'access-type': 'access_key',
'access-id': 'p-test',
'access-key': 'test-key',
}));
} catch (_) {
// expected 401 from mock server
}
});
});

expect(seen['user-agent']).toBe(DEFAULT_USER_AGENT);
expect(seen['akeylessclienttype']).toBe('github_action');
});
});
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.7
v1.1.8