diff --git a/packages/aws-cdk/README.md b/packages/aws-cdk/README.md index 8ee36557dd51c..e46213b4d2c7e 100644 --- a/packages/aws-cdk/README.md +++ b/packages/aws-cdk/README.md @@ -568,8 +568,9 @@ $ cdk bootstrap --app='node bin/main.js' foo bar By default, bootstrap stack will be protected from stack termination. This can be disabled using `--termination-protection` argument. -If you have specific needs, policies, or requirements not met by the default template, you can customize it -to fit your own situation, by exporting the default one to a file and either deploying it yourself +If you have specific prerequisites not met by the example template, you can +[customize it](https://docs.aws.amazon.com/cdk/v2/guide/bootstrapping.html#bootstrapping-customizing) +to fit your requirements, by exporting the provided one to a file and either deploying it yourself using CloudFormation directly, or by telling the CLI to use a custom template. That looks as follows: ```console @@ -582,6 +583,13 @@ $ cdk bootstrap --show-template > bootstrap-template.yaml $ cdk bootstrap --template bootstrap-template.yaml ``` +Out of the box customization options are also available as arguments. To use a permissions boundary: + +- `--example-permissions-boundary` indicates the example permissions boundary, supplied by CDK +- `--custom-permissions-boundary` specifies, by name a predefined, customer maintained, boundary + +A few notes to add at this point. The CDK supplied permissions boundary policy should be regarded as an example. Edit the content and reference the example policy if you're testing out the feature, turn it into a new policy for actual deployments (if one does not already exist). The concern here is drift as, most likely, a permissions boundary is maintained and has dedicated conventions, naming included. + ### `cdk doctor` Inspect the current command-line environment and configurations, and collect information that can be useful for diff --git a/packages/aws-cdk/lib/api/aws-auth/sdk.ts b/packages/aws-cdk/lib/api/aws-auth/sdk.ts index aa3231247309e..4f01ae207ecbc 100644 --- a/packages/aws-cdk/lib/api/aws-auth/sdk.ts +++ b/packages/aws-cdk/lib/api/aws-auth/sdk.ts @@ -9,7 +9,7 @@ import { Account } from './sdk-provider'; // We need to map regions to domain suffixes, and the SDK already has a function to do this. // It's not part of the public API, but it's also unlikely to go away. // -// Reuse that function, and add a safety check so we don't accidentally break if they ever +// Reuse that function, and add a safety check, so we don't accidentally break if they ever // refactor that away. /* eslint-disable @typescript-eslint/no-require-imports */ @@ -53,6 +53,7 @@ export interface ISDK { lambda(): AWS.Lambda; cloudFormation(): AWS.CloudFormation; ec2(): AWS.EC2; + iam(): AWS.IAM; ssm(): AWS.SSM; s3(): AWS.S3; route53(): AWS.Route53; @@ -163,6 +164,10 @@ export class SDK implements ISDK { return this.wrapServiceErrorHandling(new AWS.EC2(this.config)); } + public iam(): AWS.IAM { + return this.wrapServiceErrorHandling(new AWS.IAM(this.config)); + } + public ssm(): AWS.SSM { return this.wrapServiceErrorHandling(new AWS.SSM(this.config)); } @@ -415,4 +420,4 @@ function allChainedExceptionMessages(e: Error | undefined) { */ export function isUnrecoverableAwsError(e: Error) { return (e as any).code === 'ExpiredToken'; -} \ No newline at end of file +} diff --git a/packages/aws-cdk/lib/api/bootstrap/bootstrap-environment.ts b/packages/aws-cdk/lib/api/bootstrap/bootstrap-environment.ts index f86a3d96b85b5..80b8c4fc7a10c 100644 --- a/packages/aws-cdk/lib/api/bootstrap/bootstrap-environment.ts +++ b/packages/aws-cdk/lib/api/bootstrap/bootstrap-environment.ts @@ -4,7 +4,7 @@ import * as cxapi from '@aws-cdk/cx-api'; import { warning } from '../../logging'; import { loadStructuredFile, serializeStructure } from '../../serialize'; import { rootDir } from '../../util/directories'; -import { SdkProvider } from '../aws-auth'; +import { ISDK, Mode, SdkProvider } from '../aws-auth'; import { DeployStackResult } from '../deploy-stack'; import { BootstrapEnvironmentOptions, BootstrappingParameters } from './bootstrap-props'; import { BootstrapStack, bootstrapVersionFromTemplate } from './deploy-bootstrap'; @@ -79,6 +79,7 @@ export class Bootstrapper { const bootstrapTemplate = await this.loadTemplate(); const current = await BootstrapStack.lookup(sdkProvider, environment, options.toolkitStackName); + const partition = await current.partition(); if (params.createCustomerMasterKey !== undefined && params.kmsKeyId) { throw new Error('You cannot pass \'--bootstrap-kms-key-id\' and \'--bootstrap-customer-key\' together. Specify one or the other'); @@ -102,7 +103,7 @@ export class Bootstrapper { if (trustedAccounts.length === 0 && cloudFormationExecutionPolicies.length === 0) { // For self-trust it's okay to default to AdministratorAccess, and it improves the usability of bootstrapping a lot. // - // We don't actually make the implicity policy a physical parameter. The template will infer it instead, + // We don't actually make the implicitly policy a physical parameter. The template will infer it instead, // we simply do the UI advertising that behavior here. // // If we DID make it an explicit parameter, we wouldn't be able to tell the difference between whether @@ -113,7 +114,7 @@ export class Bootstrapper { // // Would leave AdministratorAccess policies with a trust relationship, without the user explicitly // approving the trust policy. - const implicitPolicy = `arn:${await current.partition()}:iam::aws:policy/AdministratorAccess`; + const implicitPolicy = `arn:${partition}:iam::aws:policy/AdministratorAccess`; warning(`Using default execution policy of '${implicitPolicy}'. Pass '--cloudformation-execution-policies' to customize.`); } else if (cloudFormationExecutionPolicies.length === 0) { throw new Error('Please pass \'--cloudformation-execution-policies\' when using \'--trust\' to specify deployment permissions. Try a managed policy of the form \'arn:aws:iam::aws:policy/\'.'); @@ -130,9 +131,25 @@ export class Bootstrapper { // * '-' if this is the first time we're deploying this stack (or upgrading from old to new bootstrap) const currentKmsKeyId = current.parameters.FileAssetsBucketKmsKeyId; const kmsKeyId = params.kmsKeyId ?? - (params.createCustomerMasterKey === true ? CREATE_NEW_KEY : - params.createCustomerMasterKey === false || currentKmsKeyId === undefined ? USE_AWS_MANAGED_KEY : - undefined); + (params.createCustomerMasterKey === true ? CREATE_NEW_KEY : + params.createCustomerMasterKey === false || currentKmsKeyId === undefined ? USE_AWS_MANAGED_KEY : undefined); + + /* A permissions boundary can be provided via: + * - the flag indicating the example one should be used + * - the name indicating the custom permissions boundary to be used + * Re-bootstrapping will NOT be blocked by either tightening or relaxing the permissions' boundary. + */ + const currentPermissionsBoundary = current.parameters.InputPermissionsBoundary; + const inputPolicyName = params.examplePermissionsBoundary ? CDK_BOOTSTRAP_PERMISSIONS_BOUNDARY : params.customPermissionsBoundary; + let policyName; + if (inputPolicyName) { + // If the example policy is not already in place, it must be created. + const sdk = (await sdkProvider.forEnvironment(environment, Mode.ForWriting)).sdk; + policyName = await this.getPolicyName(environment, sdk, inputPolicyName, partition, params); + } + if (currentPermissionsBoundary !== policyName) { + warning(`Switching from ${currentPermissionsBoundary} to ${policyName} as permissions boundary`); + } return current.update( bootstrapTemplate, @@ -145,12 +162,121 @@ export class Bootstrapper { CloudFormationExecutionPolicies: cloudFormationExecutionPolicies.join(','), Qualifier: params.qualifier, PublicAccessBlockConfiguration: params.publicAccessBlockConfiguration || params.publicAccessBlockConfiguration === undefined ? 'true' : 'false', + InputPermissionsBoundary: policyName, }, { ...options, terminationProtection: options.terminationProtection ?? current.terminationProtection, }); } + private async getPolicyName( + environment: cxapi.Environment, + sdk: ISDK, + permissionsBoundary: string, + partition: string, + params: BootstrappingParameters): Promise { + + if (permissionsBoundary !== CDK_BOOTSTRAP_PERMISSIONS_BOUNDARY) { + this.validatePolicyName(permissionsBoundary); + return Promise.resolve(permissionsBoundary); + } + // if no Qualifier is supplied, resort to the default one + const arn = await this.getExamplePermissionsBoundary(params.qualifier ?? 'hnb659fds', partition, environment.account, sdk); + const policyName = arn.split('/').pop(); + if (!policyName) { + throw new Error('Could not retrieve the example permission boundary!'); + } + return Promise.resolve(policyName); + } + + private async getExamplePermissionsBoundary(qualifier: string, partition: string, account: string, sdk: ISDK): Promise { + const iam = sdk.iam(); + + let policyName = `cdk-${qualifier}-permissions-boundary`; + const arn = `arn:${partition}:iam::${account}:policy/${policyName}`; + + try { + let getPolicyResp = await iam.getPolicy({ PolicyArn: arn }).promise(); + if (getPolicyResp.Policy) { + return arn; + } + } catch (e) { + // https://docs.aws.amazon.com/IAM/latest/APIReference/API_GetPolicy.html#API_GetPolicy_Errors + if (e.name === 'NoSuchEntity') { + //noop, proceed with creating the policy + } else { + throw e; + } + } + + const policyDoc = { + Version: '2012-10-17', + Statement: [ + { + Action: ['*'], + Resource: '*', + Effect: 'Allow', + Sid: 'ExplicitAllowAll', + }, + { + Condition: { + StringEquals: { + 'iam:PermissionsBoundary': `arn:${partition}:iam::${account}:policy/cdk-${qualifier}-permissions-boundary`, + }, + }, + Action: [ + 'iam:CreateUser', + 'iam:CreateRole', + 'iam:PutRolePermissionsBoundary', + 'iam:PutUserPermissionsBoundary', + ], + Resource: '*', + Effect: 'Allow', + Sid: 'DenyAccessIfRequiredPermBoundaryIsNotBeingApplied', + }, + { + Action: [ + 'iam:CreatePolicyVersion', + 'iam:DeletePolicy', + 'iam:DeletePolicyVersion', + 'iam:SetDefaultPolicyVersion', + ], + Resource: `arn:${partition}:iam::${account}:policy/cdk-${qualifier}-permissions-boundary`, + Effect: 'Deny', + Sid: 'DenyPermBoundaryIAMPolicyAlteration', + }, + { + Action: [ + 'iam:DeleteUserPermissionsBoundary', + 'iam:DeleteRolePermissionsBoundary', + ], + Resource: '*', + Effect: 'Deny', + Sid: 'DenyRemovalOfPermBoundaryFromAnyUserOrRole', + }, + ], + }; + const request = { + PolicyName: policyName, + PolicyDocument: JSON.stringify(policyDoc), + }; + const createPolicyResponse = await iam.createPolicy(request).promise(); + if (createPolicyResponse.Policy?.Arn) { + return createPolicyResponse.Policy.Arn; + } else { + throw new Error(`Could not retrieve the example permission boundary ${arn}!`); + } + } + + private validatePolicyName(permissionsBoundary: string) { + // https://docs.aws.amazon.com/IAM/latest/APIReference/API_CreatePolicy.html + const regexp: RegExp = /[\w+=,.@-]+/; + const matches = regexp.exec(permissionsBoundary); + if (!(matches && matches.length === 1 && matches[0] === permissionsBoundary)) { + throw new Error(`The permissions boundary name ${permissionsBoundary} does not match the IAM conventions.`); + } + } + private async customBootstrap( environment: cxapi.Environment, sdkProvider: SdkProvider, @@ -179,7 +305,7 @@ export class Bootstrapper { } /** - * Magic parameter value that will cause the bootstrap-template.yml to NOT create a CMK but use the default keyo + * Magic parameter value that will cause the bootstrap-template.yml to NOT create a CMK but use the default key */ const USE_AWS_MANAGED_KEY = 'AWS_MANAGED_KEY'; @@ -187,6 +313,10 @@ const USE_AWS_MANAGED_KEY = 'AWS_MANAGED_KEY'; * Magic parameter value that will cause the bootstrap-template.yml to create a CMK */ const CREATE_NEW_KEY = ''; +/** + * Parameter value indicating the use of the default, CDK provided permissions boundary for bootstrap-template.yml + */ +const CDK_BOOTSTRAP_PERMISSIONS_BOUNDARY = 'CDK_BOOTSTRAP_PERMISSIONS_BOUNDARY'; /** * Split an array-like CloudFormation parameter on , diff --git a/packages/aws-cdk/lib/api/bootstrap/bootstrap-props.ts b/packages/aws-cdk/lib/api/bootstrap/bootstrap-props.ts index 10fc4fc8ff598..fee2deddfa26b 100644 --- a/packages/aws-cdk/lib/api/bootstrap/bootstrap-props.ts +++ b/packages/aws-cdk/lib/api/bootstrap/bootstrap-props.ts @@ -101,4 +101,18 @@ export interface BootstrappingParameters { */ readonly publicAccessBlockConfiguration?: boolean; -} \ No newline at end of file + /** + * Flag for using the default permissions boundary for bootstrapping + * + * @default - No value, optional argument + */ + readonly examplePermissionsBoundary?: boolean; + + /** + * Name for the customer's custom permissions boundary for bootstrapping + * + * @default - No value, optional argument + */ + readonly customPermissionsBoundary?: string; + +} diff --git a/packages/aws-cdk/lib/api/bootstrap/bootstrap-template.yaml b/packages/aws-cdk/lib/api/bootstrap/bootstrap-template.yaml index f1c8121cdc473..16ad213a62870 100644 --- a/packages/aws-cdk/lib/api/bootstrap/bootstrap-template.yaml +++ b/packages/aws-cdk/lib/api/bootstrap/bootstrap-template.yaml @@ -42,6 +42,14 @@ Parameters: Default: 'true' Type: 'String' AllowedValues: ['true', 'false'] + InputPermissionsBoundary: + Description: Whether or not to use either the CDK supplied or custom permissions boundary + Default: '' + Type: 'String' + UseExamplePermissionsBoundary: + Default: 'false' + AllowedValues: [ 'true', 'false' ] + Type: String Conditions: HasTrustedAccounts: Fn::Not: @@ -77,6 +85,15 @@ Conditions: Fn::Equals: - 'AWS_MANAGED_KEY' - Ref: FileAssetsBucketKmsKeyId + ShouldCreatePermissionsBoundary: + Fn::Equals: + - 'true' + - Ref: UseExamplePermissionsBoundary + PermissionsBoundarySet: + Fn::Not: + - Fn::Equals: + - '' + - Ref: InputPermissionsBoundary HasCustomContainerAssetsRepositoryName: Fn::Not: - Fn::Equals: @@ -500,6 +517,66 @@ Resources: - - Fn::Sub: "arn:${AWS::Partition}:iam::aws:policy/AdministratorAccess" RoleName: Fn::Sub: cdk-${Qualifier}-cfn-exec-role-${AWS::AccountId}-${AWS::Region} + PermissionsBoundary: + Fn::If: + - PermissionsBoundarySet + - Fn::Sub: 'arn:${AWS::Partition}:iam::${AWS::AccountId}:policy/${InputPermissionsBoundary}' + - Ref: AWS::NoValue + CdkBoostrapPermissionsBoundaryPolicy: + # Edit the template prior to boostrap in order to have this example policy created + Condition: ShouldCreatePermissionsBoundary + Type: AWS::IAM::ManagedPolicy + Properties: + PolicyDocument: + Statement: + # If permission boundaries do not have an explicit `allow`, then the effect is `deny` + - Sid: ExplicitAllowAll + Action: + - "*" + Effect: Allow + Resource: "*" + # Default permissions to prevent privilege escalation + - Sid: DenyAccessIfRequiredPermBoundaryIsNotBeingApplied + Action: + - iam:CreateUser + - iam:CreateRole + - iam:PutRolePermissionsBoundary + - iam:PutUserPermissionsBoundary + Condition: + StringNotEquals: + iam:PermissionsBoundary: + Fn::Sub: arn:${AWS::Partition}:iam::${AWS::AccountId}:policy/cdk-${Qualifier}-permissions-boundary-${AWS::AccountId}-${AWS::Region} + Effect: Deny + Resource: "*" + # Forbid the policy itself being edited + - Sid: DenyPermBoundaryIAMPolicyAlteration + Action: + - iam:CreatePolicyVersion + - iam:DeletePolicy + - iam:DeletePolicyVersion + - iam:SetDefaultPolicyVersion + Effect: Deny + Resource: + Fn::Sub: arn:${AWS::Partition}:iam::${AWS::AccountId}:policy/cdk-${Qualifier}-permissions-boundary-${AWS::AccountId}-${AWS::Region} + # Forbid removing the permissions boundary from any user or role that has it associated + - Sid: DenyRemovalOfPermBoundaryFromAnyUserOrRole + Action: + - iam:DeleteUserPermissionsBoundary + - iam:DeleteRolePermissionsBoundary + Effect: Deny + Resource: "*" + # Add your specific organizational security policy here + # Uncomment the example to deny access to AWS Config + #- Sid: OrganizationalSecurityPolicy + # Action: + # - "config:*" + # Effect: Deny + # Resource: "*" + Version: "2012-10-17" + Description: "Bootstrap Permission Boundary" + ManagedPolicyName: + Fn::Sub: cdk-${Qualifier}-permissions-boundary-${AWS::AccountId}-${AWS::Region} + Path: / # The SSM parameter is used in pipeline-deployed templates to verify the version # of the bootstrap resources. CdkBootstrapVersion: diff --git a/packages/aws-cdk/lib/cli.ts b/packages/aws-cdk/lib/cli.ts index 4192c795689cb..b41bbb1731a54 100644 --- a/packages/aws-cdk/lib/cli.ts +++ b/packages/aws-cdk/lib/cli.ts @@ -92,6 +92,8 @@ async function parseCommandLineArguments() { .command('bootstrap [ENVIRONMENTS..]', 'Deploys the CDK toolkit stack into an AWS environment', (yargs: Argv) => yargs .option('bootstrap-bucket-name', { type: 'string', alias: ['b', 'toolkit-bucket-name'], desc: 'The name of the CDK toolkit bucket; bucket will be created and must not exist', default: undefined }) .option('bootstrap-kms-key-id', { type: 'string', desc: 'AWS KMS master key ID used for the SSE-KMS encryption', default: undefined, conflicts: 'bootstrap-customer-key' }) + .option('example-permissions-boundary', { type: 'boolean', alias: ['epb', 'example-permissions-boundary'], desc: 'Use the example permissions boundary.', default: undefined, conflicts: 'custom-permissions-boundary' }) + .option('custom-permissions-boundary', { type: 'string', alias: ['cpb', 'custom-permissions-boundary'], desc: 'Use the permissions boundary specified by name.', default: undefined, conflicts: 'example-permissions-boundary' }) .option('bootstrap-customer-key', { type: 'boolean', desc: 'Create a Customer Master Key (CMK) for the bootstrap bucket (you will be charged but can customize permissions, modern bootstrapping only)', default: undefined, conflicts: 'bootstrap-kms-key-id' }) .option('qualifier', { type: 'string', desc: 'String which must be unique for each bootstrap stack. You must configure it on your CDK app if you change this from the default.', default: undefined }) .option('public-access-block-configuration', { type: 'boolean', desc: 'Block public access configuration on CDK toolkit bucket (enabled by default) ', default: undefined }) @@ -462,6 +464,8 @@ async function initCommandLine() { createCustomerMasterKey: args.bootstrapCustomerKey, qualifier: args.qualifier, publicAccessBlockConfiguration: args.publicAccessBlockConfiguration, + examplePermissionsBoundary: argv.examplePermissionsBoundary, + customPermissionsBoundary: argv.customPermissionsBoundary, trustedAccounts: arrayFromYargs(args.trust), trustedAccountsForLookup: arrayFromYargs(args.trustForLookup), cloudFormationExecutionPolicies: arrayFromYargs(args.cloudformationExecutionPolicies), diff --git a/packages/aws-cdk/test/api/bootstrap2.test.ts b/packages/aws-cdk/test/api/bootstrap2.test.ts index 42b4f24d0a771..5ca8b6e3f9929 100644 --- a/packages/aws-cdk/test/api/bootstrap2.test.ts +++ b/packages/aws-cdk/test/api/bootstrap2.test.ts @@ -1,13 +1,18 @@ + const mockDeployStack = jest.fn(); jest.mock('../../lib/api/deploy-stack', () => ({ deployStack: mockDeployStack, })); +import { IAM } from 'aws-sdk'; import { Bootstrapper, DeployStackOptions, ToolkitInfo } from '../../lib/api'; import { mockBootstrapStack, MockSdk, MockSdkProvider } from '../util/mock-sdk'; let bootstrapper: Bootstrapper; +let mockGetPolicyIamCode: (params: IAM.Types.GetPolicyRequest) => IAM.Types.GetPolicyResponse; +let mockCreatePolicyIamCode: (params: IAM.Types.CreatePolicyRequest) => IAM.Types.CreatePolicyResponse; + beforeEach(() => { bootstrapper = new Bootstrapper({ source: 'default' }); }); @@ -29,6 +34,18 @@ describe('Bootstrapping v2', () => { sdk = new MockSdkProvider({ realSdk: false }); // By default, we'll return a non-found toolkit info (ToolkitInfo as any).lookup = jest.fn().mockResolvedValue(ToolkitInfo.bootstraplessDeploymentsOnly(sdk.sdk)); + const value = { + Policy: { + PolicyName: 'my-policy', + Arn: 'arn:aws:iam::0123456789012:policy/my-policy', + }, + }; + mockGetPolicyIamCode = jest.fn().mockReturnValue(value); + mockCreatePolicyIamCode = jest.fn().mockReturnValue(value); + sdk.stubIam({ + createPolicy: mockCreatePolicyIamCode, + getPolicy: mockGetPolicyIamCode, + }); }); afterEach(() => { @@ -82,6 +99,34 @@ describe('Bootstrapping v2', () => { })); }); + test('passes true to PermissionsBoundary', async () => { + await bootstrapper.bootstrapEnvironment(env, sdk, { + parameters: { + examplePermissionsBoundary: true, + }, + }); + + expect(mockDeployStack).toHaveBeenCalledWith(expect.objectContaining({ + parameters: expect.objectContaining({ + InputPermissionsBoundary: 'cdk-hnb659fds-permissions-boundary', + }), + })); + }); + + test('passes value to PermissionsBoundary', async () => { + await bootstrapper.bootstrapEnvironment(env, sdk, { + parameters: { + customPermissionsBoundary: 'permissions-boundary-name', + }, + }); + + expect(mockDeployStack).toHaveBeenCalledWith(expect.objectContaining({ + parameters: expect.objectContaining({ + InputPermissionsBoundary: 'permissions-boundary-name', + }), + })); + }); + test('passing trusted accounts without CFN managed policies results in an error', async () => { await expect(bootstrapper.bootstrapEnvironment(env, sdk, { parameters: { @@ -328,4 +373,4 @@ describe('Bootstrapping v2', () => { })); }); }); -}); \ No newline at end of file +}); diff --git a/packages/aws-cdk/test/integ/cli/bootstrapping.integtest.ts b/packages/aws-cdk/test/integ/cli/bootstrapping.integtest.ts index 95f98145a0a16..f1fa6a48873c0 100644 --- a/packages/aws-cdk/test/integ/cli/bootstrapping.integtest.ts +++ b/packages/aws-cdk/test/integ/cli/bootstrapping.integtest.ts @@ -193,6 +193,28 @@ integTest('can dump the template, modify and use it to deploy a custom bootstrap }); })); +integTest('can use the default permissions boundary to bootstrap', withDefaultFixture(async (fixture) => { + let template = await fixture.cdkBootstrapModern({ + // toolkitStackName doesn't matter for this particular invocation + toolkitStackName: fixture.bootstrapStackName, + showTemplate: true, + examplePermissionsBoundary: true, + }); + + expect(template).toContain(`arn:aws:iam::${await fixture.aws.account()}:policy/cdk-${fixture.qualifier}-permissions-boundary`); +})); + +integTest('can use the custom permissions boundary to bootstrap', withDefaultFixture(async (fixture) => { + let template = await fixture.cdkBootstrapModern({ + // toolkitStackName doesn't matter for this particular invocation + toolkitStackName: fixture.bootstrapStackName, + showTemplate: true, + customPermissionsBoundary: 'permission-boundary-name', + }); + + expect(template).toContain('permission-boundary-name'); +})); + integTest('switch on termination protection, switch is left alone on re-bootstrap', withDefaultFixture(async (fixture) => { const bootstrapStackName = fixture.bootstrapStackName; @@ -276,3 +298,4 @@ integTest('create ECR with tag IMMUTABILITY to set on', withDefaultFixture(async expect(ecrResponse.repositories?.[0].imageTagMutability).toEqual('IMMUTABLE'); })); + diff --git a/packages/aws-cdk/test/integ/helpers/cdk.ts b/packages/aws-cdk/test/integ/helpers/cdk.ts index 1dae12156313e..6b95f2d5d44f1 100644 --- a/packages/aws-cdk/test/integ/helpers/cdk.ts +++ b/packages/aws-cdk/test/integ/helpers/cdk.ts @@ -303,6 +303,16 @@ export interface CdkModernBootstrapCommandOptions extends CommonCdkBootstrapComm * @default false */ readonly terminationProtection?: boolean; + + /** + * @default undefined + */ + readonly examplePermissionsBoundary?: boolean; + + /** + * @default undefined + */ + readonly customPermissionsBoundary?: string; } export class TestFixture { @@ -415,6 +425,11 @@ export class TestFixture { if (options.tags) { args.push('--tags', options.tags); } + if (options.customPermissionsBoundary !== undefined) { + args.push('--custom-permissions-boundary', options.customPermissionsBoundary); + } else if (options.examplePermissionsBoundary !== undefined) { + args.push('--example-permissions-boundary'); + } return this.cdk(args, { ...options.cliOptions, diff --git a/packages/aws-cdk/test/util/mock-sdk.ts b/packages/aws-cdk/test/util/mock-sdk.ts index ae3d590e70f7e..7a16fda09b132 100644 --- a/packages/aws-cdk/test/util/mock-sdk.ts +++ b/packages/aws-cdk/test/util/mock-sdk.ts @@ -26,7 +26,7 @@ export interface MockSdkProviderOptions { /** * An SDK that allows replacing (some of) the clients * - * Its the responsibility of the consumer to replace all calls that + * It's the responsibility of the consumer to replace all calls that * actually will be called. */ export class MockSdkProvider extends SdkProvider { @@ -114,6 +114,10 @@ export class MockSdkProvider extends SdkProvider { (this.sdk as any).lambda = jest.fn().mockReturnValue(partialAwsService(stubs, additionalProperties)); } + public stubIam(stubs: SyncHandlerSubsetOf, additionalProperties: { [key: string]: any } = {}) { + (this.sdk as any).iam = jest.fn().mockReturnValue(partialAwsService(stubs, additionalProperties)); + } + public stubStepFunctions(stubs: SyncHandlerSubsetOf) { (this.sdk as any).stepFunctions = jest.fn().mockReturnValue(partialAwsService(stubs)); } @@ -138,6 +142,7 @@ export class MockSdkProvider extends SdkProvider { export class MockSdk implements ISDK { public readonly currentRegion: string = 'bermuda-triangle-1337'; public readonly lambda = jest.fn(); + public readonly iam = jest.fn(); public readonly cloudFormation = jest.fn(); public readonly ec2 = jest.fn(); public readonly ssm = jest.fn();