CVE-2023-6974: Server-Side Request Forgery (SSRF) (CWE-918)
value: A malicious user could use this issue to access internal HTTP(s) servers and in the worst case (ie: aws instance) it could be abuse to get a remote code execution on the victim machine.
CWE description: The web server receives a URL or similar request from an upstream component and retrieves the contents of this URL, but it does not sufficiently ensure that the request is being sent to the expected destination.
CVSS v3 base score: 9.8
Published: 2023-12-20T06:15Z
Repository: https://github.com/mlflow/mlflow
Language: Python
Files changed: mlflow/environment_variables.py, mlflow/utils/request_utils.py, tests/projects/test_databricks.py, tests/store/artifact/test_databricks_artifact_repo.py, tests/store/tracking/test_rest_store.py, tests/utils/test_request_utils.py, tests/utils/test_rest_utils.py
Commit date: 2023-12-13 11:43:32 +0800
Commit message: Add env var to disable redirects again (#10673)

Signed-off-by: Daniel Lok <daniel.lok@databricks.com>
Diff: diff --git a/mlflow/environment_variables.py b/mlflow/environment_variables.py
index 8a47faad9e..e5d9f9cc40 100644
--- a/mlflow/environment_variables.py
+++ b/mlflow/environment_variables.py
@@ -491,5 +491,11 @@ MLFLOW_MULTIPART_UPLOAD_CHUNK_SIZE = _EnvironmentVariable(
 #: Specifies the chunk size in bytes to use when performing multipart download
 #: (default: ``104_857_600`` (100 MB))
 MLFLOW_MULTIPART_DOWNLOAD_CHUNK_SIZE = _EnvironmentVariable(
     "MLFLOW_MULTIPART_DOWNLOAD_CHUNK_SIZE", int, 100 * 1024**2
 )
+
+#: Specifies whether or not to allow the MLflow server to follow redirects when
+#: making HTTP requests. If set to False, the server will throw an exception if it
+#: encounters a redirect response.
+#: (default: ``True``)
+MLFLOW_ALLOW_HTTP_REDIRECTS = _BooleanEnvironmentVariable("MLFLOW_ALLOW_HTTP_REDIRECTS", True)
diff --git a/mlflow/utils/request_utils.py b/mlflow/utils/request_utils.py
index 104fb37691..f7c4744d70 100644
--- a/mlflow/utils/request_utils.py
+++ b/mlflow/utils/request_utils.py
@@ -163,10 +163,11 @@ def _get_http_response_with_retries(
     max_retries,
     backoff_factor,
     backoff_jitter,
     retry_codes,
     raise_on_status=True,
+    allow_redirects=None,
     **kwargs,
 ):
     """
     Performs an HTTP request using Python's `requests` module with an automatic retry policy.
 
@@ -185,11 +186,17 @@ def _get_http_response_with_retries(
     :return: requests.Response object.
     """
     session = _get_request_session(
         max_retries, backoff_factor, backoff_jitter, retry_codes, raise_on_status
     )
-    return session.request(method, url, **kwargs)
+
+    # the environment variable is hardcoded here to avoid importing mlflow.
+    # however, documentation is available in environment_variables.py
+    env_value = os.getenv("MLFLOW_ALLOW_HTTP_REDIRECTS", "true").lower() in ["true", "1"]
+    allow_redirects = env_value if allow_redirects is None else allow_redirects
+
+    return session.request(method, url, allow_redirects=allow_redirects, **kwargs)
 
 
 def cloud_storage_http_request(
     method,
     url,
diff --git a/tests/projects/test_databricks.py b/tests/projects/test_databricks.py
index 5bb1c53043..6433a0bf3f 100644
--- a/tests/projects/test_databricks.py
+++ b/tests/projects/test_databricks.py
@@ -447,10 +447,11 @@ def test_databricks_http_request_integration(get_config, request):
     def confirm_request_params(*args, **kwargs):
         headers = DefaultRequestHeaderProvider().request_headers()
         headers["Authorization"] = "Basic dXNlcjpwYXNz"
         assert args == ("PUT", "host/clusters/list")
         assert kwargs == {
+            "allow_redirects": True,
             "headers": headers,
             "verify": True,
             "json": {"a": "b"},
             "timeout": 120,
         }
diff --git a/tests/store/artifact/test_databricks_artifact_repo.py b/tests/store/artifact/test_databricks_artifact_repo.py
index f5d17d9b8a..95ad167ced 100644
--- a/tests/store/artifact/test_databricks_artifact_repo.py
+++ b/tests/store/artifact/test_databricks_artifact_repo.py
@@ -256,10 +256,11 @@ def test_log_artifact_azure_with_headers(
             GetCredentialsForWrite, MOCK_RUN_ID, [expected_location]
         )
         request_mock.assert_called_with(
             "put",
             f"{MOCK_AZURE_SIGNED_URI}?comp=blocklist",
+            allow_redirects=True,
             data=ANY,
             headers=filtered_azure_headers,
             timeout=None,
         )
 
@@ -342,37 +343,42 @@ def test_log_artifact_adls_gen2_with_headers(
         )
         # test with block size 5
         request_mock.assert_any_call(
             "put",
             f"{MOCK_ADLS_GEN2_SIGNED_URI}?resource=file",
+            allow_redirects=True,
             headers=filtered_azure_headers,
             timeout=None,
         )
         request_mock.assert_any_call(
             "patch",
             f"{MOCK_ADLS_GEN2_SIGNED_URI}?action=append&position=0",
+            allow_redirects=True,
             data=ANY,
             headers=filtered_azure_headers,
             timeout=None,
         )
         request_mock.assert_any_call(
             "patch",
             f"{MOCK_ADLS_GEN2_SIGNED_URI}?action=append&position=5",
+            allow_redirects=True,
             data=ANY,
             headers=filtered_azure_headers,
             timeout=None,
         )
         request_mock.assert_any_call(
             "patch",
             f"{MOCK_ADLS_GEN2_SIGNED_URI}?action=append&position=10",
+            allow_redirects=True,
             data=ANY,
             headers=filtered_azure_headers,
             timeout=None,
         )
         request_mock.assert_called_with(
             "patch",
             f"{MOCK_ADLS_GEN2_SIGNED_URI}?action=flush&position=14",
+            allow_redirects=True,
             headers=filtered_azure_headers,
             timeout=None,
         )
 
 
@@ -400,16 +406,18 @@ def test_log_artifact_adls_gen2_flush_error(databricks_artifact_repo, test_file)
         get_credential_infos_mock.assert_called_with(GetCredentialsForWrite, MOCK_RUN_ID, ANY)
         assert request_mock.mock_calls == [
             mock.call(
                 "put",
                 f"{MOCK_ADLS_GEN2_SIGNED_URI}?resource=file",
+                allow_redirects=True,
                 headers={},
                 timeout=None,
             ),
             mock.call(
                 "patch",
                 f"{MOCK_ADLS_GEN2_SIGNED_URI}?action=append&position=0&flush=true",
+                allow_redirects=True,
                 data=ANY,
                 headers={},
                 timeout=None,
             ),
         ]
@@ -432,11 +440,11 @@ def test_log_artifact_aws(databricks_artifact_repo, test_file, artifact_path, ex
         databricks_artifact_repo.log_artifact(test_file, artifact_path)
         get_credential_infos_mock.assert_called_with(
             GetCredentialsForWrite, MOCK_RUN_ID, [expected_location]
         )
         request_mock.assert_called_with(
-            "put", MOCK_AWS_SIGNED_URI, data=ANY, headers={}, timeout=None
+            "put", MOCK_AWS_SIGNED_URI, allow_redirects=True, data=ANY, headers={}, timeout=None
         )
 
 
 @pytest.mark.parametrize(("artifact_path", "expected_location"), [(None, "test.txt")])
 def test_log_artifact_aws_with_headers(
@@ -460,11 +468,16 @@ def test_log_artifact_aws_with_headers(
         databricks_artifact_repo.log_artifact(test_file, artifact_path)
         get_credential_infos_mock.assert_called_with(
             GetCredentialsForWrite, MOCK_RUN_ID, [expected_location]
         )
         request_mock.assert_called_with(
-            "put", MOCK_AWS_SIGNED_URI, data=ANY, headers=expected_headers, timeout=None
+            "put",
+            MOCK_AWS_SIGNED_URI,
+            allow_redirects=True,
+            data=ANY,
+            headers=expected_headers,
+            timeout=None,
         )
 
 
 def test_log_artifact_aws_presigned_url_error(databricks_artifact_repo, test_file):
     mock_credential_info = ArtifactCredentialInfo(
@@ -498,11 +511,11 @@ def test_log_artifact_gcp(databricks_artifact_repo, test_file, artifact_path, ex
         databricks_artifact_repo.log_artifact(test_file, artifact_path)
         get_credential_infos_mock.assert_called_with(
             GetCredentialsForWrite, MOCK_RUN_ID, [expected_location]
         )
         request_mock.assert_called_with(
-            "put", MOCK_GCP_SIGNED_URL, data=ANY, headers={}, timeout=None
+            "put", MOCK_GCP_SIGNED_URL, allow_redirects=True, data=ANY, headers={}, timeout=None
         )
 
 
 @pytest.mark.parametrize(("artifact_path", "expected_location"), [(None, "test.txt")])
 def test_log_artifact_gcp_with_headers(
@@ -526,11 +539,16 @@ def test_log_artifact_gcp_with_headers(
         databricks_artifact_repo.log_artifact(test_file, artifact_path)
         get_credential_infos_mock.assert_called_with(
             GetCredentialsForWrite, MOCK_RUN_ID, [expected_location]
         )
         request_mock.assert_called_with(
-            "put", MOCK_GCP_SIGNED_URL, data=ANY, headers=expected_headers, timeout=None
+            "put",
+            MOCK_GCP_SIGNED_URL,
+            allow_redirects=True,
+            data=ANY,
+            headers=expected_headers,
+            timeout=None,
         )
 
 
 def test_log_artifact_gcp_presigned_url_error(databricks_artifact_repo, test_file):
     mock_credential_info = ArtifactCredentialInfo(
@@ -1292,10 +1310,11 @@ def test_multipart_upload(databricks_artifact_repo, large_file, mock_chunk_size)
         with large_file.open("rb") as f:
             expected_calls = [
                 mock.call(
                     "put",
                     f"{MOCK_AWS_SIGNED_URI}partNumber={i + 1}",
+                    allow_redirects=True,
                     data=f.read(mock_chunk_size),
                     headers={"header": f"part-{i + 1}"},
                     timeout=None,
                 )
                 for i in range(2)
@@ -1381,10 +1400,11 @@ def test_multipart_upload_retry_part_upload(databricks_artifact_repo, large_file
         with large_file.open("rb") as f:
             expected_calls = [
                 mock.call(
                     "put",
                     f"{MOCK_AWS_SIGNED_URI}partNumber={i + 1}",
+                    allow_redirects=True,
                     data=f.read(mock_chunk_size),
                     headers={"header": f"part-{i + 1}"},
                     timeout=None,
                 )
                 for i in range(2)
@@ -1439,10 +1459,11 @@ def test_multipart_upload_abort(databricks_artifact_repo, large_file, mock_chunk
         with large_file.open("rb") as f:
             expected_calls = [
                 mock.call(
                     "put",
                     f"{MOCK_AWS_SIGNED_URI}partNumber={i + 1}",
+                    allow_redirects=True,
                     data=f.read(mock_chunk_size),
                     headers={"header": f"part-{i + 1}"},
                     timeout=None,
                 )
                 for i in range(2)
@@ -1457,8 +1478,9 @@ def test_multipart_upload_abort(databricks_artifact_repo, large_file, mock_chunk
             {"part_number": 2, "etag": "etag-2"},
         ]
         assert abort_call == mock.call(
             "delete",
             f"{MOCK_AWS_SIGNED_URI}uploadId=abort",
+            allow_redirects=True,
             headers={"header": "abort"},
             timeout=None,
         )
diff --git a/tests/store/tracking/test_rest_store.py b/tests/store/tracking/test_rest_store.py
index dbb42ad3b6..4904708d1c 100644
--- a/tests/store/tracking/test_rest_store.py
+++ b/tests/store/tracking/test_rest_store.py
@@ -68,10 +68,11 @@ def test_successful_http_request(request):
     def mock_request(*args, **kwargs):
         # Filter out None arguments
         assert args == ("POST", "https://hello/api/2.0/mlflow/experiments/search")
         kwargs = {k: v for k, v in kwargs.items() if v is not None}
         assert kwargs == {
+            "allow_redirects": True,
             "json": {"view_type": "ACTIVE_ONLY"},
             "headers": DefaultRequestHeaderProvider().request_headers(),
             "verify": True,
             "timeout": 120,
         }
diff --git a/tests/utils/test_request_utils.py b/tests/utils/test_request_utils.py
index 292a64d6eb..4d95aaa8be 100644
--- a/tests/utils/test_request_utils.py
+++ b/tests/utils/test_request_utils.py
@@ -56,5 +56,107 @@ def test_download_chunk_incomplete_read(tmp_path):
                 range_end=999,
                 headers={},
                 download_path=download_path,
                 http_uri="https://example.com",
             )
+
+
+@pytest.mark.parametrize("env_value", ["0", "false", "False", "FALSE"])
+def test_redirects_disabled_if_env_var_set(monkeypatch, env_value):
+    monkeypatch.setenv("MLFLOW_ALLOW_HTTP_REDIRECTS", env_value)
+
+    with mock.patch("requests.Session.request") as mock_request:
+        mock_request.return_value.status_code = 302
+        mock_request.return_value.text = "mock response"
+
+        response = request_utils.cloud_storage_http_request("GET", "http://localhost:5000")
+
+        assert response.text == "mock response"
+        mock_request.assert_called_once_with(
+            "GET",
+            "http://localhost:5000",
+            allow_redirects=False,
+            timeout=None,
+        )
+
+
+@pytest.mark.parametrize("env_value", ["1", "true", "True", "TRUE"])
+def test_redirects_enabled_if_env_var_set(monkeypatch, env_value):
+    monkeypatch.setenv("MLFLOW_ALLOW_HTTP_REDIRECTS", env_value)
+
+    with mock.patch("requests.Session.request") as mock_request:
+        mock_request.return_value.status_code = 302
+        mock_request.return_value.text = "mock response"
+
+        response = request_utils.cloud_storage_http_request(
+            "GET",
+            "http://localhost:5000",
+        )
+
+        assert response.text == "mock response"
+        mock_request.assert_called_once_with(
+            "GET",
+            "http://localhost:5000",
+            allow_redirects=True,
+            timeout=None,
+        )
+
+
+@pytest.mark.parametrize("env_value", ["0", "false", "False", "FALSE"])
+def test_redirect_kwarg_overrides_env_value_false(monkeypatch, env_value):
+    monkeypatch.setenv("MLFLOW_ALLOW_HTTP_REDIRECTS", env_value)
+
+    with mock.patch("requests.Session.request") as mock_request:
+        mock_request.return_value.status_code = 302
+        mock_request.return_value.text = "mock response"
+
+        response = request_utils.cloud_storage_http_request(
+            "GET", "http://localhost:5000", allow_redirects=True
+        )
+
+        assert response.text == "mock response"
+        mock_request.assert_called_once_with(
+            "GET",
+            "http://localhost:5000",
+            allow_redirects=True,
+            timeout=None,
+        )
+
+
+@pytest.mark.parametrize("env_value", ["1", "true", "True", "TRUE"])
+def test_redirect_kwarg_overrides_env_value_true(monkeypatch, env_value):
+    monkeypatch.setenv("MLFLOW_ALLOW_HTTP_REDIRECTS", env_value)
+
+    with mock.patch("requests.Session.request") as mock_request:
+        mock_request.return_value.status_code = 302
+        mock_request.return_value.text = "mock response"
+
+        response = request_utils.cloud_storage_http_request(
+            "GET", "http://localhost:5000", allow_redirects=False
+        )
+
+        assert response.text == "mock response"
+        mock_request.assert_called_once_with(
+            "GET",
+            "http://localhost:5000",
+            allow_redirects=False,
+            timeout=None,
+        )
+
+
+def test_redirects_enabled_by_default():
+    with mock.patch("requests.Session.request") as mock_request:
+        mock_request.return_value.status_code = 302
+        mock_request.return_value.text = "mock response"
+
+        response = request_utils.cloud_storage_http_request(
+            "GET",
+            "http://localhost:5000",
+        )
+
+        assert response.text == "mock response"
+        mock_request.assert_called_once_with(
+            "GET",
+            "http://localhost:5000",
+            allow_redirects=True,
+            timeout=None,
+        )
diff --git a/tests/utils/test_rest_utils.py b/tests/utils/test_rest_utils.py
index 744b5ea6c8..be1bf67cef 100644
--- a/tests/utils/test_rest_utils.py
+++ b/tests/utils/test_rest_utils.py
@@ -114,10 +114,11 @@ def test_http_request_hostonly(request):
     request.return_value = response
     http_request(host_only, "/my/endpoint", "GET")
     request.assert_called_with(
         "GET",
         "http://my-host/my/endpoint",
+        allow_redirects=True,
         verify=True,
         headers=DefaultRequestHeaderProvider().request_headers(),
         timeout=120,
     )
 
@@ -131,10 +132,11 @@ def test_http_request_cleans_hostname(request):
     request.return_value = response
     http_request(host_only, "/my/endpoint", "GET")
     request.assert_called_with(
         "GET",
         "http://my-host/my/endpoint",
+        allow_redirects=True,
         verify=True,
         headers=DefaultRequestHeaderProvider().request_headers(),
         timeout=120,
     )
 
@@ -149,10 +151,11 @@ def test_http_request_with_basic_auth(request):
     headers = DefaultRequestHeaderProvider().request_headers()
     headers["Authorization"] = "Basic dXNlcjpwYXNz"
     request.assert_called_with(
         "GET",
         "http://my-host/my/endpoint",
+        allow_redirects=True,
         verify=True,
         headers=headers,
         timeout=120,
     )
 
@@ -181,10 +184,11 @@ def test_http_request_with_aws_sigv4(request, monkeypatch):
             return isinstance(other, AWSSigV4)
 
     request.assert_called_once_with(
         "GET",
         "http://my-host/my/endpoint",
+        allow_redirects=True,
         verify=mock.ANY,
         headers=mock.ANY,
         timeout=mock.ANY,
         auth=AuthMatcher(),
     )
@@ -205,10 +209,11 @@ def test_http_request_with_auth(fetch_auth, request):
     fetch_auth.assert_called_with(auth)
 
     request.assert_called_with(
         "GET",
         "http://my-host/my/endpoint",
+        allow_redirects=True,
         verify=mock.ANY,
         headers=mock.ANY,
         timeout=mock.ANY,
         auth=mock_fetch_auth,
     )
@@ -224,10 +229,11 @@ def test_http_request_with_token(request):
     headers = DefaultRequestHeaderProvider().request_headers()
     headers["Authorization"] = "Bearer my-token"
     request.assert_called_with(
         "GET",
         "http://my-host/my/endpoint",
+        allow_redirects=True,
         verify=True,
         headers=headers,
         timeout=120,
     )
 
@@ -240,10 +246,11 @@ def test_http_request_with_insecure(request):
     request.return_value = response
     http_request(host_only, "/my/endpoint", "GET")
     request.assert_called_with(
         "GET",
         "http://my-host/my/endpoint",
+        allow_redirects=True,
         verify=False,
         headers=DefaultRequestHeaderProvider().request_headers(),
         timeout=120,
     )
 
@@ -256,10 +263,11 @@ def test_http_request_client_cert_path(request):
     request.return_value = response
     http_request(host_only, "/my/endpoint", "GET")
     request.assert_called_with(
         "GET",
         "http://my-host/my/endpoint",
+        allow_redirects=True,
         verify=True,
         cert="/some/path",
         headers=DefaultRequestHeaderProvider().request_headers(),
         timeout=120,
     )
@@ -273,10 +281,11 @@ def test_http_request_server_cert_path(request):
     request.return_value = response
     http_request(host_only, "/my/endpoint", "GET")
     request.assert_called_with(
         "GET",
         "http://my-host/my/endpoint",
+        allow_redirects=True,
         verify="/some/path",
         headers=DefaultRequestHeaderProvider().request_headers(),
         timeout=120,
     )
 
@@ -293,10 +302,11 @@ def test_http_request_with_content_type_header(request):
     headers["Authorization"] = "Bearer my-token"
     headers["Content-Type"] = "text/plain"
     request.assert_called_with(
         "GET",
         "http://my-host/my/endpoint",
+        allow_redirects=True,
         verify=True,
         headers=headers,
         timeout=120,
     )
 
@@ -318,10 +328,11 @@ def test_http_request_request_headers(request):
         request.return_value = response
         http_request(host_only, "/my/endpoint", "GET")
         request.assert_called_with(
             "GET",
             "http://my-host/my/endpoint",
+            allow_redirects=True,
             verify="/some/path",
             headers={**DefaultRequestHeaderProvider().request_headers(), "test": "header"},
             timeout=120,
         )
 
@@ -354,10 +365,11 @@ def test_http_request_request_headers_user_agent(request):
         request.return_value = response
         http_request(host_only, "/my/endpoint", "GET")
         request.assert_called_with(
             "GET",
             "http://my-host/my/endpoint",
+            allow_redirects=True,
             verify="/some/path",
             headers=expected_headers,
             timeout=120,
         )
 
@@ -391,10 +403,11 @@ def test_http_request_request_headers_user_agent_and_extra_header(request):
         request.return_value = response
         http_request(host_only, "/my/endpoint", "GET")
         request.assert_called_with(
             "GET",
             "http://my-host/my/endpoint",
+            allow_redirects=True,
             verify="/some/path",
             headers=expected_headers,
             timeout=120,
         )
 
@@ -438,20 +451,22 @@ def test_http_request_wrapper(request):
     request.return_value = response
     http_request_safe(host_only, "/my/endpoint", "GET")
     request.assert_called_with(
         "GET",
         "http://my-host/my/endpoint",
+        allow_redirects=True,
         verify=False,
         headers=DefaultRequestHeaderProvider().request_headers(),
         timeout=120,
     )
     response.text = "non json"
     request.return_value = response
     http_request_safe(host_only, "/my/endpoint", "GET")
     request.assert_called_with(
         "GET",
         "http://my-host/my/endpoint",
+        allow_redirects=True,
         verify=False,
         headers=DefaultRequestHeaderProvider().request_headers(),
         timeout=120,
     )
     response.status_code = 400
@@ -558,5 +573,28 @@ def test_augmented_raise_for_status():
         augmented_raise_for_status(response)
 
     assert e.value.response == response
     assert e.value.request == response.request
     assert response.text in str(e.value)
+
+
+def test_provide_redirect_kwarg():
+    with mock.patch("requests.Session.request") as mock_request:
+        mock_request.return_value.status_code = 302
+        mock_request.return_value.text = "mock response"
+
+        response = http_request(
+            MlflowHostCreds("http://my-host"),
+            "/my/endpoint",
+            "GET",
+            allow_redirects=False,
+        )
+
+        assert response.text == "mock response"
+        mock_request.assert_called_with(
+            "GET",
+            "http://my-host/my/endpoint",
+            allow_redirects=False,
+            headers=mock.ANY,
+            verify=mock.ANY,
+            timeout=120,
+        )

Vulnerable code:     return session.request(method, url, **kwargs)
            "put", MOCK_AWS_SIGNED_URI, data=ANY, headers={}, timeout=None
            "put", MOCK_AWS_SIGNED_URI, data=ANY, headers=expected_headers, timeout=None
            "put", MOCK_GCP_SIGNED_URL, data=ANY, headers={}, timeout=None
            "put", MOCK_GCP_SIGNED_URL, data=ANY, headers=expected_headers, timeout=None
Fixed code: 
#: Specifies whether or not to allow the MLflow server to follow redirects when
#: making HTTP requests. If set to False, the server will throw an exception if it
#: encounters a redirect response.
#: (default: ``True``)
MLFLOW_ALLOW_HTTP_REDIRECTS = _BooleanEnvironmentVariable("MLFLOW_ALLOW_HTTP_REDIRECTS", True)
    allow_redirects=None,

    # the environment variable is hardcoded here to avoid importing mlflow.
    # however, documentation is available in environment_variables.py
    env_value = os.getenv("MLFLOW_ALLOW_HTTP_REDIRECTS", "true").lower() in ["true", "1"]
    allow_redirects = env_value if allow_redirects is None else allow_redirects

    return session.request(method, url, allow_redirects=allow_redirects, **kwargs)
            "allow_redirects": True,
            allow_redirects=True,
            allow_redirects=True,
            allow_redirects=True,
            allow_redirects=True,
            allow_redirects=True,
            allow_redirects=True,
                allow_redirects=True,
                allow_redirects=True,
            "put", MOCK_AWS_SIGNED_URI, allow_redirects=True, data=ANY, headers={}, timeout=None
            "put",
            MOCK_AWS_SIGNED_URI,
            allow_redirects=True,
            data=ANY,
            headers=expected_headers,
            timeout=None,
            "put", MOCK_GCP_SIGNED_URL, allow_redirects=True, data=ANY, headers={}, timeout=None
            "put",
            MOCK_GCP_SIGNED_URL,
            allow_redirects=True,
            data=ANY,
            headers=expected_headers,
            timeout=None,
                    allow_redirects=True,
                    allow_redirects=True,
                    allow_redirects=True,
            allow_redirects=True,
            "allow_redirects": True,


@pytest.mark.parametrize("env_value", ["0", "false", "False", "FALSE"])
def test_redirects_disabled_if_env_var_set(monkeypatch, env_value):
    monkeypatch.setenv("MLFLOW_ALLOW_HTTP_REDIRECTS", env_value)

    with mock.patch("requests.Session.request") as mock_request:
        mock_request.return_value.status_code = 302
        mock_request.return_value.text = "mock response"

        response = request_utils.cloud_storage_http_request("GET", "http://localhost:5000")

        assert response.text == "mock response"
        mock_request.assert_called_once_with(
            "GET",
            "http://localhost:5000",
            allow_redirects=False,
            timeout=None,
        )


@pytest.mark.parametrize("env_value", ["1", "true", "True", "TRUE"])
def test_redirects_enabled_if_env_var_set(monkeypatch, env_value):
    monkeypatch.setenv("MLFLOW_ALLOW_HTTP_REDIRECTS", env_value)

    with mock.patch("requests.Session.request") as mock_request:
        mock_request.return_value.status_code = 302
        mock_request.return_value.text = "mock response"

        response = request_utils.cloud_storage_http_request(
            "GET",
            "http://localhost:5000",
        )

        assert response.text == "mock response"
        mock_request.assert_called_once_with(
            "GET",
            "http://localhost:5000",
            allow_redirects=True,
            timeout=None,
        )


@pytest.mark.parametrize("env_value", ["0", "false", "False", "FALSE"])
def test_redirect_kwarg_overrides_env_value_false(monkeypatch, env_value):
    monkeypatch.setenv("MLFLOW_ALLOW_HTTP_REDIRECTS", env_value)

    with mock.patch("requests.Session.request") as mock_request:
        mock_request.return_value.status_code = 302
        mock_request.return_value.text = "mock response"

        response = request_utils.cloud_storage_http_request(
            "GET", "http://localhost:5000", allow_redirects=True
        )

        assert response.text == "mock response"
        mock_request.assert_called_once_with(
            "GET",
            "http://localhost:5000",
            allow_redirects=True,
            timeout=None,
        )


@pytest.mark.parametrize("env_value", ["1", "true", "True", "TRUE"])
def test_redirect_kwarg_overrides_env_value_true(monkeypatch, env_value):
    monkeypatch.setenv("MLFLOW_ALLOW_HTTP_REDIRECTS", env_value)

    with mock.patch("requests.Session.request") as mock_request:
        mock_request.return_value.status_code = 302
        mock_request.return_value.text = "mock response"

        response = request_utils.cloud_storage_http_request(
            "GET", "http://localhost:5000", allow_redirects=False
        )

        assert response.text == "mock response"
        mock_request.assert_called_once_with(
            "GET",
            "http://localhost:5000",
            allow_redirects=False,
            timeout=None,
        )


def test_redirects_enabled_by_default():
    with mock.patch("requests.Session.request") as mock_request:
        mock_request.return_value.status_code = 302
        mock_request.return_value.text = "mock response"

        response = request_utils.cloud_storage_http_request(
            "GET",
            "http://localhost:5000",
        )

        assert response.text == "mock response"
        mock_request.assert_called_once_with(
            "GET",
            "http://localhost:5000",
            allow_redirects=True,
            timeout=None,
        )
        allow_redirects=True,
        allow_redirects=True,
        allow_redirects=True,
        allow_redirects=True,
        allow_redirects=True,
        allow_redirects=True,
        allow_redirects=True,
        allow_redirects=True,
        allow_redirects=True,
        allow_redirects=True,
            allow_redirects=True,
            allow_redirects=True,
            allow_redirects=True,
        allow_redirects=True,
        allow_redirects=True,


def test_provide_redirect_kwarg():
    with mock.patch("requests.Session.request") as mock_request:
        mock_request.return_value.status_code = 302
        mock_request.return_value.text = "mock response"

        response = http_request(
            MlflowHostCreds("http://my-host"),
            "/my/endpoint",
            "GET",
            allow_redirects=False,
        )

        assert response.text == "mock response"
        mock_request.assert_called_with(
            "GET",
            "http://my-host/my/endpoint",
            allow_redirects=False,
            headers=mock.ANY,
            verify=mock.ANY,
            timeout=120,
        )