Skip to content

Commit 463585d

Browse files
committed
update deny policy sid + fix integ tests after refactor
1 parent 6f7b56c commit 463585d

File tree

3 files changed

+61
-91
lines changed

3 files changed

+61
-91
lines changed

sagemaker-mlops/src/sagemaker/mlops/feature_store/feature_group_manager.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -158,9 +158,11 @@ def _generate_s3_deny_statements(
158158
if caller_arn:
159159
allowed_principals.append(caller_arn)
160160

161+
sid_suffix = s3_prefix.rstrip("/").rsplit("/", 1)[-1]
162+
161163
return [
162164
{
163-
"Sid": "DenyAllAccessToFeatureStorePrefixExceptAllowedPrincipals",
165+
"Sid": f"DenyFSObjectAccess_{sid_suffix}",
164166
"Effect": "Deny",
165167
"Principal": "*",
166168
"Action": ["s3:GetObject", "s3:PutObject", "s3:DeleteObject"],
@@ -170,7 +172,7 @@ def _generate_s3_deny_statements(
170172
},
171173
},
172174
{
173-
"Sid": "DenyListOnPrefixExceptAllowedPrincipals",
175+
"Sid": f"DenyFSListAccess_{sid_suffix}",
174176
"Effect": "Deny",
175177
"Principal": "*",
176178
"Action": "s3:ListBucket",
@@ -472,7 +474,6 @@ def _apply_bucket_policy(
472474

473475
policy["Statement"].extend(new_statements)
474476
s3_client.put_bucket_policy(Bucket=bucket_name, Policy=json.dumps(policy))
475-
# TODO: Remove verbose policy logging
476477
logger.info(f"Applied bucket policy: {json.dumps(policy, indent=2)}")
477478
return True
478479

sagemaker-mlops/tests/integ/test_featureStore_lakeformation.py

Lines changed: 51 additions & 82 deletions
Original file line numberDiff line numberDiff line change
@@ -472,19 +472,18 @@ def test_enable_lake_formation_fails_with_nonexistent_role(
472472

473473
@pytest.mark.serial
474474
@pytest.mark.slow_test
475-
def test_enable_lake_formation_full_flow_with_policy_output(s3_uri, role, region, caplog):
475+
def test_enable_lake_formation_full_flow_with_policy_output(s3_uri, role, region):
476476
"""
477-
Test the full Lake Formation flow with S3 deny policy output.
477+
Test the full Lake Formation flow with S3 deny policy application.
478478
479479
This test verifies:
480480
1. Creates a FeatureGroupManager with offline store
481-
2. Enables Lake Formation with show_s3_policy=True
482-
3. Verifies all Lake Formation phases complete successfully
483-
4. Verifies the S3 deny policy is logged
484-
5. Verifies the policy structure contains expected elements
485-
486-
This validates Requirements 6.1-6.9 from the design document.
481+
2. Enables Lake Formation (bucket policy is applied automatically)
482+
3. Verifies all Lake Formation phases complete successfully (including bucket policy)
483+
4. Fetches the actual bucket policy and verifies its structure
487484
"""
485+
import json
486+
488487
fg_name = generate_feature_group_name()
489488
fg = None
490489

@@ -497,48 +496,25 @@ def test_enable_lake_formation_full_flow_with_policy_output(s3_uri, role, region
497496
fg.wait_for_status(target_status="Created", poll=30, timeout=300)
498497
assert fg.feature_group_status == "Created"
499498

500-
# Enable Lake Formation governance with policy output
501-
with caplog.at_level(logging.INFO, logger="sagemaker.mlops.feature_store.feature_group_manager"):
502-
result = fg.enable_lake_formation(show_s3_policy=True)
499+
# Enable Lake Formation governance
500+
result = fg.enable_lake_formation()
503501

504502
# Verify all phases completed successfully
505503
assert result["s3_registration"] is True
506504
assert result["permissions_granted"] is True
507505
assert result["iam_principal_revoked"] is True
506+
assert result["bucket_policy_applied"] is True
508507

509-
output = caplog.text
510-
511-
# Verify the policy header is logged
512-
assert "S3 Bucket Policy Update recommended" in output
513-
# Verify bucket information is logged
514-
# Extract bucket name from s3_uri (s3://bucket/path -> bucket)
515-
expected_bucket = s3_uri.replace("s3://", "").split("/")[0]
516-
assert f"Bucket: {expected_bucket}" in output
517-
518-
# Verify policy structure elements are present
519-
assert '"Version": "2012-10-17"' in output
520-
assert '"Statement"' in output
521-
assert '"Effect": "Deny"' in output
522-
assert '"Principal": "*"' in output
523-
524-
# Verify the deny actions are present
525-
assert "s3:GetObject" in output
526-
assert "s3:PutObject" in output
527-
assert "s3:DeleteObject" in output
528-
assert "s3:ListBucket" in output
529-
530-
# Verify the condition structure is present
531-
assert "StringNotEquals" in output
532-
assert "aws:PrincipalArn" in output
533-
534-
# Verify the role ARN is in the allowed principals
535-
assert role in output
536-
537-
# Verify the service-linked role pattern is present (default use_service_linked_role=True)
538-
assert "aws-service-role/lakeformation.amazonaws.com/AWSServiceRoleForLakeFormationDataAccess" in output
508+
# Fetch the actual bucket policy from S3 and verify the role is allowed
509+
bucket_name = s3_uri.replace("s3://", "").split("/")[0]
510+
s3_client = boto3.client("s3")
511+
policy = json.loads(s3_client.get_bucket_policy(Bucket=bucket_name)["Policy"])
539512

540-
# Verify instructions are logged
541-
assert "Merge this with your existing bucket policy" in output
513+
# Find a DenyFS statement and verify the execution role is in allowed principals
514+
deny_stmts = [s for s in policy["Statement"] if s.get("Sid", "").startswith("DenyFS")]
515+
assert len(deny_stmts) >= 1
516+
allowed = deny_stmts[0]["Condition"]["StringNotEquals"]["aws:PrincipalArn"]
517+
assert role in allowed
542518

543519
finally:
544520
# Cleanup
@@ -548,17 +524,14 @@ def test_enable_lake_formation_full_flow_with_policy_output(s3_uri, role, region
548524

549525
@pytest.mark.serial
550526
@pytest.mark.slow_test
551-
def test_enable_lake_formation_no_policy_output_by_default(s3_uri, role, region, caplog):
527+
def test_enable_lake_formation_default_applies_bucket_policy(s3_uri, role, region, caplog):
552528
"""
553-
Test that S3 deny policy is NOT logged when show_s3_policy=False (default).
529+
Test that bucket policy is applied automatically with default arguments.
554530
555531
This test verifies:
556532
1. Creates a FeatureGroupManager with offline store
557-
2. Enables Lake Formation without show_s3_policy (defaults to False)
558-
3. Verifies all Lake Formation phases complete successfully
559-
4. Verifies the S3 deny policy is NOT logged
560-
561-
This validates Requirement 6.2 from the design document.
533+
2. Enables Lake Formation with default arguments
534+
3. Verifies all four phases complete successfully (including bucket policy)
562535
"""
563536
fg_name = generate_feature_group_name()
564537
fg = None
@@ -572,21 +545,15 @@ def test_enable_lake_formation_no_policy_output_by_default(s3_uri, role, region,
572545
fg.wait_for_status(target_status="Created", poll=30, timeout=300)
573546
assert fg.feature_group_status == "Created"
574547

575-
# Enable Lake Formation governance WITHOUT policy output (default)
548+
# Enable Lake Formation governance with defaults
576549
with caplog.at_level(logging.INFO, logger="sagemaker.mlops.feature_store.feature_group_manager"):
577550
result = fg.enable_lake_formation()
578551

579552
# Verify all phases completed successfully
580553
assert result["s3_registration"] is True
581554
assert result["permissions_granted"] is True
582555
assert result["iam_principal_revoked"] is True
583-
584-
output = caplog.text
585-
586-
# Verify the policy is NOT logged
587-
assert "S3 Bucket Policy Update recommended" not in output
588-
assert '"Version": "2012-10-17"' not in output
589-
assert "s3:GetObject" not in output
556+
assert result["bucket_policy_applied"] is True
590557

591558
finally:
592559
# Cleanup
@@ -596,20 +563,17 @@ def test_enable_lake_formation_no_policy_output_by_default(s3_uri, role, region,
596563

597564
@pytest.mark.serial
598565
@pytest.mark.slow_test
599-
def test_enable_lake_formation_with_custom_role_policy_output(s3_uri, role, region, caplog):
566+
def test_enable_lake_formation_with_custom_role_policy_output(s3_uri, role, region):
600567
"""
601-
Test the full Lake Formation flow with custom registration role and policy output.
568+
Test the full Lake Formation flow with custom registration role.
602569
603570
This test verifies:
604571
1. Creates a FeatureGroupManager with offline store
605572
2. Enables Lake Formation with use_service_linked_role=False and a custom registration_role_arn
606-
3. Verifies the S3 deny policy uses the custom role ARN instead of service-linked role
607-
608-
This validates Requirements 6.4, 6.5 from the design document.
609-
610-
Note: This test uses the same execution role as the registration role for simplicity.
611-
In production, these would typically be different roles.
573+
3. Fetches the actual bucket policy and verifies the custom role ARN is in the allowed principals
612574
"""
575+
import json
576+
613577
fg_name = generate_feature_group_name()
614578
fg = None
615579

@@ -622,31 +586,36 @@ def test_enable_lake_formation_with_custom_role_policy_output(s3_uri, role, regi
622586
fg.wait_for_status(target_status="Created", poll=30, timeout=300)
623587
assert fg.feature_group_status == "Created"
624588

625-
# Enable Lake Formation with custom registration role and policy output
626-
# Using the same role for both execution and registration for test simplicity
627-
with caplog.at_level(logging.INFO, logger="sagemaker.mlops.feature_store.feature_group_manager"):
628-
result = fg.enable_lake_formation(
629-
use_service_linked_role=False,
630-
registration_role_arn=role,
631-
show_s3_policy=True,
632-
)
589+
# Enable Lake Formation with custom registration role
590+
result = fg.enable_lake_formation(
591+
use_service_linked_role=False,
592+
registration_role_arn=role,
593+
)
633594

634595
# Verify all phases completed successfully
635596
assert result["s3_registration"] is True
636597
assert result["permissions_granted"] is True
637598
assert result["iam_principal_revoked"] is True
599+
assert result["bucket_policy_applied"] is True
638600

639-
output = caplog.text
601+
# Fetch the actual bucket policy from S3 and verify the role is in allowed principals
602+
bucket_name = s3_uri.replace("s3://", "").split("/")[0]
603+
s3_client = boto3.client("s3")
604+
policy = json.loads(s3_client.get_bucket_policy(Bucket=bucket_name)["Policy"])
640605

641-
# Verify the policy header is logged
642-
assert "S3 Bucket Policy Update recommended" in output
606+
# Find the deny statements for this feature group's prefix
607+
fg.refresh()
608+
s3_resolved = fg.offline_store_config.s3_storage_config.resolved_output_s3_uri
609+
fg_prefix = s3_resolved.replace(f"s3://{bucket_name}/", "").rstrip("/")
610+
sid_suffix = fg_prefix.rsplit("/", 1)[-1]
643611

644-
# Verify the custom role ARN is used in the policy (appears twice - once for each principal)
645-
# The role should appear as both the Lake Formation role and the Feature Store role
646-
assert output.count(role) >= 2
612+
object_sid = f"DenyFSObjectAccess_{sid_suffix}"
613+
deny_stmts = [s for s in policy["Statement"] if s.get("Sid") == object_sid]
614+
assert len(deny_stmts) == 1
647615

648-
# Verify the service-linked role is NOT used
649-
assert "aws-service-role/lakeformation.amazonaws.com/AWSServiceRoleForLakeFormationDataAccess" not in output
616+
# Verify the custom role ARN is in the allowed principals
617+
allowed = deny_stmts[0]["Condition"]["StringNotEquals"]["aws:PrincipalArn"]
618+
assert role in allowed
650619

651620
finally:
652621
# Cleanup

sagemaker-mlops/tests/unit/sagemaker/mlops/feature_store/test_feature_group_manager.py

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1583,14 +1583,14 @@ def test_policy_structure_validation(self):
15831583
assert len(statements) == 2
15841584

15851585
object_statement = statements[0]
1586-
assert object_statement["Sid"] == "DenyAllAccessToFeatureStorePrefixExceptAllowedPrincipals"
1586+
assert object_statement["Sid"] == "DenyFSObjectAccess_prefix"
15871587
assert object_statement["Effect"] == "Deny"
15881588
assert object_statement["Principal"] == "*"
15891589
assert "Condition" in object_statement
15901590
assert "StringNotEquals" in object_statement["Condition"]
15911591

15921592
list_statement = statements[1]
1593-
assert list_statement["Sid"] == "DenyListOnPrefixExceptAllowedPrincipals"
1593+
assert list_statement["Sid"] == "DenyFSListAccess_prefix"
15941594
assert list_statement["Effect"] == "Deny"
15951595
assert list_statement["Principal"] == "*"
15961596
assert "Condition" in list_statement
@@ -1696,8 +1696,8 @@ def test_sids_already_exist_skips_put(self):
16961696
existing_policy = {
16971697
"Version": "2012-10-17",
16981698
"Statement": [
1699-
{"Sid": "DenyAllAccessToFeatureStorePrefixExceptAllowedPrincipals", "Effect": "Deny"},
1700-
{"Sid": "DenyListOnPrefixExceptAllowedPrincipals", "Effect": "Deny"},
1699+
{"Sid": "DenyFSObjectAccess_prefix", "Effect": "Deny"},
1700+
{"Sid": "DenyFSListAccess_prefix", "Effect": "Deny"},
17011701
]
17021702
}
17031703
self.mock_s3_client.get_bucket_policy.return_value = {"Policy": _json.dumps(existing_policy)}
@@ -1753,7 +1753,7 @@ def test_partial_sid_overlap_appends_only_missing(self):
17531753
existing_policy = {
17541754
"Version": "2012-10-17",
17551755
"Statement": [
1756-
{"Sid": "DenyAllAccessToFeatureStorePrefixExceptAllowedPrincipals", "Effect": "Deny"},
1756+
{"Sid": "DenyFSObjectAccess_prefix", "Effect": "Deny"},
17571757
]
17581758
}
17591759
self.mock_s3_client.get_bucket_policy.return_value = {"Policy": _json.dumps(existing_policy)}
@@ -1769,7 +1769,7 @@ def test_partial_sid_overlap_appends_only_missing(self):
17691769
policy = _json.loads(put_args[1]["Policy"])
17701770
assert len(policy["Statement"]) == 2 # 1 existing + 1 new
17711771
sids = [s["Sid"] for s in policy["Statement"]]
1772-
assert "DenyListOnPrefixExceptAllowedPrincipals" in sids
1772+
assert "DenyFSListAccess_prefix" in sids
17731773

17741774

17751775
class TestEnableLakeFormationServiceLinkedRoleInPolicy:

0 commit comments

Comments
 (0)