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

Use flask-talisman for handling backend response headers #3404

Merged
merged 8 commits into from
Mar 27, 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
13 changes: 5 additions & 8 deletions redash/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@

import redis
from flask import Flask, current_app
from flask_sslify import SSLify
from werkzeug.contrib.fixers import ProxyFix
from werkzeug.routing import BaseConverter
from statsd import StatsClient
Expand Down Expand Up @@ -98,11 +97,11 @@ def to_url(self, value):


def create_app():
from redash import authentication, extensions, handlers
from redash import authentication, extensions, handlers, security
from redash.handlers.webpack import configure_webpack
from redash.handlers import chrome_logger
from redash.models import db, users
from redash.metrics.request import provision_app
from redash.metrics import request as request_metrics
from redash.utils import sentry

sentry.init()
Expand All @@ -116,22 +115,20 @@ def create_app():
app.wsgi_app = ProxyFix(app.wsgi_app, settings.PROXIES_COUNT)
app.url_map.converters['org_slug'] = SlugConverter

if settings.ENFORCE_HTTPS:
SSLify(app, skips=['ping'])

# configure our database
app.config['SQLALCHEMY_DATABASE_URI'] = settings.SQLALCHEMY_DATABASE_URI
app.config.update(settings.all_settings())

provision_app(app)
security.init_app(app)
request_metrics.init_app(app)
db.init_app(app)
migrate.init_app(app, db)
mail.init_app(app)
authentication.init_app(app)
limiter.init_app(app)
handlers.init_app(app)
configure_webpack(app)
extensions.init_extensions(app)
extensions.init_app(app)
chrome_logger.init_app(app)
users.init_app(app)

Expand Down
1 change: 0 additions & 1 deletion redash/authentication/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -244,7 +244,6 @@ def init_app(app):
login_manager.init_app(app)
login_manager.anonymous_user = models.AnonymousUser

app.secret_key = settings.COOKIE_SECRET
jezdez marked this conversation as resolved.
Show resolved Hide resolved
app.register_blueprint(google_oauth.blueprint)
app.register_blueprint(saml_auth.blueprint)
app.register_blueprint(remote_user_auth.blueprint)
Expand Down
3 changes: 1 addition & 2 deletions redash/authentication/account.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,11 @@
from redash import settings
from redash.tasks import send_mail
from redash.utils import base_url
from redash.models import User
# noinspection PyUnresolvedReferences
from itsdangerous import URLSafeTimedSerializer, SignatureExpired, BadSignature

logger = logging.getLogger(__name__)
serializer = URLSafeTimedSerializer(settings.COOKIE_SECRET)
serializer = URLSafeTimedSerializer(settings.SECRET_KEY)


def invite_token(user):
Expand Down
2 changes: 1 addition & 1 deletion redash/extensions.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
from pkg_resources import iter_entry_points, resource_isdir, resource_listdir


def init_extensions(app):
def init_app(app):
"""
Load the Redash extensions for the given Redash Flask app.
"""
Expand Down
2 changes: 2 additions & 0 deletions redash/handlers/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,11 @@
from redash.handlers.base import routes
from redash.monitor import get_status
from redash.permissions import require_super_admin
from redash.security import talisman


@routes.route('/ping', methods=['GET'])
@talisman(force_https=False)
def ping():
return 'PONG.'

Expand Down
5 changes: 4 additions & 1 deletion redash/handlers/dashboards.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,11 @@
from redash.handlers.base import (BaseResource, get_object_or_404, paginate,
filter_by_tags,
order_results as _order_results)
from redash.serializers import serialize_dashboard
from redash.permissions import (can_modify, require_admin_or_owner,
require_object_modify_permission,
require_permission)
from redash.security import csp_allows_embeding
from redash.serializers import serialize_dashboard
from sqlalchemy.orm.exc import StaleDataError


Expand Down Expand Up @@ -235,6 +236,8 @@ def delete(self, dashboard_slug):


class PublicDashboardResource(BaseResource):
decorators = BaseResource.decorators + [csp_allows_embeding]

