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.
|
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
|
SSL Access to databases
|
||||||
-----------------------
|
-----------------------
|
||||||
This example worked with a MySQL database that requires SSL. The configuration
|
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 = 'mysql://myapp@localhost/myapp'
|
||||||
# SQLALCHEMY_DATABASE_URI = 'postgresql://root:password@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
|
# The limit of queries fetched for query search
|
||||||
QUERY_SEARCH_LIMIT = 1000
|
QUERY_SEARCH_LIMIT = 1000
|
||||||
|
|
||||||
|
|
|
@ -561,6 +561,7 @@ class Database(Model, AuditMixinNullable):
|
||||||
}
|
}
|
||||||
"""))
|
"""))
|
||||||
perm = Column(String(1000))
|
perm = Column(String(1000))
|
||||||
|
custom_password_store = config.get('SQLALCHEMY_CUSTOM_PASSWORD_STORE')
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return self.verbose_name if self.verbose_name else self.database_name
|
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):
|
def set_sqlalchemy_uri(self, uri):
|
||||||
password_mask = "X" * 10
|
password_mask = "X" * 10
|
||||||
conn = sqla.engine.url.make_url(uri)
|
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
|
# do not over-write the password with the password mask
|
||||||
self.password = conn.password
|
self.password = conn.password
|
||||||
conn.password = password_mask if conn.password else None
|
conn.password = password_mask if conn.password else None
|
||||||
|
@ -725,6 +726,9 @@ class Database(Model, AuditMixinNullable):
|
||||||
@property
|
@property
|
||||||
def sqlalchemy_uri_decrypted(self):
|
def sqlalchemy_uri_decrypted(self):
|
||||||
conn = sqla.engine.url.make_url(self.sqlalchemy_uri)
|
conn = sqla.engine.url.make_url(self.sqlalchemy_uri)
|
||||||
|
if self.custom_password_store:
|
||||||
|
conn.password = self.custom_password_store(conn)
|
||||||
|
else:
|
||||||
conn.password = self.password
|
conn.password = self.password
|
||||||
return str(conn)
|
return str(conn)
|
||||||
|
|
||||||
|
@ -736,6 +740,7 @@ class Database(Model, AuditMixinNullable):
|
||||||
return (
|
return (
|
||||||
"[{obj.database_name}].(id:{obj.id})").format(obj=self)
|
"[{obj.database_name}].(id:{obj.id})").format(obj=self)
|
||||||
|
|
||||||
|
|
||||||
sqla.event.listen(Database, 'after_insert', set_perm)
|
sqla.event.listen(Database, 'after_insert', set_perm)
|
||||||
sqla.event.listen(Database, 'after_update', set_perm)
|
sqla.event.listen(Database, 'after_update', set_perm)
|
||||||
|
|
||||||
|
|
|
@ -13,6 +13,7 @@ import random
|
||||||
import unittest
|
import unittest
|
||||||
|
|
||||||
from flask import escape
|
from flask import escape
|
||||||
|
import sqlalchemy as sqla
|
||||||
|
|
||||||
from superset import db, utils, appbuilder, sm, jinja_context, sql_lab
|
from superset import db, utils, appbuilder, sm, jinja_context, sql_lab
|
||||||
from superset.models import core as models
|
from superset.models import core as models
|
||||||
|
@ -296,6 +297,19 @@ class CoreTests(SupersetTestCase):
|
||||||
assert response.status_code == 200
|
assert response.status_code == 200
|
||||||
assert response.headers['Content-Type'] == 'application/json'
|
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'):
|
def test_databaseview_edit(self, username='admin'):
|
||||||
# validate that sending a password-masked uri does not over-write the decrypted uri
|
# validate that sending a password-masked uri does not over-write the decrypted uri
|
||||||
self.login(username=username)
|
self.login(username=username)
|
||||||
|
|
Loading…
Reference in New Issue