diff --git a/migrations/versions/98af61feea92_add_encrypted_options_to_data_sources.py b/migrations/versions/98af61feea92_add_encrypted_options_to_data_sources.py new file mode 100644 index 0000000000..86f1eb47e7 --- /dev/null +++ b/migrations/versions/98af61feea92_add_encrypted_options_to_data_sources.py @@ -0,0 +1,49 @@ +"""add_encrypted_options_to_data_sources + +Revision ID: 98af61feea92 +Revises: 73beceabb948 +Create Date: 2019-01-31 09:21:31.517265 + +""" +from alembic import op +import sqlalchemy as sa +from sqlalchemy.dialects import postgresql +from sqlalchemy.sql import table +from sqlalchemy_utils.types.encrypted.encrypted_type import FernetEngine + +from redash import settings +from redash.utils.configuration import ConfigurationContainer +from redash.models.types import EncryptedConfiguration, Configuration, MutableDict, MutableList, PseudoJSON + +# revision identifiers, used by Alembic. +revision = '98af61feea92' +down_revision = '73beceabb948' +branch_labels = None +depends_on = None + + +def upgrade(): + op.add_column('data_sources', sa.Column('encrypted_options', postgresql.BYTEA(), nullable=True)) + + # copy values + data_sources = table( + 'data_sources', + sa.Column('id', sa.Integer, primary_key=True), + sa.Column('encrypted_options', ConfigurationContainer.as_mutable(EncryptedConfiguration(sa.Text, settings.SECRET_KEY, FernetEngine))), + sa.Column('options', ConfigurationContainer.as_mutable(Configuration))) + + conn = op.get_bind() + for ds in conn.execute(data_sources.select()): + conn.execute( + data_sources + .update() + .where(data_sources.c.id == ds.id) + .values(encrypted_options=ds.options)) + + op.drop_column('data_sources', 'options') + op.alter_column('data_sources', 'encrypted_options', + nullable=False) + + +def downgrade(): + pass diff --git a/redash/models/__init__.py b/redash/models/__init__.py index 032b0782a2..19cc8be623 100644 --- a/redash/models/__init__.py +++ b/redash/models/__init__.py @@ -18,8 +18,9 @@ from sqlalchemy_utils import generic_relationship from sqlalchemy_utils.types import TSVectorType from sqlalchemy_utils.models import generic_repr +from sqlalchemy_utils.types.encrypted.encrypted_type import FernetEngine -from redash import redis_connection, utils +from redash import redis_connection, utils, settings from redash.destinations import (get_configuration_schema_for_destination_type, get_destination) from redash.metrics import database # noqa: F401 @@ -32,7 +33,7 @@ from .changes import ChangeTrackingMixin, Change # noqa from .mixins import BelongsToOrgMixin, TimestampMixin from .organizations import Organization -from .types import Configuration, MutableDict, MutableList, PseudoJSON +from .types import EncryptedConfiguration, Configuration, MutableDict, MutableList, PseudoJSON from .users import (AccessPermission, AnonymousUser, ApiUser, Group, User) # noqa logger = logging.getLogger(__name__) @@ -72,7 +73,7 @@ class DataSource(BelongsToOrgMixin, db.Model): name = Column(db.String(255)) type = Column(db.String(255)) - options = Column(ConfigurationContainer.as_mutable(Configuration)) + options = Column('encrypted_options', ConfigurationContainer.as_mutable(EncryptedConfiguration(db.Text, settings.SECRET_KEY, FernetEngine))) queue_name = Column(db.String(255), default="queries") scheduled_queue_name = Column(db.String(255), default="scheduled_queries") created_at = Column(db.DateTime(True), default=db.func.now()) diff --git a/redash/models/types.py b/redash/models/types.py index cda072c6fb..60a5916b60 100644 --- a/redash/models/types.py +++ b/redash/models/types.py @@ -2,6 +2,7 @@ from sqlalchemy.types import TypeDecorator from sqlalchemy.ext.indexable import index_property from sqlalchemy.ext.mutable import Mutable +from sqlalchemy_utils import EncryptedType from redash.utils import json_dumps, json_loads from redash.utils.configuration import ConfigurationContainer @@ -19,6 +20,14 @@ def process_result_value(self, value, dialect): return ConfigurationContainer.from_json(value) +class EncryptedConfiguration(EncryptedType): + def process_bind_param(self, value, dialect): + return super(EncryptedConfiguration, self).process_bind_param(value.to_json(), dialect) + + def process_result_value(self, value, dialect): + return ConfigurationContainer.from_json(super(EncryptedConfiguration, self).process_result_value(value, dialect)) + + # XXX replace PseudoJSON and MutableDict with real JSON field class PseudoJSON(TypeDecorator): impl = db.Text @@ -87,6 +96,7 @@ class json_cast_property(index_property): entity attribute as the specified cast type. Useful for JSON and JSONB colums for easier querying/filtering. """ + def __init__(self, cast_type, *args, **kwargs): super(json_cast_property, self).__init__(*args, **kwargs) self.cast_type = cast_type diff --git a/redash/settings/__init__.py b/redash/settings/__init__.py index 47dbb99e77..a54d788022 100644 --- a/redash/settings/__init__.py +++ b/redash/settings/__init__.py @@ -15,6 +15,7 @@ def all_settings(): return settings + REDIS_URL = os.environ.get('REDASH_REDIS_URL', os.environ.get('REDIS_URL', "redis://localhost:6379/0")) PROXIES_COUNT = int(os.environ.get('REDASH_PROXIES_COUNT', "1")) @@ -107,6 +108,7 @@ def all_settings(): JOB_EXPIRY_TIME = int(os.environ.get("REDASH_JOB_EXPIRY_TIME", 3600 * 12)) COOKIE_SECRET = os.environ.get("REDASH_COOKIE_SECRET", "c292a0a3aa32397cdb050e233733900f") SESSION_COOKIE_SECURE = parse_boolean(os.environ.get("REDASH_SESSION_COOKIE_SECURE") or str(ENFORCE_HTTPS)) +SECRET_KEY = os.environ.get('REDASH_SECRET_KEY', COOKIE_SECRET) LOG_LEVEL = os.environ.get("REDASH_LOG_LEVEL", "INFO") LOG_STDOUT = parse_boolean(os.environ.get('REDASH_LOG_STDOUT', 'false')) diff --git a/redash/utils/configuration.py b/redash/utils/configuration.py index f19c281053..6473c02c6e 100644 --- a/redash/utils/configuration.py +++ b/redash/utils/configuration.py @@ -92,4 +92,6 @@ def __contains__(self, item): @classmethod def from_json(cls, config_in_json): + if config_in_json is None: + return cls({}) return cls(json_loads(config_in_json))