This is an automated email from the ASF dual-hosted git repository. maximebeauchemin pushed a commit to branch master in repository https://gitbox.apache.org/repos/asf/incubator-superset.git
The following commit(s) were added to refs/heads/master by this push: new fdee06b Adding hook for external password store for databases (#3436) fdee06b is described below commit fdee06bbf2a3a9289dd92bbc88c919f68a81b9ea Author: fabianmenges <fabianmen...@users.noreply.github.com> AuthorDate: Wed Sep 13 23:59:03 2017 -0400 Adding hook for external password store for databases (#3436) --- docs/installation.rst | 18 ++++++++++++++++++ superset/config.py | 9 +++++++++ superset/models/core.py | 9 +++++++-- tests/core_tests.py | 14 ++++++++++++++ 4 files changed, 48 insertions(+), 2 deletions(-) diff --git a/docs/installation.rst b/docs/installation.rst index e04bd68..9cd3de6 100644 --- a/docs/installation.rst +++ b/docs/installation.rst @@ -284,6 +284,24 @@ on top of the **database**. For Superset to connect to a specific schema, there's a **schema** parameter you can set in the table form. +External Password store for SQLAlchemy connections +-------------------------------------------------- +It is possible to use an external store for you database passwords. This is +useful if you a running a custom secret distribution framework and do not wish +to store secrets in Superset's meta database. + +Example: +Write a function that takes a single argument of type ``sqla.engine.url`` and returns +the password for the given connection string. Then set ``SQLALCHEMY_CUSTOM_PASSWORD_STORE`` +in your config file to point to that function. :: + + def example_lookup_password(url): + secret = <<get password from external framework>> + return 'secret' + + SQLALCHEMY_CUSTOM_PASSWORD_STORE = example_lookup_password + + SSL Access to databases ----------------------- This example worked with a MySQL database that requires SSL. The configuration diff --git a/superset/config.py b/superset/config.py index 94d8dfc..d7125a4 100644 --- a/superset/config.py +++ b/superset/config.py @@ -60,6 +60,15 @@ SQLALCHEMY_DATABASE_URI = 'sqlite:///' + os.path.join(DATA_DIR, 'superset.db') # SQLALCHEMY_DATABASE_URI = 'mysql://myapp@localhost/myapp' # SQLALCHEMY_DATABASE_URI = 'postgresql://root:password@localhost/myapp' +# In order to hook up a custom password store for all SQLACHEMY connections +# implement a function that takes a single argument of type 'sqla.engine.url', +# returns a password and set SQLALCHEMY_CUSTOM_PASSWORD_STORE. +# +# e.g.: +# def lookup_password(url): +# return 'secret' +# SQLALCHEMY_CUSTOM_PASSWORD_STORE = lookup_password + # The limit of queries fetched for query search QUERY_SEARCH_LIMIT = 1000 diff --git a/superset/models/core.py b/superset/models/core.py index 9a38b25..637ed09 100644 --- a/superset/models/core.py +++ b/superset/models/core.py @@ -561,6 +561,7 @@ class Database(Model, AuditMixinNullable): } """)) perm = Column(String(1000)) + custom_password_store = config.get('SQLALCHEMY_CUSTOM_PASSWORD_STORE') def __repr__(self): return self.verbose_name if self.verbose_name else self.database_name @@ -581,7 +582,7 @@ class Database(Model, AuditMixinNullable): def set_sqlalchemy_uri(self, uri): password_mask = "X" * 10 conn = sqla.engine.url.make_url(uri) - if conn.password != password_mask: + if conn.password != password_mask and not self.custom_password_store: # do not over-write the password with the password mask self.password = conn.password conn.password = password_mask if conn.password else None @@ -725,7 +726,10 @@ class Database(Model, AuditMixinNullable): @property def sqlalchemy_uri_decrypted(self): conn = sqla.engine.url.make_url(self.sqlalchemy_uri) - conn.password = self.password + if self.custom_password_store: + conn.password = self.custom_password_store(conn) + else: + conn.password = self.password return str(conn) @property @@ -736,6 +740,7 @@ class Database(Model, AuditMixinNullable): return ( "[{obj.database_name}].(id:{obj.id})").format(obj=self) + sqla.event.listen(Database, 'after_insert', set_perm) sqla.event.listen(Database, 'after_update', set_perm) diff --git a/tests/core_tests.py b/tests/core_tests.py index 34f30a1..159d16d 100644 --- a/tests/core_tests.py +++ b/tests/core_tests.py @@ -13,6 +13,7 @@ import random import unittest from flask import escape +import sqlalchemy as sqla from superset import db, utils, appbuilder, sm, jinja_context, sql_lab from superset.models import core as models @@ -296,6 +297,19 @@ class CoreTests(SupersetTestCase): assert response.status_code == 200 assert response.headers['Content-Type'] == 'application/json' + def test_custom_password_store(self): + database = self.get_main_database(db.session) + conn_pre = sqla.engine.url.make_url(database.sqlalchemy_uri_decrypted) + + def custom_password_store(uri): + return "password_store_test" + + database.custom_password_store = custom_password_store + conn = sqla.engine.url.make_url(database.sqlalchemy_uri_decrypted) + if conn_pre.password: + assert conn.password == "password_store_test" + assert conn.password != conn_pre.password + def test_databaseview_edit(self, username='admin'): # validate that sending a password-masked uri does not over-write the decrypted uri self.login(username=username) -- To stop receiving notification emails like this one, please contact ['"comm...@superset.apache.org" <comm...@superset.apache.org>'].