Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions packages/@aws-cdk-testing/cli-integ/lib/with-cdk-app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -290,6 +290,10 @@ export interface CdkModernBootstrapCommandOptions extends CommonCdkBootstrapComm
* @default undefined
*/
readonly usePreviousParameters?: boolean;

readonly trust?: string[];

readonly untrust?: string[];
}

export interface CdkGarbageCollectionCommandOptions {
Expand Down Expand Up @@ -445,6 +449,13 @@ export class TestFixture extends ShellHelper {
args.push('--template', options.bootstrapTemplate);
}

if (options.trust != null) {
args.push('--trust', options.trust.join(','));
}
if (options.untrust != null) {
args.push('--untrust', options.untrust.join(','));
}

return this.cdk(args, {
...options.cliOptions,
modEnv: {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -491,3 +491,28 @@ integTest('create ECR with tag IMMUTABILITY to set on', withoutBootstrap(async (
expect(ecrResponse.repositories?.[0].imageTagMutability).toEqual('IMMUTABLE');
}));

integTest('can remove trusted account', withoutBootstrap(async (fixture) => {
const bootstrapStackName = fixture.bootstrapStackName;

await fixture.cdkBootstrapModern({
verbose: false,
toolkitStackName: bootstrapStackName,
cfnExecutionPolicy: 'arn:aws:iam::aws:policy/AdministratorAccess',
trust: ['599757620138', '730170552321'],
});

await fixture.cdkBootstrapModern({
verbose: true,
toolkitStackName: bootstrapStackName,
cfnExecutionPolicy: ' arn:aws:iam::aws:policy/AdministratorAccess',
untrust: ['730170552321'],
});

const response = await fixture.aws.cloudFormation.send(
new DescribeStacksCommand({ StackName: bootstrapStackName }),
);

const trustedAccounts = response.Stacks?.[0].Parameters?.find(p => p.ParameterKey === 'TrustedAccounts')?.ParameterValue;
expect(trustedAccounts).toEqual('599757620138');
}));

23 changes: 20 additions & 3 deletions packages/aws-cdk/lib/api/bootstrap/bootstrap-environment.ts
Original file line number Diff line number Diff line change
Expand Up @@ -102,11 +102,24 @@ export class Bootstrapper {
// Ideally we'd do this inside the template, but the `Rules` section of CFN
// templates doesn't seem to be able to express the conditions that we need
// (can't use Fn::Join or reference Conditions) so we do it here instead.
const trustedAccounts = params.trustedAccounts ?? splitCfnArray(current.parameters.TrustedAccounts);
const allTrusted = new Set([
...params.trustedAccounts ?? [],
...params.trustedAccountsForLookup ?? [],
]);
const invalid = intersection(allTrusted, new Set(params.untrustedAccounts));
if (invalid.size > 0) {
throw new ToolkitError(`Accounts cannot be both trusted and untrusted. Found: ${[...invalid].join(',')}`);
}

const removeUntrusted = (accounts: string[]) =>
accounts.filter(acc => !params.untrustedAccounts?.map(String).includes(String(acc)));
Copy link
Contributor Author

@otaviomacedo otaviomacedo Jan 23, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Defensively programming against yargs: despite being typed as string, these values may come as numbers.


const trustedAccounts = removeUntrusted(params.trustedAccounts ?? splitCfnArray(current.parameters.TrustedAccounts));
info(`Trusted accounts for deployment: ${trustedAccounts.length > 0 ? trustedAccounts.join(', ') : '(none)'}`);

const trustedAccountsForLookup =
params.trustedAccountsForLookup ?? splitCfnArray(current.parameters.TrustedAccountsForLookup);
const trustedAccountsForLookup = removeUntrusted(
params.trustedAccountsForLookup ?? splitCfnArray(current.parameters.TrustedAccountsForLookup),
);
info(
`Trusted accounts for lookup: ${trustedAccountsForLookup.length > 0 ? trustedAccountsForLookup.join(', ') : '(none)'}`,
);
Expand Down Expand Up @@ -376,3 +389,7 @@ function splitCfnArray(xs: string | undefined): string[] {
}
return xs.split(',');
}

function intersection<A>(xs: Set<A>, ys: Set<A>): Set<A> {
return new Set<A>(Array.from(xs).filter(x => ys.has(x)));
}
8 changes: 8 additions & 0 deletions packages/aws-cdk/lib/api/bootstrap/bootstrap-props.ts
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,14 @@ export interface BootstrappingParameters {
*/
readonly trustedAccountsForLookup?: string[];

/**
* The list of AWS account IDs that should not be trusted by the bootstrapped environment.
* If these accounts are already trusted, they will be removed on bootstrapping.
*
* @default - no account will be untrusted.
*/
readonly untrustedAccounts?: string[];

/**
* The ARNs of the IAM managed policies that should be attached to the role performing CloudFormation deployments.
* In most cases, this will be the AdministratorAccess policy.
Expand Down
1 change: 1 addition & 0 deletions packages/aws-cdk/lib/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -279,6 +279,7 @@ export async function exec(args: string[], synthesizer?: Synthesizer): Promise<n
customPermissionsBoundary: argv.customPermissionsBoundary,
trustedAccounts: arrayFromYargs(args.trust),
trustedAccountsForLookup: arrayFromYargs(args.trustForLookup),
untrustedAccounts: arrayFromYargs(args.untrust),
cloudFormationExecutionPolicies: arrayFromYargs(args.cloudformationExecutionPolicies),
},
});
Expand Down
1 change: 1 addition & 0 deletions packages/aws-cdk/lib/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,7 @@ export async function makeConfig(): Promise<CliConfig> {
'execute': { type: 'boolean', desc: 'Whether to execute ChangeSet (--no-execute will NOT execute the ChangeSet)', default: true },
'trust': { type: 'array', desc: 'The AWS account IDs that should be trusted to perform deployments into this environment (may be repeated, modern bootstrapping only)', default: [] },
'trust-for-lookup': { type: 'array', desc: 'The AWS account IDs that should be trusted to look up values in this environment (may be repeated, modern bootstrapping only)', default: [] },
'untrust': { type: 'array', desc: 'The AWS account IDs that should not be trusted by this environment (may be repeated, modern bootstrapping only)', default: [] },
'cloudformation-execution-policies': { type: 'array', desc: 'The Managed Policy ARNs that should be attached to the role performing deployments into this environment (may be repeated, modern bootstrapping only)', default: [] },
'force': { alias: 'f', type: 'boolean', desc: 'Always bootstrap even if it would downgrade template version', default: false },
'termination-protection': { type: 'boolean', default: undefined, desc: 'Toggle CloudFormation termination protection on the bootstrap stacks' },
Expand Down
2 changes: 2 additions & 0 deletions packages/aws-cdk/lib/convert-to-user-input.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ export function convertYargsToUserInput(args: any): UserInput {
execute: args.execute,
trust: args.trust,
trustForLookup: args.trustForLookup,
untrust: args.untrust,
cloudformationExecutionPolicies: args.cloudformationExecutionPolicies,
force: args.force,
terminationProtection: args.terminationProtection,
Expand Down Expand Up @@ -309,6 +310,7 @@ export function convertConfigToUserInput(config: any): UserInput {
execute: config.bootstrap?.execute,
trust: config.bootstrap?.trust,
trustForLookup: config.bootstrap?.trustForLookup,
untrust: config.bootstrap?.untrust,
cloudformationExecutionPolicies: config.bootstrap?.cloudformationExecutionPolicies,
force: config.bootstrap?.force,
terminationProtection: config.bootstrap?.terminationProtection,
Expand Down
7 changes: 7 additions & 0 deletions packages/aws-cdk/lib/parse-command-line-arguments.ts
Original file line number Diff line number Diff line change
Expand Up @@ -263,6 +263,13 @@ export function parseCommandLineArguments(args: Array<string>): any {
nargs: 1,
requiresArg: true,
})
.option('untrust', {
default: [],
type: 'array',
desc: 'The AWS account IDs that should not be trusted by this environment (may be repeated, modern bootstrapping only)',
nargs: 1,
requiresArg: true,
})
.option('cloudformation-execution-policies', {
default: [],
type: 'array',
Expand Down
7 changes: 7 additions & 0 deletions packages/aws-cdk/lib/user-input.ts
Original file line number Diff line number Diff line change
Expand Up @@ -464,6 +464,13 @@ export interface BootstrapOptions {
*/
readonly trustForLookup?: Array<string>;

/**
* The AWS account IDs that should not be trusted by this environment (may be repeated, modern bootstrapping only)
*
* @default - []
*/
readonly untrust?: Array<string>;

/**
* The Managed Policy ARNs that should be attached to the role performing deployments into this environment (may be repeated, modern bootstrapping only)
*
Expand Down
69 changes: 69 additions & 0 deletions packages/aws-cdk/test/api/bootstrap2.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -330,6 +330,75 @@ describe('Bootstrapping v2', () => {
// Did not throw
});

test('removes trusted account when it is listed as untrusted', async () => {
// GIVEN
mockTheToolkitInfo({
Parameters: [
{
ParameterKey: 'CloudFormationExecutionPolicies',
ParameterValue: 'arn:aws:something',
},
{
ParameterKey: 'TrustedAccounts',
ParameterValue: '111111111111,222222222222',
},
],
});

await bootstrapper.bootstrapEnvironment(env, sdk, {
parameters: {
untrustedAccounts: ['111111111111'],
},
});

expect(mockDeployStack).toHaveBeenCalledWith(
expect.objectContaining({
parameters: expect.objectContaining({
TrustedAccounts: '222222222222',
}),
}),
);
});

test('removes trusted account for lookup when it is listed as untrusted', async () => {
// GIVEN
mockTheToolkitInfo({
Parameters: [
{
ParameterKey: 'CloudFormationExecutionPolicies',
ParameterValue: 'arn:aws:something',
},
{
ParameterKey: 'TrustedAccountsForLookup',
ParameterValue: '111111111111,222222222222',
},
],
});

await bootstrapper.bootstrapEnvironment(env, sdk, {
parameters: {
untrustedAccounts: ['111111111111'],
},
});

expect(mockDeployStack).toHaveBeenCalledWith(
expect.objectContaining({
parameters: expect.objectContaining({
TrustedAccountsForLookup: '222222222222',
}),
}),
);
});

test('do not allow accounts to be listed as both trusted and untrusted', async () => {
await expect(bootstrapper.bootstrapEnvironment(env, sdk, {
parameters: {
trustedAccountsForLookup: ['123456789012'],
untrustedAccounts: ['123456789012'],
},
})).rejects.toThrow('Accounts cannot be both trusted and untrusted. Found: 123456789012');
});

test('Do not allow downgrading bootstrap stack version', async () => {
// GIVEN
mockTheToolkitInfo({
Expand Down
Loading