Skip to content

Commit d179403

Browse files
committed
feat!: initial v6 work
1 parent f226b05 commit d179403

File tree

7 files changed

+656
-534
lines changed

7 files changed

+656
-534
lines changed

biome.jsonc

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,10 +12,6 @@
1212
"performance": {
1313
"noDelete": "off"
1414
},
15-
"correctness": {
16-
// Specifying a radix disables interpretation of 0x as a number (needed for backwards compat)
17-
"useParseIntRadix": "off"
18-
},
1915
"complexity": {
2016
"noExtraBooleanCast": "off"
2117
}

src/CredentialsClient.ts

Lines changed: 79 additions & 87 deletions
Original file line numberDiff line numberDiff line change
@@ -1,104 +1,96 @@
1-
import { info } from '@actions/core';
2-
import { STSClient } from '@aws-sdk/client-sts';
3-
import type { AwsCredentialIdentity } from '@aws-sdk/types';
4-
import { NodeHttpHandler } from '@smithy/node-http-handler';
5-
import { ProxyAgent } from 'proxy-agent';
6-
import { errorMessage, getCallerIdentity } from './helpers';
7-
import { ProxyResolver } from './ProxyResolver';
1+
import {
2+
fromContainerMetadata,
3+
fromEnv,
4+
fromInstanceMetadata,
5+
fromNodeProviderChain,
6+
fromTemporaryCredentials,
7+
fromWebToken,
8+
} from '@aws-sdk/credential-providers';
9+
import type { AwsCredentialIdentityProvider } from '@aws-sdk/types';
10+
import type { NodeHttpHandler } from '@smithy/node-http-handler';
811

9-
const USER_AGENT = 'configure-aws-credentials-for-github-actions';
12+
type EnvMode = { mode: 'env' };
13+
type WebIdentityMode = { mode: 'web-identity'; webIdentityToken: string } & AssumeRoleOptions;
14+
type StsMode = { mode: 'sts'; masterCredentials?: AwsCredentialIdentityProvider } & AssumeRoleOptions;
15+
type ContainerMetadataMode = { mode: 'container-metadata' };
16+
type InstanceMetadataMode = { mode: 'instance-metadata' };
1017

11-
export interface CredentialsClientProps {
12-
region?: string;
13-
proxyServer?: string;
14-
noProxy?: string;
15-
}
18+
type AssumeRoleOptions = {
19+
roleArn: string;
20+
roleSessionName?: string;
21+
durationSeconds?: number;
22+
policy?: string;
23+
policyArns?: { arn: string }[];
24+
region: string;
25+
requestHandler?: NodeHttpHandler;
26+
};
27+
28+
export type CredentialsClientProps = EnvMode | WebIdentityMode | StsMode | ContainerMetadataMode | InstanceMetadataMode;
1629

