Repository: getmoto/moto (python)

Issue:
S3 get_object_attributes fails for GLACIER objects
Environment:

Python 3.11
Windows 11 Pro
moto==5.0.20
pytest==8.3.3

My failing test

   ```
 def test_get_glacier_object_attributes(self) -> None:
        s3 = client(
            "s3",
            region_name="eu-west-1"
        )
        s3.create_bucket(
            Bucket="test-bucket",
            CreateBucketConfiguration={"LocationConstraint": "eu-west-1"},
        )

        csv_body = "field_1,field_2\n1,2"

        s3.put_object(
            Body=csv_body.encode("UTF-8"),
            Bucket="test-bucket",
            Key="file.csv",
            StorageClass="GLACIER",
        )

        attributes = s3.get_object_attributes(
            Bucket="test-bucket",
            Key="file.csv",
            ObjectAttributes=["Checksum", "ObjectSize", "StorageClass"],
        )

        assert attributes is not None
```

The error 

```
....
if http.status_code >= 300:
            error_info = parsed_response.get("Error", {})
            error_code = error_info.get("QueryErrorCode") or error_info.get(
                "Code"
            )
            error_class = self.exceptions.from_code(error_code)
>           raise error_class(parsed_response, operation_name)
E           botocore.errorfactory.InvalidObjectState: An error occurred (InvalidObjectState) when calling the GetObjectAttributes operation: The operation is not valid for the object's storage class


..\..\.venv\Lib\site-packages\botocore\client.py:1009: InvalidObjectState
```


Expected behaviour is that the call to s3..get_object_attributes(
                    Bucket=bucket,
                    Key=key,
                    ObjectAttributes=["Checksum", "ObjectSize", "StorageClass"],
                )
returns a valid response as it does for non-glacier storage class object

Pull request description:
S3: get_object_attributes() now also works for Glacier objects
Fixes #8307 

Patch:
diff --git a/IMPLEMENTATION_COVERAGE.md b/IMPLEMENTATION_COVERAGE.md
index e5ee66fff..2349a0b5f 100644
--- a/IMPLEMENTATION_COVERAGE.md
+++ b/IMPLEMENTATION_COVERAGE.md
@@ -4628,7 +4628,7 @@
 - [ ] get_buckets_aggregation
 - [ ] get_cardinality
 - [ ] get_effective_policies
-- [ ] get_indexing_configuration
+- [X] get_indexing_configuration
 - [X] get_job_document
 - [ ] get_logging_options
 - [ ] get_ota_update
@@ -4742,7 +4742,7 @@
 - [ ] update_dynamic_thing_group
 - [ ] update_event_configurations
 - [ ] update_fleet_metric
-- [ ] update_indexing_configuration
+- [X] update_indexing_configuration
 - [ ] update_job
 - [ ] update_mitigation_action
 - [ ] update_package
diff --git a/moto/iot/models.py b/moto/iot/models.py
index a92765c95..d3efc19d6 100644
--- a/moto/iot/models.py
+++ b/moto/iot/models.py
@@ -3,6 +3,7 @@ import json
 import re
 import time
 from collections import OrderedDict
+from dataclasses import asdict, dataclass, field, replace
 from datetime import datetime, timedelta
 from json import JSONDecodeError
 from typing import TYPE_CHECKING, Any, Dict, Iterable, List, Optional, Pattern, Tuple
@@ -1027,6 +1028,83 @@ class FakeRoleAlias(CloudFormationModel):
         iot_backend.delete_role_alias(role_alias_name=role_alias_name)
 
 
