Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(credential-provider-ini): support credential_source in shared file #2237

Merged
merged 3 commits into from
Apr 15, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 25 additions & 1 deletion packages/credential-provider-ini/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ aws_access_key_id=foo4
aws_secret_access_key=bar4
```

### source profile with static credentials
### profile with source profile

```ini
[second]
Expand All @@ -94,6 +94,30 @@ source_profile=first
role_arn=arn:aws:iam::123456789012:role/example-role-arn
```

### profile with source provider

You can supply `credential_source` options to tell the SDK where to source
credentials for the call to `AssumeRole`. The supported credential providers are
listed bellow:

```ini
[default]
role_arn=arn:aws:iam::123456789012:role/example-role-arn
credential_source = Ec2InstanceMetadata
```

```ini
[default]
role_arn=arn:aws:iam::123456789012:role/example-role-arn
credential_source = Environment
```

```ini
[default]
role_arn=arn:aws:iam::123456789012:role/example-role-arn
credential_source = EcsContainer
```

### profile with web_identity_token_file

```ini
Expand Down
2 changes: 2 additions & 0 deletions packages/credential-provider-ini/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@
},
"license": "Apache-2.0",
"dependencies": {
"@aws-sdk/credential-provider-env": "3.12.0",
"@aws-sdk/credential-provider-imds": "3.12.0",
"@aws-sdk/credential-provider-web-identity": "3.12.0",
"@aws-sdk/property-provider": "3.12.0",
"@aws-sdk/shared-ini-file-loader": "3.12.0",
Expand Down
131 changes: 131 additions & 0 deletions packages/credential-provider-ini/src/index.spec.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { fromEnv } from "@aws-sdk/credential-provider-env";
import { fromContainerMetadata, fromInstanceMetadata } from "@aws-sdk/credential-provider-imds";
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";
Expand Down Expand Up @@ -54,6 +56,10 @@ import { homedir } from "os";

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

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

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

const DEFAULT_CREDS = {
accessKeyId: "AKIAIOSFODNN7EXAMPLE",
secretAccessKey: "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY",
Expand Down Expand Up @@ -752,6 +758,131 @@ source_profile = default`.trim()
tryNextLink: false,
});
});

describe("assume role with source credential providers", () => {
const setUpTest = (credentialSource: string) => {
const roleArn = `arn:aws:iam::123456789:role/${credentialSource}`;
const roleSessionName = `${credentialSource}SessionName`;
const mfaSerial = `mfaSerial${credentialSource}`;
const mfaCode = Date.now().toString(10);
__addMatcher(
join(homedir(), ".aws", "credentials"),
`
[default]
role_arn = ${roleArn}
role_session_name = ${roleSessionName}
mfa_serial = ${mfaSerial}
credential_source = ${credentialSource}
`.trim()
);
return {
roleArn,
roleSessionName,
mfaSerial,
mfaCode,
};
};

it("should assume role from source credentials from EC2 instance provider", async () => {
(fromInstanceMetadata as jest.Mock).mockReturnValueOnce(() => Promise.resolve(FOO_CREDS));
const { roleArn, roleSessionName, mfaCode, mfaSerial } = setUpTest("Ec2InstanceMetadata");
const provider = fromIni({
mfaCodeProvider(mfa) {
expect(mfa).toBe(mfaSerial);
return Promise.resolve(mfaCode);
},
roleAssumer(sourceCreds: Credentials, params: AssumeRoleParams): Promise<Credentials> {
expect(fromInstanceMetadata as jest.Mock).toBeCalledTimes(1);
expect(params.RoleSessionName).toBe(roleSessionName);
expect(params.RoleArn).toBe(roleArn);
expect(params.TokenCode).toBe(mfaCode);
expect(sourceCreds).toEqual(FOO_CREDS);
return Promise.resolve(FIZZ_CREDS);
},
});
expect(await provider()).toEqual(FIZZ_CREDS);
});

it("should assume role from source credentials from environmental variable provider", async () => {
(fromEnv as jest.Mock).mockReturnValueOnce(() => Promise.resolve(FOO_CREDS));
const { roleArn, roleSessionName, mfaCode, mfaSerial } = setUpTest("Environment");
const provider = fromIni({
mfaCodeProvider(mfa) {
expect(mfa).toBe(mfaSerial);
return Promise.resolve(mfaCode);
},
roleAssumer(sourceCreds: Credentials, params: AssumeRoleParams): Promise<Credentials> {
expect(fromEnv as jest.Mock).toBeCalledTimes(1);
expect(params.RoleSessionName).toBe(roleSessionName);
expect(params.RoleArn).toBe(roleArn);
expect(params.TokenCode).toBe(mfaCode);
expect(sourceCreds).toEqual(FOO_CREDS);
return Promise.resolve(FIZZ_CREDS);
},
});
expect(await provider()).toEqual(FIZZ_CREDS);
});

it("should assume role from source credentials from ECS container provider", async () => {
(fromContainerMetadata as jest.Mock).mockReturnValueOnce(() => Promise.resolve(FOO_CREDS));
const { roleArn, roleSessionName, mfaCode, mfaSerial } = setUpTest("EcsContainer");
const provider = fromIni({
mfaCodeProvider(mfa) {
expect(mfa).toBe(mfaSerial);
return Promise.resolve(mfaCode);
},
roleAssumer(sourceCreds: Credentials, params: AssumeRoleParams): Promise<Credentials> {
expect(fromContainerMetadata as jest.Mock).toBeCalledTimes(1);
expect(params.RoleSessionName).toBe(roleSessionName);
expect(params.RoleArn).toBe(roleArn);
expect(params.TokenCode).toBe(mfaCode);
expect(sourceCreds).toEqual(FOO_CREDS);
return Promise.resolve(FIZZ_CREDS);
},
});
expect(await provider()).toEqual(FIZZ_CREDS);
});

it("should throw if source credentials provider is not supported", () => {
const someProvider = "SomeProvider";
setUpTest(someProvider);
const provider = fromIni({
roleAssumer(): Promise<Credentials> {
return Promise.resolve(FIZZ_CREDS);
},
});
return expect(async () => await provider()).rejects.toMatchObject({
message:
`Unsupported credential source in profile default. Got ${someProvider}, expected EcsContainer or ` +
`Ec2InstanceMetadata or Environment.`,
});
});

it("should throw if both source profile and credential source is specified", async () => {
__addMatcher(
join(homedir(), ".aws", "credentials"),
`
[profile A]
aws_access_key_id = abc123
aws_secret_access_key = def456
[default]
role_arn = arn:aws:iam::123456789:role/Role
credential_source = Ec2InstanceMetadata
source_profile = A
`.trim()
);
try {
await fromIni({
roleAssumer(): Promise<Credentials> {
return Promise.resolve(FIZZ_CREDS);
},
})();
fail("Expected error to be thrown");
} catch (e) {
expect(e).toBeDefined();
}
});
});
});

describe("assume role with web identity", () => {
Expand Down
58 changes: 49 additions & 9 deletions packages/credential-provider-ini/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { fromEnv } from "@aws-sdk/credential-provider-env";
import { fromContainerMetadata, fromInstanceMetadata } from "@aws-sdk/credential-provider-imds";
import { AssumeRoleWithWebIdentityParams, fromTokenFile } from "@aws-sdk/credential-provider-web-identity";
import { ProviderError } from "@aws-sdk/property-provider";
import {
Expand Down Expand Up @@ -115,20 +117,31 @@ const isWebIdentityProfile = (arg: any): arg is WebIdentityProfile =>
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 {

interface AssumeRoleWithSourceProfile extends Profile {
role_arn: string;
source_profile: string;
}

const isAssumeRoleWithSourceProfile = (arg: any): arg is AssumeRoleProfile =>
interface AssumeRoleWithProviderProfile extends Profile {
role_arn: string;
credential_source: string;
}

const isAssumeRoleProfile = (arg: any) =>
Boolean(arg) &&
typeof arg === "object" &&
typeof arg.role_arn === "string" &&
typeof arg.source_profile === "string" &&
["undefined", "string"].indexOf(typeof arg.role_session_name) > -1 &&
["undefined", "string"].indexOf(typeof arg.external_id) > -1 &&
["undefined", "string"].indexOf(typeof arg.mfa_serial) > -1;

const isAssumeRoleWithSourceProfile = (arg: any): arg is AssumeRoleWithSourceProfile =>
isAssumeRoleProfile(arg) && typeof arg.source_profile === "string" && typeof arg.credential_source === "undefined";

const isAssumeRoleWithProviderProfile = (arg: any): arg is AssumeRoleWithProviderProfile =>
isAssumeRoleProfile(arg) && typeof arg.credential_source === "string" && typeof arg.source_profile === "undefined";

/**
* Creates a credential provider that will read from ini files and supports
* role assumption and multi-factor authentication.
Expand Down Expand Up @@ -177,13 +190,14 @@ const resolveProfileData = async (

// If this is the first profile visited, role assumption keys should be
// given precedence over static credentials.
if (isAssumeRoleWithSourceProfile(data)) {
if (isAssumeRoleWithSourceProfile(data) || isAssumeRoleWithProviderProfile(data)) {
const {
external_id: ExternalId,
mfa_serial,
role_arn: RoleArn,
role_session_name: RoleSessionName = "aws-sdk-js-" + Date.now(),
source_profile,
credential_source,
} = data;

if (!options.roleAssumer) {
Expand All @@ -193,7 +207,7 @@ const resolveProfileData = async (
);
}

if (source_profile in visitedProfiles) {
if (source_profile && source_profile in visitedProfiles) {
throw new ProviderError(
`Detected a cycle attempting to resolve credentials for profile` +
` ${getMasterProfileName(options)}. Profiles visited: ` +
Expand All @@ -202,10 +216,13 @@ const resolveProfileData = async (
);
}

const sourceCreds = resolveProfileData(source_profile, profiles, options, {
...visitedProfiles,
[source_profile]: true,
});
const sourceCreds = source_profile
? resolveProfileData(source_profile, profiles, options, {
...visitedProfiles,
[source_profile]: true,
})
: resolveCredentialSource(credential_source!, profileName)();

const params: AssumeRoleParams = { RoleArn, RoleSessionName, ExternalId };
if (mfa_serial) {
if (!options.mfaCodeProvider) {
Expand Down Expand Up @@ -241,6 +258,29 @@ const resolveProfileData = async (
throw new ProviderError(`Profile ${profileName} could not be found or parsed in shared` + ` credentials file.`);
};

/**
* Resolve the `credential_source` entry from the profile, and return the
* credential providers respectively. No memoization is needed for the
* credential source providers because memoization should be added outside the
* fromIni() provider. The source credential needs to be refreshed every time
* fromIni() is called.
*/
const resolveCredentialSource = (credentialSource: string, profileName: string): CredentialProvider => {
const sourceProvidersMap: { [name: string]: () => CredentialProvider } = {
EcsContainer: fromContainerMetadata,
Ec2InstanceMetadata: fromInstanceMetadata,
Environment: fromEnv,
};
if (credentialSource in sourceProvidersMap) {
return sourceProvidersMap[credentialSource]();
} else {
throw new ProviderError(
`Unsupported credential source in profile ${profileName}. Got ${credentialSource}, ` +
`expected EcsContainer or Ec2InstanceMetadata or Environment.`
);
}
};

const resolveStaticCredentials = (profile: StaticCredsProfile): Promise<Credentials> =>
Promise.resolve({
accessKeyId: profile.aws_access_key_id,
Expand Down