Repository: getmoto/moto
Pull request #7953 (2024-08-09 07:25:05)

Fix issues for non-default partitions in elastictranscoder, sagemaker, and SSM
## Motivation
During extensive testing, I found some more errors when accepting / generating ARNs for partitions other than `aws` in moto.

## Changes
* Accept role ARNs from non-default partitions in elastictranscoder
* Accept cluster ARNs from non-default partitions in sagemaker
* Properly generate the SSM prefix, to strip it correctly in the `get_parameter` operation.

codecov[bot]: ## [Codecov](https://app.codecov.io/gh/getmoto/moto/pull/7953?dropdown=coverage&src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=getmoto) Report
All modified and coverable lines are covered by tests :white_check_mark:
> Project coverage is 94.36%. Comparing base [(`560d556`)](https://app.codecov.io/gh/getmoto/moto/commit/560d55605020a2e3b1333b355a6c7d7240ea7311?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=getmoto) to head [(`fe7fabb`)](https://app.codecov.io/gh/getmoto/moto/commit/fe7fabbf26b0e6d98942060d33817bdf2b183138?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=getmoto).

<details><summary>Additional details and impacted files</summary>


```diff
@@           Coverage Diff           @@
##           master    #7953   +/-   ##
=======================================
  Coverage   94.36%   94.36%           
=======================================
  Files        1120     1120           
  Lines       96028    96029    +1     
=======================================
+ Hits        90614    90615    +1     
  Misses       5414     5414           
```

| [Flag](https://app.codecov.io/gh/getmoto/moto/pull/7953/flags?src=pr&el=flags&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=getmoto) | Coverage Δ | |
|---|---|---|
| [servertests](https://app.codecov.io/gh/getmoto/moto/pull/7953/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=getmoto) | `28.95% <25.00%> (+<0.01%)` | :arrow_up: |
| [unittests](https://app.codecov.io/gh/getmoto/moto/pull/7953/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=getmoto) | `94.33% <100.00%> (+<0.01%)` | :arrow_up: |

Flags with carried forward coverage won't be shown. [Click here](https://docs.codecov.io/docs/carryforward-flags?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=getmoto#carryforward-flags-in-the-pull-request-comment) to find out more.

</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/getmoto/moto/pull/7953?dropdown=coverage&src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=getmoto).   
:loudspeaker: Have feedback on the report? [Share it here](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=getmoto).


Patch:
diff --git a/docs/docs/multi_account.rst b/docs/docs/multi_account.rst
index 31a873b3a..4fcf790d0 100644
--- a/docs/docs/multi_account.rst
+++ b/docs/docs/multi_account.rst
@@ -131,3 +131,8 @@ Things get interesting when assuming a role within a different account.
 
 Because we've assumed a role in a different account, no buckets were found. The `foobar`-bucket only exists in the default account, not in `111111111111`.
 
+.. note::
+
+    Be aware, however, that roles assumed in different accounts through this approach will still have full access to actions
+    on all buckets created, in addition to other AWS permissions. To learn more about simulating access controls, see
+    `IAM-like Access Control <./iam.rst>`_.
diff --git a/moto/core/responses.py b/moto/core/responses.py
index 8a171ffd7..4b0ae6689 100644
--- a/moto/core/responses.py
+++ b/moto/core/responses.py
@@ -1139,6 +1139,8 @@ def to_str(value: Any, spec: Dict[str, Any]) -> str:
         return utcfromtimestamp(value).replace(tzinfo=datetime.timezone.utc).isoformat()
     elif vtype == "string":
         return str(value)
+    elif vtype == "structure":
+        return ""
     elif value is None:
         return "null"
     else:
diff --git a/moto/elastictranscoder/responses.py b/moto/elastictranscoder/responses.py
index 99fc116c6..50e7588cc 100644
--- a/moto/elastictranscoder/responses.py
+++ b/moto/elastictranscoder/responses.py
@@ -4,6 +4,7 @@ from typing import Any, Optional
 
 from moto.core.common_types import TYPE_RESPONSE
 from moto.core.responses import BaseResponse
+from moto.utilities.utils import ARN_PARTITION_REGEX
 
 from .models import ElasticTranscoderBackend, elastictranscoder_backends
 
@@ -45,7 +46,7 @@ class ElasticTranscoderResponse(BaseResponse):
         thumbnail_config = self._get_param("ThumbnailConfig")
         if not role:
             return self.err("Role cannot be blank")
-        if not re.match("^arn:aws:iam::[0-9]+:role/.+", role):
+        if not re.match(f"{ARN_PARTITION_REGEX}:iam::[0-9]+:role/.+", role):
             return self.err(f"Role ARN is invalid: {role}")
         if not output_bucket and not content_config:
             return self.err(
diff --git a/moto/emr/models.py b/moto/emr/models.py
index 1e1381878..fc14a121a 100644
--- a/moto/emr/models.py
+++ b/moto/emr/models.py
@@ -556,6 +556,7 @@ class ElasticMapReduceBackend(BaseBackend):
         self.clusters: Dict[str, FakeCluster] = {}
         self.instance_groups: Dict[str, FakeInstanceGroup] = {}
         self.security_configurations: Dict[str, FakeSecurityConfiguration] = {}
+        self.block_public_access_configuration: Dict[str, Any] = {}  # type: ignore [misc]
 
     @property
     def ec2_backend(self) -> Any:  # type: ignore[misc]
@@ -885,5 +886,39 @@ class ElasticMapReduceBackend(BaseBackend):
             )
         del self.security_configurations[name]
 
+    def get_block_public_access_configuration(
+        self,
+    ) -> Dict[str, Any]:  # type ignore[misc]
+        return self.block_public_access_configuration
+
+    def put_block_public_access_configuration(
+        self,
+        block_public_security_group_rules: bool,
+        rule_ranges: Optional[List[Dict[str, int]]],
+    ) -> None:
+        from moto.sts import sts_backends
+
+        sts_backend = sts_backends[self.account_id]["global"]
+        _, user_arn, _ = sts_backend.get_caller_identity(
+            self.account_id, region=self.region_name
+        )
+        self.block_public_access_configuration = {
+            "block_public_access_configuration": {
+                "block_public_security_group_rules": block_public_security_group_rules,
+                "permitted_public_security_group_rule_ranges": [
+                    {
+                        "min_range": rule_range.get("MinRange"),
+                        "max_range": rule_range.get("MaxRange"),
+                    }
+                    for rule_range in rule_ranges or []
+                ],
+            },
+            "block_public_access_configuration_metadata": {
+                "creation_date_time": datetime.now(),
+                "created_by_arn": user_arn,
+            },
+        }
+        return
+
 
 emr_backends = BackendDict(ElasticMapReduceBackend, "emr")
diff --git a/moto/emr/responses.py b/moto/emr/responses.py
index 52688e171..c1618455a 100644
--- a/moto/emr/responses.py
+++ b/moto/emr/responses.py
@@ -535,6 +535,45 @@ class ElasticMapReduceResponse(BaseResponse):
         template = self.response_template(REMOVE_AUTO_SCALING_POLICY)
         return template.render()
 
+    @generate_boto3_response("GetBlockPublicAccessConfiguration")
+    def get_block_public_access_configuration(self) -> str:
+        configuration = self.backend.get_block_public_access_configuration()
+        config = configuration.get("block_public_access_configuration") or {}
+        metadata = configuration.get("block_public_access_configuration_metadata") or {}
+        template = self.response_template(
+            GET_BLOCK_PUBLIC_ACCESS_CONFIGURATION_TEMPLATE
+        )
+        return template.render(
+            block_public_security_group_rules=config.get(
+                "block_public_security_group_rules"
+            ),
+            permitted_public_security_group_rule_ranges=config.get(
+                "permitted_public_security_group_rule_ranges"
+            ),
+            creation_date_time=metadata.get("creation_date_time"),
+            created_by_arn=metadata.get("created_by_arn"),
+        )
+
+    @generate_boto3_response("PutBlockPublicAccessConfiguration")
+    def put_block_public_access_configuration(self) -> str:
+        params = self._get_params()
+        block_public_access_configuration = (
+            params.get("BlockPublicAccessConfiguration") or {}
+        )
+        self.backend.put_block_public_access_configuration(
+            block_public_security_group_rules=block_public_access_configuration.get(
+                "BlockPublicSecurityGroupRules"
+            )
+            or True,
+            rule_ranges=block_public_access_configuration.get(
+                "PermittedPublicSecurityGroupRuleRanges"
+            ),
+        )
+        template = self.response_template(
+            PUT_BLOCK_PUBLIC_ACCESS_CONFIGURATION_TEMPLATE
+        )
+        return template.render()
+
 
 ADD_INSTANCE_GROUPS_TEMPLATE = """<AddInstanceGroupsResponse xmlns="http://elasticmapreduce.amazonaws.com/doc/2009-03-31">
   <AddInstanceGroupsResult>
@@ -1418,3 +1457,36 @@ DELETE_SECURITY_CONFIGURATION_TEMPLATE = """<DeleteSecurityConfigurationResponse
     <RequestId>2690d7eb-ed86-11dd-9877-6fad448a8419</RequestId>
   </ResponseMetadata>
 </DeleteSecurityConfigurationResponse>"""
+
+PUT_BLOCK_PUBLIC_ACCESS_CONFIGURATION_TEMPLATE = """<PutBlockPublicAccessConfigurationResponse xmlns="http://elasticmapreduce.amazonaws.com/doc/2009-03-31">
+  <ResponseMetadata>
+    <RequestId>2690d7eb-ed86-11dd-9877-6fad448a8419</RequestId>
+  </ResponseMetadata>
+</PutBlockPublicAccessConfigurationResponse>"""
+
+GET_BLOCK_PUBLIC_ACCESS_CONFIGURATION_TEMPLATE = """
+  <GetBlockPublicAccessConfigurationResponse xmlns="http://elasticmapreduce.amazonaws.com/doc/2009-03-31">
+    <GetBlockPublicAccessConfigurationResult>
+      <BlockPublicAccessConfiguration>
+        <BlockPublicSecurityGroupRules>
+          {{block_public_security_group_rules}}
+        </BlockPublicSecurityGroupRules>
+        <PermittedPublicSecurityGroupRuleRanges>
+          {% for rule_range in permitted_public_security_group_rule_ranges %}
+            <member>
+              <MinRange>{{rule_range['min_range']}}</MinRange>
+              <MaxRange>{{rule_range['max_range']}}</MaxRange>      
+            </member>
+          {% endfor %}
+        </PermittedPublicSecurityGroupRuleRanges>
+      </BlockPublicAccessConfiguration>
+      <BlockPublicAccessConfigurationMetadata>
+        <CreationDateTime>{{creation_date_time}}</CreationDateTime>
+        <CreatedByArn>{{created_by_arn}}</CreatedByArn>
+      </BlockPublicAccessConfigurationMetadata>
+    </GetBlockPublicAccessConfigurationResult>
+    <ResponseMetadata>
+      <RequestId>2690d7eb-ed86-11dd-9877-6fad448a8419</RequestId>
+    </ResponseMetadata>
+  </GetBlockPublicAccessConfigurationResponse>
+"""
diff --git a/moto/sagemaker/models.py b/moto/sagemaker/models.py
index 281af4c82..1090d5702 100644
--- a/moto/sagemaker/models.py
+++ b/moto/sagemaker/models.py
@@ -4764,7 +4764,7 @@ class SageMakerModelBackend(BaseBackend):
         return cluster.arn
 
     def describe_cluster(self, cluster_name: str) -> Dict[str, Any]:
-        if cluster_name.startswith("arn:aws:sagemaker:"):
+        if cluster_name.startswith(f"arn:{self.partition}:sagemaker:"):
             cluster_name = (cluster_name.split(":")[-1]).split("/")[-1]
         cluster = self.clusters.get(cluster_name)
         if not cluster:
@@ -4772,7 +4772,7 @@ class SageMakerModelBackend(BaseBackend):
         return cluster.describe()
 
     def delete_cluster(self, cluster_name: str) -> str:
-        if cluster_name.startswith("arn:aws:sagemaker:"):
+        if cluster_name.startswith(f"arn:{self.partition}:sagemaker:"):
             cluster_name = (cluster_name.split(":")[-1]).split("/")[-1]
         cluster = self.clusters.get(cluster_name)
         if not cluster:
@@ -4783,7 +4783,7 @@ class SageMakerModelBackend(BaseBackend):
         return arn
 
     def describe_cluster_node(self, cluster_name: str, node_id: str) -> Dict[str, Any]:
-        if cluster_name.startswith("arn:aws:sagemaker:"):
+        if cluster_name.startswith(f"arn:{self.partition}:sagemaker:"):
             cluster_name = (cluster_name.split(":")[-1]).split("/")[-1]
         cluster = self.clusters.get(cluster_name)
         if not cluster:
@@ -4832,7 +4832,7 @@ class SageMakerModelBackend(BaseBackend):
         sort_by: Optional[str],
         sort_order: Optional[str],
     ) -> List[ClusterNode]:
-        if cluster_name.startswith("arn:aws:sagemaker:"):
+        if cluster_name.startswith(f"arn:{self.partition}:sagemaker:"):
             cluster_name = (cluster_name.split(":")[-1]).split("/")[-1]
         cluster = self.clusters.get(cluster_name)
         if not cluster:
@@ -6052,9 +6052,7 @@ class FakeModelBiasJobDefinition(BaseObject):
 
     @staticmethod
     def arn_formatter(name: str, account_id: str, region: str) -> str:
-        return (
-            f"arn:aws:sagemaker:{region}:{account_id}:model-bias-job-definition/{name}"
-        )
+        return f"arn:{get_partition(region)}:sagemaker:{region}:{account_id}:model-bias-job-definition/{name}"
 
     @property
     def summary_object(self) -> Dict[str, str]:
diff --git a/moto/ssm/models.py b/moto/ssm/models.py
index bde58b117..9d114696b 100644
--- a/moto/ssm/models.py
+++ b/moto/ssm/models.py
@@ -1187,7 +1187,9 @@ class SimpleSystemManagerBackend(BaseBackend):
 
         self.windows: Dict[str, FakeMaintenanceWindow] = dict()
         self.baselines: Dict[str, FakePatchBaseline] = dict()
-        self.ssm_prefix = f"arn:aws:ssm:{self.region_name}:{self.account_id}:parameter"
+        self.ssm_prefix = (
+            f"arn:{self.partition}:ssm:{self.region_name}:{self.account_id}:parameter"
+        )
 
     def _generate_document_information(
         self, ssm_document: Document, document_format: str


Test patch:
diff --git a/tests/test_emr/test_emr.py b/tests/test_emr/test_emr.py
new file mode 100644
index 000000000..aefdcac10
--- /dev/null
+++ b/tests/test_emr/test_emr.py
@@ -0,0 +1,53 @@
+"""Unit tests for emr-supported APIs."""
+
+from datetime import datetime
+
+import boto3
+import pytest
+
+from moto import mock_aws
+
+# See our Development Tips on writing tests for hints on how to write good tests:
+# http://docs.getmoto.org/en/latest/docs/contributing/development_tips/tests.html
+
+
+@pytest.fixture(name="client")
+def fixture_emr_client():
+    with mock_aws():
+        yield boto3.client("emr", region_name="us-east-2")
+
+
+@mock_aws
+def test_put_and_get_block_public_access_configuration(client):
+    client.put_block_public_access_configuration(
+        BlockPublicAccessConfiguration={
+            "BlockPublicSecurityGroupRules": True,
+            "PermittedPublicSecurityGroupRuleRanges": [
+                {"MinRange": 22, "MaxRange": 443},
+            ],
+        }
+    )
+
+    connection = client.get_block_public_access_configuration()
+    assert connection["BlockPublicAccessConfiguration"]["BlockPublicSecurityGroupRules"]
+    assert (
+        connection["BlockPublicAccessConfiguration"][
+            "PermittedPublicSecurityGroupRuleRanges"
+        ][0]["MinRange"]
+        == 22
+    )
+    assert (
+        connection["BlockPublicAccessConfiguration"][
+            "PermittedPublicSecurityGroupRuleRanges"
+        ][0]["MaxRange"]
+        == 443
+    )
+    assert isinstance(
+        connection["BlockPublicAccessConfigurationMetadata"]["CreatedByArn"], str
+    )
+
+    # botocore automatically parses timestamps into datetimes
+    assert isinstance(
+        connection["BlockPublicAccessConfigurationMetadata"]["CreationDateTime"],
+        datetime,
+    )