Skip to content

Commit

Permalink
Add cli profile credentials provider
Browse files Browse the repository at this point in the history
  • Loading branch information
JacksonTian committed Sep 3, 2024
1 parent e7464df commit 6221e85
Show file tree
Hide file tree
Showing 10 changed files with 586 additions and 10 deletions.
2 changes: 1 addition & 1 deletion README-CN.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ Alibaba Cloud Credentials for TypeScript/Node.js 是帮助 Node.js 开发者管

## 要求

- 请确保你的系统安装了不低于 8.5.0 版本的 Node.js 环境。
- 请确保你的系统安装了不低于 12 版本的 Node.js 环境。

## 安装

Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ English | [简体中文](README-CN.md)
npm install @alicloud/credentials
```

**Node.js >= 8.5.0** required.
**Node.js >= 12** required.

## Quick Examples

Expand Down
178 changes: 178 additions & 0 deletions src/providers/cli_profile.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,178 @@
import { readFile } from 'fs';
import { promisify } from 'util';

import path from 'path';
import os from 'os';
import Credentials from '../credentials';
import CredentialsProvider from '../credentials_provider'
import StaticAKCredentialsProvider from './static_ak';
import RAMRoleARNCredentialsProvider from './ram_role_arn';
import OIDCRoleArnCredentialsProvider from './oidc_role_arn';
import ECSRAMRoleCredentialsProvider from './ecs_ram_role';

const readFileAsync = promisify(readFile);

class CLIProfileCredentialsProviderBuilder {
profileName: string;
build(): CLIProfileCredentialsProvider {
// 优先级:
// 1. 使用显示指定的 profileName
// 2. 使用环境变量(ALIBABA_CLOUD_PROFILE)制定的 profileName
// 3. 使用 CLI 配置中的当前 profileName
if (!this.profileName) {
this.profileName = process.env.ALIBABA_CLOUD_PROFILE;
}

if (process.env.ALIBABA_CLOUD_CLI_PROFILE_DISABLED === 'true') {
throw new Error('the CLI profile is disabled');
}

return new CLIProfileCredentialsProvider(this);
}

withProfileName(profileName: string) {
this.profileName = profileName;
return this;
}
}

interface Profile {
name: string;
mode: string;
access_key_id: string;
access_key_secret: string;
region_id: string;
ram_role_arn: string;
ram_session_name: string;
expired_seconds: number;
sts_region: string;
source_profile: string;
ram_role_name: string;
oidc_token_file: string;
oidc_provider_arn: string;
}

class Configuration {
current: string;
profiles: Profile[];
}

export async function getConfiguration(cfgPath: string): Promise<Configuration> {
let content: string;
try {
content = await readFileAsync(cfgPath, 'utf8');
} catch (ex) {
throw new Error(`reading aliyun cli config from '${cfgPath}' failed.`);
}
let conf: Configuration;
try {
conf = JSON.parse(content) as Configuration;
} catch (ex) {
throw new Error(`parse aliyun cli config from '${cfgPath}' failed: ${content}`);
}

if (!conf || !conf.profiles || conf.profiles.length === 0) {
throw new Error(`no any configured profiles in '${cfgPath}'`);
}
return conf;
}

export function getProfile(conf: Configuration, profileName: string): Profile {
for (const p of conf.profiles) {
if (p.name === profileName) {
return p;
}
}

throw new Error(`unable to get profile with '${profileName}'`);
}

export default class CLIProfileCredentialsProvider implements CredentialsProvider {
static builder(): CLIProfileCredentialsProviderBuilder {
return new CLIProfileCredentialsProviderBuilder();
}
private readonly profileName: string;
private innerProvider: CredentialsProvider;

// used for mock
private homedir: string = os.homedir();

constructor(builder: CLIProfileCredentialsProviderBuilder) {
this.profileName = builder.profileName;
}

private getCredentialsProvider(conf: Configuration, profileName: string): CredentialsProvider {
const p = getProfile(conf, profileName);
switch (p.mode) {
case 'AK':
return StaticAKCredentialsProvider.builder()
.withAccessKeyId(p.access_key_id)
.withAccessKeySecret(p.access_key_secret)
.build();
case 'RamRoleArn': {
const previousProvider = StaticAKCredentialsProvider.builder()
.withAccessKeyId(p.access_key_id)
.withAccessKeySecret(p.access_key_secret)
.build();

return RAMRoleARNCredentialsProvider.builder()
.withCredentialsProvider(previousProvider)
.withRoleArn(p.ram_role_arn)
.withRoleSessionName(p.ram_session_name)
.withDurationSeconds(p.expired_seconds)
.withStsRegionId(p.sts_region)
.build();
}
case 'EcsRamRole':
return ECSRAMRoleCredentialsProvider.builder().withRoleName(p.ram_role_name).build();
case 'OIDC':
return OIDCRoleArnCredentialsProvider.builder()
.withOIDCTokenFilePath(p.oidc_token_file)
.withOIDCProviderArn(p.oidc_provider_arn)
.withRoleArn(p.ram_role_arn)
.withStsRegionId(p.sts_region)
.withDurationSeconds(p.expired_seconds)
.withRoleSessionName(p.ram_session_name)
.build();
case 'ChainableRamRoleArn': {
const previousProvider = this.getCredentialsProvider(conf, p.source_profile);
return RAMRoleARNCredentialsProvider.builder()
.withCredentialsProvider(previousProvider)
.withRoleArn(p.ram_role_arn)
.withRoleSessionName(p.ram_session_name)
.withDurationSeconds(p.expired_seconds)
.withStsRegionId(p.sts_region)
.build();
}
default:
throw new Error(`unsupported profile mode '${p.mode}'`);
}
}

async getCredentials(): Promise<Credentials> {
if (!this.innerProvider) {
if (!this.homedir) {
throw new Error('cannot found home dir');
}

const cfgPath = path.join(this.homedir, '.aliyun/config.json');

const conf = await getConfiguration(cfgPath);
const profileName = this.profileName || conf.current;
this.innerProvider = this.getCredentialsProvider(conf, profileName)
}

const credentials = await this.innerProvider.getCredentials()
return Credentials.builder()
.withAccessKeyId(credentials.accessKeyId)
.withAccessKeySecret(credentials.accessKeySecret)
.withSecurityToken(credentials.securityToken)
.withProviderName(`${this.getProviderName()}/${this.innerProvider.getProviderName()}`)
.build();
}

getProviderName(): string {
return 'cli_profile';
}

}
76 changes: 68 additions & 8 deletions test/fixtures/.alibabacloud/credentials
Original file line number Diff line number Diff line change
@@ -1,13 +1,73 @@
[default]
enable = true
type = access_key
access_key_id = access-key-id-04
access_key_secret = access-key-secret-04
access_key_id = foo
access_key_secret = bar

[demo1]
enable = true
[notype]
access_key_id = foo
access_key_secret = bar

[noak]
type = access_key
access_key_secret = bar

[emptyak]
type = access_key
access_key_id =
access_key_secret = bar

[ecs]
type = ecs_ram_role
role_name = EcsRamRoleTest

[noecs]
type = ecs_ram_role

[emptyecs]
type = ecs_ram_role
role_name =

[ram]
type = ram_role_arn
access_key_id = access-key-id-05
access_key_secret = access-key-secret-05
role_arn = acs:ram::demo1:role/demo1
role_session_name = demo1
access_key_id = foo
access_key_secret = bar
role_arn = role_arn
role_session_name = session_name

[noram]
type = ram_role_arn
access_key_secret = bar
role_arn = role_arn
role_session_name = session_name

[emptyram]
type = ram_role_arn
access_key_id =
access_key_secret = bar
role_arn = role_arn
role_session_name = session_name

[rsa]
type = rsa_key_pair
public_key_id = publicKeyId
private_key_file = ./pk.pem

[norsa]
type = rsa_key_pair
public_key_id = publicKeyId

[emptyrsa]
type = rsa_key_pair
public_key_id = publicKeyId
private_key_file =

[error_rsa]
type = rsa_key_pair
public_key_id = publicKeyId
private_key_file = ./pk_error.pem

[error_type]
type = error_type
public_key_id = publicKeyId
private_key_file = ./pk_error.pem
51 changes: 51 additions & 0 deletions test/fixtures/.aliyun/config.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
{
"current": "AK",
"profiles": [
{
"name": "AK",
"mode": "AK",
"access_key_id": "akid",
"access_key_secret": "secret"
},
{
"name": "RamRoleArn",
"mode": "RamRoleArn",
"access_key_id": "akid",
"access_key_secret": "secret",
"ram_role_arn": "arn"
},
{
"name": "EcsRamRole",
"mode": "EcsRamRole",
"ram_role_name": "rolename"
},
{
"name": "OIDC",
"mode": "OIDC",
"ram_role_arn": "role_arn",
"oidc_token_file": "path/to/oidc/file",
"oidc_provider_arn": "provider_arn"
},
{
"name": "ChainableRamRoleArn",
"mode": "ChainableRamRoleArn",
"source_profile": "AK"
},
{
"name": "ChainableRamRoleArn2",
"mode": "ChainableRamRoleArn",
"source_profile": "InvalidSource"
},
{
"name": "get_credentials_error",
"mode": "RamRoleArn",
"access_key_id": "akid",
"access_key_secret": "secret",
"ram_role_arn": "arn"
},
{
"name": "Unsupported",
"mode": "Unsupported"
}
]
}
1 change: 1 addition & 0 deletions test/fixtures/invalid_cli_config.json
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
invalid config
17 changes: 17 additions & 0 deletions test/fixtures/mock_cli_config.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
{
"current": "default",
"profiles": [
{
"name": "default",
"mode": "AK",
"access_key_id": "akid",
"access_key_secret": "secret"
},
{
"name": "jacksontian",
"mode": "AK",
"access_key_id": "akid",
"access_key_secret": "secret"
}
]
}
1 change: 1 addition & 0 deletions test/fixtures/mock_empty_cli_config.json
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{}
1 change: 1 addition & 0 deletions test/fixtures/mock_oidctoken
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
mock oidc token
Loading

0 comments on commit 6221e85

Please sign in to comment.