Skip to content

Commit eece76f

Browse files
authored
feat(credential-provider-sso): refactor into modular components (#3296)
1 parent c376e57 commit eece76f

File tree

11 files changed

+562
-439
lines changed

11 files changed

+562
-439
lines changed
Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
import { SSOClient } from "@aws-sdk/client-sso";
2+
import { CredentialsProviderError } from "@aws-sdk/property-provider";
3+
import { getMasterProfileName, parseKnownFiles } from "@aws-sdk/util-credentials";
4+
5+
import { fromSSO } from "./fromSSO";
6+
import { isSsoProfile } from "./isSsoProfile";
7+
import { resolveSSOCredentials } from "./resolveSSOCredentials";
8+
import { validateSsoProfile } from "./validateSsoProfile";
9+
10+
jest.mock("@aws-sdk/util-credentials");
11+
jest.mock("./isSsoProfile");
12+
jest.mock("./resolveSSOCredentials");
13+
jest.mock("./validateSsoProfile");
14+
15+
describe(fromSSO.name, () => {
16+
const mockSsoClient = {} as SSOClient;
17+
18+
const mockSsoProfile = {
19+
ssoStartUrl: "mock_sso_start_url",
20+
ssoAccountId: "mock_sso_account_id",
21+
ssoRegion: "mock_sso_region",
22+
ssoRoleName: "mock_sso_role_name",
23+
};
24+
25+
const mockCreds = {
26+
accessKeyId: "mockAccessKeyId",
27+
secretAccessKey: "mockSecretAccessKey",
28+
};
29+
30+
beforeEach(() => {
31+
(resolveSSOCredentials as jest.Mock).mockResolvedValue(mockCreds);
32+
});
33+
34+
afterEach(() => {
35+
jest.clearAllMocks();
36+
});
37+
38+
describe("all sso* values are not set", () => {
39+
const mockProfileName = "mockProfileName";
40+
const mockInit = { profile: mockProfileName };
41+
const mockProfiles = { [mockProfileName]: mockSsoProfile };
42+
43+
beforeEach(() => {
44+
(parseKnownFiles as jest.Mock).mockResolvedValue(mockProfiles);
45+
(getMasterProfileName as jest.Mock).mockReturnValue(mockProfileName);
46+
(isSsoProfile as unknown as jest.Mock).mockReturnValue(true);
47+
});
48+
49+
afterEach(() => {
50+
expect(parseKnownFiles).toHaveBeenCalledWith(mockInit);
51+
expect(getMasterProfileName).toHaveBeenCalledWith(mockInit);
52+
expect(isSsoProfile).toHaveBeenCalledWith(mockSsoProfile);
53+
});
54+
55+
it("throws error if profile is not an Sso Profile", async () => {
56+
(isSsoProfile as unknown as jest.Mock).mockReturnValue(false);
57+
const expectedError = new CredentialsProviderError(
58+
`Profile ${mockProfileName} is not configured with SSO credentials.`
59+
);
60+
61+
try {
62+
await fromSSO(mockInit)();
63+
fail(`expected ${expectedError}`);
64+
} catch (error) {
65+
expect(error).toStrictEqual(expectedError);
66+
}
67+
});
68+
69+
it("throws error if Sso Profile validation fails", async () => {
70+
const expectedError = new Error("error");
71+
(validateSsoProfile as jest.Mock).mockImplementation(() => {
72+
throw expectedError;
73+
});
74+
75+
try {
76+
await fromSSO(mockInit)();
77+
fail(`expected ${expectedError}`);
78+
} catch (error) {
79+
expect(error).toStrictEqual(expectedError);
80+
}
81+
expect(validateSsoProfile).toHaveBeenCalledWith(mockSsoProfile);
82+
});
83+
84+
it("calls resolveSSOCredentials with values from validated Sso profile", async () => {
85+
const mockValidatedSsoProfile = {
86+
sso_start_url: "mock_sso_start_url",
87+
sso_account_id: "mock_sso_account_id",
88+
sso_region: "mock_sso_region",
89+
sso_role_name: "mock_sso_role_name",
90+
};
91+
(validateSsoProfile as jest.Mock).mockReturnValue(mockValidatedSsoProfile);
92+
93+
const receivedCreds = await fromSSO(mockInit)();
94+
expect(receivedCreds).toStrictEqual(mockCreds);
95+
expect(resolveSSOCredentials).toHaveBeenCalledWith({
96+
ssoStartUrl: mockValidatedSsoProfile.sso_start_url,
97+
ssoAccountId: mockValidatedSsoProfile.sso_account_id,
98+
ssoRegion: mockValidatedSsoProfile.sso_region,
99+
ssoRoleName: mockValidatedSsoProfile.sso_role_name,
100+
});
101+
});
102+
});
103+
104+
describe("throws error if any required sso* values are not set", () => {
105+
it.each(["ssoStartUrl", "ssoAccountId", "ssoRegion", "ssoRoleName"])("missing '%s'", async (key) => {
106+
const expectedError = new CredentialsProviderError(
107+
'Incomplete configuration. The fromSSO() argument hash must include "ssoStartUrl",' +
108+
' "ssoAccountId", "ssoRegion", "ssoRoleName"'
109+
);
110+
try {
111+
await fromSSO({ ...mockSsoProfile, [key]: undefined })();
112+
fail(`expected ${expectedError}`);
113+
} catch (error) {
114+
expect(error).toStrictEqual(expectedError);
115+
}
116+
});
117+
});
118+
119+
it("calls resolveSSOCredentials if all sso* values are set", async () => {
120+
const mockOptions = { ...mockSsoProfile, ssoClient: mockSsoClient };
121+
const receivedCreds = await fromSSO(mockOptions)();
122+
expect(receivedCreds).toStrictEqual(mockCreds);
123+
expect(resolveSSOCredentials).toHaveBeenCalledWith(mockOptions);
124+
});
125+
});
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
import { SSOClient } from "@aws-sdk/client-sso";
2+
import { CredentialsProviderError } from "@aws-sdk/property-provider";
3+
import { CredentialProvider } from "@aws-sdk/types";
4+
import { getMasterProfileName, parseKnownFiles, SourceProfileInit } from "@aws-sdk/util-credentials";
5+
6+
import { isSsoProfile } from "./isSsoProfile";
7+
import { resolveSSOCredentials } from "./resolveSSOCredentials";
8+
import { validateSsoProfile } from "./validateSsoProfile";
9+
10+
export interface SsoCredentialsParameters {
11+
/**
12+
* The URL to the AWS SSO service.
13+
*/
14+
ssoStartUrl: string;
15+
16+
/**
17+
* The ID of the AWS account to use for temporary credentials.
18+
*/
19+
ssoAccountId: string;
20+
21+
/**
22+
* The AWS region to use for temporary credentials.
23+
*/
24+
ssoRegion: string;
25+
26+
/**
27+
* The name of the AWS role to assume.
28+
*/
29+
ssoRoleName: string;
30+
}
31+
32+
export interface FromSSOInit extends SourceProfileInit {
33+
ssoClient?: SSOClient;
34+
}
35+
36+
/**
37+
* Creates a credential provider that will read from a credential_process specified
38+
* in ini files.
39+
*/
40+
export const fromSSO =
41+
(init: FromSSOInit & Partial<SsoCredentialsParameters> = {}): CredentialProvider =>
42+
async () => {
43+
const { ssoStartUrl, ssoAccountId, ssoRegion, ssoRoleName, ssoClient } = init;
44+
if (!ssoStartUrl && !ssoAccountId && !ssoRegion && !ssoRoleName) {
45+
// Load the SSO config from shared AWS config file.
46+
const profiles = await parseKnownFiles(init);
47+
const profileName = getMasterProfileName(init);
48+
const profile = profiles[profileName];
49+
50+
if (!isSsoProfile(profile)) {
51+
throw new CredentialsProviderError(`Profile ${profileName} is not configured with SSO credentials.`);
52+
}
53+
54+
const { sso_start_url, sso_account_id, sso_region, sso_role_name } = validateSsoProfile(profile);
55+
return resolveSSOCredentials({
56+
ssoStartUrl: sso_start_url,
57+
ssoAccountId: sso_account_id,
58+
ssoRegion: sso_region,
59+
ssoRoleName: sso_role_name,
60+
ssoClient: ssoClient,
61+
});
62+
} else if (!ssoStartUrl || !ssoAccountId || !ssoRegion || !ssoRoleName) {
63+
throw new CredentialsProviderError(
64+
'Incomplete configuration. The fromSSO() argument hash must include "ssoStartUrl",' +
65+
' "ssoAccountId", "ssoRegion", "ssoRoleName"'
66+
);
67+
} else {
68+
return resolveSSOCredentials({ ssoStartUrl, ssoAccountId, ssoRegion, ssoRoleName, ssoClient });
69+
}
70+
};

0 commit comments

Comments
 (0)