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-providers): add fromHttp credential provider #5256

Merged
merged 1 commit into from
Sep 27, 2023
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
8 changes: 8 additions & 0 deletions packages/credential-provider-http/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
/node_modules/
/build/
/coverage/
/docs/
*.tsbuildinfo
*.tgz
*.log
package-lock.json
4 changes: 4 additions & 0 deletions packages/credential-provider-http/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
# Change Log

All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
10 changes: 10 additions & 0 deletions packages/credential-provider-http/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
# @aws-sdk/credential-provider-http

[![NPM version](https://img.shields.io/npm/v/@aws-sdk/credential-provider-http/latest.svg)](https://www.npmjs.com/package/@aws-sdk/credential-provider-http)
[![NPM downloads](https://img.shields.io/npm/dm/@aws-sdk/credential-provider-http.svg)](https://www.npmjs.com/package/@aws-sdk/credential-provider-http)

> An internal transitively required package.

## Usage

See https://www.npmjs.com/package/@aws-sdk/credential-providers
5 changes: 5 additions & 0 deletions packages/credential-provider-http/jest.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
const base = require("../../jest.config.base.js");

module.exports = {
...base,
};
67 changes: 67 additions & 0 deletions packages/credential-provider-http/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
{
"name": "@aws-sdk/credential-provider-http",
"version": "3.418.0",
"description": "AWS credential provider for containers and HTTP sources",
"main": "./dist-cjs/index.js",
"module": "./dist-es/index.js",
"scripts": {
"build": "concurrently 'yarn:build:cjs' 'yarn:build:es' 'yarn:build:types'",
"build:cjs": "tsc -p tsconfig.cjs.json",
"build:es": "tsc -p tsconfig.es.json",
"build:include:deps": "lerna run --scope $npm_package_name --include-dependencies build",
"build:types": "tsc -p tsconfig.types.json",
"build:types:downlevel": "downlevel-dts dist-types dist-types/ts3.4",
"clean": "rimraf ./dist-* && rimraf *.tsbuildinfo",
"test": "jest"
},
"keywords": [
"aws",
"credentials"
],
"author": {
"name": "AWS SDK for JavaScript Team",
"url": "https://aws.amazon.com/javascript/"
},
"license": "Apache-2.0",
"dependencies": {
"@aws-sdk/types": "*",
"@smithy/protocol-http": "^3.0.5",
"@smithy/property-provider": "^2.0.0",
"@smithy/fetch-http-handler": "^2.1.5",
"@smithy/node-http-handler": "^2.1.5",
"@smithy/types": "^2.3.3",
"tslib": "^2.5.0"
},
"devDependencies": {
"@tsconfig/recommended": "1.0.1",
"@types/node": "^14.14.31",
"concurrently": "7.0.0",
"downlevel-dts": "0.10.1",
"rimraf": "3.0.2",
"typedoc": "0.23.23",
"typescript": "~4.9.5"
},
"types": "./dist-types/index.d.ts",
"engines": {
"node": ">=14.0.0"
},
"typesVersions": {
"<4.0": {
"dist-types/*": [
"dist-types/ts3.4/*"
]
}
},
"files": [
"dist-*/**"
],
"homepage": "https://github.com/aws/aws-sdk-js-v3/tree/main/packages/credential-provider-http",
"repository": {
"type": "git",
"url": "https://github.com/aws/aws-sdk-js-v3.git",
"directory": "packages/credential-provider-http"
},
"typedoc": {
"entryPoint": "src/index.ts"
}
}
48 changes: 48 additions & 0 deletions packages/credential-provider-http/src/fromHttp/checkUrl.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import { CredentialsProviderError } from "@smithy/property-provider";

import { checkUrl } from "./checkUrl";

describe(checkUrl.name, () => {
it("allows https", () => {
expect(checkUrl(new URL("https://___.com"))).toBeUndefined();
expect(() => checkUrl(new URL("http://___.com"))).toThrow(CredentialsProviderError);
});

it("allows ECS container host", () => {
expect(checkUrl(new URL("http://169.254.170.2/test"))).toBeUndefined();
expect(() => checkUrl(new URL("http://169.254.170.3/test"))).toThrow(CredentialsProviderError);
});

it("allows EKS container host", () => {
expect(checkUrl(new URL("http://169.254.170.23/test"))).toBeUndefined();
expect(() => checkUrl(new URL("http://169.254.170.24/test"))).toThrow(CredentialsProviderError);

expect(checkUrl(new URL("http://[fd00:ec2::23]/test"))).toBeUndefined();
expect(() => checkUrl(new URL("http://[fd00:ec2::24]/test"))).toThrow(CredentialsProviderError);
});

it("allows localhost", () => {
expect(checkUrl(new URL("http://localhost/test"))).toBeUndefined();
expect(checkUrl(new URL("http://127.0.0.0/test"))).toBeUndefined();
expect(checkUrl(new URL("http://127.0.0.1/test"))).toBeUndefined();
expect(checkUrl(new URL("http://127.255.255.255/test"))).toBeUndefined();
expect(checkUrl(new URL("http://[::1]/test"))).toBeUndefined();
expect(checkUrl(new URL("http://[0000:0000:0000:0000:0000:0000:0000:0001]/test"))).toBeUndefined();
});

it("rejects other http", () => {
expect(() => checkUrl(new URL("http://abcd.com"))).toThrow(CredentialsProviderError);
});

describe("additional test cases", () => {
it("rejects forbidden host in full URI", () => {
expect(() => checkUrl(new URL("http://192.168.1.1/endpoint"))).toThrow(CredentialsProviderError);
});
it("rejects forbidden link-local host in full URI", () => {
expect(() => checkUrl(new URL("http://169.254.170.3/endpoint"))).toThrow(CredentialsProviderError);
});
it("allows http loopback v4 URI", () => {
expect(() => checkUrl(new URL("http://127.0.0.2/credentials"))).not.toThrow();
});
});
});
79 changes: 79 additions & 0 deletions packages/credential-provider-http/src/fromHttp/checkUrl.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
import { CredentialsProviderError } from "@smithy/property-provider";

/**
* @internal
* Anything starting with 127.
*/
const LOOPBACK_CIDR_IPv4 = "127.0.0.0/8";
/**
* @internal
* A single IP equal to
* 0000:0000:0000:0000:0000:0000:0000:0001
*/
const LOOPBACK_CIDR_IPv6 = "::1/128";
/**
* @internal
*/
const ECS_CONTAINER_HOST = "169.254.170.2";
/**
* @internal
*/
const EKS_CONTAINER_HOST_IPv4 = "169.254.170.23";
/**
* @internal
*/
const EKS_CONTAINER_HOST_IPv6 = "[fd00:ec2::23]";

/**
* @internal
*
* @param url - to be validated.
* @throws if not acceptable to this provider.
*/
export const checkUrl = (url: URL): void => {
if (url.protocol === "https:") {
// no additional requirements for HTTPS.
return;
}

if (
url.hostname === ECS_CONTAINER_HOST ||
url.hostname === EKS_CONTAINER_HOST_IPv4 ||
url.hostname === EKS_CONTAINER_HOST_IPv6
) {
return;
}

if (url.hostname.includes("[")) {
// IPv6
if (url.hostname === "[::1]" || url.hostname === "[0000:0000:0000:0000:0000:0000:0000:0001]") {
return;
}
} else {
// IPv4
if (url.hostname === "localhost") {
return;
}
const ipComponents = url.hostname.split(".");
const inRange = (component: string): boolean => {
const num = parseInt(component, 10);
return 0 <= num && num <= 255;
};
if (
ipComponents[0] === "127" &&
inRange(ipComponents[1]) &&
inRange(ipComponents[2]) &&
inRange(ipComponents[3]) &&
ipComponents.length === 4
) {
return;
}
}

throw new CredentialsProviderError(
`URL not accepted. It must either be HTTPS or match one of the following:
- loopback CIDR 127.0.0.0/8 or [::1/128]
- ECS container host 169.254.170.2
- EKS container host 169.254.170.23 or [fd00:ec2::23]`
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import { FetchHttpHandler } from "@smithy/fetch-http-handler";
import { CredentialsProviderError } from "@smithy/property-provider";
import { AwsCredentialIdentity, AwsCredentialIdentityProvider } from "@smithy/types";

import { checkUrl } from "./checkUrl";
import type { FromHttpOptions } from "./fromHttpTypes";
import { createGetRequest, getCredentials } from "./requestHelpers";
import { retryWrapper } from "./retry-wrapper";

/**
* Creates a provider that gets credentials via HTTP request.
*/
export const fromHttp = (options: FromHttpOptions): AwsCredentialIdentityProvider => {
let host: string;

const full = options.credentialsFullUri;

if (full) {
host = full;
} else {
throw new CredentialsProviderError("No HTTP credential provider host provided.");
}

// throws if invalid format.
const url = new URL(host);

// throws if not to spec for provider.
checkUrl(url);

const requestHandler = new FetchHttpHandler();

return retryWrapper(
async (): Promise<AwsCredentialIdentity> => {
const request = createGetRequest(url);
if (options.authorizationToken) {
request.headers.Authorization = options.authorizationToken;
}
const result = await requestHandler.handle(request);
return getCredentials(result.response);
},
options.maxRetries ?? 3,
options.timeout ?? 1000
);
};
104 changes: 104 additions & 0 deletions packages/credential-provider-http/src/fromHttp/fromHttp.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
import { HttpResponse } from "@smithy/protocol-http";
import { Readable } from "stream";

import { fromHttp } from "./fromHttp";
import * as helpers from "./requestHelpers";

const credentials = {
accessKeyId: "ABC",
secretAccessKey: "abcd",
sessionToken: "abcde",
expiration: new Date(),
};

const mockToken = "abcd";

const mockResponse = {
AccessKeyId: credentials.accessKeyId,
SecretAccessKey: credentials.secretAccessKey,
Token: credentials.sessionToken,
AccountId: "123",
Expiration: new Date(credentials.expiration).toISOString(), // rfc3339
};

const mockHandle = jest.fn().mockResolvedValue({
response: new HttpResponse({
statusCode: 200,
headers: {
"Content-Type": "application/json",
},
body: Readable.from([""]),
}),
});

jest.mock("@smithy/node-http-handler", () => ({
NodeHttpHandler: jest.fn().mockImplementation(() => ({
destroy: () => {},
handle: mockHandle,
})),
streamCollector: jest.fn(),
}));

jest.spyOn(helpers, "getCredentials").mockReturnValue(Promise.resolve(credentials));

jest.mock("fs/promises", () => ({
async readFile() {
return mockToken;
},
}));

describe(fromHttp.name, () => {
afterAll(() => {
jest.resetAllMocks();
});

it("uses the full uri", async () => {
const provider = fromHttp({
awsContainerCredentialsFullUri: "https://u1.aws",
awsContainerCredentialsRelativeUri: "",
});

await provider();

expect(mockHandle).toHaveBeenCalledWith(helpers.createGetRequest(new URL("https://u1.aws")));
});

it("uses the relative uri", async () => {
const provider = fromHttp({
awsContainerCredentialsFullUri: "",
awsContainerCredentialsRelativeUri: "/some-path",
});

await provider();

expect(mockHandle).toHaveBeenCalledWith(helpers.createGetRequest(new URL("http://169.254.170.2/some-path")));
});

it("can use the token", async () => {
const provider = fromHttp({
awsContainerCredentialsFullUri: "https://t1.aws",
awsContainerAuthorizationToken: mockToken,
});

const request = helpers.createGetRequest(new URL("https://t1.aws"));
request.headers.Authorization = mockToken;

await provider();

expect(mockHandle).toHaveBeenCalledWith(request);
});

it("can use the token file", async () => {
const provider = fromHttp({
awsContainerCredentialsFullUri: "https://t2.aws",
awsContainerAuthorizationTokenFile: "some-file",
});

const request = helpers.createGetRequest(new URL("https://t1.aws"));
request.headers.Authorization = mockToken;

await provider();

expect(mockHandle).toHaveBeenCalledWith(request);
});
});
Loading
Loading