packages/@aws-cdk/toolkit-lib/lib/api/environment/environment-access.ts (137 lines of code) (raw):
import type * as cxapi from '@aws-cdk/cx-api';
import { ToolkitError } from '../toolkit-error';
import type { EnvironmentResources } from './environment-resources';
import { EnvironmentResourcesRegistry } from './environment-resources';
import type { StringWithoutPlaceholders } from './placeholders';
import { replaceEnvPlaceholders } from './placeholders';
import { formatErrorMessage } from '../../util';
import type { SDK, CredentialsOptions, SdkForEnvironment, SdkProvider } from '../aws-auth/private';
import { IO, type IoHelper } from '../io/private';
import { Mode } from '../plugin';
/**
* Access particular AWS resources, based on information from the CX manifest
*
* It is not possible to grab direct access to AWS credentials; 9 times out of 10
* we have to allow for role assumption, and role assumption can only work if
* there is a CX Manifest that contains a role ARN.
*
* This class exists so new code isn't tempted to go and get SDK credentials directly.
*/
export class EnvironmentAccess {
private readonly sdkCache = new Map<string, SdkForEnvironment>();
private readonly environmentResources: EnvironmentResourcesRegistry;
private readonly ioHelper: IoHelper;
constructor(private readonly sdkProvider: SdkProvider, toolkitStackName: string, ioHelper: IoHelper) {
this.environmentResources = new EnvironmentResourcesRegistry(toolkitStackName);
this.ioHelper = ioHelper;
}
/**
* Resolves the environment for a stack.
*/
public async resolveStackEnvironment(stack: cxapi.CloudFormationStackArtifact): Promise<cxapi.Environment> {
return this.sdkProvider.resolveEnvironment(stack.environment);
}
/**
* Get an SDK to access the given stack's environment for stack operations
*
* Will ask plugins for readonly credentials if available, use the default
* AWS credentials if not.
*
* Will assume the deploy role if configured on the stack. Check the default `deploy-role`
* policies to see what you can do with this role.
*/
public async accessStackForReadOnlyStackOperations(stack: cxapi.CloudFormationStackArtifact): Promise<TargetEnvironment> {
return this.accessStackForStackOperations(stack, Mode.ForReading);
}
/**
* Get an SDK to access the given stack's environment for stack operations
*
* Will ask plugins for mutating credentials if available, use the default AWS
* credentials if not. The `mode` parameter is only used for querying
* plugins.
*
* Will assume the deploy role if configured on the stack. Check the default `deploy-role`
* policies to see what you can do with this role.
*/
public async accessStackForMutableStackOperations(stack: cxapi.CloudFormationStackArtifact): Promise<TargetEnvironment> {
return this.accessStackForStackOperations(stack, Mode.ForWriting);
}
/**
* Get an SDK to access the given stack's environment for environmental lookups
*
* Will use a plugin if available, use the default AWS credentials if not.
* The `mode` parameter is only used for querying plugins.
*
* Will assume the lookup role if configured on the stack. Check the default `lookup-role`
* policies to see what you can do with this role. It can generally read everything
* in the account that does not require KMS access.
*
* ---
*
* For backwards compatibility reasons, there are some scenarios that are handled here:
*
* 1. The lookup role may not exist (it was added in bootstrap stack version 7). If so:
* a. Return the default credentials if the default credentials are for the stack account
* (you will notice this as `isFallbackCredentials=true`).
* b. Throw an error if the default credentials are not for the stack account.
*
* 2. The lookup role may not have the correct permissions (for example, ReadOnlyAccess was added in
* bootstrap stack version 8); the stack will have a minimum version number on it.
* a. If it does not we throw an error which should be handled in the calling
* function (and fallback to use a different role, etc)
*
* Upon success, caller will have an SDK for the right account, which may or may not have
* the right permissions.
*/
public async accessStackForLookup(stack: cxapi.CloudFormationStackArtifact): Promise<TargetEnvironment> {
if (!stack.environment) {
throw new ToolkitError(`The stack ${stack.displayName} does not have an environment`);
}
const lookupEnv = await this.prepareSdk({
environment: stack.environment,
mode: Mode.ForReading,
assumeRoleArn: stack.lookupRole?.arn,
assumeRoleExternalId: stack.lookupRole?.assumeRoleExternalId,
assumeRoleAdditionalOptions: stack.lookupRole?.assumeRoleAdditionalOptions,
});
// if we succeed in assuming the lookup role, make sure we have the correct bootstrap stack version
if (lookupEnv.didAssumeRole && stack.lookupRole?.bootstrapStackVersionSsmParameter && stack.lookupRole.requiresBootstrapStackVersion) {
const version = await lookupEnv.resources.versionFromSsmParameter(stack.lookupRole.bootstrapStackVersionSsmParameter);
if (version < stack.lookupRole.requiresBootstrapStackVersion) {
throw new ToolkitError(`Bootstrap stack version '${stack.lookupRole.requiresBootstrapStackVersion}' is required, found version '${version}'. To get rid of this error, please upgrade to bootstrap version >= ${stack.lookupRole.requiresBootstrapStackVersion}`);
}
}
if (lookupEnv.isFallbackCredentials) {
const arn = await lookupEnv.replacePlaceholders(stack.lookupRole?.arn);
await this.ioHelper.notify(IO.DEFAULT_TOOLKIT_WARN.msg(`Lookup role ${arn} was not assumed. Proceeding with default credentials.`));
}
return lookupEnv;
}
/**
* Get an SDK to access the given stack's environment for reading stack attributes
*
* Will use a plugin if available, use the default AWS credentials if not.
* The `mode` parameter is only used for querying plugins.
*
* Will try to assume the lookup role if given, will use the regular stack operations
* access (deploy-role) otherwise. When calling this, you should assume that you will get
* the least privileged role, so don't try to use it for anything the `deploy-role`
* wouldn't be able to do. Also you cannot rely on being able to read encrypted anything.
*/
public async accessStackForLookupBestEffort(stack: cxapi.CloudFormationStackArtifact): Promise<TargetEnvironment> {
if (!stack.environment) {
throw new ToolkitError(`The stack ${stack.displayName} does not have an environment`);
}
try {
return await this.accessStackForLookup(stack);
} catch (e: any) {
await this.ioHelper.notify(IO.DEFAULT_TOOLKIT_WARN.msg(`${formatErrorMessage(e)}`));
}
return this.accessStackForStackOperations(stack, Mode.ForReading);
}
/**
* Get an SDK to access the given stack's environment for stack operations
*
* Will use a plugin if available, use the default AWS credentials if not.
* The `mode` parameter is only used for querying plugins.
*
* Will assume the deploy role if configured on the stack. Check the default `deploy-role`
* policies to see what you can do with this role.
*/
private async accessStackForStackOperations(stack: cxapi.CloudFormationStackArtifact, mode: Mode): Promise<TargetEnvironment> {
if (!stack.environment) {
throw new ToolkitError(`The stack ${stack.displayName} does not have an environment`);
}
return this.prepareSdk({
environment: stack.environment,
mode,
assumeRoleArn: stack.assumeRoleArn,
assumeRoleExternalId: stack.assumeRoleExternalId,
assumeRoleAdditionalOptions: stack.assumeRoleAdditionalOptions,
});
}
/**
* Prepare an SDK for use in the given environment and optionally with a role assumed.
*/
private async prepareSdk(
options: PrepareSdkRoleOptions,
): Promise<TargetEnvironment> {
const resolvedEnvironment = await this.sdkProvider.resolveEnvironment(options.environment);
// Substitute any placeholders with information about the current environment
const { assumeRoleArn } = await replaceEnvPlaceholders({
assumeRoleArn: options.assumeRoleArn,
}, resolvedEnvironment, this.sdkProvider);
const stackSdk = await this.cachedSdkForEnvironment(resolvedEnvironment, options.mode, {
assumeRoleArn,
assumeRoleExternalId: options.assumeRoleExternalId,
assumeRoleAdditionalOptions: options.assumeRoleAdditionalOptions,
});
return {
sdk: stackSdk.sdk,
resolvedEnvironment,
resources: this.environmentResources.for(resolvedEnvironment, stackSdk.sdk, this.ioHelper),
// If we asked for a role, did not successfully assume it, and yet got here without an exception: that
// means we must have fallback credentials.
isFallbackCredentials: !stackSdk.didAssumeRole && !!assumeRoleArn,
didAssumeRole: stackSdk.didAssumeRole,
replacePlaceholders: async <A extends string | undefined>(str: A) => {
const ret = await replaceEnvPlaceholders({ str }, resolvedEnvironment, this.sdkProvider);
return ret.str;
},
};
}
private async cachedSdkForEnvironment(
environment: cxapi.Environment,
mode: Mode,
options?: CredentialsOptions,
) {
const cacheKeyElements = [
environment.account,
environment.region,
`${mode}`,
options?.assumeRoleArn ?? '',
options?.assumeRoleExternalId ?? '',
];
if (options?.assumeRoleAdditionalOptions) {
cacheKeyElements.push(JSON.stringify(options.assumeRoleAdditionalOptions));
}
const cacheKey = cacheKeyElements.join(':');
const existing = this.sdkCache.get(cacheKey);
if (existing) {
return existing;
}
const ret = await this.sdkProvider.forEnvironment(environment, mode, options);
this.sdkCache.set(cacheKey, ret);
return ret;
}
}
/**
* SDK obtained by assuming the deploy role
* for a given environment
*/
export interface TargetEnvironment {
/**
* The SDK for the given environment
*/
readonly sdk: SDK;
/**
* The resolved environment for the stack
* (no more 'unknown-account/unknown-region')
*/
readonly resolvedEnvironment: cxapi.Environment;
/**
* Access class for environmental resources to help the deployment
*/
readonly resources: EnvironmentResources;
/**
* Whether or not we assumed a role in the process of getting these credentials
*/
readonly didAssumeRole: boolean;
/**
* Whether or not these are fallback credentials
*
* Fallback credentials means that assuming the intended role failed, but the
* base credentials happen to be for the right account so we just picked those
* and hope the future SDK calls succeed.
*
* This is a backwards compatibility mechanism from around the time we introduced
* deployment roles.
*/
readonly isFallbackCredentials: boolean;
/**
* Replace environment placeholders according to the current environment
*/
replacePlaceholders(x: string | undefined): Promise<StringWithoutPlaceholders | undefined>;
}
interface PrepareSdkRoleOptions {
readonly environment: cxapi.Environment;
readonly mode: Mode;
readonly assumeRoleArn?: string;
readonly assumeRoleExternalId?: string;
readonly assumeRoleAdditionalOptions?: { [key: string]: any };
}