RAM - implement CRUD endpoints (#3158)
* Add ram.create_resource_share * Add ram.get_resource_shares * Add ram.update_resource_share * Add ram.delete_resource_share * Add ram.enable_sharing_with_aws_organization * Fix server tests * Add CR suggestions
This commit is contained in:
		
							parent
							
								
									1e5b8acac6
								
							
						
					
					
						commit
						a507314d45
					
				| @ -81,6 +81,7 @@ mock_opsworks = lazy_load(".opsworks", "mock_opsworks") | ||||
| mock_opsworks_deprecated = lazy_load(".opsworks", "mock_opsworks_deprecated") | ||||
| mock_organizations = lazy_load(".organizations", "mock_organizations") | ||||
| mock_polly = lazy_load(".polly", "mock_polly") | ||||
| mock_ram = lazy_load(".ram", "mock_ram") | ||||
| mock_rds = lazy_load(".rds", "mock_rds") | ||||
| mock_rds_deprecated = lazy_load(".rds", "mock_rds_deprecated") | ||||
| mock_rds2 = lazy_load(".rds2", "mock_rds2") | ||||
|  | ||||
| @ -48,6 +48,7 @@ BACKENDS = { | ||||
|     "opsworks": ("opsworks", "opsworks_backends"), | ||||
|     "organizations": ("organizations", "organizations_backends"), | ||||
|     "polly": ("polly", "polly_backends"), | ||||
|     "ram": ("ram", "ram_backends"), | ||||
|     "rds": ("rds2", "rds2_backends"), | ||||
|     "redshift": ("redshift", "redshift_backends"), | ||||
|     "resource-groups": ("resourcegroups", "resourcegroups_backends"), | ||||
|  | ||||
							
								
								
									
										5
									
								
								moto/ram/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										5
									
								
								moto/ram/__init__.py
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,5 @@ | ||||
| from .models import ram_backends | ||||
| from ..core.models import base_decorator | ||||
| 
 | ||||
| ram_backend = ram_backends["us-east-1"] | ||||
| mock_ram = base_decorator(ram_backends) | ||||
							
								
								
									
										39
									
								
								moto/ram/exceptions.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										39
									
								
								moto/ram/exceptions.py
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,39 @@ | ||||
| from __future__ import unicode_literals | ||||
| from moto.core.exceptions import JsonRESTError | ||||
| 
 | ||||
| 
 | ||||
| class InvalidParameterException(JsonRESTError): | ||||
|     code = 400 | ||||
| 
 | ||||
|     def __init__(self, message): | ||||
|         super(InvalidParameterException, self).__init__( | ||||
|             "InvalidParameterException", message | ||||
|         ) | ||||
| 
 | ||||
| 
 | ||||
| class MalformedArnException(JsonRESTError): | ||||
|     code = 400 | ||||
| 
 | ||||
|     def __init__(self, message): | ||||
|         super(MalformedArnException, self).__init__("MalformedArnException", message) | ||||
| 
 | ||||
| 
 | ||||
| class OperationNotPermittedException(JsonRESTError): | ||||
|     code = 400 | ||||
| 
 | ||||
|     def __init__(self): | ||||
|         super(OperationNotPermittedException, self).__init__( | ||||
|             "OperationNotPermittedException", | ||||
|             "Unable to enable sharing with AWS Organizations. " | ||||
|             "Received AccessDeniedException from AWSOrganizations with the following error message: " | ||||
|             "You don't have permissions to access this resource.", | ||||
|         ) | ||||
| 
 | ||||
| 
 | ||||
| class UnknownResourceException(JsonRESTError): | ||||
|     code = 400 | ||||
| 
 | ||||
|     def __init__(self, message): | ||||
|         super(UnknownResourceException, self).__init__( | ||||
|             "UnknownResourceException", message | ||||
|         ) | ||||
							
								
								
									
										247
									
								
								moto/ram/models.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										247
									
								
								moto/ram/models.py
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,247 @@ | ||||
| import re | ||||
| import string | ||||
| from datetime import datetime | ||||
| import random | ||||
| from uuid import uuid4 | ||||
| 
 | ||||
| from boto3 import Session | ||||
| from moto.core import BaseBackend, BaseModel, ACCOUNT_ID | ||||
| from moto.core.utils import unix_time | ||||
| from moto.organizations import organizations_backends | ||||
| from moto.ram.exceptions import ( | ||||
|     MalformedArnException, | ||||
|     InvalidParameterException, | ||||
|     UnknownResourceException, | ||||
|     OperationNotPermittedException, | ||||
| ) | ||||
| 
 | ||||
| 
 | ||||
| def random_resource_id(size): | ||||
|     return "".join(random.choice(string.digits + "abcdef") for _ in range(size)) | ||||
| 
 | ||||
| 
 | ||||
| class ResourceShare(BaseModel): | ||||
|     # List of shareable resources can be found here | ||||
|     # https://docs.aws.amazon.com/ram/latest/userguide/shareable.html | ||||
|     SHAREABLE_RESOURCES = [ | ||||
|         "cluster",  # Amazon Aurora cluster | ||||
|         "component",  # Amazon EC2 Image Builder component | ||||
|         "group",  # AWS Resource Groups | ||||
|         "image",  # Amazon EC2 Image Builder image | ||||
|         "image-recipe",  # Amazon EC2 Image Builder image recipe | ||||
|         "license-configuration",  # AWS License Manager configuration | ||||
|         "mesh",  # AWS App Mesh | ||||
|         "prefix-list",  # Amazon EC2 prefix list | ||||
|         "project",  # AWS CodeBuild project | ||||
|         "report-group",  # AWS CodeBuild report group | ||||
|         "resolver-rule",  # Amazon Route 53 forwarding rule | ||||
|         "subnet",  # Amazon EC2 subnet | ||||
|         "transit-gateway",  # Amazon EC2 transit gateway | ||||
|     ] | ||||
| 
 | ||||
|     def __init__(self, region, **kwargs): | ||||
|         self.region = region | ||||
| 
 | ||||
|         self.allow_external_principals = kwargs.get("allowExternalPrincipals", True) | ||||
|         self.arn = "arn:aws:ram:{0}:{1}:resource-share/{2}".format( | ||||
|             self.region, ACCOUNT_ID, uuid4() | ||||
|         ) | ||||
|         self.creation_time = datetime.utcnow() | ||||
|         self.feature_set = "STANDARD" | ||||
|         self.last_updated_time = datetime.utcnow() | ||||
|         self.name = kwargs["name"] | ||||
|         self.owning_account_id = ACCOUNT_ID | ||||
|         self.principals = [] | ||||
|         self.resource_arns = [] | ||||
|         self.status = "ACTIVE" | ||||
| 
 | ||||
|     @property | ||||
|     def organizations_backend(self): | ||||
|         return organizations_backends["global"] | ||||
| 
 | ||||
|     def add_principals(self, principals): | ||||
|         for principal in principals: | ||||
|             match = re.search( | ||||
|                 r"^arn:aws:organizations::\d{12}:organization/(o-\w+)$", principal | ||||
|             ) | ||||
|             if match: | ||||
|                 organization = self.organizations_backend.describe_organization() | ||||
|                 if principal == organization["Organization"]["Arn"]: | ||||
|                     continue | ||||
|                 else: | ||||
|                     raise UnknownResourceException( | ||||
|                         "Organization {} could not be found.".format(match.group(1)) | ||||
|                     ) | ||||
| 
 | ||||
|             match = re.search( | ||||
|                 r"^arn:aws:organizations::\d{12}:ou/(o-\w+)/(ou-[\w-]+)$", principal | ||||
|             ) | ||||
|             if match: | ||||
|                 roots = self.organizations_backend.list_roots() | ||||
|                 root_id = next( | ||||
|                     ( | ||||
|                         root["Id"] | ||||
|                         for root in roots["Roots"] | ||||
|                         if root["Name"] == "Root" and match.group(1) in root["Arn"] | ||||
|                     ), | ||||
|                     None, | ||||
|                 ) | ||||
| 
 | ||||
|                 if root_id: | ||||
|                     ous = self.organizations_backend.list_organizational_units_for_parent( | ||||
|                         ParentId=root_id | ||||
|                     ) | ||||
|                     if any(principal == ou["Arn"] for ou in ous["OrganizationalUnits"]): | ||||
|                         continue | ||||
| 
 | ||||
|                 raise UnknownResourceException( | ||||
|                     "OrganizationalUnit {} in unknown organization could not be found.".format( | ||||
|                         match.group(2) | ||||
|                     ) | ||||
|                 ) | ||||
| 
 | ||||
|             if not re.match(r"^\d{12}$", principal): | ||||
|                 raise InvalidParameterException( | ||||
|                     "Principal ID {} is malformed. " | ||||
|                     "Verify the ID and try again.".format(principal) | ||||
|                 ) | ||||
| 
 | ||||
|         for principal in principals: | ||||
|             self.principals.append(principal) | ||||
| 
 | ||||
|     def add_resources(self, resource_arns): | ||||
|         for resource in resource_arns: | ||||
|             match = re.search( | ||||
|                 r"^arn:aws:[a-z0-9-]+:[a-z0-9-]*:[0-9]{12}:([a-z-]+)[/:].*$", resource | ||||
|             ) | ||||
|             if not match: | ||||
|                 raise MalformedArnException( | ||||
|                     "The specified resource ARN {} is not valid. " | ||||
|                     "Verify the ARN and try again.".format(resource) | ||||
|                 ) | ||||
| 
 | ||||
|             if match.group(1) not in self.SHAREABLE_RESOURCES: | ||||
|                 raise MalformedArnException( | ||||
|                     "You cannot share the selected resource type." | ||||
|                 ) | ||||
| 
 | ||||
|         for resource in resource_arns: | ||||
|             self.resource_arns.append(resource) | ||||
| 
 | ||||
|     def delete(self): | ||||
|         self.last_updated_time = datetime.utcnow() | ||||
|         self.status = "DELETED" | ||||
| 
 | ||||
|     def describe(self): | ||||
|         return { | ||||
|             "allowExternalPrincipals": self.allow_external_principals, | ||||
|             "creationTime": unix_time(self.creation_time), | ||||
|             "featureSet": self.feature_set, | ||||
|             "lastUpdatedTime": unix_time(self.last_updated_time), | ||||
|             "name": self.name, | ||||
|             "owningAccountId": self.owning_account_id, | ||||
|             "resourceShareArn": self.arn, | ||||
|             "status": self.status, | ||||
|         } | ||||
| 
 | ||||
|     def update(self, **kwargs): | ||||
|         self.allow_external_principals = kwargs.get( | ||||
|             "allowExternalPrincipals", self.allow_external_principals | ||||
|         ) | ||||
|         self.last_updated_time = datetime.utcnow() | ||||
|         self.name = kwargs.get("name", self.name) | ||||
| 
 | ||||
| 
 | ||||
| class ResourceAccessManagerBackend(BaseBackend): | ||||
|     def __init__(self, region_name=None): | ||||
|         super(ResourceAccessManagerBackend, self).__init__() | ||||
|         self.region_name = region_name | ||||
|         self.resource_shares = [] | ||||
| 
 | ||||
|     @property | ||||
|     def organizations_backend(self): | ||||
|         return organizations_backends["global"] | ||||
| 
 | ||||
|     def reset(self): | ||||
|         region_name = self.region_name | ||||
|         self.__dict__ = {} | ||||
|         self.__init__(region_name) | ||||
| 
 | ||||
|     def create_resource_share(self, **kwargs): | ||||
|         resource = ResourceShare(self.region_name, **kwargs) | ||||
|         resource.add_principals(kwargs.get("principals", [])) | ||||
|         resource.add_resources(kwargs.get("resourceArns", [])) | ||||
| 
 | ||||
|         self.resource_shares.append(resource) | ||||
| 
 | ||||
|         response = resource.describe() | ||||
|         response.pop("featureSet") | ||||
| 
 | ||||
|         return dict(resourceShare=response) | ||||
| 
 | ||||
|     def get_resource_shares(self, **kwargs): | ||||
|         owner = kwargs["resourceOwner"] | ||||
| 
 | ||||
|         if owner not in ["SELF", "OTHER-ACCOUNTS"]: | ||||
|             raise InvalidParameterException( | ||||
|                 "{} is not a valid resource owner. " | ||||
|                 "Specify either SELF or OTHER-ACCOUNTS and try again.".format(owner) | ||||
|             ) | ||||
| 
 | ||||
|         if owner == "OTHER-ACCOUNTS": | ||||
|             raise NotImplementedError( | ||||
|                 "Value 'OTHER-ACCOUNTS' for parameter 'resourceOwner' not implemented." | ||||
|             ) | ||||
| 
 | ||||
|         resouces = [resource.describe() for resource in self.resource_shares] | ||||
| 
 | ||||
|         return dict(resourceShares=resouces) | ||||
| 
 | ||||
|     def update_resource_share(self, **kwargs): | ||||
|         arn = kwargs["resourceShareArn"] | ||||
| 
 | ||||
|         resource = next( | ||||
|             (resource for resource in self.resource_shares if arn == resource.arn), | ||||
|             None, | ||||
|         ) | ||||
| 
 | ||||
|         if not resource: | ||||
|             raise UnknownResourceException( | ||||
|                 "ResourceShare {} could not be found.".format(arn) | ||||
|             ) | ||||
| 
 | ||||
|         resource.update(**kwargs) | ||||
|         response = resource.describe() | ||||
|         response.pop("featureSet") | ||||
| 
 | ||||
|         return dict(resourceShare=response) | ||||
| 
 | ||||
|     def delete_resource_share(self, arn): | ||||
|         resource = next( | ||||
|             (resource for resource in self.resource_shares if arn == resource.arn), | ||||
|             None, | ||||
|         ) | ||||
| 
 | ||||
|         if not resource: | ||||
|             raise UnknownResourceException( | ||||
|                 "ResourceShare {} could not be found.".format(arn) | ||||
|             ) | ||||
| 
 | ||||
|         resource.delete() | ||||
| 
 | ||||
|         return dict(returnValue=True) | ||||
| 
 | ||||
|     def enable_sharing_with_aws_organization(self): | ||||
|         if not self.organizations_backend.org: | ||||
|             raise OperationNotPermittedException | ||||
| 
 | ||||
|         return dict(returnValue=True) | ||||
| 
 | ||||
| 
 | ||||
| ram_backends = {} | ||||
| for region in Session().get_available_regions("ram"): | ||||
|     ram_backends[region] = ResourceAccessManagerBackend(region) | ||||
| for region in Session().get_available_regions("ram", partition_name="aws-us-gov"): | ||||
|     ram_backends[region] = ResourceAccessManagerBackend(region) | ||||
| for region in Session().get_available_regions("ram", partition_name="aws-cn"): | ||||
|     ram_backends[region] = ResourceAccessManagerBackend(region) | ||||
							
								
								
									
										39
									
								
								moto/ram/responses.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										39
									
								
								moto/ram/responses.py
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,39 @@ | ||||
| from __future__ import unicode_literals | ||||
| from moto.core.responses import BaseResponse | ||||
| from .models import ram_backends | ||||
| import json | ||||
| 
 | ||||
| 
 | ||||
| class ResourceAccessManagerResponse(BaseResponse): | ||||
|     SERVICE_NAME = "ram" | ||||
| 
 | ||||
|     @property | ||||
|     def ram_backend(self): | ||||
|         return ram_backends[self.region] | ||||
| 
 | ||||
|     @property | ||||
|     def request_params(self): | ||||
|         try: | ||||
|             if self.method == "DELETE": | ||||
|                 return None | ||||
| 
 | ||||
|             return json.loads(self.body) | ||||
|         except ValueError: | ||||
|             return {} | ||||
| 
 | ||||
|     def create_resource_share(self): | ||||
|         return json.dumps(self.ram_backend.create_resource_share(**self.request_params)) | ||||
| 
 | ||||
|     def get_resource_shares(self): | ||||
|         return json.dumps(self.ram_backend.get_resource_shares(**self.request_params)) | ||||
| 
 | ||||
|     def update_resource_share(self): | ||||
|         return json.dumps(self.ram_backend.update_resource_share(**self.request_params)) | ||||
| 
 | ||||
|     def delete_resource_share(self): | ||||
|         return json.dumps( | ||||
|             self.ram_backend.delete_resource_share(self._get_param("resourceShareArn")) | ||||
|         ) | ||||
| 
 | ||||
|     def enable_sharing_with_aws_organization(self): | ||||
|         return json.dumps(self.ram_backend.enable_sharing_with_aws_organization()) | ||||
							
								
								
									
										12
									
								
								moto/ram/urls.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										12
									
								
								moto/ram/urls.py
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,12 @@ | ||||
| from __future__ import unicode_literals | ||||
| from .responses import ResourceAccessManagerResponse | ||||
| 
 | ||||
| url_bases = ["https?://ram.(.+).amazonaws.com"] | ||||
| 
 | ||||
| url_paths = { | ||||
|     "{0}/createresourceshare$": ResourceAccessManagerResponse.dispatch, | ||||
|     "{0}/deleteresourceshare/?$": ResourceAccessManagerResponse.dispatch, | ||||
|     "{0}/enablesharingwithawsorganization$": ResourceAccessManagerResponse.dispatch, | ||||
|     "{0}/getresourceshares$": ResourceAccessManagerResponse.dispatch, | ||||
|     "{0}/updateresourceshare$": ResourceAccessManagerResponse.dispatch, | ||||
| } | ||||
							
								
								
									
										381
									
								
								tests/test_ram/test_ram.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										381
									
								
								tests/test_ram/test_ram.py
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,381 @@ | ||||
| import time | ||||
| from datetime import datetime | ||||
| 
 | ||||
| import boto3 | ||||
| import sure  # noqa | ||||
| from botocore.exceptions import ClientError | ||||
| from nose.tools import assert_raises | ||||
| 
 | ||||
| from moto import mock_ram, mock_organizations | ||||
| from moto.core import ACCOUNT_ID | ||||
| 
 | ||||
| 
 | ||||
| @mock_ram | ||||
| def test_create_resource_share(): | ||||
|     # given | ||||
|     client = boto3.client("ram", region_name="us-east-1") | ||||
| 
 | ||||
|     # when | ||||
|     response = client.create_resource_share(name="test") | ||||
| 
 | ||||
|     # then | ||||
|     resource = response["resourceShare"] | ||||
|     resource["allowExternalPrincipals"].should.be.ok | ||||
|     resource["creationTime"].should.be.a(datetime) | ||||
|     resource["lastUpdatedTime"].should.be.a(datetime) | ||||
|     resource["name"].should.equal("test") | ||||
|     resource["owningAccountId"].should.equal(ACCOUNT_ID) | ||||
|     resource["resourceShareArn"].should.match( | ||||
|         r"arn:aws:ram:us-east-1:\d{12}:resource-share/[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}" | ||||
|     ) | ||||
|     resource["status"].should.equal("ACTIVE") | ||||
|     resource.should_not.have.key("featureSet") | ||||
| 
 | ||||
|     # creating a resource share with the name should result in a second one | ||||
|     # not overwrite/update the old one | ||||
|     # when | ||||
|     response = client.create_resource_share( | ||||
|         name="test", | ||||
|         allowExternalPrincipals=False, | ||||
|         resourceArns=[ | ||||
|             "arn:aws:ec2:us-east-1:{}:transit-gateway/tgw-123456789".format(ACCOUNT_ID) | ||||
|         ], | ||||
|     ) | ||||
| 
 | ||||
|     # then | ||||
|     resource = response["resourceShare"] | ||||
|     resource["allowExternalPrincipals"].should_not.be.ok | ||||
|     resource["creationTime"].should.be.a(datetime) | ||||
|     resource["lastUpdatedTime"].should.be.a(datetime) | ||||
|     resource["name"].should.equal("test") | ||||
|     resource["owningAccountId"].should.equal(ACCOUNT_ID) | ||||
|     resource["resourceShareArn"].should.match( | ||||
|         r"arn:aws:ram:us-east-1:\d{12}:resource-share/[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}" | ||||
|     ) | ||||
|     resource["status"].should.equal("ACTIVE") | ||||
| 
 | ||||
|     response = client.get_resource_shares(resourceOwner="SELF") | ||||
|     response["resourceShares"].should.have.length_of(2) | ||||
| 
 | ||||
| 
 | ||||
| @mock_ram | ||||
| def test_create_resource_share_errors(): | ||||
|     # given | ||||
|     client = boto3.client("ram", region_name="us-east-1") | ||||
| 
 | ||||
|     # invalid ARN | ||||
|     # when | ||||
|     with assert_raises(ClientError) as e: | ||||
|         client.create_resource_share(name="test", resourceArns=["inalid-arn"]) | ||||
|     ex = e.exception | ||||
|     ex.operation_name.should.equal("CreateResourceShare") | ||||
|     ex.response["ResponseMetadata"]["HTTPStatusCode"].should.equal(400) | ||||
|     ex.response["Error"]["Code"].should.contain("MalformedArnException") | ||||
|     ex.response["Error"]["Message"].should.equal( | ||||
|         "The specified resource ARN inalid-arn is not valid. " | ||||
|         "Verify the ARN and try again." | ||||
|     ) | ||||
| 
 | ||||
|     # valid ARN, but not shareable resource type | ||||
|     # when | ||||
|     with assert_raises(ClientError) as e: | ||||
|         client.create_resource_share( | ||||
|             name="test", resourceArns=["arn:aws:iam::{}:role/test".format(ACCOUNT_ID)] | ||||
|         ) | ||||
|     ex = e.exception | ||||
|     ex.operation_name.should.equal("CreateResourceShare") | ||||
|     ex.response["ResponseMetadata"]["HTTPStatusCode"].should.equal(400) | ||||
|     ex.response["Error"]["Code"].should.contain("MalformedArnException") | ||||
|     ex.response["Error"]["Message"].should.equal( | ||||
|         "You cannot share the selected resource type." | ||||
|     ) | ||||
| 
 | ||||
|     # invalid principal ID | ||||
|     # when | ||||
|     with assert_raises(ClientError) as e: | ||||
|         client.create_resource_share( | ||||
|             name="test", | ||||
|             principals=["invalid"], | ||||
|             resourceArns=[ | ||||
|                 "arn:aws:ec2:us-east-1:{}:transit-gateway/tgw-123456789".format( | ||||
|                     ACCOUNT_ID | ||||
|                 ) | ||||
|             ], | ||||
|         ) | ||||
|     ex = e.exception | ||||
|     ex.operation_name.should.equal("CreateResourceShare") | ||||
|     ex.response["ResponseMetadata"]["HTTPStatusCode"].should.equal(400) | ||||
|     ex.response["Error"]["Code"].should.contain("InvalidParameterException") | ||||
|     ex.response["Error"]["Message"].should.equal( | ||||
|         "Principal ID invalid is malformed. Verify the ID and try again." | ||||
|     ) | ||||
| 
 | ||||
| 
 | ||||
| @mock_ram | ||||
| @mock_organizations | ||||
| def test_create_resource_share_with_organization(): | ||||
|     # given | ||||
|     client = boto3.client("organizations", region_name="us-east-1") | ||||
|     org_arn = client.create_organization(FeatureSet="ALL")["Organization"]["Arn"] | ||||
|     root_id = client.list_roots()["Roots"][0]["Id"] | ||||
|     ou_arn = client.create_organizational_unit(ParentId=root_id, Name="test")[ | ||||
|         "OrganizationalUnit" | ||||
|     ]["Arn"] | ||||
|     client = boto3.client("ram", region_name="us-east-1") | ||||
| 
 | ||||
|     # share in whole Organization | ||||
|     # when | ||||
|     response = client.create_resource_share( | ||||
|         name="test", | ||||
|         principals=[org_arn], | ||||
|         resourceArns=[ | ||||
|             "arn:aws:ec2:us-east-1:{}:transit-gateway/tgw-123456789".format(ACCOUNT_ID) | ||||
|         ], | ||||
|     ) | ||||
| 
 | ||||
|     # then | ||||
|     response["resourceShare"]["name"].should.equal("test") | ||||
| 
 | ||||
|     # share in an OU | ||||
|     # when | ||||
|     response = client.create_resource_share( | ||||
|         name="test", | ||||
|         principals=[ou_arn], | ||||
|         resourceArns=[ | ||||
|             "arn:aws:ec2:us-east-1:{}:transit-gateway/tgw-123456789".format(ACCOUNT_ID) | ||||
|         ], | ||||
|     ) | ||||
| 
 | ||||
|     # then | ||||
|     response["resourceShare"]["name"].should.equal("test") | ||||
| 
 | ||||
| 
 | ||||
| @mock_ram | ||||
| @mock_organizations | ||||
| def test_create_resource_share_with_organization_errors(): | ||||
|     # given | ||||
|     client = boto3.client("organizations", region_name="us-east-1") | ||||
|     client.create_organization(FeatureSet="ALL") | ||||
|     root_id = client.list_roots()["Roots"][0]["Id"] | ||||
|     client.create_organizational_unit(ParentId=root_id, Name="test") | ||||
|     client = boto3.client("ram", region_name="us-east-1") | ||||
| 
 | ||||
|     # unknown Organization | ||||
|     # when | ||||
|     with assert_raises(ClientError) as e: | ||||
|         client.create_resource_share( | ||||
|             name="test", | ||||
|             principals=[ | ||||
|                 "arn:aws:organizations::{}:organization/o-unknown".format(ACCOUNT_ID) | ||||
|             ], | ||||
|             resourceArns=[ | ||||
|                 "arn:aws:ec2:us-east-1:{}:transit-gateway/tgw-123456789".format( | ||||
|                     ACCOUNT_ID | ||||
|                 ) | ||||
|             ], | ||||
|         ) | ||||
|     ex = e.exception | ||||
|     ex.operation_name.should.equal("CreateResourceShare") | ||||
|     ex.response["ResponseMetadata"]["HTTPStatusCode"].should.equal(400) | ||||
|     ex.response["Error"]["Code"].should.contain("UnknownResourceException") | ||||
|     ex.response["Error"]["Message"].should.equal( | ||||
|         "Organization o-unknown could not be found." | ||||
|     ) | ||||
| 
 | ||||
|     # unknown OU | ||||
|     # when | ||||
|     with assert_raises(ClientError) as e: | ||||
|         client.create_resource_share( | ||||
|             name="test", | ||||
|             principals=[ | ||||
|                 "arn:aws:organizations::{}:ou/o-unknown/ou-unknown".format(ACCOUNT_ID) | ||||
|             ], | ||||
|             resourceArns=[ | ||||
|                 "arn:aws:ec2:us-east-1:{}:transit-gateway/tgw-123456789".format( | ||||
|                     ACCOUNT_ID | ||||
|                 ) | ||||
|             ], | ||||
|         ) | ||||
|     ex = e.exception | ||||
|     ex.operation_name.should.equal("CreateResourceShare") | ||||
|     ex.response["ResponseMetadata"]["HTTPStatusCode"].should.equal(400) | ||||
|     ex.response["Error"]["Code"].should.contain("UnknownResourceException") | ||||
|     ex.response["Error"]["Message"].should.equal( | ||||
|         "OrganizationalUnit ou-unknown in unknown organization could not be found." | ||||
|     ) | ||||
| 
 | ||||
| 
 | ||||
| @mock_ram | ||||
| def test_get_resource_shares(): | ||||
|     # given | ||||
|     client = boto3.client("ram", region_name="us-east-1") | ||||
|     client.create_resource_share(name="test") | ||||
| 
 | ||||
|     # when | ||||
|     response = client.get_resource_shares(resourceOwner="SELF") | ||||
| 
 | ||||
|     # then | ||||
|     response["resourceShares"].should.have.length_of(1) | ||||
|     resource = response["resourceShares"][0] | ||||
|     resource["allowExternalPrincipals"].should.be.ok | ||||
|     resource["creationTime"].should.be.a(datetime) | ||||
|     resource["featureSet"].should.equal("STANDARD") | ||||
|     resource["lastUpdatedTime"].should.be.a(datetime) | ||||
|     resource["name"].should.equal("test") | ||||
|     resource["owningAccountId"].should.equal(ACCOUNT_ID) | ||||
|     resource["resourceShareArn"].should.match( | ||||
|         r"arn:aws:ram:us-east-1:\d{12}:resource-share/[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}" | ||||
|     ) | ||||
|     resource["status"].should.equal("ACTIVE") | ||||
| 
 | ||||
| 
 | ||||
| @mock_ram | ||||
| def test_get_resource_shares_errors(): | ||||
|     # given | ||||
|     client = boto3.client("ram", region_name="us-east-1") | ||||
| 
 | ||||
|     # invalid resource owner | ||||
|     # when | ||||
|     with assert_raises(ClientError) as e: | ||||
|         client.get_resource_shares(resourceOwner="invalid") | ||||
|     ex = e.exception | ||||
|     ex.operation_name.should.equal("GetResourceShares") | ||||
|     ex.response["ResponseMetadata"]["HTTPStatusCode"].should.equal(400) | ||||
|     ex.response["Error"]["Code"].should.contain("InvalidParameterException") | ||||
|     ex.response["Error"]["Message"].should.equal( | ||||
|         "invalid is not a valid resource owner. " | ||||
|         "Specify either SELF or OTHER-ACCOUNTS and try again." | ||||
|     ) | ||||
| 
 | ||||
| 
 | ||||
| @mock_ram | ||||
| def test_update_resource_share(): | ||||
|     # given | ||||
|     client = boto3.client("ram", region_name="us-east-1") | ||||
|     arn = client.create_resource_share(name="test")["resourceShare"]["resourceShareArn"] | ||||
| 
 | ||||
|     # when | ||||
|     time.sleep(0.1) | ||||
|     response = client.update_resource_share(resourceShareArn=arn, name="test-update") | ||||
| 
 | ||||
|     # then | ||||
|     resource = response["resourceShare"] | ||||
|     resource["allowExternalPrincipals"].should.be.ok | ||||
|     resource["name"].should.equal("test-update") | ||||
|     resource["owningAccountId"].should.equal(ACCOUNT_ID) | ||||
|     resource["resourceShareArn"].should.match( | ||||
|         r"arn:aws:ram:us-east-1:\d{12}:resource-share/[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}" | ||||
|     ) | ||||
|     resource["status"].should.equal("ACTIVE") | ||||
|     resource.should_not.have.key("featureSet") | ||||
|     creation_time = resource["creationTime"] | ||||
|     resource["lastUpdatedTime"].should.be.greater_than(creation_time) | ||||
| 
 | ||||
|     response = client.get_resource_shares(resourceOwner="SELF") | ||||
|     response["resourceShares"].should.have.length_of(1) | ||||
| 
 | ||||
| 
 | ||||
| @mock_ram | ||||
| def test_update_resource_share_errors(): | ||||
|     # given | ||||
|     client = boto3.client("ram", region_name="us-east-1") | ||||
| 
 | ||||
|     # invalid resource owner | ||||
|     # when | ||||
|     with assert_raises(ClientError) as e: | ||||
|         client.update_resource_share( | ||||
|             resourceShareArn="arn:aws:ram:us-east-1:{}:resource-share/not-existing".format( | ||||
|                 ACCOUNT_ID | ||||
|             ), | ||||
|             name="test-update", | ||||
|         ) | ||||
|     ex = e.exception | ||||
|     ex.operation_name.should.equal("UpdateResourceShare") | ||||
|     ex.response["ResponseMetadata"]["HTTPStatusCode"].should.equal(400) | ||||
|     ex.response["Error"]["Code"].should.contain("UnknownResourceException") | ||||
|     ex.response["Error"]["Message"].should.equal( | ||||
|         "ResourceShare arn:aws:ram:us-east-1:{}:resource-share/not-existing could not be found.".format( | ||||
|             ACCOUNT_ID | ||||
|         ) | ||||
|     ) | ||||
| 
 | ||||
| 
 | ||||
| @mock_ram | ||||
| def test_delete_resource_share(): | ||||
|     # given | ||||
|     client = boto3.client("ram", region_name="us-east-1") | ||||
|     arn = client.create_resource_share(name="test")["resourceShare"]["resourceShareArn"] | ||||
| 
 | ||||
|     # when | ||||
|     time.sleep(0.1) | ||||
|     response = client.delete_resource_share(resourceShareArn=arn) | ||||
| 
 | ||||
|     # then | ||||
|     response["returnValue"].should.be.ok | ||||
| 
 | ||||
|     response = client.get_resource_shares(resourceOwner="SELF") | ||||
|     response["resourceShares"].should.have.length_of(1) | ||||
|     resource = response["resourceShares"][0] | ||||
|     resource["status"].should.equal("DELETED") | ||||
|     creation_time = resource["creationTime"] | ||||
|     resource["lastUpdatedTime"].should.be.greater_than(creation_time) | ||||
| 
 | ||||
| 
 | ||||
| @mock_ram | ||||
| def test_delete_resource_share_errors(): | ||||
|     # given | ||||
|     client = boto3.client("ram", region_name="us-east-1") | ||||
| 
 | ||||
|     # invalid resource owner | ||||
|     # when | ||||
|     with assert_raises(ClientError) as e: | ||||
|         client.delete_resource_share( | ||||
|             resourceShareArn="arn:aws:ram:us-east-1:{}:resource-share/not-existing".format( | ||||
|                 ACCOUNT_ID | ||||
|             ) | ||||
|         ) | ||||
|     ex = e.exception | ||||
|     ex.operation_name.should.equal("DeleteResourceShare") | ||||
|     ex.response["ResponseMetadata"]["HTTPStatusCode"].should.equal(400) | ||||
|     ex.response["Error"]["Code"].should.contain("UnknownResourceException") | ||||
|     ex.response["Error"]["Message"].should.equal( | ||||
|         "ResourceShare arn:aws:ram:us-east-1:{}:resource-share/not-existing could not be found.".format( | ||||
|             ACCOUNT_ID | ||||
|         ) | ||||
|     ) | ||||
| 
 | ||||
| 
 | ||||
| @mock_ram | ||||
| @mock_organizations | ||||
| def test_enable_sharing_with_aws_organization(): | ||||
|     # given | ||||
|     client = boto3.client("organizations", region_name="us-east-1") | ||||
|     client.create_organization(FeatureSet="ALL") | ||||
|     client = boto3.client("ram", region_name="us-east-1") | ||||
| 
 | ||||
|     # when | ||||
|     response = client.enable_sharing_with_aws_organization() | ||||
| 
 | ||||
|     # then | ||||
|     response["returnValue"].should.be.ok | ||||
| 
 | ||||
| 
 | ||||
| @mock_ram | ||||
| @mock_organizations | ||||
| def test_enable_sharing_with_aws_organization_errors(): | ||||
|     # given | ||||
|     client = boto3.client("ram", region_name="us-east-1") | ||||
| 
 | ||||
|     # no Organization defined | ||||
|     # when | ||||
|     with assert_raises(ClientError) as e: | ||||
|         client.enable_sharing_with_aws_organization() | ||||
|     ex = e.exception | ||||
|     ex.operation_name.should.equal("EnableSharingWithAwsOrganization") | ||||
|     ex.response["ResponseMetadata"]["HTTPStatusCode"].should.equal(400) | ||||
|     ex.response["Error"]["Code"].should.contain("OperationNotPermittedException") | ||||
|     ex.response["Error"]["Message"].should.equal( | ||||
|         "Unable to enable sharing with AWS Organizations. " | ||||
|         "Received AccessDeniedException from AWSOrganizations with the following error message: " | ||||
|         "You don't have permissions to access this resource." | ||||
|     ) | ||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user