def get(self, token):
"""
Retrieve a public dashboard.
Expand Down
4 changes: 3 additions & 1 deletion redash/handlers/embed.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,12 @@
from redash.handlers.base import (get_object_or_404, org_scoped_rule,
record_event)
from redash.handlers.static import render_index
from redash.security import csp_allows_embeding


@routes.route(org_scoped_rule('/embed/query/<query_id>/visualization/<visualization_id>'), methods=['GET'])
@login_required
@csp_allows_embeding
def embed(query_id, visualization_id, org_slug=None):
record_event(current_org, current_user._get_current_object(), {
'action': 'view',
Expand All @@ -22,12 +24,12 @@ def embed(query_id, visualization_id, org_slug=None):
'embed': True,
'referer': request.headers.get('Referer')
})

return render_index()


@routes.route(org_scoped_rule('/public/dashboards/<token>'), methods=['GET'])
@login_required
@csp_allows_embeding
def public_dashboard(token, org_slug=None):
if current_user.is_api_user():
dashboard = current_user.object
Expand Down
5 changes: 1 addition & 4 deletions redash/handlers/static.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,4 @@
import os

from flask import current_app, render_template, safe_join, send_file
from werkzeug.exceptions import NotFound
from flask import render_template, safe_join, send_file

from flask_login import login_required
from redash import settings
Expand Down
2 changes: 1 addition & 1 deletion redash/metrics/request.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ def calculate_metrics_on_exception(error):
calculate_metrics(MockResponse(500, '?', -1))


def provision_app(app):
def init_app(app):
app.before_request(record_requets_start_time)
app.after_request(calculate_metrics)
app.teardown_request(calculate_metrics_on_exception)
2 changes: 1 addition & 1 deletion redash/models/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ class DataSource(BelongsToOrgMixin, db.Model):

name = Column(db.String(255))
type = Column(db.String(255))
options = Column('encrypted_options', ConfigurationContainer.as_mutable(EncryptedConfiguration(db.Text, settings.SECRET_KEY, FernetEngine)))
options = Column('encrypted_options', ConfigurationContainer.as_mutable(EncryptedConfiguration(db.Text, settings.DATASOURCE_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())
Expand Down
43 changes: 43 additions & 0 deletions redash/security.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import functools
from flask_talisman import talisman

from redash import settings


talisman = talisman.Talisman()


def csp_allows_embeding(fn):

@functools.wraps(fn)
def decorated(*args, **kwargs):
return fn(*args, **kwargs)

embedable_csp = talisman.content_security_policy + "frame-ancestors *;"
return talisman(
content_security_policy=embedable_csp,
frame_options=None,
)(decorated)


def init_app(app):
talisman.init_app(
app,
feature_policy=settings.FEATURE_POLICY,
force_https=settings.ENFORCE_HTTPS,
force_https_permanent=settings.ENFORCE_HTTPS_PERMANENT,
force_file_save=settings.ENFORCE_FILE_SAVE,
frame_options=settings.FRAME_OPTIONS,
frame_options_allow_from=settings.FRAME_OPTIONS_ALLOW_FROM,
strict_transport_security=settings.HSTS_ENABLED,
strict_transport_security_preload=settings.HSTS_PRELOAD,
strict_transport_security_max_age=settings.HSTS_MAX_AGE,
strict_transport_security_include_subdomains=settings.HSTS_INCLUDE_SUBDOMAINS,
content_security_policy=settings.CONTENT_SECURITY_POLICY,
content_security_policy_report_uri=settings.CONTENT_SECURITY_POLICY_REPORT_URI,
content_security_policy_report_only=settings.CONTENT_SECURITY_POLICY_REPORT_ONLY,
content_security_policy_nonce_in=settings.CONTENT_SECURITY_POLICY_NONCE_IN,
referrer_policy=settings.REFERRER_POLICY,
session_cookie_secure=settings.SESSION_COOKIE_SECURE,
session_cookie_http_only=settings.SESSION_COOKIE_HTTPONLY,
arikfr marked this conversation as resolved.
Show resolved Hide resolved
)
84 changes: 79 additions & 5 deletions redash/settings/__init__.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import os
from funcy import distinct, remove
from flask_talisman import talisman

from .helpers import fix_assets_path, array_from_string, parse_boolean, int_or_none, set_from_string
from .organization import DATE_FORMAT
Expand All @@ -15,7 +16,6 @@ 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 @@ -50,9 +50,86 @@ def all_settings():
SCHEMAS_REFRESH_QUEUE = os.environ.get("REDASH_SCHEMAS_REFRESH_QUEUE", "celery")

AUTH_TYPE = os.environ.get("REDASH_AUTH_TYPE", "api_key")
ENFORCE_HTTPS = parse_boolean(os.environ.get("REDASH_ENFORCE_HTTPS", "false"))
INVITATION_TOKEN_MAX_AGE = int(os.environ.get("REDASH_INVITATION_TOKEN_MAX_AGE", 60 * 60 * 24 * 7))

# The secret key to use in the Flask app for various cryptographic features
SECRET_KEY = os.environ.get("REDASH_COOKIE_SECRET", "c292a0a3aa32397cdb050e233733900f")
# The secret key to use when encrypting data source options
DATASOURCE_SECRET_KEY = os.environ.get('REDASH_SECRET_KEY', SECRET_KEY)

# Whether and how to redirect non-HTTP requests to HTTPS. Disabled by default.
ENFORCE_HTTPS = parse_boolean(os.environ.get("REDASH_ENFORCE_HTTPS", "false"))
ENFORCE_HTTPS_PERMANENT = parse_boolean(
os.environ.get("REDASH_ENFORCE_HTTPS_PERMANENT", "false"))
# Whether file downloads are enforced or not.
ENFORCE_FILE_SAVE = parse_boolean(
os.environ.get("REDASH_ENFORCE_FILE_SAVE", "true"))

# Whether to use secure cookies by default.
COOKIES_SECURE = parse_boolean(
os.environ.get("REDASH_COOKIES_SECURE", str(ENFORCE_HTTPS)))
# Whether the session cookie is set to secure.
SESSION_COOKIE_SECURE = parse_boolean(
os.environ.get("REDASH_SESSION_COOKIE_SECURE") or str(COOKIES_SECURE))
# Whether the session cookie is set HttpOnly.
SESSION_COOKIE_HTTPONLY = parse_boolean(
os.environ.get("REDASH_SESSION_COOKIE_HTTPONLY", "true"))
# Whether the session cookie is set to secure.
REMEMBER_COOKIE_SECURE = parse_boolean(
os.environ.get("REDASH_REMEMBER_COOKIE_SECURE") or str(COOKIES_SECURE))
# Whether the remember cookie is set HttpOnly.
REMEMBER_COOKIE_HTTPONLY = parse_boolean(
os.environ.get("REDASH_REMEMBER_COOKIE_HTTPONLY", "true"))

# Doesn't set X-Frame-Options by default since it's highly dependent
# on the specific deployment.
# See https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Frame-Options
# for more information.
FRAME_OPTIONS = os.environ.get("REDASH_FRAME_OPTIONS", "deny")
FRAME_OPTIONS_ALLOW_FROM = os.environ.get(
"REDASH_FRAME_OPTIONS_ALLOW_FROM", "")

# Whether and how to send Strict-Transport-Security response headers.
# See https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Strict-Transport-Security
# for more information.
HSTS_ENABLED = parse_boolean(
os.environ.get("REDASH_HSTS_ENABLED") or str(ENFORCE_HTTPS))
HSTS_PRELOAD = parse_boolean(os.environ.get("REDASH_HSTS_PRELOAD", "false"))
HSTS_MAX_AGE = int(
os.environ.get("REDASH_HSTS_MAX_AGE", talisman.ONE_YEAR_IN_SECS))
HSTS_INCLUDE_SUBDOMAINS = parse_boolean(
os.environ.get("REDASH_HSTS_INCLUDE_SUBDOMAINS", "false"))

# Whether and how to send Content-Security-Policy response headers.
# See https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy
# for more information.
# Overriding this value via an environment variables requires setting it
# as a string in the general CSP format of a semicolon separated list of
# individual CSP directives, see https://github.com/GoogleCloudPlatform/flask-talisman#example-7
# for more information. E.g.:
CONTENT_SECURITY_POLICY = os.environ.get(
jezdez marked this conversation as resolved.
Show resolved Hide resolved
"REDASH_CONTENT_SECURITY_POLICY",
"default-src 'self'; style-src 'self' 'unsafe-inline'; script-src 'self' 'unsafe-eval'; font-src 'self' data:; img-src 'self' http: https: data:; object-src 'none'; frame-ancestors 'none';"
)
CONTENT_SECURITY_POLICY_REPORT_URI = os.environ.get(
"REDASH_CONTENT_SECURITY_POLICY_REPORT_URI", "")
CONTENT_SECURITY_POLICY_REPORT_ONLY = parse_boolean(
os.environ.get("REDASH_CONTENT_SECURITY_POLICY_REPORT_ONLY", "false"))
CONTENT_SECURITY_POLICY_NONCE_IN = array_from_string(
os.environ.get("REDASH_CONTENT_SECURITY_POLICY_NONCE_IN", ""))

# Whether and how to send Referrer-Policy response headers. Defaults to
# 'strict-origin-when-cross-origin'.
# See https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Referrer-Policy
# for more information.
REFERRER_POLICY = os.environ.get(
"REDASH_REFERRER_POLICY", "strict-origin-when-cross-origin")
# Whether and how to send Feature-Policy response headers. Defaults to
# an empty value.
# See https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Feature-Policy
# for more information.
FEATURE_POLICY = os.environ.get("REDASH_REFERRER_POLICY", "")

MULTI_ORG = parse_boolean(os.environ.get("REDASH_MULTI_ORG", "false"))

GOOGLE_CLIENT_ID = os.environ.get("REDASH_GOOGLE_CLIENT_ID", "")
Expand Down Expand Up @@ -111,9 +188,6 @@ def all_settings():
STATIC_ASSETS_PATH = fix_assets_path(os.environ.get("REDASH_STATIC_ASSETS_PATH", "../client/dist/"))

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)
jezdez marked this conversation as resolved.
Show resolved Hide resolved

LOG_LEVEL = os.environ.get("REDASH_LOG_LEVEL", "INFO")
LOG_STDOUT = parse_boolean(os.environ.get('REDASH_LOG_STDOUT', 'false'))
Expand Down
2 changes: 1 addition & 1 deletion requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ requests-oauthlib>=0.6.2,<1.2.0
Flask-SQLAlchemy==2.3.2
Flask-Migrate==2.0.1
flask-mail==0.9.1
flask-sslify==0.1.5
flask-talisman==0.6.0
Flask-Limiter==0.9.3
passlib==1.6.2
aniso8601==1.1.0
Expand Down
2 changes: 2 additions & 0 deletions setup/generate_key.sh
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,12 @@
FLAG="/var/log/generate_secrets.log"
if [ ! -f $FLAG ]; then
COOKIE_SECRET=$(pwgen -1s 32)
SECRET_KEY=$(pwgen -1s 32)
POSTGRES_PASSWORD=$(pwgen -1s 32)
REDASH_DATABASE_URL="postgresql:\/\/postgres:$POSTGRES_PASSWORD@postgres\/postgres"

sed -i "s/REDASH_COOKIE_SECRET=.*/REDASH_COOKIE_SECRET=$COOKIE_SECRET/g" /opt/redash/env
sed -i "s/REDASH_SECRET_KEY=.*/REDASH_SECRET_KEY=$SECRET_KEY/g" /opt/redash/env
sed -i "s/POSTGRES_PASSWORD=.*/POSTGRES_PASSWORD=$POSTGRES_PASSWORD/g" /opt/redash/env
sed -i "s/REDASH_DATABASE_URL=.*/REDASH_DATABASE_URL=$REDASH_DATABASE_URL/g" /opt/redash/env

Expand Down
4 changes: 3 additions & 1 deletion setup/setup.sh
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ REDASH_BASE_PATH=/opt/redash

install_docker(){
# Install Docker
sudo apt-get update
sudo apt-get update
sudo apt-get -yy install apt-transport-https ca-certificates curl software-properties-common wget pwgen
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo apt-key add -
sudo add-apt-repository "deb [arch=amd64] https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable"
Expand Down Expand Up @@ -38,6 +38,7 @@ create_config() {
fi

COOKIE_SECRET=$(pwgen -1s 32)
SECRET_KEY=$(pwgen -1s 32)
POSTGRES_PASSWORD=$(pwgen -1s 32)
REDASH_DATABASE_URL="postgresql://postgres:${POSTGRES_PASSWORD}@postgres/postgres"

Expand All @@ -46,6 +47,7 @@ create_config() {
echo "REDASH_REDIS_URL=redis://redis:6379/0" >> $REDASH_BASE_PATH/env
echo "POSTGRES_PASSWORD=$POSTGRES_PASSWORD" >> $REDASH_BASE_PATH/env
echo "REDASH_COOKIE_SECRET=$COOKIE_SECRET" >> $REDASH_BASE_PATH/env
echo "REDASH_SECRET_KEY=$SECRET_KEY" >> $REDASH_BASE_PATH/env
echo "REDASH_DATABASE_URL=$REDASH_DATABASE_URL" >> $REDASH_BASE_PATH/env
}

Expand Down
2 changes: 1 addition & 1 deletion tests/handlers/test_data_sources.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
from funcy import pairwise
from tests import BaseTestCase

from redash.models import DataSource, Query
from redash.models import DataSource


class TestDataSourceGetSchema(BaseTestCase):
Expand Down
Loading