Adding hook for external password store for databases (#3436)

This commit is contained in:
fabianmenges 2017-09-13 23:59:03 -04:00 committed by Maxime Beauchemin
parent 816c517f0f
commit fdee06bbf2
4 changed files with 48 additions and 2 deletions

View File

@ -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

View File

@ -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

View File

@ -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)

View File

@ -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)