1
0
Fork 0
mirror of synced 2026-06-05 14:58:19 +00:00

feat: role-chaining

This commit is contained in:
peterwoodworth 2023-06-21 16:45:34 -07:00
commit d26f2d03f8
No known key found for this signature in database
GPG key ID: 01931412FD685922
8 changed files with 66 additions and 69 deletions

View file

@ -9,15 +9,14 @@ branding:
color: orange color: orange
icon: cloud icon: cloud
inputs: inputs:
audience:
description: The audience to use for the OIDC provider
required: false
default: sts.amazonaws.com
aws-access-key-id:
description: AWS Access Key ID. This input is required if running in the GitHub hosted environment. It is optional if running in a self-hosted environment that already has AWS credentials, for example on an EC2 instance.
required: false
aws-region: aws-region:
description: AWS Region, e.g. us-east-2 description: AWS Region, e.g. us-east-2
required: true
role-to-assume:
description: The Amazon Resource Name (ARN) of the role to assume. Use the provided credentials to assume an IAM role and configure the Actions environment with the assumed role credentials rather than with the provided credentials.
required: false
aws-access-key-id:
description: AWS Access Key ID. This input is required if running in the GitHub hosted environment. It is optional if running in a self-hosted environment that already has AWS credentials, for example on an EC2 instance.
required: false required: false
aws-secret-access-key: aws-secret-access-key:
description: AWS Access Key ID. This input is required if running in the GitHub hosted environment. It is optional if running in a self-hosted environment that already has AWS credentials, for example on an EC2 instance. description: AWS Access Key ID. This input is required if running in the GitHub hosted environment. It is optional if running in a self-hosted environment that already has AWS credentials, for example on an EC2 instance.
@ -25,6 +24,16 @@ inputs:
aws-session-token: aws-session-token:
description: AWS Session Token description: AWS Session Token
required: false required: false
web-identity-token-file:
description: Use the web identity token file from the provided file system path in order to assume an IAM role using a web identity, e.g. from within an Amazon EKS worker node.
required: false
role-chaining:
description: 'Use existing credentials from the environment to assume a new role'
required: false
audience:
description: The audience to use for the OIDC provider
required: false
default: sts.amazonaws.com
disable-oidc: disable-oidc:
description: Strictly disable action from attempting to fetch credentials with OIDC description: Strictly disable action from attempting to fetch credentials with OIDC
required: false required: false
@ -46,12 +55,6 @@ inputs:
role-skip-session-tagging: role-skip-session-tagging:
description: Skip session tagging during role assumption description: Skip session tagging during role assumption
required: false required: false
role-to-assume:
description: The Amazon Resource Name (ARN) of the role to assume. Use the provided credentials to assume an IAM role and configure the Actions environment with the assumed role credentials rather than with the provided credentials.
required: false
web-identity-token-file:
description: Use the web identity token file from the provided file system path in order to assume an IAM role using a web identity, e.g. from within an Amazon EKS worker node.
required: false
inline-session-policy: inline-session-policy:
description: 'Inline session policy' description: 'Inline session policy'
required: false required: false

2
dist/cleanup/index.js generated vendored
View file

