Repository: getmoto/moto
Pull request #7136 (2023-12-18 21:06:06)

Add cognitoidp.admin_respond_to_auth_challenge
Implements `cognitoidp`.`admin_respond_to_auth_challenge`. It simply calls the corresponding public endpoint `respond_to_auth_challenge` internally as there is no relevant differences between them.

Since there are no changes in logic, I didn't add any additional tests, but I did convert existing tests so a call to `admin_initiate_auth` and `initiate_auth` uses the appropriate API `admin_respond_to_auth_challenge` and `respond_to_auth_challenge` respectively.

Let me know if there's anything I missed.


Patch:
diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml
index 8f0d65f5f..f9f10b2e2 100644
--- a/.github/workflows/codeql-analysis.yml
+++ b/.github/workflows/codeql-analysis.yml
@@ -38,7 +38,7 @@ jobs:
 
     # Initializes the CodeQL tools for scanning.
     - name: Initialize CodeQL
-      uses: github/codeql-action/init@v2
+      uses: github/codeql-action/init@v3
       with:
         languages: ${{ matrix.language }}
         # If you wish to specify custom queries, you can do so here or in a config file.
@@ -49,7 +49,7 @@ jobs:
     # Autobuild attempts to build any compiled languages  (C/C++, C#, or Java).
     # If this step fails, then you should remove it and run the build manually (see below)
     - name: Autobuild
-      uses: github/codeql-action/autobuild@v2
+      uses: github/codeql-action/autobuild@v3
 
     # ℹ️ Command-line programs to run using the OS shell.
     # 📚 https://git.io/JvXDl
@@ -63,4 +63,4 @@ jobs:
     #   make release
 
     - name: Perform CodeQL Analysis
-      uses: github/codeql-action/analyze@v2
+      uses: github/codeql-action/analyze@v3
diff --git a/IMPLEMENTATION_COVERAGE.md b/IMPLEMENTATION_COVERAGE.md
index ebcb2d39c..bd76a8b7c 100644
--- a/IMPLEMENTATION_COVERAGE.md
+++ b/IMPLEMENTATION_COVERAGE.md
@@ -1180,7 +1180,7 @@
 
 ## cognito-idp
 <details>
-<summary>59% implemented</summary>
+<summary>60% implemented</summary>
 
 - [X] add_custom_attributes
 - [X] admin_add_user_to_group
@@ -1201,7 +1201,7 @@
 - [ ] admin_list_user_auth_events
 - [X] admin_remove_user_from_group
 - [X] admin_reset_user_password
-- [ ] admin_respond_to_auth_challenge
+- [X] admin_respond_to_auth_challenge
 - [X] admin_set_user_mfa_preference
 - [X] admin_set_user_password
 - [ ] admin_set_user_settings
diff --git a/docs/docs/services/cognito-idp.rst b/docs/docs/services/cognito-idp.rst
index b5021803d..3b487dc2f 100644
--- a/docs/docs/services/cognito-idp.rst
+++ b/docs/docs/services/cognito-idp.rst
@@ -46,7 +46,7 @@ cognito-idp
 - [ ] admin_list_user_auth_events
 - [X] admin_remove_user_from_group
 - [X] admin_reset_user_password
-- [ ] admin_respond_to_auth_challenge
+- [X] admin_respond_to_auth_challenge
 - [X] admin_set_user_mfa_preference
 - [X] admin_set_user_password
 - [ ] admin_set_user_settings
diff --git a/moto/cognitoidp/models.py b/moto/cognitoidp/models.py
index a2cac85fd..7cb137803 100644
--- a/moto/cognitoidp/models.py
+++ b/moto/cognitoidp/models.py
@@ -1483,6 +1483,27 @@ class CognitoIdpBackend(BaseBackend):
             # We shouldn't get here due to enum validation of auth_flow
             return None  # type: ignore[return-value]
 
