mirror of
https://github.com/chanzuckerberg/cellxgene.git
synced 2026-09-19 19:08:11 +08:00
The log level was mistakenly set to DEBUG, which sent out tons of less than useful messages. INFO works much more useful, and much less verbose.
201 lines
7.3 KiB
Python
201 lines
7.3 KiB
Python
"""cellxgene AWS elastic beanstalk application"""
|
|
|
|
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(
|
|
filename="/opt/python/log/app.log",
|
|
level=logging.INFO,
|
|
format="%(asctime)s.%(msecs)03d %(levelname)s %(module)s - %(funcName)s: %(message)s",
|
|
datefmt="%Y-%m-%d %H:%M:%S",
|
|
)
|
|
|
|
# echo the logs to stdout. Useful for local testing
|
|
logging.getLogger().addHandler(logging.StreamHandler(sys.stdout))
|
|
|
|
SERVERDIR = os.path.dirname(os.path.realpath(__file__))
|
|
sys.path.append(SERVERDIR)
|
|
|
|
try:
|
|
from server.common.app_config import AppConfig
|
|
from server.app.app import Server
|
|
from server.common.data_locator import DataLocator, discover_s3_region_name
|
|
except Exception:
|
|
logging.critical("Exception importing server modules", exc_info=True)
|
|
sys.exit(1)
|
|
|
|
|
|
def get_flask_secret_key(region_name, secret_name):
|
|
session = boto3.session.Session()
|
|
client = session.client(service_name="secretsmanager", region_name=region_name)
|
|
|
|
try:
|
|
get_secret_value_response = client.get_secret_value(SecretId=secret_name)
|
|
if "SecretString" in get_secret_value_response:
|
|
var = get_secret_value_response["SecretString"]
|
|
secret = json.loads(var)
|
|
return secret.get("flask_secret_key")
|
|
except Exception:
|
|
logging.critical("Caught exception during get_secret_key", exc_info=True)
|
|
sys.exit(1)
|
|
|
|
return None
|
|
|
|
|
|
class WSGIServer(Server):
|
|
def __init__(self, app_config):
|
|
super().__init__(app_config)
|
|
|
|
@staticmethod
|
|
def _before_adding_routes(app, app_config):
|
|
script_hashes, style_hashes = WSGIServer.get_csp_hashes(app, app_config)
|
|
csp = {
|
|
"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'"],
|
|
}
|
|
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_static_csp_hashes(app):
|
|
csp_hashes = None
|
|
try:
|
|
with app.open_resource("../common/web/csp-hashes.json") as f:
|
|
csp_hashes = json.load(f)
|
|
except FileNotFoundError:
|
|
pass
|
|
if not isinstance(csp_hashes, dict):
|
|
csp_hashes = {}
|
|
script_hashes = [f"'{hash}'" for hash in csp_hashes.get("script-hashes", [])]
|
|
style_hashes = [f"'{hash}'" for hash in csp_hashes.get("style-hashes", [])]
|
|
|
|
if len(script_hashes) == 0 or len(style_hashes) == 0:
|
|
logging.error("Content security policy hashes are missing, falling back to unsafe-inline policy")
|
|
|
|
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()
|
|
|
|
dataroot = os.getenv("CXG_DATAROOT")
|
|
config_file = os.getenv("CXG_CONFIG_FILE")
|
|
|
|
secret_name = os.getenv("CXG_AWS_SECRET_NAME")
|
|
secret_region_name = os.getenv("CXG_AWS_SECRET_REGION_NAME")
|
|
|
|
if config_file:
|
|
region_name = discover_s3_region_name(config_file)
|
|
config_location = DataLocator(config_file, region_name)
|
|
if config_location.exists():
|
|
with config_location.local_handle() as lh:
|
|
logging.info(f"Configuration from {config_file}")
|
|
app_config.update_from_config_file(lh)
|
|
else:
|
|
logging.critical(f"Configuration file not found {config_file}")
|
|
sys.exit(1)
|
|
else:
|
|
# no config file specified, try "config.yaml" in the current working directory
|
|
config_file = "config.yaml"
|
|
config_location = DataLocator(config_file)
|
|
if config_location.exists():
|
|
with config_location.local_handle() as lh:
|
|
logging.info(f"Configuration from {config_file}")
|
|
app_config.update_from_config_file(lh)
|
|
|
|
if dataroot:
|
|
logging.info(f"Configuration from CXG_DATAROOT")
|
|
app_config.update(multi_dataset__dataroot=dataroot)
|
|
|
|
if secret_name:
|
|
if secret_region_name is None:
|
|
secret_region_name = discover_s3_region_name(app_config.multi_dataset__dataroot)
|
|
if not secret_region_name:
|
|
logging.error(f"Expected to discover the s3 region name from {app_config.multi_dataset__dataroot}")
|
|
flask_secret_key = get_flask_secret_key(secret_region_name, secret_name)
|
|
app_config.update(server__flask_secret_key=flask_secret_key)
|
|
|
|
# features are unsupported in the current hosted server
|
|
app_config.update(
|
|
user_annotations__enable=False,
|
|
embeddings__enable_reembedding=False,
|
|
multi_dataset__allowed_matrix_types=["cxg"],
|
|
)
|
|
|
|
app_config.complete_config(logging.info)
|
|
|
|
if not app_config.server__flask_secret_key:
|
|
logging.critical(
|
|
f"flask_secret_key is not provided. Either set in config file, CXG_SECRET_KEY environment variable, "
|
|
"or in AWS Secret Manager"
|
|
)
|
|
sys.exit(1)
|
|
|
|
user_annotations = app_config.user_annotations
|
|
|
|
server = WSGIServer(app_config)
|
|
|
|
debug = False
|
|
application = server.app
|
|
|
|
except Exception:
|
|
logging.critical("Caught exception during initialization", exc_info=True)
|
|
sys.exit(1)
|
|
|
|
if app_config.multi_dataset__dataroot:
|
|
logging.info(f"starting server with multi_dataset__dataroot={app_config.multi_dataset__dataroot}")
|
|
elif app_config.single_dataset__datapath:
|
|
logging.info(f"starting server with single_dataset__datapath={app_config.single_dataset__datapath}")
|
|
|
|
if __name__ == "__main__":
|
|
try:
|
|
application.run(debug=debug, threaded=not debug, use_debugger=False)
|
|
except Exception:
|
|
logging.critical("Caught exception during initialization", exc_info=True)
|
|
sys.exit(1)
|