Skip to content

Commit

Permalink
fix(credential-provider-node): add fromHttp credential provider to de…
Browse files Browse the repository at this point in the history
…fault chain (#5739)

* fix(credential-provider-node): add fromHttp credential provider to default chain

* chore(credential-provider-ini): pass through logger

* fix(credential-provider-sso): prefer sso region for inner client if configured
  • Loading branch information
kuhe authored Jan 30, 2024
1 parent 5cb98d6 commit 6b99b0b
Show file tree
Hide file tree
Showing 11 changed files with 69 additions and 28 deletions.
18 changes: 14 additions & 4 deletions packages/credential-provider-http/src/fromHttp/fromHttp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,10 @@ export const fromHttp = (options: FromHttpOptions): AwsCredentialIdentityProvide
} else if (relative) {
host = `${DEFAULT_LINK_LOCAL_HOST}${relative}`;
} else {
throw new CredentialsProviderError("No HTTP credential provider host provided.");
throw new CredentialsProviderError(
`No HTTP credential provider host provided.
Set AWS_CONTAINER_CREDENTIALS_FULL_URI or AWS_CONTAINER_CREDENTIALS_RELATIVE_URI.`
);
}

// throws if invalid format.
Expand All @@ -56,7 +59,10 @@ export const fromHttp = (options: FromHttpOptions): AwsCredentialIdentityProvide
// throws if not to spec for provider.
checkUrl(url);

const requestHandler = new NodeHttpHandler();
const requestHandler = new NodeHttpHandler({
requestTimeout: options.timeout ?? 1000,
connectionTimeout: options.timeout ?? 1000,
});

return retryWrapper(
async (): Promise<AwsCredentialIdentity> => {
Expand All @@ -69,8 +75,12 @@ export const fromHttp = (options: FromHttpOptions): AwsCredentialIdentityProvide
// to allow for updates to the file contents.
request.headers.Authorization = (await fs.readFile(tokenFile)).toString();
}
const result = await requestHandler.handle(request);
return getCredentials(result.response);
try {
const result = await requestHandler.handle(request);
return getCredentials(result.response);
} catch (e: unknown) {
throw new CredentialsProviderError(String(e));
}
},
options.maxRetries ?? 3,
options.timeout ?? 1000
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,6 @@ describe(resolveProfileData.name, () => {
(resolveSsoCredentials as jest.Mock).mockImplementation(() => Promise.resolve(mockCreds));
const receivedCreds = await resolveProfileData(mockProfileName, mockProfiles, mockOptions);
expect(receivedCreds).toStrictEqual(mockCreds);
expect(resolveSsoCredentials).toHaveBeenCalledWith(mockProfileName);
expect(resolveSsoCredentials).toHaveBeenCalledWith(mockProfileName, mockOptions);
});
});
2 changes: 1 addition & 1 deletion packages/credential-provider-ini/src/resolveProfileData.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ export const resolveProfileData = async (
}

if (isSsoProfile(data)) {
return await resolveSsoCredentials(profileName);
return await resolveSsoCredentials(profileName, options);
}

// If the profile cannot be parsed or contains neither static credentials
Expand Down
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
import type { SsoProfile } from "@aws-sdk/credential-provider-sso";
import type { CredentialProviderOptions } from "@aws-sdk/types";
import type { Profile } from "@smithy/types";

