Repository: ibis-project/ibis

Issue:
bug: Impala backend throws spurious cert error
### What happened?

Hi there - I'm trying out Ibis at my organization for use with our Impala instance. I tried to establish a connection to the server with SSL enabled, but _without_ CA validation, as I was working in a container without the necessary CA cert in place.

I tried establishing a connection as follows:
```python
con = ibis.impala.connect(
    host="my.impala.server", 
    port=21050,
    use_ssl=True,
    ca_cert=None,
    auth_mechanism="GSSAPI",
    database="database",
)
```
My read of [the docs](https://ibis-project.org/backends/Impala/#ibis.backends.impala.Backend.do_connect) is that the `ca_cert=None` directive should cause Ibis to establish a connection without validating against  CA certs. I instead get an error that complains it cannot verify the location of the CA certs
```
failed to initialize SSL
Traceback (most recent call last):
  File "/home/vscode/.local/lib/python3.9/site-packages/thrift/transport/TSSLSocket.py", line 293, in _do_open
    return self._wrap_socket(plain_sock)
  File "/home/vscode/.local/lib/python3.9/site-packages/thrift/transport/TSSLSocket.py", line 181, in _wrap_socket
    self.ssl_context.load_verify_locations(self.ca_certs)
FileNotFoundError: [Errno 2] No such file or directory
```

I suspect [this line](https://github.com/ibis-project/ibis/blob/a7e845ac9a2c2069135c38a923babc1f1d6de65c/ibis/backends/impala/__init__.py#L289) may be the culprit - it looks like it is casting `None` to the string `"None"`, which is then interpreted as a path. I can try to create a patch if it would be helpful, though I am brand new to the codebase at this point.

### What version of ibis are you using?

ibis-framework==5.1.0

### What backend(s) are you using, if any?

Impala

### Relevant log output

```sh
failed to initialize SSL
Traceback (most recent call last):
  File "/home/vscode/.local/lib/python3.9/site-packages/thrift/transport/TSSLSocket.py", line 293, in _do_open
    return self._wrap_socket(plain_sock)
  File "/home/vscode/.local/lib/python3.9/site-packages/thrift/transport/TSSLSocket.py", line 181, in _wrap_socket
    self.ssl_context.load_verify_locations(self.ca_certs)
FileNotFoundError: [Errno 2] No such file or directory
---------------------------------------------------------------------------
FileNotFoundError                         Traceback (most recent call last)
File ~/.local/lib/python3.9/site-packages/thrift/transport/TSSLSocket.py:293, in TSSLSocket._do_open(self, family, socktype)
    292 try:
--> 293     return self._wrap_socket(plain_sock)
    294 except Exception as ex:

File ~/.local/lib/python3.9/site-packages/thrift/transport/TSSLSocket.py:181, in TSSLBase._wrap_socket(self, sock)
    180     if self.ca_certs:
--> 181         self.ssl_context.load_verify_locations(self.ca_certs)
    182 return self.ssl_context.wrap_socket(
    183     sock, server_side=self._server_side,
    184     server_hostname=self._server_hostname)

FileNotFoundError: [Errno 2] No such file or directory

During handling of the above exception, another exception occurred:

TTransportException                       Traceback (most recent call last)
Cell In[13], line 1
----> 1 con = ibis.impala.connect(
      2         host="my.impala.server", 
      3         port=21050,
      4         use_ssl=True,
      5         ca_cert=None,
      6         auth_mechanism="GSSAPI",
      7         database="database",
      8         )

File ~/.local/lib/python3.9/site-packages/ibis/__init__.py:97, in __getattr__.<locals>.connect(*args, **kwargs)
     96 def connect(*args, **kwargs):
---> 97     return backend.connect(*args, **kwargs)

File ~/.local/lib/python3.9/site-packages/ibis/backends/base/__init__.py:530, in BaseBackend.connect(self, *args, **kwargs)
    507 """Connect to the database.
    508 
    509 Parameters
   (...)
    527     An instance of the backend
    528 """
    529 new_backend = self.__class__(*args, **kwargs)
--> 530 new_backend.reconnect()
    531 return new_backend

File ~/.local/lib/python3.9/site-packages/ibis/backends/base/__init__.py:545, in BaseBackend.reconnect(self)
    543 def reconnect(self) -> None:
    544     """Reconnect to the database already configured with connect."""
--> 545     self.do_connect(*self._con_args, **self._con_kwargs)

File ~/.local/lib/python3.9/site-packages/ibis/backends/impala/__init__.py:296, in Backend.do_connect(self, host, port, database, timeout, use_ssl, ca_cert, user, password, auth_mechanism, kerberos_service_name, pool_size, hdfs_client)
    282 params = {
    283     'host': host,
    284     'port': port,
   (...)
    292     'kerberos_service_name': kerberos_service_name,
    293 }
    294 self.con = ImpalaConnection(pool_size=pool_size, **params)
--> 296 self._ensure_temp_db_exists()

File ~/.local/lib/python3.9/site-packages/ibis/backends/impala/__init__.py:840, in Backend._ensure_temp_db_exists(self)
    837 def _ensure_temp_db_exists(self):
    838     # TODO: session memoize to avoid unnecessary `SHOW DATABASES` calls
    839     name, path = options.impala.temp_db, options.impala.temp_hdfs_path
--> 840     if name not in self.list_databases():
    841         if self._hdfs is not None:
    842             self.create_database(name, path=path, force=True)

File ~/.local/lib/python3.9/site-packages/ibis/backends/impala/__init__.py:306, in Backend.list_databases(self, like)
    305 def list_databases(self, like=None):
--> 306     cur = self.raw_sql('SHOW DATABASES')
    307     databases = self._get_list(cur)
    308     cur.release()

File ~/.local/lib/python3.9/site-packages/ibis/backends/base/sql/__init__.py:134, in BaseSQLBackend.raw_sql(self, query)
    124 def raw_sql(self, query: str):
    125     """Execute a query string.
    126 
    127     !!! warning "The returned cursor object must be **manually** released if results are returned."
   (...)
    132         DDL or DML statement
    133     """
--> 134     cursor = self.con.execute(query)
    135     if cursor:
    136         return cursor

File ~/.local/lib/python3.9/site-packages/ibis/backends/impala/client.py:83, in ImpalaConnection.execute(self, query)
     79     query = query.compile()
     81 util.log(query)
---> 83 cursor = self.pool.connect()
     84 try:
     85     cursor.execute(query)

File ~/.local/lib/python3.9/site-packages/sqlalchemy/pool/base.py:455, in Pool.connect(self)
    447 def connect(self) -> PoolProxiedConnection:
    448     """Return a DBAPI connection from the pool.
    449 
    450     The connection is instrumented such that when its
   (...)
    453 
    454     """
--> 455     return _ConnectionFairy._checkout(self)

File ~/.local/lib/python3.9/site-packages/sqlalchemy/pool/base.py:1271, in _ConnectionFairy._checkout(cls, pool, threadconns, fairy)
   1262 @classmethod
   1263 def _checkout(
   1264     cls,
   (...)
   1267     fairy: Optional[_ConnectionFairy] = None,
   1268 ) -> _ConnectionFairy:
   1270     if not fairy:
-> 1271         fairy = _ConnectionRecord.checkout(pool)
   1273         if threadconns is not None:
   1274             threadconns.current = weakref.ref(fairy)

File ~/.local/lib/python3.9/site-packages/sqlalchemy/pool/base.py:719, in _ConnectionRecord.checkout(cls, pool)
    717     rec = cast(_ConnectionRecord, pool._do_get())
    718 else:
--> 719     rec = pool._do_get()
    721 try:
    722     dbapi_connection = rec.get_connection()

File ~/.local/lib/python3.9/site-packages/sqlalchemy/pool/impl.py:169, in QueuePool._do_get(self)
    167     except:
    168         with util.safe_reraise():
--> 169             self._dec_overflow()
    170         raise
    171 else:

File ~/.local/lib/python3.9/site-packages/sqlalchemy/util/langhelpers.py:147, in safe_reraise.__exit__(self, type_, value, traceback)
    145     assert exc_value is not None
    146     self._exc_info = None  # remove potential circular references
--> 147     raise exc_value.with_traceback(exc_tb)
    148 else:
    149     self._exc_info = None  # remove potential circular references

File ~/.local/lib/python3.9/site-packages/sqlalchemy/pool/impl.py:166, in QueuePool._do_get(self)
    164 if self._inc_overflow():
    165     try:
--> 166         return self._create_connection()
    167     except:
    168         with util.safe_reraise():

File ~/.local/lib/python3.9/site-packages/sqlalchemy/pool/base.py:396, in Pool._create_connection(self)
    393 def _create_connection(self) -> ConnectionPoolEntry:
    394     """Called by subclasses to create a new ConnectionRecord."""
--> 396     return _ConnectionRecord(self)

File ~/.local/lib/python3.9/site-packages/sqlalchemy/pool/base.py:681, in _ConnectionRecord.__init__(self, pool, connect)
    679 self.__pool = pool
    680 if connect:
--> 681     self.__connect()
    682 self.finalize_callback = deque()

File ~/.local/lib/python3.9/site-packages/sqlalchemy/pool/base.py:906, in _ConnectionRecord.__connect(self)
    904 except BaseException as e:
    905     with util.safe_reraise():
--> 906         pool.logger.debug("Error on connect(): %s", e)
    907 else:
    908     # in SQLAlchemy 1.4 the first_connect event is not used by
    909     # the engine, so this will usually not be set
    910     if pool.dispatch.first_connect:

File ~/.local/lib/python3.9/site-packages/sqlalchemy/util/langhelpers.py:147, in safe_reraise.__exit__(self, type_, value, traceback)
    145     assert exc_value is not None
    146     self._exc_info = None  # remove potential circular references
--> 147     raise exc_value.with_traceback(exc_tb)
    148 else:
    149     self._exc_info = None  # remove potential circular references

File ~/.local/lib/python3.9/site-packages/sqlalchemy/pool/base.py:901, in _ConnectionRecord.__connect(self)
    899 try:
    900     self.starttime = time.time()
--> 901     self.dbapi_connection = connection = pool._invoke_creator(self)
    902     pool.logger.debug("Created new connection %r", connection)
    903     self.fresh = True

File ~/.local/lib/python3.9/site-packages/sqlalchemy/pool/base.py:368, in Pool._should_wrap_creator.<locals>.<lambda>(rec)
    364 # all other cases, just wrap and assume legacy "creator" callable
    365 # thing
    366 else:
    367     creator_fn = cast(_CreatorFnType, creator)
--> 368     return lambda rec: creator_fn()

File ~/.local/lib/python3.9/site-packages/ibis/backends/impala/client.py:103, in ImpalaConnection._new_cursor(self)
    101 def _new_cursor(self):
    102     params = self.params.copy()
--> 103     con = impyla.connect(database=self.database, **params)
    105     # make sure the connection works
    106     cursor = con.cursor(user=params.get('user'), convert_types=True)

File ~/.local/lib/python3.9/site-packages/impala/dbapi.py:194, in connect(host, port, database, timeout, use_ssl, ca_cert, auth_mechanism, user, password, kerberos_service_name, use_ldap, ldap_user, ldap_password, use_kerberos, protocol, krb_host, use_http_transport, http_path, auth_cookie_names, http_cookie_names, retries, jwt, user_agent)
    190 elif http_cookie_names is None:
    191     # Set default value as the list of HTTP cookie names used by Impala and Hive.
    192     http_cookie_names = ['impala.auth', 'impala.session.id', 'hive.server2.auth']
--> 194 service = hs2.connect(host=host, port=port,
    195                       timeout=timeout, use_ssl=use_ssl,
    196                       ca_cert=ca_cert, user=user, password=password,
    197                       kerberos_service_name=kerberos_service_name,
    198                       auth_mechanism=auth_mechanism, krb_host=krb_host,
    199                       use_http_transport=use_http_transport,
    200                       http_path=http_path,
    201                       http_cookie_names=http_cookie_names,
    202                       retries=retries,
    203                       jwt=jwt, user_agent=user_agent)
    204 return hs2.HiveServer2Connection(service, default_db=database)

File ~/.local/lib/python3.9/site-packages/impala/hiveserver2.py:865, in connect(host, port, timeout, use_ssl, ca_cert, user, password, kerberos_service_name, auth_mechanism, krb_host, use_http_transport, http_path, http_cookie_names, retries, jwt, user_agent)
    861     log.debug('sock=%s', sock)
    862     transport = get_transport(sock, kerberos_host, kerberos_service_name,
    863                             auth_mechanism, user, password)
--> 865 transport.open()
    866 protocol = TBinaryProtocolAccelerated(transport)
    867 service = ThriftClient(protocol)

File ~/.local/lib/python3.9/site-packages/thrift_sasl/__init__.py:74, in TSaslClientTransport.open(self)
     72 def open(self):
     73   if not self.isOpen():
---> 74     self._trans.open()
     76   if self.sasl is not None:
     77     raise TTransportException(
     78       type=TTransportException.NOT_OPEN,
     79       message="Already open!")

File ~/.local/lib/python3.9/site-packages/thrift/transport/TSSLSocket.py:301, in TSSLSocket.open(self)
    300 def open(self):
--> 301     super(TSSLSocket, self).open()
    302     if self._should_verify:
    303         self.peercert = self.handle.getpeercert()

File ~/.local/lib/python3.9/site-packages/thrift/transport/TSocket.py:129, in TSocket.open(self)
    127     raise TTransportException(type=TTransportException.NOT_OPEN, message=msg, inner=gai)
    128 for family, socktype, _, _, sockaddr in addrs:
--> 129     handle = self._do_open(family, socktype)
    131     # TCP_KEEPALIVE
    132     if self._socket_keepalive:

File ~/.local/lib/python3.9/site-packages/thrift/transport/TSSLSocket.py:298, in TSSLSocket._do_open(self, family, socktype)
    296 msg = 'failed to initialize SSL'
    297 logger.exception(msg)
--> 298 raise TTransportException(type=TTransportException.NOT_OPEN, message=msg, inner=ex)

TTransportException: failed to initialize SSL
```


### Code of Conduct

- [X] I agree to follow this project's Code of Conduct


Discussion:
@noahg2 Welcome 👋🏻!

This does look like a bug! I'll put up a PR to address it. Would you mind testing it out? We don't have a great way to test an authenticated Impala setup.

Resolving patch:
diff --git a/ibis/backends/impala/__init__.py b/ibis/backends/impala/__init__.py
--- a/ibis/backends/impala/__init__.py
+++ b/ibis/backends/impala/__init__.py
@@ -280,19 +280,22 @@ def do_connect(
         self._temp_objects = set()
         self._hdfs = hdfs_client
 
-        params = {
-            'host': host,
-            'port': port,
-            'database': database,
-            'timeout': timeout,
-            'use_ssl': use_ssl,
-            'ca_cert': str(ca_cert),
-            'user': user,
-            'password': password,
-            'auth_mechanism': auth_mechanism,
-            'kerberos_service_name': kerberos_service_name,
-        }
-        self.con = ImpalaConnection(pool_size=pool_size, **params)
+        params = {}
+        if ca_cert is not None:
+            params["ca_cert"] = str(ca_cert)
+        self.con = ImpalaConnection(
+            pool_size=pool_size,
+            host=host,
+            port=port,
+            database=database,
+            timeout=timeout,
+            use_ssl=use_ssl,
+            user=user,
+            password=password,
+            auth_mechanism=auth_mechanism,
+            kerberos_service_name=kerberos_service_name,
+            **params,
+        )
 
         self._ensure_temp_db_exists()
 


Test patch: