mirror of https://github.com/apache/superset.git
Adding hook for external password store for databases (#3436)
This commit is contained in:
parent
816c517f0f
commit
fdee06bbf2
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
Loading…
Reference in New Issue