+@dataclass(frozen=True)
+class FakeConfigField:
+    name: str
+    type: str
+
+
+@dataclass(frozen=True)
+class FakeThingGroupIndexingConfiguration:
+    customFields: List[FakeConfigField] = field(default_factory=list)
+    managedFields: List[FakeConfigField] = field(default_factory=list)
+    thingGroupIndexingMode: str = "OFF"
+
+
+@dataclass(frozen=True)
+class FakeThingIndexingConfigurationFilterGeoLocations:
+    name: str
+    order: str
+
+
+@dataclass(frozen=True)
+class FakeThingIndexingConfigurationFilter:
+    geoLocations: List[FakeThingIndexingConfigurationFilterGeoLocations] = field(
+        default_factory=list
+    )
+    namedShadowNames: List[str] = field(default_factory=list)
+
+
+@dataclass(frozen=True)
+class FakeThingIndexingConfiguration:
+    customFields: List[FakeConfigField] = field(default_factory=list)
+    managedFields: List[FakeConfigField] = field(default_factory=list)
+    filter: FakeThingIndexingConfigurationFilter = field(
+        default_factory=FakeThingIndexingConfigurationFilter
+    )
+    deviceDefenderIndexingMode: str = "OFF"
+    namedShadowIndexingMode: str = "OFF"
+    thingConnectivityIndexingMode: str = "OFF"
+    thingIndexingMode: str = "OFF"
+
+
+@dataclass(frozen=True)
+class FakeIndexingConfigurationData:
+    thingGroupIndexingConfiguration: FakeThingGroupIndexingConfiguration = field(
+        default_factory=FakeThingGroupIndexingConfiguration
+    )
+    thingIndexingConfiguration: FakeThingIndexingConfiguration = field(
+        default_factory=FakeThingIndexingConfiguration
+    )
+
+
+class FakeIndexingConfiguration(BaseModel):
+    def __init__(self, region_name: str, account_id: str) -> None:
+        self.region_name = region_name
+        self.account_id = account_id
+        self.configuration = FakeIndexingConfigurationData()
+
+    def to_dict(self) -> Dict[str, Any]:
+        return asdict(self.configuration)
+
+    def update_configuration(
+        self,
+        thingIndexingConfiguration: Dict[str, Any],
+        thingGroupIndexingConfiguration: Dict[str, Any],
+    ) -> None:
+        self.configuration = replace(
+            self.configuration,
+            thingIndexingConfiguration=replace(
+                self.configuration.thingIndexingConfiguration,
+                **thingIndexingConfiguration,
+            ),
+            thingGroupIndexingConfiguration=replace(
+                self.configuration.thingGroupIndexingConfiguration,
+                **thingGroupIndexingConfiguration,
+            ),
+        )
+
+
 class IoTBackend(BaseBackend):
     def __init__(self, region_name: str, account_id: str):
         super().__init__(region_name, account_id)
@@ -1048,6 +1126,7 @@ class IoTBackend(BaseBackend):
         self.role_aliases: Dict[str, FakeRoleAlias] = OrderedDict()
         self.endpoint: Optional[FakeEndpoint] = None
         self.domain_configurations: Dict[str, FakeDomainConfiguration] = OrderedDict()
+        self.indexing_configuration = FakeIndexingConfiguration(region_name, account_id)
 
     @staticmethod
     def default_vpc_endpoint_service(
@@ -2384,5 +2463,17 @@ class IoTBackend(BaseBackend):
         self.describe_role_alias(role_alias_name=role_alias_name)
         del self.role_aliases[role_alias_name]
 
+    def get_index_configuration(self) -> Dict[str, Any]:
+        return self.indexing_configuration.to_dict()
+
+    def update_indexing_configuration(
+        self,
+        thingIndexingConfiguration: Dict[str, Any],
+        thingGroupIndexingConfiguration: Dict[str, Any],
+    ) -> None:
+        self.indexing_configuration.update_configuration(
+            thingIndexingConfiguration, thingGroupIndexingConfiguration
+        )
+
 
 iot_backends = BackendDict(IoTBackend, "iot")
diff --git a/moto/iot/responses.py b/moto/iot/responses.py
index 7d256dbb6..55842b818 100644
--- a/moto/iot/responses.py
+++ b/moto/iot/responses.py
@@ -826,3 +826,13 @@ class IoTResponse(BaseResponse):
         role_alias_name = self._get_param("roleAlias")
         self.iot_backend.delete_role_alias(role_alias_name=role_alias_name)
         return json.dumps({})
+
+    def get_indexing_configuration(self) -> str:
+        return json.dumps(self.iot_backend.get_index_configuration())
+
+    def update_indexing_configuration(self) -> str:
+        self.iot_backend.update_indexing_configuration(
+            self._get_param("thingIndexingConfiguration", {}),
+            self._get_param("thingGroupIndexingConfiguration", {}),
+        )
+        return json.dumps({})
diff --git a/moto/s3/responses.py b/moto/s3/responses.py
index eb79a3228..d0c82428a 100644
--- a/moto/s3/responses.py
+++ b/moto/s3/responses.py
@@ -1516,7 +1516,8 @@ class S3Response(BaseResponse):
         return 200, response_headers, key.value
 
     def get_object_attributes(self) -> TYPE_RESPONSE:
-        key, not_modified = self._get_key()
+        # Get the Key, but do not validate StorageClass - we can retrieve the attributes of Glacier-objects
+        key, not_modified = self._get_key(validate_storage_class=False)
         response_headers = self._get_cors_headers_other()
         if not_modified:
             return 304, response_headers, "Not Modified"
@@ -1608,7 +1609,7 @@ class S3Response(BaseResponse):
             ),
         )
 
