From 95579181c2589b1d08c4d3ffcb90d439055ba68d Mon Sep 17 00:00:00 2001 From: Romain Marcadier Date: Tue, 25 Jan 2022 14:55:19 +0100 Subject: [PATCH] fix(region-info): incorrect codedeploy service principals (#18505) In "special" regions (GovCloud, US-ISO), the CodeDeploy service principal uses the `amazonaws.com` DNS suffix. In fact, that is true of all regions, in all partitions, with the notable exception of `aws-cn` which uses the `amazonaws.com.cn` suffix instead. ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license* --- .../test/lambda/deployment-group.test.ts | 10 +- .../@aws-cdk/region-info/lib/aws-entities.ts | 33 +++-- packages/@aws-cdk/region-info/lib/default.ts | 134 ++++++++++-------- packages/@aws-cdk/region-info/lib/fact.ts | 3 +- .../__snapshots__/region-info.test.js.snap | 6 +- .../@aws-cdk/region-info/test/default.test.ts | 10 +- 6 files changed, 112 insertions(+), 84 deletions(-) diff --git a/packages/@aws-cdk/aws-codedeploy/test/lambda/deployment-group.test.ts b/packages/@aws-cdk/aws-codedeploy/test/lambda/deployment-group.test.ts index 9333b05106220..365a03c4d5d30 100644 --- a/packages/@aws-cdk/aws-codedeploy/test/lambda/deployment-group.test.ts +++ b/packages/@aws-cdk/aws-codedeploy/test/lambda/deployment-group.test.ts @@ -87,7 +87,15 @@ describe('CodeDeploy Lambda DeploymentGroup', () => { Action: 'sts:AssumeRole', Effect: 'Allow', Principal: { - Service: { 'Fn::Join': ['', ['codedeploy.', { Ref: 'AWS::Region' }, '.', { Ref: 'AWS::URLSuffix' }]] }, + Service: { + 'Fn::FindInMap': [ + 'ServiceprincipalMap', + { + Ref: 'AWS::Region', + }, + 'codedeploy', + ], + }, }, }], Version: '2012-10-17', diff --git a/packages/@aws-cdk/region-info/lib/aws-entities.ts b/packages/@aws-cdk/region-info/lib/aws-entities.ts index 84749afba24b9..9c14e89605607 100644 --- a/packages/@aws-cdk/region-info/lib/aws-entities.ts +++ b/packages/@aws-cdk/region-info/lib/aws-entities.ts @@ -1,17 +1,14 @@ -// Rule prefix -const RULE_ = 'RULE_'; - /** * After this point, SSM only creates regional principals */ -export const RULE_SSM_PRINCIPALS_ARE_REGIONAL = `${RULE_}SSM_PRINCIPALS_ARE_REGIONAL`; +export const RULE_SSM_PRINCIPALS_ARE_REGIONAL = Symbol('SSM_PRINCIPALS_ARE_REGIONAL'); /** * After this point, S3 website domains look like `s3-website.REGION.s3.amazonaws.com` * * Before this point, S3 website domains look like `s3-website-REGION.s3.amazonaws.com`. */ -export const RULE_S3_WEBSITE_REGIONAL_SUBDOMAIN = `${RULE_}S3_WEBSITE_REGIONAL_SUBDOMAIN`; +export const RULE_S3_WEBSITE_REGIONAL_SUBDOMAIN = Symbol('S3_WEBSITE_REGIONAL_SUBDOMAIN'); /** * List of AWS region, ordered by launch date (oldest to newest) @@ -21,13 +18,13 @@ export const RULE_S3_WEBSITE_REGIONAL_SUBDOMAIN = `${RULE_}S3_WEBSITE_REGIONAL_S * regions are left as-is. * * We mix the list of regions with a list of rules that were introduced over - * time (rules are strings starting with `RULE_`). + * time (rules are symbols). * * Therefore, if we want to know if a rule applies to a certain region, we * only need to check its position in the list and compare it to when a * rule was introduced. */ -export const AWS_REGIONS_AND_RULES = [ +export const AWS_REGIONS_AND_RULES: readonly (string | symbol)[] = [ 'us-east-1', // US East (N. Virginia) 'eu-west-1', // Europe (Ireland) 'us-west-1', // US West (N. California) @@ -68,15 +65,15 @@ export const AWS_REGIONS_AND_RULES = [ * Not in the list ==> no built-in data for that region. */ export const AWS_REGIONS = AWS_REGIONS_AND_RULES - .filter((x) => !x.startsWith(RULE_)) - .sort(); + .filter((x) => typeof x === 'string') + .sort() as readonly string[]; /** * Possibly non-exaustive list of all service names, used to locate service principals. * * Not in the list ==> default service principal mappings. */ -export const AWS_SERVICES = [ +export const AWS_SERVICES: readonly string[] = [ 'application-autoscaling', 'autoscaling', 'codedeploy', @@ -96,10 +93,10 @@ export const AWS_SERVICES = [ * * Unknown region => we have to assume no. */ -export function before(region: string, ruleOrRegion: string) { +export function before(region: string, ruleOrRegion: string | symbol) { const ruleIx = AWS_REGIONS_AND_RULES.indexOf(ruleOrRegion); if (ruleIx === -1) { - throw new Error(`Unknown rule: ${ruleOrRegion}`); + throw new Error(`Unknown rule: ${String(ruleOrRegion)}`); } const regionIx = AWS_REGIONS_AND_RULES.indexOf(region); return regionIx === -1 ? false : regionIx < ruleIx; @@ -108,17 +105,19 @@ export function before(region: string, ruleOrRegion: string) { /** * Return all regions before a given rule was introduced (or region) */ -export function regionsBefore(ruleOrRegion: string): string[] { +export function regionsBefore(ruleOrRegion: string | symbol): string[] { const ruleIx = AWS_REGIONS_AND_RULES.indexOf(ruleOrRegion); if (ruleIx === -1) { - throw new Error(`Unknown rule: ${ruleOrRegion}`); + throw new Error(`Unknown rule: ${String(ruleOrRegion)}`); } - return AWS_REGIONS_AND_RULES.filter((_, i) => i < ruleIx).sort(); + return AWS_REGIONS_AND_RULES.slice(0, ruleIx) + .filter((entry) => typeof entry === 'string') + .sort() as string[]; } -export interface Region { partition: string, domainSuffix: string } +export interface Region { readonly partition: string, readonly domainSuffix: string } -const PARTITION_MAP: { [region: string]: Region } = { +const PARTITION_MAP: {readonly [region: string]: Region } = { 'default': { partition: 'aws', domainSuffix: 'amazonaws.com' }, 'cn-': { partition: 'aws-cn', domainSuffix: 'amazonaws.com.cn' }, 'us-gov-': { partition: 'aws-us-gov', domainSuffix: 'amazonaws.com' }, diff --git a/packages/@aws-cdk/region-info/lib/default.ts b/packages/@aws-cdk/region-info/lib/default.ts index b11aa881f955c..e306bd8c1fc25 100644 --- a/packages/@aws-cdk/region-info/lib/default.ts +++ b/packages/@aws-cdk/region-info/lib/default.ts @@ -23,8 +23,8 @@ export class Default { * @param urlSuffix deprecated and ignored. */ public static servicePrincipal(serviceFqn: string, region: string, urlSuffix: string): string { - const service = extractSimpleName(serviceFqn); - if (!service) { + const serviceName = extractSimpleName(serviceFqn); + if (!serviceName) { // Return "service" if it does not look like any of the following: // - s3 // - s3.amazonaws.com @@ -34,72 +34,86 @@ export class Default { return serviceFqn; } - // Exceptions for Service Principals in us-iso-* - const US_ISO_EXCEPTIONS = new Set([ - 'cloudhsm', - 'config', - 'states', - 'workspaces', - ]); - - // Account for idiosyncratic Service Principals in `us-iso-*` regions - if (region.startsWith('us-iso-') && US_ISO_EXCEPTIONS.has(service)) { - switch (service) { - // Services with universal principal - case ('states'): - return `${service}.amazonaws.com`; - - // Services with a partitional principal - default: - return `${service}.${urlSuffix}`; + function determineConfiguration(service: string): (service: string, region: string, urlSuffix: string) => string { + function universal(s: string) { return `${s}.amazonaws.com`; }; + function partitional(s: string, _: string, u: string) { return `${s}.${u}`; }; + function regional(s: string, r: string) { return `${s}.${r}.amazonaws.com`; }; + function regionalPartitional(s: string, r: string, u: string) { return `${s}.${r}.${u}`; }; + + // Exceptions for Service Principals in us-iso-* + const US_ISO_EXCEPTIONS = new Set([ + 'cloudhsm', + 'config', + 'states', + 'workspaces', + ]); + + // Account for idiosyncratic Service Principals in `us-iso-*` regions + if (region.startsWith('us-iso-') && US_ISO_EXCEPTIONS.has(service)) { + switch (service) { + // Services with universal principal + case ('states'): + return universal; + + // Services with a partitional principal + default: + return partitional; + } } - } - // Exceptions for Service Principals in us-isob-* - const US_ISOB_EXCEPTIONS = new Set([ - 'dms', - 'states', - ]); + // Exceptions for Service Principals in us-isob-* + const US_ISOB_EXCEPTIONS = new Set([ + 'dms', + 'states', + ]); + + // Account for idiosyncratic Service Principals in `us-isob-*` regions + if (region.startsWith('us-isob-') && US_ISOB_EXCEPTIONS.has(service)) { + switch (service) { + // Services with universal principal + case ('states'): + return universal; + + // Services with a partitional principal + default: + return partitional; + } + } - // Account for idiosyncratic Service Principals in `us-isob-*` regions - if (region.startsWith('us-isob-') && US_ISOB_EXCEPTIONS.has(service)) { switch (service) { - // Services with universal principal - case ('states'): - return `${service}.amazonaws.com`; + // SSM turned from global to regional at some point + case 'ssm': + return before(region, RULE_SSM_PRINCIPALS_ARE_REGIONAL) + ? universal + : regional; + + // CodeDeploy is regional+partitional in CN, only regional everywhere else + case 'codedeploy': + return region.startsWith('cn-') + ? regionalPartitional + : regional; + + // Services with a regional AND partitional principal + case 'logs': + return regionalPartitional; + + // Services with a regional principal + case 'states': + return regional; // Services with a partitional principal - default: - return `${service}.${urlSuffix}`; - } - } - - // SSM turned from global to regional at some point - if (service === 'ssm') { - return before(region, RULE_SSM_PRINCIPALS_ARE_REGIONAL) - ? `${service}.amazonaws.com` - : `${service}.${region}.amazonaws.com`; - } - - switch (service) { - // Services with a regional AND partitional principal - case 'codedeploy': - case 'logs': - return `${service}.${region}.${urlSuffix}`; - - // Services with a regional principal - case 'states': - return `${service}.${region}.amazonaws.com`; + case 'ec2': + return partitional; - // Services with a partitional principal - case 'ec2': - return `${service}.${urlSuffix}`; + // Services with a universal principal across all regions/partitions (the default case) + default: + return universal; - // Services with a universal principal across all regions/partitions (the default case) - default: - return `${service}.amazonaws.com`; + } + }; - } + const configuration = determineConfiguration(serviceName); + return configuration(serviceName, region, urlSuffix); } private constructor() { } @@ -108,4 +122,4 @@ export class Default { function extractSimpleName(serviceFqn: string) { const matches = serviceFqn.match(/^([^.]+)(?:(?:\.amazonaws\.com(?:\.cn)?)|(?:\.c2s\.ic\.gov)|(?:\.sc2s\.sgov\.gov))?$/); return matches ? matches[1] : undefined; -} \ No newline at end of file +} diff --git a/packages/@aws-cdk/region-info/lib/fact.ts b/packages/@aws-cdk/region-info/lib/fact.ts index 0c4d12a53bec4..9c2831f67d2c6 100644 --- a/packages/@aws-cdk/region-info/lib/fact.ts +++ b/packages/@aws-cdk/region-info/lib/fact.ts @@ -9,7 +9,8 @@ export class Fact { * may not be an exhaustive list of all available AWS regions. */ public static get regions(): string[] { - return AWS_REGIONS; + // Return by copy to ensure no modifications can be made to the undelying constant. + return Array.from(AWS_REGIONS); } /** diff --git a/packages/@aws-cdk/region-info/test/__snapshots__/region-info.test.js.snap b/packages/@aws-cdk/region-info/test/__snapshots__/region-info.test.js.snap index 7ba9353557b9b..678a65fb4ccc3 100644 --- a/packages/@aws-cdk/region-info/test/__snapshots__/region-info.test.js.snap +++ b/packages/@aws-cdk/region-info/test/__snapshots__/region-info.test.js.snap @@ -795,7 +795,7 @@ Object { "servicePrincipals": Object { "application-autoscaling": "application-autoscaling.amazonaws.com", "autoscaling": "autoscaling.amazonaws.com", - "codedeploy": "codedeploy.us-iso-east-1.c2s.ic.gov", + "codedeploy": "codedeploy.us-iso-east-1.amazonaws.com", "ec2": "ec2.c2s.ic.gov", "events": "events.amazonaws.com", "lambda": "lambda.amazonaws.com", @@ -826,7 +826,7 @@ Object { "servicePrincipals": Object { "application-autoscaling": "application-autoscaling.amazonaws.com", "autoscaling": "autoscaling.amazonaws.com", - "codedeploy": "codedeploy.us-iso-west-1.c2s.ic.gov", + "codedeploy": "codedeploy.us-iso-west-1.amazonaws.com", "ec2": "ec2.c2s.ic.gov", "events": "events.amazonaws.com", "lambda": "lambda.amazonaws.com", @@ -857,7 +857,7 @@ Object { "servicePrincipals": Object { "application-autoscaling": "application-autoscaling.amazonaws.com", "autoscaling": "autoscaling.amazonaws.com", - "codedeploy": "codedeploy.us-isob-east-1.sc2s.sgov.gov", + "codedeploy": "codedeploy.us-isob-east-1.amazonaws.com", "ec2": "ec2.sc2s.sgov.gov", "events": "events.amazonaws.com", "lambda": "lambda.amazonaws.com", diff --git a/packages/@aws-cdk/region-info/test/default.test.ts b/packages/@aws-cdk/region-info/test/default.test.ts index d1a320fcaac60..dd0dcd68b0a8d 100644 --- a/packages/@aws-cdk/region-info/test/default.test.ts +++ b/packages/@aws-cdk/region-info/test/default.test.ts @@ -5,12 +5,12 @@ const urlSuffix = '.nowhere.null'; describe('servicePrincipal', () => { for (const suffix of ['', '.amazonaws.com', '.amazonaws.com.cn']) { - for (const service of ['states', 'ssm']) { + for (const service of ['codedeploy', 'states', 'ssm']) { test(`${service}${suffix}`, () => { expect(Default.servicePrincipal(`${service}${suffix}`, region, urlSuffix)).toBe(`${service}.${region}.amazonaws.com`); }); } - for (const service of ['codedeploy', 'logs']) { + for (const service of ['logs']) { test(`${service}${suffix}`, () => { expect(Default.servicePrincipal(`${service}${suffix}`, region, urlSuffix)).toBe(`${service}.${region}.${urlSuffix}`); }); @@ -48,6 +48,12 @@ describe('servicePrincipal', () => { }); } + for (const cnRegion of ['cn-north-1', 'cn-northwest-1']) { + test(`Exceptions: codedeploy in ${cnRegion}`, () => { + expect(Default.servicePrincipal('codedeploy', cnRegion, 'amazonaws.com.cn')).toBe(`codedeploy.${cnRegion}.amazonaws.com.cn`); + }); + } + });