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

jscheffl 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 c47c0817d8e Remove unused NullFernet from Crypto (#57988)
c47c0817d8e is described below

commit c47c0817d8e11f885f4eef91232c08e1469e8c95
Author: Jens Scheffler <[email protected]>
AuthorDate: Sun Nov 9 00:23:34 2025 +0100

    Remove unused NullFernet from Crypto (#57988)
---
 airflow-core/src/airflow/models/connection.py      | 14 +----
 airflow-core/src/airflow/models/crypto.py          | 61 ++--------------------
 airflow-core/src/airflow/models/variable.py        |  2 +-
 airflow-core/tests/unit/always/test_connection.py  | 12 -----
 .../cli/commands/test_rotate_fernet_key_command.py | 24 ---------
 airflow-core/tests/unit/models/test_variable.py    | 14 -----
 6 files changed, 6 insertions(+), 121 deletions(-)

diff --git a/airflow-core/src/airflow/models/connection.py 
b/airflow-core/src/airflow/models/connection.py
index f64c38b5efe..3f9e5e3deed 100644
--- a/airflow-core/src/airflow/models/connection.py
+++ b/airflow-core/src/airflow/models/connection.py
@@ -343,11 +343,6 @@ class Connection(Base, LoggingMixin):
         """Return encrypted password."""
         if self._password and self.is_encrypted:
             fernet = get_fernet()
-            if not fernet.is_encrypted:
-                raise AirflowException(
-                    f"Can't decrypt encrypted password for login={self.login}  
"
-                    f"FERNET_KEY configuration is missing"
-                )
             return fernet.decrypt(bytes(self._password, "utf-8")).decode()
         return self._password
 
@@ -356,7 +351,7 @@ class Connection(Base, LoggingMixin):
         if value:
             fernet = get_fernet()
             self._password = fernet.encrypt(bytes(value, "utf-8")).decode()
-            self.is_encrypted = fernet.is_encrypted
+            self.is_encrypted = True
 
     @declared_attr
     def password(cls):
@@ -367,11 +362,6 @@ class Connection(Base, LoggingMixin):
         """Return encrypted extra-data."""
         if self._extra and self.is_extra_encrypted:
             fernet = get_fernet()
-            if not fernet.is_encrypted:
-                raise AirflowException(
-                    f"Can't decrypt `extra` params for login={self.login}, "
-                    f"FERNET_KEY configuration is missing"
-                )
             extra_val = fernet.decrypt(bytes(self._extra, "utf-8")).decode()
         else:
             extra_val = self._extra
@@ -385,7 +375,7 @@ class Connection(Base, LoggingMixin):
             self._validate_extra(value, self.conn_id)
             fernet = get_fernet()
             self._extra = fernet.encrypt(bytes(value, "utf-8")).decode()
-            self.is_extra_encrypted = fernet.is_encrypted
+            self.is_extra_encrypted = True
         else:
             self._extra = value
             self.is_extra_encrypted = False
diff --git a/airflow-core/src/airflow/models/crypto.py 
b/airflow-core/src/airflow/models/crypto.py
index c62446b7631..31423bb8444 100644
--- a/airflow-core/src/airflow/models/crypto.py
+++ b/airflow-core/src/airflow/models/crypto.py
@@ -30,8 +30,6 @@ log = logging.getLogger(__name__)
 class FernetProtocol(Protocol):
     """This class is only used for TypeChecking (for IDEs, mypy, etc)."""
 
-    is_encrypted: bool
-
     def decrypt(self, msg: bytes | str, ttl: int | None = None) -> bytes:
         """Decrypt with Fernet."""
         ...
@@ -40,57 +38,9 @@ class FernetProtocol(Protocol):
         """Encrypt with Fernet."""
         ...
 
-
-class _NullFernet:
-    """
-    A "Null" encryptor class that doesn't encrypt or decrypt but that presents 
a similar interface to Fernet.
-
-    The purpose of this is to make the rest of the code not have to know the
-    difference, and to only display the message once, not 20 times when
-    `airflow db migrate` is run.
-    """
-
-    is_encrypted = False
-
-    def decrypt(self, msg: bytes | str, ttl: int | None = None) -> bytes:
-        """Decrypt with Fernet."""
-        if isinstance(msg, bytes):
-            return msg
-        if isinstance(msg, str):
-            return msg.encode("utf-8")
-        raise ValueError(f"Expected bytes or str, got {type(msg)}")
-
-    def encrypt(self, msg: bytes) -> bytes:
-        """Encrypt with Fernet."""
-        return msg
-
-
-class _RealFernet:
-    """
-    A wrapper around the real Fernet to set is_encrypted to True.
-
-    This class is only used internally to avoid changing the interface of
-    the get_fernet function.
-    """
-
-    from cryptography.fernet import Fernet, MultiFernet
-
-    is_encrypted = True
-
-    def __init__(self, fernet: MultiFernet):
-        self._fernet = fernet
-
-    def decrypt(self, msg: bytes | str, ttl: int | None = None) -> bytes:
-        """Decrypt with Fernet."""
-        return self._fernet.decrypt(msg, ttl)
-
-    def encrypt(self, msg: bytes) -> bytes:
-        """Encrypt with Fernet."""
-        return self._fernet.encrypt(msg)
-
     def rotate(self, msg: bytes | str) -> bytes:
         """Rotate the Fernet key for the given message."""
-        return self._fernet.rotate(msg)
+        ...
 
 
 @cache
@@ -107,12 +57,7 @@ def get_fernet() -> FernetProtocol:
     from cryptography.fernet import Fernet, MultiFernet
 
     try:
-        fernet_key = conf.get("core", "FERNET_KEY")
-        if not fernet_key:
-            log.warning("empty cryptography key - values will not be stored 
encrypted.")
-            return _NullFernet()
-
-        fernet = MultiFernet([Fernet(fernet_part.encode("utf-8")) for 
fernet_part in fernet_key.split(",")])
-        return _RealFernet(fernet)
+        fernet_key = conf.get_mandatory_value("core", "FERNET_KEY")
+        return MultiFernet([Fernet(fernet_part.encode("utf-8")) for 
fernet_part in fernet_key.split(",")])
     except (ValueError, TypeError) as value_error:
         raise AirflowException(f"Could not create Fernet object: 
{value_error}")
diff --git a/airflow-core/src/airflow/models/variable.py 
b/airflow-core/src/airflow/models/variable.py
index a13eb4fe158..c5037561cbc 100644
--- a/airflow-core/src/airflow/models/variable.py
+++ b/airflow-core/src/airflow/models/variable.py
@@ -97,7 +97,7 @@ class Variable(Base, LoggingMixin):
         if value is not None:
             fernet = get_fernet()
             self._val = fernet.encrypt(bytes(value, "utf-8")).decode()
-            self.is_encrypted = fernet.is_encrypted
+            self.is_encrypted = True
 
     @declared_attr
     def val(cls):
diff --git a/airflow-core/tests/unit/always/test_connection.py 
b/airflow-core/tests/unit/always/test_connection.py
index d613c0d77da..3f3b3af4bfa 100644
--- a/airflow-core/tests/unit/always/test_connection.py
+++ b/airflow-core/tests/unit/always/test_connection.py
@@ -109,18 +109,6 @@ class TestConnection:
     def teardown_method(self):
         self.patcher.stop()
 
-    @conf_vars({("core", "fernet_key"): ""})
-    def test_connection_extra_no_encryption(self):
-        """
-        Tests extras on a new connection without encryption. The fernet key
-        is set to a non-base64-encoded string and the extra is stored without
-        encryption.
-        """
-        crypto.get_fernet.cache_clear()
-        test_connection = Connection(extra='{"apache": "airflow"}')
-        assert not test_connection.is_extra_encrypted
-        assert test_connection.extra == '{"apache": "airflow"}'
-
     @conf_vars({("core", "fernet_key"): Fernet.generate_key().decode()})
     def test_connection_extra_with_encryption(self):
         """
diff --git 
a/airflow-core/tests/unit/cli/commands/test_rotate_fernet_key_command.py 
b/airflow-core/tests/unit/cli/commands/test_rotate_fernet_key_command.py
index dbd250c2c2d..b2638208fa3 100644
--- a/airflow-core/tests/unit/cli/commands/test_rotate_fernet_key_command.py
+++ b/airflow-core/tests/unit/cli/commands/test_rotate_fernet_key_command.py
@@ -49,14 +49,8 @@ class TestRotateFernetKeyCommand:
     def test_should_rotate_variable(self, session):
         fernet_key1 = Fernet.generate_key()
         fernet_key2 = Fernet.generate_key()
-        var1_key = f"{__file__}_var1"
         var2_key = f"{__file__}_var2"
 
-        # Create unencrypted variable
-        with conf_vars({("core", "fernet_key"): ""}):
-            get_fernet.cache_clear()  # Clear cached fernet
-            Variable.set(key=var1_key, value="value")
-
         # Create encrypted variable
         with conf_vars({("core", "fernet_key"): fernet_key1.decode()}):
             get_fernet.cache_clear()  # Clear cached fernet
@@ -71,25 +65,14 @@ class TestRotateFernetKeyCommand:
         # Assert correctness using a new fernet key
         with conf_vars({("core", "fernet_key"): fernet_key2.decode()}):
             get_fernet.cache_clear()  # Clear cached fernet
-            var1 = session.query(Variable).filter(Variable.key == 
var1_key).first()
-            # Unencrypted variable should be unchanged
-            assert Variable.get(key=var1_key) == "value"
-            assert var1._val == "value"
             assert Variable.get(key=var2_key) == "value"
 
     @provide_session
     def test_should_rotate_connection(self, session, mock_supervisor_comms):
         fernet_key1 = Fernet.generate_key()
         fernet_key2 = Fernet.generate_key()
-        var1_key = f"{__file__}_var1"
         var2_key = f"{__file__}_var2"
 
-        # Create unencrypted variable
-        with conf_vars({("core", "fernet_key"): ""}):
-            get_fernet.cache_clear()  # Clear cached fernet
-            session.add(Connection(conn_id=var1_key, 
uri="mysql://user:pass@localhost"))
-            session.commit()
-
         # Create encrypted variable
         with conf_vars({("core", "fernet_key"): fernet_key1.decode()}):
             get_fernet.cache_clear()  # Clear cached fernet
@@ -119,17 +102,10 @@ class TestRotateFernetKeyCommand:
                 )
             raise Exception(f"Connection {conn_id} not found")
 
-        # Mock the send method to return our connection data
-        mock_supervisor_comms.send.return_value = mock_get_connection(var1_key)
-
         # Assert correctness using a new fernet key
         with conf_vars({("core", "fernet_key"): fernet_key2.decode()}):
             get_fernet.cache_clear()  # Clear cached fernet
 
-            # Unencrypted variable should be unchanged
-            conn1: Connection = BaseHook.get_connection(var1_key)
-            assert conn1.password == "pass"
-
             # Mock for the second connection
             mock_supervisor_comms.send.return_value = 
mock_get_connection(var2_key)
             assert BaseHook.get_connection(var2_key).password == "pass"
diff --git a/airflow-core/tests/unit/models/test_variable.py 
b/airflow-core/tests/unit/models/test_variable.py
index b02a760a665..6ae364be503 100644
--- a/airflow-core/tests/unit/models/test_variable.py
+++ b/airflow-core/tests/unit/models/test_variable.py
@@ -52,20 +52,6 @@ class TestVariable:
             yield
         db.clear_db_variables()
 
-    @conf_vars({("core", "fernet_key"): "", ("core", "unit_test_mode"): 
"True"})
-    def test_variable_no_encryption(self, session):
-        """
-        Test variables without encryption
-        """
-        crypto.get_fernet.cache_clear()
-        Variable.set(key="key", value="value", session=session)
-        test_var = session.query(Variable).filter(Variable.key == "key").one()
-        assert not test_var.is_encrypted
-        assert test_var.val == "value"
-        # We always call mask_secret for variables, and let the SecretsMasker 
decide based on the name if it
-        # should mask anything. That logic is tested in test_secrets_masker.py
-        self.mask_secret.assert_called_once_with("value", "key")
-
     @conf_vars({("core", "fernet_key"): Fernet.generate_key().decode()})
     def test_variable_with_encryption(self, session):
         """

Reply via email to