Repository: getmoto/moto
Pull request #6930 (2023-10-20 09:12:25)

IoT: Fix bad account IDs in ARNs
Certain IoT resource types had bad hardcoded account ID (`1`) in their ARNs, like so:

```
arn:aws:iot:us-east-1:1:job/asdf123
```

This PR ensures that the ARNs have the proper account IDs.

The resource types are:
- ThingType
- ThingGroup
- Job
- Rule
- DomainConfiguration


Patch:
diff --git a/moto/iot/models.py b/moto/iot/models.py
index 9ef417831..9696784a6 100644
--- a/moto/iot/models.py
+++ b/moto/iot/models.py
@@ -91,15 +91,17 @@ class FakeThingType(BaseModel):
         self,
         thing_type_name: str,
         thing_type_properties: Optional[Dict[str, Any]],
+        account_id: str,
         region_name: str,
     ):
+        self.account_id = account_id
         self.region_name = region_name
         self.thing_type_name = thing_type_name
         self.thing_type_properties = thing_type_properties
         self.thing_type_id = str(random.uuid4())  # I don't know the rule of id
         t = time.time()
         self.metadata = {"deprecated": False, "creationDate": int(t * 1000) / 1000.0}
-        self.arn = f"arn:aws:iot:{self.region_name}:1:thingtype/{thing_type_name}"
+        self.arn = f"arn:aws:iot:{self.region_name}:{self.account_id}:thingtype/{thing_type_name}"
 
     def to_dict(self) -> Dict[str, Any]:
         return {
@@ -117,9 +119,11 @@ class FakeThingGroup(BaseModel):
         thing_group_name: str,
         parent_group_name: str,
         thing_group_properties: Dict[str, str],
+        account_id: str,
         region_name: str,
         thing_groups: Dict[str, "FakeThingGroup"],
     ):
+        self.account_id = account_id
         self.region_name = region_name
         self.thing_group_name = thing_group_name
         self.thing_group_id = str(random.uuid4())  # I don't know the rule of id
@@ -153,7 +157,7 @@ class FakeThingGroup(BaseModel):
                         }
                     ]
                 )
-        self.arn = f"arn:aws:iot:{self.region_name}:1:thinggroup/{thing_group_name}"
+        self.arn = f"arn:aws:iot:{self.region_name}:{self.account_id}:thinggroup/{thing_group_name}"
         self.things: Dict[str, FakeThing] = OrderedDict()
 
     def to_dict(self) -> Dict[str, Any]:
@@ -345,14 +349,16 @@ class FakeJob(BaseModel):
         target_selection: str,
         job_executions_rollout_config: Dict[str, Any],
         document_parameters: Dict[str, str],
+        account_id: str,
         region_name: str,
     ):
         if not self._job_id_matcher(self.JOB_ID_REGEX, job_id):
             raise InvalidRequestException()
 
+        self.account_id = account_id
         self.region_name = region_name
         self.job_id = job_id
-        self.job_arn = f"arn:aws:iot:{self.region_name}:1:job/{job_id}"
+        self.job_arn = f"arn:aws:iot:{self.region_name}:{self.account_id}:job/{job_id}"
         self.targets = targets
         self.document_source = document_source
         self.document = document
@@ -508,8 +514,10 @@ class FakeRule(BaseModel):
         error_action: Dict[str, Any],
         sql: str,
         aws_iot_sql_version: str,
+        account_id: str,
         region_name: str,
     ):
+        self.account_id = account_id
         self.region_name = region_name
         self.rule_name = rule_name
         self.description = description or ""
@@ -520,7 +528,7 @@ class FakeRule(BaseModel):
         self.error_action = error_action or {}
         self.sql = sql
         self.aws_iot_sql_version = aws_iot_sql_version or "2016-03-23"
-        self.arn = f"arn:aws:iot:{self.region_name}:1:rule/{rule_name}"
+        self.arn = f"arn:aws:iot:{self.region_name}:{self.account_id}:rule/{rule_name}"
 
     def to_get_dict(self) -> Dict[str, Any]:
         return {
@@ -550,6 +558,7 @@ class FakeRule(BaseModel):
 class FakeDomainConfiguration(BaseModel):
     def __init__(
         self,
+        account_id: str,
         region_name: str,
         domain_configuration_name: str,
         domain_name: str,
@@ -565,7 +574,7 @@ class FakeDomainConfiguration(BaseModel):
                 f"operation: Service type {service_type} not recognized."
             )
         self.domain_configuration_name = domain_configuration_name
-        self.domain_configuration_arn = f"arn:aws:iot:{region_name}:1:domainconfiguration/{domain_configuration_name}/{random.get_random_string(length=5)}"
+        self.domain_configuration_arn = f"arn:aws:iot:{region_name}:{account_id}:domainconfiguration/{domain_configuration_name}/{random.get_random_string(length=5)}"
         self.domain_name = domain_name
         self.server_certificates = []
         if server_certificate_arns:
@@ -721,7 +730,7 @@ class IoTBackend(BaseBackend):
         if thing_type_properties is None:
             thing_type_properties = {}
         thing_type = FakeThingType(
-            thing_type_name, thing_type_properties, self.region_name
+            thing_type_name, thing_type_properties, self.account_id, self.region_name
         )
         self.thing_types[thing_type.arn] = thing_type
         return thing_type.thing_type_name, thing_type.arn
@@ -1340,6 +1349,7 @@ class IoTBackend(BaseBackend):
             thing_group_name,
             parent_group_name,
             thing_group_properties,
+            self.account_id,
             self.region_name,
             self.thing_groups,
         )
