diff --git a/package-lock.json b/package-lock.json index 7ab12d8f9..a8b10e113 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2717,14 +2717,14 @@ ] }, "node_modules/@guardian/cdk": { - "version": "62.5.1", - "resolved": "https://registry.npmjs.org/@guardian/cdk/-/cdk-62.5.1.tgz", - "integrity": "sha512-nu5Bp/T2XGvSchR3HrjeEwQk6US37a3x3d6zG7RHx6moNFO+fHMmkLvtNv9aMKDl9ZBdYZg5udHpPQbkI9RNZQ==", + "version": "62.5.3", + "resolved": "https://registry.npmjs.org/@guardian/cdk/-/cdk-62.5.3.tgz", + "integrity": "sha512-eP/43jSoUHyA873S2Cd/VxuVOpMLHr3jgKVw/7UlfBQzAldKU78XFUmfBaemmtEC4JcGyYjWS2QQScGc0AIA6Q==", "dev": true, "dependencies": { - "@aws-sdk/client-ec2": "^3.1000.0", - "@aws-sdk/client-ssm": "^3.1000.0", - "@aws-sdk/credential-providers": "^3.1000.0", + "@aws-sdk/client-ec2": "^3.1001.0", + "@aws-sdk/client-ssm": "^3.1001.0", + "@aws-sdk/credential-providers": "^3.1001.0", "chalk": "^4.1.2", "codemaker": "^1.127.0", "git-url-parse": "^16.0.1", @@ -2736,7 +2736,7 @@ "gu-cdk": "bin/gu-cdk" }, "peerDependencies": { - "aws-cdk": "^2.1108.0", + "aws-cdk": "^2.1110.0", "aws-cdk-lib": "^2.241.0", "constructs": "^10.5.1" } @@ -14472,7 +14472,7 @@ "version": "1.0.0", "devDependencies": { "@guardian/aws-account-setup": "github:guardian/aws-account-setup#shared@0.0.1", - "@guardian/cdk": "62.5.1", + "@guardian/cdk": "62.5.3", "@types/jest": "^30.0.0", "@types/js-yaml": "^4.0.9", "aws-cron-parser": "^1.1.12", diff --git a/packages/cdk/lib/__snapshots__/service-catalogue.test.ts.snap b/packages/cdk/lib/__snapshots__/service-catalogue.test.ts.snap index 0dfe02af4..2f4a48ae4 100644 --- a/packages/cdk/lib/__snapshots__/service-catalogue.test.ts.snap +++ b/packages/cdk/lib/__snapshots__/service-catalogue.test.ts.snap @@ -11,6 +11,7 @@ exports[`The ServiceCatalogue stack matches the snapshot 1`] = ` "GuLoggingStreamNameParameter", "GuStringParameter", "GuStringParameter", + "GuDeveloperPolicyExperimental", "GuDistributionBucketParameter", "GuScheduledLambda", "GuAnghammaradTopicParameter", @@ -24140,6 +24141,110 @@ spec: "Type": "AWS::SecretsManager::Secret", "UpdateReplacePolicy": "Delete", }, + "ServiceCatalogueCliPolicy7789F330": { + "Properties": { + "Description": "Service Catalogue CLI", + "Path": "/developer-policy/service-catalogue-cli/", + "PolicyDocument": { + "Statement": [ + { + "Action": [ + "ecs:RunTask", + "ecs:List*", + "ecs:Describe*", + ], + "Effect": "Allow", + "Resource": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition", + }, + ":ecs:eu-west-1:", + { + "Ref": "AWS::AccountId", + }, + ":task/", + { + "Ref": "servicecatalogueCluster5FC34DC5", + }, + "/*", + ], + ], + }, + }, + { + "Action": "ssm:GetParameter", + "Effect": "Allow", + "Resource": [ + { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition", + }, + ":ssm:eu-west-1:", + { + "Ref": "AWS::AccountId", + }, + ":parameter//PROD/deploy/service-catalogue/*", + ], + ], + }, + { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition", + }, + ":ssm:eu-west-1:", + { + "Ref": "AWS::AccountId", + }, + ":parameter//PROD/deploy/riff-raff/external-database-access-security-group", + ], + ], + }, + { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition", + }, + ":ssm:eu-west-1:", + { + "Ref": "AWS::AccountId", + }, + ":parameter//account/vpc/primary/subnets/private", + ], + ], + }, + ], + }, + { + "Action": [ + "secretsmanager:GetSecretValue", + "secretsmanager:ListSecrets", + ], + "Effect": "Allow", + "Resource": { + "Ref": "PostgresInstance1SecretAttachmentBA0D257D", + }, + }, + ], + "Version": "2012-10-17", + }, + }, + "Type": "AWS::IAM::ManagedPolicy", + }, "TopicBFC7AF6E": { "Properties": { "Tags": [ diff --git a/packages/cdk/lib/cloudquery/index.ts b/packages/cdk/lib/cloudquery/index.ts index 23ca5447c..ca5242ff4 100644 --- a/packages/cdk/lib/cloudquery/index.ts +++ b/packages/cdk/lib/cloudquery/index.ts @@ -1,13 +1,16 @@ import { GuardianAwsAccounts } from '@guardian/aws-account-setup'; +import { NAMED_SSM_PARAMETER_PATHS } from '@guardian/cdk/lib/constants'; import type { GuStack } from '@guardian/cdk/lib/constructs/core'; import { GuStringParameter } from '@guardian/cdk/lib/constructs/core'; import { GuSecurityGroup } from '@guardian/cdk/lib/constructs/ec2'; import { GuS3Bucket } from '@guardian/cdk/lib/constructs/s3'; +import type { GuWorkloadPolicyProps } from '@guardian/cdk/lib/experimental/constructs/iam/policies'; +import { GuDeveloperPolicyExperimental } from '@guardian/cdk/lib/experimental/constructs/iam/policies'; import { Duration } from 'aws-cdk-lib'; import type { IVpc } from 'aws-cdk-lib/aws-ec2'; import { Secret } from 'aws-cdk-lib/aws-ecs'; import { Schedule } from 'aws-cdk-lib/aws-events'; -import type { PolicyStatement } from 'aws-cdk-lib/aws-iam'; +import { Effect, PolicyStatement } from 'aws-cdk-lib/aws-iam'; import type { DatabaseInstance } from 'aws-cdk-lib/aws-rds'; import { Secret as SecretsManager } from 'aws-cdk-lib/aws-secretsmanager'; import { StringParameter } from 'aws-cdk-lib/aws-ssm'; @@ -61,6 +64,14 @@ interface CloudqueryEcsClusterProps { enableCloudquerySchedules: boolean; } +function ssmArn(stack: GuStack, parameterName: string): string { + return stack.formatArn({ + service: 'ssm', + resource: 'parameter', + resourceName: parameterName, + }); +} + export function addCloudqueryEcsCluster( scope: GuStack, props: CloudqueryEcsClusterProps, @@ -656,7 +667,7 @@ export function addCloudqueryEcsCluster( config: endOfLifeSourceConfig(), }; - return new CloudqueryCluster(scope, `${app}Cluster`, { + const cluster = new CloudqueryCluster(scope, `${app}Cluster`, { enableCloudquerySchedules, app, vpc, @@ -678,4 +689,45 @@ export function addCloudqueryEcsCluster( ], cloudqueryApiKey, }); + + const SSMPolicy = new PolicyStatement({ + effect: Effect.ALLOW, + actions: ['ssm:GetParameter'], + resources: [ + ssmArn(scope, `/${stage}/${stack}/${app}/*`), + ssmArn( + scope, + `/${stage}/deploy/riff-raff/external-database-access-security-group`, + ), + ssmArn(scope, NAMED_SSM_PARAMETER_PATHS.PrimaryVpcPrivateSubnets.path), + ], + }); + + const cloudqueryClusterArnForTasks = cluster.arnForTasks('*'); + + const ecsPolicy = new PolicyStatement({ + effect: Effect.ALLOW, + actions: ['ecs:RunTask', 'ecs:List*', 'ecs:Describe*'], + resources: [cloudqueryClusterArnForTasks], + }); + + const dbSecretPolicy = new PolicyStatement({ + effect: Effect.ALLOW, + actions: ['secretsmanager:GetSecretValue', 'secretsmanager:ListSecrets'], + resources: [db.secret!.secretArn], // The secret definitely exists, as CloudQuery needs it to connect to the database. + }); + + const cliPolicyProps: GuWorkloadPolicyProps = { + permission: 'service-catalogue-cli', + description: 'Service Catalogue CLI', + statements: [ecsPolicy, SSMPolicy, dbSecretPolicy], + }; + + new GuDeveloperPolicyExperimental( + scope, + 'ServiceCatalogueCliPolicy', + cliPolicyProps, + ); + + return cluster; } diff --git a/packages/cdk/package.json b/packages/cdk/package.json index 4195409cf..aac2ea7cc 100644 --- a/packages/cdk/package.json +++ b/packages/cdk/package.json @@ -11,7 +11,7 @@ }, "devDependencies": { "@guardian/aws-account-setup": "github:guardian/aws-account-setup#shared@0.0.1", - "@guardian/cdk": "62.5.1", + "@guardian/cdk": "62.5.3", "@types/jest": "^30.0.0", "@types/js-yaml": "^4.0.9", "aws-cron-parser": "^1.1.12",