Skip to content

Commit

Permalink
Add allowed-ids-list, tests, and modify action.yml and README
Browse files Browse the repository at this point in the history
  • Loading branch information
lehmanmj committed Jan 24, 2025
1 parent f171d5c commit db0087a
Show file tree
Hide file tree
Showing 4 changed files with 63 additions and 1 deletion.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,7 @@ See [action.yml](./action.yml) for more detail.
| disable-retry | Disabled retry/backoff logic for assume role calls. By default, retries are enabled. | No |
| retry-max-attempts | Limits the number of retry attempts before giving up. Defaults to 12. | No |
| special-characters-workaround | Uncommonly, some environments cannot tolerate special characters in a secret key. This option will retry fetching credentials until the secret access key does not contain special characters. This option overrides disable-retry and retry-max-attempts. | No |
| allowed-account-ids | You may define a comma-separated list of allowed account IDs to configure credentials for. This is to prevent accidentally deploying to the wrong environment. | No |

#### Credential Lifetime
The default session duration is **1 hour**.
Expand Down
3 changes: 3 additions & 0 deletions action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,9 @@ inputs:
special-characters-workaround:
description: Some environments do not support special characters in AWS_SECRET_ACCESS_KEY. This option will retry fetching credentials until the secret access key does not contain special characters. This option overrides disable-retry and retry-max-attempts. This option is disabled by default
required: false
allowed-account-ids:
description: Comma-separated list of allowed AWS account IDs to prevent accidentally deploying to the wrong environment.
required: false
outputs:
aws-account-id:
description: The AWS account ID for the provided credentials
Expand Down
27 changes: 26 additions & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import {
const DEFAULT_ROLE_DURATION = 3600; // One hour (seconds)
const ROLE_SESSION_NAME = 'GitHubActions';
const REGION_REGEX = /^[a-z0-9-]+$/g;
const ACCOUNT_ID_LIST_REGEX = /^\d{12}(,\s?\d{12})*$/;

export async function run() {
try {
Expand Down Expand Up @@ -60,6 +61,7 @@ export async function run() {
const specialCharacterWorkaroundInput =
core.getInput('special-characters-workaround', { required: false }) || 'false';
const specialCharacterWorkaround = specialCharacterWorkaroundInput.toLowerCase() === 'true';
const allowedAccountIds = core.getInput('allowed-account-ids', { required: false });
let maxRetries = Number.parseInt(core.getInput('retry-max-attempts', { required: false })) || 12;
switch (true) {
case specialCharacterWorkaround:
Expand Down Expand Up @@ -111,9 +113,18 @@ export async function run() {
}
exportRegion(region);

//validate the provided allowed account ID list
if (allowedAccountIds && !allowedAccountIds.match(ACCOUNT_ID_LIST_REGEX)) {
let errorMessage = 'Allowed account ID list is not valid; it must be a comma-separated list of 12-digit IDs';
if (!maskAccountId) {
errorMessage += `: ${allowedAccountIds}`;
}
throw new Error(errorMessage);
}

// Instantiate credentials client
const credentialsClient = new CredentialsClient({ region, proxyServer });
let sourceAccountId: string;
let sourceAccountId: string | undefined = undefined;
let webIdentityToken: string;

// If OIDC is being used, generate token
Expand Down Expand Up @@ -190,6 +201,20 @@ export async function run() {
} else {
core.info('Proceeding with IAM user credentials');
}

if (allowedAccountIds) {
const accountIdList = allowedAccountIds.split(',').map((id) => id.trim());
if (sourceAccountId === undefined) {
sourceAccountId = await exportAccountId(credentialsClient, maskAccountId);
}
if (!accountIdList.includes(sourceAccountId)) {
let errorMessage = "Account ID of the provided credentials is not in 'allowed-account-ids'";
if (!maskAccountId) {
errorMessage += `: ${sourceAccountId}`;
}
throw new Error(errorMessage);
}
}
} catch (error) {
core.setFailed(errorMessage(error));

Expand Down
33 changes: 33 additions & 0 deletions test/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -300,4 +300,37 @@ describe('Configure AWS Credentials', {}, () => {
expect(core.setFailed).toHaveBeenCalled();
});
});

describe('Allowed-id-list', {}, () => {
it('denies accounts not defined in allow-account-ids', async () => {
vi.spyOn(core, 'getInput').mockImplementation(mocks.getInput({ ...mocks.IAM_USER_INPUTS, 'allowed-account-ids': '000000000000'}));
mockedSTSClient.on(GetCallerIdentityCommand).resolvesOnce({ ...mocks.outputs.GET_CALLER_IDENTITY });
// biome-ignore lint/suspicious/noExplicitAny: any required to mock private method
vi.spyOn(CredentialsClient.prototype as any, 'loadCredentials').mockResolvedValueOnce({
accessKeyId: 'MYAWSACCESSKEYID',
});
await run();
expect(core.setFailed).toHaveBeenCalledWith(`Account ID of the provided credentials is not in 'allowed-account-ids': 111111111111`);
})
it('allows accounts defined in allow-account-ids', async () => {
vi.spyOn(core, 'getInput').mockImplementation(mocks.getInput({ ...mocks.IAM_USER_INPUTS, 'allowed-account-ids': '111111111111'}));
mockedSTSClient.on(GetCallerIdentityCommand).resolvesOnce({ ...mocks.outputs.GET_CALLER_IDENTITY });
// biome-ignore lint/suspicious/noExplicitAny: any required to mock private method
vi.spyOn(CredentialsClient.prototype as any, 'loadCredentials').mockResolvedValueOnce({
accessKeyId: 'MYAWSACCESSKEYID',
});
await run();
})
it('throws if account id list is invalid', async () => {
vi.spyOn(core, 'getInput').mockImplementation(mocks.getInput({ ...mocks.IAM_USER_INPUTS, 'allowed-account-ids': '0'}));
mockedSTSClient.on(GetCallerIdentityCommand).resolvesOnce({ ...mocks.outputs.GET_CALLER_IDENTITY });
// biome-ignore lint/suspicious/noExplicitAny: any required to mock private method
vi.spyOn(CredentialsClient.prototype as any, 'loadCredentials').mockResolvedValueOnce({
accessKeyId: 'MYAWSACCESSKEYID',
});
await run();
expect(core.setFailed).toHaveBeenCalledWith('Allowed account ID list is not valid; it must be a comma-separated list of 12-digit IDs: 0');
})
});

});

0 comments on commit db0087a

Please sign in to comment.