Skip to content

Commit

Permalink
feat(credential-provider-sso): support sso credential when resolving …
Browse files Browse the repository at this point in the history
…shared credential file (#2583)

* feat(util-credentials): move shared credential utils to util-credential package

* fix(credential-provider-sso): support sso credential in ini credential provider
  • Loading branch information
AllanZhengYP committed Jul 16, 2021
1 parent 160aeba commit 9480e70
Show file tree
Hide file tree
Showing 19 changed files with 829 additions and 219 deletions.
43 changes: 42 additions & 1 deletion packages/credential-provider-ini/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ aws_access_key_id=foo
aws_secret_access_key=bar

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

Expand Down Expand Up @@ -125,3 +125,44 @@ credential_source = EcsContainer
web_identity_token_file=/temp/token
role_arn=arn:aws:iam::123456789012:role/example-role-arn
```

You can specify another profile(`second`) whose credentials are used to assume
the role by the `role_arn` setting in this profile(`first`).

```ini
[second]
web_identity_token_file=/temp/token
role_arn=arn:aws:iam::123456789012:role/example-role-2

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

### profile with sso credentials

Please refer the the [`sso credential provider package`](https://www.npmjs.com/package/@aws-sdk/credential-provider-sso)
for how to configure the SSO credentials.

```ini
[default]
sso_account_id = 012345678901
sso_region = us-east-1
sso_role_name = SampleRole
sso_start_url = https://d-abc123.awsapps.com/start
```

You can specify another profile(`second`) whose credentials derived from SSO
are used to assume the role by the `role_arn` setting in this profile(`first`).

```ini
[second]
sso_account_id = 012345678901
sso_region = us-east-1
sso_role_name = example-role-2
sso_start_url = https://d-abc123.awsapps.com/start

[first]
source_profile=second
role_arn=arn:aws:iam::123456789012:role/example-role
```
2 changes: 2 additions & 0 deletions packages/credential-provider-ini/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,10 +23,12 @@
"dependencies": {
"@aws-sdk/credential-provider-env": "3.20.0",
"@aws-sdk/credential-provider-imds": "3.20.0",
"@aws-sdk/credential-provider-sso": "3.21.0",
"@aws-sdk/credential-provider-web-identity": "3.20.0",
"@aws-sdk/property-provider": "3.20.0",
"@aws-sdk/shared-ini-file-loader": "3.20.0",
"@aws-sdk/types": "3.20.0",
"@aws-sdk/util-credentials": "3.0.0",
"tslib": "^2.0.0"
},
"devDependencies": {
Expand Down
120 changes: 119 additions & 1 deletion packages/credential-provider-ini/src/index.spec.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
import { fromEnv } from "@aws-sdk/credential-provider-env";
import { fromContainerMetadata, fromInstanceMetadata } from "@aws-sdk/credential-provider-imds";
import { fromSSO, isSsoProfile, validateSsoProfile } from "@aws-sdk/credential-provider-sso";
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 { ENV_PROFILE } from "@aws-sdk/util-credentials";
import { join, sep } from "path";

import { AssumeRoleParams, ENV_PROFILE, fromIni } from "./";
import { AssumeRoleParams, fromIni } from "./";

jest.mock("fs", () => {
interface FsModule {
Expand Down Expand Up @@ -60,6 +62,8 @@ jest.mock("@aws-sdk/credential-provider-imds");

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

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

const DEFAULT_CREDS = {
accessKeyId: "AKIAIOSFODNN7EXAMPLE",
secretAccessKey: "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY",
Expand Down Expand Up @@ -988,6 +992,120 @@ role_arn = ${roleArn}`.trim()
});
});

describe("assume role with SSO", () => {
const DEFAULT_PATH = join(homedir(), ".aws", "credentials");
it("should continue if profile is not configured with an SSO credential", async () => {
__addMatcher(
DEFAULT_PATH,
`[default]
aws_access_key_id = ${DEFAULT_CREDS.accessKeyId}
aws_secret_access_key = ${DEFAULT_CREDS.secretAccessKey}
aws_session_token = ${DEFAULT_CREDS.sessionToken}
`.trim()
);
await fromIni()();
expect(fromSSO).not.toHaveBeenCalled();
});

it("should throw if profile is configured with incomplete SSO credential", async () => {
(isSsoProfile as unknown as jest.Mock).mockImplementationOnce(() => true);
const originalValidator = jest.requireActual("@aws-sdk/credential-provider-sso").validateSsoProfile;
(validateSsoProfile as unknown as jest.Mock).mockImplementationOnce(originalValidator);
__addMatcher(
DEFAULT_PATH,
`[default]
sso_account_id = 1234567890
sso_start_url = https://example.com/sso/
`.trim()
);
try {
await fromIni()();
} catch (e) {
console.error(e.message);
expect(e.message).toEqual(expect.stringContaining("Profile is configured with invalid SSO credentials"));
}
});

it("should resolve valid SSO credential", async () => {
(isSsoProfile as unknown as jest.Mock).mockImplementationOnce(() => true);
const originalValidator = jest.requireActual("@aws-sdk/credential-provider-sso").validateSsoProfile;
(validateSsoProfile as jest.Mock).mockImplementationOnce(originalValidator);
(fromSSO as jest.Mock).mockImplementationOnce(() => async () => DEFAULT_CREDS);
const accountId = "1234567890";
const startUrl = "https://example.com/sso/";
const region = "us-east-1";
const roleName = "role";
__addMatcher(
DEFAULT_PATH,
`[default]
sso_account_id = ${accountId}
sso_start_url = ${startUrl}
sso_region = ${region}
sso_role_name = ${roleName}
`.trim()
);
await fromIni()();
expect(fromSSO as unknown as jest.Mock).toBeCalledWith({
ssoAccountId: accountId,
ssoStartUrl: startUrl,
ssoRegion: region,
ssoRoleName: roleName,
});
});

it("should call fromTokenFile with assume role chaining", async () => {
(isSsoProfile as unknown as jest.Mock).mockImplementationOnce(
jest.requireActual("@aws-sdk/credential-provider-sso").isSsoProfile
);
(validateSsoProfile as unknown as jest.Mock).mockImplementationOnce(
jest.requireActual("@aws-sdk/credential-provider-sso").validateSsoProfile
);
(fromSSO as jest.Mock).mockImplementationOnce(() => async () => DEFAULT_CREDS);
const accountId = "1234567890";
const startUrl = "https://example.com/sso/";
const region = "us-east-1";
const roleName = "role";
const roleAssumerWithWebIdentity = jest.fn();

const fooRoleArn = "arn:aws:iam::123456789:role/foo";
const fooSessionName = "fooSession";
__addMatcher(
DEFAULT_PATH,
`
[bar]
sso_account_id = ${accountId}
sso_start_url = ${startUrl}
sso_region = ${region}
sso_role_name = ${roleName}
[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(fromSSO).toHaveBeenCalledTimes(1);
expect(fromSSO).toHaveBeenCalledWith({
ssoAccountId: accountId,
ssoStartUrl: startUrl,
ssoRegion: region,
ssoRoleName: roleName,
});
});
});

it("should prefer credentials in ~/.aws/credentials to those in ~/.aws/config", async () => {
__addMatcher(
join(homedir(), ".aws", "credentials"),
Expand Down
59 changes: 12 additions & 47 deletions packages/credential-provider-ini/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,11 @@
import { fromEnv } from "@aws-sdk/credential-provider-env";
import { fromContainerMetadata, fromInstanceMetadata } from "@aws-sdk/credential-provider-imds";
import { fromSSO, isSsoProfile, validateSsoProfile } from "@aws-sdk/credential-provider-sso";
import { AssumeRoleWithWebIdentityParams, fromTokenFile } from "@aws-sdk/credential-provider-web-identity";
import { CredentialsProviderError } from "@aws-sdk/property-provider";
import {
loadSharedConfigFiles,
ParsedIniData,
Profile,
SharedConfigFiles,
SharedConfigInit,
} from "@aws-sdk/shared-ini-file-loader";
import { ParsedIniData, Profile } from "@aws-sdk/shared-ini-file-loader";
import { CredentialProvider, Credentials } from "@aws-sdk/types";

const DEFAULT_PROFILE = "default";
export const ENV_PROFILE = "AWS_PROFILE";
import { getMasterProfileName, parseKnownFiles, SourceProfileInit } from "@aws-sdk/util-credentials";

/**
* @see http://docs.aws.amazon.com/AWSJavaScriptSDK/latest/AWS/STS.html#assumeRole-property
Expand Down Expand Up @@ -47,21 +40,6 @@ export interface AssumeRoleParams {
TokenCode?: string;
}

export interface SourceProfileInit extends SharedConfigInit {
/**
* The configuration profile to use.
*/
profile?: string;

/**
* A promise that will be resolved with loaded and parsed credentials files.
* Used to avoid loading shared config files multiple times.
*
* @internal
*/
loadedConfig?: Promise<SharedConfigFiles>;
}

export interface FromIniInit extends SourceProfileInit {
/**
* A function that returns a promise fulfilled with an MFA token code for
Expand Down Expand Up @@ -153,28 +131,6 @@ export const fromIni =
return resolveProfileData(getMasterProfileName(init), profiles, init);
};

/**
* Load profiles from credentials and config INI files and normalize them into a
* single profile list.
*
* @internal
*/
export const parseKnownFiles = async (init: SourceProfileInit): Promise<ParsedIniData> => {
const { loadedConfig = loadSharedConfigFiles(init) } = init;

const parsedFiles = await loadedConfig;
return {
...parsedFiles.configFile,
...parsedFiles.credentialsFile,
};
};

/**
* @internal
*/
export const getMasterProfileName = (init: { profile?: string }): string =>
init.profile || process.env[ENV_PROFILE] || DEFAULT_PROFILE;

const resolveProfileData = async (
profileName: string,
profiles: ParsedIniData,
Expand Down Expand Up @@ -251,6 +207,15 @@ const resolveProfileData = async (
if (isWebIdentityProfile(data)) {
return resolveWebIdentityCredentials(data, options);
}
if (isSsoProfile(data)) {
const { sso_start_url, sso_account_id, sso_region, sso_role_name } = validateSsoProfile(data);
return fromSSO({
ssoStartUrl: sso_start_url,
ssoAccountId: sso_account_id,
ssoRegion: sso_region,
ssoRoleName: sso_role_name,
})();
}

// If the profile cannot be parsed or contains neither static credentials
// nor role assumption metadata, throw an error. This should be considered a
Expand Down
3 changes: 2 additions & 1 deletion packages/credential-provider-node/src/index.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,9 @@ jest.mock("@aws-sdk/credential-provider-ini", () => {
fromIni: jest.fn().mockReturnValue(iniProvider),
};
});
import { ENV_PROFILE, fromIni, FromIniInit } from "@aws-sdk/credential-provider-ini";
import { fromIni, FromIniInit } from "@aws-sdk/credential-provider-ini";
import { ENV_CONFIG_PATH, ENV_CREDENTIALS_PATH } from "@aws-sdk/shared-ini-file-loader";
import { ENV_PROFILE } from "@aws-sdk/util-credentials";

jest.mock("@aws-sdk/credential-provider-process", () => {
const processProvider = jest.fn();
Expand Down
3 changes: 2 additions & 1 deletion packages/credential-provider-node/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,14 @@ import {
fromInstanceMetadata,
RemoteProviderInit,
} from "@aws-sdk/credential-provider-imds";
import { ENV_PROFILE, fromIni, FromIniInit } from "@aws-sdk/credential-provider-ini";
import { fromIni, FromIniInit } from "@aws-sdk/credential-provider-ini";
import { fromProcess, FromProcessInit } from "@aws-sdk/credential-provider-process";
import { fromSSO, FromSSOInit } from "@aws-sdk/credential-provider-sso";
import { fromTokenFile, FromTokenFileInit } from "@aws-sdk/credential-provider-web-identity";
import { chain, CredentialsProviderError, memoize } from "@aws-sdk/property-provider";
import { loadSharedConfigFiles } from "@aws-sdk/shared-ini-file-loader";
import { CredentialProvider } from "@aws-sdk/types";
import { ENV_PROFILE } from "@aws-sdk/util-credentials";

export const ENV_IMDS_DISABLED = "AWS_EC2_METADATA_DISABLED";

Expand Down
2 changes: 1 addition & 1 deletion packages/credential-provider-process/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,10 @@
},
"license": "Apache-2.0",
"dependencies": {
"@aws-sdk/credential-provider-ini": "3.20.0",
"@aws-sdk/property-provider": "3.20.0",
"@aws-sdk/shared-ini-file-loader": "3.20.0",
"@aws-sdk/types": "3.20.0",
"@aws-sdk/util-credentials": "3.0.0",
"tslib": "^2.0.0"
},
"devDependencies": {
Expand Down
2 changes: 1 addition & 1 deletion packages/credential-provider-process/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { getMasterProfileName, parseKnownFiles, SourceProfileInit } from "@aws-sdk/credential-provider-ini";
import { CredentialsProviderError } from "@aws-sdk/property-provider";
import { ParsedIniData } from "@aws-sdk/shared-ini-file-loader";
import { CredentialProvider, Credentials } from "@aws-sdk/types";
import { getMasterProfileName, parseKnownFiles, SourceProfileInit } from "@aws-sdk/util-credentials";
import { exec } from "child_process";

/**
Expand Down
27 changes: 22 additions & 5 deletions packages/credential-provider-sso/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,20 +6,37 @@
## AWS Credential Provider for Node.js - AWS Single Sign-On (SSO)

This module provides a function, `fromSSO`, that creates
`CredentialProvider` functions that read from [AWS SDKs and Tools
shared configuration and credentials
files](https://docs.aws.amazon.com/credref/latest/refdocs/creds-config-files.html).
Profiles in the `credentials` file are given precedence over
profiles in the `config` file. This provider loads the
`CredentialProvider` functions that read from the
_resolved_ access token from local disk then requests temporary AWS
credentials. For guidance on the AWS Single Sign-On service, please
refer to [AWS's Single Sign-On documentation](https://aws.amazon.com/single-sign-on/).

You can create the `CredentialProvider` functions using the inline SSO
parameters(`ssoStartUrl`, `ssoAccountId`, `ssoRegion`, `ssoRoleName`) or load
them from [AWS SDKs and Tools shared configuration and credentials files](https://docs.aws.amazon.com/credref/latest/refdocs/creds-config-files.html).
Profiles in the `credentials` file are given precedence over
profiles in the `config` file.

This credential provider is intended for use with the AWS SDK for Node.js.

This credential provider **ONLY** supports profiles using the SSO credential. If
you have a profile that assumes a role which derived from the SSO credential,
you should use the `@aws-sdk/credential-provider-ini`, or
`@aws-sdk/credential-provider-node` package.

## Supported configuration

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

- `ssoStartUrl`: The URL to the AWS SSO service. Required if any of the `sso*`
options(except for `ssoClient`) is provided.
- `ssoAccountId`: The ID of the AWS account to use for temporary credentials.
Required if any of the `sso*` options(except for `ssoClient`) is provided.
- `ssoRegion`: The AWS region to use for temporary credentials. Required if any
of the `sso*` options(except for `ssoClient`) is provided.
- `ssoRoleName`: The name of the AWS role to assume. Required if any of the
`sso*` options(except for `ssoClient`) is provided.
- `profile` - The configuration profile to use. If not specified, the provider
will use the value in the `AWS_PROFILE` environment variable or `default` by
default.
Expand Down
Loading

0 comments on commit 9480e70

Please sign in to comment.