From 85227912b3c6d0fcecc9c2904c93277f99771f7a Mon Sep 17 00:00:00 2001 From: Maxime Beauchemin Date: Sat, 21 Nov 2015 12:35:49 -0800 Subject: [PATCH] Encrypting the passwords out of connection strings --- ...89ce07647b_add_encrypted_password_field.py | 28 +++++++++++++++++++ panoramix/models.py | 19 ++++++++++--- panoramix/views.py | 10 +++++++ setup.py | 2 ++ 4 files changed, 55 insertions(+), 4 deletions(-) create mode 100644 panoramix/migrations/versions/289ce07647b_add_encrypted_password_field.py diff --git a/panoramix/migrations/versions/289ce07647b_add_encrypted_password_field.py b/panoramix/migrations/versions/289ce07647b_add_encrypted_password_field.py new file mode 100644 index 000000000..6d64887b2 --- /dev/null +++ b/panoramix/migrations/versions/289ce07647b_add_encrypted_password_field.py @@ -0,0 +1,28 @@ +"""Add encrypted password field + +Revision ID: 289ce07647b +Revises: 2929af7925ed +Create Date: 2015-11-21 11:18:00.650587 + +""" + +# revision identifiers, used by Alembic. +revision = '289ce07647b' +down_revision = '2929af7925ed' + +from alembic import op +import sqlalchemy as sa +from sqlalchemy_utils.types.encrypted import EncryptedType + + +def upgrade(): + op.add_column( + 'dbs', + sa.Column( + 'password', + EncryptedType(sa.String(1024)), + nullable=True)) + + +def downgrade(): + op.drop_column('dbs', 'password') diff --git a/panoramix/models.py b/panoramix/models.py index 19ec9eabc..fbf8d56fb 100644 --- a/panoramix/models.py +++ b/panoramix/models.py @@ -6,13 +6,14 @@ from flask.ext.appbuilder.models.mixins import AuditMixin from pandas import read_sql_query from pydruid import client from pydruid.utils.filters import Dimension, Filter +import sqlalchemy as sqla from sqlalchemy import ( - Column, Integer, String, ForeignKey, Text, Boolean, DateTime) -from sqlalchemy import Table -from sqlalchemy import create_engine, MetaData, desc, select, and_ + Column, Integer, String, ForeignKey, Text, Boolean, DateTime, + Table, create_engine, MetaData, desc, select, and_) from sqlalchemy.orm import relationship from sqlalchemy.sql import table, literal_column, text from sqlalchemy.sql.elements import ColumnClause +from sqlalchemy_utils import EncryptedType from copy import deepcopy, copy from collections import namedtuple @@ -187,12 +188,16 @@ class Database(Model, AuditMixinNullable): id = Column(Integer, primary_key=True) database_name = Column(String(250), unique=True) sqlalchemy_uri = Column(String(1024)) + password = Column(EncryptedType(String(1024), config.get('SECRET_KEY'))) def __repr__(self): return self.database_name def get_sqla_engine(self): - return create_engine(self.sqlalchemy_uri) + return create_engine(self.sqlalchemy_uri_decrypted) + + def safe_sqlalchemy_uri(self): + return self.sqlalchemy_uri def get_table(self, table_name): meta = MetaData() @@ -201,6 +206,12 @@ class Database(Model, AuditMixinNullable): autoload=True, autoload_with=self.get_sqla_engine()) + @property + def sqlalchemy_uri_decrypted(self): + conn = sqla.engine.url.make_url(self.sqlalchemy_uri) + conn.password = self.password + return str(conn) + class SqlaTable(Model, Queryable, AuditMixinNullable): type = "table" diff --git a/panoramix/views.py b/panoramix/views.py index 3af7337b4..677720b5c 100644 --- a/panoramix/views.py +++ b/panoramix/views.py @@ -10,6 +10,7 @@ from flask.ext.appbuilder.models.sqla.interface import SQLAInterface from flask.ext.appbuilder.security.decorators import has_access from pydruid.client import doublesum from sqlalchemy import create_engine +import sqlalchemy as sqla from wtforms.validators import ValidationError from panoramix import appbuilder, db, models, viz, utils, app, sm, ascii_art @@ -105,6 +106,7 @@ class DatabaseView(PanoramixModelView, DeleteMixin): datamodel = SQLAInterface(models.Database) list_columns = ['database_name', 'created_by', 'created_on'] add_columns = ['database_name', 'sqlalchemy_uri'] + search_exclude_columns = ('password',) edit_columns = add_columns add_template = "panoramix/models/database/add.html" edit_template = "panoramix/models/database/edit.html" @@ -114,6 +116,14 @@ class DatabaseView(PanoramixModelView, DeleteMixin): "to structure your URI here: " "http://docs.sqlalchemy.org/en/rel_1_0/core/engines.html") } + def pre_add(self, db): + conn = sqla.engine.url.make_url(db.sqlalchemy_uri) + db.password = conn.password + conn.password = "X" * 10 if conn.password else None + db.sqlalchemy_uri = str(conn) # hides the password + + def pre_update(self, db): + self.pre_add(db) appbuilder.add_view( DatabaseView, diff --git a/setup.py b/setup.py index 78d502d56..f0266b5a0 100644 --- a/setup.py +++ b/setup.py @@ -18,6 +18,7 @@ setup( scripts=['panoramix/bin/panoramix'], install_requires=[ 'alembic>=0.7.7, <0.8.0', + 'cryptography>=1.1.1, <2.0.0', 'flask>=0.10.1, <1.0.0', 'flask-appbuilder>=1.4.5, <2.0.0', 'flask-login==0.2.11', @@ -33,6 +34,7 @@ setup( 'python-dateutil>=2.4.2, <3.0.0', 'requests>=2.7.0, <3.0.0', 'sqlparse>=0.1.16, <0.2.0', + 'sqlalchemy-utils>=0.31.3, <0.32.0', 'sqlalchemy==1.0.8', 'flask-sqlalchemy==2.0', ],