From ce7bdea1d1b79de517b2b7ff6d4d73212f8a84c3 Mon Sep 17 00:00:00 2001 From: Kaizen Conroy <36202692+kaizencc@users.noreply.github.com> Date: Fri, 5 May 2023 11:10:19 -0400 Subject: [PATCH] feat(ecr): grantRead on repositories (#25445) Adds a grantRead method with `ecr:DescribeRepositories` and `ecr:DescribeImages` permissions. The use case is for something like cdk-assets, which tries to look up the repository / image name as part of the publishing step (so it doesn't publish a duplicate). ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license* --- .../aws-ecr-integ-stack.assets.json | 6 +- .../aws-ecr-integ-stack.template.json | 64 +++++++-- .../test/integ.basic.js.snapshot/cdk.out | 2 +- ...efaultTestDeployAssert4F7FBFB4.assets.json | 2 +- .../test/integ.basic.js.snapshot/integ.json | 2 +- .../integ.basic.js.snapshot/manifest.json | 16 ++- .../test/integ.basic.js.snapshot/tree.json | 121 ++++++++++++++++-- .../test/aws-ecr/test/integ.basic.ts | 4 + .../aws-cdk-lib/aws-ecr/lib/repository.ts | 18 ++- .../aws-ecr/test/repository.test.ts | 26 ++++ 10 files changed, 228 insertions(+), 33 deletions(-) diff --git a/packages/@aws-cdk-testing/framework-integ/test/aws-ecr/test/integ.basic.js.snapshot/aws-ecr-integ-stack.assets.json b/packages/@aws-cdk-testing/framework-integ/test/aws-ecr/test/integ.basic.js.snapshot/aws-ecr-integ-stack.assets.json index c6ae5f14f7ac3..de9244e843078 100644 --- a/packages/@aws-cdk-testing/framework-integ/test/aws-ecr/test/integ.basic.js.snapshot/aws-ecr-integ-stack.assets.json +++ b/packages/@aws-cdk-testing/framework-integ/test/aws-ecr/test/integ.basic.js.snapshot/aws-ecr-integ-stack.assets.json @@ -1,7 +1,7 @@ { - "version": "30.1.0", + "version": "31.0.0", "files": { - "26df443ecb3d9a917feccf0349d0f8852c227c138904499fe5e26de6a090654c": { + "a047e78171779d23d25e3fc35f2b3ce7ff7313e616a588b6f8773b9360f12b26": { "source": { "path": "aws-ecr-integ-stack.template.json", "packaging": "file" @@ -9,7 +9,7 @@ "destinations": { "current_account-current_region": { "bucketName": "cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}", - "objectKey": "26df443ecb3d9a917feccf0349d0f8852c227c138904499fe5e26de6a090654c.json", + "objectKey": "a047e78171779d23d25e3fc35f2b3ce7ff7313e616a588b6f8773b9360f12b26.json", "assumeRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-file-publishing-role-${AWS::AccountId}-${AWS::Region}" } } diff --git a/packages/@aws-cdk-testing/framework-integ/test/aws-ecr/test/integ.basic.js.snapshot/aws-ecr-integ-stack.template.json b/packages/@aws-cdk-testing/framework-integ/test/aws-ecr/test/integ.basic.js.snapshot/aws-ecr-integ-stack.template.json index 32a2d469c5f8d..fea2eae1cf611 100644 --- a/packages/@aws-cdk-testing/framework-integ/test/aws-ecr/test/integ.basic.js.snapshot/aws-ecr-integ-stack.template.json +++ b/packages/@aws-cdk-testing/framework-integ/test/aws-ecr/test/integ.basic.js.snapshot/aws-ecr-integ-stack.template.json @@ -7,20 +7,64 @@ "LifecyclePolicyText": "{\"rules\":[{\"rulePriority\":1,\"selection\":{\"tagStatus\":\"any\",\"countType\":\"imageCountMoreThan\",\"countNumber\":5},\"action\":{\"type\":\"expire\"}}]}" }, "RepositoryPolicyText": { - "Statement": [ - { - "Action": "ecr:GetDownloadUrlForLayer", - "Effect": "Allow", - "Principal": { - "AWS": "*" - } + "Statement": [ + { + "Action": "ecr:GetDownloadUrlForLayer", + "Effect": "Allow", + "Principal": { + "AWS": "*" } - ], - "Version": "2012-10-17" - } + } + ], + "Version": "2012-10-17" + } }, "UpdateReplacePolicy": "Retain", "DeletionPolicy": "Retain" + }, + "MyUserDC45028B": { + "Type": "AWS::IAM::User" + }, + "MyUserDefaultPolicy7B897426": { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": [ + "ecr:BatchCheckLayerAvailability", + "ecr:BatchGetImage", + "ecr:CompleteLayerUpload", + "ecr:DescribeImages", + "ecr:DescribeRepositories", + "ecr:GetDownloadUrlForLayer", + "ecr:InitiateLayerUpload", + "ecr:PutImage", + "ecr:UploadLayerPart" + ], + "Effect": "Allow", + "Resource": { + "Fn::GetAtt": [ + "Repo02AC86CF", + "Arn" + ] + } + }, + { + "Action": "ecr:GetAuthorizationToken", + "Effect": "Allow", + "Resource": "*" + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "MyUserDefaultPolicy7B897426", + "Users": [ + { + "Ref": "MyUserDC45028B" + } + ] + } } }, "Outputs": { diff --git a/packages/@aws-cdk-testing/framework-integ/test/aws-ecr/test/integ.basic.js.snapshot/cdk.out b/packages/@aws-cdk-testing/framework-integ/test/aws-ecr/test/integ.basic.js.snapshot/cdk.out index b72fef144f05c..7925065efbcc4 100644 --- a/packages/@aws-cdk-testing/framework-integ/test/aws-ecr/test/integ.basic.js.snapshot/cdk.out +++ b/packages/@aws-cdk-testing/framework-integ/test/aws-ecr/test/integ.basic.js.snapshot/cdk.out @@ -1 +1 @@ -{"version":"30.1.0"} \ No newline at end of file +{"version":"31.0.0"} \ No newline at end of file diff --git a/packages/@aws-cdk-testing/framework-integ/test/aws-ecr/test/integ.basic.js.snapshot/cdkecrintegtestbasicDefaultTestDeployAssert4F7FBFB4.assets.json b/packages/@aws-cdk-testing/framework-integ/test/aws-ecr/test/integ.basic.js.snapshot/cdkecrintegtestbasicDefaultTestDeployAssert4F7FBFB4.assets.json index 8fd843ce656f5..fbfe3f9089f79 100644 --- a/packages/@aws-cdk-testing/framework-integ/test/aws-ecr/test/integ.basic.js.snapshot/cdkecrintegtestbasicDefaultTestDeployAssert4F7FBFB4.assets.json +++ b/packages/@aws-cdk-testing/framework-integ/test/aws-ecr/test/integ.basic.js.snapshot/cdkecrintegtestbasicDefaultTestDeployAssert4F7FBFB4.assets.json @@ -1,5 +1,5 @@ { - "version": "30.1.0", + "version": "31.0.0", "files": { "21fbb51d7b23f6a6c262b46a9caee79d744a3ac019fd45422d988b96d44b2a22": { "source": { diff --git a/packages/@aws-cdk-testing/framework-integ/test/aws-ecr/test/integ.basic.js.snapshot/integ.json b/packages/@aws-cdk-testing/framework-integ/test/aws-ecr/test/integ.basic.js.snapshot/integ.json index 0e898c63ccb23..8ded05fd44287 100644 --- a/packages/@aws-cdk-testing/framework-integ/test/aws-ecr/test/integ.basic.js.snapshot/integ.json +++ b/packages/@aws-cdk-testing/framework-integ/test/aws-ecr/test/integ.basic.js.snapshot/integ.json @@ -1,5 +1,5 @@ { - "version": "30.1.0", + "version": "31.0.0", "testCases": { "cdk-ecr-integ-test-basic/DefaultTest": { "stacks": [ diff --git a/packages/@aws-cdk-testing/framework-integ/test/aws-ecr/test/integ.basic.js.snapshot/manifest.json b/packages/@aws-cdk-testing/framework-integ/test/aws-ecr/test/integ.basic.js.snapshot/manifest.json index fc9bbb2d7138c..299140c835cd5 100644 --- a/packages/@aws-cdk-testing/framework-integ/test/aws-ecr/test/integ.basic.js.snapshot/manifest.json +++ b/packages/@aws-cdk-testing/framework-integ/test/aws-ecr/test/integ.basic.js.snapshot/manifest.json @@ -1,5 +1,5 @@ { - "version": "30.1.0", + "version": "31.0.0", "artifacts": { "aws-ecr-integ-stack.assets": { "type": "cdk:asset-manifest", @@ -17,7 +17,7 @@ "validateOnSynth": false, "assumeRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-deploy-role-${AWS::AccountId}-${AWS::Region}", "cloudFormationExecutionRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-cfn-exec-role-${AWS::AccountId}-${AWS::Region}", - "stackTemplateAssetObjectUrl": "s3://cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}/26df443ecb3d9a917feccf0349d0f8852c227c138904499fe5e26de6a090654c.json", + "stackTemplateAssetObjectUrl": "s3://cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}/a047e78171779d23d25e3fc35f2b3ce7ff7313e616a588b6f8773b9360f12b26.json", "requiresBootstrapStackVersion": 6, "bootstrapStackVersionSsmParameter": "/cdk-bootstrap/hnb659fds/version", "additionalDependencies": [ @@ -39,6 +39,18 @@ "data": "Repo02AC86CF" } ], + "/aws-ecr-integ-stack/MyUser/Resource": [ + { + "type": "aws:cdk:logicalId", + "data": "MyUserDC45028B" + } + ], + "/aws-ecr-integ-stack/MyUser/DefaultPolicy/Resource": [ + { + "type": "aws:cdk:logicalId", + "data": "MyUserDefaultPolicy7B897426" + } + ], "/aws-ecr-integ-stack/RepositoryURI": [ { "type": "aws:cdk:logicalId", diff --git a/packages/@aws-cdk-testing/framework-integ/test/aws-ecr/test/integ.basic.js.snapshot/tree.json b/packages/@aws-cdk-testing/framework-integ/test/aws-ecr/test/integ.basic.js.snapshot/tree.json index 64255404444da..5278e08ffe6e2 100644 --- a/packages/@aws-cdk-testing/framework-integ/test/aws-ecr/test/integ.basic.js.snapshot/tree.json +++ b/packages/@aws-cdk-testing/framework-integ/test/aws-ecr/test/integ.basic.js.snapshot/tree.json @@ -20,17 +20,110 @@ "aws:cdk:cloudformation:props": { "lifecyclePolicy": { "lifecyclePolicyText": "{\"rules\":[{\"rulePriority\":1,\"selection\":{\"tagStatus\":\"any\",\"countType\":\"imageCountMoreThan\",\"countNumber\":5},\"action\":{\"type\":\"expire\"}}]}" + }, + "repositoryPolicyText": { + "Statement": [ + { + "Action": "ecr:GetDownloadUrlForLayer", + "Effect": "Allow", + "Principal": { + "AWS": "*" + } + } + ], + "Version": "2012-10-17" } } }, "constructInfo": { - "fqn": "@aws-cdk/aws-ecr.CfnRepository", + "fqn": "aws-cdk-lib.aws_ecr.CfnRepository", "version": "0.0.0" } } }, "constructInfo": { - "fqn": "@aws-cdk/aws-ecr.Repository", + "fqn": "aws-cdk-lib.aws_ecr.Repository", + "version": "0.0.0" + } + }, + "MyUser": { + "id": "MyUser", + "path": "aws-ecr-integ-stack/MyUser", + "children": { + "Resource": { + "id": "Resource", + "path": "aws-ecr-integ-stack/MyUser/Resource", + "attributes": { + "aws:cdk:cloudformation:type": "AWS::IAM::User", + "aws:cdk:cloudformation:props": {} + }, + "constructInfo": { + "fqn": "aws-cdk-lib.aws_iam.CfnUser", + "version": "0.0.0" + } + }, + "DefaultPolicy": { + "id": "DefaultPolicy", + "path": "aws-ecr-integ-stack/MyUser/DefaultPolicy", + "children": { + "Resource": { + "id": "Resource", + "path": "aws-ecr-integ-stack/MyUser/DefaultPolicy/Resource", + "attributes": { + "aws:cdk:cloudformation:type": "AWS::IAM::Policy", + "aws:cdk:cloudformation:props": { + "policyDocument": { + "Statement": [ + { + "Action": [ + "ecr:BatchCheckLayerAvailability", + "ecr:BatchGetImage", + "ecr:CompleteLayerUpload", + "ecr:DescribeImages", + "ecr:DescribeRepositories", + "ecr:GetDownloadUrlForLayer", + "ecr:InitiateLayerUpload", + "ecr:PutImage", + "ecr:UploadLayerPart" + ], + "Effect": "Allow", + "Resource": { + "Fn::GetAtt": [ + "Repo02AC86CF", + "Arn" + ] + } + }, + { + "Action": "ecr:GetAuthorizationToken", + "Effect": "Allow", + "Resource": "*" + } + ], + "Version": "2012-10-17" + }, + "policyName": "MyUserDefaultPolicy7B897426", + "users": [ + { + "Ref": "MyUserDC45028B" + } + ] + } + }, + "constructInfo": { + "fqn": "aws-cdk-lib.aws_iam.CfnPolicy", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "aws-cdk-lib.aws_iam.Policy", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "aws-cdk-lib.aws_iam.User", "version": "0.0.0" } }, @@ -38,7 +131,7 @@ "id": "RepositoryURI", "path": "aws-ecr-integ-stack/RepositoryURI", "constructInfo": { - "fqn": "@aws-cdk/core.CfnOutput", + "fqn": "aws-cdk-lib.CfnOutput", "version": "0.0.0" } }, @@ -46,7 +139,7 @@ "id": "BootstrapVersion", "path": "aws-ecr-integ-stack/BootstrapVersion", "constructInfo": { - "fqn": "@aws-cdk/core.CfnParameter", + "fqn": "aws-cdk-lib.CfnParameter", "version": "0.0.0" } }, @@ -54,13 +147,13 @@ "id": "CheckBootstrapVersion", "path": "aws-ecr-integ-stack/CheckBootstrapVersion", "constructInfo": { - "fqn": "@aws-cdk/core.CfnRule", + "fqn": "aws-cdk-lib.CfnRule", "version": "0.0.0" } } }, "constructInfo": { - "fqn": "@aws-cdk/core.Stack", + "fqn": "aws-cdk-lib.Stack", "version": "0.0.0" } }, @@ -77,7 +170,7 @@ "path": "cdk-ecr-integ-test-basic/DefaultTest/Default", "constructInfo": { "fqn": "constructs.Construct", - "version": "10.1.252" + "version": "10.1.270" } }, "DeployAssert": { @@ -88,7 +181,7 @@ "id": "BootstrapVersion", "path": "cdk-ecr-integ-test-basic/DefaultTest/DeployAssert/BootstrapVersion", "constructInfo": { - "fqn": "@aws-cdk/core.CfnParameter", + "fqn": "aws-cdk-lib.CfnParameter", "version": "0.0.0" } }, @@ -96,25 +189,25 @@ "id": "CheckBootstrapVersion", "path": "cdk-ecr-integ-test-basic/DefaultTest/DeployAssert/CheckBootstrapVersion", "constructInfo": { - "fqn": "@aws-cdk/core.CfnRule", + "fqn": "aws-cdk-lib.CfnRule", "version": "0.0.0" } } }, "constructInfo": { - "fqn": "@aws-cdk/core.Stack", + "fqn": "aws-cdk-lib.Stack", "version": "0.0.0" } } }, "constructInfo": { - "fqn": "@aws-cdk/integ-tests.IntegTestCase", + "fqn": "@aws-cdk/integ-tests-alpha.IntegTestCase", "version": "0.0.0" } } }, "constructInfo": { - "fqn": "@aws-cdk/integ-tests.IntegTest", + "fqn": "@aws-cdk/integ-tests-alpha.IntegTest", "version": "0.0.0" } }, @@ -123,12 +216,12 @@ "path": "Tree", "constructInfo": { "fqn": "constructs.Construct", - "version": "10.1.252" + "version": "10.1.270" } } }, "constructInfo": { - "fqn": "@aws-cdk/core.App", + "fqn": "aws-cdk-lib.App", "version": "0.0.0" } } diff --git a/packages/@aws-cdk-testing/framework-integ/test/aws-ecr/test/integ.basic.ts b/packages/@aws-cdk-testing/framework-integ/test/aws-ecr/test/integ.basic.ts index 74c0913bfd0f8..3830660077478 100644 --- a/packages/@aws-cdk-testing/framework-integ/test/aws-ecr/test/integ.basic.ts +++ b/packages/@aws-cdk-testing/framework-integ/test/aws-ecr/test/integ.basic.ts @@ -13,6 +13,10 @@ repo.addToResourcePolicy(new iam.PolicyStatement({ principals: [new iam.AnyPrincipal()], })); +const user = new iam.User(stack, 'MyUser'); +repo.grantRead(user); +repo.grantPullPush(user); + new cdk.CfnOutput(stack, 'RepositoryURI', { value: repo.repositoryUri, }); diff --git a/packages/aws-cdk-lib/aws-ecr/lib/repository.ts b/packages/aws-cdk-lib/aws-ecr/lib/repository.ts index 25c5806ad4beb..2be5e491f2ac6 100644 --- a/packages/aws-cdk-lib/aws-ecr/lib/repository.ts +++ b/packages/aws-cdk-lib/aws-ecr/lib/repository.ts @@ -88,6 +88,11 @@ export interface IRepository extends IResource { */ grant(grantee: iam.IGrantable, ...actions: string[]): iam.Grant; + /** + * Gran tthe given identity permissions to read images in this repository. + */ + grantRead(grantee: iam.IGrantable): iam.Grant; + /** * Grant the given identity permissions to pull images in this repository. */ @@ -342,6 +347,16 @@ export abstract class RepositoryBase extends Resource implements IRepository { } } + /** + * Grant the given identity permissions to read the images in this repository + */ + public grantRead(grantee: iam.IGrantable): iam.Grant { + return this.grant(grantee, + 'ecr:DescribeRepositories', + 'ecr:DescribeImages', + ); + } + /** * Grant the given identity permissions to use the images in this repository */ @@ -367,7 +382,8 @@ export abstract class RepositoryBase extends Resource implements IRepository { 'ecr:PutImage', 'ecr:InitiateLayerUpload', 'ecr:UploadLayerPart', - 'ecr:CompleteLayerUpload'); + 'ecr:CompleteLayerUpload', + ); } /** diff --git a/packages/aws-cdk-lib/aws-ecr/test/repository.test.ts b/packages/aws-cdk-lib/aws-ecr/test/repository.test.ts index 68d0907301bd9..b32ebaaf905e1 100644 --- a/packages/aws-cdk-lib/aws-ecr/test/repository.test.ts +++ b/packages/aws-cdk-lib/aws-ecr/test/repository.test.ts @@ -694,6 +694,32 @@ describe('repository', () => { }, }); }); + + test('grant read adds appropriate permissions', () => { + // GIVEN + const stack = new cdk.Stack(); + const repo = new ecr.Repository(stack, 'TestRepo'); + + // WHEN + repo.grantRead(new iam.AnyPrincipal()); + + // THEN + Template.fromStack(stack).hasResourceProperties('AWS::ECR::Repository', { + 'RepositoryPolicyText': { + 'Statement': [ + { + 'Action': [ + 'ecr:DescribeRepositories', + 'ecr:DescribeImages', + ], + 'Effect': 'Allow', + 'Principal': { 'AWS': '*' }, + }, + ], + 'Version': '2012-10-17', + }, + }); + }); }); describe('repository name validation', () => {