Repository: GoogleCloudPlatform/cloud-sql-python-connector
Pull request #602 (2023-02-07 16:37:23)

chore: validate region matches instance
Adding validation to check that instance connection name region matches that of actual Cloud SQL instance.


Patch:
diff --git a/google/cloud/sql/connector/instance.py b/google/cloud/sql/connector/instance.py
index 6e8d97c..444c677 100644
--- a/google/cloud/sql/connector/instance.py
+++ b/google/cloud/sql/connector/instance.py
@@ -292,6 +292,7 @@ class Instance:
                     self._sqladmin_api_endpoint,
                     self._credentials,
                     self._project,
+                    self._region,
                     self._instance,
                 )
             )
diff --git a/google/cloud/sql/connector/refresh_utils.py b/google/cloud/sql/connector/refresh_utils.py
index e3c3b44..fbb0cc4 100644
--- a/google/cloud/sql/connector/refresh_utils.py
+++ b/google/cloud/sql/connector/refresh_utils.py
@@ -39,6 +39,7 @@ async def _get_metadata(
     sqladmin_api_endpoint: str,
     credentials: Credentials,
     project: str,
+    region: str,
     instance: str,
 ) -> Dict[str, Any]:
     """Requests metadata from the Cloud SQL Instance
@@ -59,6 +60,9 @@ async def _get_metadata(
     :param project:
         A string representing the name of the project.
 
+    :type region: str
+    :param region : A string representing the name of the region.
+
     :type instance: str
     :param instance: A string representing the name of the instance.
 
@@ -77,6 +81,8 @@ async def _get_metadata(
         )
     elif not isinstance(project, str):
         raise TypeError(f"project must be of type str, got {type(project)}")
+    elif not isinstance(region, str):
+        raise TypeError(f"region must be of type str, got {type(region)}")
     elif not isinstance(instance, str):
         raise TypeError(f"instance must be of type str, got {type(instance)}")
 
@@ -95,6 +101,11 @@ async def _get_metadata(
     resp = await client_session.get(url, headers=headers, raise_for_status=True)
     ret_dict = await resp.json()
 
+    if ret_dict["region"] != region:
+        raise ValueError(
+            f'[{project}:{region}:{instance}]: Provided region was mismatched - got region {region}, expected {ret_dict["region"]}.'
+        )
+
     metadata = {
         "ip_addresses": {ip["type"]: ip["ipAddress"] for ip in ret_dict["ipAddresses"]},
         "server_ca_cert": ret_dict["serverCaCert"]["cert"],


Test patch:
diff --git a/tests/unit/test_refresh_utils.py b/tests/unit/test_refresh_utils.py
index 5c65c56..b94b3d0 100644
--- a/tests/unit/test_refresh_utils.py
+++ b/tests/unit/test_refresh_utils.py
@@ -149,11 +149,12 @@ async def test_get_metadata(
     parameters.
     """
     project = "my-project"
+    region = "my-region"
     instance = "my-instance"
     # mock Cloud SQL Admin API call
     with aioresponses() as mocked:
         mocked.get(
-            "https://sqladmin.googleapis.com/sql/v1beta4/projects/my-project/instances/my-instance/connectSettings",
+            f"https://sqladmin.googleapis.com/sql/v1beta4/projects/{project}/instances/{instance}/connectSettings",
             status=200,
             body=mock_instance.connect_settings(),
             repeat=True,
@@ -165,6 +166,7 @@ async def test_get_metadata(
                 "https://sqladmin.googleapis.com",
                 credentials,
                 project,
+                region,
                 instance,
             )
 
@@ -184,6 +186,7 @@ async def test_get_metadata_TypeError(credentials: Credentials) -> None:
     """
     client_session = Mock(aiohttp.ClientSession)
     project = "my-project"
+    region = "my-region"
     instance = "my-instance"
 
     # incorrect credentials type
@@ -193,6 +196,7 @@ async def test_get_metadata_TypeError(credentials: Credentials) -> None:
             sqladmin_api_endpoint="https://sqladmin.googleapis.com",
             credentials="bad-credentials",
             project=project,
+            region=region,
             instance=instance,
         )
     # incorrect project type
@@ -202,6 +206,17 @@ async def test_get_metadata_TypeError(credentials: Credentials) -> None:
             sqladmin_api_endpoint="https://sqladmin.googleapis.com",
             credentials=credentials,
             project=12345,
+            region=region,
+            instance=instance,
+        )
+    # incorrect region type
+    with pytest.raises(TypeError):
+        await _get_metadata(
+            client_session=client_session,
+            sqladmin_api_endpoint="https://sqladmin.googleapis.com",
+            credentials=credentials,
+            project=project,
+            region=1,
             instance=instance,
         )
     # incorrect instance type
@@ -211,10 +226,46 @@ async def test_get_metadata_TypeError(credentials: Credentials) -> None:
             sqladmin_api_endpoint="https://sqladmin.googleapis.com",
             credentials=credentials,
             project=project,
+            region=region,
             instance=12345,
         )
 
 
+@pytest.mark.asyncio
+@no_type_check
+async def test_get_metadata_region_mismatch(
+    mock_instance: FakeCSQLInstance, credentials: Credentials
+) -> None:
+    """
+    Test to check whether _get_metadata throws proper ValueError
+    when given mismatched region.
+    """
+    client_session = Mock(aiohttp.ClientSession)
+    project = "my-project"
+    region = "bad-region"
+    instance = "my-instance"
+
+    # mock Cloud SQL Admin API call
+    with aioresponses() as mocked:
+        mocked.get(
+            f"https://sqladmin.googleapis.com/sql/v1beta4/projects/{project}/instances/{instance}/connectSettings",
+            status=200,
+            body=mock_instance.connect_settings(),
+            repeat=True,
+        )
+
+        async with aiohttp.ClientSession() as client_session:
+            with pytest.raises(ValueError):
+                await _get_metadata(
+                    client_session=client_session,
+                    sqladmin_api_endpoint="https://sqladmin.googleapis.com",
+                    credentials=credentials,
+                    project=project,
+                    region=region,
+                    instance=instance,
+                )
+
+
 @pytest.mark.asyncio
 @no_type_check
 async def test_is_valid_with_valid_metadata() -> None: