Copilot commented on code in PR #64643:
URL: https://github.com/apache/airflow/pull/64643#discussion_r3066476752


##########
providers/samba/src/airflow/providers/samba/hooks/samba.py:
##########
@@ -43,29 +43,55 @@ class SambaHook(BaseHook):
         the connection is used in its place.
     :param share_type:
         An optional share type name. If this is unset then it will assume a 
posix share type.
+    :param auth_protocol:
+        An optional authentication protocol. If this is unset then it defaults 
to negotiate.
     """
 
     conn_name_attr = "samba_conn_id"
     default_conn_name = "samba_default"
     conn_type = "samba"
     hook_name = "Samba"
 
+    VALID_AUTH_PROTOCOLS = {"negotiate", "ntlm", "kerberos"}
+
     def __init__(
         self,
         samba_conn_id: str = default_conn_name,
         share: str | None = None,
         share_type: Literal["posix", "windows"] | None = None,
+        auth_protocol: Literal["negotiate", "ntlm", "kerberos"] | None = None,
     ) -> None:
         super().__init__()
         conn = self.get_connection(samba_conn_id)
+        extra = conn.extra_dejson
+
+        self._auth_protocol: str = (
+            auth_protocol or extra.get("auth_protocol") or extra.get("auth") 
or "negotiate"

Review Comment:
   `extra.get(\"auth\")` is a very generic key and may be used for other 
purposes in existing connections; interpreting it as an auth protocol can cause 
unexpected `ValueError`s or behavior changes. To keep backward compatibility 
without broadening the API unexpectedly, only treat the legacy `auth` key as an 
auth protocol when it’s a string and matches a supported protocol (or 
explicitly equals `'kerberos'`), otherwise ignore it.
   ```suggestion
           legacy_auth = extra.get("auth")
           if isinstance(legacy_auth, str) and legacy_auth in 
self.VALID_AUTH_PROTOCOLS:
               legacy_auth_protocol = legacy_auth
           else:
               legacy_auth_protocol = None
   
           self._auth_protocol: str = (
               auth_protocol or extra.get("auth_protocol") or 
legacy_auth_protocol or "negotiate"
   ```



##########
providers/samba/pyproject.toml:
##########
@@ -70,6 +70,9 @@ dependencies = [
 "google" = [
     "apache-airflow-providers-google"
 ]
+"kerberos" = [
+    "smbprotocol[kerberos]>=1.5.0",

Review Comment:
   The runtime check in `SambaHook` requires the `krb5` module to be 
importable, but this optional extra only adds `smbprotocol[kerberos]`. If 
`smbprotocol[kerberos]` doesn’t reliably provide an importable `krb5` module 
across environments, users can still hit 
`AirflowOptionalProviderFeatureException` even after installing 
`apache-airflow-providers-samba[kerberos]`. Consider adding an explicit 
dependency that provides the `krb5` import (or updating the import/check to 
match the package actually installed by the extra), so the error message and 
extras are consistent.
   ```suggestion
       "smbprotocol[kerberos]>=1.5.0",
       "krb5",
   ```



##########
providers/samba/tests/unit/samba/hooks/test_samba.py:
##########
@@ -191,6 +196,99 @@ def test__join_path(
         hook = SambaHook("samba_default", share_type=path_type)
         assert hook._join_path(path) == full_path
 
+    @mock.patch.dict("sys.modules", {"krb5": mock.MagicMock()})
+    @mock.patch("smbclient.register_session")
+    @mock.patch(f"{BASEHOOK_PATCH_PATH}.get_connection")
+    def test_kerberos_auth_via_extra(self, get_conn_mock, register_session):
+        """Test that auth_protocol='kerberos' from extra is passed to 
smbclient."""
+        connection = Connection(
+            host="kerb-host.example.com",
+            schema="share",
+            extra='{"auth_protocol": "kerberos"}',
+        )
+        get_conn_mock.return_value = connection
+        register_session.return_value = None
+        with SambaHook("samba_default"):
+            _, kwargs = tuple(register_session.call_args_list[0])
+            assert kwargs["auth_protocol"] == "kerberos"
+            assert kwargs["username"] is None
+            assert kwargs["password"] is None
+
+    @mock.patch.dict("sys.modules", {"krb5": mock.MagicMock()})
+    @mock.patch("smbclient.register_session")
+    @mock.patch(f"{BASEHOOK_PATCH_PATH}.get_connection")
+    def test_kerberos_auth_via_legacy_auth_key(self, get_conn_mock, 
register_session):
+        """Test backward compat: extra {"auth": "kerberos"} is recognized."""
+        connection = Connection(
+            host="kerb-host.example.com",
+            schema="share",
+            extra='{"auth": "kerberos"}',
+        )
+        get_conn_mock.return_value = connection
+        register_session.return_value = None
+        with SambaHook("samba_default"):
+            _, kwargs = tuple(register_session.call_args_list[0])
+            assert kwargs["auth_protocol"] == "kerberos"
+
+    @mock.patch.dict("sys.modules", {"krb5": mock.MagicMock()})
+    @mock.patch("smbclient.register_session")
+    @mock.patch(f"{BASEHOOK_PATCH_PATH}.get_connection")
+    def test_kerberos_auth_via_constructor(self, get_conn_mock, 
register_session):
+        """Test that constructor auth_protocol overrides extra."""
+        connection = Connection(
+            host="kerb-host.example.com",
+            schema="share",
+            login="user",
+            password="pass",
+        )
+        get_conn_mock.return_value = connection
+        register_session.return_value = None
+        with SambaHook("samba_default", auth_protocol="kerberos"):
+            _, kwargs = tuple(register_session.call_args_list[0])
+            assert kwargs["auth_protocol"] == "kerberos"
+
+    @mock.patch(f"{BASEHOOK_PATCH_PATH}.get_connection")
+    def test_invalid_auth_protocol_raises(self, get_conn_mock):
+        """Test that an invalid auth_protocol raises ValueError."""
+        connection = Connection(
+            host="host",
+            schema="share",
+            extra='{"auth_protocol": "invalid"}',
+        )
+        get_conn_mock.return_value = connection
+        with pytest.raises(ValueError, match="Invalid auth_protocol 
'invalid'"):
+            SambaHook("samba_default")
+
+    @mock.patch(f"{BASEHOOK_PATCH_PATH}.get_connection")
+    def test_kerberos_without_dependency_raises(self, get_conn_mock):
+        """Test that kerberos auth without krb5 installed raises 
AirflowOptionalProviderFeatureException."""
+        connection = Connection(
+            host="kerb-host.example.com",
+            schema="share",
+            extra='{"auth_protocol": "kerberos"}',
+        )
+        get_conn_mock.return_value = connection
+        with mock.patch.dict("sys.modules", {"krb5": None}):
+            with pytest.raises(AirflowOptionalProviderFeatureException, 
match="krb5"):
+                SambaHook("samba_default")

Review Comment:
   This test’s approach to simulating a missing `krb5` dependency can be 
brittle: setting `sys.modules['krb5'] = None` may not consistently force 
`import krb5` to raise `ImportError` across Python/importlib behaviors, which 
can make the test flaky (especially if `krb5` is installed in the test 
environment). A more deterministic approach is to patch the import mechanism 
for `krb5` (e.g., patch `builtins.__import__` to raise `ImportError` for name 
`'krb5'`, or patch a helper you control) so the exception path is guaranteed.



##########
providers/samba/src/airflow/providers/samba/hooks/samba.py:
##########
@@ -325,12 +352,22 @@ def get_ui_field_behaviour(cls) -> dict[str, Any]:
     def get_connection_form_widgets(cls) -> dict[str, Any]:
         """Return connection widgets to add to connection form."""
         from flask_babel import lazy_gettext
-        from wtforms import StringField
+        from wtforms import SelectField, StringField
 
         return {
             "share_type": StringField(
                 label=lazy_gettext("Share Type"),
                 description="The share OS type (`posix` or `windows`). Used to 
determine the formatting of file and folder paths.",
                 default="posix",
-            )
+            ),
+            "auth_protocol": SelectField(
+                label=lazy_gettext("Auth Protocol"),
+                description=(
+                    "Authentication protocol: `negotiate` (auto-select, 
default), "
+                    "`ntlm`, or `kerberos`. When using `kerberos`, the 
system's "
+                    "Kerberos ticket cache is used and username/password are 
optional."
+                ),
+                choices=["negotiate", "ntlm", "kerberos"],

Review Comment:
   For WTForms `SelectField`, using `choices` as `(value, label)` pairs is more 
explicit and makes it easier to localize labels via `lazy_gettext` while 
keeping stable stored values. Consider changing `choices` to tuples like 
`[(\"negotiate\", lazy_gettext(\"negotiate\")), ...]` (or non-localized labels 
if you prefer) to avoid ambiguity and improve UI/i18n maintainability.
   ```suggestion
                   choices=[
                       ("negotiate", lazy_gettext("negotiate")),
                       ("ntlm", lazy_gettext("ntlm")),
                       ("kerberos", lazy_gettext("kerberos")),
                   ],
   ```



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]

Reply via email to