1730
export class CredentialsClient {
18-
public region?: string;
19-
private _stsClient?: STSClient;
20-
private readonly requestHandler?: NodeHttpHandler;
31+
readonly DEFAULT_ROLE_DURATION = 3600;
32+
readonly DEFAULT_ROLE_SESSION_NAME = 'GitHubActions';
33+
private provider: AwsCredentialIdentityProvider;
2134

2235
constructor(props: CredentialsClientProps) {
23-
if (props.region !== undefined) {
24-
this.region = props.region;
25-
}
26-
if (props.proxyServer) {
27-
info('Configuring proxy handler for STS client');
28-
const proxyOptions: { httpProxy: string; httpsProxy: string; noProxy?: string } = {
29-
httpProxy: props.proxyServer,
30-
httpsProxy: props.proxyServer,
31-
};
32-
if (props.noProxy !== undefined) {
33-
proxyOptions.noProxy = props.noProxy;
34-
}
35-
const getProxyForUrl = new ProxyResolver(proxyOptions).getProxyForUrl;
36-
const handler = new ProxyAgent({ getProxyForUrl });
37-
this.requestHandler = new NodeHttpHandler({
38-
httpsAgent: handler,
39-
httpAgent: handler,
40-
});
41-
}
36+
this.provider = this.createCredentialChain(props);
4237
}
4338

44-
public get stsClient(): STSClient {
45-
if (!this._stsClient) {
46-
const config = { customUserAgent: USER_AGENT } as {
47-
customUserAgent: string;
48-
region?: string;
49-
requestHandler?: NodeHttpHandler;
50-
};
51-
if (this.region !== undefined) config.region = this.region;
52-
if (this.requestHandler !== undefined) config.requestHandler = this.requestHandler;
53-
this._stsClient = new STSClient(config);
54-
}
55-
return this._stsClient;
56-
}
39+
private createCredentialChain(props: CredentialsClientProps): AwsCredentialIdentityProvider {
40+
const primaryProvider = this.getPrimaryProvider(props);
41+
const fallbackProvider = fromNodeProviderChain();
5742

58-
public async validateCredentials(
59-
expectedAccessKeyId?: string,
60-
roleChaining?: boolean,
61-
expectedAccountIds?: string[],
62-
) {
63-
let credentials: AwsCredentialIdentity;
64-
try {
65-
credentials = await this.loadCredentials();
66-
if (!credentials.accessKeyId) {
67-
throw new Error('Access key ID empty after loading credentials');
68-
}
69-
} catch (error) {
70-
throw new Error(`Credentials could not be loaded, please check your action inputs: ${errorMessage(error)}`);
71-
}
72-
if (expectedAccountIds && expectedAccountIds.length > 0 && expectedAccountIds[0] !== '') {
73-
let callerIdentity: Awaited<ReturnType<typeof getCallerIdentity>>;
43+
return async () => {
7444
try {
75-
callerIdentity = await getCallerIdentity(this.stsClient);
76-
} catch (error) {
77-
throw new Error(`Could not validate account ID of credentials: ${errorMessage(error)}`);
78-
}
79-
if (!callerIdentity.Account || !expectedAccountIds.includes(callerIdentity.Account)) {
80-
throw new Error(
81-
`The account ID of the provided credentials (${
82-
callerIdentity.Account ?? 'unknown'
83-
}) does not match any of the expected account IDs: ${expectedAccountIds.join(', ')}`,
84-
);
45+
return await primaryProvider();
46+
} catch {
47+
return await fallbackProvider();
8548
}
86-
}
49+
};
50+
}
8751

88-
if (!roleChaining) {
89-
const actualAccessKeyId = credentials.accessKeyId;
90-
if (expectedAccessKeyId && expectedAccessKeyId !== actualAccessKeyId) {
91-
throw new Error(
92-
'Credentials loaded by the SDK do not match the expected access key ID configured by the action',
93-
);
94-
}
52+
private getPrimaryProvider(props: CredentialsClientProps): AwsCredentialIdentityProvider {
53+
switch (props.mode) {
54+
case 'env':
55+
return fromEnv();
56+
case 'web-identity':
57+
return fromWebToken({
58+
clientConfig: {
59+
region: props.region,
60+
...(props.requestHandler && { requestHandler: props.requestHandler }),
61+
maxAttempts: 1, // Disable built-in retry logic
62+
},
63+
roleArn: props.roleArn,
64+
webIdentityToken: props.webIdentityToken,
65+
durationSeconds: props.durationSeconds ?? this.DEFAULT_ROLE_DURATION,
66+
roleSessionName: props.roleSessionName ?? this.DEFAULT_ROLE_SESSION_NAME,
67+
...(props.policy && { policy: props.policy }),
68+
...(props.policyArns && { policyArns: props.policyArns }),
69+
});
70+
case 'sts':
71+
return fromTemporaryCredentials({
72+
params: {
73+
RoleArn: props.roleArn,
74+
DurationSeconds: props.durationSeconds ?? this.DEFAULT_ROLE_DURATION,
75+
RoleSessionName: props.roleSessionName ?? this.DEFAULT_ROLE_SESSION_NAME,
76+
...(props.policy && { Policy: props.policy }),
77+
...(props.policyArns && { PolicyArns: props.policyArns }),
78+
},
79+
clientConfig: {
80+
region: props.region,
81+
...(props.requestHandler && { requestHandler: props.requestHandler }),
82+
maxAttempts: 1, // Disable built-in retry logic
83+
},
84+
...(props.masterCredentials && { masterCredentials: props.masterCredentials }),
85+
});
86+
case 'container-metadata':
87+
return fromContainerMetadata();
88+
case 'instance-metadata':
89+
return fromInstanceMetadata();
9590
}
9691
}
9792

98-
private async loadCredentials() {
99-
const config = {} as { requestHandler?: NodeHttpHandler };
100-
if (this.requestHandler !== undefined) config.requestHandler = this.requestHandler;
101-
const client = new STSClient(config);
102-
return client.config.credentials();
93+
getCredentials(): AwsCredentialIdentityProvider {
94+
return this.provider;
10395
}
10496
}

src/ProxyResolver.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ export class ProxyResolver {
3333
const proto = parsedUrl.protocol.split(':', 1)[0];
3434
if (!proto) return ''; // Don't proxy URLs without a protocol.
3535
const hostname = parsedUrl.host;
36-
const port = parseInt(parsedUrl.port || '') || DEFAULT_PORTS[proto] || 0;
36+
const port = Number.parseInt(parsedUrl.port || '', 10) || DEFAULT_PORTS[proto] || 0;
3737

3838
if (options?.noProxy && !this.shouldProxy(hostname, port, options.noProxy)) return '';
3939
if (proto === 'http' && options?.httpProxy) return options.httpProxy;
@@ -50,7 +50,7 @@ export class ProxyResolver {
5050

5151
const parsedProxy = proxy.match(/^(.+):(\d+)$/);
5252
const parsedProxyHostname = parsedProxy ? parsedProxy[1] : proxy;
53-
const parsedProxyPort = parsedProxy?.[2] ? parseInt(parsedProxy[2]) : 0;
53+
const parsedProxyPort = parsedProxy?.[2] ? Number.parseInt(parsedProxy[2], 10) : 0;
5454

5555
if (parsedProxyPort && parsedProxyPort !== port) return true; // Skip if ports don't match.
5656

src/assumeRole.ts

Lines changed: 0 additions & 167 deletions
This file was deleted.

0 commit comments

Comments
 (0)