From a507314d45e579e9fd9304e0b9cdb73287737010 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Anton=20Gr=C3=BCbel?= Date: Tue, 21 Jul 2020 15:15:13 +0200 Subject: [PATCH] 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 --- moto/__init__.py | 1 + moto/backends.py | 1 + moto/ram/__init__.py | 5 + moto/ram/exceptions.py | 39 ++++ moto/ram/models.py | 247 ++++++++++++++++++++++++ moto/ram/responses.py | 39 ++++ moto/ram/urls.py | 12 ++ tests/test_ram/test_ram.py | 381 +++++++++++++++++++++++++++++++++++++ 8 files changed, 725 insertions(+) create mode 100644 moto/ram/__init__.py create mode 100644 moto/ram/exceptions.py create mode 100644 moto/ram/models.py create mode 100644 moto/ram/responses.py create mode 100644 moto/ram/urls.py create mode 100644 tests/test_ram/test_ram.py diff --git a/moto/__init__.py b/moto/__init__.py index 5143a4933..7d841fbbc 100644 --- a/moto/__init__.py +++ b/moto/__init__.py @@ -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") diff --git a/moto/backends.py b/moto/backends.py index a73940909..4252bfd95 100644 --- a/moto/backends.py +++ b/moto/backends.py @@ -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"), diff --git a/moto/ram/__init__.py b/moto/ram/__init__.py new file mode 100644 index 000000000..a4925944f --- /dev/null +++ b/moto/ram/__init__.py @@ -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) diff --git a/moto/ram/exceptions.py b/moto/ram/exceptions.py new file mode 100644 index 000000000..49e57a61a --- /dev/null +++ b/moto/ram/exceptions.py @@ -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 + ) diff --git a/moto/ram/models.py b/moto/ram/models.py new file mode 100644 index 000000000..d38099374 --- /dev/null +++ b/moto/ram/models.py @@ -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) diff --git a/moto/ram/responses.py b/moto/ram/responses.py new file mode 100644 index 000000000..b01254007 --- /dev/null +++ b/moto/ram/responses.py @@ -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()) diff --git a/moto/ram/urls.py b/moto/ram/urls.py new file mode 100644 index 000000000..1414b89b0 --- /dev/null +++ b/moto/ram/urls.py @@ -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, +} diff --git a/tests/test_ram/test_ram.py b/tests/test_ram/test_ram.py new file mode 100644 index 000000000..624221929 --- /dev/null +++ b/tests/test_ram/test_ram.py @@ -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." + )