@@ -1576,6 +1586,7 @@ class IoTBackend(BaseBackend):
             target_selection,
             job_executions_rollout_config,
             document_parameters,
+            self.account_id,
             self.region_name,
         )
         self.jobs[job_id] = job
@@ -1772,6 +1783,7 @@ class IoTBackend(BaseBackend):
             created_at=int(time.time()),
             topic_pattern=topic,
             sql=sql,
+            account_id=self.account_id,
             region_name=self.region_name,
             **kwargs,
         )
@@ -1817,6 +1829,7 @@ class IoTBackend(BaseBackend):
                 ].domain_configuration_arn,
             )
         self.domain_configurations[domain_configuration_name] = FakeDomainConfiguration(
+            self.account_id,
             self.region_name,
             domain_configuration_name,
             domain_name,
diff --git a/moto/s3/responses.py b/moto/s3/responses.py
index d8f0d286c..907bdce54 100644
--- a/moto/s3/responses.py
+++ b/moto/s3/responses.py
@@ -173,6 +173,10 @@ class S3Response(BaseResponse):
         else:
             return unquote(part)
 
+    @property
+    def is_access_point(self) -> bool:
+        return ".s3-accesspoint." in self.headers["host"]
+
     @property
     def backend(self) -> S3Backend:
         return s3_backends[self.current_account]["global"]
@@ -247,10 +251,23 @@ class S3Response(BaseResponse):
         return "delete" in qs
 
     def parse_bucket_name_from_url(self, request: Any, url: str) -> str:
+        bucket_name = ""
         if self.subdomain_based_buckets(request):
-            return bucket_name_from_url(url)  # type: ignore
+            bucket_name = bucket_name_from_url(url)  # type: ignore
         else:
-            return bucketpath_bucket_name_from_url(url)  # type: ignore
+            bucket_name = bucketpath_bucket_name_from_url(url)  # type: ignore
+
+        if self.is_access_point:
+            # import here to avoid circular dependency error
+            from moto.s3control import s3control_backends
+
+            ap_name = bucket_name[: -(len(self.current_account) + 1)]
+            ap = s3control_backends[self.current_account]["global"].get_access_point(
+                self.current_account, ap_name
+            )
+            bucket_name = ap.bucket
+
+        return bucket_name
 
     def parse_key_name(self, request: Any, url: str) -> str:
         if self.subdomain_based_buckets(request):


Test patch:
diff --git a/tests/test_s3control/test_s3control_s3.py b/tests/test_s3control/test_s3control_s3.py
index 7947496ac..2ea3d3afb 100644
--- a/tests/test_s3control/test_s3control_s3.py
+++ b/tests/test_s3control/test_s3control_s3.py
@@ -5,13 +5,16 @@ from moto import mock_s3, mock_s3control, settings
 from moto.core import DEFAULT_ACCOUNT_ID as ACCOUNT_ID
 
 
+REGION = "us-east-1"
+
+
 if not settings.TEST_SERVER_MODE:
 
     @mock_s3
     @mock_s3control
     def test_pab_are_kept_separate():
-        client = boto3.client("s3control", region_name="us-east-1")
-        s3_client = boto3.client("s3", region_name="us-east-1")
+        client = boto3.client("s3control", region_name=REGION)
+        s3_client = boto3.client("s3", region_name=REGION)
         s3_client.create_bucket(Bucket="bucket")
 
         client.put_public_access_block(
@@ -53,8 +56,8 @@ if not settings.TEST_SERVER_MODE:
     @mock_s3control
     @mock_s3
     def test_pab_are_kept_separate_with_inverse_mocks():
-        client = boto3.client("s3control", region_name="us-east-1")
-        s3_client = boto3.client("s3", region_name="us-east-1")
+        client = boto3.client("s3control", region_name=REGION)
+        s3_client = boto3.client("s3", region_name=REGION)
         s3_client.create_bucket(Bucket="bucket")
 
         client.put_public_access_block(
@@ -92,3 +95,39 @@ if not settings.TEST_SERVER_MODE:
             "BlockPublicPolicy": True,
             "RestrictPublicBuckets": False,
         }
+
+    @mock_s3
+    @mock_s3control
+    def test_access_point_read_write():
+        # Setup
+        bucket = "test-bucket"
+        ap_client = boto3.client("s3control", region_name=REGION)
+        s3_client = boto3.client("s3", region_name=REGION)
+        s3_client.create_bucket(Bucket=bucket)
+
+        read_ap = ap_client.create_access_point(
+            AccountId=ACCOUNT_ID, Name="read-ap", Bucket=bucket
+        )
+        write_ap = ap_client.create_access_point(
+            AccountId=ACCOUNT_ID, Name="write-ap", Bucket=bucket
+        )
+
+        content = b"This is test content"
+        key = "test/object.txt"
+
+        # Execute
+        s3_client.put_object(
+            Bucket=write_ap["AccessPointArn"],
+            Key=key,
+            Body=content,
+            ContentType="text/plain",
+        )
+
+        # Verify
+        assert (
+            s3_client.get_object(Bucket=read_ap["AccessPointArn"], Key=key)[
+                "Body"
+            ].read()
+            == content
+        )
+        assert s3_client.get_object(Bucket=bucket, Key=key)["Body"].read() == content