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

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

Merged
merged 3 commits into from
Jan 30, 2024
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
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);
});
});
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
Loading