Repository: apache/airflow

Issue:
Allow for specification of cipher in SftpHook
<!--

Welcome to Apache Airflow!  For a smooth issue process, try to answer the following questions.
Don't worry if they're not all applicable; just try to include what you can :-)

If you need to include code snippets or logs, please put them in fenced code
blocks.  If they're super-long, please use the details tag like
<details><summary>super-long log</summary> lots of stuff </details>

Please delete these comment blocks before submitting the issue.

-->

**Description**

I want to specify the cipher to use when creating a SftpHook. Currently this is not possible because there is not a way to propagate the value to CnOpts. 

https://github.com/apache/airflow/blob/master/airflow/providers/sftp/hooks/sftp.py
https://pysftp.readthedocs.io/en/release_0.2.9/pysftp.html?#pysftp.CnOpts
https://pysftp.readthedocs.io/en/release_0.2.9/cookbook.html

**Use case / motivation**

<!-- What do you want to happen?

Rather than telling us how you might implement this solution, try to take a
step back and describe what you are trying to achieve.

-->

I want to be able to access SFTP servers that have disabled access from connection with weak ciphers.



Discussion:
Here's some [unit tests](https://github.com/ryhsiao/pysftp/blob/master/tests/test_ciphers.py) in pysftp using copts and ciphers.


@wolfier Looks interesting. Can I assign you to this issue? We are open to pull requests from external contributors.

Resolving patch:
diff --git a/airflow/providers/sftp/hooks/sftp.py b/airflow/providers/sftp/hooks/sftp.py
--- a/airflow/providers/sftp/hooks/sftp.py
+++ b/airflow/providers/sftp/hooks/sftp.py
@@ -51,6 +51,7 @@ def __init__(self, ftp_conn_id: str = 'sftp_default', *args, **kwargs) -> None:
 
         self.conn = None
         self.private_key_pass = None
+        self.ciphers = None
 
         # Fail for unverified hosts, unless this is explicitly allowed
         self.no_host_key_check = False
@@ -80,6 +81,9 @@ def __init__(self, ftp_conn_id: str = 'sftp_default', *args, **kwargs) -> None:
 
                 if 'no_host_key_check' in extra_options:
                     self.no_host_key_check = str(extra_options['no_host_key_check']).lower() == 'true'
+                    
+                if 'ciphers' in extra_options:
+                    self.ciphers = extra_options['ciphers']
 
                 if 'private_key' in extra_options:
                     warnings.warn(
@@ -98,6 +102,7 @@ def get_conn(self) -> pysftp.Connection:
             if self.no_host_key_check:
                 cnopts.hostkeys = None
             cnopts.compression = self.compress
+            cnopts.ciphers = self.ciphers
             conn_params = {
                 'host': self.remote_host,
                 'port': self.port,


Test patch:
diff --git a/tests/providers/sftp/hooks/test_sftp.py b/tests/providers/sftp/hooks/test_sftp.py
--- a/tests/providers/sftp/hooks/test_sftp.py
+++ b/tests/providers/sftp/hooks/test_sftp.py
@@ -145,6 +145,14 @@ def test_no_host_key_check_disabled(self, get_connection):
         get_connection.return_value = connection
         hook = SFTPHook()
         self.assertEqual(hook.no_host_key_check, False)
+        
+    @mock.patch('airflow.providers.sftp.hooks.sftp.SFTPHook.get_connection')
+    def test_ciphers(self, get_connection):
+        connection = Connection(login='login', host='host', extra='{"ciphers": ["A", "B", "C"]}')
+
+        get_connection.return_value = connection
+        hook = SFTPHook()
+        self.assertEqual(hook.ciphers, ["A", "B", "C"])
 
     @mock.patch('airflow.providers.sftp.hooks.sftp.SFTPHook.get_connection')
     def test_no_host_key_check_disabled_for_all_but_true(self, get_connection):