Skip to content

fix: mask both source and role credentials #40

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Mar 5, 2020
Merged
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
57 changes: 34 additions & 23 deletions index.js
Original file line number Diff line number Diff line change
@@ -16,17 +16,15 @@ async function assumeRole(params) {
const isDefined = i => !!i;

const {
sourceAccountId,
roleToAssume,
roleExternalId,
roleDurationSeconds,
roleSessionName,
accessKeyId,
secretAccessKey,
sessionToken,
region,
} = params;
assert(
[roleToAssume, roleDurationSeconds, roleSessionName, accessKeyId, secretAccessKey, region].every(isDefined),
[sourceAccountId, roleToAssume, roleDurationSeconds, roleSessionName, region].every(isDefined),
"Missing required input when assuming a Role."
);

@@ -36,18 +34,12 @@ async function assumeRole(params) {
'Missing required environment value. Are you running in GitHub Actions?'
);

const endpoint = util.format('https://sts.%s.amazonaws.com', region);

const sts = new aws.STS({
accessKeyId, secretAccessKey, sessionToken, region, endpoint, customUserAgent: USER_AGENT
});
const sts = getStsClient(region);

let roleArn = roleToAssume;
if (!roleArn.startsWith('arn:aws')) {
const identity = await sts.getCallerIdentity().promise();
const accountId = identity.Account;
// Supports only 'aws' partition. Customers in other partitions ('aws-cn') will need to provide full ARN
roleArn = `arn:aws:iam::${accountId}:role/${roleArn}`;
roleArn = `arn:aws:iam::${sourceAccountId}:role/${roleArn}`;
}

const assumeRoleRequest = {
@@ -125,15 +117,25 @@ function exportRegion(region) {
core.exportVariable('AWS_REGION', region);
}

async function exportAccountId(maskAccountId) {
async function exportAccountId(maskAccountId, region) {
// Get the AWS account ID
const sts = new aws.STS({customUserAgent: USER_AGENT});
const sts = getStsClient(region);
const identity = await sts.getCallerIdentity().promise();
const accountId = identity.Account;
core.setOutput('aws-account-id', accountId);
if (!maskAccountId || maskAccountId.toLowerCase() == 'true') {
core.setSecret(accountId);
}
return accountId;
}

function getStsClient(region) {
const endpoint = util.format('https://sts.%s.amazonaws.com', region);
return new aws.STS({
region,
endpoint,
customUserAgent: USER_AGENT
});
}

async function run() {
@@ -149,19 +151,28 @@ async function run() {
const roleDurationSeconds = core.getInput('role-duration-seconds', {required: false}) || MAX_ACTION_RUNTIME;
const roleSessionName = core.getInput('role-session-name', { required: false }) || ROLE_SESSION_NAME;

// Always export the source credentials and account ID.
// The STS client for calling AssumeRole pulls creds from the environment.
// Plus, in the assume role case, if the AssumeRole call fails, we want
// the source credentials and accound ID to already be masked as secrets
// in any error messages.
exportRegion(region);
exportCredentials({accessKeyId, secretAccessKey, sessionToken});
const sourceAccountId = await exportAccountId(maskAccountId, region);

// Get role credentials if configured to do so
if (roleToAssume) {
const roleCredentials = await assumeRole(
{accessKeyId, secretAccessKey, sessionToken, region, roleToAssume, roleExternalId, roleDurationSeconds, roleSessionName}
);
const roleCredentials = await assumeRole({
sourceAccountId,
region,
roleToAssume,
roleExternalId,
roleDurationSeconds,
roleSessionName
});
exportCredentials(roleCredentials);
} else {
exportCredentials({accessKeyId, secretAccessKey, sessionToken});
await exportAccountId(maskAccountId, region);
}

exportRegion(region);

await exportAccountId(maskAccountId);
}
catch (error) {
core.setFailed(error.message);
59 changes: 41 additions & 18 deletions index.test.js
Original file line number Diff line number Diff line change
@@ -13,8 +13,9 @@ const FAKE_STS_SECRET_ACCESS_KEY = 'STS-AWS-SECRET-ACCESS-KEY';
const FAKE_STS_SESSION_TOKEN = 'STS-AWS-SESSION-TOKEN';
const FAKE_REGION = 'fake-region-1';
const FAKE_ACCOUNT_ID = '123456789012';
const FAKE_ROLE_ACCOUNT_ID = '111111111111';
const ROLE_NAME = 'MY-ROLE';
const ROLE_ARN = 'arn:aws:iam::123456789012:role/MY-ROLE';
const ROLE_ARN = 'arn:aws:iam::111111111111:role/MY-ROLE';
const ENVIRONMENT_VARIABLE_OVERRIDES = {
SHOW_STACK_TRACE: 'true',
GITHUB_REPOSITORY: 'MY-REPOSITORY-NAME',
@@ -68,13 +69,18 @@ describe('Configure AWS Credentials', () => {
.fn()
.mockImplementation(mockGetInput(DEFAULT_INPUTS));

mockStsCallerIdentity.mockImplementation(() => {
return {
mockStsCallerIdentity.mockReset();
mockStsCallerIdentity
.mockReturnValueOnce({
promise() {
return Promise.resolve({ Account: FAKE_ACCOUNT_ID });
}
};
});
})
.mockReturnValueOnce({
promise() {
return Promise.resolve({ Account: FAKE_ROLE_ACCOUNT_ID });
}
});

mockStsAssumeRole.mockImplementation(() => {
return {
@@ -154,6 +160,7 @@ describe('Configure AWS Credentials', () => {
test('error is caught by core.setFailed and caught', async () => {
process.env.SHOW_STACK_TRACE = 'false';

mockStsCallerIdentity.mockReset();
mockStsCallerIdentity.mockImplementation(() => {
throw new Error();
});
@@ -165,6 +172,7 @@ describe('Configure AWS Credentials', () => {

test('error is caught by core.setFailed and passed', async () => {

mockStsCallerIdentity.mockReset();
mockStsCallerIdentity.mockImplementation(() => {
throw new Error();
});
@@ -181,18 +189,33 @@ describe('Configure AWS Credentials', () => {

await run();
expect(mockStsAssumeRole).toHaveBeenCalledTimes(1);
expect(core.exportVariable).toHaveBeenCalledTimes(5);
expect(core.setSecret).toHaveBeenCalledTimes(4);
expect(core.exportVariable).toHaveBeenCalledWith('AWS_ACCESS_KEY_ID', FAKE_STS_ACCESS_KEY_ID);
expect(core.setSecret).toHaveBeenCalledWith(FAKE_STS_ACCESS_KEY_ID);
expect(core.exportVariable).toHaveBeenCalledWith('AWS_SECRET_ACCESS_KEY', FAKE_STS_SECRET_ACCESS_KEY);
expect(core.setSecret).toHaveBeenCalledWith(FAKE_STS_SECRET_ACCESS_KEY);
expect(core.exportVariable).toHaveBeenCalledWith('AWS_SESSION_TOKEN', FAKE_STS_SESSION_TOKEN);
expect(core.setSecret).toHaveBeenCalledWith(FAKE_STS_SESSION_TOKEN);
expect(core.exportVariable).toHaveBeenCalledWith('AWS_DEFAULT_REGION', FAKE_REGION);
expect(core.exportVariable).toHaveBeenCalledWith('AWS_REGION', FAKE_REGION);
expect(core.setOutput).toHaveBeenCalledWith('aws-account-id', FAKE_ACCOUNT_ID);
expect(core.setSecret).toHaveBeenCalledWith(FAKE_ACCOUNT_ID);
expect(core.exportVariable).toHaveBeenCalledTimes(7);
expect(core.setSecret).toHaveBeenCalledTimes(7);
expect(core.setOutput).toHaveBeenCalledTimes(2);

// first the source credentials are exported and masked
expect(core.setSecret).toHaveBeenNthCalledWith(1, FAKE_ACCESS_KEY_ID);
expect(core.setSecret).toHaveBeenNthCalledWith(2, FAKE_SECRET_ACCESS_KEY);
expect(core.setSecret).toHaveBeenNthCalledWith(3, FAKE_ACCOUNT_ID);

expect(core.exportVariable).toHaveBeenNthCalledWith(1, 'AWS_DEFAULT_REGION', FAKE_REGION);
expect(core.exportVariable).toHaveBeenNthCalledWith(2, 'AWS_REGION', FAKE_REGION);
expect(core.exportVariable).toHaveBeenNthCalledWith(3, 'AWS_ACCESS_KEY_ID', FAKE_ACCESS_KEY_ID);
expect(core.exportVariable).toHaveBeenNthCalledWith(4, 'AWS_SECRET_ACCESS_KEY', FAKE_SECRET_ACCESS_KEY);

expect(core.setOutput).toHaveBeenNthCalledWith(1, 'aws-account-id', FAKE_ACCOUNT_ID);

// then the role credentials are exported and masked
expect(core.setSecret).toHaveBeenNthCalledWith(4, FAKE_STS_ACCESS_KEY_ID);
expect(core.setSecret).toHaveBeenNthCalledWith(5, FAKE_STS_SECRET_ACCESS_KEY);
expect(core.setSecret).toHaveBeenNthCalledWith(6, FAKE_STS_SESSION_TOKEN);
expect(core.setSecret).toHaveBeenNthCalledWith(7, FAKE_ROLE_ACCOUNT_ID);

expect(core.exportVariable).toHaveBeenNthCalledWith(5, 'AWS_ACCESS_KEY_ID', FAKE_STS_ACCESS_KEY_ID);
expect(core.exportVariable).toHaveBeenNthCalledWith(6, 'AWS_SECRET_ACCESS_KEY', FAKE_STS_SECRET_ACCESS_KEY);
expect(core.exportVariable).toHaveBeenNthCalledWith(7, 'AWS_SESSION_TOKEN', FAKE_STS_SESSION_TOKEN);

expect(core.setOutput).toHaveBeenNthCalledWith(2, 'aws-account-id', FAKE_ROLE_ACCOUNT_ID);
});

test('role assumption tags', async () => {
@@ -268,7 +291,7 @@ describe('Configure AWS Credentials', () => {

await run();
expect(mockStsAssumeRole).toHaveBeenCalledWith({
RoleArn: ROLE_ARN,
RoleArn: 'arn:aws:iam::123456789012:role/MY-ROLE',
RoleSessionName: 'GitHubActions',
DurationSeconds: 6 * 3600,
Tags: [