Repository: getmoto/moto
Pull request #7379 (2024-02-21 10:03:58)

CognitoIDP: Add `SMS_MFA` support to admin_initiate_auth and respond_to_auth_challenge methods
- Return `SMS_MFA` challenge if user has sms MFA enabled in `admin_initiate_auth`
- Support `SMS_MFA` type in `respond_to_auth_challange`


Patch:
diff --git a/moto/cognitoidp/models.py b/moto/cognitoidp/models.py
index 15416d4e7..0e960764e 100644
--- a/moto/cognitoidp/models.py
+++ b/moto/cognitoidp/models.py
@@ -1466,6 +1466,16 @@ class CognitoIdpBackend(BaseBackend):
                     "Session": session,
                 }
 
+            if user.sms_mfa_enabled and user.preferred_mfa_setting == "SMS_MFA":
+                session = str(random.uuid4())
+                self.sessions[session] = user_pool
+
+                return {
+                    "ChallengeName": "SMS_MFA",
+                    "ChallengeParameters": {},
+                    "Session": session,
+                }
+
             return self._log_user_in(user_pool, client, username)
         elif auth_flow in (AuthFlow.REFRESH_TOKEN, AuthFlow.REFRESH_TOKEN_AUTH):
             refresh_token: str = auth_parameters.get("REFRESH_TOKEN")  # type: ignore[assignment]
@@ -1578,13 +1588,13 @@ class CognitoIdpBackend(BaseBackend):
 
             del self.sessions[session]
             return self._log_user_in(user_pool, client, username)
-        elif challenge_name == "SOFTWARE_TOKEN_MFA":
+        elif challenge_name == "SOFTWARE_TOKEN_MFA" or challenge_name == "SMS_MFA":
             username: str = challenge_responses.get("USERNAME")  # type: ignore[no-redef]
             self.admin_get_user(user_pool.id, username)
 
-            software_token_mfa_code = challenge_responses.get("SOFTWARE_TOKEN_MFA_CODE")
-            if not software_token_mfa_code:
-                raise ResourceNotFoundError(software_token_mfa_code)
+            mfa_code = challenge_responses.get(f"{challenge_name}_CODE")
+            if not mfa_code:
+                raise ResourceNotFoundError(mfa_code)
 
             if client.generate_secret:
                 secret_hash = challenge_responses.get("SECRET_HASH")
diff --git a/moto/core/responses.py b/moto/core/responses.py
index 73f2b37c2..54b575912 100644
--- a/moto/core/responses.py
+++ b/moto/core/responses.py
@@ -343,8 +343,6 @@ class BaseResponse(_TemplateEnvironmentMixin, ActionAuthenticatorMixin):
         if hasattr(self.body, "read"):
             self.body = self.body.read()
 
-        raw_body = self.body
-
         # https://github.com/getmoto/moto/issues/6692
         # Content coming from SDK's can be GZipped for performance reasons
         if (
@@ -379,7 +377,7 @@ class BaseResponse(_TemplateEnvironmentMixin, ActionAuthenticatorMixin):
                         OrderedDict(
                             (key, [value])
                             for key, value in parse_qsl(
-                                raw_body, keep_blank_values=True
+                                self.body, keep_blank_values=True
                             )
                         )
                     )
diff --git a/moto/dynamodb/models/__init__.py b/moto/dynamodb/models/__init__.py
index c3be0ff5d..0627a238e 100644
--- a/moto/dynamodb/models/__init__.py
+++ b/moto/dynamodb/models/__init__.py
@@ -579,7 +579,11 @@ class DynamoDBBackend(BaseBackend):
                     item = item["Put"]
                     attrs = item["Item"]
                     table_name = item["TableName"]
