Skip to content

Commit a7b4768

Browse files
committed
feat: allow account sharing restrictions by partition
Currently the list of given account IDs in the "share" config section for each image will be applied no matter on which AWS partition. But account IDs are different on the different partitions so account 123456789123 is something different on "aws", "aws-cn" and "aws-us-gov". Being able to restrict account sharing by partition makes it possible to reuse the same awspub.yaml configuration across different partitions.
1 parent 0d8c632 commit a7b4768

File tree

6 files changed

+101
-4
lines changed

6 files changed

+101
-4
lines changed

awspub/common.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
from typing import Tuple
2+
3+
4+
def _split_partition(val: str) -> Tuple[str, str]:
5+
"""
6+
Split a string into partition and resource, separated by a colon. If no partition is given, assume "aws"
7+
:param val: the string to split
8+
:type val: str
9+
:return: the partition and the resource
10+
:rtype: Tuple[str, str]
11+
"""
12+
if ":" in val:
13+
partition, resource = val.split(":")
14+
else:
15+
# if no partition is given, assume default commercial partition "aws"
16+
partition = "aws"
17+
resource = val
18+
return partition, resource

awspub/configmodels.py

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
import pathlib
22
from typing import Dict, List, Literal, Optional
33

4-
from pydantic import BaseModel, ConfigDict, Field
4+
from pydantic import BaseModel, ConfigDict, Field, field_validator
5+
6+
from awspub.common import _split_partition
57

68

79
class ConfigS3Model(BaseModel):
@@ -124,7 +126,9 @@ class ConfigImageModel(BaseModel):
124126
)
125127
imds_support: Optional[Literal["v2.0"]] = Field(description="Optional IMDS support", default=None)
126128
share: Optional[List[str]] = Field(
127-
description="Optional list of account IDs the image and snapshot will be shared with", default=None
129+
description="Optional list of account IDs the image and snapshot will be shared with. The account"
130+
"ID can be prefixed with the partition and separated by ':'. Eg 'aws-cn:123456789123'",
131+
default=None,
128132
)
129133
temporary: Optional[bool] = Field(
130134
description="Optional boolean field indicates that a image is only temporary", default=False
@@ -145,6 +149,21 @@ class ConfigImageModel(BaseModel):
145149
groups: Optional[List[str]] = Field(description="Optional list of groups this image is part of", default=[])
146150
tags: Optional[Dict[str, str]] = Field(description="Optional Tags to apply to this image only", default={})
147151

152+
@field_validator("share")
153+
@classmethod
154+
def check_share(cls, v: Optional[List[str]]) -> Optional[List[str]]:
155+
"""
156+
Make sure the account IDs are valid and if given the partition is correct
157+
"""
158+
if v is not None:
159+
for val in v:
160+
partition, account_id = _split_partition(val)
161+
if len(account_id) != 12:
162+
raise ValueError("Account ID must be 12 characters long")
163+
if partition not in ["aws", "aws-cn", "aws-us-gov"]:
164+
raise ValueError("Partition must be one of 'aws', 'aws-cn', 'aws-us-gov'")
165+
return v
166+
148167

149168
class ConfigModel(BaseModel):
150169
"""

awspub/image.py

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
from mypy_boto3_ssm import SSMClient
1010

1111
from awspub import exceptions
12+
from awspub.common import _split_partition
1213
from awspub.context import Context
1314
from awspub.image_marketplace import ImageMarketplace
1415
from awspub.s3 import S3
@@ -159,16 +160,34 @@ def _tags(self):
159160
tags.append({"Key": name, "Value": value})
160161
return tags
161162

163+
def _share_list_filtered(self, share_conf: List[str]) -> List[Dict[str, str]]:
164+
"""
165+
Get a filtered list of share configurations based on the current partition
166+
:param share_conf: the share configuration
167+
:type share_conf: List[str]
168+
:return: a List of share configurations that is usable by modify_image_attribute()
169+
:rtype: List[Dict[str, str]]
170+
"""
171+
# the current partition
172+
partition_current = boto3.client("ec2").meta.partition
173+
174+
share_list: List[Dict[str, str]] = []
175+
for share in share_conf:
176+
partition, account_id = _split_partition(share)
177+
if partition == partition_current:
178+
share_list.append({"UserId": account_id})
179+
return share_list
180+
162181
def _share(self, share_conf: List[str], images: Dict[str, _ImageInfo]):
163182
"""
164183
Share images with accounts
165184
166-
:param share_conf: the share configuration. eg. self.conf["share_create"]
185+
:param share_conf: the share configuration containing list
167186
:type share_conf: List[str]
168187
:param images: a Dict with region names as keys and _ImageInfo objects as values
169188
:type images: Dict[str, _ImageInfo]
170189
"""
171-
share_list: List[Dict[str, str]] = [{"UserId": user_id} for user_id in share_conf]
190+
share_list = self._share_list_filtered(share_conf)
172191

173192
for region, image_info in images.items():
174193
ec2client: EC2Client = boto3.client("ec2", region_name=region)

awspub/tests/fixtures/config1.yaml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,11 @@ awspub:
7878
public: true
7979
tags:
8080
key1: value1
81+
share:
82+
- "123456789123"
83+
- "221020170000"
84+
- "aws:290620200000"
85+
- "aws-cn:334455667788"
8186
marketplace:
8287
entity_id: "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
8388
access_role_arn: "arn:aws:iam::xxxxxxxxxxxx:role/AWSMarketplaceAccess"

awspub/tests/test_common.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import pytest
2+
3+
from awspub.common import _split_partition
4+
5+
6+
@pytest.mark.parametrize(
7+
"input,expected_output",
8+
[
9+
("123456789123", ("aws", "123456789123")),
10+
("aws:123456789123", ("aws", "123456789123")),
11+
("aws-cn:123456789123", ("aws-cn", "123456789123")),
12+
("aws-us-gov:123456789123", ("aws-us-gov", "123456789123")),
13+
],
14+
)
15+
def test_common__split_partition(input, expected_output):
16+
assert _split_partition(input) == expected_output

awspub/tests/test_image.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -456,3 +456,23 @@ def test_image__verify(image_found, config, config_image_name, expected_problems
456456
img = image.Image(ctx, config_image_name)
457457
problems = img._verify("eu-central-1")
458458
assert problems == expected_problems
459+
460+
461+
@pytest.mark.parametrize(
462+
"partition,imagename,share_list_expected",
463+
[
464+
("aws", "test-image-8", [{"UserId": "123456789123"}, {"UserId": "221020170000"}, {"UserId": "290620200000"}]),
465+
("aws-cn", "test-image-8", [{"UserId": "334455667788"}]),
466+
("aws-us-gov", "test-image-8", []),
467+
],
468+
)
469+
def test_image__share_list_filtered(partition, imagename, share_list_expected):
470+
"""
471+
Test _share_list_filtered() for a given image
472+
"""
473+
with patch("boto3.client") as bclient_mock:
474+
instance = bclient_mock.return_value
475+
instance.meta.partition = partition
476+
ctx = context.Context(curdir / "fixtures/config1.yaml", None)
477+
img = image.Image(ctx, imagename)
478+
assert img._share_list_filtered(img.conf["share"]) == share_list_expected

0 commit comments

Comments
 (0)