/**
* @internal
*/
export const resolveSsoCredentials = async (profile: string) => {
export const resolveSsoCredentials = async (profile: string, options: CredentialProviderOptions = {}) => {
const { fromSSO } = await import("@aws-sdk/credential-provider-sso");
return fromSSO({
profile,
logger: options.logger,
})();
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,5 +34,6 @@ export const resolveWebIdentityCredentials = async (
roleArn: profile.role_arn,
roleSessionName: profile.role_session_name,
roleAssumerWithWebIdentity: options.roleAssumerWithWebIdentity,
logger: options.logger,
})()
);
1 change: 1 addition & 0 deletions packages/credential-provider-node/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
"license": "Apache-2.0",
"dependencies": {
"@aws-sdk/credential-provider-env": "*",
"@aws-sdk/credential-provider-http": "*",
"@aws-sdk/credential-provider-ini": "*",
"@aws-sdk/credential-provider-process": "*",
"@aws-sdk/credential-provider-sso": "*",
Expand Down
37 changes: 27 additions & 10 deletions packages/credential-provider-node/src/remoteProvider.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,10 @@ describe(remoteProvider.name, () => {
accessKeyId: "mockInstanceMetadataAccessKeyId",
secretAccessKey: "mockInstanceMetadataSecretAccessKey",
};
const mockFromHttp = jest.fn().mockReturnValue(async () => mockCredsFromContainer);

const sampleFullUri = "http://localhost";
const sampleRelativeUri = "/";

beforeEach(() => {
process.env = {
Expand All @@ -29,6 +33,10 @@ describe(remoteProvider.name, () => {
[ENV_CMDS_FULL_URI]: undefined,
[ENV_IMDS_DISABLED]: undefined,
};

jest.mock("@aws-sdk/credential-provider-http", () => ({
fromHttp: mockFromHttp,
}));
(fromContainerMetadata as jest.Mock).mockReturnValue(async () => mockCredsFromContainer);
(fromInstanceMetadata as jest.Mock).mockReturnValue(async () => mockSourceCredsFromInstanceMetadata);
});
Expand All @@ -38,16 +46,23 @@ describe(remoteProvider.name, () => {
jest.clearAllMocks();
});

it.each([ENV_CMDS_RELATIVE_URI, ENV_CMDS_FULL_URI])(
"returns fromContainerMetadata if env['%s'] is set",
async (key) => {
process.env[key] = "defined";
const receivedCreds = await (await remoteProvider(mockInit))();
expect(receivedCreds).toStrictEqual(mockCredsFromContainer);
expect(fromContainerMetadata).toHaveBeenCalledWith(mockInit);
expect(fromInstanceMetadata).not.toHaveBeenCalled();
}
);
it(`returns fromContainerMetadata if env[${ENV_CMDS_RELATIVE_URI}] is set`, async () => {
process.env[ENV_CMDS_RELATIVE_URI] = sampleRelativeUri;
const receivedCreds = await (await remoteProvider(mockInit))();
expect(receivedCreds).toStrictEqual(mockCredsFromContainer);
expect(mockFromHttp).toHaveBeenCalledWith(mockInit);
expect(fromContainerMetadata).toHaveBeenCalledWith(mockInit);
expect(fromInstanceMetadata).not.toHaveBeenCalled();
});

it(`returns fromContainerMetadata if env[${ENV_CMDS_FULL_URI}] is set`, async () => {
process.env[ENV_CMDS_FULL_URI] = sampleFullUri;
const receivedCreds = await (await remoteProvider(mockInit))();
expect(receivedCreds).toStrictEqual(mockCredsFromContainer);
expect(mockFromHttp).toHaveBeenCalledWith(mockInit);
expect(fromContainerMetadata).toHaveBeenCalledWith(mockInit);
expect(fromInstanceMetadata).not.toHaveBeenCalled();
});

it(`throws if env['${ENV_IMDS_DISABLED}'] is set`, async () => {
process.env[ENV_IMDS_DISABLED] = "1";
Expand All @@ -60,6 +75,7 @@ describe(remoteProvider.name, () => {
} catch (error) {
expect(error).toStrictEqual(expectedError);
}
expect(mockFromHttp).not.toHaveBeenCalled();
expect(fromContainerMetadata).not.toHaveBeenCalled();
expect(fromInstanceMetadata).not.toHaveBeenCalled();
});
Expand All @@ -69,5 +85,6 @@ describe(remoteProvider.name, () => {
expect(receivedCreds).toStrictEqual(mockSourceCredsFromInstanceMetadata);
expect(fromInstanceMetadata).toHaveBeenCalledWith(mockInit);
expect(fromContainerMetadata).not.toHaveBeenCalled();
expect(mockFromHttp).not.toHaveBeenCalled();
});
});
10 changes: 7 additions & 3 deletions packages/credential-provider-node/src/remoteProvider.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
import type { RemoteProviderInit } from "@smithy/credential-provider-imds";
import { CredentialsProviderError } from "@smithy/property-provider";
import { chain, CredentialsProviderError } from "@smithy/property-provider";
import type { AwsCredentialIdentityProvider } from "@smithy/types";

/**
* @internal
*/
export const ENV_IMDS_DISABLED = "AWS_EC2_METADATA_DISABLED";

/**
Expand All @@ -13,8 +16,9 @@ export const remoteProvider = async (init: RemoteProviderInit): Promise<AwsCrede
);

if (process.env[ENV_CMDS_RELATIVE_URI] || process.env[ENV_CMDS_FULL_URI]) {
init.logger?.debug("@aws-sdk/credential-provider-node", "remoteProvider::fromContainerMetadata");
return fromContainerMetadata(init);
init.logger?.debug("@aws-sdk/credential-provider-node", "remoteProvider::fromHttp/fromContainerMetadata");
const { fromHttp } = await import("@aws-sdk/credential-provider-http");
return chain(fromHttp(init), fromContainerMetadata(init));
}

if (process.env[ENV_IMDS_DISABLED]) {
Expand Down
5 changes: 3 additions & 2 deletions packages/credential-provider-sso/src/fromSSO.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,7 @@ describe(fromSSO.name, () => {
expect(validateSsoProfile).toHaveBeenCalledWith(mockSsoProfile);
});

it("calls resolveSSOCredentials with values from validated Sso profile", async () => {
it("calls resolveSSOCredentials with values from validated SSO profile", async () => {
const mockValidatedSsoProfile = {
sso_start_url: "mock_sso_start_url",
sso_account_id: "mock_sso_account_id",
Expand All @@ -119,7 +119,8 @@ describe(fromSSO.name, () => {
ssoRoleName: mockValidatedSsoProfile.sso_role_name,
profile: mockProfileName,
ssoSession: undefined,
ssoClient: expect.any(SSOClient),
ssoClient: undefined,
clientConfig: undefined,
});
});
});
Expand Down
8 changes: 3 additions & 5 deletions packages/credential-provider-sso/src/fromSSO.ts
Original file line number Diff line number Diff line change
Expand Up @@ -83,11 +83,7 @@ export const fromSSO =
async () => {
init.logger?.debug("@aws-sdk/credential-provider-sso", "fromSSO");
const { ssoStartUrl, ssoAccountId, ssoRegion, ssoRoleName, ssoSession } = init;
let { ssoClient } = init;
if (!ssoClient) {
const { SSOClient } = await import("./loadSso");
ssoClient = new SSOClient(init.clientConfig ?? {});
}
const { ssoClient } = init;
const profileName = getProfileName(init);

if (!ssoStartUrl && !ssoAccountId && !ssoRegion && !ssoRoleName && !ssoSession) {
Expand Down Expand Up @@ -125,6 +121,7 @@ export const fromSSO =
ssoRegion: sso_region,
ssoRoleName: sso_role_name,
ssoClient: ssoClient,
clientConfig: init.clientConfig,
profile: profileName,
});
} else if (!ssoStartUrl || !ssoAccountId || !ssoRegion || !ssoRoleName) {
Expand All @@ -140,6 +137,7 @@ export const fromSSO =
ssoRegion,
ssoRoleName,
ssoClient,
clientConfig: init.clientConfig,
profile: profileName,
});
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ export const resolveSSOCredentials = async ({
ssoRegion,
ssoRoleName,
ssoClient,
clientConfig,
profile,
}: FromSSOInit & SsoCredentialsParameters): Promise<AwsCredentialIdentity> => {
let token: SSOToken;
Expand Down Expand Up @@ -55,7 +56,13 @@ export const resolveSSOCredentials = async ({

const { SSOClient, GetRoleCredentialsCommand } = await import("./loadSso");

const sso = ssoClient || new SSOClient({ region: ssoRegion });
const sso =
ssoClient ||
new SSOClient(
Object.assign({}, clientConfig ?? {}, {
region: clientConfig?.region ?? ssoRegion,
})
);
let ssoResp: GetRoleCredentialsCommandOutput;
try {
ssoResp = await sso.send(
Expand Down

0 comments on commit 6b99b0b

Please sign in to comment.