Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Change: encrypt data source options. 🔓 #2970

Merged
merged 3 commits into from
Feb 17, 2019
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
Original file line number Diff line number Diff line change
@@ -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
7 changes: 4 additions & 3 deletions redash/models/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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__)
Expand Down Expand Up @@ -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)))
Copy link

Choose a reason for hiding this comment

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

Line too long (144 > 120 characters)

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())
Expand Down
10 changes: 10 additions & 0 deletions redash/models/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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))
arikfr marked this conversation as resolved.
Show resolved Hide resolved


# XXX replace PseudoJSON and MutableDict with real JSON field
class PseudoJSON(TypeDecorator):
impl = db.Text
Expand Down Expand Up @@ -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
Expand Down
2 changes: 2 additions & 0 deletions redash/settings/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"))

Expand Down Expand Up @@ -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)
Copy link
Member

Choose a reason for hiding this comment

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

I would take the opportunity to make sure that this is a non empty value since this is a critical setting in Flask.

Copy link
Member Author

Choose a reason for hiding this comment

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

Good idea.

Copy link
Member Author

Choose a reason for hiding this comment

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

I realized that this is 1) beyond the scope of the PR; 2) I don't have the time to implement it :-(. To avoid having this PR lingering any longer, went ahead without it.

Copy link
Member

Choose a reason for hiding this comment

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

Makes sense.


LOG_LEVEL = os.environ.get("REDASH_LOG_LEVEL", "INFO")
LOG_STDOUT = parse_boolean(os.environ.get('REDASH_LOG_STDOUT', 'false'))
Expand Down
2 changes: 2 additions & 0 deletions redash/utils/configuration.py
Original file line number Diff line number Diff line change
Expand Up @@ -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))