Skip to content

Commit

Permalink
feat(credential-provider-node): add sso cred to default chain
Browse files Browse the repository at this point in the history
  • Loading branch information
AllanZhengYP committed Feb 19, 2021
1 parent 4f128c0 commit ca0c67d
Show file tree
Hide file tree
Showing 3 changed files with 113 additions and 19 deletions.
1 change: 1 addition & 0 deletions packages/credential-provider-node/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
"@aws-sdk/credential-provider-imds": "3.4.1",
"@aws-sdk/credential-provider-ini": "3.4.1",
"@aws-sdk/credential-provider-process": "3.4.1",
"@aws-sdk/credential-provider-sso": "3.0.0",
"@aws-sdk/property-provider": "3.4.1",
"@aws-sdk/shared-ini-file-loader": "3.4.1",
"@aws-sdk/types": "3.4.1",
Expand Down
122 changes: 105 additions & 17 deletions packages/credential-provider-node/src/index.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,14 @@ jest.mock("@aws-sdk/shared-ini-file-loader", () => ({
}));
import { loadSharedConfigFiles } from "@aws-sdk/shared-ini-file-loader";

jest.mock("@aws-sdk/credential-provider-sso", () => {
const ssoProvider = jest.fn();
return {
fromSSO: jest.fn().mockReturnValue(ssoProvider),
};
});
import { fromSSO, FromSSOInit } from "@aws-sdk/credential-provider-sso";

