mirror of
https://github.com/aws-actions/configure-aws-credentials.git
synced 2026-08-31 05:35:04 +09:00
4d281fbc56
* fix: enforce allowed-account-ids on all auth paths The allowed-account-ids list was only enforced in some auth flows. This was due to the check being included in validateCredentials, which was skipped if (GITHUB_ACTIONS && AccessKeyId && output-env-credentials) -> false. This unifies credential validation into a single path. - validateCredentials(credentials?, ...) resolves credentials, proves liveness via one GetCallerIdentity call, and returns the identity. - validateAccountId(expectedAccountIds, account) is now a pure comparison against the resolved account, enforced against the final (assumed) account independent of auth method, GITHUB_ACTIONS, or output-env-credentials. - exportAccountId(identity, ...) consumes the resolved identity instead of making its own GetCallerIdentity call, so credential resolution happens exactly once per credential set. Pre-assume account checks remain gated on !roleToAssume so cross-account assume-role (source account differs from the role's target) is preserved. Adds regression tests for the OIDC wrong-account case (the previously missing negative test), OIDC with output-env-credentials: false, and the assume-role wrong-account case. * chore: move validateAccountId into helpers
114 lines
4.2 KiB
TypeScript
114 lines
4.2 KiB
TypeScript
import { info } from '@actions/core';
|
|
import { STSClient } from '@aws-sdk/client-sts';
|
|
import type { AwsCredentialIdentity } from '@aws-sdk/types';
|
|
import { NodeHttpHandler } from '@smithy/node-http-handler';
|
|
import { ProxyAgent } from 'proxy-agent';
|
|
import { buildCustomUserAgent, errorMessage, getCallerIdentity } from './helpers';
|
|
import { ProxyResolver } from './ProxyResolver';
|
|
|
|
if (!process.env.AWS_EXECUTION_ENV) {
|
|
process.env.AWS_EXECUTION_ENV = 'GitHubActions';
|
|
}
|
|
|
|
export interface CredentialsClientProps {
|
|
region?: string;
|
|
proxyServer?: string;
|
|
noProxy?: string;
|
|
stsEndpoint?: string;
|
|
roleChaining: boolean;
|
|
}
|
|
|
|
export class CredentialsClient {
|
|
public region?: string;
|
|
private _stsClient?: STSClient;
|
|
private readonly requestHandler?: NodeHttpHandler;
|
|
private readonly stsEndpoint?: string;
|
|
private roleChaining?: boolean;
|
|
|
|
constructor(props: CredentialsClientProps) {
|
|
if (props.region !== undefined) {
|
|
this.region = props.region;
|
|
}
|
|
if (props.proxyServer) {
|
|
info('Configuring proxy handler for STS client');
|
|
const proxyOptions: { httpProxy: string; httpsProxy: string; noProxy?: string } = {
|
|
httpProxy: props.proxyServer,
|
|
httpsProxy: props.proxyServer,
|
|
};
|
|
if (props.noProxy !== undefined) {
|
|
proxyOptions.noProxy = props.noProxy;
|
|
}
|
|
const getProxyForUrl = new ProxyResolver(proxyOptions).getProxyForUrl;
|
|
const handler = new ProxyAgent({ getProxyForUrl });
|
|
this.requestHandler = new NodeHttpHandler({
|
|
httpsAgent: handler,
|
|
httpAgent: handler,
|
|
});
|
|
}
|
|
if (props.stsEndpoint) {
|
|
this.stsEndpoint = props.stsEndpoint;
|
|
}
|
|
this.roleChaining = props.roleChaining;
|
|
}
|
|
|
|
public get stsClient(): STSClient {
|
|
if (!this._stsClient || this.roleChaining) {
|
|
this._stsClient = this.createStsClient();
|
|
}
|
|
return this._stsClient;
|
|
}
|
|
|
|
// Builds an STS client using the action's configured region/endpoint/proxy. When explicit credentials are provided,
|
|
// the client uses them directly instead of the SDK default credential provider chain.
|
|
// This matters for validateAccountId.
|
|
private createStsClient(credentials?: AwsCredentialIdentity): STSClient {
|
|
return new STSClient({
|
|
customUserAgent: buildCustomUserAgent(),
|
|
...(this.region !== undefined && { region: this.region }),
|
|
...(this.stsEndpoint !== undefined && { endpoint: this.stsEndpoint }),
|
|
...(this.requestHandler !== undefined && { requestHandler: this.requestHandler }),
|
|
...(credentials !== undefined && { credentials }),
|
|
});
|
|
}
|
|
|
|
// Validates that the credentials the action will hand to subsequent steps actually work, and returns the resolved
|
|
// caller identity (account + ARN). "Work" is proven by a sts:GetCallerIdentity call, which both confirms the
|
|
// credentials are accepted by AWS and returns the identity for later checks and outputs to use.
|
|
public async validateCredentials(
|
|
credentials?: AwsCredentialIdentity,
|
|
expectedAccessKeyId?: string,
|
|
roleChaining?: boolean,
|
|
): Promise<Awaited<ReturnType<typeof getCallerIdentity>>> {
|
|
if (!credentials) {
|
|
let resolved: AwsCredentialIdentity;
|
|
try {
|
|
resolved = await this.loadCredentials();
|
|
if (!resolved.accessKeyId) {
|
|
throw new Error('Access key ID empty after loading credentials');
|
|
}
|
|
} catch (error) {
|
|
throw new Error(`Credentials could not be loaded, please check your action inputs: ${errorMessage(error)}`);
|
|
}
|
|
if (!roleChaining && expectedAccessKeyId && expectedAccessKeyId !== resolved.accessKeyId) {
|
|
throw new Error(
|
|
'Credentials loaded by the SDK do not match the expected access key ID configured by the action',
|
|
);
|
|
}
|
|
}
|
|
|
|
const client = credentials ? this.createStsClient(credentials) : this.stsClient;
|
|
try {
|
|
return await getCallerIdentity(client);
|
|
} catch (error) {
|
|
throw new Error(`Credentials could not be loaded, please check your action inputs: ${errorMessage(error)}`);
|
|
}
|
|
}
|
|
|
|
private async loadCredentials() {
|
|
const config = {} as { requestHandler?: NodeHttpHandler };
|
|
if (this.requestHandler !== undefined) config.requestHandler = this.requestHandler;
|
|
const client = new STSClient(config);
|
|
return client.config.credentials();
|
|
}
|
|
}
|