Skip to content

Commit

Permalink
chore(credential-providers): add credential attribution (#6546)
Browse files Browse the repository at this point in the history
* chore(credential-providers): add credential attribution

* chore(credential-providers): attribute credential feature sources

* test(credential-provider-node): add credential source assertions

* test: fix unit test

* test(credential-providers): fix unit tests

* chore(core): separate function files

* chore: undo script change

* chore: change Promise.resolve to async fn
  • Loading branch information
kuhe authored Oct 8, 2024
1 parent 7a98e5f commit 089f1a4
Show file tree
Hide file tree
Showing 38 changed files with 326 additions and 63 deletions.
12 changes: 10 additions & 2 deletions clients/client-sts/src/defaultStsRoleAssumers.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
// smithy-typescript generated code
// Please do not touch this file. It's generated from template in:
// https://github.com/aws/aws-sdk-js-v3/blob/main/codegen/smithy-aws-typescript-codegen/src/main/resources/software/amazon/smithy/aws/typescript/codegen/sts-client-defaultStsRoleAssumers.ts
import { setCredentialFeature } from "@aws-sdk/core/client";
import type { CredentialProviderOptions } from "@aws-sdk/types";
import { AwsCredentialIdentity, Logger, Provider } from "@smithy/types";

Expand Down Expand Up @@ -118,7 +119,7 @@ export const getDefaultRoleAssumer = (

const accountId = getAccountIdFromAssumedRoleUser(AssumedRoleUser);

return {
const credentials = {
accessKeyId: Credentials.AccessKeyId,
secretAccessKey: Credentials.SecretAccessKey,
sessionToken: Credentials.SessionToken,
Expand All @@ -127,6 +128,8 @@ export const getDefaultRoleAssumer = (
...((Credentials as any).CredentialScope && { credentialScope: (Credentials as any).CredentialScope }),
...(accountId && { accountId }),
};
setCredentialFeature(credentials, "CREDENTIALS_STS_ASSUME_ROLE", "i");
return credentials;
};
};

Expand Down Expand Up @@ -174,7 +177,7 @@ export const getDefaultRoleAssumerWithWebIdentity = (

const accountId = getAccountIdFromAssumedRoleUser(AssumedRoleUser);

return {
const credentials = {
accessKeyId: Credentials.AccessKeyId,
secretAccessKey: Credentials.SecretAccessKey,
sessionToken: Credentials.SessionToken,
Expand All @@ -183,6 +186,11 @@ export const getDefaultRoleAssumerWithWebIdentity = (
...((Credentials as any).CredentialScope && { credentialScope: (Credentials as any).CredentialScope }),
...(accountId && { accountId }),
};
if (accountId) {
setCredentialFeature(credentials, "RESOLVED_ACCOUNT_ID", "T");
}
setCredentialFeature(credentials, "CREDENTIALS_STS_ASSUME_ROLE_WEB_ID", "k");
return credentials;
};
};

Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { setCredentialFeature } from "@aws-sdk/core/client";
import type { CredentialProviderOptions } from "@aws-sdk/types";
import { AwsCredentialIdentity, Logger, Provider } from "@smithy/types";

Expand Down Expand Up @@ -115,7 +116,7 @@ export const getDefaultRoleAssumer = (

const accountId = getAccountIdFromAssumedRoleUser(AssumedRoleUser);

return {
const credentials = {
accessKeyId: Credentials.AccessKeyId,
secretAccessKey: Credentials.SecretAccessKey,
sessionToken: Credentials.SessionToken,
Expand All @@ -124,6 +125,8 @@ export const getDefaultRoleAssumer = (
...((Credentials as any).CredentialScope && { credentialScope: (Credentials as any).CredentialScope }),
...(accountId && { accountId }),
};
setCredentialFeature(credentials, "CREDENTIALS_STS_ASSUME_ROLE", "i");
return credentials;
};
};

Expand Down Expand Up @@ -171,7 +174,7 @@ export const getDefaultRoleAssumerWithWebIdentity = (

const accountId = getAccountIdFromAssumedRoleUser(AssumedRoleUser);

return {
const credentials = {
accessKeyId: Credentials.AccessKeyId,
secretAccessKey: Credentials.SecretAccessKey,
sessionToken: Credentials.SessionToken,
Expand All @@ -180,6 +183,11 @@ export const getDefaultRoleAssumerWithWebIdentity = (
...((Credentials as any).CredentialScope && { credentialScope: (Credentials as any).CredentialScope }),
...(accountId && { accountId }),
};
if (accountId) {
setCredentialFeature(credentials, "RESOLVED_ACCOUNT_ID", "T");
}
setCredentialFeature(credentials, "CREDENTIALS_STS_ASSUME_ROLE_WEB_ID", "k");
return credentials;
};
};

Expand Down
1 change: 1 addition & 0 deletions packages/core/src/submodules/client/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
export * from "./emitWarningIfUnsupportedVersion";
export * from "./setCredentialFeature";
export * from "./setFeature";
40 changes: 40 additions & 0 deletions packages/core/src/submodules/client/setCredentialFeature.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import { AttributedAwsCredentialIdentity } from "@aws-sdk/types";

import { setCredentialFeature } from "./setCredentialFeature";

describe(setCredentialFeature.name, () => {
it("should create the data path if it does't exist", () => {
const credentials = {
accessKeyId: "",
secretAccessKey: "",
} as AttributedAwsCredentialIdentity;
expect(setCredentialFeature(credentials, "CREDENTIALS_CODE", "e")).toEqual({
accessKeyId: "",
secretAccessKey: "",
$source: {
CREDENTIALS_CODE: "e",
},
});
});

it("should track a set of features", () => {
const credentials = {
accessKeyId: "",
secretAccessKey: "",
} as AttributedAwsCredentialIdentity;

setCredentialFeature(credentials, "CREDENTIALS_CODE", "e");
setCredentialFeature(credentials, "CREDENTIALS_ENV_VARS", "g");
// it ignores duplicates.
setCredentialFeature(credentials, "CREDENTIALS_ENV_VARS", "g");

expect(setCredentialFeature(credentials, "CREDENTIALS_CODE", "e")).toEqual({
accessKeyId: "",
secretAccessKey: "",
$source: {
CREDENTIALS_CODE: "e",
CREDENTIALS_ENV_VARS: "g",
},
});
});
});
18 changes: 18 additions & 0 deletions packages/core/src/submodules/client/setCredentialFeature.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import type { AttributedAwsCredentialIdentity, AwsSdkCredentialsFeatures } from "@aws-sdk/types";

/**
* @internal
*
* @returns the credentials with source feature attribution.
*/
export function setCredentialFeature<F extends keyof AwsSdkCredentialsFeatures>(
credentials: AttributedAwsCredentialIdentity,
feature: F,
value: AwsSdkCredentialsFeatures[F]
): AttributedAwsCredentialIdentity {
if (!credentials.$source) {
credentials.$source = {};
}
credentials.$source![feature] = value;
return credentials;
}
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { setCredentialFeature } from "@aws-sdk/core/client";
import { AttributedAwsCredentialIdentity } from "@aws-sdk/types";
import {
doesIdentityRequireRefresh,
isIdentityExpired,
Expand Down Expand Up @@ -102,9 +104,11 @@ export interface AwsSdkSigV4AuthResolvedConfig {
export const resolveAwsSdkSigV4Config = <T>(
config: T & AwsSdkSigV4AuthInputConfig & AwsSdkSigV4PreviouslyResolved
): T & AwsSdkSigV4AuthResolvedConfig => {
let isUserSupplied = false;
// Normalize credentials
let normalizedCreds: AwsCredentialIdentityProvider | undefined;
if (config.credentials) {
isUserSupplied = true;
normalizedCreds = memoizeIdentityProvider(config.credentials, isIdentityExpired, doesIdentityRequireRefresh);
}
if (!normalizedCreds) {
Expand Down Expand Up @@ -218,7 +222,12 @@ export const resolveAwsSdkSigV4Config = <T>(
...config,
systemClockOffset,
signingEscapePath,
credentials: normalizedCreds!,
credentials: isUserSupplied
? async () =>
normalizedCreds!().then((creds: AttributedAwsCredentialIdentity) =>
setCredentialFeature(creds, "CREDENTIALS_CODE", "e")
)
: normalizedCreds!,
signer,
};
};
Expand Down
1 change: 1 addition & 0 deletions packages/credential-provider-env/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
},
"license": "Apache-2.0",
"dependencies": {
"@aws-sdk/core": "*",
"@aws-sdk/types": "*",
"@smithy/property-provider": "^3.1.7",
"@smithy/types": "^3.5.0",
Expand Down
6 changes: 6 additions & 0 deletions packages/credential-provider-env/src/fromEnv.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,9 @@ describe(fromEnv.name, () => {
sessionToken: mockSessionToken,
expiration: new Date(mockExpiration),
accountId: mockAccountId,
$source: {
CREDENTIALS_ENV_VARS: "g",
},
});
});

Expand All @@ -44,6 +47,9 @@ describe(fromEnv.name, () => {
expect(receivedCreds).toStrictEqual({
accessKeyId: mockAccessKeyId,
secretAccessKey: mockSecretAccessKey,
$source: {
CREDENTIALS_ENV_VARS: "g",
},
});
});

Expand Down
9 changes: 6 additions & 3 deletions packages/credential-provider-env/src/fromEnv.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import type { CredentialProviderOptions } from "@aws-sdk/types";
import { setCredentialFeature } from "@aws-sdk/core/client";
import type { AttributedAwsCredentialIdentity, CredentialProviderOptions } from "@aws-sdk/types";
import { CredentialsProviderError } from "@smithy/property-provider";
import { AwsCredentialIdentityProvider } from "@smithy/types";

Expand Down Expand Up @@ -48,14 +49,16 @@ export const fromEnv =
const accountId: string | undefined = process.env[ENV_ACCOUNT_ID];

if (accessKeyId && secretAccessKey) {
return {
const credentials = {
accessKeyId,
secretAccessKey,
...(sessionToken && { sessionToken }),
...(expiry && { expiration: new Date(expiry) }),
...(credentialScope && { credentialScope }),
...(accountId && { accountId }),
};
} as AttributedAwsCredentialIdentity;
setCredentialFeature(credentials, "CREDENTIALS_ENV_VARS", "g");
return credentials;
}

throw new CredentialsProviderError("Unable to find environment variable credentials.", { logger: init?.logger });
Expand Down
1 change: 1 addition & 0 deletions packages/credential-provider-http/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
},
"license": "Apache-2.0",
"dependencies": {
"@aws-sdk/core": "*",
"@aws-sdk/types": "*",
"@smithy/fetch-http-handler": "^3.2.9",
"@smithy/node-http-handler": "^3.2.4",
Expand Down
3 changes: 2 additions & 1 deletion packages/credential-provider-http/src/fromHttp/fromHttp.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { setCredentialFeature } from "@aws-sdk/core/client";
import { NodeHttpHandler } from "@smithy/node-http-handler";
import { CredentialsProviderError } from "@smithy/property-provider";
import { AwsCredentialIdentity, AwsCredentialIdentityProvider } from "@smithy/types";
Expand Down Expand Up @@ -81,7 +82,7 @@ Set AWS_CONTAINER_CREDENTIALS_FULL_URI or AWS_CONTAINER_CREDENTIALS_RELATIVE_URI
}
try {
const result = await requestHandler.handle(request);
return getCredentials(result.response);
return getCredentials(result.response).then((creds) => setCredentialFeature(creds, "CREDENTIALS_HTTP", "z"));
} catch (e: unknown) {
throw new CredentialsProviderError(String(e), { logger: options.logger });
}
Expand Down
1 change: 1 addition & 0 deletions packages/credential-provider-ini/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
},
"license": "Apache-2.0",
"dependencies": {
"@aws-sdk/core": "*",
"@aws-sdk/credential-provider-env": "*",
"@aws-sdk/credential-provider-http": "*",
"@aws-sdk/credential-provider-process": "*",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { resolveProfileData } from "./resolveProfileData";

jest.mock("@aws-sdk/client-sts", () => {
return {
getDefaultRoleAssumer: jest.fn().mockReturnValue(() => {}),
getDefaultRoleAssumer: jest.fn().mockReturnValue(async () => ({})),
};
});
jest.mock("@smithy/shared-ini-file-loader");
Expand Down Expand Up @@ -98,7 +98,7 @@ describe(resolveAssumeRoleCredentials.name, () => {
const mockProfiles = { [mockProfileName]: {} };
const mockOptions = {
mfaCodeProvider: jest.fn(),
roleAssumer: jest.fn().mockReturnValue(mockCreds),
roleAssumer: jest.fn().mockReturnValue(Promise.resolve(mockCreds)),
roleAssumerWithWebIdentity: jest.fn(),
};
const mockCredentialSource = "mockCredentialSource";
Expand All @@ -120,7 +120,7 @@ describe(resolveAssumeRoleCredentials.name, () => {
beforeEach(() => {
(getProfileName as jest.Mock).mockReturnValue(mockProfileName);
(resolveProfileData as jest.Mock).mockResolvedValue(mockSourceCredsFromProfile);
(resolveCredentialSource as jest.Mock).mockReturnValue(() => () => Promise.resolve(mockSourceCredsFromCredential));
(resolveCredentialSource as jest.Mock).mockReturnValue(async () => async () => mockSourceCredsFromCredential);
});

afterEach(() => {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { setCredentialFeature } from "@aws-sdk/core/client";
import { CredentialsProviderError } from "@smithy/property-provider";
import { getProfileName } from "@smithy/shared-ini-file-loader";
import { AwsCredentialIdentity, IniSection, Logger, ParsedIniData, Profile } from "@smithy/types";
Expand Down Expand Up @@ -159,7 +160,7 @@ export const resolveAssumeRoleCredentials = async (
* can use its role_arn instead of redundantly needing another role_arn at
* this final layer.
*/
return sourceCredsProvider;
return sourceCredsProvider.then((creds) => setCredentialFeature(creds, "CREDENTIALS_PROFILE_SOURCE_PROFILE", "o"));
} else {
const params: AssumeRoleParams = {
RoleArn: data.role_arn!,
Expand All @@ -181,7 +182,9 @@ export const resolveAssumeRoleCredentials = async (
}

const sourceCreds = await sourceCredsProvider;
return options.roleAssumer!(sourceCreds, params);
return options.roleAssumer!(sourceCreds, params).then((creds) =>
setCredentialFeature(creds, "CREDENTIALS_PROFILE_SOURCE_PROFILE", "o")
);
}
};

Expand Down
12 changes: 8 additions & 4 deletions packages/credential-provider-ini/src/resolveCredentialSource.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import type { CredentialProviderOptions } from "@aws-sdk/types";
import { setCredentialFeature } from "@aws-sdk/core/client";
import type { AwsCredentialIdentity, CredentialProviderOptions } from "@aws-sdk/types";
import { chain, CredentialsProviderError } from "@smithy/property-provider";
import { AwsCredentialIdentityProvider, Logger } from "@smithy/types";

Expand All @@ -21,17 +22,17 @@ export const resolveCredentialSource = (
const { fromHttp } = await import("@aws-sdk/credential-provider-http");
const { fromContainerMetadata } = await import("@smithy/credential-provider-imds");
logger?.debug("@aws-sdk/credential-provider-ini - credential_source is EcsContainer");
return chain(fromHttp(options ?? {}), fromContainerMetadata(options));
return async () => chain(fromHttp(options ?? {}), fromContainerMetadata(options))().then(setNamedProvider);
},
Ec2InstanceMetadata: async (options?: CredentialProviderOptions) => {
logger?.debug("@aws-sdk/credential-provider-ini - credential_source is Ec2InstanceMetadata");
const { fromInstanceMetadata } = await import("@smithy/credential-provider-imds");
return fromInstanceMetadata(options);
return async () => fromInstanceMetadata(options)().then(setNamedProvider);
},
Environment: async (options?: CredentialProviderOptions) => {
logger?.debug("@aws-sdk/credential-provider-ini - credential_source is Environment");
const { fromEnv } = await import("@aws-sdk/credential-provider-env");
return fromEnv(options);
return async () => fromEnv(options)().then(setNamedProvider);
},
};
if (credentialSource in sourceProvidersMap) {
Expand All @@ -44,3 +45,6 @@ export const resolveCredentialSource = (
);
}
};

const setNamedProvider = (creds: AwsCredentialIdentity) =>
setCredentialFeature(creds, "CREDENTIALS_PROFILE_NAMED_PROVIDER", "p");
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { setCredentialFeature } from "@aws-sdk/core/client";
import { Credentials, Profile } from "@aws-sdk/types";

import { FromIniInit } from "./fromIni";
Expand All @@ -23,5 +24,5 @@ export const resolveProcessCredentials = async (options: FromIniInit, profile: s
fromProcess({
...options,
profile,
})()
})().then((creds) => setCredentialFeature(creds, "CREDENTIALS_PROFILE_PROCESS", "v"))
);
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,6 @@ describe(resolveProfileData.name, () => {
(resolveSsoCredentials as jest.Mock).mockImplementation(() => Promise.resolve(mockCreds));
const receivedCreds = await resolveProfileData(mockProfileName, mockProfiles, mockOptions);
expect(receivedCreds).toStrictEqual(mockCreds);
expect(resolveSsoCredentials).toHaveBeenCalledWith(mockProfileName, mockOptions);
expect(resolveSsoCredentials).toHaveBeenCalledWith(mockProfileName, {}, mockOptions);
});
});
2 changes: 1 addition & 1 deletion packages/credential-provider-ini/src/resolveProfileData.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ export const resolveProfileData = async (
}

if (isSsoProfile(data)) {
return await resolveSsoCredentials(profileName, options);
return await resolveSsoCredentials(profileName, data, options);
}

// If the profile cannot be parsed or contains neither static credentials
Expand Down
Loading

0 comments on commit 089f1a4

Please sign in to comment.