Skip to content

Commit

Permalink
feat(credential-provider-sso): refactor into modular components (#3296)
Browse files Browse the repository at this point in the history
  • Loading branch information
trivikr authored Feb 14, 2022
1 parent c376e57 commit eece76f
Show file tree
Hide file tree
Showing 11 changed files with 562 additions and 439 deletions.
125 changes: 125 additions & 0 deletions packages/credential-provider-sso/src/fromSSO.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
import { SSOClient } from "@aws-sdk/client-sso";
import { CredentialsProviderError } from "@aws-sdk/property-provider";
import { getMasterProfileName, parseKnownFiles } from "@aws-sdk/util-credentials";

import { fromSSO } from "./fromSSO";
import { isSsoProfile } from "./isSsoProfile";
import { resolveSSOCredentials } from "./resolveSSOCredentials";
import { validateSsoProfile } from "./validateSsoProfile";

jest.mock("@aws-sdk/util-credentials");
jest.mock("./isSsoProfile");
jest.mock("./resolveSSOCredentials");
jest.mock("./validateSsoProfile");

describe(fromSSO.name, () => {
const mockSsoClient = {} as SSOClient;

const mockSsoProfile = {
ssoStartUrl: "mock_sso_start_url",
ssoAccountId: "mock_sso_account_id",
ssoRegion: "mock_sso_region",
ssoRoleName: "mock_sso_role_name",
};

const mockCreds = {
accessKeyId: "mockAccessKeyId",
secretAccessKey: "mockSecretAccessKey",
};

beforeEach(() => {
(resolveSSOCredentials as jest.Mock).mockResolvedValue(mockCreds);
});

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

describe("all sso* values are not set", () => {
const mockProfileName = "mockProfileName";
const mockInit = { profile: mockProfileName };
const mockProfiles = { [mockProfileName]: mockSsoProfile };

beforeEach(() => {
(parseKnownFiles as jest.Mock).mockResolvedValue(mockProfiles);
(getMasterProfileName as jest.Mock).mockReturnValue(mockProfileName);
(isSsoProfile as unknown as jest.Mock).mockReturnValue(true);
});

afterEach(() => {
expect(parseKnownFiles).toHaveBeenCalledWith(mockInit);
expect(getMasterProfileName).toHaveBeenCalledWith(mockInit);
expect(isSsoProfile).toHaveBeenCalledWith(mockSsoProfile);
});

it("throws error if profile is not an Sso Profile", async () => {
(isSsoProfile as unknown as jest.Mock).mockReturnValue(false);
const expectedError = new CredentialsProviderError(
`Profile ${mockProfileName} is not configured with SSO credentials.`
);

try {
await fromSSO(mockInit)();
fail(`expected ${expectedError}`);
} catch (error) {
expect(error).toStrictEqual(expectedError);
}
});

it("throws error if Sso Profile validation fails", async () => {
const expectedError = new Error("error");
(validateSsoProfile as jest.Mock).mockImplementation(() => {
throw expectedError;
});

try {
await fromSSO(mockInit)();
fail(`expected ${expectedError}`);
} catch (error) {
expect(error).toStrictEqual(expectedError);
}
expect(validateSsoProfile).toHaveBeenCalledWith(mockSsoProfile);
});

it("calls resolveSSOCredentials with values from validated Sso profile", async () => {
const mockValidatedSsoProfile = {
sso_start_url: "mock_sso_start_url",
sso_account_id: "mock_sso_account_id",
sso_region: "mock_sso_region",
sso_role_name: "mock_sso_role_name",
};
(validateSsoProfile as jest.Mock).mockReturnValue(mockValidatedSsoProfile);

const receivedCreds = await fromSSO(mockInit)();
expect(receivedCreds).toStrictEqual(mockCreds);
expect(resolveSSOCredentials).toHaveBeenCalledWith({
ssoStartUrl: mockValidatedSsoProfile.sso_start_url,
ssoAccountId: mockValidatedSsoProfile.sso_account_id,
ssoRegion: mockValidatedSsoProfile.sso_region,
ssoRoleName: mockValidatedSsoProfile.sso_role_name,
});
});
});

describe("throws error if any required sso* values are not set", () => {
it.each(["ssoStartUrl", "ssoAccountId", "ssoRegion", "ssoRoleName"])("missing '%s'", async (key) => {
const expectedError = new CredentialsProviderError(
'Incomplete configuration. The fromSSO() argument hash must include "ssoStartUrl",' +
' "ssoAccountId", "ssoRegion", "ssoRoleName"'
);
try {
await fromSSO({ ...mockSsoProfile, [key]: undefined })();
fail(`expected ${expectedError}`);
} catch (error) {
expect(error).toStrictEqual(expectedError);
}
});
});

it("calls resolveSSOCredentials if all sso* values are set", async () => {
const mockOptions = { ...mockSsoProfile, ssoClient: mockSsoClient };
const receivedCreds = await fromSSO(mockOptions)();
expect(receivedCreds).toStrictEqual(mockCreds);
expect(resolveSSOCredentials).toHaveBeenCalledWith(mockOptions);
});
});
70 changes: 70 additions & 0 deletions packages/credential-provider-sso/src/fromSSO.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import { SSOClient } from "@aws-sdk/client-sso";
import { CredentialsProviderError } from "@aws-sdk/property-provider";
import { CredentialProvider } from "@aws-sdk/types";
import { getMasterProfileName, parseKnownFiles, SourceProfileInit } from "@aws-sdk/util-credentials";

import { isSsoProfile } from "./isSsoProfile";
import { resolveSSOCredentials } from "./resolveSSOCredentials";
import { validateSsoProfile } from "./validateSsoProfile";

export interface SsoCredentialsParameters {
/**
* The URL to the AWS SSO service.
*/
ssoStartUrl: string;

/**
* The ID of the AWS account to use for temporary credentials.
*/
ssoAccountId: string;

/**
* The AWS region to use for temporary credentials.
*/
ssoRegion: string;

/**
* The name of the AWS role to assume.
*/
ssoRoleName: string;
}

export interface FromSSOInit extends SourceProfileInit {
ssoClient?: SSOClient;
}

/**
* Creates a credential provider that will read from a credential_process specified
* in ini files.
*/
export const fromSSO =
(init: FromSSOInit & Partial<SsoCredentialsParameters> = {}): CredentialProvider =>
async () => {
const { ssoStartUrl, ssoAccountId, ssoRegion, ssoRoleName, ssoClient } = init;
if (!ssoStartUrl && !ssoAccountId && !ssoRegion && !ssoRoleName) {
// Load the SSO config from shared AWS config file.
const profiles = await parseKnownFiles(init);
const profileName = getMasterProfileName(init);
const profile = profiles[profileName];

if (!isSsoProfile(profile)) {
throw new CredentialsProviderError(`Profile ${profileName} is not configured with SSO credentials.`);
}

const { sso_start_url, sso_account_id, sso_region, sso_role_name } = validateSsoProfile(profile);
return resolveSSOCredentials({
ssoStartUrl: sso_start_url,
ssoAccountId: sso_account_id,
ssoRegion: sso_region,
ssoRoleName: sso_role_name,
ssoClient: ssoClient,
});
} else if (!ssoStartUrl || !ssoAccountId || !ssoRegion || !ssoRoleName) {
throw new CredentialsProviderError(
'Incomplete configuration. The fromSSO() argument hash must include "ssoStartUrl",' +
' "ssoAccountId", "ssoRegion", "ssoRoleName"'
);
} else {
return resolveSSOCredentials({ ssoStartUrl, ssoAccountId, ssoRegion, ssoRoleName, ssoClient });
}
};
Loading

0 comments on commit eece76f

Please sign in to comment.