@ -17616,7 +17616,7 @@ function exportRegion(region) {
exports.exportRegion = exportRegion; exports.exportRegion = exportRegion;
// Obtains account ID from STS Client and sets it as output // Obtains account ID from STS Client and sets it as output
async function exportAccountId(credentialsClient, maskAccountId) { async function exportAccountId(credentialsClient, maskAccountId) {
const client = credentialsClient.getStsClient(); const client = credentialsClient.stsClient;
const identity = await client.send(new client_sts_1.GetCallerIdentityCommand({})); const identity = await client.send(new client_sts_1.GetCallerIdentityCommand({}));
const accountId = identity.Account; const accountId = identity.Account;
if (!accountId) { if (!accountId) {

View file

@ -5,10 +5,10 @@ export interface CredentialsClientProps {
} }
export declare class CredentialsClient { export declare class CredentialsClient {
region?: string; region?: string;
private stsClient?; private _stsClient?;
private readonly requestHandler?; private readonly requestHandler?;
constructor(props: CredentialsClientProps); constructor(props: CredentialsClientProps);
getStsClient(): STSClient; get stsClient(): STSClient;
validateCredentials(expectedAccessKeyId?: string): Promise<void>; validateCredentials(expectedAccessKeyId?: string, roleChaining?: boolean): Promise<void>;
private loadCredentials; private loadCredentials;
} }

48
dist/index.js generated vendored
View file

@ -16,14 +16,9 @@ const helpers_1 = __nccwpck_require__(9787);
const USER_AGENT = 'configure-aws-credentials-for-github-actions'; const USER_AGENT = 'configure-aws-credentials-for-github-actions';
class CredentialsClient { class CredentialsClient {
constructor(props) { constructor(props) {
if (props.region) { this.region = props.region;
this.region = props.region;
}
else {
(0, core_1.info)('No region provided, using global STS endpoint');
}
if (props.proxyServer) { if (props.proxyServer) {
(0, core_1.info)('Configurint proxy handler for STS client'); (0, core_1.info)('Configuring proxy handler for STS client');
const handler = new https_proxy_agent_1.HttpsProxyAgent(props.proxyServer); const handler = new https_proxy_agent_1.HttpsProxyAgent(props.proxyServer);
this.requestHandler = new node_http_handler_1.NodeHttpHandler({ this.requestHandler = new node_http_handler_1.NodeHttpHandler({
httpAgent: handler, httpAgent: handler,
@ -31,18 +26,17 @@ class CredentialsClient {
}); });
} }
} }
getStsClient() { get stsClient() {
if (!this.stsClient) { if (!this._stsClient) {
this.stsClient = new client_sts_1.STSClient({ this._stsClient = new client_sts_1.STSClient({
region: this.region ? this.region : undefined, region: this.region,
customUserAgent: USER_AGENT, customUserAgent: USER_AGENT,
requestHandler: this.requestHandler ? this.requestHandler : undefined, requestHandler: this.requestHandler ? this.requestHandler : undefined,
useGlobalEndpoint: this.region ? false : true,
}); });
} }
return this.stsClient; return this._stsClient;
} }
async validateCredentials(expectedAccessKeyId) { async validateCredentials(expectedAccessKeyId, roleChaining) {
let credentials; let credentials;
try { try {
credentials = await this.loadCredentials(); credentials = await this.loadCredentials();
@ -53,9 +47,11 @@ class CredentialsClient {
catch (error) { catch (error) {
throw new Error(`Credentials could not be loaded, please check your action inputs: ${(0, helpers_1.errorMessage)(error)}`); throw new Error(`Credentials could not be loaded, please check your action inputs: ${(0, helpers_1.errorMessage)(error)}`);
} }
const actualAccessKeyId = credentials.accessKeyId; if (!roleChaining) {
if (expectedAccessKeyId && expectedAccessKeyId !== actualAccessKeyId) { const actualAccessKeyId = credentials.accessKeyId;
throw new Error('Unexpected failure: Credentials loaded by the SDK do not match the access key ID configured by the action'); 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');
}
} }
} }
async loadCredentials() { async loadCredentials() {
@ -197,7 +193,7 @@ async function assumeRole(params) {
const keys = Object.keys(commonAssumeRoleParams); const keys = Object.keys(commonAssumeRoleParams);
keys.forEach((k) => commonAssumeRoleParams[k] === undefined && delete commonAssumeRoleParams[k]); keys.forEach((k) => commonAssumeRoleParams[k] === undefined && delete commonAssumeRoleParams[k]);
// Instantiate STS client // Instantiate STS client
const stsClient = credentialsClient.getStsClient(); const stsClient = credentialsClient.stsClient;
// Assume role using one of three methods // Assume role using one of three methods
switch (true) { switch (true) {
case !!webIdentityToken: { case !!webIdentityToken: {
@ -278,7 +274,7 @@ function exportRegion(region) {
exports.exportRegion = exportRegion; exports.exportRegion = exportRegion;
// Obtains account ID from STS Client and sets it as output // Obtains account ID from STS Client and sets it as output
async function exportAccountId(credentialsClient, maskAccountId) { async function exportAccountId(credentialsClient, maskAccountId) {
const client = credentialsClient.getStsClient(); const client = credentialsClient.stsClient;
const identity = await client.send(new client_sts_1.GetCallerIdentityCommand({})); const identity = await client.send(new client_sts_1.GetCallerIdentityCommand({}));
const accountId = identity.Account; const accountId = identity.Account;
if (!accountId) { if (!accountId) {
@ -390,9 +386,7 @@ async function run() {
const SecretAccessKey = core.getInput('aws-secret-access-key', { required: false }); const SecretAccessKey = core.getInput('aws-secret-access-key', { required: false });
const sessionTokenInput = core.getInput('aws-session-token', { required: false }); const sessionTokenInput = core.getInput('aws-session-token', { required: false });
const SessionToken = sessionTokenInput === '' ? undefined : sessionTokenInput; const SessionToken = sessionTokenInput === '' ? undefined : sessionTokenInput;
const region = core.getInput('aws-region', { required: false }) || const region = core.getInput('aws-region', { required: true });
process.env['AWS_REGION'] ||
process.env['AWS_DEFAULT_REGION'];
const roleToAssume = core.getInput('role-to-assume', { required: false }); const roleToAssume = core.getInput('role-to-assume', { required: false });
const audience = core.getInput('audience', { required: false }); const audience = core.getInput('audience', { required: false });
const maskAccountId = core.getInput('mask-aws-account-id', { required: false }); const maskAccountId = core.getInput('mask-aws-account-id', { required: false });
@ -410,6 +404,8 @@ async function run() {
for (const managedSessionPolicy of managedSessionPoliciesInput) { for (const managedSessionPolicy of managedSessionPoliciesInput) {
managedSessionPolicies.push({ arn: managedSessionPolicy }); managedSessionPolicies.push({ arn: managedSessionPolicy });
} }
const roleChainingInput = core.getInput('role-chaining', { required: false }) || 'false';
const roleChaining = roleChainingInput.toLowerCase() === 'true';
// Logic to decide whether to attempt to use OIDC or not // Logic to decide whether to attempt to use OIDC or not
const useGitHubOIDCProvider = () => { const useGitHubOIDCProvider = () => {
// The `ACTIONS_ID_TOKEN_REQUEST_TOKEN` environment variable is set when the `id-token` permission is granted. // The `ACTIONS_ID_TOKEN_REQUEST_TOKEN` environment variable is set when the `id-token` permission is granted.
@ -420,14 +416,16 @@ async function run() {
!webIdentityTokenFile && !webIdentityTokenFile &&
!AccessKeyId && !AccessKeyId &&
!disableOIDC && !disableOIDC &&
!process.env['ACTIONS_ID_TOKEN_REQUEST_TOKEN']) { !process.env['ACTIONS_ID_TOKEN_REQUEST_TOKEN'] &&
!roleChaining) {
core.info('It looks like you might be trying to authenticate with OIDC. Did you mean to set the `id-token` permission?'); core.info('It looks like you might be trying to authenticate with OIDC. Did you mean to set the `id-token` permission?');
} }
return (!!roleToAssume && return (!!roleToAssume &&
!!process.env['ACTIONS_ID_TOKEN_REQUEST_TOKEN'] && !!process.env['ACTIONS_ID_TOKEN_REQUEST_TOKEN'] &&
!AccessKeyId && !AccessKeyId &&
!webIdentityTokenFile && !webIdentityTokenFile &&
!disableOIDC); !disableOIDC &&
!roleChaining);
}; };
// Validate and export region // Validate and export region
if (region) { if (region) {
@ -463,7 +461,7 @@ async function run() {
// cases where this action is on a self-hosted runner that doesn't have credentials // cases where this action is on a self-hosted runner that doesn't have credentials
// configured correctly, and cases where the user intended to provide input // configured correctly, and cases where the user intended to provide input
// credentials but the secrets inputs resolved to empty strings. // credentials but the secrets inputs resolved to empty strings.
await credentialsClient.validateCredentials(AccessKeyId); await credentialsClient.validateCredentials(AccessKeyId, roleChaining);
sourceAccountId = await (0, helpers_1.exportAccountId)(credentialsClient, maskAccountId); sourceAccountId = await (0, helpers_1.exportAccountId)(credentialsClient, maskAccountId);
} }
// Get role credentials if configured to do so // Get role credentials if configured to do so

View file

@ -13,17 +13,13 @@ export interface CredentialsClientProps {
export class CredentialsClient { export class CredentialsClient {
public region?: string; public region?: string;
private stsClient?: STSClient; private _stsClient?: STSClient;
private readonly requestHandler?: NodeHttpHandler; private readonly requestHandler?: NodeHttpHandler;
constructor(props: CredentialsClientProps) { constructor(props: CredentialsClientProps) {
if (props.region) { this.region = props.region;
this.region = props.region;
} else {
info('No region provided, using global STS endpoint');
}
if (props.proxyServer) { if (props.proxyServer) {
info('Configurint proxy handler for STS client'); info('Configuring proxy handler for STS client');
const handler = new HttpsProxyAgent(props.proxyServer); const handler = new HttpsProxyAgent(props.proxyServer);
this.requestHandler = new NodeHttpHandler({ this.requestHandler = new NodeHttpHandler({
httpAgent: handler, httpAgent: handler,
@ -32,19 +28,18 @@ export class CredentialsClient {
} }
} }
public getStsClient(): STSClient { public get stsClient(): STSClient {
if (!this.stsClient) { if (!this._stsClient) {
this.stsClient = new STSClient({ this._stsClient = new STSClient({
region: this.region ? this.region : undefined, region: this.region,
customUserAgent: USER_AGENT, customUserAgent: USER_AGENT,
requestHandler: this.requestHandler ? this.requestHandler : undefined, requestHandler: this.requestHandler ? this.requestHandler : undefined,
useGlobalEndpoint: this.region ? false : true,
}); });
} }
return this.stsClient; return this._stsClient;
} }
public async validateCredentials(expectedAccessKeyId?: string) { public async validateCredentials(expectedAccessKeyId?: string, roleChaining?: boolean) {
let credentials; let credentials;
try { try {
credentials = await this.loadCredentials(); credentials = await this.loadCredentials();
@ -55,12 +50,12 @@ export class CredentialsClient {
throw new Error(`Credentials could not be loaded, please check your action inputs: ${errorMessage(error)}`); throw new Error(`Credentials could not be loaded, please check your action inputs: ${errorMessage(error)}`);
} }
const actualAccessKeyId = credentials.accessKeyId; if (!roleChaining) {
const actualAccessKeyId = credentials.accessKeyId;
if (expectedAccessKeyId && expectedAccessKeyId !== actualAccessKeyId) { if (expectedAccessKeyId && expectedAccessKeyId !== actualAccessKeyId) {
throw new Error( throw new Error('Unexpected failure: Credentials loaded by the SDK do not match the access key ID configured by the action');
'Unexpected failure: Credentials loaded by the SDK do not match the access key ID configured by the action' }
);
} }
} }

View file

@ -139,7 +139,7 @@ export async function assumeRole(params: assumeRoleParams) {
keys.forEach((k) => commonAssumeRoleParams[k] === undefined && delete commonAssumeRoleParams[k]); keys.forEach((k) => commonAssumeRoleParams[k] === undefined && delete commonAssumeRoleParams[k]);
// Instantiate STS client // Instantiate STS client
const stsClient = credentialsClient.getStsClient(); const stsClient = credentialsClient.stsClient;
// Assume role using one of three methods // Assume role using one of three methods
switch (true) { switch (true) {

View file

@ -35,7 +35,7 @@ export function exportRegion(region: string) {
// Obtains account ID from STS Client and sets it as output // Obtains account ID from STS Client and sets it as output
export async function exportAccountId(credentialsClient: CredentialsClient, maskAccountId?: string) { export async function exportAccountId(credentialsClient: CredentialsClient, maskAccountId?: string) {
const client = credentialsClient.getStsClient(); const client = credentialsClient.stsClient;
const identity = await client.send(new GetCallerIdentityCommand({})); const identity = await client.send(new GetCallerIdentityCommand({}));
const accountId = identity.Account; const accountId = identity.Account;
if (!accountId) { if (!accountId) {

View file

@ -14,10 +14,7 @@ export async function run() {
const SecretAccessKey = core.getInput('aws-secret-access-key', { required: false }); const SecretAccessKey = core.getInput('aws-secret-access-key', { required: false });
const sessionTokenInput = core.getInput('aws-session-token', { required: false }); const sessionTokenInput = core.getInput('aws-session-token', { required: false });
const SessionToken = sessionTokenInput === '' ? undefined : sessionTokenInput; const SessionToken = sessionTokenInput === '' ? undefined : sessionTokenInput;
const region = const region = core.getInput('aws-region', { required: true });
core.getInput('aws-region', { required: false }) ||
process.env['AWS_REGION'] ||
process.env['AWS_DEFAULT_REGION'];
const roleToAssume = core.getInput('role-to-assume', { required: false }); const roleToAssume = core.getInput('role-to-assume', { required: false });
const audience = core.getInput('audience', { required: false }); const audience = core.getInput('audience', { required: false });
const maskAccountId = core.getInput('mask-aws-account-id', { required: false }); const maskAccountId = core.getInput('mask-aws-account-id', { required: false });
@ -35,6 +32,8 @@ export async function run() {
for (const managedSessionPolicy of managedSessionPoliciesInput) { for (const managedSessionPolicy of managedSessionPoliciesInput) {
managedSessionPolicies.push({arn: managedSessionPolicy}); managedSessionPolicies.push({arn: managedSessionPolicy});
} }
const roleChainingInput = core.getInput('role-chaining', { required: false }) || 'false';
const roleChaining = roleChainingInput.toLowerCase() === 'true';
// Logic to decide whether to attempt to use OIDC or not // Logic to decide whether to attempt to use OIDC or not
const useGitHubOIDCProvider = () => { const useGitHubOIDCProvider = () => {
@ -47,7 +46,8 @@ export async function run() {
!webIdentityTokenFile && !webIdentityTokenFile &&
!AccessKeyId && !AccessKeyId &&
!disableOIDC && !disableOIDC &&
!process.env['ACTIONS_ID_TOKEN_REQUEST_TOKEN'] !process.env['ACTIONS_ID_TOKEN_REQUEST_TOKEN'] &&
!roleChaining
) { ) {
core.info( core.info(
'It looks like you might be trying to authenticate with OIDC. Did you mean to set the `id-token` permission?' 'It looks like you might be trying to authenticate with OIDC. Did you mean to set the `id-token` permission?'
@ -58,7 +58,8 @@ export async function run() {
!!process.env['ACTIONS_ID_TOKEN_REQUEST_TOKEN'] && !!process.env['ACTIONS_ID_TOKEN_REQUEST_TOKEN'] &&
!AccessKeyId && !AccessKeyId &&
!webIdentityTokenFile && !webIdentityTokenFile &&
!disableOIDC !disableOIDC &&
!roleChaining
); );
}; };
@ -98,7 +99,7 @@ export async function run() {
// cases where this action is on a self-hosted runner that doesn't have credentials // cases where this action is on a self-hosted runner that doesn't have credentials
// configured correctly, and cases where the user intended to provide input // configured correctly, and cases where the user intended to provide input
// credentials but the secrets inputs resolved to empty strings. // credentials but the secrets inputs resolved to empty strings.
await credentialsClient.validateCredentials(AccessKeyId); await credentialsClient.validateCredentials(AccessKeyId, roleChaining);
sourceAccountId = await exportAccountId(credentialsClient, maskAccountId); sourceAccountId = await exportAccountId(credentialsClient, maskAccountId);
} }