Skip to content

Commit 2341f2f

Browse files
authored
fix: deduplicate principals in sagemaker-templates ModelPackageGroupPolicy (#397)
* fix: deduplicate principals in sagemaker-templates ModelPackageGroupPolicy Fixes "Invalid policy provided: Duplicate principal" error when dev, pre-prod, and prod account IDs resolve to the same AWS account in single-account deployments. * style: fix ruff formatting * style: fix ruff formatting for older version
1 parent a328411 commit 2341f2f

5 files changed

Lines changed: 109 additions & 20 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1111

1212
### **Changed**
1313

14+
- fixed duplicate principal error in `sagemaker-templates` module when dev, pre-prod, and prod account IDs resolve to the same AWS account
1415
- fixed `sagemaker-templates` Model Deploy seed code incorrectly granting S3 permissions to a ManagedPolicy instead of a Role, which caused deployment failures with CDK 2.174.0+
1516
- update qs to 6.14.1 via npm override to address security vulnerability
1617
- pin @cdklabs/generative-ai-cdk-constructs to 0.1.311 to fix build compatibility

modules/sagemaker/sagemaker-templates/templates/finetune_llm_evaluation/product_stack.py

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,9 @@ def __init__(
4848
pre_prod_account_id = Aws.ACCOUNT_ID if not pre_prod_account_id else pre_prod_account_id
4949
prod_account_id = Aws.ACCOUNT_ID if not prod_account_id else prod_account_id
5050

51+
# Deduplicate account IDs to avoid "Duplicate principal" errors in single-account deployments
52+
unique_account_ids = list(dict.fromkeys([dev_account_id, pre_prod_account_id, prod_account_id]))
53+
5154
Tags.of(self).add("sagemaker:project-id", sagemaker_project_id)
5255
Tags.of(self).add("sagemaker:project-name", sagemaker_project_name)
5356
if sagemaker_domain_id:
@@ -147,9 +150,8 @@ def __init__(
147150
)
148151
],
149152
principals=[
150-
iam.ArnPrincipal(f"arn:{Aws.PARTITION}:iam::{dev_account_id}:root"),
151-
iam.ArnPrincipal(f"arn:{Aws.PARTITION}:iam::{pre_prod_account_id}:root"),
152-
iam.ArnPrincipal(f"arn:{Aws.PARTITION}:iam::{prod_account_id}:root"),
153+
iam.ArnPrincipal(f"arn:{Aws.PARTITION}:iam::{account_id}:root")
154+
for account_id in unique_account_ids
153155
],
154156
),
155157
iam.PolicyStatement(
@@ -167,9 +169,8 @@ def __init__(
167169
)
168170
],
169171
principals=[
170-
iam.ArnPrincipal(f"arn:{Aws.PARTITION}:iam::{dev_account_id}:root"),
171-
iam.ArnPrincipal(f"arn:{Aws.PARTITION}:iam::{pre_prod_account_id}:root"),
172-
iam.ArnPrincipal(f"arn:{Aws.PARTITION}:iam::{prod_account_id}:root"),
172+
iam.ArnPrincipal(f"arn:{Aws.PARTITION}:iam::{account_id}:root")
173+
for account_id in unique_account_ids
173174
],
174175
),
175176
]

modules/sagemaker/sagemaker-templates/templates/hf_import_models/product_stack.py

Lines changed: 5 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,9 @@ def __init__(
4545
pre_prod_account_id = Aws.ACCOUNT_ID if not pre_prod_account_id else pre_prod_account_id
4646
prod_account_id = Aws.ACCOUNT_ID if not prod_account_id else prod_account_id
4747

48+
# Deduplicate account IDs to avoid "Duplicate principal" errors in single-account deployments
49+
unique_account_ids = list(dict.fromkeys([pre_prod_account_id, prod_account_id]))
50+
4851
Tags.of(self).add("sagemaker:project-id", sagemaker_project_id)
4952
Tags.of(self).add("sagemaker:project-name", sagemaker_project_name)
5053
if sagemaker_domain_id:
@@ -129,10 +132,7 @@ def __init__(
129132
resources=[
130133
f"arn:{Aws.PARTITION}:sagemaker:{Aws.REGION}:{Aws.ACCOUNT_ID}:model-package-group/{model_package_group_name}"
131134
],
132-
principals=[
133-
iam.AccountPrincipal(pre_prod_account_id),
134-
iam.AccountPrincipal(prod_account_id),
135-
],
135+
principals=[iam.AccountPrincipal(account_id) for account_id in unique_account_ids],
136136
),
137137
iam.PolicyStatement(
138138
sid="ModelPackage",
@@ -145,10 +145,7 @@ def __init__(
145145
resources=[
146146
f"arn:{Aws.PARTITION}:sagemaker:{Aws.REGION}:{Aws.ACCOUNT_ID}:model-package/{model_package_group_name}/*"
147147
],
148-
principals=[
149-
iam.AccountPrincipal(pre_prod_account_id),
150-
iam.AccountPrincipal(prod_account_id),
151-
],
148+
principals=[iam.AccountPrincipal(account_id) for account_id in unique_account_ids],
152149
),
153150
]
154151
).to_json()

