app
Flask application factory for AIFT.
Provides the create_app() factory function that initialises the Flask
application, loads configuration from config/config.yaml, sets the upload
size limit, registers all HTTP route blueprints, and configures CSRF protection.
A Python version guard runs at import time so that downstream code can assume a supported interpreter.
Attributes:
- CSRF_HEADER: Name of the HTTP header used to transmit the CSRF token.
- CSRF_SAFE_METHODS: HTTP methods exempt from CSRF validation (read-only methods that do not modify server state).
1"""Flask application factory for AIFT. 2 3Provides the :func:`create_app` factory function that initialises the Flask 4application, loads configuration from ``config/config.yaml``, sets the upload 5size limit, registers all HTTP route blueprints, and configures CSRF protection. 6 7A Python version guard runs at import time so that downstream code can 8assume a supported interpreter. 9 10Attributes: 11 CSRF_HEADER: Name of the HTTP header used to transmit the CSRF token. 12 CSRF_SAFE_METHODS: HTTP methods exempt from CSRF validation (read-only 13 methods that do not modify server state). 14""" 15 16from __future__ import annotations 17 18import secrets 19from pathlib import Path 20from typing import TYPE_CHECKING 21 22from runtime_compat import assert_supported_python_version 23 24assert_supported_python_version() 25 26if TYPE_CHECKING: 27 from flask import Flask 28 29__all__ = [ 30 "create_app", 31] 32 33CSRF_HEADER = "X-CSRF-Token" 34CSRF_SAFE_METHODS = frozenset({"GET", "HEAD", "OPTIONS"}) 35 36 37def _default_config_path() -> Path: 38 """Return the default config path without importing config eagerly.""" 39 from .utils.config import DEFAULT_CONFIG_RELATIVE_PATH, PROJECT_ROOT 40 41 return PROJECT_ROOT / DEFAULT_CONFIG_RELATIVE_PATH 42 43 44def create_app( 45 config_path: str | None = None, 46 config: dict | None = None, 47) -> Flask: 48 """Create and configure the Flask application instance. 49 50 Loads AIFT configuration (merging defaults, YAML, and environment 51 variables), stores it in ``app.config["AIFT_CONFIG"]``, configures the 52 maximum upload size, generates a per-process CSRF token, installs CSRF 53 validation middleware, and registers all HTTP routes. 54 55 Args: 56 config_path: Optional path to a YAML configuration file. When 57 *None*, the default ``config/config.yaml`` in the project root is used. 58 Ignored when *config* is provided. 59 config: Optional pre-loaded configuration dictionary. When 60 provided, :func:`~app.utils.config.load_config` is **not** called, 61 avoiding redundant parsing and validation of the YAML config file. 62 63 Returns: 64 A fully configured :class:`~flask.Flask` application instance. 65 """ 66 from flask import Flask 67 from .utils.config import load_config 68 69 app = Flask(__name__, template_folder="../templates", static_folder="../static") 70 aift_config = config if config is not None else load_config(config_path) 71 # Store the resolved absolute path so all downstream code uses it consistently. 72 resolved_config_path = ( 73 str(Path(config_path).resolve()) 74 if config_path is not None 75 else str(_default_config_path()) 76 ) 77 app.config["AIFT_CONFIG"] = aift_config 78 app.config["AIFT_CONFIG_PATH"] = resolved_config_path 79 80 # Enforce an upload size limit so Flask/Werkzeug rejects oversized request 81 # bodies before buffering them fully into memory. A value of 0 means 82 # "unlimited" per project convention, so MAX_CONTENT_LENGTH stays None. 83 large_file_threshold_mb: int | float = ( 84 aift_config.get("evidence", {}).get("large_file_threshold_mb", 0) 85 ) 86 if large_file_threshold_mb > 0: 87 app.config["MAX_CONTENT_LENGTH"] = int(large_file_threshold_mb * 1024 * 1024) 88 89 # Generate a per-process CSRF token for protecting state-changing requests. 90 app.config["CSRF_TOKEN"] = secrets.token_hex(32) 91 92 _register_csrf_protection(app) 93 from .routes import register_routes 94 95 register_routes(app) 96 97 return app 98 99 100def _register_csrf_protection(app: Flask) -> None: 101 """Install a ``before_request`` hook that validates the CSRF token. 102 103 All requests whose method is not in :data:`CSRF_SAFE_METHODS` must 104 include a valid ``X-CSRF-Token`` header matching the token stored in 105 ``app.config["CSRF_TOKEN"]``. Requests to the CSRF token endpoint 106 itself (``/api/csrf-token``) are exempt so the frontend can obtain the 107 token. 108 109 Args: 110 app: The Flask application to attach the hook to. 111 """ 112 from flask import jsonify, request 113 114 @app.before_request 115 def _enforce_csrf() -> tuple | None: 116 """Reject state-changing requests that lack a valid CSRF token. 117 118 Returns: 119 A 403 JSON error response tuple when validation fails, or 120 ``None`` to allow the request to proceed. 121 """ 122 if request.method in CSRF_SAFE_METHODS: 123 return None 124 if request.path == "/api/csrf-token": 125 return None 126 # Automation API is for programmatic access; no CSRF required. 127 if request.path.startswith("/api/automation/"): 128 return None 129 token = request.headers.get(CSRF_HEADER, "") 130 if not secrets.compare_digest(token, app.config["CSRF_TOKEN"]): 131 return jsonify({"error": "CSRF token missing or invalid."}), 403 132 return None 133 134 @app.get("/api/csrf-token") 135 def _get_csrf_token() -> tuple: 136 """Return the CSRF token so the frontend can include it in requests. 137 138 Returns: 139 A JSON response containing the CSRF token with a 200 status. 140 """ 141 return jsonify({"csrf_token": app.config["CSRF_TOKEN"]}), 200
def
create_app( config_path: str | None = None, config: dict | None = None) -> flask.app.Flask:
45def create_app( 46 config_path: str | None = None, 47 config: dict | None = None, 48) -> Flask: 49 """Create and configure the Flask application instance. 50 51 Loads AIFT configuration (merging defaults, YAML, and environment 52 variables), stores it in ``app.config["AIFT_CONFIG"]``, configures the 53 maximum upload size, generates a per-process CSRF token, installs CSRF 54 validation middleware, and registers all HTTP routes. 55 56 Args: 57 config_path: Optional path to a YAML configuration file. When 58 *None*, the default ``config/config.yaml`` in the project root is used. 59 Ignored when *config* is provided. 60 config: Optional pre-loaded configuration dictionary. When 61 provided, :func:`~app.utils.config.load_config` is **not** called, 62 avoiding redundant parsing and validation of the YAML config file. 63 64 Returns: 65 A fully configured :class:`~flask.Flask` application instance. 66 """ 67 from flask import Flask 68 from .utils.config import load_config 69 70 app = Flask(__name__, template_folder="../templates", static_folder="../static") 71 aift_config = config if config is not None else load_config(config_path) 72 # Store the resolved absolute path so all downstream code uses it consistently. 73 resolved_config_path = ( 74 str(Path(config_path).resolve()) 75 if config_path is not None 76 else str(_default_config_path()) 77 ) 78 app.config["AIFT_CONFIG"] = aift_config 79 app.config["AIFT_CONFIG_PATH"] = resolved_config_path 80 81 # Enforce an upload size limit so Flask/Werkzeug rejects oversized request 82 # bodies before buffering them fully into memory. A value of 0 means 83 # "unlimited" per project convention, so MAX_CONTENT_LENGTH stays None. 84 large_file_threshold_mb: int | float = ( 85 aift_config.get("evidence", {}).get("large_file_threshold_mb", 0) 86 ) 87 if large_file_threshold_mb > 0: 88 app.config["MAX_CONTENT_LENGTH"] = int(large_file_threshold_mb * 1024 * 1024) 89 90 # Generate a per-process CSRF token for protecting state-changing requests. 91 app.config["CSRF_TOKEN"] = secrets.token_hex(32) 92 93 _register_csrf_protection(app) 94 from .routes import register_routes 95 96 register_routes(app) 97 98 return app
Create and configure the Flask application instance.
Loads AIFT configuration (merging defaults, YAML, and environment
variables), stores it in app.config["AIFT_CONFIG"], configures the
maximum upload size, generates a per-process CSRF token, installs CSRF
validation middleware, and registers all HTTP routes.
Arguments:
- config_path: Optional path to a YAML configuration file. When
None, the default
config/config.yamlin the project root is used. Ignored when config is provided. - config: Optional pre-loaded configuration dictionary. When
provided,
~app.utils.config.load_config()is not called, avoiding redundant parsing and validation of the YAML config file.
Returns:
A fully configured
~flask.Flaskapplication instance.