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>'].

Reply via email to