modules/sagemaker/sagemaker-templates/templates/xgboost_abalone/product_stack.py

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,9 @@ def __init__(
5050
pre_prod_account_id = Aws.ACCOUNT_ID if not pre_prod_account_id else pre_prod_account_id
5151
prod_account_id = Aws.ACCOUNT_ID if not prod_account_id else prod_account_id
5252

53+
# Deduplicate account IDs to avoid "Duplicate principal" errors in single-account deployments
54+
unique_account_ids = list(dict.fromkeys([dev_account_id, pre_prod_account_id, prod_account_id]))
55+
5356
dev_vpc = None
5457
if dev_vpc_id:
5558
dev_vpc = ec2.Vpc.from_lookup(self, "dev-vpc", vpc_id=dev_vpc_id)
@@ -153,9 +156,8 @@ def __init__(
153156
)
154157
],
155158
principals=[
156-
iam.ArnPrincipal(f"arn:{Aws.PARTITION}:iam::{dev_account_id}:root"),
157-
iam.ArnPrincipal(f"arn:{Aws.PARTITION}:iam::{pre_prod_account_id}:root"),
158-
iam.ArnPrincipal(f"arn:{Aws.PARTITION}:iam::{prod_account_id}:root"),
159+
iam.ArnPrincipal(f"arn:{Aws.PARTITION}:iam::{account_id}:root")
160+
for account_id in unique_account_ids
159161
],
160162
),
161163
iam.PolicyStatement(
@@ -173,9 +175,8 @@ def __init__(
173175
)
174176
],
175177
principals=[
176-
iam.ArnPrincipal(f"arn:{Aws.PARTITION}:iam::{dev_account_id}:root"),
177-
iam.ArnPrincipal(f"arn:{Aws.PARTITION}:iam::{pre_prod_account_id}:root"),
178-
iam.ArnPrincipal(f"arn:{Aws.PARTITION}:iam::{prod_account_id}:root"),
178+
iam.ArnPrincipal(f"arn:{Aws.PARTITION}:iam::{account_id}:root")
179+
for account_id in unique_account_ids
179180
],
180181
),
181182
]

modules/sagemaker/sagemaker-templates/tests/test_stack.py

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -157,3 +157,92 @@ def test_no_cdk_nag_errors(stack: cdk.Stack, project_template_type) -> None:
157157
Match.string_like_regexp(r"AwsSolutions-.*"),
158158
)
159159
assert not nag_errors, f"Found {len(nag_errors)} CDK nag errors"
160+
161+
162+
@pytest.fixture(scope="function")
163+
def stack_single_account(stack_defaults, project_template_type) -> cdk.Stack:
164+
import stack
165+
166+
app = cdk.App()
167+
same_account_id = os.environ["CDK_DEFAULT_ACCOUNT"]
168+
sagemaker_domain_id = "domain_id"
169+
170+
xgboost_abalone_settings = None
171+
hf_import_models_settings = None
172+
173+
if project_template_type == ProjectTemplateType.XGBOOST_ABALONE:
174+
xgboost_abalone_settings = XGBoostAbaloneProjectSettings(
175+
enable_network_isolation="False", encrypt_inter_container_traffic="False"
176+
)
177+
elif project_template_type == ProjectTemplateType.HF_IMPORT_MODELS:
178+
hf_import_models_settings = HfImportModelsProjectSettings(
179+
hf_access_token_secret="test-hf-token-secret", hf_model_id="test-model-id"
180+
)
181+
182+
return stack.ProjectStack(
183+
app,
184+
"test-single-account-stack",
185+
project_template_type=project_template_type,
186+
sagemaker_project_name="test-project",
187+
sagemaker_project_id="test-project-id",
188+
dev_vpc_id="",
189+
dev_subnet_ids=[],
190+
dev_security_group_ids=[],
191+
pre_prod_account_id=same_account_id,
192+
pre_prod_region="us-east-1",
193+
pre_prod_vpc_id="",
194+
pre_prod_subnet_ids=[],
195+
pre_prod_security_group_ids=[],
196+
prod_account_id=same_account_id,
197+
prod_region="us-east-1",
198+
prod_vpc_id="",
199+
prod_subnet_ids=[],
200+
prod_security_group_ids=[],
201+
env=cdk.Environment(
202+
account=same_account_id,
203+
region=os.environ["CDK_DEFAULT_REGION"],
204+
),
205+
sagemaker_domain_id=sagemaker_domain_id,
206+
sagemaker_domain_arn=f"arn:aws:sagemaker:::domain/{sagemaker_domain_id}",
207+
repository_type=RepositoryType.CODECOMMIT,
208+
access_token_secret_name=None,
209+
aws_codeconnection_arn=None,
210+
repository_owner=None,
211+
xgboost_abalone_project_settings=xgboost_abalone_settings,
212+
model_deploy_project_settings=None,
213+
hf_import_models_project_settings=hf_import_models_settings,
214+
batch_inference_project_settings=None,
215+
)
216+
217+
218+
@pytest.mark.parametrize(
219+
"project_template_type",
220+
[
221+
ProjectTemplateType.XGBOOST_ABALONE,
222+
ProjectTemplateType.FINETUNE_LLM_EVALUATION,
223+
ProjectTemplateType.HF_IMPORT_MODELS,
224+
],
225+
indirect=True,
226+
)
227+
def test_no_duplicate_principals_in_model_package_group_policy(
228+
stack_single_account: cdk.Stack, project_template_type
229+
) -> None:
230+
import json
231+
232+
from aws_cdk.assertions import Template
233+
234+
template = Template.from_stack(stack_single_account)
235+
model_package_groups = template.find_resources("AWS::SageMaker::ModelPackageGroup")
236+
237+
for logical_id, resource in model_package_groups.items():
238+
policy = resource.get("Properties", {}).get("ModelPackageGroupPolicy")
239+
if policy and isinstance(policy, str):
240+
policy_doc = json.loads(policy)
241+
for statement in policy_doc.get("Statement", []):
242+
principal = statement.get("Principal", {})
243+
if isinstance(principal, dict):
244+
aws_principals = principal.get("AWS", [])
245+
if isinstance(aws_principals, list):
246+
assert len(aws_principals) == len(
247+
set(aws_principals)
248+
), f"Duplicate principals in {logical_id}: {aws_principals}"

0 commit comments

Comments
 (0)