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-imds): signed IMDS workflow #1358

Merged
merged 1 commit into from
Jul 13, 2020
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
208 changes: 182 additions & 26 deletions packages/credential-provider-imds/src/fromInstanceMetadata.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,14 +12,29 @@ jest.mock("./remoteProvider/retry");
jest.mock("./remoteProvider/RemoteProviderInit");

describe("fromInstanceMetadata", () => {
const host = "169.254.169.254";
const mockTimeout = 1000;
const mockMaxRetries = 3;
const mockProfile = "foo";
const mockToken = "fooToken";
const mockProfile = "fooProfile";

const mockHttpRequestOptions = {
host: "169.254.169.254",
const mockTokenRequestOptions = {
host,
path: "/latest/api/token",
method: "PUT",
headers: {
"x-aws-ec2-metadata-token-ttl-seconds": "21600",
},
timeout: mockTimeout,
};

const mockProfileRequestOptions = {
host,
path: "/latest/meta-data/iam/security-credentials/",
timeout: mockTimeout,
headers: {
"x-aws-ec2-metadata-token": mockToken,
},
};

const mockImdsCreds = Object.freeze({
Expand Down Expand Up @@ -48,35 +63,38 @@ describe("fromInstanceMetadata", () => {
jest.resetAllMocks();
});

it("gets profile name from IMDS, and passes profile name to fetch credentials", async () => {
(httpRequest as jest.Mock).mockResolvedValueOnce(mockProfile).mockResolvedValueOnce(JSON.stringify(mockImdsCreds));
it("gets token and profile name to fetch credentials", async () => {
(httpRequest as jest.Mock)
.mockResolvedValueOnce(mockToken)
.mockResolvedValueOnce(mockProfile)
.mockResolvedValueOnce(JSON.stringify(mockImdsCreds));

(retry as jest.Mock).mockImplementation((fn: any) => fn());
(fromImdsCredentials as jest.Mock).mockReturnValue(mockCreds);

await expect(fromInstanceMetadata()()).resolves.toEqual(mockCreds);
expect(httpRequest).toHaveBeenCalledTimes(2);
expect(httpRequest).toHaveBeenNthCalledWith(1, mockHttpRequestOptions);
expect(httpRequest).toHaveBeenNthCalledWith(2, {
...mockHttpRequestOptions,
path: `${mockHttpRequestOptions.path}${mockProfile}`,
expect(httpRequest).toHaveBeenCalledTimes(3);
expect(httpRequest).toHaveBeenNthCalledWith(1, mockTokenRequestOptions);
expect(httpRequest).toHaveBeenNthCalledWith(2, mockProfileRequestOptions);
expect(httpRequest).toHaveBeenNthCalledWith(3, {
...mockProfileRequestOptions,
path: `${mockProfileRequestOptions.path}${mockProfile}`,
});
});

it("trims profile returned name from IMDS", async () => {
(httpRequest as jest.Mock)
.mockResolvedValueOnce(mockToken)
.mockResolvedValueOnce(" " + mockProfile + " ")
.mockResolvedValueOnce(JSON.stringify(mockImdsCreds));

(retry as jest.Mock).mockImplementation((fn: any) => fn());
(fromImdsCredentials as jest.Mock).mockReturnValue(mockCreds);

await expect(fromInstanceMetadata()()).resolves.toEqual(mockCreds);
expect(httpRequest).toHaveBeenCalledTimes(2);
expect(httpRequest).toHaveBeenNthCalledWith(1, mockHttpRequestOptions);
expect(httpRequest).toHaveBeenNthCalledWith(2, {
...mockHttpRequestOptions,
path: `${mockHttpRequestOptions.path}${mockProfile}`,
expect(httpRequest).toHaveBeenNthCalledWith(3, {
...mockProfileRequestOptions,
path: `${mockProfileRequestOptions.path}${mockProfile}`,
});
});

Expand Down Expand Up @@ -107,7 +125,10 @@ describe("fromInstanceMetadata", () => {
});

it("throws ProviderError if credentials returned are incorrect", async () => {
(httpRequest as jest.Mock).mockResolvedValueOnce(mockProfile).mockResolvedValueOnce(JSON.stringify(mockImdsCreds));
(httpRequest as jest.Mock)
.mockResolvedValueOnce(mockToken)
.mockResolvedValueOnce(mockProfile)
.mockResolvedValueOnce(JSON.stringify(mockImdsCreds));

(retry as jest.Mock).mockImplementation((fn: any) => fn());
((isImdsCredentials as unknown) as jest.Mock).mockReturnValueOnce(false);
Expand All @@ -116,40 +137,175 @@ describe("fromInstanceMetadata", () => {
new ProviderError("Invalid response received from instance metadata service.")
);
expect(retry).toHaveBeenCalledTimes(2);
expect(httpRequest).toHaveBeenCalledTimes(2);
expect(httpRequest).toHaveBeenCalledTimes(3);
expect(isImdsCredentials).toHaveBeenCalledTimes(1);
expect(isImdsCredentials).toHaveBeenCalledWith(mockImdsCreds);
expect(fromImdsCredentials).not.toHaveBeenCalled();
});

it("throws Error if requestFromEc2Imds for profile fails", async () => {
it("throws Error if httpRequest for profile fails", async () => {
const mockError = new Error("profile not found");
(httpRequest as jest.Mock).mockRejectedValueOnce(mockError);
(httpRequest as jest.Mock).mockResolvedValueOnce(mockToken).mockRejectedValueOnce(mockError);
(retry as jest.Mock).mockImplementation((fn: any) => fn());

await expect(fromInstanceMetadata()()).rejects.toEqual(mockError);
expect(retry).toHaveBeenCalledTimes(1);
expect(httpRequest).toHaveBeenCalledTimes(1);
expect(httpRequest).toHaveBeenCalledTimes(2);
});

it("throws Error if requestFromEc2Imds for credentials fails", async () => {
it("throws Error if httpRequest for credentials fails", async () => {
const mockError = new Error("creds not found");
(httpRequest as jest.Mock).mockResolvedValueOnce(mockProfile).mockRejectedValueOnce(mockError);
(httpRequest as jest.Mock)
.mockResolvedValueOnce(mockToken)
.mockResolvedValueOnce(mockProfile)
.mockRejectedValueOnce(mockError);
(retry as jest.Mock).mockImplementation((fn: any) => fn());

await expect(fromInstanceMetadata()()).rejects.toEqual(mockError);
expect(retry).toHaveBeenCalledTimes(2);
expect(httpRequest).toHaveBeenCalledTimes(2);
expect(httpRequest).toHaveBeenCalledTimes(3);
expect(fromImdsCredentials).not.toHaveBeenCalled();
});

it("throws SyntaxError if requestFromEc2Imds returns unparseable creds", async () => {
(httpRequest as jest.Mock).mockResolvedValueOnce(mockProfile).mockResolvedValueOnce(".");
it("throws SyntaxError if httpRequest returns unparseable creds", async () => {
(httpRequest as jest.Mock)
.mockResolvedValueOnce(mockToken)
.mockResolvedValueOnce(mockProfile)
.mockResolvedValueOnce(".");
(retry as jest.Mock).mockImplementation((fn: any) => fn());

await expect(fromInstanceMetadata()()).rejects.toEqual(new SyntaxError("Unexpected token . in JSON at position 0"));
expect(retry).toHaveBeenCalledTimes(2);
expect(httpRequest).toHaveBeenCalledTimes(2);
expect(httpRequest).toHaveBeenCalledTimes(3);
expect(fromImdsCredentials).not.toHaveBeenCalled();
});

it("throws error if metadata token errors with statusCode 400", async () => {
const tokenError = Object.assign(new Error("token not found"), {
statusCode: 400,
});
(httpRequest as jest.Mock).mockRejectedValueOnce(tokenError);

await expect(fromInstanceMetadata()()).rejects.toEqual(tokenError);
});

describe("disables fetching of token", () => {
beforeEach(() => {
(retry as jest.Mock).mockImplementation((fn: any) => fn());
(fromImdsCredentials as jest.Mock).mockReturnValue(mockCreds);
});

it("when token fetch returns with TimeoutError", async () => {
const tokenError = new Error("TimeoutError");

(httpRequest as jest.Mock)
.mockRejectedValueOnce(tokenError)
.mockResolvedValueOnce(mockProfile)
.mockResolvedValueOnce(JSON.stringify(mockImdsCreds))
.mockResolvedValueOnce(mockProfile)
.mockResolvedValueOnce(JSON.stringify(mockImdsCreds));

const fromInstanceMetadataFunc = fromInstanceMetadata();
await expect(fromInstanceMetadataFunc()).resolves.toEqual(mockCreds);
await expect(fromInstanceMetadataFunc()).resolves.toEqual(mockCreds);
});

[403, 404, 405].forEach((statusCode) => {
it(`when token fetch errors with statusCode ${statusCode}`, async () => {
const tokenError = Object.assign(new Error(), { statusCode });

(httpRequest as jest.Mock)
.mockRejectedValueOnce(tokenError)
.mockResolvedValueOnce(mockProfile)
.mockResolvedValueOnce(JSON.stringify(mockImdsCreds))
.mockResolvedValueOnce(mockProfile)
.mockResolvedValueOnce(JSON.stringify(mockImdsCreds));

const fromInstanceMetadataFunc = fromInstanceMetadata();
await expect(fromInstanceMetadataFunc()).resolves.toEqual(mockCreds);
await expect(fromInstanceMetadataFunc()).resolves.toEqual(mockCreds);
});
});
});

it("uses insecure data flow once, if error is not TimeoutError", async () => {
const tokenError = new Error("Error");

(httpRequest as jest.Mock)
.mockRejectedValueOnce(tokenError)
.mockResolvedValueOnce(mockProfile)
.mockResolvedValueOnce(JSON.stringify(mockImdsCreds))
.mockResolvedValueOnce(mockToken)
.mockResolvedValueOnce(mockProfile)
.mockResolvedValueOnce(JSON.stringify(mockImdsCreds));

(retry as jest.Mock).mockImplementation((fn: any) => fn());
(fromImdsCredentials as jest.Mock).mockReturnValue(mockCreds);

const fromInstanceMetadataFunc = fromInstanceMetadata();
await expect(fromInstanceMetadataFunc()).resolves.toEqual(mockCreds);
await expect(fromInstanceMetadataFunc()).resolves.toEqual(mockCreds);
});

it("uses insecure data flow once, if error statusCode is not 400, 403, 404, 405", async () => {
const tokenError = Object.assign(new Error("Error"), { statusCode: 406 });

(httpRequest as jest.Mock)
.mockRejectedValueOnce(tokenError)
.mockResolvedValueOnce(mockProfile)
.mockResolvedValueOnce(JSON.stringify(mockImdsCreds))
.mockResolvedValueOnce(mockToken)
.mockResolvedValueOnce(mockProfile)
.mockResolvedValueOnce(JSON.stringify(mockImdsCreds));

(retry as jest.Mock).mockImplementation((fn: any) => fn());
(fromImdsCredentials as jest.Mock).mockReturnValue(mockCreds);

const fromInstanceMetadataFunc = fromInstanceMetadata();
await expect(fromInstanceMetadataFunc()).resolves.toEqual(mockCreds);
await expect(fromInstanceMetadataFunc()).resolves.toEqual(mockCreds);
});

describe("re-enables fetching of token", () => {
const error401 = Object.assign(new Error("error"), { statusCode: 401 });

beforeEach(() => {
const tokenError = new Error("TimeoutError");

(httpRequest as jest.Mock)
.mockRejectedValueOnce(tokenError)
.mockResolvedValueOnce(mockProfile)
.mockResolvedValueOnce(JSON.stringify(mockImdsCreds));

(retry as jest.Mock).mockImplementation((fn: any) => fn());
(fromImdsCredentials as jest.Mock).mockReturnValue(mockCreds);
});

it("when profile error with 401", async () => {
(httpRequest as jest.Mock)
.mockRejectedValueOnce(error401)
.mockResolvedValueOnce(mockToken)
.mockResolvedValueOnce(mockProfile)
.mockResolvedValueOnce(JSON.stringify(mockImdsCreds));

const fromInstanceMetadataFunc = fromInstanceMetadata();
await expect(fromInstanceMetadataFunc()).resolves.toEqual(mockCreds);
await expect(fromInstanceMetadataFunc()).rejects.toEqual(error401);
await expect(fromInstanceMetadataFunc()).resolves.toEqual(mockCreds);
});

it("when creds error with 401", async () => {
(httpRequest as jest.Mock)
.mockResolvedValueOnce(mockProfile)
.mockRejectedValueOnce(error401)
.mockResolvedValueOnce(mockToken)
.mockResolvedValueOnce(mockProfile)
.mockResolvedValueOnce(JSON.stringify(mockImdsCreds));

const fromInstanceMetadataFunc = fromInstanceMetadata();
await expect(fromInstanceMetadataFunc()).resolves.toEqual(mockCreds);
await expect(fromInstanceMetadataFunc()).rejects.toEqual(error401);
await expect(fromInstanceMetadataFunc()).resolves.toEqual(mockCreds);
});
});
});
Loading