Skip to content

Commit

Permalink
feat(credential-provider-ini): call fromTokenFile in assumeRole chain…
Browse files Browse the repository at this point in the history
…ing (#2178)
  • Loading branch information
trivikr authored Mar 26, 2021
1 parent 95b0e19 commit fb95408
Show file tree
Hide file tree
Showing 4 changed files with 176 additions and 7 deletions.
34 changes: 29 additions & 5 deletions packages/credential-provider-ini/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,13 @@

## AWS Credential Provider for Node.JS - Shared Configuration Files

This module provides a function, `fromSharedConfigFiles` that will create
This module provides a function, `fromIni` that will create
`CredentialProvider` functions that read from a shared credentials file at
`~/.aws/credentials` and a shared configuration file at `~/.aws/config`. Both
files are expected to be INI formatted with section names corresponding to
profiles. Sections in the credentials file are treated as profile names, whereas
profile sections in the config file must have the format of`[profile profile-name]`, except for the default profile. Please see the [sample
profile sections in the config file must have the format of`[profile profile-name]`,
except for the default profile. Please see the [sample
files](#sample-files) below for examples of well-formed configuration and
credentials files.

Expand All @@ -21,8 +22,7 @@ in the config file.
## Supported configuration

You may customize how credentials are resolved by providing an options hash to
the `fromSharedConfigFiles` factory function. The following options are
supported:
the `fromIni` factory function. The following options are supported:

- `profile` - The configuration profile to use. If not specified, the provider
will use the value in the `AWS_PROFILE` environment variable or a default of
Expand All @@ -38,7 +38,11 @@ supported:
code and `mfaCodeProvider` is not a valid function, the credential provider
promise will be rejected.
- `roleAssumer` - A function that assumes a role and returns a promise
fulfilled with credentials for the assumed role.
fulfilled with credentials for the assumed role. You may call `sts:assumeRole`
API within this function.
- `roleAssumerWithWebIdentity` - A function that assumes a role with web identity
and returns a promise fulfilled with credentials for the assumed role. You may call
`sts:assumeRoleWithWebIdentity` API within this function.

## Sample files

Expand Down Expand Up @@ -77,3 +81,23 @@ aws_secret_access_key=bar3
aws_access_key_id=foo4
aws_secret_access_key=bar4
```

### source profile with static credentials

```ini
[second]
aws_access_key_id=foo
aws_secret_access_key=bar

[first]
source_profile=first
role_arn=arn:aws:iam::123456789012:role/example-role-arn
```

### profile with web_identity_token_file

```ini
[default]
web_identity_token_file=/temp/token
role_arn=arn:aws:iam::123456789012:role/example-role-arn
```
1 change: 1 addition & 0 deletions packages/credential-provider-ini/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
},
"license": "Apache-2.0",
"dependencies": {
"@aws-sdk/credential-provider-web-identity": "3.0.0",
"@aws-sdk/property-provider": "3.8.0",
"@aws-sdk/shared-ini-file-loader": "3.8.0",
"@aws-sdk/types": "3.6.1",
Expand Down
108 changes: 108 additions & 0 deletions packages/credential-provider-ini/src/index.spec.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { fromTokenFile } from "@aws-sdk/credential-provider-web-identity";
import { ENV_CONFIG_PATH, ENV_CREDENTIALS_PATH } from "@aws-sdk/shared-ini-file-loader";
import { Credentials } from "@aws-sdk/types";
import { join, sep } from "path";
Expand Down Expand Up @@ -48,8 +49,11 @@ jest.mock("os", () => {

return os;
});

import { homedir } from "os";

jest.mock("@aws-sdk/credential-provider-web-identity");

const DEFAULT_CREDS = {
accessKeyId: "AKIAIOSFODNN7EXAMPLE",
secretAccessKey: "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY",
Expand Down Expand Up @@ -83,6 +87,7 @@ const envAtLoadTime: { [key: string]: string | undefined } = [

beforeEach(() => {
__clearMatchers();
jest.clearAllMocks();
Object.keys(envAtLoadTime).forEach((envKey) => {
delete process.env[envKey];
});
Expand Down Expand Up @@ -749,6 +754,109 @@ source_profile = default`.trim()
});
});

