Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

EC2: Cross-account VPC peering connections #6826

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
19 changes: 15 additions & 4 deletions moto/ec2/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -632,21 +632,23 @@ def __init__(self, cidr_block: str):
super().__init__("InvalidVpc.Range", f"The CIDR '{cidr_block}' is invalid.")


# accept exception
# Raised when attempting to accept a VPC peering connection request in own account but in the requester region
class OperationNotPermitted2(EC2ClientError):
def __init__(self, client_region: str, pcx_id: str, acceptor_region: str):
super().__init__(
"OperationNotPermitted",
f"Incorrect region ({client_region}) specified for this request.VPC peering connection {pcx_id} must be accepted in region {acceptor_region}",
f"Incorrect region ({client_region}) specified for this request. "
f"VPC peering connection {pcx_id} must be accepted in region {acceptor_region}",
)


# reject exception
# Raised when attempting to reject a VPC peering connection request in own account but in the requester region
class OperationNotPermitted3(EC2ClientError):
def __init__(self, client_region: str, pcx_id: str, acceptor_region: str):
super().__init__(
"OperationNotPermitted",
f"Incorrect region ({client_region}) specified for this request.VPC peering connection {pcx_id} must be accepted or rejected in region {acceptor_region}",
f"Incorrect region ({client_region}) specified for this request. "
f"VPC peering connection {pcx_id} must be accepted or rejected in region {acceptor_region}",
)


Expand All @@ -658,6 +660,15 @@ def __init__(self, instance_id: str):
)


# Raised when attempting to accept or reject a VPC peering connection request for a VPC not belonging to self
class OperationNotPermitted5(EC2ClientError):
def __init__(self, account_id: str, pcx_id: str, operation: str):
super().__init__(
"OperationNotPermitted",
f"User ({account_id}) cannot {operation} peering {pcx_id}",
)


class InvalidLaunchTemplateNameAlreadyExistsError(EC2ClientError):
def __init__(self) -> None:
super().__init__(
Expand Down
4 changes: 2 additions & 2 deletions moto/ec2/models/transit_gateway_attachments.py
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,7 @@ def __init__(
"region": region_name,
"transitGatewayId": transit_gateway_id,
}
self.status = PeeringConnectionStatus()
self.status = PeeringConnectionStatus(accepter_id=peer_account_id)


class TransitGatewayAttachmentBackend:
Expand Down Expand Up @@ -342,5 +342,5 @@ def delete_transit_gateway_peering_attachment(
transit_gateway_attachment_id
]
transit_gateway_attachment.state = "deleted"
transit_gateway_attachment.status.deleted() # type: ignore[attr-defined]
transit_gateway_attachment.status.deleted(deleter_id=self.account_id) # type: ignore[attr-defined]
return transit_gateway_attachment
62 changes: 43 additions & 19 deletions moto/ec2/models/vpc_peering_connections.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,28 +7,32 @@
InvalidVPCPeeringConnectionStateTransitionError,
OperationNotPermitted2,
OperationNotPermitted3,
OperationNotPermitted5,
)
from .core import TaggedEC2Resource
from .vpcs import VPC
from ..utils import random_vpc_peering_connection_id


class PeeringConnectionStatus:
def __init__(self, code: str = "initiating-request", message: str = ""):
def __init__(
self, accepter_id: str, code: str = "initiating-request", message: str = ""
):
self.accepter_id = accepter_id
self.code = code
self.message = message

def deleted(self) -> None:
def deleted(self, deleter_id: str) -> None:
self.code = "deleted"
self.message = "Deleted by {deleter ID}"
self.message = f"Deleted by {deleter_id}"

def initiating(self) -> None:
self.code = "initiating-request"
self.message = "Initiating Request to {accepter ID}"
self.message = f"Initiating Request to {self.accepter_id}"

