Skip to content

Commit 2b64304

Browse files
authored
feat(credential-provider-process): refactor into modular components (#3287)
1 parent 7c891b2 commit 2b64304

9 files changed

+342
-724
lines changed
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
export type ProcessCredentials = {
2+
Version: number;
3+
AccessKeyId: string;
4+
SecretAccessKey: string;
5+
SessionToken?: string;
6+
Expiration?: number;
7+
};
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
import { Credentials } from "@aws-sdk/types";
2+
import { getMasterProfileName, parseKnownFiles } from "@aws-sdk/util-credentials";
3+
4+
import { fromProcess } from "./fromProcess";
5+
import { resolveProcessCredentials } from "./resolveProcessCredentials";
6+
7+
jest.mock("@aws-sdk/util-credentials");
8+
jest.mock("./resolveProcessCredentials");
9+
10+
describe(fromProcess.name, () => {
11+
const mockMasterProfileName = "mockMasterProfileName";
12+
const mockProfileName = "mockProfileName";
13+
const mockInit = { profile: mockProfileName };
14+
const mockProfiles = { [mockProfileName]: { key: "value" } };
15+
16+
beforeEach(() => {
17+
(parseKnownFiles as jest.Mock).mockResolvedValue(mockProfiles);
18+
(getMasterProfileName as jest.Mock).mockReturnValue(mockMasterProfileName);
19+
});
20+
21+
afterEach(() => {
22+
jest.clearAllMocks();
23+
});
24+
25+
it("rethrows error if parsing known files fails", async () => {
26+
const expectedError = new Error("from parseKnownFiles");
27+
(parseKnownFiles as jest.Mock).mockRejectedValue(expectedError);
28+
try {
29+
await fromProcess(mockInit)();
30+
fail(`expected ${expectedError}`);
31+
} catch (error) {
32+
expect(error).toStrictEqual(expectedError);
33+
}
34+
expect(parseKnownFiles).toHaveBeenCalledWith(mockInit);
35+
expect(getMasterProfileName).not.toHaveBeenCalled();
36+
expect(resolveProcessCredentials).not.toHaveBeenCalled();
37+
});
38+
39+
it("rethrows error if resolving process creds fails", async () => {
40+
const expectedError = new Error("from resolveProcessCredentials");
41+
(resolveProcessCredentials as jest.Mock).mockRejectedValue(expectedError);
42+
try {
43+
await fromProcess(mockInit)();
44+
fail(`expected ${expectedError}`);
45+
} catch (error) {
46+
expect(error).toStrictEqual(expectedError);
47+
}
48+
expect(parseKnownFiles).toHaveBeenCalledWith(mockInit);
49+
expect(getMasterProfileName).toHaveBeenCalledWith(mockInit);
50+
expect(resolveProcessCredentials).toHaveBeenCalledWith(mockMasterProfileName, mockProfiles);
51+
});
52+
53+
it("returns resolved process creds", async () => {
54+
const expectedCreds: Credentials = {
55+
accessKeyId: "mockAccessKeyId",
56+
secretAccessKey: "mockSecretAccessKey",
57+
};
58+
(resolveProcessCredentials as jest.Mock).mockResolvedValue(expectedCreds);
59+
const receivedCreds = await fromProcess(mockInit)();
60+
expect(receivedCreds).toStrictEqual(expectedCreds);
61+
expect(parseKnownFiles).toHaveBeenCalledWith(mockInit);
62+
expect(getMasterProfileName).toHaveBeenCalledWith(mockInit);
63+
expect(resolveProcessCredentials).toHaveBeenCalledWith(mockMasterProfileName, mockProfiles);
64+
});
65+
});
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import { CredentialProvider } from "@aws-sdk/types";
2+
import { getMasterProfileName, parseKnownFiles, SourceProfileInit } from "@aws-sdk/util-credentials";
3+
4+
import { resolveProcessCredentials } from "./resolveProcessCredentials";
5+
6+
export interface FromProcessInit extends SourceProfileInit {}
7+
8+
/**
9+
* Creates a credential provider that will read from a credential_process specified
10+
* in ini files.
11+
*/
12+
export const fromProcess =
13+
(init: FromProcessInit = {}): CredentialProvider =>
14+
async () => {
15+
const profiles = await parseKnownFiles(init);
16+
return resolveProcessCredentials(getMasterProfileName(init), profiles);
17+
};
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
import { Credentials } from "@aws-sdk/types";
2+
3+
import { getValidatedProcessCredentials } from "./getValidatedProcessCredentials";
4+
import { ProcessCredentials } from "./ProcessCredentials";
5+
6+
describe(getValidatedProcessCredentials.name, () => {
7+
const mockProfileName = "mockProfileName";
8+
const mockAccessKeyId = "mockAccessKeyId";
9+
const mockSecretAccessKey = "mockSecretAccessKey";
10+
const mockSessionToken = "mockSessionToken";
11+
const mockExpiration = Date.now() + 24 * 60 * 60 * 1000;
12+
13+
const getMockProcessCreds = (): ProcessCredentials => ({
14+
Version: 1,
15+
AccessKeyId: mockAccessKeyId,
16+
SecretAccessKey: mockSecretAccessKey,
17+
SessionToken: mockSessionToken,
18+
Expiration: mockExpiration,
19+
});
20+
21+
it.each([undefined, 2])("throws Error when Version is %s", (Version) => {
22+
expect(() => {
23+
getValidatedProcessCredentials(mockProfileName, {
24+
...getMockProcessCreds(),
25+
Version,
26+
});
27+
}).toThrow(`Profile ${mockProfileName} credential_process did not return Version 1.`);
28+
});
29+
30+
it.each(["AccessKeyId", "SecretAccessKey"])("throws Error when '%s' is not defined", (key) => {
31+
expect(() => {
32+
getValidatedProcessCredentials(mockProfileName, {
33+
...getMockProcessCreds(),
34+
[key]: undefined,
35+
});
36+
}).toThrow(`Profile ${mockProfileName} credential_process returned invalid credentials.`);
37+
});
38+
39+
it("throws error when credentials are expired", () => {
40+
const expirationDayBefore = Date.now() - 24 * 60 * 60 * 1000;
41+
expect(() => {
42+
getValidatedProcessCredentials(mockProfileName, {
43+
...getMockProcessCreds(),
44+
Expiration: expirationDayBefore,
45+
});
46+
}).toThrow(`Profile ${mockProfileName} credential_process returned expired credentials.`);
47+
});
48+
49+
describe("returns validated Process credentials", () => {
50+
const getValidatedCredentials = (data: ProcessCredentials): Credentials => ({
51+
accessKeyId: data.AccessKeyId,
52+
secretAccessKey: data.SecretAccessKey,
53+
...(data.SessionToken && { sessionToken: data.SessionToken }),
54+
...(data.Expiration && { expiration: new Date(data.Expiration) }),
55+
});
56+
57+
it("with all values", () => {
58+
const mockProcessCreds = getMockProcessCreds();
59+
const mockOutputCreds = getValidatedCredentials(mockProcessCreds);
60+
expect(getValidatedProcessCredentials(mockProfileName, mockProcessCreds)).toStrictEqual(mockOutputCreds);
61+
});
62+
63+
it.each(["SessionToken", "Expiration"])("without '%s'", (key) => {
64+
const mockProcessCreds = { ...getMockProcessCreds(), [key]: undefined };
65+
const mockOutputCreds = getValidatedCredentials(mockProcessCreds);
66+
expect(getValidatedProcessCredentials(mockProfileName, mockProcessCreds)).toStrictEqual(mockOutputCreds);
67+
});
68+
});
69+
});
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import { Credentials } from "@aws-sdk/types";
2+
3+
import { ProcessCredentials } from "./ProcessCredentials";
4+
5+
export const getValidatedProcessCredentials = (profileName: string, data: ProcessCredentials): Credentials => {
6+
if (data.Version !== 1) {
7+
throw Error(`Profile ${profileName} credential_process did not return Version 1.`);
8+
}
9+
10+
if (data.AccessKeyId === undefined || data.SecretAccessKey === undefined) {
11+
throw Error(`Profile ${profileName} credential_process returned invalid credentials.`);
12+
}
13+
14+
if (data.Expiration) {
15+
const currentTime = new Date();
16+
const expireTime = new Date(data.Expiration);
17+
if (expireTime < currentTime) {
18+
throw Error(`Profile ${profileName} credential_process returned expired credentials.`);
19+
}
20+
}
21+
22+
return {
23+
accessKeyId: data.AccessKeyId,
24+
secretAccessKey: data.SecretAccessKey,
25+
...(data.SessionToken && { sessionToken: data.SessionToken }),
26+
...(data.Expiration && { expiration: new Date(data.Expiration) }),
27+
};
28+
};

0 commit comments

Comments
 (0)