diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e751bf4..fff1f2b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -19,10 +19,10 @@ jobs: fail-fast: false steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Use Node.js ${{ matrix.node-version }} - uses: actions/setup-node@v3 + uses: actions/setup-node@v4 with: node-version: ${{ matrix.node-version }} diff --git a/package.json b/package.json index fb1204f..2efdfdc 100644 --- a/package.json +++ b/package.json @@ -43,8 +43,8 @@ "typescript": "^3.7.5" }, "dependencies": { - "@alicloud/tea-typescript": "^1.5.3", - "httpx": "^2.2.0", + "@alicloud/tea-typescript": "^1.8.0", + "httpx": "^2.3.3", "ini": "^1.3.5", "kitx": "^2.0.0" }, diff --git a/src/client.ts b/src/client.ts index cf16e79..92e7a1c 100644 --- a/src/client.ts +++ b/src/client.ts @@ -14,6 +14,7 @@ import RAMRoleARNCredentialsProvider from './providers/ram_role_arn'; import OIDCRoleArnCredentialsProvider from './providers/oidc_role_arn'; import ECSRAMRoleCredentialsProvider from './providers/ecs_ram_role'; import DefaultCredentialsProvider from './providers/default'; +import URICredentialsProvider from './providers/uri'; export { Config }; @@ -140,6 +141,8 @@ export default class Credential implements ICredential { this.credential = new InnerCredentialsClient('ecs_ram_role', ECSRAMRoleCredentialsProvider.builder() .withRoleName(config.roleName) .withDisableIMDSv1(config.disableIMDSv1) + .withReadTimeout(config.timeout) + .withConnectTimeout(config.connectTimeout) .build()); break; case 'ram_role_arn': { @@ -162,6 +165,12 @@ export default class Credential implements ICredential { .withPolicy(config.policy) .withDurationSeconds(config.roleSessionExpiration) .withRoleSessionName(config.roleSessionName) + .withReadTimeout(config.timeout) + .withConnectTimeout(config.connectTimeout) + .withEnableVpc(config.enableVpc) + .withStsEndpoint(config.stsEndpoint) + .withStsRegionId(config.stsRegionId) + .withExternalId(config.externalId) // .withHttpOptions(runtime) .build()); } @@ -174,6 +183,9 @@ export default class Credential implements ICredential { .withRoleSessionName(config.roleSessionName) .withPolicy(config.policy) .withDurationSeconds(config.roleSessionExpiration) + .withStsEndpoint(config.stsEndpoint) + .withReadTimeout(config.timeout) + .withConnectTimeout(config.connectTimeout) .build()); break; case 'rsa_key_pair': @@ -183,7 +195,11 @@ export default class Credential implements ICredential { this.credential = new BearerTokenCredential(config.bearerToken); break; case 'credentials_uri': - this.credential = new URICredential(config.credentialsURI); + this.credential = new InnerCredentialsClient('credentials_uri', URICredentialsProvider.builder() + .withCredentialsURI(config.credentialsURI) + .withReadTimeout(config.timeout) + .withConnectTimeout(config.connectTimeout) + .build()); break; default: throw new Error('Invalid type option, support: access_key, sts, ecs_ram_role, ram_role_arn, rsa_key_pair, credentials_uri'); diff --git a/src/config.ts b/src/config.ts index 17e7dd7..68e4b86 100644 --- a/src/config.ts +++ b/src/config.ts @@ -20,6 +20,10 @@ export default class Config extends $tea.Model { oidcProviderArn: string; oidcTokenFilePath: string; type?: string; + externalId?: string; + stsEndpoint?: string; + timeout?: number; + connectTimeout?: number; static names(): { [key: string]: string } { return { @@ -32,6 +36,12 @@ export default class Config extends $tea.Model { policy: 'policy', roleSessionExpiration: 'roleSessionExpiration', roleSessionName: 'roleSessionName', + externalId: 'externalId', + stsEndpoint: 'stsEndpoint', + stsRegionId: 'stsRegionId', + enableVpc: 'enableVpc', + timeout: 'readTimeout', + connectTimeout: 'connectTimeout', publicKeyId: 'publicKeyId', privateKeyFile: 'privateKeyFile', roleName: 'roleName', @@ -56,6 +66,12 @@ export default class Config extends $tea.Model { policy: 'string', roleSessionExpiration: 'number', roleSessionName: 'string', + externalId: 'string', + stsEndpoint: 'string', + stsRegionId: 'string', + enableVpc: 'string', + timeout: 'number', + connectTimeout: 'number', publicKeyId: 'string', privateKeyFile: 'string', roleName: 'string', diff --git a/src/ecs_ram_role_credential.ts b/src/ecs_ram_role_credential.ts index c4d4b81..1c9a6cb 100644 --- a/src/ecs_ram_role_credential.ts +++ b/src/ecs_ram_role_credential.ts @@ -13,6 +13,8 @@ export default class EcsRamRoleCredential extends SessionCredential implements I runtime: { [key: string]: any }; metadataToken?: string; staleTime?: number + readTimeout?: number; + connectTimeout?: number; constructor(roleName: string = '', runtime: { [key: string]: any } = {}, enableIMDSv2: boolean = false, metadataTokenDuration: number = 21600) { const conf = new Config({ @@ -58,7 +60,9 @@ export default class EcsRamRoleCredential extends SessionCredential implements I options = { headers: { 'X-aliyun-ecs-metadata-token': this.metadataToken - } + }, + readTimeout: this.readTimeout, + connectTimeout: this.connectTimeout } } const roleName = await this.getRoleName(); diff --git a/src/providers/cli_profile.ts b/src/providers/cli_profile.ts index d9e7166..b33f126 100644 --- a/src/providers/cli_profile.ts +++ b/src/providers/cli_profile.ts @@ -23,7 +23,7 @@ class CLIProfileCredentialsProviderBuilder { this.profileName = process.env.ALIBABA_CLOUD_PROFILE; } - if (process.env.ALIBABA_CLOUD_CLI_PROFILE_DISABLED === 'true') { + if (process.env.ALIBABA_CLOUD_CLI_PROFILE_DISABLED && process.env.ALIBABA_CLOUD_CLI_PROFILE_DISABLED.toLowerCase() === 'true') { throw new Error('the CLI profile is disabled'); } @@ -50,6 +50,9 @@ interface Profile { ram_role_name: string; oidc_token_file: string; oidc_provider_arn: string; + sts_endpoint: string, + enable_vpc: boolean, + duration_seconds: number } class Configuration { @@ -121,6 +124,8 @@ export default class CLIProfileCredentialsProvider implements CredentialsProvide .withRoleSessionName(p.ram_session_name) .withDurationSeconds(p.expired_seconds) .withStsRegionId(p.sts_region) + .withStsEndpoint(p.sts_endpoint) + .withEnableVpc(p.enable_vpc) .build(); } case 'EcsRamRole': @@ -133,6 +138,8 @@ export default class CLIProfileCredentialsProvider implements CredentialsProvide .withStsRegionId(p.sts_region) .withDurationSeconds(p.expired_seconds) .withRoleSessionName(p.ram_session_name) + .withDurationSeconds(p.duration_seconds) + .withEnableVpc(p.enable_vpc) .build(); case 'ChainableRamRoleArn': { const previousProvider = this.getCredentialsProvider(conf, p.source_profile); diff --git a/src/providers/default.ts b/src/providers/default.ts index c3d7189..e007a2f 100644 --- a/src/providers/default.ts +++ b/src/providers/default.ts @@ -4,11 +4,12 @@ import CLIProfileCredentialsProvider from './cli_profile'; import ECSRAMRoleCredentialsProvider from './ecs_ram_role'; import EnvironmentVariableCredentialsProvider from './env'; import OIDCRoleArnCredentialsProvider from './oidc_role_arn'; +import URICredentialsProvider from './uri'; import ProfileCredentialsProvider from './profile'; export default class DefaultCredentialsProvider implements CredentialsProvider { private readonly providers: CredentialsProvider[]; - private lastUsedProvider : CredentialsProvider; + private lastUsedProvider: CredentialsProvider; static builder() { return new DefaultCredentialsProviderBuilder(); } @@ -48,17 +49,21 @@ export default class DefaultCredentialsProvider implements CredentialsProvider { } // Add IMDS - if (process.env.ALIBABA_CLOUD_ECS_METADATA) { - try { - const ecsRamRoleProvider = ECSRAMRoleCredentialsProvider.builder().withRoleName(process.env.ALIBABA_CLOUD_ECS_METADATA).build(); - this.providers.push(ecsRamRoleProvider); - } - catch (ex) { - // ignore - } + try { + const ecsRamRoleProvider = ECSRAMRoleCredentialsProvider.builder().withRoleName(process.env.ALIBABA_CLOUD_ECS_METADATA).build(); + this.providers.push(ecsRamRoleProvider); + } catch (ex) { + // ignore } - // TODO: ALIBABA_CLOUD_CREDENTIALS_URI check + // credentials uri + try { + const uriProvider = URICredentialsProvider.builder().withCredentialsURI(process.env.ALIBABA_CLOUD_CREDENTIALS_URI).build(); + this.providers.push(uriProvider); + } + catch (ex) { + // ignore + } } async getCredentials(): Promise { @@ -78,7 +83,7 @@ export default class DefaultCredentialsProvider implements CredentialsProvider { let inner; try { inner = await provider.getCredentials(); - } catch(ex) { + } catch (ex) { errors.push(ex); continue; } diff --git a/src/providers/ecs_ram_role.ts b/src/providers/ecs_ram_role.ts index 0938505..94ad3d3 100644 --- a/src/providers/ecs_ram_role.ts +++ b/src/providers/ecs_ram_role.ts @@ -14,6 +14,8 @@ export default class ECSRAMRoleCredentialsProvider implements CredentialsProvide private expirationTimestamp: number // for mock private doRequest = doRequest; + private readonly readTimeout: number; + private readonly connectTimeout: number; static builder(): ECSRAMRoleCredentialsProviderBuilder { return new ECSRAMRoleCredentialsProviderBuilder(); @@ -22,6 +24,8 @@ export default class ECSRAMRoleCredentialsProvider implements CredentialsProvide constructor(builder: ECSRAMRoleCredentialsProviderBuilder) { this.roleName = builder.roleName; this.disableIMDSv1 = builder.disableIMDSv1; + this.readTimeout = builder.readTimeout; + this.connectTimeout = builder.connectTimeout; } async getCredentials(): Promise { @@ -58,6 +62,8 @@ export default class ECSRAMRoleCredentialsProvider implements CredentialsProvide .withHeaders({ 'x-aliyun-ecs-metadata-token-ttl-seconds': `${defaultMetadataTokenDuration}` }) + .withReadTimeout(this.readTimeout || 1000) + .withConnectTimeout(this.connectTimeout || 1000) .build(); // ConnectTimeout: 5 * time.Second, @@ -82,7 +88,9 @@ export default class ECSRAMRoleCredentialsProvider implements CredentialsProvide .withMethod('GET') .withProtocol('http') .withHost('100.100.100.200') - .withPath('/latest/meta-data/ram/security-credentials/'); + .withPath('/latest/meta-data/ram/security-credentials/') + .withReadTimeout(this.readTimeout || 1000) + .withConnectTimeout(this.connectTimeout || 1000); const metadataToken = await this.getMetadataToken(); if (metadataToken !== null) { @@ -114,7 +122,9 @@ export default class ECSRAMRoleCredentialsProvider implements CredentialsProvide .withMethod('GET') .withProtocol('http') .withHost('100.100.100.200') - .withPath(`/latest/meta-data/ram/security-credentials/${roleName}`); + .withPath(`/latest/meta-data/ram/security-credentials/${roleName}`) + .withReadTimeout(this.readTimeout || 1000) + .withConnectTimeout(this.connectTimeout || 1000); // ConnectTimeout: 5 * time.Second, // ReadTimeout: 5 * time.Second, @@ -160,6 +170,8 @@ export default class ECSRAMRoleCredentialsProvider implements CredentialsProvide class ECSRAMRoleCredentialsProviderBuilder { roleName: string disableIMDSv1: boolean + readTimeout?: number; + connectTimeout?: number; constructor() { this.disableIMDSv1 = false; @@ -175,14 +187,29 @@ class ECSRAMRoleCredentialsProviderBuilder { return this; } + withReadTimeout(readTimeout: number): ECSRAMRoleCredentialsProviderBuilder{ + this.readTimeout = readTimeout + return this; + } + + withConnectTimeout(connectTimeout: number): ECSRAMRoleCredentialsProviderBuilder{ + this.connectTimeout = connectTimeout + return this; + } + build(): ECSRAMRoleCredentialsProvider { + // 允许通过环境变量强制关闭 IMDS + if (process.env.ALIBABA_CLOUD_ECS_METADATA_DISABLED && process.env.ALIBABA_CLOUD_ECS_METADATA_DISABLED.toLowerCase() === 'true') { + throw new Error('IMDS credentials is disabled'); + } + // 设置 roleName 默认值 if (!this.roleName) { this.roleName = process.env.ALIBABA_CLOUD_ECS_METADATA; } // 允许通过环境变量强制关闭 V1 - if (process.env.ALIBABA_CLOUD_IMDSV1_DISABLED === 'true') { + if (process.env.ALIBABA_CLOUD_IMDSV1_DISABLED && process.env.ALIBABA_CLOUD_IMDSV1_DISABLED.toLowerCase() === 'true') { this.disableIMDSv1 = true; } diff --git a/src/providers/http.ts b/src/providers/http.ts index a7379a9..1acd28f 100644 --- a/src/providers/http.ts +++ b/src/providers/http.ts @@ -9,6 +9,9 @@ export class Request { readonly path: any; readonly bodyForm: { [key: string]: string; }; readonly bodyBytes: Buffer; + readonly url: string; + readonly readTimeout: number; + readonly connectTimeout: number; static builder() { return new RequestBuilder(); @@ -23,9 +26,15 @@ export class Request { this.headers = builder.headers; this.bodyForm = builder.bodyForm; this.bodyBytes = builder.bodyBytes; + this.url = builder.url; + this.readTimeout = builder.readTimeout; + this.connectTimeout = builder.connectTimeout; } toRequestURL(): string { + if(this.url){ + return this.url; + } let url = `${this.protocol}://${this.host}${this.path}`; if (this.queries && Object.keys(this.queries).length > 0) { url += `?` + querystringify(this.queries) @@ -43,6 +52,9 @@ export class RequestBuilder { headers: { [key: string]: string; }; bodyForm: { [key: string]: string; }; bodyBytes: Buffer; + readTimeout: number; + connectTimeout: number; + url: string; build(): Request { // set default values @@ -99,6 +111,21 @@ export class RequestBuilder { this.bodyForm = bodyForm; return this; } + + withURL(url: string){ + this.url = url; + return this; + } + + withReadTimeout(readTimeout: number) { + this.readTimeout = readTimeout; + return this; + } + + withConnectTimeout(connectTimeout: number) { + this.connectTimeout = connectTimeout; + return this; + } } export class Response { @@ -168,7 +195,9 @@ export async function doRequest(req: Request): Promise { const response = await httpx.request(url, { method: req.method, data: body, - headers: req.headers + headers: req.headers, + readTimeout: req.readTimeout, + connectTimeout: req.connectTimeout }); const responseBody = await httpx.read(response, ''); diff --git a/src/providers/oidc_role_arn.ts b/src/providers/oidc_role_arn.ts index e8abf6a..dae5c05 100644 --- a/src/providers/oidc_role_arn.ts +++ b/src/providers/oidc_role_arn.ts @@ -19,6 +19,9 @@ class OIDCRoleArnCredentialsProviderBuilder { stsRegionId: string; policy: string; durationSeconds: number; + enableVpc?: boolean; + readTimeout?: number; + connectTimeout?: number; withOIDCProviderArn(oidcProviderArn: string) { this.oidcProviderArn = oidcProviderArn; @@ -60,6 +63,21 @@ class OIDCRoleArnCredentialsProviderBuilder { return this; } + withEnableVpc(enableVpc: boolean): OIDCRoleArnCredentialsProviderBuilder { + this.enableVpc = enableVpc + return this; + } + + withReadTimeout(readTimeout: number): OIDCRoleArnCredentialsProviderBuilder { + this.readTimeout = readTimeout + return this; + } + + withConnectTimeout(connectTimeout: number): OIDCRoleArnCredentialsProviderBuilder { + this.connectTimeout = connectTimeout + return this; + } + build(): OIDCRoleArnCredentialsProvider { // set default values if (!this.oidcProviderArn) { @@ -102,13 +120,23 @@ class OIDCRoleArnCredentialsProviderBuilder { throw new Error('session duration should be in the range of 900s - max session duration'); } + if (!this.stsRegionId) { + this.stsRegionId = process.env.ALIBABA_CLOUD_STS_REGION; + } + + if (!this.enableVpc) { + this.enableVpc = process.env.ALIBABA_CLOUD_VPC_ENDPOINT_ENABLED && process.env.ALIBABA_CLOUD_VPC_ENDPOINT_ENABLED.toLowerCase() === 'true' || false; + } + // sts endpoint if (!this.stsEndpoint) { if (this.stsRegionId) { - this.stsEndpoint = `sts.${this.stsRegionId}.aliyuncs.com` - } else { - this.stsEndpoint = 'sts.aliyuncs.com' - } + if (this.enableVpc) { + this.stsEndpoint = `sts-vpc.${this.stsRegionId}.aliyuncs.com` + } else { + this.stsEndpoint = `sts.${this.stsRegionId}.aliyuncs.com` + } + } else { this.stsEndpoint = 'sts.aliyuncs.com' } } return new OIDCRoleArnCredentialsProvider(this); @@ -125,6 +153,8 @@ export default class OIDCRoleArnCredentialsProvider implements CredentialsProvid runtime: { [key: string]: any }; private readonly stsEndpoint: string; private doRequest = doRequest; + private readonly readTimeout: number; + private readonly connectTimeout: number; private session: Session; expirationTimestamp: number; @@ -142,6 +172,8 @@ export default class OIDCRoleArnCredentialsProvider implements CredentialsProvid this.durationSeconds = builder.durationSeconds; this.roleSessionName = builder.roleSessionName; this.stsEndpoint = builder.stsEndpoint; + this.readTimeout = builder.readTimeout; + this.connectTimeout = builder.connectTimeout; // used for mock this.doRequest = doRequest; } @@ -171,7 +203,7 @@ export default class OIDCRoleArnCredentialsProvider implements CredentialsProvid async getCredentialsInternal(): Promise { const oidcToken = await readFileAsync(this.oidcTokenFilePath, 'utf8'); - const builder = Request.builder().withMethod('POST').withProtocol('https').withHost(this.stsEndpoint); + const builder = Request.builder().withMethod('POST').withProtocol('https').withHost(this.stsEndpoint).withReadTimeout(this.readTimeout || 10000).withConnectTimeout(this.connectTimeout || 5000); const queries = Object.create(null); queries['Version'] = '2015-04-01'; diff --git a/src/providers/profile.ts b/src/providers/profile.ts index c786374..d322da3 100644 --- a/src/providers/profile.ts +++ b/src/providers/profile.ts @@ -8,6 +8,7 @@ import { loadIni } from '../util/utils'; import StaticAKCredentialsProvider from './static_ak'; import ECSRAMRoleCredentialsProvider from './ecs_ram_role'; import RAMRoleARNCredentialsProvider from './ram_role_arn'; +import OIDCRoleArnCredentialsProvider from './oidc_role_arn' export default class ProfileCredentialsProvider implements CredentialsProvider { private readonly profileName: string; @@ -46,27 +47,34 @@ export default class ProfileCredentialsProvider implements CredentialsProvider { } switch (config.type) { - case 'access_key': - return StaticAKCredentialsProvider.builder() - .withAccessKeyId(config.access_key_id) - .withAccessKeySecret(config.access_key_secret) - .build(); - case 'ecs_ram_role': - return ECSRAMRoleCredentialsProvider.builder() - .withRoleName(config.role_name) - .build(); - case 'ram_role_arn': { - const previous = StaticAKCredentialsProvider.builder() - .withAccessKeyId(config.access_key_id) - .withAccessKeySecret(config.access_key_secret) - .build(); - return RAMRoleARNCredentialsProvider.builder() - .withCredentialsProvider(previous) - .withRoleArn(config.role_arn) - .build(); - } - default: - throw new Error('Invalid type option, support: access_key, ecs_ram_role, ram_role_arn'); + case 'access_key': + return StaticAKCredentialsProvider.builder() + .withAccessKeyId(config.access_key_id) + .withAccessKeySecret(config.access_key_secret) + .build(); + case 'ecs_ram_role': + return ECSRAMRoleCredentialsProvider.builder() + .withRoleName(config.role_name) + .build(); + case 'ram_role_arn': + { + const previous = StaticAKCredentialsProvider.builder() + .withAccessKeyId(config.access_key_id) + .withAccessKeySecret(config.access_key_secret) + .build(); + return RAMRoleARNCredentialsProvider.builder() + .withCredentialsProvider(previous) + .withRoleArn(config.role_arn) + .withRoleSessionName(config.role_session_name) + .withPolicy(config.policy) + // .withStsEndpoint(config.stsEndpoint) + // .withStsRegionId(config.stsRegionId) + // .withEnableVpc(config.enableVpc) + // .withExternalId(config.enableVpc) + .build(); + } + default: + throw new Error('Invalid type option, support: access_key, ecs_ram_role, ram_role_arn'); } } diff --git a/src/providers/ram_role_arn.ts b/src/providers/ram_role_arn.ts index e4b0fae..b983d86 100644 --- a/src/providers/ram_role_arn.ts +++ b/src/providers/ram_role_arn.ts @@ -26,18 +26,27 @@ class RAMRoleARNCredentialsProviderBuilder { stsRegionId: string; policy: string; externalId: string; + enableVpc?: boolean; + readTimeout?: number; + connectTimeout?: number; build(): RAMRoleARNCredentialsProvider { if (!this.credentialsProvider) { throw new Error('must specify a previous credentials provider to asssume role'); } - if (!this.roleArn) { - throw new Error('the RoleArn is empty') - } + if (!(this.roleArn = this.roleArn || process.env.ALIBABA_CLOUD_ROLE_ARN)) throw new Error('the RoleArn is empty'); if (!this.roleSessionName) { - this.roleSessionName = 'credentials-nodejs-' + Date.now() + this.roleSessionName = process.env.ALIBABA_CLOUD_ROLE_SESSION_NAME || 'credentials-nodejs-' + Date.now(); + } + + if (!this.stsRegionId) { + this.stsRegionId = process.env.ALIBABA_CLOUD_STS_REGION; + } + + if (!this.enableVpc) { + this.enableVpc = process.env.ALIBABA_CLOUD_VPC_ENDPOINT_ENABLED && process.env.ALIBABA_CLOUD_VPC_ENDPOINT_ENABLED.toLowerCase() === 'true' || false; } // duration seconds @@ -53,10 +62,12 @@ class RAMRoleARNCredentialsProviderBuilder { // sts endpoint if (!this.stsEndpoint) { if (this.stsRegionId) { - this.stsEndpoint = `sts.${this.stsRegionId}.aliyuncs.com` - } else { - this.stsEndpoint = 'sts.aliyuncs.com' - } + if (this.enableVpc) { + this.stsEndpoint = `sts-vpc.${this.stsRegionId}.aliyuncs.com` + } else { + this.stsEndpoint = `sts.${this.stsRegionId}.aliyuncs.com` + } + } else { this.stsEndpoint = 'sts.aliyuncs.com' } } return new RAMRoleARNCredentialsProvider(this); @@ -102,10 +113,20 @@ class RAMRoleARNCredentialsProviderBuilder { return this; } - // withHttpOptions(httpOptions *HttpOptions) RAMRoleARNCredentialsProviderBuilder { - // this.httpOptions = httpOptions - // return this; - // } + withEnableVpc(enableVpc: boolean): RAMRoleARNCredentialsProviderBuilder { + this.enableVpc = enableVpc + return this; + } + + withReadTimeout(readTimeout: number): RAMRoleARNCredentialsProviderBuilder { + this.readTimeout = readTimeout + return this; + } + + withConnectTimeout(connectTimeout: number): RAMRoleARNCredentialsProviderBuilder { + this.connectTimeout = connectTimeout + return this; + } } function encode(str: string): string { @@ -126,6 +147,8 @@ export default class RAMRoleARNCredentialsProvider implements CredentialsProvide private readonly durationSeconds: number; private readonly externalId: string; private readonly roleArn: string; + private readonly readTimeout: number; + private readonly connectTimeout: number; // used for mock private doRequest = doRequest; @@ -146,11 +169,13 @@ export default class RAMRoleARNCredentialsProvider implements CredentialsProvide this.durationSeconds = builder.durationSeconds; this.roleArn = builder.roleArn; this.externalId = builder.externalId; + this.readTimeout = builder.readTimeout; + this.connectTimeout = builder.connectTimeout; } private async getCredentialsInternal(credentials: Credentials): Promise { const method = 'POST'; - const builder = Request.builder().withMethod(method).withProtocol('https').withHost(this.stsEndpoint); + const builder = Request.builder().withMethod(method).withProtocol('https').withHost(this.stsEndpoint).withReadTimeout(this.readTimeout || 10000).withConnectTimeout(this.connectTimeout || 5000); const queries = Object.create(null); queries['Version'] = '2015-04-01'; diff --git a/src/providers/uri.ts b/src/providers/uri.ts new file mode 100644 index 0000000..1cc8c6b --- /dev/null +++ b/src/providers/uri.ts @@ -0,0 +1,117 @@ + +import Credentials from '../credentials'; +import CredentialsProvider from '../credentials_provider'; +import Session from './session' +import { Request, doRequest } from './http' +import { parseUTC } from './time' + + +/** + * @internal + */ +export default class URICredentialsProvider implements CredentialsProvider { + static builder(): URICredentialsProviderBuilder { + return new URICredentialsProviderBuilder(); + } + + private readonly credentialsURI: string; + private session: Session + private doRequest = doRequest; + private readonly readTimeout: number; + private readonly connectTimeout: number; + private expirationTimestamp: number + + public constructor(builder: URICredentialsProviderBuilder) { + this.credentialsURI = builder.credentialsURI; + this.readTimeout = builder.readTimeout; + this.connectTimeout = builder.connectTimeout; + } + + getProviderName(): string { + return 'credential_uri'; + } + + async getCredentials(): Promise { + if (!this.session || this.needUpdateCredential()) { + const session = await this.getCredentialsUri(); + const expirationTime = parseUTC(session.expiration); + this.session = session; + this.expirationTimestamp = expirationTime / 1000; + } + + return Credentials.builder() + .withAccessKeyId(this.session.accessKeyId) + .withAccessKeySecret(this.session.accessKeySecret) + .withSecurityToken(this.session.securityToken) + .withProviderName(this.getProviderName()) + .build(); + } + + private needUpdateCredential(): boolean { + if (!this.expirationTimestamp) { + return true + } + + return this.expirationTimestamp - (Date.now() / 1000) <= 180; + } + + private async getCredentialsUri(): Promise { + const builder = Request.builder() + .withMethod('GET') + .withURL(this.credentialsURI) + .withReadTimeout(this.readTimeout || 10000) + .withConnectTimeout(this.connectTimeout || 5000); + + const request = builder.build(); + const response = await this.doRequest(request); + + if (response.statusCode !== 200) { + throw new Error(`get sts token failed, httpStatus: ${response.statusCode}, message = ${response.body.toString('utf8')}.`); + } + + let data; + try { + data = JSON.parse(response.body.toString('utf8')); + } catch (ex) { + throw new Error(`get sts token failed, json parse failed: ${ex.message}, result: ${response.body.toString('utf8')}.`) + } + + if (!data || !data.AccessKeyId || !data.AccessKeySecret || !data.SecurityToken) { + throw new Error(`error retrieving credentials from credentialsURI result: ${JSON.stringify(data)}.`) + } + + return new Session(data.AccessKeyId, data.AccessKeySecret, data.SecurityToken, data.Expiration); + } +} + + + +/** + * @internal + */ +export class URICredentialsProviderBuilder { + credentialsURI: string; + readTimeout?: number; + connectTimeout?: number; + + public withCredentialsURI(credentialsURI: string): URICredentialsProviderBuilder { + this.credentialsURI = credentialsURI; + return this; + } + withReadTimeout(readTimeout: number): URICredentialsProviderBuilder { + this.readTimeout = readTimeout + return this; + } + + withConnectTimeout(connectTimeout: number): URICredentialsProviderBuilder { + this.connectTimeout = connectTimeout + return this; + } + + public build(): URICredentialsProvider { + if (!this.credentialsURI) { + this.credentialsURI = process.env.ALIBABA_CLOUD_CREDENTIALS_URI; + } + return new URICredentialsProvider(this); + } +} diff --git a/src/uri_credential.ts b/src/uri_credential.ts index cbb7047..4c2c4b5 100644 --- a/src/uri_credential.ts +++ b/src/uri_credential.ts @@ -6,6 +6,8 @@ import SessionCredential from './session_credential'; export default class URICredential extends SessionCredential implements ICredential { credentialsURI: string; + readTimeout?: number; + connectTimeout?: number; constructor(uri: string) { const conf = new Config({ @@ -27,7 +29,7 @@ export default class URICredential extends SessionCredential implements ICredent async updateCredential(): Promise { const url = this.credentialsURI; - const response = await httpx.request(url, {}); + const response = await httpx.request(url, { readTimeout: this.readTimeout, connectTimeout: this.connectTimeout }); if (response.statusCode !== 200) { throw new Error(`Get credentials from ${url} failed, status code is ${response.statusCode}`); } diff --git a/test/providers/default.test.ts b/test/providers/default.test.ts index 39dd7a9..d2023f3 100644 --- a/test/providers/default.test.ts +++ b/test/providers/default.test.ts @@ -12,7 +12,7 @@ describe('DefaultCredentialsProvider', function () { it('DefaultCredentialsProvider', async function () { let provider = DefaultCredentialsProvider.builder().build(); - assert.ok((provider as any).providers.length === 3); + assert.ok((provider as any).providers.length === 5); assert.ok((provider as any).providers[0] instanceof EnvironmentVariableCredentialsProvider); assert.ok((provider as any).providers[1] instanceof CLIProfileCredentialsProvider); assert.ok((provider as any).providers[2] instanceof ProfileCredentialsProvider); @@ -24,7 +24,7 @@ describe('DefaultCredentialsProvider', function () { provider = DefaultCredentialsProvider.builder().build(); - assert.ok((provider as any).providers.length === 4); + assert.ok((provider as any).providers.length === 6); assert.ok((provider as any).providers[0] instanceof EnvironmentVariableCredentialsProvider) assert.ok((provider as any).providers[1] instanceof OIDCRoleArnCredentialsProvider) assert.ok((provider as any).providers[2] instanceof CLIProfileCredentialsProvider) @@ -33,8 +33,7 @@ describe('DefaultCredentialsProvider', function () { // Add ecs ram role process.env.ALIBABA_CLOUD_ECS_METADATA = 'rolename'; provider = DefaultCredentialsProvider.builder().build(); - - assert.ok((provider as any).providers.length === 5); + assert.ok((provider as any).providers.length === 6); assert.ok((provider as any).providers[0] instanceof EnvironmentVariableCredentialsProvider); assert.ok((provider as any).providers[1] instanceof OIDCRoleArnCredentialsProvider); assert.ok((provider as any).providers[2] instanceof CLIProfileCredentialsProvider); @@ -51,7 +50,7 @@ describe('DefaultCredentialsProvider', function () { process.env.ALIBABA_CLOUD_CLI_PROFILE_DISABLED = 'true'; let provider = DefaultCredentialsProvider.builder().build(); - assert.ok((provider as any).providers.length === 2); + assert.ok((provider as any).providers.length === 4); try { await provider.getCredentials(); assert.fail(); @@ -62,7 +61,7 @@ describe('DefaultCredentialsProvider', function () { process.env.ALIBABA_CLOUD_ACCESS_KEY_ID = 'akid'; process.env.ALIBABA_CLOUD_ACCESS_KEY_SECRET = 'aksecret'; provider = DefaultCredentialsProvider.builder().build(); - assert.ok((provider as any).providers.length === 2); + assert.ok((provider as any).providers.length === 4); let cc = await provider.getCredentials(); assert.deepStrictEqual(cc, Credentials.builder() .withAccessKeyId('akid') diff --git a/test/providers/ecs_ram_role.test.ts b/test/providers/ecs_ram_role.test.ts index a91caff..588242e 100644 --- a/test/providers/ecs_ram_role.test.ts +++ b/test/providers/ecs_ram_role.test.ts @@ -438,4 +438,14 @@ describe('ECSRAMRoleCredentialsProvider', function () { metadataToken = await (p as any).getMetadataToken(); assert.strictEqual('tokenxxxxx', metadataToken); }); + + it('env ALIBABA_CLOUD_ECS_METADATA_DISABLED should ok', async function () { + try { + process.env.ALIBABA_CLOUD_ECS_METADATA_DISABLED = "true"; + let p = ECSRAMRoleCredentialsProvider.builder().build(); + } catch (ex) { + assert.strictEqual(ex.message, 'IMDS credentials is disabled'); + } + delete process.env.ALIBABA_CLOUD_ECS_METADATA_DISABLED; + }); }); diff --git a/test/providers/oidc_role_arn.test.ts b/test/providers/oidc_role_arn.test.ts index a585b3f..e97ad21 100644 --- a/test/providers/oidc_role_arn.test.ts +++ b/test/providers/oidc_role_arn.test.ts @@ -65,6 +65,15 @@ describe('OIDCCredentialsProvider', function () { .build(); assert.strictEqual((p as any)['stsEndpoint'], 'sts.cn-hangzhou.aliyuncs.com') + p = OIDCCredentialsProvider.builder() + .withRoleArn('roleArn') + .withOIDCProviderArn('oidcProviderArn') + .withOIDCTokenFilePath('/tmp/inexist') + .withStsRegionId('cn-hangzhou') + .withEnableVpc(true) + .build(); + assert.strictEqual((p as any)['stsEndpoint'], 'sts-vpc.cn-hangzhou.aliyuncs.com') + // test for roleSesssionName p = OIDCCredentialsProvider.builder() .withRoleArn('roleArn') @@ -120,6 +129,8 @@ describe('OIDCCredentialsProvider', function () { .withOIDCProviderArn('oidcProviderArn') .withOIDCTokenFilePath(path.join(__dirname, '../fixtures/OIDCToken.txt')) .withDurationSeconds(1000) + .withReadTimeout(1000) + .withConnectTimeout(1000) .build(); // case 1: server error @@ -196,12 +207,12 @@ describe('OIDCCredentialsProvider', function () { assert.strictEqual(creds.accessKeySecret, 'saks'); assert.strictEqual(creds.securityToken, 'token'); assert.strictEqual(creds.expiration, '2021-10-20T04:27:09Z'); - + // needUpdateCredential assert.ok(p.needUpdateCredential() === true); (p as any).expirationTimestamp = Date.now() / 1000; assert.ok(p.needUpdateCredential() === true); - + (p as any).expirationTimestamp = Date.now() / 1000 + 300 assert.ok(p.needUpdateCredential() === false); }); @@ -218,10 +229,10 @@ describe('OIDCCredentialsProvider', function () { (p as any).doRequest = async function (req: Request) { assert.strictEqual(req.host, 'sts.cn-beijing.aliyuncs.com'); - assert.strictEqual(req.bodyForm['Policy'] , 'policy'); - assert.strictEqual(req.bodyForm['RoleArn'] , 'roleArn'); - assert.strictEqual(req.bodyForm['RoleSessionName'] , 'rsn'); - assert.strictEqual(req.bodyForm['DurationSeconds'] , '3600'); + assert.strictEqual(req.bodyForm['Policy'], 'policy'); + assert.strictEqual(req.bodyForm['RoleArn'], 'roleArn'); + assert.strictEqual(req.bodyForm['RoleSessionName'], 'rsn'); + assert.strictEqual(req.bodyForm['DurationSeconds'], '3600'); throw new Error('mock server error'); }; @@ -281,4 +292,35 @@ describe('OIDCCredentialsProvider', function () { } }); + it('env ALIBABA_CLOUD_STS_REGION should ok', async function () { + process.env.ALIBABA_CLOUD_STS_REGION = 'cn-hangzhou'; + const p = OIDCCredentialsProvider.builder() + .withRoleArn('roleArn') + .withOIDCProviderArn('oidcProviderArn') + .withOIDCTokenFilePath(path.join(__dirname, '../fixtures/OIDCToken.txt')) + .build(); + assert.strictEqual((p as any).stsEndpoint, 'sts.cn-hangzhou.aliyuncs.com'); + + delete process.env.ALIBABA_CLOUD_STS_REGION; + }); + + it('env ALIBABA_CLOUD_VPC_ENDPOINT_ENABLED should ok', async function () { + process.env.ALIBABA_CLOUD_VPC_ENDPOINT_ENABLED = "true"; + let p = OIDCCredentialsProvider.builder() + .withRoleArn('roleArn') + .withOIDCProviderArn('oidcProviderArn') + .withOIDCTokenFilePath(path.join(__dirname, '../fixtures/OIDCToken.txt')) + .build(); + assert.strictEqual((p as any).stsEndpoint, 'sts.aliyuncs.com'); + + p = OIDCCredentialsProvider.builder() + .withRoleArn('roleArn') + .withOIDCProviderArn('oidcProviderArn') + .withOIDCTokenFilePath(path.join(__dirname, '../fixtures/OIDCToken.txt')) + .withStsRegionId('cn-beijing') + .build(); + assert.strictEqual((p as any).stsEndpoint, 'sts-vpc.cn-beijing.aliyuncs.com'); + delete process.env.ALIBABA_CLOUD_VPC_ENDPOINT_ENABLED; + }); + }); diff --git a/test/providers/ram_role_arn.test.ts b/test/providers/ram_role_arn.test.ts index 14575cd..c402a29 100644 --- a/test/providers/ram_role_arn.test.ts +++ b/test/providers/ram_role_arn.test.ts @@ -276,4 +276,66 @@ describe('RAMRoleARNCredentialsProvider', function () { } }); + it('env ALIBABA_CLOUD_ROLE_ARN should ok', async function () { + const akProvider = StaticAKCredentialsProvider + .builder() + .withAccessKeyId('akid') + .withAccessKeySecret('aksecret') + .build(); + try { + RAMRoleARNCredentialsProvider.builder().withCredentialsProvider(akProvider).build(); + } catch (ex) { + assert.strictEqual(ex.message, 'the RoleArn is empty'); + } + process.env.ALIBABA_CLOUD_ROLE_ARN = "roleArn"; + let p = RAMRoleARNCredentialsProvider.builder() + .withCredentialsProvider(akProvider) + .build(); + assert.strictEqual((p as any).roleArn, "roleArn"); + delete process.env.ALIBABA_CLOUD_ROLE_ARN; + }); + + it('env ALIBABA_CLOUD_ROLE_SESSION_NAME should ok', async function () { + const akProvider = StaticAKCredentialsProvider + .builder() + .withAccessKeyId('akid') + .withAccessKeySecret('aksecret') + .build(); + + let p = RAMRoleARNCredentialsProvider.builder().withRoleArn("roleArn").withCredentialsProvider(akProvider).build(); + assert.ok((p as any).roleSessionName); + + process.env.ALIBABA_CLOUD_ROLE_SESSION_NAME = "sessionName"; + p = RAMRoleARNCredentialsProvider.builder().withRoleArn("roleArn").withCredentialsProvider(akProvider).build(); + assert.strictEqual((p as any).roleSessionName, 'sessionName'); + delete process.env.ALIBABA_CLOUD_ROLE_SESSION_NAME; + }); + + it('env ALIBABA_CLOUD_STS_REGION should ok', async function () { + const akProvider = StaticAKCredentialsProvider + .builder() + .withAccessKeyId('akid') + .withAccessKeySecret('aksecret') + .build(); + process.env.ALIBABA_CLOUD_STS_REGION = 'cn-hangzhou'; + let p = RAMRoleARNCredentialsProvider.builder().withRoleArn("roleArn").withCredentialsProvider(akProvider).build(); + assert.strictEqual((p as any).stsEndpoint, 'sts.cn-hangzhou.aliyuncs.com'); + + delete process.env.ALIBABA_CLOUD_STS_REGION; + }); + + it('env ALIBABA_CLOUD_VPC_ENDPOINT_ENABLED should ok', async function () { + const akProvider = StaticAKCredentialsProvider + .builder() + .withAccessKeyId('akid') + .withAccessKeySecret('aksecret') + .build(); + process.env.ALIBABA_CLOUD_VPC_ENDPOINT_ENABLED = "true"; + let p = RAMRoleARNCredentialsProvider.builder().withRoleArn("roleArn").withCredentialsProvider(akProvider).build(); + assert.strictEqual((p as any).stsEndpoint, 'sts.aliyuncs.com'); + + p = RAMRoleARNCredentialsProvider.builder().withRoleArn("roleArn").withCredentialsProvider(akProvider).withStsRegionId("cn-beijing").build(); + assert.strictEqual((p as any).stsEndpoint, 'sts-vpc.cn-beijing.aliyuncs.com'); + delete process.env.ALIBABA_CLOUD_VPC_ENDPOINT_ENABLED; + }); }); diff --git a/test/providers/uri.test.ts b/test/providers/uri.test.ts new file mode 100644 index 0000000..3adeda7 --- /dev/null +++ b/test/providers/uri.test.ts @@ -0,0 +1,114 @@ +import assert from 'assert'; +import URICredentialsProvider from '../../src/providers/uri'; +import { Response } from '../../src/providers/http'; + +describe('URICredentialsProvider', function () { + it('URICredentialsProvider', async function () { + let p = URICredentialsProvider.builder().build() + assert.ok(!(p as any).credentialsURI) + + p = URICredentialsProvider.builder() + .withCredentialsURI('http://127.0.0.1:9999') + .build() + assert.strictEqual((p as any).credentialsURI, 'http://127.0.0.1:9999'); + + assert.ok((p as any).needUpdateCredential()); + }); + + it('getCredentials() should ok', async function () { + const p = URICredentialsProvider.builder() + .withCredentialsURI('http://127.0.0.1:9999') + .withReadTimeout(1000) + .withConnectTimeout(1000) + .build(); + + const expiration = new Date(); + (p as any).doRequest = async function () { + return Response.builder() + .withStatusCode(200) + .withBody(Buffer.from(`{"AccessKeyId":"akid","AccessKeySecret":"aksecret","Expiration":"${expiration.toISOString()}","SecurityToken":"ststoken"}`)) + .build(); + }; + + let cc = await p.getCredentials() + assert.strictEqual(cc.accessKeyId, 'akid'); + assert.strictEqual(cc.accessKeySecret, 'aksecret'); + assert.strictEqual(cc.securityToken, 'ststoken') + assert.strictEqual(cc.providerName, 'credential_uri'); + assert.ok((p as any).needUpdateCredential() === true); + + // get credentials again + (p as any).expirationTimestamp = Date.now() / 1000 + 300; + cc = await p.getCredentials() + assert.strictEqual(cc.accessKeyId, 'akid'); + assert.strictEqual(cc.accessKeySecret, 'aksecret'); + assert.strictEqual(cc.securityToken, 'ststoken') + assert.strictEqual(cc.providerName, 'credential_uri'); + assert.ok((p as any).needUpdateCredential() === false); + }); + + it('getCredentials() with error', async function () { + const p = URICredentialsProvider.builder() + .withCredentialsURI('http://127.0.0.1:9999') + .withReadTimeout(1000) + .withConnectTimeout(1000) + .build(); + + try { + await p.getCredentials(); + assert.fail('should not to be here'); + } catch (ex) { + assert.ok(ex.message.includes(`ECONNREFUSED`)); + } + + (p as any).doRequest = async function () { + return Response.builder() + .withStatusCode(400) + .withBody(Buffer.from(`error`)) + .build(); + }; + + try { + await p.getCredentials(); + assert.fail('should not to be here'); + } catch (ex) { + assert.strictEqual(ex.message, 'get sts token failed, httpStatus: 400, message = error.'); + } + + (p as any).doRequest = async function () { + return Response.builder() + .withStatusCode(200) + .withBody(Buffer.from(`{"AccessKeyId":"akid"}`)) + .build(); + }; + + try { + await p.getCredentials(); + assert.fail('should not to be here'); + } catch (ex) { + assert.strictEqual(ex.message, 'error retrieving credentials from credentialsURI result: {"AccessKeyId":"akid"}.'); + } + + (p as any).doRequest = async function () { + return Response.builder() + .withStatusCode(200) + .withBody(Buffer.from(`error json`)) + .build(); + }; + + try { + await p.getCredentials(); + assert.fail('should not to be here'); + } catch (ex) { + assert.strictEqual(ex.message, 'get sts token failed, json parse failed: Unexpected token e in JSON at position 0, result: error json.'); + } + }); + + it('env ALIBABA_CLOUD_CREDENTIALS_URI should ok', async function () { + process.env.ALIBABA_CLOUD_CREDENTIALS_URI = 'http://127.0.0.1:9999'; + let p = URICredentialsProvider.builder().build(); + assert.strictEqual((p as any).credentialsURI, 'http://127.0.0.1:9999'); + + delete process.env.ALIBABA_CLOUD_CREDENTIALS_URI; + }); +});