-                    check_unicity(table_name, item)
+                    table = self.get_table(table_name)
+                    key = {table.hash_key_attr: attrs[table.hash_key_attr]}
+                    if table.range_key_attr is not None:
+                        key[table.range_key_attr] = attrs[table.range_key_attr]
+                    check_unicity(table_name, key)
                     condition_expression = item.get("ConditionExpression", None)
                     expression_attribute_names = item.get(
                         "ExpressionAttributeNames", None


Test patch:
diff --git a/tests/test_cloudwatch/test_cloudwatch_boto3.py b/tests/test_cloudwatch/test_cloudwatch_boto3.py
index a5d881ac5..7de4cb844 100644
--- a/tests/test_cloudwatch/test_cloudwatch_boto3.py
+++ b/tests/test_cloudwatch/test_cloudwatch_boto3.py
@@ -2,6 +2,7 @@ import copy
 from datetime import datetime, timedelta, timezone
 from decimal import Decimal
 from operator import itemgetter
+from unittest import SkipTest
 from uuid import uuid4
 
 import boto3
@@ -10,7 +11,7 @@ from botocore.exceptions import ClientError
 from dateutil.tz import tzutc
 from freezegun import freeze_time
 
-from moto import mock_aws
+from moto import mock_aws, settings
 from moto.core import DEFAULT_ACCOUNT_ID as ACCOUNT_ID
 from moto.core.utils import utcnow
 
@@ -27,6 +28,43 @@ def test_put_metric_data_no_dimensions():
     assert {"Namespace": "tester", "MetricName": "metric", "Dimensions": []} in metrics
 
 
+@mock_aws
+def test_put_a_ton_of_metric_data():
+    """
+    A sufficiently large call with metric data triggers request compression
+    Moto should decompress the request if this is the case, and the request should succeed
+    """
+    if not settings.TEST_DECORATOR_MODE:
+        raise SkipTest("Can't test large requests in ServerMode")
+    cloudwatch = boto3.client("cloudwatch", "us-east-1")
+
+    metrics = []
+    metric_dimensions = [{"Name": "Environment", "Value": "env1"}]
+    metric_args = {"Unit": "Seconds", "MetricName": "TestCWMetrics"}
+
+    for type, licences in {
+        k: v
+        for k, v in {
+            "userTypes": [{"id": f"UserType{x}"} for x in range(0, 50)]
+        }.items()
+        if k in ["userTypes", "appBundles", "apps", "extensions", "portalCapabilities"]
+    }.items():
+        for licence in licences:
+            metrics.append(
+                {
+                    **metric_args,
+                    "Dimensions": [
+                        *metric_dimensions,
+                        {"Name": "Type", "Value": f"{type}/{licence['id']}"},
+                    ],
+                    "Value": 1,
+                }
+            )
+
+    cloudwatch.put_metric_data(Namespace="acme", MetricData=metrics)
+    # We don't really need any assertions - we just need to know that the call succeeds
+
+
 @mock_aws
 def test_put_metric_data_can_not_have_nan():
     client = boto3.client("cloudwatch", region_name="us-west-2")
diff --git a/tests/test_cognitoidp/test_cognitoidp.py b/tests/test_cognitoidp/test_cognitoidp.py
index 8867282e4..1ff151bfb 100644
--- a/tests/test_cognitoidp/test_cognitoidp.py
+++ b/tests/test_cognitoidp/test_cognitoidp.py
@@ -4454,6 +4454,56 @@ def test_admin_initiate_auth_when_token_totp_enabled():
     assert result["AuthenticationResult"]["TokenType"] == "Bearer"
 
 
+@mock_aws
+def test_admin_initiate_auth_when_sms_mfa_enabled():
+    conn = boto3.client("cognito-idp", "us-west-2")
+
+    result = authentication_flow(conn, "ADMIN_NO_SRP_AUTH")
+    user_pool_id = result["user_pool_id"]
+    username = result["username"]
+    client_id = result["client_id"]
+    password = result["password"]
+
+    # Set MFA SMS methods
+    conn.admin_set_user_mfa_preference(
+        Username=username,
+        UserPoolId=user_pool_id,
+        SMSMfaSettings={"Enabled": True, "PreferredMfa": True},
+    )
+    result = conn.admin_get_user(UserPoolId=user_pool_id, Username=username)
+    assert len(result["UserMFASettingList"]) == 1
+    assert result["PreferredMfaSetting"] == "SMS_MFA"
+
+    result = conn.admin_initiate_auth(
+        UserPoolId=user_pool_id,
+        ClientId=client_id,
+        AuthFlow="ADMIN_NO_SRP_AUTH",
+        AuthParameters={
+            "USERNAME": username,
+            "PASSWORD": password,
+        },
+    )
+
+    assert result["ChallengeName"] == "SMS_MFA"
+    assert result["Session"] != ""
+
+    result = conn.admin_respond_to_auth_challenge(
+        UserPoolId=user_pool_id,
+        ClientId=client_id,
+        ChallengeName="SMS_MFA",
+        Session=result["Session"],
+        ChallengeResponses={
+            "SMS_MFA_CODE": "123456",
+            "USERNAME": username,
+        },
+    )
+
+    assert result["AuthenticationResult"]["IdToken"] != ""
+    assert result["AuthenticationResult"]["AccessToken"] != ""
+    assert result["AuthenticationResult"]["RefreshToken"] != ""
+    assert result["AuthenticationResult"]["TokenType"] == "Bearer"
+
+
 @mock_aws
 def test_admin_setting_mfa_when_token_not_verified():
     conn = boto3.client("cognito-idp", "us-west-2")
diff --git a/tests/test_dynamodb/exceptions/test_dynamodb_transactions.py b/tests/test_dynamodb/exceptions/test_dynamodb_transactions.py
index 61f2c59dd..212739388 100644
--- a/tests/test_dynamodb/exceptions/test_dynamodb_transactions.py
+++ b/tests/test_dynamodb/exceptions/test_dynamodb_transactions.py
@@ -46,6 +46,55 @@ def test_multiple_transactions_on_same_item():
     )
 
 
