"""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}") secret_region_name = discover_s3_region_name(config_file) 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)