diff --git a/packages/aws-cdk-lib/aws-iam/README.md b/packages/aws-cdk-lib/aws-iam/README.md index b020dc7c55063..ff56eedf93bc6 100644 --- a/packages/aws-cdk-lib/aws-iam/README.md +++ b/packages/aws-cdk-lib/aws-iam/README.md @@ -490,11 +490,7 @@ OR ```ts new App({ - context: { - [PERMISSIONS_BOUNDARY_CONTEXT_KEY]: { - name: 'cdk-${Qualifier}-PermissionsBoundary', - }, - }, + permissionsBoundary: PermissionsBoundary.fromName('cdk-${Qualifier}-PermissionsBoundary'), }); ``` diff --git a/packages/aws-cdk-lib/core/lib/app.ts b/packages/aws-cdk-lib/core/lib/app.ts index d24ec829f00df..88d311003d90f 100644 --- a/packages/aws-cdk-lib/core/lib/app.ts +++ b/packages/aws-cdk-lib/core/lib/app.ts @@ -1,4 +1,6 @@ import { Construct } from 'constructs'; +import { Environment } from './environment'; +import { PermissionsBoundary } from './permissions-boundary'; import * as fs from 'fs-extra'; import { PRIVATE_CONTEXT_DEFAULT_STACK_SYNTHESIZER } from './private/private-context'; import { addCustomSynthesis, ICustomSynthesis } from './private/synthesis'; @@ -126,6 +128,47 @@ export interface AppProps { * @default - no validation plugins */ readonly policyValidationBeta1?: IPolicyValidationPluginBeta1[]; + + /** + * Default AWS environment (account/region) for `Stack`s in this `App`. + * + * Stacks defined inside this `App` with either `region` or `account` missing + * from its env will use the corresponding field given here. + * + * If either `region` or `account`is is not configured for `Stack` (either on + * the `Stack` itself or on the containing `App` or `Stage`), the Stack will be + * *environment-agnostic*. + * + * Environment-agnostic stacks can be deployed to any environment, may not be + * able to take advantage of all features of the CDK. For example, they will + * not be able to use environmental context lookups, will not automatically + * translate Service Principals to the right format based on the environment's + * AWS partition, and other such enhancements. + * + * @example + * + * // Use a concrete account and region to deploy this Stage to + * new App({ + * env: { account: '123456789012', region: 'us-east-1' }, + * }); + * + * // Use the CLI's current credentials to determine the target environment + * new App({ + * env: { account: process.env.CDK_DEFAULT_ACCOUNT, region: process.env.CDK_DEFAULT_REGION }, + * }); + * + * @default - The environments should be configured on the `Stack`s. + */ + readonly env?: Environment; + + /** + * Options for applying a permissions boundary to all IAM Roles + * and Users created within this App + * + * @default - no permissions boundary is applied + */ + readonly permissionsBoundary?: PermissionsBoundary; + } /** @@ -168,6 +211,8 @@ export class App extends Stage { super(undefined as any, '', { outdir: props.outdir ?? process.env[cxapi.OUTDIR_ENV], policyValidationBeta1: props.policyValidationBeta1, + permissionsBoundary: props.permissionsBoundary, + env: props.env, }); Object.defineProperty(this, APP_SYMBOL, { value: true }); diff --git a/packages/aws-cdk-lib/core/test/app.test.ts b/packages/aws-cdk-lib/core/test/app.test.ts index 49654658ef8d4..02dcad11a07d1 100644 --- a/packages/aws-cdk-lib/core/test/app.test.ts +++ b/packages/aws-cdk-lib/core/test/app.test.ts @@ -4,7 +4,7 @@ import { Construct } from 'constructs'; import * as fs from 'fs-extra'; import { ContextProvider } from '../../cloud-assembly-schema'; import * as cxapi from '../../cx-api'; -import { CfnResource, DefaultStackSynthesizer, Stack, StackProps } from '../lib'; +import { CfnResource, DefaultStackSynthesizer, Stack, StackProps, Stage } from '../lib'; import { Annotations } from '../lib/annotations'; import { App, AppProps } from '../lib/app'; @@ -361,6 +361,37 @@ describe('app', () => { expect(app.node.tryGetContext('isNumber')).toEqual(10); expect(app.node.tryGetContext('isObject')).toEqual({ isString: 'string', isNumber: 10 }); }); + test('Stack inherits unspecified part of the env from App', () => { + // GIVEN + const app = new App({ + env: { account: 'account', region: 'region' }, + }); + + // WHEN + const stack1 = new Stack(app, 'Stack1', { env: { region: 'elsewhere' } }); + const stack2 = new Stack(app, 'Stack2', { env: { account: 'tnuocca' } }); + + // THEN + expect(acctRegion(stack1)).toEqual(['account', 'elsewhere']); + expect(acctRegion(stack2)).toEqual(['tnuocca', 'region']); + }); + + test('envs are inherited deeply', () => { + // GIVEN + const app = new App({ + env: { account: 'account', region: 'region' }, + }); + + // WHEN + const innerAcct = new Stage(app, 'Acct', { env: { account: 'tnuocca' } }); + const innerRegion = new Stage(app, 'Rgn', { env: { region: 'elsewhere' } }); + const innerNeither = new Stage(app, 'Neither'); + + // THEN + expect(acctRegion(new Stack(innerAcct, 'Stack'))).toEqual(['tnuocca', 'region']); + expect(acctRegion(new Stack(innerRegion, 'Stack'))).toEqual(['account', 'elsewhere']); + expect(acctRegion(new Stack(innerNeither, 'Stack'))).toEqual(['account', 'region']); + }); }); class MyConstruct extends Construct { @@ -371,3 +402,7 @@ class MyConstruct extends Construct { new CfnResource(this, 'r2', { type: 'ResourceType2', properties: { FromContext: this.node.tryGetContext('ctx1') } }); } } + +function acctRegion(s: Stack) { + return [s.account, s.region]; +} \ No newline at end of file