Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions docs/installation.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
9 changes: 9 additions & 0 deletions superset/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,15 @@
# 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
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Better add documentation to documentation instead of config files. You can just set
SQLALCHEMY_CUSTOM_PASSWORD_STORE = False here

# 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

Expand Down
9 changes: 7 additions & 2 deletions superset/models/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -581,7 +582,7 @@ def backend(self):
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
Expand Down Expand Up @@ -725,7 +726,10 @@ def get_foreign_keys(self, table_name, schema=None):
@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
Expand All @@ -736,6 +740,7 @@ def get_perm(self):
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)

Expand Down
14 changes: 14 additions & 0 deletions tests/core_tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
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
Expand Down Expand Up @@ -296,6 +297,19 @@ def test_testconn(self):
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:
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sqlite does not support passwords, so this test would fail otherwise.

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)
Expand Down