mirror of
https://github.com/chanzuckerberg/cellxgene.git
synced 2026-09-23 04:58:13 +08:00
Revised terms and privacy consent dialog, analytics hooks (#1426)
* revised terms and privacy consent * reorg code * fix conditional * Overlay reflects un-dissmissable state * add inline scripts, and consent callback * add csp_directive config hook * revert config.yaml * fix logic error Co-authored-by: Colin Megill <colinmegill@gmail.com>
This commit is contained in:
co-authored by
Colin Megill
parent
666e6d9849
commit
05fcdaf93c
+4
-1
@@ -69,12 +69,15 @@ def dataset_index(dataset=None):
|
||||
location = path_join(config.multi_dataset__dataroot, dataset)
|
||||
|
||||
scripts = config.server__scripts
|
||||
inline_scripts = config.server__inline_scripts
|
||||
|
||||
try:
|
||||
cache_manager = current_app.matrix_data_cache_manager
|
||||
with cache_manager.data_adaptor(location, config) as data_adaptor:
|
||||
dataset_title = config.get_title(data_adaptor)
|
||||
return render_template("index.html", datasetTitle=dataset_title, SCRIPTS=scripts)
|
||||
return render_template(
|
||||
"index.html", datasetTitle=dataset_title, SCRIPTS=scripts, INLINE_SCRIPTS=inline_scripts
|
||||
)
|
||||
except DatasetAccessError:
|
||||
return common_rest.abort_and_log(
|
||||
HTTPStatus.BAD_REQUEST, f"Invalid dataset {dataset}", loglevel=logging.INFO, include_exc_info=True
|
||||
|
||||
@@ -50,6 +50,7 @@ class AppConfig(object):
|
||||
self.server__host = dc["server"]["host"]
|
||||
self.server__port = dc["server"]["port"]
|
||||
self.server__scripts = dc["server"]["scripts"]
|
||||
self.server__inline_scripts = dc["server"]["inline_scripts"]
|
||||
self.server__open_browser = dc["server"]["open_browser"]
|
||||
self.server__about_legal_tos = dc["server"]["about_legal_tos"]
|
||||
self.server__about_legal_privacy = dc["server"]["about_legal_privacy"]
|
||||
@@ -57,6 +58,7 @@ class AppConfig(object):
|
||||
self.server__flask_secret_key = dc["server"]["flask_secret_key"]
|
||||
self.server__generate_cache_control_headers = dc["server"]["generate_cache_control_headers"]
|
||||
self.server__server_timing_headers = dc["server"]["server_timing_headers"]
|
||||
self.server__csp_directives = dc["server"]["csp_directives"]
|
||||
|
||||
self.multi_dataset__dataroot = dc["multi_dataset"]["dataroot"]
|
||||
self.multi_dataset__index = dc["multi_dataset"]["index"]
|
||||
@@ -129,6 +131,12 @@ class AppConfig(object):
|
||||
mapping["adaptor__cxg_adaptor__tiledb_ctx"] = (("adaptor", "cxg_adaptor", "tiledb_ctx"), val)
|
||||
del dc["adaptor"]["cxg_adaptor"]["tiledb_ctx"]
|
||||
|
||||
# special case for csp_directives whose value is a dict.
|
||||
val = config.get("server", {}).get("csp_directives", {})
|
||||
if val is not None:
|
||||
mapping["server__csp_directives"] = (("server", "csp_directives"), val)
|
||||
del dc["server"]["csp_directives"]
|
||||
|
||||
flat_config = flatten(dc)
|
||||
for key, value in flat_config.items():
|
||||
# name of the attribute
|
||||
@@ -231,6 +239,7 @@ class AppConfig(object):
|
||||
self.__check_attr("server__host", str)
|
||||
self.__check_attr("server__port", (type(None), int))
|
||||
self.__check_attr("server__scripts", (list, tuple))
|
||||
self.__check_attr("server__inline_scripts", (list, tuple))
|
||||
self.__check_attr("server__open_browser", bool)
|
||||
self.__check_attr("server__force_https", bool)
|
||||
self.__check_attr("server__flask_secret_key", (type(None), str))
|
||||
@@ -238,6 +247,7 @@ class AppConfig(object):
|
||||
self.__check_attr("server__about_legal_tos", (type(None), str))
|
||||
self.__check_attr("server__about_legal_privacy", (type(None), str))
|
||||
self.__check_attr("server__server_timing_headers", bool)
|
||||
self.__check_attr("server__csp_directives", (type(None), dict))
|
||||
|
||||
if self.server__port:
|
||||
if not is_port_available(self.server__host, self.server__port):
|
||||
@@ -262,6 +272,18 @@ class AppConfig(object):
|
||||
# second, from config file
|
||||
self.server__flask_secret_key = os.environ.get("CXG_SECRET_KEY", self.server__flask_secret_key)
|
||||
|
||||
# CSP Directives are a dict of string: list(string) or string: string
|
||||
if self.server__csp_directives is not None:
|
||||
for k, v in self.server__csp_directives.items():
|
||||
if not isinstance(k, str):
|
||||
raise ConfigurationError(f"CSP directive names must be a string.")
|
||||
if isinstance(v, list):
|
||||
for policy in v:
|
||||
if not isinstance(policy, str):
|
||||
raise ConfigurationError(f"CSP directive value must be a string or list of strings.")
|
||||
elif not isinstance(v, str):
|
||||
raise ConfigurationError(f"CSP directive value must be a string or list of strings.")
|
||||
|
||||
def handle_data_locator(self, context):
|
||||
self.__check_attr("data_locator__s3__region_name", (type(None), bool, str))
|
||||
if self.data_locator__s3__region_name is True:
|
||||
|
||||
@@ -9,6 +9,7 @@ server:
|
||||
host: "127.0.0.1"
|
||||
port : null
|
||||
scripts : []
|
||||
inline_scripts: []
|
||||
open_browser: false
|
||||
about_legal_tos: null
|
||||
about_legal_privacy: null
|
||||
@@ -16,6 +17,7 @@ server:
|
||||
flask_secret_key: null
|
||||
generate_cache_control_headers: false
|
||||
server_timing_headers: false
|
||||
csp_directives: null
|
||||
|
||||
presentation:
|
||||
max_categories: 1000
|
||||
|
||||
+39
-8
@@ -2,11 +2,14 @@
|
||||
|
||||
import sys
|
||||
import os
|
||||
import hashlib
|
||||
import base64
|
||||
from flask import json
|
||||
import logging
|
||||
from flask_talisman import Talisman
|
||||
import boto3
|
||||
|
||||
|
||||
if os.path.isdir("/opt/python/log"):
|
||||
# This is the standard location where Amazon EC2 instances store the application logs.
|
||||
logging.basicConfig(
|
||||
@@ -54,24 +57,32 @@ class WSGIServer(Server):
|
||||
|
||||
@staticmethod
|
||||
def _before_adding_routes(app, app_config):
|
||||
script_hashes, style_hashes = WSGIServer.load_csp_hashes(app)
|
||||
script_hashes, style_hashes = WSGIServer.get_csp_hashes(app, app_config)
|
||||
csp = {
|
||||
"default-src": "'self'",
|
||||
"default-src": ["'self'"],
|
||||
"script-src": ["'unsafe-eval'", "'unsafe-inline'"] + script_hashes,
|
||||
"img-src": ["'self'", "data:"],
|
||||
"object-src": "'none'",
|
||||
"base-uri": "'none'",
|
||||
"upgrade-insecure-requests": "",
|
||||
"frame-ancestors": "'none'",
|
||||
"require-trusted-types-for": "'script'",
|
||||
"object-src": ["'none'"],
|
||||
"base-uri": ["'none'"],
|
||||
"upgrade-insecure-requests": [""],
|
||||
"frame-ancestors": ["'none'"],
|
||||
"require-trusted-types-for": ["'script'"],
|
||||
}
|
||||
if len(style_hashes) > 0:
|
||||
csp["style-src"] = style_hashes
|
||||
if app_config.server__inline_scripts:
|
||||
csp["script-src"].append("'strict-dynamic'")
|
||||
|
||||
if app_config.server__csp_directives:
|
||||
for k, v in app_config.server__csp_directives.items():
|
||||
if not isinstance(v, list):
|
||||
v = [v]
|
||||
csp[k] = csp.get(k, []) + v
|
||||
|
||||
Talisman(app, force_https=app_config.server__force_https, frame_options="DENY", content_security_policy=csp)
|
||||
|
||||
@staticmethod
|
||||
def load_csp_hashes(app):
|
||||
def load_static_csp_hashes(app):
|
||||
csp_hashes = None
|
||||
try:
|
||||
with app.open_resource("../common/web/csp-hashes.json") as f:
|
||||
@@ -88,6 +99,26 @@ class WSGIServer(Server):
|
||||
|
||||
return (script_hashes, style_hashes)
|
||||
|
||||
@staticmethod
|
||||
def compute_inline_scp_hashes(app, app_config):
|
||||
inline_scripts = app_config.server__inline_scripts
|
||||
hashes = []
|
||||
for script in inline_scripts:
|
||||
with app.open_resource(f"../common/web/templates/{script}") as f:
|
||||
content = f.read()
|
||||
# we use jinja2 template include, which trims final newline if present.
|
||||
if content[-1] == 0x0A:
|
||||
content = content[0:-1]
|
||||
hash = base64.b64encode(hashlib.sha256(content).digest())
|
||||
hashes.append(f"'sha256-{hash.decode('utf-8')}'")
|
||||
return hashes
|
||||
|
||||
@staticmethod
|
||||
def get_csp_hashes(app, app_config):
|
||||
script_hashes, style_hashes = WSGIServer.load_static_csp_hashes(app)
|
||||
script_hashes += WSGIServer.compute_inline_scp_hashes(app, app_config)
|
||||
return (script_hashes, style_hashes)
|
||||
|
||||
|
||||
try:
|
||||
app_config = AppConfig()
|
||||
|
||||
Reference in New Issue
Block a user