From 4e715b830b7790cab1f4c01361a7c5a72851a297 Mon Sep 17 00:00:00 2001 From: Rico Hermans Date: Fri, 1 Nov 2024 11:43:15 +0100 Subject: [PATCH] refactor: gate access to environment SDK behind new class (#31904) Previously there were methods on the `Deployments` class that made it possible to directly get an SDK from the `SdkProvider` for a particular environment. Calling these methods made it possible to get an SDK without thinking of assuming roles to go into a different account. This PR introduces a new class, `EnvironmentAccess`, with a couple of public methods that are the only ones allowed to obtain SDKs with credentials. It has the methods: - accessStackForStackOperations(stack) - accessStackForLookup(stack) - accessStackForReading(stack) These will always respect the role information on the stack. Ideally there would have been similar methods for assets as well, but the `cdk-assets` library is entirely handling asset roles itself, and it's not in the scope of this PR to change that. That keeps on using a plain `SdkProvider`. Hotswap deployments will also just use CLI credentials and not assume role, so that also keeps on using an `SdkProvider`. All other uses have moved to `EnvironmentAccess`. ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license* --- .../lib/api/bootstrap/bootstrap-props.ts | 3 +- packages/aws-cdk/lib/api/deploy-stack.ts | 22 +- packages/aws-cdk/lib/api/deployments.ts | 354 ++++-------------- .../aws-cdk/lib/api/environment-access.ts | 272 ++++++++++++++ .../lib/api/logs/find-cloudwatch-logs.ts | 9 +- .../aws-cdk/lib/api/util/cloudformation.ts | 12 +- packages/aws-cdk/lib/api/util/placeholders.ts | 8 +- packages/aws-cdk/lib/util/type-brands.ts | 44 +++ .../api/cloudformation-deployments.test.ts | 31 ++ packages/aws-cdk/test/cdk-toolkit.test.ts | 18 +- packages/aws-cdk/test/version.test.ts | 5 + 11 files changed, 473 insertions(+), 305 deletions(-) create mode 100644 packages/aws-cdk/lib/api/environment-access.ts create mode 100644 packages/aws-cdk/lib/util/type-brands.ts diff --git a/packages/aws-cdk/lib/api/bootstrap/bootstrap-props.ts b/packages/aws-cdk/lib/api/bootstrap/bootstrap-props.ts index 47ce8e2a78532..3f01a6ebbbe42 100644 --- a/packages/aws-cdk/lib/api/bootstrap/bootstrap-props.ts +++ b/packages/aws-cdk/lib/api/bootstrap/bootstrap-props.ts @@ -1,4 +1,5 @@ import { Tag } from '../../cdk-toolkit'; +import { StringWithoutPlaceholders } from '../util/placeholders'; export const BUCKET_NAME_OUTPUT = 'BucketName'; export const REPOSITORY_NAME_OUTPUT = 'ImageRepositoryName'; @@ -17,7 +18,7 @@ export const DEFAULT_BOOTSTRAP_VARIANT = 'AWS CDK: Default Resources'; */ export interface BootstrapEnvironmentOptions { readonly toolkitStackName?: string; - readonly roleArn?: string; + readonly roleArn?: StringWithoutPlaceholders; readonly parameters?: BootstrappingParameters; readonly force?: boolean; diff --git a/packages/aws-cdk/lib/api/deploy-stack.ts b/packages/aws-cdk/lib/api/deploy-stack.ts index 16da0447b81f5..fff70866b617c 100644 --- a/packages/aws-cdk/lib/api/deploy-stack.ts +++ b/packages/aws-cdk/lib/api/deploy-stack.ts @@ -19,6 +19,7 @@ import { TemplateBodyParameter, makeBodyParameter } from './util/template-body-p import { AssetManifestBuilder } from '../util/asset-manifest-builder'; import { determineAllowCrossAccountAssetPublishing } from './util/checks'; import { publishAssets } from '../util/asset-publishing'; +import { StringWithoutPlaceholders } from './util/placeholders'; export interface DeployStackResult { readonly noOp: boolean; @@ -51,14 +52,13 @@ export interface DeployStackOptions { /** * SDK provider (seeded with default credentials) * - * Will exclusively be used to assume publishing credentials (which must - * start out from current credentials regardless of whether we've assumed an - * action role to touch the stack or not). + * Will be used to: * - * Used for the following purposes: - * - * - Publish legacy assets. - * - Upload large CloudFormation templates to the staging bucket. + * - Publish assets, either legacy assets or large CFN templates + * that aren't themselves assets from a manifest. (Needs an SDK + * Provider because the file publishing role is declared as part + * of the asset). + * - Hotswap */ readonly sdkProvider: SdkProvider; @@ -70,9 +70,13 @@ export interface DeployStackOptions { /** * Role to pass to CloudFormation to execute the change set * - * @default - Role specified on stack, otherwise current + * To obtain a `StringWithoutPlaceholders`, run a regular + * string though `TargetEnvironment.replacePlaceholders`. + * + * @default - No execution role; CloudFormation either uses the role currently associated with + * the stack, or otherwise uses current AWS credentials. */ - readonly roleArn?: string; + readonly roleArn?: StringWithoutPlaceholders; /** * Notification ARNs to pass to CloudFormation to notify when the change set has completed diff --git a/packages/aws-cdk/lib/api/deployments.ts b/packages/aws-cdk/lib/api/deployments.ts index 6231aa9a70567..1240127469804 100644 --- a/packages/aws-cdk/lib/api/deployments.ts +++ b/packages/aws-cdk/lib/api/deployments.ts @@ -4,56 +4,25 @@ import * as cdk_assets from 'cdk-assets'; import { AssetManifest, IManifestEntry } from 'cdk-assets'; import * as chalk from 'chalk'; import { Tag } from '../cdk-toolkit'; -import { debug, warning, error } from '../logging'; -import { Mode } from './aws-auth/credentials'; -import { ISDK } from './aws-auth/sdk'; -import { CredentialsOptions, SdkForEnvironment, SdkProvider } from './aws-auth/sdk-provider'; +import { debug, warning } from '../logging'; +import { SdkProvider } from './aws-auth/sdk-provider'; import { deployStack, DeployStackResult, destroyStack, DeploymentMethod } from './deploy-stack'; -import { EnvironmentResources, EnvironmentResourcesRegistry } from './environment-resources'; +import { EnvironmentAccess } from './environment-access'; +import { EnvironmentResources } from './environment-resources'; import { HotswapMode, HotswapPropertyOverrides } from './hotswap/common'; import { loadCurrentTemplateWithNestedStacks, loadCurrentTemplate, RootTemplateWithNestedStacks } from './nested-stack-helpers'; +import { DEFAULT_TOOLKIT_STACK_NAME } from './toolkit-info'; import { determineAllowCrossAccountAssetPublishing } from './util/checks'; import { CloudFormationStack, Template, ResourcesToImport, ResourceIdentifierSummaries, stabilizeStack, uploadStackTemplateAssets } from './util/cloudformation'; import { StackActivityMonitor, StackActivityProgress } from './util/cloudformation/stack-activity-monitor'; import { StackEventPoller } from './util/cloudformation/stack-event-poller'; import { RollbackChoice } from './util/cloudformation/stack-status'; -import { replaceEnvPlaceholders } from './util/placeholders'; import { makeBodyParameter } from './util/template-body-parameter'; import { AssetManifestBuilder } from '../util/asset-manifest-builder'; import { buildAssets, publishAssets, BuildAssetsOptions, PublishAssetsOptions, PublishingAws, EVENT_TO_LOGGER } from '../util/asset-publishing'; const BOOTSTRAP_STACK_VERSION_FOR_ROLLBACK = 23; -/** - * SDK obtained by assuming the lookup role - * for a given environment - */ -export interface PreparedSdkWithLookupRoleForEnvironment { - /** - * The SDK for the given environment - */ - readonly sdk: ISDK; - - /** - * The resolved environment for the stack - * (no more 'unknown-account/unknown-region') - */ - readonly resolvedEnvironment: cxapi.Environment; - - /** - * Whether or not the assume role was successful. - * If the assume role was not successful (false) - * then that means that the 'sdk' returned contains - * the default credentials (not the assume role credentials) - */ - readonly didAssumeRole: boolean; - - /** - * An object for accessing the bootstrap resources in this environment - */ - readonly envResources: EnvironmentResources; -} - export interface DeployStackOptions { /** * Stack to deploy @@ -352,69 +321,61 @@ export interface DeploymentsProps { } /** - * SDK obtained by assuming the deploy role - * for a given environment + * Scope for a single set of deployments from a set of Cloud Assembly Artifacts + * + * Manages lookup of SDKs, Bootstrap stacks, etc. */ -export interface PreparedSdkForEnvironment { - /** - * The SDK for the given environment - */ - readonly stackSdk: ISDK; +export class Deployments { + public readonly envs: EnvironmentAccess; /** - * The resolved environment for the stack - * (no more 'unknown-account/unknown-region') - */ - readonly resolvedEnvironment: cxapi.Environment; - /** - * The Execution Role that should be passed to CloudFormation. + * SDK provider for asset publishing (do not use for anything else). + * + * This SDK provider is only allowed to be used for that purpose, nothing else. * - * @default - no execution role is used + * It's not a different object, but the field name should imply that this + * object should not be used directly, except to pass to asset handling routines. */ - readonly cloudFormationRoleArn?: string; + private readonly assetSdkProvider: SdkProvider; /** - * Access class for environmental resources to help the deployment + * SDK provider for passing to deployStack + * + * This SDK provider is only allowed to be used for that purpose, nothing else. + * + * It's not a different object, but the field name should imply that this + * object should not be used directly, except to pass to `deployStack`. */ - readonly envResources: EnvironmentResources; -} + private readonly deployStackSdkProvider: SdkProvider; -/** - * Scope for a single set of deployments from a set of Cloud Assembly Artifacts - * - * Manages lookup of SDKs, Bootstrap stacks, etc. - */ -export class Deployments { - private readonly sdkProvider: SdkProvider; - private readonly sdkCache = new Map(); private readonly publisherCache = new Map(); - private readonly environmentResources: EnvironmentResourcesRegistry; private _allowCrossAccountAssetPublishing: boolean | undefined; constructor(private readonly props: DeploymentsProps) { - this.sdkProvider = props.sdkProvider; - this.environmentResources = new EnvironmentResourcesRegistry(props.toolkitStackName); + this.assetSdkProvider = props.sdkProvider; + this.deployStackSdkProvider = props.sdkProvider; + this.envs = new EnvironmentAccess(props.sdkProvider, props.toolkitStackName ?? DEFAULT_TOOLKIT_STACK_NAME); } /** * Resolves the environment for a stack. */ public async resolveEnvironment(stack: cxapi.CloudFormationStackArtifact): Promise { - return this.sdkProvider.resolveEnvironment(stack.environment); + return this.envs.resolveStackEnvironment(stack); } public async readCurrentTemplateWithNestedStacks( rootStackArtifact: cxapi.CloudFormationStackArtifact, retrieveProcessedTemplate: boolean = false, ): Promise { - const sdk = (await this.prepareSdkWithLookupOrDeployRole(rootStackArtifact)).stackSdk; - return loadCurrentTemplateWithNestedStacks(rootStackArtifact, sdk, retrieveProcessedTemplate); + const env = await this.envs.accessStackForLookupBestEffort(rootStackArtifact); + return loadCurrentTemplateWithNestedStacks(rootStackArtifact, env.sdk, retrieveProcessedTemplate); } public async readCurrentTemplate(stackArtifact: cxapi.CloudFormationStackArtifact): Promise