+    def admin_respond_to_auth_challenge(
+        self,
+        session: str,
+        client_id: str,
+        challenge_name: str,
+        challenge_responses: Dict[str, str],
+    ) -> Dict[str, Any]:
+        """
+        Responds to an authentication challenge, as an administrator.
+
+        The only differences between this admin endpoint and public endpoint are not relevant and so we can safely call
+        the public endpoint to do the work:
+        - The admin endpoint requires a user pool id along with a session; the public endpoint searches across all pools
+        - ContextData is passed in; we don't use it
+
+        ref: https://docs.aws.amazon.com/cognito-user-identity-pools/latest/APIReference/API_AdminRespondToAuthChallenge.html
+        """
+        return self.respond_to_auth_challenge(
+            session, client_id, challenge_name, challenge_responses
+        )
+
     def respond_to_auth_challenge(
         self,
         session: str,
@@ -1490,6 +1511,11 @@ class CognitoIdpBackend(BaseBackend):
         challenge_name: str,
         challenge_responses: Dict[str, str],
     ) -> Dict[str, Any]:
+        """
+        Responds to an authentication challenge, from public client.
+
+        ref: https://docs.aws.amazon.com/cognito-user-identity-pools/latest/APIReference/API_RespondToAuthChallenge.html
+        """
         if challenge_name == "PASSWORD_VERIFIER":
             session = challenge_responses.get("PASSWORD_CLAIM_SECRET_BLOCK")  # type: ignore[assignment]
 
@@ -2180,6 +2206,18 @@ class RegionAgnosticBackend:
         backend = self._find_backend_by_access_token(access_token)
         return backend.get_user(access_token)
 
+    def admin_respond_to_auth_challenge(
+        self,
+        session: str,
+        client_id: str,
+        challenge_name: str,
+        challenge_responses: Dict[str, str],
+    ) -> Dict[str, Any]:
+        backend = self._find_backend_for_clientid(client_id)
+        return backend.admin_respond_to_auth_challenge(
+            session, client_id, challenge_name, challenge_responses
+        )
+
     def respond_to_auth_challenge(
         self,
         session: str,
diff --git a/moto/cognitoidp/responses.py b/moto/cognitoidp/responses.py
index dbe3ae509..adf4f2889 100644
--- a/moto/cognitoidp/responses.py
+++ b/moto/cognitoidp/responses.py
@@ -449,6 +449,17 @@ class CognitoIdpResponse(BaseResponse):
 
         return json.dumps(auth_result)
 
+    def admin_respond_to_auth_challenge(self) -> str:
+        session = self._get_param("Session")
+        client_id = self._get_param("ClientId")
+        challenge_name = self._get_param("ChallengeName")
+        challenge_responses = self._get_param("ChallengeResponses")
+        auth_result = region_agnostic_backend.admin_respond_to_auth_challenge(
+            session, client_id, challenge_name, challenge_responses
+        )
+
+        return json.dumps(auth_result)
+
     def respond_to_auth_challenge(self) -> str:
         session = self._get_param("Session")
         client_id = self._get_param("ClientId")
diff --git a/moto/ec2/models/launch_templates.py b/moto/ec2/models/launch_templates.py
index 1e4a63f34..16db8d79d 100644
--- a/moto/ec2/models/launch_templates.py
+++ b/moto/ec2/models/launch_templates.py
@@ -13,6 +13,7 @@ from ..utils import (
     convert_tag_spec,
     generic_filter,
     random_launch_template_id,
+    random_launch_template_name,
     utc_date_and_time,
 )
 from .core import TaggedEC2Resource
@@ -141,6 +142,9 @@ class LaunchTemplate(TaggedEC2Resource, CloudFormationModel):
             properties.get("TagSpecifications", {}), tag_key="Tags"
         )
 
+        if name is None:
+            name = random_launch_template_name()
+
         launch_template = backend.create_launch_template(
             name, description, data, tag_spec
         )
diff --git a/moto/ec2/utils.py b/moto/ec2/utils.py
index 08638ac37..821712af8 100644
--- a/moto/ec2/utils.py
+++ b/moto/ec2/utils.py
@@ -233,6 +233,10 @@ def random_launch_template_id() -> str:
     return random_id(prefix=EC2_RESOURCE_TO_PREFIX["launch-template"], size=17)
 
 
+def random_launch_template_name() -> str:
+    return f"LaunchTemplate_{random_resource_id(size=12)}"
+
+
 def random_iam_instance_profile_association_id() -> str:
     return random_id(prefix=EC2_RESOURCE_TO_PREFIX["iam-instance-profile-association"])
 


