Skip to content

Commit 472e549

Browse files
clareliguorimergify[bot]
andauthoredJun 3, 2020
feat: Refresh and validate credentials after setting env var creds (#71)
* feat: Refresh and validate credentials after setting env var creds * Positive test case Co-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com>
1 parent 187737a commit 472e549

File tree

3 files changed

+193
-2
lines changed

3 files changed

+193
-2
lines changed
 

‎dist/index.js

+52
Original file line numberDiff line numberDiff line change
@@ -265,6 +265,50 @@ async function exportAccountId(maskAccountId, region) {
265265
return accountId;
266266
}
267267

268+
function loadCredentials() {
269+
// Force the SDK to re-resolve credentials with the default provider chain.
270+
//
271+
// This action typically sets credentials in the environment via environment variables.
272+
// The SDK never refreshes those env-var-based credentials after initial load.
273+
// In case there were already env-var creds set in the actions environment when this action
274+
// loaded, this action needs to refresh the SDK creds after overwriting those environment variables.
275+
//
276+
// The credentials object needs to be entirely recreated (instead of simply refreshed),
277+
// because the credential object type could change when this action writes env var creds.
278+
// For example, the first load could return EC2 instance metadata credentials
279+
// in a self-hosted runner, and the second load could return environment credentials
280+
// from an assume-role call in this action.
281+
aws.config.credentials = null;
282+
283+
return new Promise((resolve, reject) => {
284+
aws.config.getCredentials((err) => {
285+
if (err) {
286+
reject(err);
287+
}
288+
resolve(aws.config.credentials);
289+
})
290+
});
291+
}
292+
293+
async function validateCredentials(expectedAccessKeyId) {
294+
let credentials;
295+
try {
296+
credentials = await loadCredentials();
297+
298+
if (!credentials.accessKeyId) {
299+
throw new Error('Access key ID empty after loading credentials');
300+
}
301+
} catch (error) {
302+
throw new Error(`Credentials could not be loaded, please check your action inputs: ${error.message}`);
303+
}
304+
305+
const actualAccessKeyId = credentials.accessKeyId;
306+
307+
if (expectedAccessKeyId && expectedAccessKeyId != actualAccessKeyId) {
308+
throw new Error('Unexpected failure: Credentials loaded by the SDK do not match the access key ID configured by the action');
309+
}
310+
}
311+
268312
function getStsClient(region) {
269313
return new aws.STS({
270314
region,
@@ -305,6 +349,13 @@ async function run() {
305349
exportCredentials({accessKeyId, secretAccessKey, sessionToken});
306350
}
307351

352+
// Regardless of whether any source credentials were provided as inputs,
353+
// validate that the SDK can actually pick up credentials. This validates
354+
// cases where this action is on a self-hosted runner that doesn't have credentials
355+
// configured correctly, and cases where the user intended to provide input
356+
// credentials but the secrets inputs resolved to empty strings.
357+
await validateCredentials(accessKeyId);
358+
308359
const sourceAccountId = await exportAccountId(maskAccountId, region);
309360

310361
// Get role credentials if configured to do so
@@ -318,6 +369,7 @@ async function run() {
318369
roleSessionName
319370
});
320371
exportCredentials(roleCredentials);
372+
await validateCredentials(roleCredentials.accessKeyId);
321373
await exportAccountId(maskAccountId, region);
322374
}
323375
}

‎index.js

+52
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,50 @@ async function exportAccountId(maskAccountId, region) {
132132
return accountId;
133133
}
134134

135+
function loadCredentials() {
136+
// Force the SDK to re-resolve credentials with the default provider chain.
137+
//
138+
// This action typically sets credentials in the environment via environment variables.
139+
// The SDK never refreshes those env-var-based credentials after initial load.
140+
// In case there were already env-var creds set in the actions environment when this action
141+
// loaded, this action needs to refresh the SDK creds after overwriting those environment variables.
142+
//
143+
// The credentials object needs to be entirely recreated (instead of simply refreshed),
144+
// because the credential object type could change when this action writes env var creds.
145+
// For example, the first load could return EC2 instance metadata credentials
146+
// in a self-hosted runner, and the second load could return environment credentials
147+
// from an assume-role call in this action.
148+
aws.config.credentials = null;
149+
150+
return new Promise((resolve, reject) => {
151+
aws.config.getCredentials((err) => {
152+
if (err) {
153+
reject(err);
154+
}
155+
resolve(aws.config.credentials);
156+
})
157+
});
158+
}
159+
160+
async function validateCredentials(expectedAccessKeyId) {
161+
let credentials;
162+
try {
163+
credentials = await loadCredentials();
164+
165+
if (!credentials.accessKeyId) {
166+
throw new Error('Access key ID empty after loading credentials');
167+
}
168+
} catch (error) {
169+
throw new Error(`Credentials could not be loaded, please check your action inputs: ${error.message}`);
170+
}
171+
172+
const actualAccessKeyId = credentials.accessKeyId;
173+
174+
if (expectedAccessKeyId && expectedAccessKeyId != actualAccessKeyId) {
175+
throw new Error('Unexpected failure: Credentials loaded by the SDK do not match the access key ID configured by the action');
176+
}
177+
}
178+
135179
function getStsClient(region) {
136180
return new aws.STS({
137181
region,
@@ -172,6 +216,13 @@ async function run() {
172216
exportCredentials({accessKeyId, secretAccessKey, sessionToken});
173217
}
174218

219+
// Regardless of whether any source credentials were provided as inputs,
220+
// validate that the SDK can actually pick up credentials. This validates
221+
// cases where this action is on a self-hosted runner that doesn't have credentials
222+
// configured correctly, and cases where the user intended to provide input
223+
// credentials but the secrets inputs resolved to empty strings.
224+
await validateCredentials(accessKeyId);
225+
175226
const sourceAccountId = await exportAccountId(maskAccountId, region);
176227

177228
// Get role credentials if configured to do so
@@ -185,6 +236,7 @@ async function run() {
185236
roleSessionName
186237
});
187238
exportCredentials(roleCredentials);
239+
await validateCredentials(roleCredentials.accessKeyId);
188240
await exportAccountId(maskAccountId, region);
189241
}
190242
}

‎index.test.js

+89-2
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
const core = require('@actions/core');
22
const assert = require('assert');
3-
3+
const aws = require('aws-sdk');
44
const run = require('.');
55

66
jest.mock('@actions/core');
@@ -49,6 +49,9 @@ const mockStsAssumeRole = jest.fn();
4949

5050
jest.mock('aws-sdk', () => {
5151
return {
52+
config: {
53+
getCredentials: jest.fn()
54+
},
5255
STS: jest.fn(() => ({
5356
getCallerIdentity: mockStsCallerIdentity,
5457
assumeRole: mockStsAssumeRole,
@@ -82,6 +85,27 @@ describe('Configure AWS Credentials', () => {
8285
}
8386
});
8487

88+
aws.config.getCredentials.mockReset();
89+
aws.config.getCredentials
90+
.mockImplementationOnce(callback => {
91+
if (!aws.config.credentials) {
92+
aws.config.credentials = {
93+
accessKeyId: FAKE_ACCESS_KEY_ID,
94+
secretAccessKey: FAKE_SECRET_ACCESS_KEY
95+
}
96+
}
97+
callback(null);
98+
})
99+
.mockImplementationOnce(callback => {
100+
if (!aws.config.credentials) {
101+
aws.config.credentials = {
102+
accessKeyId: FAKE_STS_ACCESS_KEY_ID,
103+
secretAccessKey: FAKE_STS_SECRET_ACCESS_KEY
104+
}
105+
}
106+
callback(null);
107+
});
108+
85109
mockStsAssumeRole.mockImplementation(() => {
86110
return {
87111
promise() {
@@ -134,6 +158,59 @@ describe('Configure AWS Credentials', () => {
134158
expect(core.setSecret).toHaveBeenCalledWith(FAKE_ACCOUNT_ID);
135159
});
136160

161+
test('action with no accessible credentials fails', async () => {
162+
process.env.SHOW_STACK_TRACE = 'false';
163+
const mockInputs = {'aws-region': FAKE_REGION};
164+
core.getInput = jest
165+
.fn()
166+
.mockImplementation(mockGetInput(mockInputs));
167+
aws.config.getCredentials.mockReset();
168+
aws.config.getCredentials.mockImplementation(callback => {
169+
callback(new Error('No credentials to load'));
170+
});
171+
172+
await run();
173+
174+
expect(core.setFailed).toHaveBeenCalledWith("Credentials could not be loaded, please check your action inputs: No credentials to load");
175+
});
176+
177+
test('action with empty credentials fails', async () => {
178+
process.env.SHOW_STACK_TRACE = 'false';
179+
const mockInputs = {'aws-region': FAKE_REGION};
180+
core.getInput = jest
181+
.fn()
182+
.mockImplementation(mockGetInput(mockInputs));
183+
aws.config.getCredentials.mockReset();
184+
aws.config.getCredentials.mockImplementation(callback => {
185+
aws.config.credentials = {
186+
accessKeyId: ''
187+
}
188+
callback(null);
189+
});
190+
191+
await run();
192+
193+
expect(core.setFailed).toHaveBeenCalledWith("Credentials could not be loaded, please check your action inputs: Access key ID empty after loading credentials");
194+
});
195+
196+
test('action fails when credentials are not set in the SDK correctly', async () => {
197+
process.env.SHOW_STACK_TRACE = 'false';
198+
core.getInput = jest
199+
.fn()
200+
.mockImplementation(mockGetInput(ASSUME_ROLE_INPUTS));
201+
aws.config.getCredentials.mockReset();
202+
aws.config.getCredentials.mockImplementation(callback => {
203+
aws.config.credentials = {
204+
accessKeyId: FAKE_ACCESS_KEY_ID
205+
}
206+
callback(null);
207+
});
208+
209+
await run();
210+
211+
expect(core.setFailed).toHaveBeenCalledWith("Unexpected failure: Credentials loaded by the SDK do not match the access key ID configured by the action");
212+
});
213+
137214
test('session token is optional', async () => {
138215
const mockInputs = {...CREDS_INPUTS, 'aws-region': 'eu-west-1'};
139216
core.getInput = jest
@@ -154,12 +231,19 @@ describe('Configure AWS Credentials', () => {
154231
expect(core.setSecret).toHaveBeenCalledWith(FAKE_ACCOUNT_ID);
155232
});
156233

157-
test('session token is cleared if necessary', async () => {
234+
test('existing env var creds are cleared', async () => {
158235
const mockInputs = {...CREDS_INPUTS, 'aws-region': 'eu-west-1'};
159236
core.getInput = jest
160237
.fn()
161238
.mockImplementation(mockGetInput(mockInputs));
239+
process.env.AWS_ACCESS_KEY_ID = 'foo';
240+
process.env.AWS_SECRET_ACCESS_KEY = 'bar';
162241
process.env.AWS_SESSION_TOKEN = 'helloworld';
242+
aws.config.credentials = {
243+
accessKeyId: 'foo',
244+
secretAccessKey: 'bar',
245+
sessionToken: 'helloworld'
246+
};
163247

164248
await run();
165249
expect(mockStsAssumeRole).toHaveBeenCalledTimes(0);
@@ -174,6 +258,9 @@ describe('Configure AWS Credentials', () => {
174258
expect(core.exportVariable).toHaveBeenCalledWith('AWS_REGION', 'eu-west-1');
175259
expect(core.setOutput).toHaveBeenCalledWith('aws-account-id', FAKE_ACCOUNT_ID);
176260
expect(core.setSecret).toHaveBeenCalledWith(FAKE_ACCOUNT_ID);
261+
expect(aws.config.credentials.accessKeyId).toBe(FAKE_ACCESS_KEY_ID);
262+
expect(aws.config.credentials.secretAccessKey).toBe(FAKE_SECRET_ACCESS_KEY);
263+
expect(aws.config.credentials.sessionToken).toBeUndefined();
177264
});
178265

179266
test('validates region name', async () => {

0 commit comments

Comments
 (0)
Please sign in to comment.