This is an automated email from the ASF dual-hosted git repository.

henry3260 pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/airflow.git


The following commit(s) were added to refs/heads/main by this push:
     new 2bb32e370cc Stop swallowing unexpected keyring errors when saving 
airflowctl credentials (#70991)
2bb32e370cc is described below

commit 2bb32e370cce86b1c422a249b95af1a0f743517b
Author: rjgoyln <[email protected]>
AuthorDate: Thu Sep 10 21:59:27 2026 +0800

    Stop swallowing unexpected keyring errors when saving airflowctl 
credentials (#70991)
    
    * Stop swallowing unexpected keyring errors when saving airflowctl 
credentials
    
    The TypeError handler in Credentials.save() only produced a meaningful
    error when api_token was None and client_kind was ClientKind.CLI at the
    same time. No caller can satisfy both: the two save() call sites in
    auth_command.py pass ClientKind.AUTH or leave client_kind at its default
    of None, and each already guarantees a non-None token. The branch was
    therefore unreachable, leaving the handler as an unconditional swallow
    that let a failed credential save return as if it had succeeded.
    
    * Update airflow-ctl/src/airflowctl/api/client.py
    
    Co-authored-by: Y-C <[email protected]>
    
    * Repair the duplicated keyring call left by the applied suggestion
    
    Applying the review suggestion from the web UI kept the original
    set_password call alongside the replacement, leaving the module
    unparseable. The accompanying test constructed Credentials without a
    token and only passed because the mocked backend accepted None.
    
    Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
    
    ---------
    
    Co-authored-by: Henry Chen <[email protected]>
    Co-authored-by: Y-C <[email protected]>
    Co-authored-by: Claude Opus 5 (1M context) <[email protected]>
---
 airflow-ctl/src/airflowctl/api/client.py         |  8 +++-----
 airflow-ctl/tests/airflow_ctl/api/test_client.py | 22 +++++++++++++++++++++-
 2 files changed, 24 insertions(+), 6 deletions(-)

diff --git a/airflow-ctl/src/airflowctl/api/client.py 
b/airflow-ctl/src/airflowctl/api/client.py
index e0c9c60f0ee..e160cf295b7 100644
--- a/airflow-ctl/src/airflowctl/api/client.py
+++ b/airflow-ctl/src/airflowctl/api/client.py
@@ -229,10 +229,12 @@ class Credentials:
                 for candidate in candidates:
                     if hasattr(candidate, "_get_new_password"):
                         candidate._get_new_password = _bounded_get_new_password
+                if self.api_token is None:
+                    raise AirflowCtlCredentialNotFoundException("No API token 
found. Please login first.")
                 keyring.set_password(
                     "airflowctl",
                     self.token_key_for_environment(self.api_environment),
-                    self.api_token,  # type: ignore[arg-type]
+                    self.api_token,
                 )
         except (NoKeyringError, NotImplementedError) as e:
             log.error(e)
@@ -243,10 +245,6 @@ class Credentials:
                 "the --api-token flag to any command.\n"
                 "Use `airflowctl auth login --skip-keyring ...` to dismiss 
this error."
             ) from e
-        except TypeError as e:
-            # This happens when the token is None, which is not allowed by 
keyring
-            if self.api_token is None and self.client_kind == ClientKind.CLI:
-                raise AirflowCtlCredentialNotFoundException("No API token 
found. Please login first.") from e
 
     def load(self) -> Credentials:
         """Load the credentials from keyring and URL from disk file."""
diff --git a/airflow-ctl/tests/airflow_ctl/api/test_client.py 
b/airflow-ctl/tests/airflow_ctl/api/test_client.py
index b7e50073851..3db2a46a896 100644
--- a/airflow-ctl/tests/airflow_ctl/api/test_client.py
+++ b/airflow-ctl/tests/airflow_ctl/api/test_client.py
@@ -216,7 +216,27 @@ class TestCredentials:
         mock_keyring.set_password.side_effect = NoKeyringError("no backend")
 
         with pytest.raises(AirflowCtlKeyringException, match="Keyring backend 
is not available"):
-            Credentials(client_kind=cli_client).save()
+            Credentials(client_kind=cli_client, api_token="TEST_TOKEN").save()
+
+    @patch.dict(os.environ, {"AIRFLOW_CLI_ENVIRONMENT": 
"TEST_SAVE_KEYRING_TYPE_ERROR"})
+    @patch("airflowctl.api.client.keyring")
+    def test_save_propagates_unexpected_keyring_error(self, mock_keyring):
+        mock_keyring.set_password.side_effect = TypeError("password must be a 
string")
+
+        with pytest.raises(TypeError, match="password must be a string"):
+            Credentials(
+                api_url="http://localhost:8080";,
+                api_token="TEST_TOKEN",
+                client_kind=ClientKind.AUTH,
+            ).save()
+
+    @patch.dict(os.environ, {"AIRFLOW_CLI_ENVIRONMENT": "TEST_SAVE_NO_TOKEN"})
+    @patch("airflowctl.api.client.keyring")
+    def test_save_without_token(self, mock_keyring):
+        with pytest.raises(AirflowCtlCredentialNotFoundException, match="No 
API token found"):
+            Credentials(api_url="http://localhost:8080";, 
client_kind=ClientKind.AUTH).save()
+
+        mock_keyring.set_password.assert_not_called()
 
     @patch.dict(os.environ, {"AIRFLOW_CLI_ENVIRONMENT": 
"TEST_SAVE_SKIP_KEYRING"})
     @patch("airflowctl.api.client.keyring")

Reply via email to