packages/@aws-cdk/toolkit-lib/lib/api/aws-auth/credential-plugins.ts (109 lines of code) (raw):
import { inspect } from 'util';
import type { CredentialProviderSource, ForReading, ForWriting, PluginProviderResult, SDKv2CompatibleCredentials, SDKv3CompatibleCredentialProvider, SDKv3CompatibleCredentials } from '@aws-cdk/cli-plugin-contract';
import type { AwsCredentialIdentity, AwsCredentialIdentityProvider } from '@smithy/types';
import { credentialsAboutToExpire, makeCachingProvider } from './provider-caching';
import { formatErrorMessage } from '../../util';
import { IO, type IoHelper } from '../io/private';
import type { PluginHost } from '../plugin';
import type { Mode } from '../plugin/mode';
import { AuthenticationError } from '../toolkit-error';
/**
* Cache for credential providers.
*
* Given an account and an operating mode (read or write) will return an
* appropriate credential provider for credentials for the given account. The
* credential provider will be cached so that multiple AWS clients for the same
* environment will not make multiple network calls to obtain credentials.
*
* Will use default credentials if they are for the right account; otherwise,
* all loaded credential provider plugins will be tried to obtain credentials
* for the given account.
*/
export class CredentialPlugins {
private readonly cache: { [key: string]: PluginCredentialsFetchResult | undefined } = {};
constructor(private readonly host: PluginHost, private readonly ioHelper: IoHelper) {
}
public async fetchCredentialsFor(awsAccountId: string, mode: Mode): Promise<PluginCredentialsFetchResult | undefined> {
const key = `${awsAccountId}-${mode}`;
if (!(key in this.cache)) {
this.cache[key] = await this.lookupCredentials(awsAccountId, mode);
}
return this.cache[key];
}
public get availablePluginNames(): string[] {
return this.host.credentialProviderSources.map((s) => s.name);
}
private async lookupCredentials(awsAccountId: string, mode: Mode): Promise<PluginCredentialsFetchResult | undefined> {
const triedSources: CredentialProviderSource[] = [];
// Otherwise, inspect the various credential sources we have
for (const source of this.host.credentialProviderSources) {
let available: boolean;
try {
available = await source.isAvailable();
} catch (e: any) {
// This shouldn't happen, but let's guard against it anyway
await this.ioHelper.notify(IO.CDK_TOOLKIT_W0100.msg(`Uncaught exception in ${source.name}: ${formatErrorMessage(e)}`));
available = false;
}
if (!available) {
await this.ioHelper.notify(IO.DEFAULT_TOOLKIT_DEBUG.msg(`Credentials source ${source.name} is not available, ignoring it.`));
continue;
}
triedSources.push(source);
let canProvide: boolean;
try {
canProvide = await source.canProvideCredentials(awsAccountId);
} catch (e: any) {
// This shouldn't happen, but let's guard against it anyway
await this.ioHelper.notify(IO.CDK_TOOLKIT_W0100.msg(`Uncaught exception in ${source.name}: ${formatErrorMessage(e)}`));
canProvide = false;
}
if (!canProvide) {
continue;
}
await this.ioHelper.notify(IO.DEFAULT_TOOLKIT_DEBUG.msg(`Using ${source.name} credentials for account ${awsAccountId}`));
return {
credentials: await v3ProviderFromPlugin(() => source.getProvider(awsAccountId, mode as ForReading | ForWriting, {
supportsV3Providers: true,
})),
pluginName: source.name,
};
}
return undefined;
}
}
/**
* Result from trying to fetch credentials from the Plugin host
*/
export interface PluginCredentialsFetchResult {
/**
* SDK-v3 compatible credential provider
*/
readonly credentials: AwsCredentialIdentityProvider;
/**
* Name of plugin that successfully provided credentials
*/
readonly pluginName: string;
}
/**
* Take a function that calls the plugin, and turn it into an SDKv3-compatible credential provider.
*
* What we will do is the following:
*
* - Query the plugin and see what kind of result it gives us.
* - If the result is self-refreshing or doesn't need refreshing, we turn it into an SDKv3 provider
* and return it directly.
* * If the underlying return value is a provider, we will make it a caching provider
* (because we can't know if it will cache by itself or not).
* * If the underlying return value is a static credential, caching isn't relevant.
* * If the underlying return value is V2 credentials, those have caching built-in.
* - If the result is a static credential that expires, we will wrap it in an SDKv3 provider
* that will query the plugin again when the credential expires.
*/
async function v3ProviderFromPlugin(producer: () => Promise<PluginProviderResult>): Promise<AwsCredentialIdentityProvider> {
const initial = await producer();
if (isV3Provider(initial)) {
// Already a provider, make caching
return makeCachingProvider(initial);
} else if (isV3Credentials(initial) && initial.expiration === undefined) {
// Static credentials that don't need refreshing nor caching
return () => Promise.resolve(initial);
} else if (isV3Credentials(initial) && initial.expiration !== undefined) {
// Static credentials that do need refreshing and caching
return refreshFromPluginProvider(initial, producer);
} else if (isV2Credentials(initial)) {
// V2 credentials that refresh and cache themselves
return v3ProviderFromV2Credentials(initial);
} else {
throw new AuthenticationError(`Plugin returned a value that doesn't resemble AWS credentials: ${inspect(initial)}`);
}
}
/**
* Converts a V2 credential into a V3-compatible provider
*/
function v3ProviderFromV2Credentials(x: SDKv2CompatibleCredentials): AwsCredentialIdentityProvider {
return async () => {
// Get will fetch or refresh as necessary
await x.getPromise();
return {
accessKeyId: x.accessKeyId,
secretAccessKey: x.secretAccessKey,
sessionToken: x.sessionToken,
expiration: x.expireTime ?? undefined,
};
};
}
function refreshFromPluginProvider(current: AwsCredentialIdentity, producer: () => Promise<PluginProviderResult>): AwsCredentialIdentityProvider {
return async () => {
if (credentialsAboutToExpire(current)) {
const newCreds = await producer();
if (!isV3Credentials(newCreds)) {
throw new AuthenticationError(`Plugin initially returned static V3 credentials but now returned something else: ${inspect(newCreds)}`);
}
current = newCreds;
}
return current;
};
}
function isV3Provider(x: PluginProviderResult): x is SDKv3CompatibleCredentialProvider {
return typeof x === 'function';
}
function isV2Credentials(x: PluginProviderResult): x is SDKv2CompatibleCredentials {
return !!(x && typeof x === 'object' && (x as SDKv2CompatibleCredentials).getPromise);
}
function isV3Credentials(x: PluginProviderResult): x is SDKv3CompatibleCredentials {
return !!(x && typeof x === 'object' && x.accessKeyId && !isV2Credentials(x));
}