jest.mock("@aws-sdk/credential-provider-ini", () => {
const iniProvider = jest.fn();
return {
Expand Down Expand Up @@ -81,11 +89,13 @@ beforeEach(() => {
});

(fromEnv() as any).mockClear();
(fromSSO() as any).mockClear();
(fromIni() as any).mockClear();
(fromProcess() as any).mockClear();
(fromContainerMetadata() as any).mockClear();
(fromInstanceMetadata() as any).mockClear();
(fromEnv as any).mockClear();
(fromSSO as any).mockClear();
(fromIni as any).mockClear();
(fromProcess as any).mockClear();
(fromContainerMetadata as any).mockClear();
Expand Down Expand Up @@ -120,17 +130,37 @@ describe("defaultProvider", () => {
expect((fromInstanceMetadata() as any).mock.calls.length).toBe(0);
});

it("should stop after the SSO provider if credentials have been found", async () => {
const creds = {
accessKeyId: "foo",
secretAccessKey: "bar",
};

(fromEnv() as any).mockImplementation(() => Promise.reject(new ProviderError("Nothing here!")));
(fromSSO() as any).mockImplementation(() => Promise.resolve(creds));

expect(await defaultProvider()()).toEqual(creds);
expect((fromEnv() as any).mock.calls.length).toBe(1);
expect((fromSSO() as any).mock.calls.length).toBe(1);
expect((fromIni() as any).mock.calls.length).toBe(0);
expect((fromProcess() as any).mock.calls.length).toBe(0);
expect((fromContainerMetadata() as any).mock.calls.length).toBe(0);
expect((fromInstanceMetadata() as any).mock.calls.length).toBe(0);
});

it("should stop after the ini provider if credentials have been found", async () => {
const creds = {
accessKeyId: "foo",
secretAccessKey: "bar",
};

(fromEnv() as any).mockImplementation(() => Promise.reject(new ProviderError("Nothing here!")));
(fromSSO() as any).mockImplementation(() => Promise.reject(new ProviderError("Nothing here!")));
(fromIni() as any).mockImplementation(() => Promise.resolve(creds));

expect(await defaultProvider()()).toEqual(creds);
expect((fromEnv() as any).mock.calls.length).toBe(1);
expect((fromSSO() as any).mock.calls.length).toBe(1);
expect((fromIni() as any).mock.calls.length).toBe(1);
expect((fromProcess() as any).mock.calls.length).toBe(0);
expect((fromContainerMetadata() as any).mock.calls.length).toBe(0);
Expand All @@ -144,11 +174,13 @@ describe("defaultProvider", () => {
};

(fromEnv() as any).mockImplementation(() => Promise.reject(new ProviderError("Nothing here!")));
(fromSSO() as any).mockImplementation(() => Promise.reject(new ProviderError("Nothing here!")));
(fromIni() as any).mockImplementation(() => Promise.reject(new ProviderError("Nothing here!")));
(fromProcess() as any).mockImplementation(() => Promise.resolve(creds));

expect(await defaultProvider()()).toEqual(creds);
expect((fromEnv() as any).mock.calls.length).toBe(1);
expect((fromSSO() as any).mock.calls.length).toBe(1);
expect((fromIni() as any).mock.calls.length).toBe(1);
expect((fromProcess() as any).mock.calls.length).toBe(1);
expect((fromContainerMetadata() as any).mock.calls.length).toBe(0);
Expand All @@ -161,12 +193,14 @@ describe("defaultProvider", () => {
secretAccessKey: "bar",
};
(fromEnv() as any).mockImplementation(() => Promise.reject(new ProviderError("Keep moving!")));
(fromSSO() as any).mockImplementation(() => Promise.reject(new ProviderError("Nope!")));
(fromIni() as any).mockImplementation(() => Promise.reject(new ProviderError("Nothing here!")));
(fromProcess() as any).mockImplementation(() => Promise.reject(new ProviderError("Nor here!")));
(fromInstanceMetadata() as any).mockImplementation(() => Promise.resolve(creds));

expect(await defaultProvider()()).toEqual(creds);
expect((fromEnv() as any).mock.calls.length).toBe(1);
expect((fromSSO() as any).mock.calls.length).toBe(1);
expect((fromIni() as any).mock.calls.length).toBe(1);
expect((fromProcess() as any).mock.calls.length).toBe(1);
expect((fromContainerMetadata() as any).mock.calls.length).toBe(0);
Expand All @@ -180,6 +214,7 @@ describe("defaultProvider", () => {
};

(fromEnv() as any).mockImplementation(() => Promise.reject(new ProviderError("Keep moving!")));
(fromSSO() as any).mockImplementation(() => Promise.reject(new ProviderError("Nope!")));
(fromIni() as any).mockImplementation(() => Promise.reject(new ProviderError("Nothing here!")));
(fromProcess() as any).mockImplementation(() => Promise.reject(new ProviderError("Nor here!")));
(fromInstanceMetadata() as any).mockImplementation(() => Promise.resolve(creds));
Expand All @@ -198,6 +233,7 @@ describe("defaultProvider", () => {
};

(fromEnv() as any).mockImplementation(() => Promise.reject(new ProviderError("Keep moving!")));
(fromSSO() as any).mockImplementation(() => Promise.reject(new ProviderError("Nope!")));
(fromIni() as any).mockImplementation(() => Promise.reject(new ProviderError("Nothing here!")));
(fromProcess() as any).mockImplementation(() => Promise.reject(new ProviderError("Nor here!")));
(fromInstanceMetadata() as any).mockImplementation(() => Promise.reject(new Error("PANIC")));
Expand All @@ -207,6 +243,7 @@ describe("defaultProvider", () => {

expect(await defaultProvider()()).toEqual(creds);
expect((fromEnv() as any).mock.calls.length).toBe(1);
expect((fromSSO() as any).mock.calls.length).toBe(1);
expect((fromIni() as any).mock.calls.length).toBe(1);
expect((fromProcess() as any).mock.calls.length).toBe(1);
expect((fromContainerMetadata() as any).mock.calls.length).toBe(1);
Expand All @@ -220,16 +257,41 @@ describe("defaultProvider", () => {
};

(fromEnv() as any).mockImplementation(() => Promise.reject(new ProviderError("Keep moving!")));
(fromSSO() as any).mockImplementation(() => Promise.reject(new ProviderError("Nope!")));
(fromIni() as any).mockImplementation(() => Promise.reject(new ProviderError("Nothing here!")));
(fromProcess() as any).mockImplementation(() => Promise.reject(new ProviderError("Nor here!")));
(fromInstanceMetadata() as any).mockImplementation(() => Promise.resolve(creds));

await expect(defaultProvider()()).resolves;
expect((loadSharedConfigFiles as any).mock.calls.length).toBe(1);
expect((fromIni as any).mock.calls[1][0]).toMatchObject({ loadedConfig: loadSharedConfigFiles() });
expect((fromSSO as any).mock.calls[1][0]).toMatchObject({ loadedConfig: loadSharedConfigFiles() });
expect((fromProcess as any).mock.calls[1][0]).toMatchObject({ loadedConfig: loadSharedConfigFiles() });
});

it("should pass configuration on to the SSO provider", async () => {
const ssoConfig: FromSSOInit = {
profile: "foo",
filepath: "/home/user/.secrets/credentials.ini",
configFilepath: "/home/user/.secrets/credentials.ini",
};

(fromEnv() as any).mockImplementation(() => Promise.reject(new ProviderError("Keep moving!")));
(fromSSO() as any).mockImplementation(() =>
Promise.resolve({
accessKeyId: "foo",
secretAccessKey: "bar",
})
);

(fromSSO as any).mockClear();

await expect(defaultProvider(ssoConfig)()).resolves;

expect((fromSSO as any).mock.calls.length).toBe(1);
expect((fromSSO as any).mock.calls[0][0]).toEqual({ ...ssoConfig, loadedConfig });
});

it("should pass configuration on to the ini provider", async () => {
const iniConfig: FromIniInit = {
profile: "foo",
Expand Down Expand Up @@ -387,60 +449,86 @@ describe("defaultProvider", () => {

// CF https://github.com/boto/botocore/blob/1.8.32/botocore/credentials.py#L104
describe("explicit profiles", () => {
it("should only consult the ini provider if a profile has been specified", async () => {
it("should only consult SSO provider if profile has been set", async () => {
const creds = {
accessKeyId: "foo",
secretAccessKey: "bar",
};

(fromEnv() as any).mockImplementation(() => Promise.reject(new Error("PANIC")));
(fromIni() as any).mockImplementation(() => Promise.resolve(creds));
(fromInstanceMetadata() as any).mockImplementation(() => Promise.reject(new Error("PANIC")));
(fromContainerMetadata() as any).mockImplementation(() => Promise.reject(new Error("PANIC")));
(fromEnv() as any).mockImplementation(() => Promise.reject(new ProviderError("PANIC")));
(fromSSO() as any).mockImplementation(() => Promise.resolve(Promise.resolve(creds)));
(fromIni() as any).mockImplementation(() => Promise.reject(new ProviderError("PANIC")));
(fromInstanceMetadata() as any).mockImplementation(() => Promise.reject(new ProviderError("PANIC")));
(fromContainerMetadata() as any).mockImplementation(() => Promise.reject(new ProviderError("PANIC")));

expect(await defaultProvider({ profile: "foo" })()).toEqual(creds);
expect((fromEnv() as any).mock.calls.length).toBe(0);
expect((fromIni() as any).mock.calls.length).toBe(1);
expect((fromSSO() as any).mock.calls.length).toBe(1);
expect((fromIni() as any).mock.calls.length).toBe(0);
expect((fromContainerMetadata() as any).mock.calls.length).toBe(0);
expect((fromInstanceMetadata() as any).mock.calls.length).toBe(0);
});

it("should only consult the ini provider if the profile environment variable has been set", async () => {
it("should on consult SSO provider if the profile environment variable has been set", async () => {
const creds = {
accessKeyId: "foo",
secretAccessKey: "bar",
};

(fromEnv() as any).mockImplementation(() => Promise.reject(new Error("PANIC")));
(fromIni() as any).mockImplementation(() => Promise.resolve(creds));
(fromProcess() as any).mockImplementation(() => Promise.reject(new Error("PANIC")));
(fromInstanceMetadata() as any).mockImplementation(() => Promise.reject(new Error("PANIC")));
(fromContainerMetadata() as any).mockImplementation(() => Promise.reject(new Error("PANIC")));
(fromEnv() as any).mockImplementation(() => Promise.reject(new ProviderError("PANIC")));
(fromSSO() as any).mockImplementation(() => Promise.resolve(creds));
(fromIni() as any).mockImplementation(() => Promise.reject(new ProviderError("PANIC")));
(fromProcess() as any).mockImplementation(() => Promise.reject(new ProviderError("PANIC")));
(fromInstanceMetadata() as any).mockImplementation(() => Promise.reject(new ProviderError("PANIC")));
(fromContainerMetadata() as any).mockImplementation(() => Promise.reject(new ProviderError("PANIC")));

process.env[ENV_PROFILE] = "foo";
expect(await defaultProvider()()).toEqual(creds);
expect((fromEnv() as any).mock.calls.length).toBe(0);
expect((fromIni() as any).mock.calls.length).toBe(1);
expect((fromSSO() as any).mock.calls.length).toBe(1);
expect((fromIni() as any).mock.calls.length).toBe(0);
expect((fromProcess() as any).mock.calls.length).toBe(0);
expect((fromContainerMetadata() as any).mock.calls.length).toBe(0);
expect((fromInstanceMetadata() as any).mock.calls.length).toBe(0);
});

it("should consult ini provider if no credentials is not found in SSO provider", async () => {
const creds = {
accessKeyId: "foo",
secretAccessKey: "bar",
};

(fromEnv() as any).mockImplementation(() => Promise.reject(new ProviderError("PANIC")));
(fromSSO() as any).mockImplementation(() => Promise.reject(new ProviderError("PANIC")));
(fromIni() as any).mockImplementation(() => Promise.resolve(Promise.resolve(creds)));
(fromInstanceMetadata() as any).mockImplementation(() => Promise.reject(new ProviderError("PANIC")));
(fromContainerMetadata() as any).mockImplementation(() => Promise.reject(new ProviderError("PANIC")));

expect(await defaultProvider({ profile: "foo" })()).toEqual(creds);
expect((fromEnv() as any).mock.calls.length).toBe(0);
expect((fromSSO() as any).mock.calls.length).toBe(1);
expect((fromIni() as any).mock.calls.length).toBe(1);
expect((fromContainerMetadata() as any).mock.calls.length).toBe(0);
expect((fromInstanceMetadata() as any).mock.calls.length).toBe(0);
});

it("should consult the process provider if no credentials are found in the ini provider", async () => {
const creds = {
accessKeyId: "foo",
secretAccessKey: "bar",
};

(fromEnv() as any).mockImplementation(() => Promise.reject(new Error("PANIC")));
(fromIni() as any).mockImplementation(() => Promise.reject(new ProviderError("Nothing here!")));
(fromEnv() as any).mockImplementation(() => Promise.reject(new ProviderError("PANIC")));
(fromSSO() as any).mockImplementation(() => Promise.reject(new ProviderError("PANIC")));
(fromIni() as any).mockImplementation(() => Promise.reject(new ProviderError("PANIC")));
(fromProcess() as any).mockImplementation(() => Promise.resolve(creds));
(fromInstanceMetadata() as any).mockImplementation(() => Promise.reject(new Error("PANIC")));
(fromContainerMetadata() as any).mockImplementation(() => Promise.reject(new Error("PANIC")));
(fromInstanceMetadata() as any).mockImplementation(() => Promise.reject(new ProviderError("PANIC")));
(fromContainerMetadata() as any).mockImplementation(() => Promise.reject(new ProviderError("PANIC")));

process.env[ENV_PROFILE] = "foo";
expect(await defaultProvider()()).toEqual(creds);
expect((fromEnv() as any).mock.calls.length).toBe(0);
expect((fromSSO() as any).mock.calls.length).toBe(1);
expect((fromIni() as any).mock.calls.length).toBe(1);
expect((fromProcess() as any).mock.calls.length).toBe(1);
expect((fromContainerMetadata() as any).mock.calls.length).toBe(0);
Expand Down
9 changes: 7 additions & 2 deletions packages/credential-provider-node/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
} from "@aws-sdk/credential-provider-imds";
import { ENV_PROFILE, 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 { chain, memoize, ProviderError } from "@aws-sdk/property-provider";
import { loadSharedConfigFiles } from "@aws-sdk/shared-ini-file-loader";
import { CredentialProvider } from "@aws-sdk/types";
Expand All @@ -33,6 +34,8 @@ export const ENV_IMDS_DISABLED = "AWS_EC2_METADATA_DISABLED";
*
* @see fromEnv The function used to source credentials from
* environment variables
* @see fromSSO The function used to source credentials from
* resolved SSO token cache
* @see fromIni The function used to source credentials from INI
* files
* @see fromProcess The function used to sources credentials from
Expand All @@ -42,10 +45,12 @@ export const ENV_IMDS_DISABLED = "AWS_EC2_METADATA_DISABLED";
* @see fromContainerMetadata The function used to source credentials from the
* ECS Container Metadata Service
*/
export const defaultProvider = (init: FromIniInit & RemoteProviderInit & FromProcessInit = {}): CredentialProvider => {
export const defaultProvider = (
init: FromIniInit & RemoteProviderInit & FromProcessInit & FromSSOInit = {}
): CredentialProvider => {
const options = { profile: process.env[ENV_PROFILE], ...init };
if (!options.loadedConfig) options.loadedConfig = loadSharedConfigFiles(init);
const providers = [fromIni(options), fromProcess(options), remoteProvider(options)];
const providers = [fromSSO(options), fromIni(options), fromProcess(options), remoteProvider(options)];
if (!options.profile) providers.unshift(fromEnv());
const providerChain = chain(...providers);

Expand Down

0 comments on commit ca0c67d

Please sign in to comment.