Test patch:
diff --git a/.github/workflows/dockertests.yml b/.github/workflows/dockertests.yml
index 390df94a3..c057b4d37 100644
--- a/.github/workflows/dockertests.yml
+++ b/.github/workflows/dockertests.yml
@@ -88,7 +88,7 @@ jobs:
         docker stop motoserver
     - name: Archive Logs
       if: always()
-      uses: actions/upload-artifact@v3
+      uses: actions/upload-artifact@v4
       with:
         name: motoserver-${{ matrix.python-version }}
         path: |
@@ -146,7 +146,7 @@ jobs:
         cp server_output.log serverlogs2/server_output.log
     - name: Archive logs
       if: always()
-      uses: actions/upload-artifact@v3
+      uses: actions/upload-artifact@v4
       with:
         name: motoserver-${{ matrix.python-version }}
         path: |
@@ -205,7 +205,7 @@ jobs:
         cp server_output.log serverlogs3/server_output.log
     - name: Archive Logs
       if: always()
-      uses: actions/upload-artifact@v3
+      uses: actions/upload-artifact@v4
       with:
         name: motoserver-${{ matrix.python-version }}
         path: |
diff --git a/.github/workflows/test_outdated_versions.yml b/.github/workflows/test_outdated_versions.yml
index 92e497fde..8d6c801b5 100644
--- a/.github/workflows/test_outdated_versions.yml
+++ b/.github/workflows/test_outdated_versions.yml
@@ -63,7 +63,7 @@ jobs:
         docker stop motoserver
     - name: Archive TF logs
       if: always()
-      uses: actions/upload-artifact@v3
+      uses: actions/upload-artifact@v4
       with:
         name: motoserver
         path: |
diff --git a/.github/workflows/tests_proxymode.yml b/.github/workflows/tests_proxymode.yml
index b0f7ff2d0..ff3380b1e 100644
--- a/.github/workflows/tests_proxymode.yml
+++ b/.github/workflows/tests_proxymode.yml
@@ -50,7 +50,7 @@ jobs:
         kill $(lsof -t -i:5005)
     - name: Archive Proxy logs
       if: always()
-      uses: actions/upload-artifact@v3
+      uses: actions/upload-artifact@v4
       with:
         name: motoproxy-${{ matrix.python-version }}
         path: |
diff --git a/.github/workflows/tests_servermode.yml b/.github/workflows/tests_servermode.yml
index 12d904650..06c6fe444 100644
--- a/.github/workflows/tests_servermode.yml
+++ b/.github/workflows/tests_servermode.yml
@@ -66,7 +66,7 @@ jobs:
         docker stop motoserver
     - name: Archive TF logs
       if: always()
-      uses: actions/upload-artifact@v3
+      uses: actions/upload-artifact@v4
       with:
         name: motoserver-${{ matrix.python-version }}
         path: |
diff --git a/tests/test_cognitoidp/test_cognitoidp.py b/tests/test_cognitoidp/test_cognitoidp.py
index d7abd1beb..9b5b1db1f 100644
--- a/tests/test_cognitoidp/test_cognitoidp.py
+++ b/tests/test_cognitoidp/test_cognitoidp.py
@@ -1532,7 +1532,8 @@ def test_group_in_access_token():
 
     # This sets a new password and logs the user in (creates tokens)
     new_password = "P2$Sword"