-    def _get_key(self) -> Tuple[FakeKey, bool]:
+    def _get_key(self, validate_storage_class: bool = True) -> Tuple[FakeKey, bool]:
         key_name = self.parse_key_name()
         version_id = self.querystring.get("versionId", [None])[0]
         if_modified_since = self.headers.get("If-Modified-Since")
@@ -1621,7 +1622,7 @@ class S3Response(BaseResponse):
             raise MissingKey(key=key_name)
         elif key is None:
             raise MissingVersion()
-        if key.storage_class in ARCHIVE_STORAGE_CLASSES:
+        if validate_storage_class and key.storage_class in ARCHIVE_STORAGE_CLASSES:
             if 'ongoing-request="false"' not in key.response_dict.get(
                 "x-amz-restore", ""
             ):


Test patch:
diff --git a/tests/test_iot/test_iot_indexing_configuration.py b/tests/test_iot/test_iot_indexing_configuration.py
new file mode 100644
index 000000000..d2ec29a58
--- /dev/null
+++ b/tests/test_iot/test_iot_indexing_configuration.py
@@ -0,0 +1,63 @@
+import boto3
+
+from moto import mock_aws
+
+
+@mock_aws
+def test_validate_default_indexing_configuration():
+    client = boto3.client("iot", region_name="us-east-1")
+    indexing_config = client.get_indexing_configuration()
+
+    thingIndexingConfiguration = indexing_config["thingIndexingConfiguration"]
+    assert thingIndexingConfiguration["thingIndexingMode"] in [
+        "OFF",
+        "REGISTRY",
+        "REGISTRY_AND_SHADOW",
+    ]
+
+    thingGroupIndexingConfiguration = indexing_config["thingGroupIndexingConfiguration"]
+    assert thingGroupIndexingConfiguration["thingGroupIndexingMode"] in ["ON", "OFF"]
+
+
+@mock_aws
+def test_update_indexing_mode():
+    client = boto3.client("iot", region_name="us-east-1")
+    client.update_indexing_configuration(
+        thingIndexingConfiguration={
+            "thingIndexingMode": "REGISTRY",
+            "thingConnectivityIndexingMode": "STATUS",
+            "deviceDefenderIndexingMode": "VIOLATIONS",
+            "namedShadowIndexingMode": "ON",
+            "managedFields": [{"name": "field1", "type": "String"}],
+            "customFields": [{"name": "custom_field1", "type": "Boolean"}],
+            "filter": {"namedShadowNames": ["shadow_1", "shadow_2"]},
+        },
+        thingGroupIndexingConfiguration={
+            "thingGroupIndexingMode": "ON",
+            "managedFields": [{"name": "thing_field_1", "type": "String"}],
+            "customFields": [{"name": "thing_custom_field", "type": "Number"}],
+        },
+    )
+
+    indexing_config = client.get_indexing_configuration()
+
+    thingIndexingConfiguration = indexing_config["thingIndexingConfiguration"]
+    assert thingIndexingConfiguration["thingIndexingMode"] == "REGISTRY"
+    assert thingIndexingConfiguration["thingConnectivityIndexingMode"] == "STATUS"
+    assert thingIndexingConfiguration["deviceDefenderIndexingMode"] == "VIOLATIONS"
+    assert thingIndexingConfiguration["namedShadowIndexingMode"] == "ON"
+    assert thingIndexingConfiguration["managedFields"] == [
+        {"name": "field1", "type": "String"}
+    ]
+    assert thingIndexingConfiguration["customFields"] == [
+        {"name": "custom_field1", "type": "Boolean"}
+    ]
+
+    thingGroupIndexingConfiguration = indexing_config["thingGroupIndexingConfiguration"]
+    assert thingGroupIndexingConfiguration["thingGroupIndexingMode"] == "ON"
+    assert thingGroupIndexingConfiguration["managedFields"] == [
+        {"name": "thing_field_1", "type": "String"}
+    ]
+    assert thingGroupIndexingConfiguration["customFields"] == [
+        {"name": "thing_custom_field", "type": "Number"}
+    ]
diff --git a/tests/test_s3/test_s3_storageclass.py b/tests/test_s3/test_s3_storageclass.py
index f40c97ee6..4ba0b3214 100644
--- a/tests/test_s3/test_s3_storageclass.py
+++ b/tests/test_s3/test_s3_storageclass.py
@@ -269,3 +269,9 @@ def test_s3_get_object_from_glacier():
         "The operation is not valid for the object's storage class"
     )
     assert err["StorageClass"] == "GLACIER"
+
+    # Note that get_object_attributes should work
+    resp = s3_client.get_object_attributes(
+        Bucket=bucket_name, Key="test.txt", ObjectAttributes=["StorageClass"]
+    )
+    assert resp["StorageClass"] == "GLACIER"