def pending(self) -> None:
self.code = "pending-acceptance"
self.message = "Pending Acceptance by {accepter ID}"
self.message = f"Pending Acceptance by {self.accepter_id}"

def accept(self) -> None:
self.code = "active"
Expand Down Expand Up @@ -61,7 +65,7 @@ def __init__(
self.requester_options = self.DEFAULT_OPTIONS.copy()
self.accepter_options = self.DEFAULT_OPTIONS.copy()
self.add_tags(tags or {})
self._status = PeeringConnectionStatus()
self._status = PeeringConnectionStatus(accepter_id=peer_vpc.owner_id)

@staticmethod
def cloudformation_name_type() -> str:
Expand All @@ -79,7 +83,7 @@ def create_from_cloudformation_json( # type: ignore[misc]
cloudformation_json: Any,
account_id: str,
region_name: str,
**kwargs: Any
**kwargs: Any,
) -> "VPCPeeringConnection":
from ..models import ec2_backends

Expand Down Expand Up @@ -120,11 +124,15 @@ def create_vpc_peering_connection(
vpc_pcx = VPCPeeringConnection(self, vpc_pcx_id, vpc, peer_vpc, tags)
vpc_pcx._status.pending()
self.vpc_pcxs[vpc_pcx_id] = vpc_pcx
# insert cross region peering info
if vpc.ec2_backend.region_name != peer_vpc.ec2_backend.region_name:
for vpc_pcx_cx in peer_vpc.ec2_backend.get_vpc_pcx_refs():
if vpc_pcx_cx.region_name == peer_vpc.ec2_backend.region_name:
vpc_pcx_cx.vpc_pcxs[vpc_pcx_id] = vpc_pcx
# insert cross-account/cross-region peering info
if vpc.owner_id != peer_vpc.owner_id or vpc.region != peer_vpc.region:
for backend in peer_vpc.ec2_backend.get_vpc_pcx_refs():
if (
backend.account_id == peer_vpc.owner_id
and backend.region_name == peer_vpc.region
):
backend.vpc_pcxs[vpc_pcx_id] = vpc_pcx

return vpc_pcx

def describe_vpc_peering_connections(
Expand All @@ -142,28 +150,44 @@ def get_vpc_peering_connection(self, vpc_pcx_id: str) -> VPCPeeringConnection:

def delete_vpc_peering_connection(self, vpc_pcx_id: str) -> VPCPeeringConnection:
deleted = self.get_vpc_peering_connection(vpc_pcx_id)
deleted._status.deleted()
deleted._status.deleted(deleter_id=self.account_id) # type: ignore[attr-defined]
return deleted

def accept_vpc_peering_connection(self, vpc_pcx_id: str) -> VPCPeeringConnection:
vpc_pcx = self.get_vpc_peering_connection(vpc_pcx_id)
# if cross region need accepter from another region
pcx_req_region = vpc_pcx.vpc.ec2_backend.region_name
pcx_acp_region = vpc_pcx.peer_vpc.ec2_backend.region_name

# validate cross-account acceptance
req_account_id = vpc_pcx.vpc.owner_id
acp_account_id = vpc_pcx.peer_vpc.owner_id
if req_account_id != acp_account_id and self.account_id != acp_account_id: # type: ignore[attr-defined]
raise OperationNotPermitted5(self.account_id, vpc_pcx_id, "accept") # type: ignore[attr-defined]

# validate cross-region acceptance
pcx_req_region = vpc_pcx.vpc.region
pcx_acp_region = vpc_pcx.peer_vpc.region
if pcx_req_region != pcx_acp_region and self.region_name == pcx_req_region: # type: ignore[attr-defined]
raise OperationNotPermitted2(self.region_name, vpc_pcx.id, pcx_acp_region) # type: ignore[attr-defined]

if vpc_pcx._status.code != "pending-acceptance":
raise InvalidVPCPeeringConnectionStateTransitionError(vpc_pcx.id)
vpc_pcx._status.accept()
return vpc_pcx

def reject_vpc_peering_connection(self, vpc_pcx_id: str) -> VPCPeeringConnection:
vpc_pcx = self.get_vpc_peering_connection(vpc_pcx_id)
# if cross region need accepter from another region
pcx_req_region = vpc_pcx.vpc.ec2_backend.region_name
pcx_acp_region = vpc_pcx.peer_vpc.ec2_backend.region_name

# validate cross-account rejection
req_account_id = vpc_pcx.vpc.owner_id
acp_account_id = vpc_pcx.peer_vpc.owner_id
if req_account_id != acp_account_id and self.account_id != acp_account_id: # type: ignore[attr-defined]
raise OperationNotPermitted5(self.account_id, vpc_pcx_id, "reject") # type: ignore[attr-defined]

# validate cross-region acceptance
pcx_req_region = vpc_pcx.vpc.region
pcx_acp_region = vpc_pcx.peer_vpc.region
if pcx_req_region != pcx_acp_region and self.region_name == pcx_req_region: # type: ignore[attr-defined]
raise OperationNotPermitted3(self.region_name, vpc_pcx.id, pcx_acp_region) # type: ignore[attr-defined]

if vpc_pcx._status.code != "pending-acceptance":
raise InvalidVPCPeeringConnectionStateTransitionError(vpc_pcx.id)
vpc_pcx._status.reject()
Expand Down
4 changes: 4 additions & 0 deletions moto/ec2/models/vpcs.py
Original file line number Diff line number Diff line change
Expand Up @@ -205,6 +205,10 @@ def __init__(
def owner_id(self) -> str:
return self.ec2_backend.account_id

@property
def region(self) -> str:
return self.ec2_backend.region_name

@staticmethod
def cloudformation_name_type() -> str:
return ""
Expand Down
114 changes: 68 additions & 46 deletions moto/ec2/responses/vpc_peering_connections.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,21 +3,23 @@

class VPCPeeringConnections(EC2BaseResponse):
def create_vpc_peering_connection(self) -> str:
peer_region = self._get_param("PeerRegion")
tags = self._parse_tag_specification().get("vpc-peering-connection", {})

if peer_region == self.region or peer_region is None:
peer_vpc = self.ec2_backend.get_vpc(self._get_param("PeerVpcId"))
else:
from moto.ec2.models import ec2_backends
account_id = self._get_param("PeerOwnerId") or self.current_account
region_name = self._get_param("PeerRegion") or self.region

peer_vpc = ec2_backends[self.current_account][peer_region].get_vpc(
self._get_param("PeerVpcId")
)
vpc = self.ec2_backend.get_vpc(self._get_param("VpcId"))

# Peer VPC could belong to another account or region
from moto.ec2.models import ec2_backends

peer_vpc = ec2_backends[account_id][region_name].get_vpc(
self._get_param("PeerVpcId")
)

vpc_pcx = self.ec2_backend.create_vpc_peering_connection(vpc, peer_vpc, tags)
template = self.response_template(CREATE_VPC_PEERING_CONNECTION_RESPONSE)
return template.render(account_id=self.current_account, vpc_pcx=vpc_pcx)
return template.render(vpc_pcx=vpc_pcx)

def delete_vpc_peering_connection(self) -> str:
vpc_pcx_id = self._get_param("VpcPeeringConnectionId")
Expand All @@ -31,13 +33,13 @@ def describe_vpc_peering_connections(self) -> str:
vpc_peering_ids=ids
)
template = self.response_template(DESCRIBE_VPC_PEERING_CONNECTIONS_RESPONSE)
return template.render(account_id=self.current_account, vpc_pcxs=vpc_pcxs)
return template.render(vpc_pcxs=vpc_pcxs)

def accept_vpc_peering_connection(self) -> str:
vpc_pcx_id = self._get_param("VpcPeeringConnectionId")
vpc_pcx = self.ec2_backend.accept_vpc_peering_connection(vpc_pcx_id)
template = self.response_template(ACCEPT_VPC_PEERING_CONNECTION_RESPONSE)
return template.render(account_id=self.current_account, vpc_pcx=vpc_pcx)
return template.render(vpc_pcx=vpc_pcx)

def reject_vpc_peering_connection(self) -> str:
vpc_pcx_id = self._get_param("VpcPeeringConnectionId")
Expand Down Expand Up @@ -68,39 +70,46 @@ def modify_vpc_peering_connection_options(self) -> str:
<CreateVpcPeeringConnectionResponse xmlns="http://ec2.amazonaws.com/doc/2016-11-15/">
<requestId>7a62c49f-347e-4fc4-9331-6e8eEXAMPLE</requestId>
<vpcPeeringConnection>
<vpcPeeringConnectionId>{{ vpc_pcx.id }}</vpcPeeringConnectionId>
<requesterVpcInfo>
<ownerId>{{ account_id }}</ownerId>
<vpcPeeringConnectionId>{{ vpc_pcx.id }}</vpcPeeringConnectionId>
<requesterVpcInfo>
<ownerId>{{ vpc_pcx.vpc.owner_id }}</ownerId>
<region>{{ vpc_pcx.vpc.region }}</region>
<vpcId>{{ vpc_pcx.vpc.id }}</vpcId>
<cidrBlock>{{ vpc_pcx.vpc.cidr_block }}</cidrBlock>
<cidrBlockSet></cidrBlockSet>
<ipv6CidrBlockSet></ipv6CidrBlockSet>
<peeringOptions>
<allowEgressFromLocalClassicLinkToRemoteVpc>{{ vpc_pcx.requester_options.AllowEgressFromLocalClassicLinkToRemoteVpc or '' }}</allowEgressFromLocalClassicLinkToRemoteVpc>
<allowEgressFromLocalVpcToRemoteClassicLink>{{ vpc_pcx.requester_options.AllowEgressFromLocalVpcToRemoteClassicLink or '' }}</allowEgressFromLocalVpcToRemoteClassicLink>
<allowDnsResolutionFromRemoteVpc>{{ vpc_pcx.requester_options.AllowDnsResolutionFromRemoteVpc or '' }}</allowDnsResolutionFromRemoteVpc>
</peeringOptions>
</requesterVpcInfo>
<accepterVpcInfo>
<ownerId>{{ account_id }}</ownerId>
<vpcId>{{ vpc_pcx.peer_vpc.id }}</vpcId>
<peeringOptions>
<allowEgressFromLocalClassicLinkToRemoteVpc>{{ vpc_pcx.accepter_options.AllowEgressFromLocalClassicLinkToRemoteVpc or '' }}</allowEgressFromLocalClassicLinkToRemoteVpc>
<allowEgressFromLocalVpcToRemoteClassicLink>{{ vpc_pcx.accepter_options.AllowEgressFromLocalVpcToRemoteClassicLink or '' }}</allowEgressFromLocalVpcToRemoteClassicLink>
<allowDnsResolutionFromRemoteVpc>{{ vpc_pcx.accepter_options.AllowDnsResolutionFromRemoteVpc or '' }}</allowDnsResolutionFromRemoteVpc>
</peeringOptions>
</accepterVpcInfo>
<status>
</requesterVpcInfo>
<accepterVpcInfo>
<ownerId>{{ vpc_pcx.peer_vpc.owner_id }}</ownerId>
<region>{{ vpc_pcx.peer_vpc.region }}</region>
<vpcId>{{ vpc_pcx.peer_vpc.id }}</vpcId>
<cidrBlock>{{ vpc_pcx.peer_vpc.cidr_block }}</cidrBlock>
<cidrBlockSet></cidrBlockSet>
<ipv6CidrBlockSet></ipv6CidrBlockSet>
<peeringOptions>
<allowEgressFromLocalClassicLinkToRemoteVpc>{{ vpc_pcx.accepter_options.AllowEgressFromLocalClassicLinkToRemoteVpc or '' }}</allowEgressFromLocalClassicLinkToRemoteVpc>
<allowEgressFromLocalVpcToRemoteClassicLink>{{ vpc_pcx.accepter_options.AllowEgressFromLocalVpcToRemoteClassicLink or '' }}</allowEgressFromLocalVpcToRemoteClassicLink>
<allowDnsResolutionFromRemoteVpc>{{ vpc_pcx.accepter_options.AllowDnsResolutionFromRemoteVpc or '' }}</allowDnsResolutionFromRemoteVpc>
</peeringOptions>
</accepterVpcInfo>
<status>
<code>initiating-request</code>
<message>Initiating Request to {accepter ID}</message>
</status>
<expirationTime>2014-02-18T14:37:25.000Z</expirationTime>
<tagSet>
{% for tag in vpc_pcx.get_tags() %}
<item>
<key>{{ tag.key }}</key>
<value>{{ tag.value }}</value>
</item>
{% endfor %}
</tagSet>
<message>Initiating Request to {{ vpc_pcx.peer_vpc.owner_id }}</message>
</status>
<expirationTime>2014-02-18T14:37:25.000Z</expirationTime>
<tagSet>
{% for tag in vpc_pcx.get_tags() %}
<item>
<key>{{ tag.key }}</key>
<value>{{ tag.value }}</value>
</item>
{% endfor %}
</tagSet>
</vpcPeeringConnection>
</CreateVpcPeeringConnectionResponse>
"""
Expand All @@ -113,21 +122,25 @@ def modify_vpc_peering_connection_options(self) -> str:
<item>
<vpcPeeringConnectionId>{{ vpc_pcx.id }}</vpcPeeringConnectionId>
<requesterVpcInfo>
<ownerId>{{ account_id }}</ownerId>
<ownerId>{{ vpc_pcx.vpc.owner_id }}</ownerId>
<region>{{ vpc_pcx.vpc.region }}</region>
<vpcId>{{ vpc_pcx.vpc.id }}</vpcId>
<cidrBlock>{{ vpc_pcx.vpc.cidr_block }}</cidrBlock>
<region>{{ vpc_pcx.vpc.ec2_backend.region_name }}</region>
<cidrBlockSet></cidrBlockSet>
<ipv6CidrBlockSet></ipv6CidrBlockSet>
<peeringOptions>
<allowEgressFromLocalClassicLinkToRemoteVpc>{{ vpc_pcx.requester_options.AllowEgressFromLocalClassicLinkToRemoteVpc or '' }}</allowEgressFromLocalClassicLinkToRemoteVpc>
<allowEgressFromLocalVpcToRemoteClassicLink>{{ vpc_pcx.requester_options.AllowEgressFromLocalVpcToRemoteClassicLink or '' }}</allowEgressFromLocalVpcToRemoteClassicLink>
<allowDnsResolutionFromRemoteVpc>{{ vpc_pcx.requester_options.AllowDnsResolutionFromRemoteVpc or '' }}</allowDnsResolutionFromRemoteVpc>
</peeringOptions>
</requesterVpcInfo>
<accepterVpcInfo>
<ownerId>{{ account_id }}</ownerId>
<ownerId>{{ vpc_pcx.peer_vpc.owner_id }}</ownerId>
<region>{{ vpc_pcx.peer_vpc.region }}</region>
<vpcId>{{ vpc_pcx.peer_vpc.id }}</vpcId>
<cidrBlock>{{ vpc_pcx.peer_vpc.cidr_block }}</cidrBlock>
<region>{{ vpc_pcx.peer_vpc.ec2_backend.region_name }}</region>
<cidrBlockSet></cidrBlockSet>
<ipv6CidrBlockSet></ipv6CidrBlockSet>
<peeringOptions>
<allowEgressFromLocalClassicLinkToRemoteVpc>{{ vpc_pcx.accepter_options.AllowEgressFromLocalClassicLinkToRemoteVpc or '' }}</allowEgressFromLocalClassicLinkToRemoteVpc>
<allowEgressFromLocalVpcToRemoteClassicLink>{{ vpc_pcx.accepter_options.AllowEgressFromLocalVpcToRemoteClassicLink or '' }}</allowEgressFromLocalVpcToRemoteClassicLink>
Expand Down Expand Up @@ -165,21 +178,30 @@ def modify_vpc_peering_connection_options(self) -> str:
<vpcPeeringConnection>
<vpcPeeringConnectionId>{{ vpc_pcx.id }}</vpcPeeringConnectionId>
<requesterVpcInfo>
<ownerId>{{ account_id }}</ownerId>
<vpcId>{{ vpc_pcx.vpc.id }}</vpcId>
<cidrBlock>{{ vpc_pcx.vpc.cidr_block }}</cidrBlock>
<region>{{ vpc_pcx.vpc.ec2_backend.region_name }}</region>
<ownerId>{{ vpc_pcx.vpc.owner_id }}</ownerId>
<region>{{ vpc_pcx.vpc.region }}</region>
<vpcId>{{ vpc_pcx.vpc.id }}</vpcId>
<cidrBlock>{{ vpc_pcx.vpc.cidr_block }}</cidrBlock>
<cidrBlockSet></cidrBlockSet>
<ipv6CidrBlockSet></ipv6CidrBlockSet>
<peeringOptions>
<allowEgressFromLocalClassicLinkToRemoteVpc>{{ vpc_pcx.requester_options.AllowEgressFromLocalClassicLinkToRemoteVpc or '' }}</allowEgressFromLocalClassicLinkToRemoteVpc>
<allowEgressFromLocalVpcToRemoteClassicLink>{{ vpc_pcx.requester_options.AllowEgressFromLocalVpcToRemoteClassicLink or '' }}</allowEgressFromLocalVpcToRemoteClassicLink>
<allowDnsResolutionFromRemoteVpc>{{ vpc_pcx.requester_options.AllowDnsResolutionFromRemoteVpc or '' }}</allowDnsResolutionFromRemoteVpc>
</peeringOptions>
</requesterVpcInfo>
<accepterVpcInfo>
<ownerId>{{ account_id }}</ownerId>
<ownerId>{{ vpc_pcx.peer_vpc.owner_id }}</ownerId>
<region>{{ vpc_pcx.peer_vpc.region }}</region>
<vpcId>{{ vpc_pcx.peer_vpc.id }}</vpcId>
<cidrBlock>{{ vpc_pcx.peer_vpc.cidr_block }}</cidrBlock>
<cidrBlockSet></cidrBlockSet>
<ipv6CidrBlockSet></ipv6CidrBlockSet>
<peeringOptions>
<allowEgressFromLocalClassicLinkToRemoteVpc>{{ vpc_pcx.accepter_options.AllowEgressFromLocalClassicLinkToRemoteVpc or '' }}</allowEgressFromLocalClassicLinkToRemoteVpc>
<allowEgressFromLocalVpcToRemoteClassicLink>{{ vpc_pcx.accepter_options.AllowEgressFromLocalVpcToRemoteClassicLink or '' }}</allowEgressFromLocalVpcToRemoteClassicLink>
<allowDnsResolutionFromRemoteVpc>{{ vpc_pcx.accepter_options.AllowDnsResolutionFromRemoteVpc or '' }}</allowDnsResolutionFromRemoteVpc>
</peeringOptions>
<region>{{ vpc_pcx.peer_vpc.ec2_backend.region_name }}</region>
</accepterVpcInfo>
<status>
<code>{{ vpc_pcx._status.code }}</code>
Expand Down