Sharad Chavali
Sharad Chavali

Reputation: 1

How to connect Apache Superset 4.1.1. to LDAP server?

I am using Apache Superset 4.1.1. docker compose setup and trying to connect Superset to LDAP using ldapjdbc driver. I am using openldap jdbc driver, also used CData’s ldap jdbc driver.

I wrote a SQLAlchemy custom dialect which uses jaydebeapi DBAPI implementation to connect using JDBC driver. I added the Database type Other and specified custom dialect, and Apache superset is able to connect to the database successfully when tested with Test Connection. But when I go to Dataset and select the Database (Other), it gets the schema but it fails to retrieve the tables

The Setup

—————————

I tested the JDBC driver for SQL queries and it works fine. Since the schema and table queries might be different for different drivers, I am trying to first test the custom dialect with hardcoded schema and table names.

Following is my custom dialect :

'


import jaydebeapi
from sqlalchemy.engine import default
from sqlalchemy.engine.url import URL
import logging

logging.basicConfig()
logging.getLogger('sqlalchemy.engine').setLevel(logging.INFO)

class CustomJDBCDialect(default.DefaultDialect):
    driver='jaydebeapi'
    name='custom_jdbc'
    supports_alter = False
    supports_sequences = False
    preexecute_autoincrement_sequences = False
    supports_native_boolean = False

    def create_connect_args(self, url: URL):
        jdbc_driver='cdata.jdbc.ldap.LDAPDriver’        # even used openldap’s ldapjdbc driver too.
        jdbc_url = 'jdbc:ldap:user=uid=testuser;password=<passwordhere>;Server=host.docker.internal;Port=636;BaseDN=ou=users,dc=test,dc=com;LDAPVersion=3;'
        jdbc_creds = ['uid=testuser’, ‘<passwordhere>’]
        jar_path = '/app/pythonpath/LDAPJDBCDriver/lib/cdata.jdbc.ldap.jar'
        return ([jdbc_driver, jdbc_url, jdbc_creds, jar_path], {})
    @classmethod
    def dbapi(cls):
        return jaydebeapi

    def get_connection(self, *args, **kwargs):
        jdbc_driver='cdata.jdbc.ldap.LDAPDriver'
        jdbc_url = 'jdbc:ldap:user=uid=testuser;password=<passwordhere>;Server=host.docker.internal;Port=636;BaseDN=ou=users,dc=test,dc=com;LDAPVersion=3;''
        jdbc_creds = ['uid=testuser’, ‘<passwordhere>’]
        jar_path = '/app/pythonpath/LDAPJDBCDriver/lib/cdata.jdbc.ldap.jar'

        return jaydebeapi.connect(jdbc_driver, jdbc_url, ['uid=testuser’, ‘<‘passwordhere>], '/app/pythonpath/LDAPJDBCDriver/lib/cdata.jdbc.ldap.jar')

    def get_schema_names(self, connection, **kw):
        return ['publicschema']

    def has_table(self, connection, table_name, **kwargs):
        return true

    def get_table_names(self, connection, schema=None, **kw):
        """
        Fetch the list of table names from the database.
        """
        #cursor = connection.cursor()
        #cursor.execute('SELECT CN FROM person')
        #return [row[0] for row in cursor.fetchall()]
        return ['table1','table2','table3']
    
    def get_columns(self, connection, table_name, schema=None, **kw):
        columns_info = {
            'table1': [
                {'name': 'id', 'type': 'INTEGER', 'nullable': False, 'default': None},
                {'name': 'name', 'type': 'VARCHAR', 'nullable': True, 'default': None},
            ],
            'table2': [
                {'name': 'id', 'type': 'INTEGER', 'nullable': False, 'default': None},
                {'name': 'description', 'type': 'TEXT', 'nullable': True, 'default': None},
            ],
            'table3': [
                {'name': 'id', 'type': 'INTEGER', 'nullable': False, 'default': None},
                {'name': 'value', 'type': 'FLOAT', 'nullable': True, 'default': 0.0},
            ],
        }
        return columns_info.get(table_name, [])


    def do_begin(self, dbapi_connection):
        # Override to do nothing
        pass

    def do_commit(self, dbapi_connection):
        # Override to do nothing
        pass

    def do_rollback(self, dbapi_connection):
        # Override to do nothing
        pass

# Register the dialect
from sqlalchemy.dialects import registry
#registry.register("custom_jdbc", __name__, "CustomJDBCDialect")

'

The Problem/Issue

——————————

The apache superset docker shows the following logs, from which I couldn’t deduce anything :
'

INFO:sqlalchemy.engine.Engine:[cached since 359.9s ago] {'action': 'DatasetRestApi.get_list', 'user_id': 1, 'dashboard_id': 0, 'slice_id': 0, 'json': '{"path": "/api/v1/dataset/", "q": "(filters:!((col:database,opr:rel_o_m,value:2),(col:schema,opr:eq,value:publicschema),(col:sql,opr:dataset_is_null_ ... (89 characters truncated) ...  "value": 2}, {"col": "schema", "opr": "eq", "value": "publicschema"}, {"col": "sql", "opr": "dataset_is_null_or_empty", "value": true}], "page": 0}}', 'dttm': datetime.datetime(2025, 2, 19, 4, 18, 29, 588733), 'duration_ms': 15, 'referrer': 'http://localhost:8088/dataset/add/'}

superset_app          | INFO:sqlalchemy.engine.Engine:SELECT ssh_tunnels.uuid AS ssh_tunnels_uuid, ssh_tunnels.created_on AS ssh_tunnels_created_on, ssh_tunnels.changed_on AS ssh_tunnels_changed_on, ssh_tunnels.extra_json AS ssh_tunnels_extra_json, ssh_tunnels.id AS ssh_tunnels_id, ssh_tunnels.database_id AS ssh_tunnels_database_id, ssh_tunnels.server_address AS ssh_tunnels_server_address, ssh_tunnels.server_port AS ssh_tunnels_server_port, ssh_tunnels.username AS ssh_tunnels_username, ssh_tunnels.password AS ssh_tunnels_password, ssh_tunnels.private_key AS ssh_tunnels_private_key, ssh_tunnels.private_key_password AS ssh_tunnels_private_key_password, ssh_tunnels.created_by_fk AS ssh_tunnels_created_by_fk, ssh_tunnels.changed_by_fk AS ssh_tunnels_changed_by_fk

superset_app          | FROM ssh_tunnels

superset_app          | WHERE ssh_tunnels.database_id = %(database_id_1)s

superset_app          | INFO:sqlalchemy.engine.Engine:[cached since 330.4s ago] {'database_id_1': 2}

superset_app          | INFO:sqlalchemy.engine.Engine:COMMIT

superset_app          | DEBUG:superset.models.core:Database._get_sqla_engine(). Masked URL: custom_jdbc://host.docker.internal:636/

superset_app          | INFO:werkzeug:172.19.0.1 - - [19/Feb/2025 04:18:29] "GET /api/v1/dataset/?q=(filters:!((col:database,opr:rel_o_m,value:2),(col:schema,opr:eq,value:publicschema),(col:sql,opr:dataset_is_null_or_empty,value:!t)),page:0) HTTP/1.1" 200 -

superset_app          | INFO:sqlalchemy.engine.Engine:INSERT INTO logs (action, user_id, dashboard_id, slice_id, json, dttm, duration_ms, referrer) VALUES (%(action)s, %(user_id)s, %(dashboard_id)s, %(slice_id)s, %(json)s, %(dttm)s, %(duration_ms)s, %(referrer)s) RETURNING logs.id

superset_app          | INFO:sqlalchemy.engine.Engine:[cached since 359.9s ago] {'action': 'DatabaseRestApi.tables', 'user_id': 1, 'dashboard_id': 0, 'slice_id': 0, 'json': '{"path": "/api/v1/database/2/tables/", "q": "(force:!f,schema_name:publicschema)", "url_rule": "/api/v1/database/<int:pk>/tables/", "object_ref": "DatabaseRestApi.tables", "pk": 2, "rison": {"force": false, "schema_name": "publicschema"}}', 'dttm': datetime.datetime(2025, 2, 19, 4, 18, 29, 593908), 'duration_ms': 26, 'referrer': 'http://localhost:8088/dataset/add/'}

superset_app          | INFO:sqlalchemy.engine.Engine:COMMIT

superset_app          | DEBUG:superset.stats_logger:[stats_logger] (incr) DatabaseRestApi.tables.warning

superset_app          | DEBUG:superset.stats_logger:[stats_logger] (timing) DatabaseRestApi.tables.time | 27.744207996875048

superset_app          | INFO:werkzeug:172.19.0.1 - - [19/Feb/2025 04:18:29] "GET /api/v1/database/2/tables/?q=(force:!f,schema_name:publicschema) HTTP/1.1" 422 -

superset_app          | WARNING:superset.views.error_handling:HTTPException

superset_app          | Traceback (most recent call last):

superset_app          |   File "/usr/local/lib/python3.10/site-packages/flask/app.py", line 1484, in full_dispatch_request

superset_app          |     rv = self.dispatch_request()

superset_app          |   File "/usr/local/lib/python3.10/site-packages/flask/app.py", line 1458, in dispatch_request

superset_app          |     self.raise_routing_exception(req)

superset_app          |   File "/usr/local/lib/python3.10/site-packages/flask/app.py", line 1440, in raise_routing_exception

superset_app          |     raise request.routing_exception  # type: ignore

superset_app          |   File "/usr/local/lib/python3.10/site-packages/flask/ctx.py", line 353, in match_request

superset_app          |     result = self.url_adapter.match(return_rule=True)  # type: ignore

superset_app          |   File "/usr/local/lib/python3.10/site-packages/werkzeug/routing/map.py", line 629, in match

superset_app          |     raise NotFound() from None

superset_app          | werkzeug.exceptions.NotFound: 404 Not Found: The requested URL was not found on the server. If you entered the URL manually please check your spelling and try again.

superset_app          | INFO:werkzeug:172.19.0.1 - - [19/Feb/2025 04:18:33] "GET /ws HTTP/1.1" 404 -

superset_app          | DEBUG:superset.stats_logger:[stats_logger] (incr) health

superset_app          | INFO:werkzeug:127.0.0.1 - - [19/Feb/2025 04:18:52] "GET /health HTTP/1.1" 200 -

superset_worker_beat  | [2025-02-19 04:19:00,053: INFO/MainProcess] Scheduler: Sending due task reports.scheduler (reports.scheduler)

'

I was thinking if jaydebeapi could log something, but looks like there is no logging in jaydebeapi implementation.

Is there any issue or anything missing with my custom dialect implementation? Is there any resolution or workaround to this?

Are there any other ways to connect Apache Superset to LDAP server ?

Upvotes: 0

Views: 43

Answers (0)

Related Questions