-    result = conn.respond_to_auth_challenge(
+    result = conn.admin_respond_to_auth_challenge(
+        UserPoolId=user_pool_id,
         Session=result["Session"],
         ClientId=client_id,
         ChallengeName="NEW_PASSWORD_REQUIRED",
@@ -1585,7 +1586,8 @@ def test_group_in_id_token():
 
     # This sets a new password and logs the user in (creates tokens)
     new_password = "P2$Sword"
-    result = conn.respond_to_auth_challenge(
+    result = conn.admin_respond_to_auth_challenge(
+        UserPoolId=user_pool_id,
         Session=result["Session"],
         ClientId=client_id,
         ChallengeName="NEW_PASSWORD_REQUIRED",
@@ -2749,7 +2751,8 @@ def authentication_flow(conn, auth_flow):
 
     # This sets a new password and logs the user in (creates tokens)
     new_password = "P2$Sword"
-    result = conn.respond_to_auth_challenge(
+    result = conn.admin_respond_to_auth_challenge(
+        UserPoolId=user_pool_id,
         Session=result["Session"],
         ClientId=client_id,
         ChallengeName="NEW_PASSWORD_REQUIRED",
@@ -4388,7 +4391,8 @@ def test_admin_initiate_auth_when_token_totp_enabled():
     assert result["Session"] != ""
 
     # Respond to challenge with TOTP
-    result = conn.respond_to_auth_challenge(
+    result = conn.admin_respond_to_auth_challenge(
+        UserPoolId=user_pool_id,
         ClientId=client_id,
         ChallengeName="SOFTWARE_TOKEN_MFA",
         Session=result["Session"],
diff --git a/tests/test_ec2/test_launch_templates_cloudformation.py b/tests/test_ec2/test_launch_templates_cloudformation.py
index 71ce94a98..ede997e20 100644
--- a/tests/test_ec2/test_launch_templates_cloudformation.py
+++ b/tests/test_ec2/test_launch_templates_cloudformation.py
@@ -245,3 +245,55 @@ def test_asg_with_default_launch_template_version():
         == launch_template_name
     )
     assert autoscaling_group["LaunchTemplate"]["Version"] == "1"
+
+
+@mock_autoscaling
+@mock_cloudformation
+@mock_ec2
+def test_two_launch_templates():
+    cf_client = boto3.client("cloudformation", region_name="us-west-1")
+    ec2_client = boto3.client("ec2", region_name="us-west-1")
+
+    stack_name = str(uuid4())
+
+    template_json = json.dumps(
+        {
+            "AWSTemplateFormatVersion": "2010-09-09",
+            "Description": "AWS CloudFormation Template to create two LaunchTemplate",
+            "Resources": {
+                "LaunchTemplate0": {
+                    "Type": "AWS::EC2::LaunchTemplate",
+                    "Properties": {
+                        "LaunchTemplateData": {
+                            "ImageId": EXAMPLE_AMI_ID,
+                            "InstanceType": "t3.small",
+                            "UserData": "",
+                        },
+                    },
+                },
+                "LaunchTemplate1": {
+                    "Type": "AWS::EC2::LaunchTemplate",
+                    "Properties": {
+                        "LaunchTemplateData": {
+                            "ImageId": EXAMPLE_AMI_ID,
+                            "InstanceType": "t3.medium",
+                            "UserData": "",
+                        },
+                    },
+                },
+            },
+        }
+    )
+
+    cf_client.create_stack(
+        StackName=stack_name,
+        TemplateBody=template_json,
+        Capabilities=["CAPABILITY_NAMED_IAM"],
+        OnFailure="DELETE",
+    )
+
+    launch_templates = ec2_client.describe_launch_templates()
+    assert (
+        launch_templates["LaunchTemplates"][0]["LaunchTemplateName"]
+        != launch_templates["LaunchTemplates"][1]["LaunchTemplateName"]
+    )
diff --git a/tests/test_redshiftdata/test_redshiftdata.py b/tests/test_redshiftdata/test_redshiftdata.py
index 50a35d0ff..ae6181e25 100644
--- a/tests/test_redshiftdata/test_redshiftdata.py
+++ b/tests/test_redshiftdata/test_redshiftdata.py
@@ -15,10 +15,10 @@ RESOURCE_NOT_FOUND_ERROR_MESSAGE = "Query does not exist."
 
 @pytest.fixture(autouse=True, name="client")
 def fixture_client():
-    yield boto3.client("redshift-data", region_name=REGION)
+    with mock_redshiftdata():
+        yield boto3.client("redshift-data", region_name=REGION)
 
 
-@mock_redshiftdata
 def test_cancel_statement_throws_exception_when_uuid_invalid(client):
     statement_id = "test"
 
@@ -30,7 +30,6 @@ def test_cancel_statement_throws_exception_when_uuid_invalid(client):
     )
 
 
-@mock_redshiftdata
 def test_cancel_statement_throws_exception_when_statement_not_found(client):
     statement_id = "890f1253-595b-4608-a0d1-73f933ccd0a0"
 
@@ -42,7 +41,6 @@ def test_cancel_statement_throws_exception_when_statement_not_found(client):
     )
 
 
-@mock_redshiftdata
 def test_describe_statement_throws_exception_when_uuid_invalid(client):
     statement_id = "test"
 
@@ -54,7 +52,6 @@ def test_describe_statement_throws_exception_when_uuid_invalid(client):
     )
 
 
-@mock_redshiftdata
 def test_describe_statement_throws_exception_when_statement_not_found(client):
     statement_id = "890f1253-595b-4608-a0d1-73f933ccd0a0"
 
@@ -66,7 +63,6 @@ def test_describe_statement_throws_exception_when_statement_not_found(client):
     )
 
 
-@mock_redshiftdata
 def test_get_statement_result_throws_exception_when_uuid_invalid(client):
     statement_id = "test"
 
@@ -78,7 +74,6 @@ def test_get_statement_result_throws_exception_when_uuid_invalid(client):
     )
 
 
-@mock_redshiftdata
 def test_get_statement_result_throws_exception_when_statement_not_found(client):
     statement_id = "890f1253-595b-4608-a0d1-73f933ccd0a0"
 
@@ -90,7 +85,6 @@ def test_get_statement_result_throws_exception_when_statement_not_found(client):
     )
 
 
-@mock_redshiftdata
 def test_execute_statement_and_cancel_statement(client):
     cluster_identifier = "cluster_identifier"
     database = "database"
@@ -115,7 +109,6 @@ def test_execute_statement_and_cancel_statement(client):
     assert cancel_response["Status"] is True
 
 
-@mock_redshiftdata
 def test_execute_statement_and_describe_statement(client):
     cluster_identifier = "cluster_identifier"
     database = "database"
@@ -146,7 +139,6 @@ def test_execute_statement_and_describe_statement(client):
     assert describe_response["Status"] == "STARTED"
 
 
-@mock_redshiftdata
 def test_execute_statement_and_get_statement_result(client):
     cluster_identifier = "cluster_identifier"
     database = "database"
diff --git a/tests/test_route53resolver/test_route53resolver_endpoint.py b/tests/test_route53resolver/test_route53resolver_endpoint.py
index bbd1387a1..d9f38878c 100644
--- a/tests/test_route53resolver/test_route53resolver_endpoint.py
+++ b/tests/test_route53resolver/test_route53resolver_endpoint.py
@@ -3,6 +3,7 @@ from datetime import datetime, timezone
 
 import boto3
 import pytest
+from botocore.config import Config
 from botocore.exceptions import ClientError
 
 from moto import mock_route53resolver, settings
@@ -74,8 +75,10 @@ def create_test_endpoint(client, ec2_client, name=None, tags=None):
 
 @mock_route53resolver
 def test_route53resolver_invalid_create_endpoint_args():
-    """Test invalid arguments to the create_resolver_endpoint API."""
-    client = boto3.client("route53resolver", region_name=TEST_REGION)
+    # Some validation is now part of botocore-1.34.3
+    # Disable validation to verify that Moto has the same validation
+    config = Config(parameter_validation=False)
+    client = boto3.client("route53resolver", region_name=TEST_REGION, config=config)
     random_num = mock_random.get_random_hex(10)
 
     # Verify ValidationException error messages are accumulated properly:
@@ -163,8 +166,10 @@ def test_route53resolver_invalid_create_endpoint_args():
 @mock_ec2
 @mock_route53resolver
 def test_route53resolver_bad_create_endpoint_subnets():
-    """Test bad subnet scenarios for create_resolver_endpoint API."""
-    client = boto3.client("route53resolver", region_name=TEST_REGION)
+    # Some validation is now part of botocore-1.34.3
+    # Disable validation to verify that Moto has the same validation
+    config = Config(parameter_validation=False)
+    client = boto3.client("route53resolver", region_name=TEST_REGION, config=config)
     ec2_client = boto3.client("ec2", region_name=TEST_REGION)
     random_num = mock_random.get_random_hex(10)