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.yaml in 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.Flask application instance.