describe("assume role with web identity", () => {
it("should call fromTokenFile with data from profile", async () => {
(fromTokenFile as jest.Mock).mockReturnValueOnce(() => Promise.resolve(FOO_CREDS));
const webIdentityTokenFile = "/temp/foo/token";
const roleArn = "arn:aws:iam::123456789:role/bar";
const roleSessionName = "bazSession";
const roleAssumerWithWebIdentity = jest.fn();
__addMatcher(
join(homedir(), ".aws", "credentials"),
`
[foo]
web_identity_token_file = ${webIdentityTokenFile}
role_arn = ${roleArn}
role_session_name = ${roleSessionName}`.trim()
);

const provider = fromIni({
profile: "foo",
roleAssumerWithWebIdentity,
});

expect(await provider()).toEqual(FOO_CREDS);
expect(fromTokenFile).toHaveBeenCalledTimes(1);
expect(fromTokenFile).toHaveBeenCalledWith({
webIdentityTokenFile,
roleArn,
roleSessionName,
roleAssumerWithWebIdentity,
});
});

it("should call fromTokenFile with assume role chaining", async () => {
(fromTokenFile as jest.Mock).mockReturnValueOnce(() => Promise.resolve(DEFAULT_CREDS));
const webIdentityTokenFile = "/temp/foo/token";
const roleArn = "arn:aws:iam::123456789:role/bar";
const roleSessionName = "bazSession";
const roleAssumerWithWebIdentity = jest.fn();

const fooRoleArn = "arn:aws:iam::123456789:role/foo";
const fooSessionName = "fooSession";
__addMatcher(
join(homedir(), ".aws", "credentials"),
`
[bar]
web_identity_token_file = ${webIdentityTokenFile}
role_arn = ${roleArn}
role_session_name = ${roleSessionName}
[foo]
role_arn = ${fooRoleArn}
role_session_name = ${fooSessionName}
source_profile = bar`.trim()
);

const provider = fromIni({
profile: "foo",
roleAssumer(sourceCreds: Credentials, params: AssumeRoleParams): Promise<Credentials> {
expect(sourceCreds).toEqual(DEFAULT_CREDS);
expect(params.RoleArn).toEqual(fooRoleArn);
expect(params.RoleSessionName).toEqual(fooSessionName);
return Promise.resolve(FOO_CREDS);
},
roleAssumerWithWebIdentity,
});

expect(await provider()).toEqual(FOO_CREDS);
expect(fromTokenFile).toHaveBeenCalledTimes(1);
expect(fromTokenFile).toHaveBeenCalledWith({
webIdentityTokenFile,
roleArn,
roleSessionName,
roleAssumerWithWebIdentity,
});
});

it("should call fromTokenFile without roleSessionName if not present in profile", async () => {
(fromTokenFile as jest.Mock).mockReturnValueOnce(() => Promise.resolve(FOO_CREDS));
const webIdentityTokenFile = "/temp/foo/token";
const roleArn = "arn:aws:iam::123456789:role/bar";
const roleAssumerWithWebIdentity = jest.fn();
__addMatcher(
join(homedir(), ".aws", "credentials"),
`
[foo]
web_identity_token_file = ${webIdentityTokenFile}
role_arn = ${roleArn}`.trim()
);

const provider = fromIni({
profile: "foo",
roleAssumerWithWebIdentity,
});

expect(await provider()).toEqual(FOO_CREDS);
expect(fromTokenFile).toHaveBeenCalledTimes(1);
expect(fromTokenFile).toHaveBeenCalledWith({
webIdentityTokenFile,
roleArn,
roleAssumerWithWebIdentity,
});
});
});

it("should prefer credentials in ~/.aws/credentials to those in ~/.aws/config", async () => {
__addMatcher(
join(homedir(), ".aws", "credentials"),
Expand Down
40 changes: 38 additions & 2 deletions packages/credential-provider-ini/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { AssumeRoleWithWebIdentityParams, fromTokenFile } from "@aws-sdk/credential-provider-web-identity";
import { ProviderError } from "@aws-sdk/property-provider";
import {
loadSharedConfigFiles,
Expand Down Expand Up @@ -78,6 +79,15 @@ export interface FromIniInit extends SourceProfileInit {
* @param params
*/
roleAssumer?: (sourceCreds: Credentials, params: AssumeRoleParams) => Promise<Credentials>;

/**
* A function that assumes a role with web identity and returns a promise fulfilled with
* credentials for the assumed role.
*
* @param sourceCreds The credentials with which to assume a role.
* @param params
*/
roleAssumerWithWebIdentity?: (params: AssumeRoleWithWebIdentityParams) => Promise<Credentials>;
}

interface StaticCredsProfile extends Profile {
Expand All @@ -93,12 +103,24 @@ const isStaticCredsProfile = (arg: any): arg is StaticCredsProfile =>
typeof arg.aws_secret_access_key === "string" &&
["undefined", "string"].indexOf(typeof arg.aws_session_token) > -1;

interface WebIdentityProfile extends Profile {
web_identity_token_file: string;
role_arn: string;
role_session_name?: string;
}

const isWebIdentityProfile = (arg: any): arg is WebIdentityProfile =>
Boolean(arg) &&
typeof arg === "object" &&
typeof arg.web_identity_token_file === "string" &&
typeof arg.role_arn === "string" &&
["undefined", "string"].indexOf(typeof arg.role_session_name) > -1;
interface AssumeRoleProfile extends Profile {
role_arn: string;
source_profile: string;
}

const isAssumeRoleProfile = (arg: any): arg is AssumeRoleProfile =>
const isAssumeRoleWithSourceProfile = (arg: any): arg is AssumeRoleProfile =>
Boolean(arg) &&
typeof arg === "object" &&
typeof arg.role_arn === "string" &&
Expand Down Expand Up @@ -155,7 +177,7 @@ const resolveProfileData = async (

// If this is the first profile visited, role assumption keys should be
// given precedence over static credentials.
if (isAssumeRoleProfile(data)) {
if (isAssumeRoleWithSourceProfile(data)) {
const {
external_id: ExternalId,
mfa_serial,
Expand Down Expand Up @@ -205,6 +227,12 @@ const resolveProfileData = async (
return resolveStaticCredentials(data);
}

// If no static credentials are present, attempt to assume role with
// web identity if web_identity_token_file and role_arn is available
if (isWebIdentityProfile(data)) {
return resolveWebIdentityCredentials(data, options);
}

// If the profile cannot be parsed or contains neither static credentials
// nor role assumption metadata, throw an error. This should be considered a
// terminal resolution error if a profile has been specified by the user
Expand All @@ -219,3 +247,11 @@ const resolveStaticCredentials = (profile: StaticCredsProfile): Promise<Credenti
secretAccessKey: profile.aws_secret_access_key,
sessionToken: profile.aws_session_token,
});

const resolveWebIdentityCredentials = async (profile: WebIdentityProfile, options: FromIniInit): Promise<Credentials> =>
fromTokenFile({
webIdentityTokenFile: profile.web_identity_token_file,
roleArn: profile.role_arn,
roleSessionName: profile.role_session_name,
roleAssumerWithWebIdentity: options.roleAssumerWithWebIdentity,
})();

0 comments on commit fb95408

Please sign in to comment.