+@mock_aws
+def test_transact_write_items__put_and_delete_on_same_item():
+    schema = {
+        "KeySchema": [
+            {"AttributeName": "pk", "KeyType": "HASH"},
+            {"AttributeName": "sk", "KeyType": "RANGE"},
+        ],
+        "AttributeDefinitions": [
+            {"AttributeName": "pk", "AttributeType": "S"},
+            {"AttributeName": "sk", "AttributeType": "S"},
+        ],
+    }
+    dynamodb = boto3.client("dynamodb", region_name="us-east-1")
+    dynamodb.create_table(
+        TableName="test-table", BillingMode="PAY_PER_REQUEST", **schema
+    )
+
+    with pytest.raises(ClientError) as exc:
+        dynamodb.transact_write_items(
+            TransactItems=[
+                {
+                    "Put": {
+                        "TableName": "test-table",
+                        "Item": {
+                            "pk": {"S": "test-pk"},
+                            "sk": {"S": "test-sk"},
+                            "field": {"S": "test-field"},
+                        },
+                    }
+                },
+                {
+                    "Delete": {
+                        "TableName": "test-table",
+                        "Key": {
+                            "pk": {"S": "test-pk"},
+                            "sk": {"S": "test-sk"},
+                        },
+                    }
+                },
+            ]
+        )
+    err = exc.value.response["Error"]
+    assert err["Code"] == "ValidationException"
+    assert (
+        err["Message"]
+        == "Transaction request cannot include multiple operations on one item"
+    )
+
+
 @mock_aws
 def test_transact_write_items__too_many_transactions():
     schema = {
diff --git a/tests/test_dynamodb/test_dynamodb.py b/tests/test_dynamodb/test_dynamodb.py
index 26d5e0545..808d48d2f 100644
--- a/tests/test_dynamodb/test_dynamodb.py
+++ b/tests/test_dynamodb/test_dynamodb.py
@@ -3856,24 +3856,27 @@ def test_transact_write_items_conditioncheck_passes():
     dynamodb.create_table(
         TableName="test-table", BillingMode="PAY_PER_REQUEST", **table_schema
     )
-    # Insert an item without email address
-    dynamodb.put_item(TableName="test-table", Item={"id": {"S": "foo"}})
-    # Put an email address, after verifying it doesn't exist yet
+    # Insert an item with email address
+    dynamodb.put_item(
+        TableName="test-table",
+        Item={"id": {"S": "foo"}, "email_address": {"S": "foo@moto.com"}},
+    )
+    # Put a new item, after verifying the exising item also has email address
     dynamodb.transact_write_items(
         TransactItems=[
             {
                 "ConditionCheck": {
                     "Key": {"id": {"S": "foo"}},
                     "TableName": "test-table",
-                    "ConditionExpression": "attribute_not_exists(#e)",
+                    "ConditionExpression": "attribute_exists(#e)",
                     "ExpressionAttributeNames": {"#e": "email_address"},
                 }
             },
             {
                 "Put": {
                     "Item": {
-                        "id": {"S": "foo"},
-                        "email_address": {"S": "test@moto.com"},
+                        "id": {"S": "bar"},
+                        "email_address": {"S": "bar@moto.com"},
                     },
                     "TableName": "test-table",
                 }
@@ -3882,8 +3885,9 @@ def test_transact_write_items_conditioncheck_passes():
     )
     # Assert all are present
     items = dynamodb.scan(TableName="test-table")["Items"]
-    assert len(items) == 1
-    assert items[0] == {"email_address": {"S": "test@moto.com"}, "id": {"S": "foo"}}
+    assert len(items) == 2
+    assert items[0] == {"email_address": {"S": "foo@moto.com"}, "id": {"S": "foo"}}
+    assert items[1] == {"email_address": {"S": "bar@moto.com"}, "id": {"S": "bar"}}
 
 
 @mock_aws
@@ -3896,12 +3900,12 @@ def test_transact_write_items_conditioncheck_fails():
     dynamodb.create_table(
         TableName="test-table", BillingMode="PAY_PER_REQUEST", **table_schema
     )
-    # Insert an item with email address
+    # Insert an item without email address
     dynamodb.put_item(
         TableName="test-table",
-        Item={"id": {"S": "foo"}, "email_address": {"S": "test@moto.com"}},
+        Item={"id": {"S": "foo"}},
     )
-    # Try to put an email address, but verify whether it exists
+    # Try putting a new item, after verifying the exising item also has email address
     # ConditionCheck should fail
     with pytest.raises(ClientError) as ex:
         dynamodb.transact_write_items(
@@ -3910,15 +3914,15 @@ def test_transact_write_items_conditioncheck_fails():
                     "ConditionCheck": {
                         "Key": {"id": {"S": "foo"}},
                         "TableName": "test-table",
-                        "ConditionExpression": "attribute_not_exists(#e)",
+                        "ConditionExpression": "attribute_exists(#e)",
                         "ExpressionAttributeNames": {"#e": "email_address"},
                     }
                 },
                 {
                     "Put": {
                         "Item": {
-                            "id": {"S": "foo"},
-                            "email_address": {"S": "update@moto.com"},
+                            "id": {"S": "bar"},
+                            "email_address": {"S": "bar@moto.com"},
                         },
                         "TableName": "test-table",
                     }
@@ -3929,10 +3933,10 @@ def test_transact_write_items_conditioncheck_fails():
     assert ex.value.response["Error"]["Code"] == "TransactionCanceledException"
     assert ex.value.response["ResponseMetadata"]["HTTPStatusCode"] == 400
 
-    # Assert the original email address is still present
+    # Assert the original item is still present
     items = dynamodb.scan(TableName="test-table")["Items"]
     assert len(items) == 1
-    assert items[0] == {"email_address": {"S": "test@moto.com"}, "id": {"S": "foo"}}
+    assert items[0] == {"id": {"S": "foo"}}
 
 
 @mock_aws