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-process): refactor into modular components #3287

Merged
merged 14 commits into from
Feb 9, 2022
Merged
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
export type ProcessCredentials = {
Version: number;
AccessKeyId: string;
SecretAccessKey: string;
SessionToken?: string;
Expiration?: number;
};
65 changes: 65 additions & 0 deletions packages/credential-provider-process/src/fromProcess.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import { Credentials } from "@aws-sdk/types";
import { getMasterProfileName, parseKnownFiles } from "@aws-sdk/util-credentials";

import { fromProcess } from "./fromProcess";
import { resolveProcessCredentials } from "./resolveProcessCredentials";

jest.mock("@aws-sdk/util-credentials");
jest.mock("./resolveProcessCredentials");

describe(fromProcess.name, () => {
const mockMasterProfileName = "mockMasterProfileName";
const mockProfileName = "mockProfileName";
const mockInit = { profile: mockProfileName };
const mockProfiles = { [mockProfileName]: { key: "value" } };

beforeEach(() => {
(parseKnownFiles as jest.Mock).mockResolvedValue(mockProfiles);
(getMasterProfileName as jest.Mock).mockReturnValue(mockMasterProfileName);
});

afterEach(() => {
jest.clearAllMocks();
});

it("rethrows error if parsing known files fails", async () => {
const expectedError = new Error("from parseKnownFiles");
(parseKnownFiles as jest.Mock).mockRejectedValue(expectedError);
try {
await fromProcess(mockInit)();
fail(`expected ${expectedError}`);
} catch (error) {
expect(error).toStrictEqual(expectedError);
}
expect(parseKnownFiles).toHaveBeenCalledWith(mockInit);
expect(getMasterProfileName).not.toHaveBeenCalled();
expect(resolveProcessCredentials).not.toHaveBeenCalled();
});

it("rethrows error if resolving process creds fails", async () => {
const expectedError = new Error("from resolveProcessCredentials");
(resolveProcessCredentials as jest.Mock).mockRejectedValue(expectedError);
try {
await fromProcess(mockInit)();
fail(`expected ${expectedError}`);
} catch (error) {
expect(error).toStrictEqual(expectedError);
}
expect(parseKnownFiles).toHaveBeenCalledWith(mockInit);
expect(getMasterProfileName).toHaveBeenCalledWith(mockInit);
expect(resolveProcessCredentials).toHaveBeenCalledWith(mockMasterProfileName, mockProfiles);
});

it("returns resolved process creds", async () => {
const expectedCreds: Credentials = {
accessKeyId: "mockAccessKeyId",
secretAccessKey: "mockSecretAccessKey",
};
(resolveProcessCredentials as jest.Mock).mockResolvedValue(expectedCreds);
const receivedCreds = await fromProcess(mockInit)();
expect(receivedCreds).toStrictEqual(expectedCreds);
expect(parseKnownFiles).toHaveBeenCalledWith(mockInit);
expect(getMasterProfileName).toHaveBeenCalledWith(mockInit);
expect(resolveProcessCredentials).toHaveBeenCalledWith(mockMasterProfileName, mockProfiles);
});
});
17 changes: 17 additions & 0 deletions packages/credential-provider-process/src/fromProcess.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { CredentialProvider } from "@aws-sdk/types";
import { getMasterProfileName, parseKnownFiles, SourceProfileInit } from "@aws-sdk/util-credentials";

import { resolveProcessCredentials } from "./resolveProcessCredentials";

export interface FromProcessInit extends SourceProfileInit {}

/**
* Creates a credential provider that will read from a credential_process specified
* in ini files.
*/
export const fromProcess =
(init: FromProcessInit = {}): CredentialProvider =>
async () => {
const profiles = await parseKnownFiles(init);
return resolveProcessCredentials(getMasterProfileName(init), profiles);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import { Credentials } from "@aws-sdk/types";

import { getValidatedProcessCredentials } from "./getValidatedProcessCredentials";
import { ProcessCredentials } from "./ProcessCredentials";

describe(getValidatedProcessCredentials.name, () => {
const mockProfileName = "mockProfileName";
const mockAccessKeyId = "mockAccessKeyId";
const mockSecretAccessKey = "mockSecretAccessKey";
const mockSessionToken = "mockSessionToken";
const mockExpiration = Date.now() + 24 * 60 * 60 * 1000;

const getMockProcessCreds = (): ProcessCredentials => ({
Version: 1,
AccessKeyId: mockAccessKeyId,
SecretAccessKey: mockSecretAccessKey,
SessionToken: mockSessionToken,
Expiration: mockExpiration,
});

it.each([undefined, 2])("throws Error when Version is %s", (Version) => {
expect(() => {
getValidatedProcessCredentials(mockProfileName, {
...getMockProcessCreds(),
Version,
});
}).toThrow(`Profile ${mockProfileName} credential_process did not return Version 1.`);
});

it.each(["AccessKeyId", "SecretAccessKey"])("throws Error when '%s' is not defined", (key) => {
expect(() => {
getValidatedProcessCredentials(mockProfileName, {
...getMockProcessCreds(),
[key]: undefined,
});
}).toThrow(`Profile ${mockProfileName} credential_process returned invalid credentials.`);
});

it("throws error when credentials are expired", () => {
const expirationDayBefore = Date.now() - 24 * 60 * 60 * 1000;
expect(() => {
getValidatedProcessCredentials(mockProfileName, {
...getMockProcessCreds(),
Expiration: expirationDayBefore,
});
}).toThrow(`Profile ${mockProfileName} credential_process returned expired credentials.`);
});

describe("returns validated Process credentials", () => {
const getValidatedCredentials = (data: ProcessCredentials): Credentials => ({
accessKeyId: data.AccessKeyId,
secretAccessKey: data.SecretAccessKey,
...(data.SessionToken && { sessionToken: data.SessionToken }),
...(data.Expiration && { expiration: new Date(data.Expiration) }),
});

it("with all values", () => {
const mockProcessCreds = getMockProcessCreds();
const mockOutputCreds = getValidatedCredentials(mockProcessCreds);
expect(getValidatedProcessCredentials(mockProfileName, mockProcessCreds)).toStrictEqual(mockOutputCreds);
});

it.each(["SessionToken", "Expiration"])("without '%s'", (key) => {
const mockProcessCreds = { ...getMockProcessCreds(), [key]: undefined };
const mockOutputCreds = getValidatedCredentials(mockProcessCreds);
expect(getValidatedProcessCredentials(mockProfileName, mockProcessCreds)).toStrictEqual(mockOutputCreds);
});
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { Credentials } from "@aws-sdk/types";

import { ProcessCredentials } from "./ProcessCredentials";

export const getValidatedProcessCredentials = (profileName: string, data: ProcessCredentials): Credentials => {
if (data.Version !== 1) {
throw Error(`Profile ${profileName} credential_process did not return Version 1.`);
}

if (data.AccessKeyId === undefined || data.SecretAccessKey === undefined) {
throw Error(`Profile ${profileName} credential_process returned invalid credentials.`);
}

if (data.Expiration) {
const currentTime = new Date();
const expireTime = new Date(data.Expiration);
if (expireTime < currentTime) {
throw Error(`Profile ${profileName} credential_process returned expired credentials.`);
}
}

return {
accessKeyId: data.AccessKeyId,
secretAccessKey: data.SecretAccessKey,
...(data.SessionToken && { sessionToken: data.SessionToken }),
...(data.Expiration && { expiration: new Date(data.Expiration) }),
};
};
Loading