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-sso): support resolving credentials from SSO token #2055

Merged
merged 7 commits into from
Feb 25, 2021
Merged
Show file tree
Hide file tree
Changes from 6 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
6 changes: 0 additions & 6 deletions clients/client-sso/SSOClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,6 @@ import {
import {
Provider,
RegionInfoProvider,
Credentials as __Credentials,
Decoder as __Decoder,
Encoder as __Encoder,
HashConstructor as __HashConstructor,
Expand Down Expand Up @@ -123,11 +122,6 @@ export interface ClientDefaults extends Partial<__SmithyResolvedConfiguration<__
*/
serviceId?: string;

/**
* Default credentials provider; Not available in browser runtime
*/
credentialDefaultProvider?: (input: any) => __Provider<__Credentials>;

/**
* The AWS region to which this client will send requests
*/
Expand Down
1 change: 0 additions & 1 deletion clients/client-sso/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,6 @@
"@aws-crypto/sha256-browser": "^1.0.0",
"@aws-crypto/sha256-js": "^1.0.0",
"@aws-sdk/config-resolver": "3.6.1",
"@aws-sdk/credential-provider-node": "3.6.1",
"@aws-sdk/fetch-http-handler": "3.6.1",
"@aws-sdk/hash-node": "3.6.1",
"@aws-sdk/invalid-dependency": "3.6.1",
Expand Down
1 change: 0 additions & 1 deletion clients/client-sso/runtimeConfig.browser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@ export const ClientDefaultValues: Required<ClientDefaults> = {
base64Decoder: fromBase64,
base64Encoder: toBase64,
bodyLengthChecker: calculateBodyLength,
credentialDefaultProvider: (_: unknown) => () => Promise.reject(new Error("Credential is missing")),
defaultUserAgentProvider: defaultUserAgent({
serviceId: ClientSharedValues.serviceId,
clientVersion: packageInfo.version,
Expand Down
2 changes: 0 additions & 2 deletions clients/client-sso/runtimeConfig.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import packageInfo from "./package.json";

import { NODE_REGION_CONFIG_FILE_OPTIONS, NODE_REGION_CONFIG_OPTIONS } from "@aws-sdk/config-resolver";
import { defaultProvider as credentialDefaultProvider } from "@aws-sdk/credential-provider-node";
import { Hash } from "@aws-sdk/hash-node";
import { NODE_MAX_ATTEMPT_CONFIG_OPTIONS } from "@aws-sdk/middleware-retry";
import { loadConfig as loadNodeConfig } from "@aws-sdk/node-config-provider";
Expand All @@ -22,7 +21,6 @@ export const ClientDefaultValues: Required<ClientDefaults> = {
base64Decoder: fromBase64,
base64Encoder: toBase64,
bodyLengthChecker: calculateBodyLength,
credentialDefaultProvider,
defaultUserAgentProvider: defaultUserAgent({
serviceId: ClientSharedValues.serviceId,
clientVersion: packageInfo.version,
Expand Down
5 changes: 3 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,8 @@
"**/karma*",
"**/karma*/**",
"**/@types/mocha*",
"**/@types/mocha*/**"
"**/@types/mocha*/**",
"**/@aws-sdk/client-sso/**"
]
},
"husky": {
Expand All @@ -117,4 +118,4 @@
],
"**/*.{ts,js,md,json}": "prettier --write"
}
}
}
1 change: 1 addition & 0 deletions packages/credential-provider-node/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
"@aws-sdk/property-provider": "3.6.1",
"@aws-sdk/shared-ini-file-loader": "3.6.1",
"@aws-sdk/types": "3.6.1",
"@aws-sdk/credential-provider-sso": "3.0.0",
"tslib": "^1.8.0"
},
"devDependencies": {
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 only 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
Loading