From b4ae6a9ccef3c33e3c99e6fc599498109e0cbd3d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Anton=20Gr=C3=BCbel?= Date: Sat, 7 Aug 2021 16:48:28 +0900 Subject: [PATCH] Add ecr repo policy (#4148) * Add ecr.set_repository_policy * Add ecr.get_repository_policy * Add ecr.delete_repository_policy --- moto/ecr/exceptions.py | 14 ++ moto/ecr/models.py | 55 +++++++ moto/ecr/responses.py | 36 +++-- moto/iam/policy_validation.py | 2 + tests/terraform-tests.failures.txt | 1 - tests/terraform-tests.success.txt | 1 + tests/test_ecr/test_ecr_boto3.py | 252 +++++++++++++++++++++++++++++ 7 files changed, 351 insertions(+), 10 deletions(-) diff --git a/moto/ecr/exceptions.py b/moto/ecr/exceptions.py index ceb8ea4fd..a8f147b43 100644 --- a/moto/ecr/exceptions.py +++ b/moto/ecr/exceptions.py @@ -42,6 +42,20 @@ class RepositoryNotFoundException(JsonRESTError): ) +class RepositoryPolicyNotFoundException(JsonRESTError): + code = 400 + + def __init__(self, repository_name, registry_id): + super().__init__( + error_type=__class__.__name__, + message=( + "Repository policy does not exist " + f"for the repository with name '{repository_name}' " + f"in the registry with id '{registry_id}'" + ), + ) + + class ImageNotFoundException(JsonRESTError): code = 400 diff --git a/moto/ecr/models.py b/moto/ecr/models.py index 850b571ff..2b18e380b 100644 --- a/moto/ecr/models.py +++ b/moto/ecr/models.py @@ -18,7 +18,10 @@ from moto.ecr.exceptions import ( RepositoryAlreadyExistsException, RepositoryNotEmptyException, InvalidParameterException, + RepositoryPolicyNotFoundException, ) +from moto.iam.exceptions import MalformedPolicyDocument +from moto.iam.policy_validation import IAMPolicyDocumentValidator from moto.utilities.tagging_service import TaggingService DEFAULT_REGISTRY_ID = ACCOUNT_ID @@ -77,6 +80,7 @@ class Repository(BaseObject, CloudFormationModel): self.encryption_configuration = self._determine_encryption_config( encryption_config ) + self.policy = None self.images = [] def _determine_encryption_config(self, encryption_config): @@ -658,6 +662,57 @@ class ECRBackend(BaseBackend): "imageScanningConfiguration": repo.image_scanning_configuration, } + def set_repository_policy(self, registry_id, repository_name, policy_text): + repo = self._get_repository(repository_name, registry_id) + + try: + iam_policy_document_validator = IAMPolicyDocumentValidator(policy_text) + # the repository policy can be defined without a resource field + iam_policy_document_validator._validate_resource_exist = lambda: None + # the repository policy can have the old version 2008-10-17 + iam_policy_document_validator._validate_version = lambda: None + iam_policy_document_validator.validate() + except MalformedPolicyDocument: + raise InvalidParameterException( + "Invalid parameter at 'PolicyText' failed to satisfy constraint: " + "'Invalid repository policy provided'" + ) + + repo.policy = policy_text + + return { + "registryId": repo.registry_id, + "repositoryName": repository_name, + "policyText": repo.policy, + } + + def get_repository_policy(self, registry_id, repository_name): + repo = self._get_repository(repository_name, registry_id) + + if not repo.policy: + raise RepositoryPolicyNotFoundException(repository_name, repo.registry_id) + + return { + "registryId": repo.registry_id, + "repositoryName": repository_name, + "policyText": repo.policy, + } + + def delete_repository_policy(self, registry_id, repository_name): + repo = self._get_repository(repository_name, registry_id) + policy = repo.policy + + if not policy: + raise RepositoryPolicyNotFoundException(repository_name, repo.registry_id) + + repo.policy = None + + return { + "registryId": repo.registry_id, + "repositoryName": repository_name, + "policyText": policy, + } + ecr_backends = {} for region, ec2_backend in ec2_backends.items(): diff --git a/moto/ecr/responses.py b/moto/ecr/responses.py index 98007dfb2..0db06e9e7 100644 --- a/moto/ecr/responses.py +++ b/moto/ecr/responses.py @@ -119,10 +119,14 @@ class ECRResponse(BaseResponse): ) def delete_repository_policy(self): - if self.is_not_dryrun("DeleteRepositoryPolicy"): - raise NotImplementedError( - "ECR.delete_repository_policy is not yet implemented" + registry_id = self._get_param("registryId") + repository_name = self._get_param("repositoryName") + + return json.dumps( + self.ecr_backend.delete_repository_policy( + registry_id=registry_id, repository_name=repository_name, ) + ) def generate_presigned_url(self): if self.is_not_dryrun("GeneratePresignedUrl"): @@ -160,10 +164,14 @@ class ECRResponse(BaseResponse): raise NotImplementedError("ECR.get_paginator is not yet implemented") def get_repository_policy(self): - if self.is_not_dryrun("GetRepositoryPolicy"): - raise NotImplementedError( - "ECR.get_repository_policy is not yet implemented" + registry_id = self._get_param("registryId") + repository_name = self._get_param("repositoryName") + + return json.dumps( + self.ecr_backend.get_repository_policy( + registry_id=registry_id, repository_name=repository_name, ) + ) def get_waiter(self): if self.is_not_dryrun("GetWaiter"): @@ -176,10 +184,20 @@ class ECRResponse(BaseResponse): ) def set_repository_policy(self): - if self.is_not_dryrun("SetRepositoryPolicy"): - raise NotImplementedError( - "ECR.set_repository_policy is not yet implemented" + registry_id = self._get_param("registryId") + repository_name = self._get_param("repositoryName") + policy_text = self._get_param("policyText") + # this is usually a safety flag to prevent accidental repository lock outs + # but this would need a much deeper validation of the provided policy + # force = self._get_param("force") + + return json.dumps( + self.ecr_backend.set_repository_policy( + registry_id=registry_id, + repository_name=repository_name, + policy_text=policy_text, ) + ) def upload_layer_part(self): if self.is_not_dryrun("UploadLayerPart"): diff --git a/moto/iam/policy_validation.py b/moto/iam/policy_validation.py index 73123ea08..659bff127 100644 --- a/moto/iam/policy_validation.py +++ b/moto/iam/policy_validation.py @@ -15,6 +15,8 @@ VALID_STATEMENT_ELEMENTS = [ "Resource", "NotResource", "Effect", + "Principal", + "NotPrincipal", "Condition", ] diff --git a/tests/terraform-tests.failures.txt b/tests/terraform-tests.failures.txt index 7fc66399f..e784b8ceb 100644 --- a/tests/terraform-tests.failures.txt +++ b/tests/terraform-tests.failures.txt @@ -2,6 +2,5 @@ TestAccAWSEc2TransitGatewayDxGatewayAttachmentDataSource TestAccAWSEc2TransitGatewayPeeringAttachmentAccepter TestAccAWSEc2TransitGatewayRouteTableAssociation TestAccAWSEc2TransitGatewayVpcAttachment -TestAccAWSEcrRepositoryPolicy TestAccAWSFms TestAccAWSIAMRolePolicy \ No newline at end of file diff --git a/tests/terraform-tests.success.txt b/tests/terraform-tests.success.txt index 0b5ae4f9f..e1aae8413 100644 --- a/tests/terraform-tests.success.txt +++ b/tests/terraform-tests.success.txt @@ -45,6 +45,7 @@ TestAccAWSEc2TransitGatewayPeeringAttachment TestAccAWSEc2TransitGatewayPeeringAttachmentDataSource TestAccAWSEcrRepository TestAccAWSEcrRepositoryDataSource +TestAccAWSEcrRepositoryPolicy TestAccAWSElasticBeanstalkSolutionStackDataSource TestAccAWSElbHostedZoneId TestAccAWSElbServiceAccount diff --git a/tests/test_ecr/test_ecr_boto3.py b/tests/test_ecr/test_ecr_boto3.py index 225bcf99b..46b92a512 100644 --- a/tests/test_ecr/test_ecr_boto3.py +++ b/tests/test_ecr/test_ecr_boto3.py @@ -1468,3 +1468,255 @@ def test_put_image_scanning_configuration_error_not_exists(): f"The repository with name '{repo_name}' does not exist " f"in the registry with id '{ACCOUNT_ID}'" ) + + +@mock_ecr +def test_set_repository_policy(): + # given + client = boto3.client("ecr", region_name="eu-central-1") + repo_name = "test-repo" + client.create_repository(repositoryName=repo_name) + policy = { + "Version": "2012-10-17", + "Statement": [ + { + "Sid": "root", + "Effect": "Allow", + "Principal": {"AWS": f"arn:aws:iam::{ACCOUNT_ID}:root"}, + "Action": ["ecr:DescribeImages"], + } + ], + } + + # when + response = client.set_repository_policy( + repositoryName=repo_name, policyText=json.dumps(policy), + ) + + # then + response["registryId"].should.equal(ACCOUNT_ID) + response["repositoryName"].should.equal(repo_name) + json.loads(response["policyText"]).should.equal(policy) + + +@mock_ecr +def test_set_repository_policy_error_not_exists(): + # given + region_name = "eu-central-1" + client = boto3.client("ecr", region_name=region_name) + repo_name = "not-exists" + policy = { + "Version": "2012-10-17", + "Statement": [ + { + "Sid": "root", + "Effect": "Allow", + "Principal": {"AWS": f"arn:aws:iam::{ACCOUNT_ID}:root"}, + "Action": ["ecr:DescribeImages"], + } + ], + } + + # when + with pytest.raises(ClientError) as e: + client.set_repository_policy( + repositoryName=repo_name, policyText=json.dumps(policy), + ) + + # then + ex = e.value + ex.operation_name.should.equal("SetRepositoryPolicy") + ex.response["ResponseMetadata"]["HTTPStatusCode"].should.equal(400) + ex.response["Error"]["Code"].should.contain("RepositoryNotFoundException") + ex.response["Error"]["Message"].should.equal( + f"The repository with name '{repo_name}' does not exist " + f"in the registry with id '{ACCOUNT_ID}'" + ) + + +@mock_ecr +def test_set_repository_policy_error_invalid_param(): + # given + region_name = "eu-central-1" + client = boto3.client("ecr", region_name=region_name) + repo_name = "test-repo" + client.create_repository(repositoryName=repo_name) + policy = { + "Version": "2012-10-17", + "Statement": [{"Effect": "Allow"}], + } + + # when + with pytest.raises(ClientError) as e: + client.set_repository_policy( + repositoryName=repo_name, policyText=json.dumps(policy), + ) + + # then + ex = e.value + ex.operation_name.should.equal("SetRepositoryPolicy") + ex.response["ResponseMetadata"]["HTTPStatusCode"].should.equal(400) + ex.response["Error"]["Code"].should.contain("InvalidParameterException") + ex.response["Error"]["Message"].should.equal( + "Invalid parameter at 'PolicyText' failed to satisfy constraint: " + "'Invalid repository policy provided'" + ) + + +@mock_ecr +def test_get_repository_policy(): + # given + client = boto3.client("ecr", region_name="eu-central-1") + repo_name = "test-repo" + client.create_repository(repositoryName=repo_name) + policy = { + "Version": "2012-10-17", + "Statement": [ + { + "Sid": "root", + "Effect": "Allow", + "Principal": {"AWS": f"arn:aws:iam::{ACCOUNT_ID}:root"}, + "Action": ["ecr:DescribeImages"], + } + ], + } + client.set_repository_policy( + repositoryName=repo_name, policyText=json.dumps(policy), + ) + + # when + response = client.get_repository_policy(repositoryName=repo_name) + + # then + response["registryId"].should.equal(ACCOUNT_ID) + response["repositoryName"].should.equal(repo_name) + json.loads(response["policyText"]).should.equal(policy) + + +@mock_ecr +def test_get_repository_policy_error_repo_not_exists(): + # given + region_name = "eu-central-1" + client = boto3.client("ecr", region_name=region_name) + repo_name = "not-exists" + + # when + with pytest.raises(ClientError) as e: + client.get_repository_policy(repositoryName=repo_name) + + # then + ex = e.value + ex.operation_name.should.equal("GetRepositoryPolicy") + ex.response["ResponseMetadata"]["HTTPStatusCode"].should.equal(400) + ex.response["Error"]["Code"].should.contain("RepositoryNotFoundException") + ex.response["Error"]["Message"].should.equal( + f"The repository with name '{repo_name}' does not exist " + f"in the registry with id '{ACCOUNT_ID}'" + ) + + +@mock_ecr +def test_get_repository_policy_error_policy_not_exists(): + # given + region_name = "eu-central-1" + client = boto3.client("ecr", region_name=region_name) + repo_name = "test-repo" + client.create_repository(repositoryName=repo_name) + + # when + with pytest.raises(ClientError) as e: + client.get_repository_policy(repositoryName=repo_name) + + # then + ex = e.value + ex.operation_name.should.equal("GetRepositoryPolicy") + ex.response["ResponseMetadata"]["HTTPStatusCode"].should.equal(400) + ex.response["Error"]["Code"].should.contain("RepositoryPolicyNotFoundException") + ex.response["Error"]["Message"].should.equal( + "Repository policy does not exist " + f"for the repository with name '{repo_name}' " + f"in the registry with id '{ACCOUNT_ID}'" + ) + + +@mock_ecr +def test_delete_repository_policy(): + # given + client = boto3.client("ecr", region_name="eu-central-1") + repo_name = "test-repo" + client.create_repository(repositoryName=repo_name) + policy = { + "Version": "2012-10-17", + "Statement": [ + { + "Sid": "root", + "Effect": "Allow", + "Principal": {"AWS": f"arn:aws:iam::{ACCOUNT_ID}:root"}, + "Action": ["ecr:DescribeImages"], + } + ], + } + client.set_repository_policy( + repositoryName=repo_name, policyText=json.dumps(policy), + ) + + # when + response = client.delete_repository_policy(repositoryName=repo_name) + + # then + response["registryId"].should.equal(ACCOUNT_ID) + response["repositoryName"].should.equal(repo_name) + json.loads(response["policyText"]).should.equal(policy) + + with pytest.raises(ClientError) as e: + client.get_repository_policy(repositoryName=repo_name) + + e.value.response["Error"]["Code"].should.contain( + "RepositoryPolicyNotFoundException" + ) + + +@mock_ecr +def test_delete_repository_policy_error_repo_not_exists(): + # given + region_name = "eu-central-1" + client = boto3.client("ecr", region_name=region_name) + repo_name = "not-exists" + + # when + with pytest.raises(ClientError) as e: + client.delete_repository_policy(repositoryName=repo_name) + + # then + ex = e.value + ex.operation_name.should.equal("DeleteRepositoryPolicy") + ex.response["ResponseMetadata"]["HTTPStatusCode"].should.equal(400) + ex.response["Error"]["Code"].should.contain("RepositoryNotFoundException") + ex.response["Error"]["Message"].should.equal( + f"The repository with name '{repo_name}' does not exist " + f"in the registry with id '{ACCOUNT_ID}'" + ) + + +@mock_ecr +def test_delete_repository_policy_error_policy_not_exists(): + # given + region_name = "eu-central-1" + client = boto3.client("ecr", region_name=region_name) + repo_name = "test-repo" + client.create_repository(repositoryName=repo_name) + + # when + with pytest.raises(ClientError) as e: + client.delete_repository_policy(repositoryName=repo_name) + + # then + ex = e.value + ex.operation_name.should.equal("DeleteRepositoryPolicy") + ex.response["ResponseMetadata"]["HTTPStatusCode"].should.equal(400) + ex.response["Error"]["Code"].should.contain("RepositoryPolicyNotFoundException") + ex.response["Error"]["Message"].should.equal( + "Repository policy does not exist " + f"for the repository with name '{repo_name}' " + f"in the registry with id '{ACCOUNT_ID}'" + )