mirror of
https://github.com/aws-actions/configure-aws-credentials.git
synced 2026-09-01 05:45:06 +09:00
feat: support account id allowlist (#1456)
* feat: support account id allowlist * chore: update readme --------- Co-authored-by: Michael Lehmann <lehmanmj@amazon.com>
This commit is contained in:
@@ -3,7 +3,7 @@ import { STSClient } from '@aws-sdk/client-sts';
|
||||
import type { AwsCredentialIdentity } from '@aws-sdk/types';
|
||||
import { NodeHttpHandler } from '@smithy/node-http-handler';
|
||||
import { HttpsProxyAgent } from 'https-proxy-agent';
|
||||
import { errorMessage } from './helpers';
|
||||
import { errorMessage, getCallerIdentity } from './helpers';
|
||||
|
||||
const USER_AGENT = 'configure-aws-credentials-for-github-actions';
|
||||
|
||||
@@ -40,7 +40,11 @@ export class CredentialsClient {
|
||||
return this._stsClient;
|
||||
}
|
||||
|
||||
public async validateCredentials(expectedAccessKeyId?: string, roleChaining?: boolean) {
|
||||
public async validateCredentials(
|
||||
expectedAccessKeyId?: string,
|
||||
roleChaining?: boolean,
|
||||
expectedAccountIds?: string[],
|
||||
) {
|
||||
let credentials: AwsCredentialIdentity;
|
||||
try {
|
||||
credentials = await this.loadCredentials();
|
||||
@@ -50,13 +54,27 @@ export class CredentialsClient {
|
||||
} catch (error) {
|
||||
throw new Error(`Credentials could not be loaded, please check your action inputs: ${errorMessage(error)}`);
|
||||
}
|
||||
if (expectedAccountIds && expectedAccountIds.length > 0 && expectedAccountIds[0] !== '') {
|
||||
let callerIdentity: Awaited<ReturnType<typeof getCallerIdentity>>;
|
||||
try {
|
||||
callerIdentity = await getCallerIdentity(this.stsClient);
|
||||
} catch (error) {
|
||||
throw new Error(`Could not validate account ID of credentials: ${errorMessage(error)}`);
|
||||
}
|
||||
if (!callerIdentity.Account || !expectedAccountIds.includes(callerIdentity.Account)) {
|
||||
throw new Error(
|
||||
`The account ID of the provided credentials (${
|
||||
callerIdentity.Account ?? 'unknown'
|
||||
}) does not match any of the expected account IDs: ${expectedAccountIds.join(', ')}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (!roleChaining) {
|
||||
const actualAccessKeyId = credentials.accessKeyId;
|
||||
|
||||
if (expectedAccessKeyId && expectedAccessKeyId !== actualAccessKeyId) {
|
||||
throw new Error(
|
||||
'Unexpected failure: Credentials loaded by the SDK do not match the access key ID configured by the action',
|
||||
'Credentials loaded by the SDK do not match the expected access key ID configured by the action',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
+11
-7
@@ -1,5 +1,5 @@
|
||||
import * as core from '@actions/core';
|
||||
import type { Credentials } from '@aws-sdk/client-sts';
|
||||
import type { Credentials, STSClient } from '@aws-sdk/client-sts';
|
||||
import { GetCallerIdentityCommand } from '@aws-sdk/client-sts';
|
||||
import type { CredentialsClient } from './CredentialsClient';
|
||||
|
||||
@@ -109,15 +109,19 @@ export function exportRegion(region: string, outputEnvCredentials?: boolean) {
|
||||
}
|
||||
}
|
||||
|
||||
// Obtains account ID from STS Client and sets it as output
|
||||
export async function exportAccountId(credentialsClient: CredentialsClient, maskAccountId?: boolean) {
|
||||
const client = credentialsClient.stsClient;
|
||||
export async function getCallerIdentity(client: STSClient): Promise<{ Account: string; Arn: string; UserId?: string }> {
|
||||
const identity = await client.send(new GetCallerIdentityCommand({}));
|
||||
const accountId = identity.Account;
|
||||
const arn = identity.Arn;
|
||||
if (!accountId || !arn) {
|
||||
if (!identity.Account || !identity.Arn) {
|
||||
throw new Error('Could not get Account ID or ARN from STS. Did you set credentials?');
|
||||
}
|
||||
return { Account: identity.Account, Arn: identity.Arn, UserId: identity.UserId };
|
||||
}
|
||||
|
||||
// Obtains account ID from STS Client and sets it as output
|
||||
export async function exportAccountId(credentialsClient: CredentialsClient, maskAccountId?: boolean) {
|
||||
const identity = await getCallerIdentity(credentialsClient.stsClient);
|
||||
const accountId = identity.Account;
|
||||
const arn = identity.Arn;
|
||||
if (maskAccountId) {
|
||||
core.setSecret(accountId);
|
||||
core.setSecret(arn);
|
||||
|
||||
+11
-3
@@ -52,6 +52,10 @@ export async function run() {
|
||||
const specialCharacterWorkaround = getBooleanInput('special-characters-workaround', { required: false });
|
||||
const useExistingCredentials = core.getInput('use-existing-credentials', { required: false });
|
||||
let maxRetries = Number.parseInt(core.getInput('retry-max-attempts', { required: false })) || 12;
|
||||
const expectedAccountIds = core
|
||||
.getInput('allowed-account-ids', { required: false })
|
||||
.split(',')
|
||||
.map((s) => s.trim());
|
||||
const forceSkipOidc = getBooleanInput('force-skip-oidc', { required: false });
|
||||
|
||||
if (forceSkipOidc && roleToAssume && !AccessKeyId && !webIdentityTokenFile) {
|
||||
@@ -145,7 +149,7 @@ export async function run() {
|
||||
exportCredentials({ AccessKeyId, SecretAccessKey, SessionToken }, outputCredentials, outputEnvCredentials);
|
||||
} else if (!webIdentityTokenFile && !roleChaining) {
|
||||
// Proceed only if credentials can be picked up
|
||||
await credentialsClient.validateCredentials();
|
||||
await credentialsClient.validateCredentials(undefined, roleChaining, expectedAccountIds);
|
||||
sourceAccountId = await exportAccountId(credentialsClient, maskAccountId);
|
||||
}
|
||||
|
||||
@@ -153,7 +157,7 @@ export async function run() {
|
||||
// Validate that the SDK can actually pick up credentials.
|
||||
// This validates cases where this action is using existing environment credentials,
|
||||
// and cases where the user intended to provide input credentials but the secrets inputs resolved to empty strings.
|
||||
await credentialsClient.validateCredentials(AccessKeyId, roleChaining);
|
||||
await credentialsClient.validateCredentials(AccessKeyId, roleChaining, expectedAccountIds);
|
||||
sourceAccountId = await exportAccountId(credentialsClient, maskAccountId);
|
||||
}
|
||||
|
||||
@@ -189,7 +193,11 @@ export async function run() {
|
||||
// is set to `true` then we are NOT in a self-hosted runner.
|
||||
// Second: Customer provided credentials manually (IAM User keys stored in GH Secrets)
|
||||
if (!process.env.GITHUB_ACTIONS || AccessKeyId) {
|
||||
await credentialsClient.validateCredentials(roleCredentials.Credentials?.AccessKeyId);
|
||||
await credentialsClient.validateCredentials(
|
||||
roleCredentials.Credentials?.AccessKeyId,
|
||||
roleChaining,
|
||||
expectedAccountIds,
|
||||
);
|
||||
}
|
||||
if (outputEnvCredentials) {
|
||||
await exportAccountId(credentialsClient, maskAccountId);
|
||||
|
||||
Reference in New Issue
Block a user