Skip to content

Commit

Permalink
test(credential-provider-node): additional integ tests for cognito
Browse files Browse the repository at this point in the history
  • Loading branch information
kuhe committed Dec 11, 2024
1 parent c1175ff commit a5a95d8
Show file tree
Hide file tree
Showing 9 changed files with 418 additions and 60 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -68,11 +68,11 @@ export function fromCognitoIdentityPool({
identityId,
});

return provider();
return provider(awsIdentityProperties);
};

return () =>
provider().catch(async (err) => {
return (awsIdentityProperties?: AwsIdentityProperties) =>
provider(awsIdentityProperties).catch(async (err) => {
if (cacheKey) {
Promise.resolve(cache.removeItem(cacheKey)).catch(() => {});
}
Expand Down
4 changes: 3 additions & 1 deletion packages/credential-provider-ini/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,9 @@
"build:types:downlevel": "downlevel-dts dist-types dist-types/ts3.4",
"clean": "rimraf ./dist-* && rimraf *.tsbuildinfo",
"test": "yarn g:vitest run",
"test:watch": "yarn g:vitest watch"
"test:watch": "yarn g:vitest watch",
"test:integration": "yarn g:vitest run -c vitest.config.integ.ts",
"test:integration:watch": "yarn g:vitest watch -c vitest.config.integ.ts"
},
"keywords": [
"aws",
Expand Down
242 changes: 242 additions & 0 deletions packages/credential-provider-ini/src/fromIni.integ.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,242 @@
import { STS } from "@aws-sdk/client-sts";
import { HttpRequest, HttpResponse } from "@smithy/protocol-http";
import { SourceProfileInit } from "@smithy/shared-ini-file-loader";
import type { NodeHttpHandlerOptions, ParsedIniData } from "@smithy/types";
import { PassThrough } from "node:stream";
import { beforeEach, describe, expect, test as it, vi } from "vitest";

import { fromIni } from "./fromIni";

let iniProfileData: ParsedIniData = null as any;
vi.mock("@smithy/shared-ini-file-loader", async () => {
const actual: any = await vi.importActual("@smithy/shared-ini-file-loader");
const pkg = {
...actual,
async loadSsoSessionData() {
return Object.entries(iniProfileData)
.filter(([key]) => key.startsWith("sso-session."))
.reduce(
(acc, [key, value]) => ({
...acc,
[key.split("sso-session.")[1]]: value,
}),
{}
);
},
async parseKnownFiles(init: SourceProfileInit): Promise<ParsedIniData> {
return iniProfileData;
},
async getSSOTokenFromFile() {
return {
accessToken: "mock_sso_token",
expiresAt: "3000-01-01T00:00:00.000Z",
};
},
};
return {
...pkg,
default: pkg,
};
});

class MockNodeHttpHandler {
static create(instanceOrOptions?: any) {
if (typeof instanceOrOptions?.handle === "function") {
return instanceOrOptions;
}
return new MockNodeHttpHandler();
}
async handle(request: HttpRequest) {
const body = new PassThrough({});

const region = (request.hostname.match(/sts\.(.*?)\./) || [, "unknown"])[1];

if (request.headers.Authorization === "container-authorization") {
body.write(
JSON.stringify({
AccessKeyId: "CONTAINER_ACCESS_KEY",
SecretAccessKey: "CONTAINER_SECRET_ACCESS_KEY",
Token: "CONTAINER_TOKEN",
Expiration: "3000-01-01T00:00:00.000Z",
})
);
} else if (request.path?.includes("/federation/credentials")) {
body.write(
JSON.stringify({
roleCredentials: {
accessKeyId: "SSO_ACCESS_KEY_ID",
secretAccessKey: "SSO_SECRET_ACCESS_KEY",
sessionToken: "SSO_SESSION_TOKEN",
expiration: "3000-01-01T00:00:00.000Z",
},
})
);
} else if (request.body?.includes("Action=AssumeRoleWithWebIdentity")) {
body.write(`
<AssumeRoleWithWebIdentityResponse xmlns="https://sts.amazonaws.com/doc/2011-06-15/">
<AssumeRoleWithWebIdentityResult>
<Credentials>
<AccessKeyId>STS_ARWI_ACCESS_KEY_ID</AccessKeyId>
<SecretAccessKey>STS_ARWI_SECRET_ACCESS_KEY</SecretAccessKey>
<SessionToken>STS_ARWI_SESSION_TOKEN_${region}</SessionToken>
<Expiration>3000-01-01T00:00:00.000Z</Expiration>
</Credentials>
</AssumeRoleWithWebIdentityResult>
<ResponseMetadata>
<RequestId>01234567-89ab-cdef-0123-456789abcdef</RequestId>
</ResponseMetadata>
</AssumeRoleWithWebIdentityResponse>`);
} else if (request.body?.includes("Action=AssumeRole")) {
body.write(`
<AssumeRoleResponse xmlns="https://sts.amazonaws.com/doc/2011-06-15/">
<AssumeRoleResult>
<Credentials>
<AccessKeyId>STS_AR_ACCESS_KEY_ID</AccessKeyId>
<SecretAccessKey>STS_AR_SECRET_ACCESS_KEY</SecretAccessKey>
<SessionToken>STS_AR_SESSION_TOKEN_${region}</SessionToken>
<Expiration>3000-01-01T00:00:00.000Z</Expiration>
</Credentials>
</AssumeRoleResult>
<ResponseMetadata>
<RequestId>01234567-89ab-cdef-0123-456789abcdef</RequestId>
</ResponseMetadata>
</AssumeRoleResponse>`);
} else if (request.body.includes("Action=GetCallerIdentity")) {
body.write(`
<GetCallerIdentityResponse xmlns="https://sts.amazonaws.com/doc/2011-06-15/">
<GetCallerIdentityResult>
<Arn>arn:aws:iam::123456789012:user/Alice</Arn>
<UserId>AIDACKCEVSQ6C2EXAMPLE</UserId>
<Account>123456789012</Account>
</GetCallerIdentityResult>
<ResponseMetadata>
<RequestId>01234567-89ab-cdef-0123-456789abcdef</RequestId>
</ResponseMetadata>
</GetCallerIdentityResponse>`);
} else {
throw new Error("request not supported.");
}
body.end();
return {
response: new HttpResponse({
statusCode: 200,
body,
headers: {},
}),
};
}
updateHttpClientConfig(key: keyof NodeHttpHandlerOptions, value: NodeHttpHandlerOptions[typeof key]): void {}
httpHandlerConfigs(): NodeHttpHandlerOptions {
return null as any;
}
}

describe("fromIni region search order", () => {
beforeEach(() => {
iniProfileData = {
default: {
region: "us-west-2",
output: "json",
},
};
iniProfileData.assume = {
region: "us-stsar-1",
aws_access_key_id: "ASSUME_STATIC_ACCESS_KEY",
aws_secret_access_key: "ASSUME_STATIC_SECRET_KEY",
};
Object.assign(iniProfileData.default, {
region: "us-stsar-1",
role_arn: "ROLE_ARN",
role_session_name: "ROLE_SESSION_NAME",
external_id: "EXTERNAL_ID",
source_profile: "assume",
});
});

it("should use 1st priority for the clientConfig given to the provider factory", async () => {
const sts = new STS({
requestHandler: new MockNodeHttpHandler(),
region: "ap-northeast-2",
credentials: fromIni({
clientConfig: {
requestHandler: new MockNodeHttpHandler(),
region: "ap-northeast-1",
},
}),
});

await sts.getCallerIdentity({});
const credentials = await sts.config.credentials();
expect(credentials).toContain({
accessKeyId: "STS_AR_ACCESS_KEY_ID",
secretAccessKey: "STS_AR_SECRET_ACCESS_KEY",
sessionToken: "STS_AR_SESSION_TOKEN_ap-northeast-1",
});
});

it("should use 2nd priority for the context client", async () => {
const sts = new STS({
requestHandler: new MockNodeHttpHandler(),
region: "ap-northeast-2",
credentials: fromIni({
clientConfig: {
requestHandler: new MockNodeHttpHandler(),
},
}),
});

await sts.getCallerIdentity({});
const credentials = await sts.config.credentials();
expect(credentials).toContain({
accessKeyId: "STS_AR_ACCESS_KEY_ID",
secretAccessKey: "STS_AR_SECRET_ACCESS_KEY",
sessionToken: "STS_AR_SESSION_TOKEN_ap-northeast-2",
});
});

it("should use 3rd priority for the profile region if not used in the context of a client with a region", async () => {
const credentialsData = await fromIni({
clientConfig: {
requestHandler: new MockNodeHttpHandler(),
},
})();

const sts = new STS({
requestHandler: new MockNodeHttpHandler(),
region: "ap-northeast-2",
credentials: credentialsData,
});

await sts.getCallerIdentity({});
const credentials = await sts.config.credentials();
expect(credentials).toContain({
accessKeyId: "STS_AR_ACCESS_KEY_ID",
secretAccessKey: "STS_AR_SECRET_ACCESS_KEY",
sessionToken: "STS_AR_SESSION_TOKEN_us-stsar-1",
});
});

it("should use 4th priority for the default partition's default region", async () => {
delete iniProfileData.default.region;

const credentialsData = await fromIni({
clientConfig: {
requestHandler: new MockNodeHttpHandler(),
},
})();

const sts = new STS({
requestHandler: new MockNodeHttpHandler(),
region: "ap-northeast-2",
credentials: credentialsData,
});

await sts.getCallerIdentity({});
const credentials = await sts.config.credentials();
expect(credentials).toContain({
accessKeyId: "STS_AR_ACCESS_KEY_ID",
secretAccessKey: "STS_AR_SECRET_ACCESS_KEY",
sessionToken: "STS_AR_SESSION_TOKEN_us-east-1",
});
});
});
10 changes: 6 additions & 4 deletions packages/credential-provider-ini/src/fromIni.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,11 +60,13 @@ export const fromIni =
async (props = {}) => {
const init: FromIniInit = {
..._init,
parentClientConfig: {
region: props.contextClientConfig?.region,
..._init.parentClientConfig,
},
};
if (props.contextClientConfig?.region) {
init.parentClientConfig = {
region: props.contextClientConfig.region,
..._init.parentClientConfig,
};
}
init.logger?.debug("@aws-sdk/credential-provider-ini - fromIni");
const profiles = await parseKnownFiles(init);
return resolveProfileData(getProfileName(init), profiles, init);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,8 @@ export const resolveAssumeRoleCredentials = async (
visitedProfiles: Record<string, true> = {}
) => {
options.logger?.debug("@aws-sdk/credential-provider-ini - resolveAssumeRoleCredentials (STS)");
const data = profiles[profileName];
const profileData = profiles[profileName];
const { source_profile, region } = profileData;

if (!options.roleAssumer) {
// @ts-ignore Cannot find module '@aws-sdk/client-sts'
Expand All @@ -116,13 +117,18 @@ export const resolveAssumeRoleCredentials = async (
{
...options.clientConfig,
credentialProviderLogger: options.logger,
parentClientConfig: options?.parentClientConfig,
parentClientConfig: {
...options?.parentClientConfig,
// The profile region is the last fallback, and only applies
// if the clientConfig.region is not defined by the user
// and no contextual outer client configuration region can be found.
region: options?.parentClientConfig?.region ?? region,
},
},
options.clientPlugins
);
}

const { source_profile } = data;
if (source_profile && source_profile in visitedProfiles) {
throw new CredentialsProviderError(
`Detected a cycle attempting to resolve credentials for profile` +
Expand All @@ -149,9 +155,9 @@ export const resolveAssumeRoleCredentials = async (
},
isCredentialSourceWithoutRoleArn(profiles[source_profile!] ?? {})
)
: (await resolveCredentialSource(data.credential_source!, profileName, options.logger)(options))();
: (await resolveCredentialSource(profileData.credential_source!, profileName, options.logger)(options))();

if (isCredentialSourceWithoutRoleArn(data)) {
if (isCredentialSourceWithoutRoleArn(profileData)) {
/**
* This control-flow branch is accessed when in a chained source_profile
* scenario, and the last step of the chain is a credential_source
Expand All @@ -163,13 +169,13 @@ export const resolveAssumeRoleCredentials = async (
return sourceCredsProvider.then((creds) => setCredentialFeature(creds, "CREDENTIALS_PROFILE_SOURCE_PROFILE", "o"));
} else {
const params: AssumeRoleParams = {
RoleArn: data.role_arn!,
RoleSessionName: data.role_session_name || `aws-sdk-js-${Date.now()}`,
ExternalId: data.external_id,
DurationSeconds: parseInt(data.duration_seconds || "3600", 10),
RoleArn: profileData.role_arn!,
RoleSessionName: profileData.role_session_name || `aws-sdk-js-${Date.now()}`,
ExternalId: profileData.external_id,
DurationSeconds: parseInt(profileData.duration_seconds || "3600", 10),
};

const { mfa_serial } = data;
const { mfa_serial } = profileData;
if (mfa_serial) {
if (!options.mfaCodeProvider) {
throw new CredentialsProviderError(
Expand Down
8 changes: 8 additions & 0 deletions packages/credential-provider-ini/vitest.config.integ.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { defineConfig } from "vitest/config";

export default defineConfig({
test: {
include: ["**/*.integ.spec.ts"],
environment: "node",
},
});
Loading

0 comments on commit a5a95d8

Please sign in to comment.