"""The application factory.""" from abacus_common_logic.connectors.database import db from abacus_common_logic.db.adapters import get_adapter from abacus_common_logic.marshalling.custom_fields import ma from abacus_common_logic.utils.users import set_flask_user_details_from_headers from flask import Flask from owslogger import flask_logger from owsrequest import flask_request from owsresponse import response from owsresponse.adaptors.flask import flaskify from werkzeug.exceptions import HTTPException, InternalServerError from abacus_contract.api import register_blueprints as register_abacus_contract from abacus_file_upload.api import register_blueprints as register_abacus_file_upload from core.hardening import init_hardening from royalties.api import register_blueprints as register_royalties REGISTRY = { 'abacus_contract': register_abacus_contract, 'abacus_file_upload': register_abacus_file_upload, 'royalties': register_royalties, } def create_app(config, modules: list[str] | None = None): """Create an app.""" app = Flask(config.SERVICE_NAME) app.config.from_object(config) app.url_map.strict_slashes = False setup_application_log(app, config) setup_owsrequest(app, config) setup_db(app, config) setup_marshmallow(app) register_request_handler(app) register_error_handlers(app) setup_cors(app, config) # Register blueprints if modules is None: modules = list(REGISTRY.keys()) print(f'Registering {len(modules)} modules:') register_common_blueprints(app) for name, register in REGISTRY.items(): if name in modules: print(f'\tRegistering {name}...') register(app) print('Registration complete.') # API hardening kit: ProxyFix, leak-free error handlers, body-size limit, security headers, # compression, and per-principal rate limiting (shadow|enforce via RATELIMIT_MODE). Wired as # the LAST statement, after blueprints register, since rate limiting walks app.view_functions. init_hardening(app, config) return app def register_request_handler(app): """Register function to preprocess request data.""" @app.before_request def before_request_start(): """Parse the user info in the header into flask.g.""" set_flask_user_details_from_headers() def setup_marshmallow(app): """Set up marshmallow.""" ma.init_app(app) def setup_owsrequest(app, config): """Set up request framework.""" try: import uwsgi uwsgiRunning = True except ImportError: uwsgiRunning = False flask_request.set_rules_validator(app, 'core/access_rules.yml') flask_request.setup( app, config.ENVIRONMENT, add_request_context=True, label_profile=True, verify_access=False, rules_file=None, access_log_only=config.ONLY_LOG_ACCESS_ERRORS, exclude_paths=config.HEALTH_CHECK_PATHS, uwsgi_cache_enabled=uwsgiRunning, ) def setup_application_log(app, config): """Set up the application log.""" flask_logger.setup( app, config.ENVIRONMENT, config.LOGGER_NAME, config.LOGGER_LEVEL, config.SERVICE_NAME, config.SERVICE_VERSION, exclude_paths=config.HEALTH_CHECK_PATHS, ) def setup_db(app, config): """Set up the database.""" get_adapter(config.DB_VENDOR).setup_db(db, app, config) def register_error_handlers(app): """Register error handlers.""" def http_error_handler(error): return flaskify( response.create_error_response( message=error.description, status=error.code, code='error' ) ) app.register_error_handler(HTTPException, http_error_handler) def exception_handler(exception): message = ( 'The server encountered an internal error ' 'and was unable to complete your request.' ) return flaskify(response.create_fatal_response(message)) app.register_error_handler(InternalServerError, exception_handler) def setup_cors(app, config): """Set up CORS for local development.""" # Only enable CORS in development environments if config.ENVIRONMENT not in ['dev']: return @app.after_request def add_cors_headers(response): """Add CORS headers to response.""" response.headers['Access-Control-Allow-Origin'] = '*' response.headers['Access-Control-Allow-Methods'] = ( 'GET, POST, PUT, DELETE, OPTIONS' ) response.headers['Access-Control-Allow-Headers'] = 'Content-Type, Authorization' response.headers['Access-Control-Max-Age'] = '3600' return response def register_common_blueprints(app): """Register blueprints.""" from core.blueprints.base import base_api app.register_blueprint(base_api)