Skip to content
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

fix: mask both source and role credentials #40

Merged
merged 1 commit into from
Mar 5, 2020
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
57 changes: 34 additions & 23 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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."
);

Expand All @@ -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 = {
Expand Down Expand Up @@ -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() {
Expand All @@ -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);
Expand Down
59 changes: 41 additions & 18 deletions index.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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();
});
Expand All @@ -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();
});
Expand All @@ -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 () => {
Expand Down Expand Up @@ -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: [
Expand Down