From 01ea3ca4a4cfd9a9a92d584d8a27f432c170c05f Mon Sep 17 00:00:00 2001 From: Nicole Rappe Date: Sun, 26 Oct 2025 01:38:39 -0600 Subject: [PATCH] Bridge legacy API registration through Engine --- Data/Engine/CODE_MIGRATION_TRACKER.md | 10 ++-- Data/Engine/server.py | 80 ++++++++++++++++++++------- Data/Server/server.py | 76 +++++++++++++++++++------ 3 files changed, 124 insertions(+), 42 deletions(-) diff --git a/Data/Engine/CODE_MIGRATION_TRACKER.md b/Data/Engine/CODE_MIGRATION_TRACKER.md index 6309850..84d22c3 100644 --- a/Data/Engine/CODE_MIGRATION_TRACKER.md +++ b/Data/Engine/CODE_MIGRATION_TRACKER.md @@ -27,10 +27,10 @@ Lastly, everytime that you complete a stage, you will create a pull request name - [x] Provide fixtures that mirror the legacy SQLite schema and seed data. - [x] Assert HTTP status codes, payloads, and side effects for parity. - [x] Integrate Engine API tests into CI/local workflows. -- [ ] **Stage 5 — Bridge the legacy server to Engine APIs** - - [ ] Delegate API blueprint registration to the Engine factory from the legacy server. - - [ ] Replace legacy API routes with Engine-provided blueprints gated by a flag. - - [ ] Emit transitional logging when Engine handles requests. +- [x] **Stage 5 — Bridge the legacy server to Engine APIs** + - [x] Delegate API blueprint registration to the Engine factory from the legacy server. + - [x] Replace legacy API routes with Engine-provided blueprints gated by a flag. + - [x] Emit transitional logging when Engine handles requests. - [ ] **Stage 6 — Plan WebUI migration** - [ ] Move static/template handling into Data/Engine/services/WebUI. - [ ] Preserve TLS-aware URL generation and caching. @@ -43,5 +43,5 @@ Lastly, everytime that you complete a stage, you will create a pull request name - [ ] Update legacy server to consume Engine WebSocket registration. ## Current Status -- **Stage:** Stage 4 — Build unit and smoke tests for Engine APIs (completed) +- **Stage:** Stage 5 — Bridge the legacy server to Engine APIs (completed) - **Active Task:** Awaiting next stage instructions. diff --git a/Data/Engine/server.py b/Data/Engine/server.py index 0499b53..c8c84cf 100644 --- a/Data/Engine/server.py +++ b/Data/Engine/server.py @@ -17,7 +17,7 @@ from pathlib import Path from typing import Any, Mapping, Optional, Sequence, Tuple import eventlet -from flask import Flask +from flask import Flask, request from flask_cors import CORS from flask_socketio import SocketIO from werkzeug.middleware.proxy_fix import ProxyFix @@ -106,7 +106,46 @@ class EngineContext: api_groups: Sequence[str] -__all__ = ["EngineContext", "create_app"] +__all__ = ["EngineContext", "create_app", "register_engine_api"] + + +def _build_engine_context(settings: EngineSettings, logger: logging.Logger) -> EngineContext: + return EngineContext( + database_path=settings.database_path, + logger=logger, + scheduler=None, + tls_cert_path=settings.tls_cert_path, + tls_key_path=settings.tls_key_path, + tls_bundle_path=settings.tls_bundle_path, + config=settings.as_dict(), + api_groups=settings.api_groups, + ) + + +def _attach_transition_logging(app: Flask, context: EngineContext, logger: logging.Logger) -> None: + tracked = {group.strip().lower() for group in context.api_groups if group} + if not tracked: + tracked = {"tokens", "enrollment"} + + existing = getattr(app, "_engine_api_tracked_blueprints", set()) + if existing: + tracked.update(existing) + setattr(app, "_engine_api_tracked_blueprints", tracked) + + if getattr(app, "_engine_api_logging_installed", False): + return + + @app.before_request + def _log_engine_api_bridge() -> None: # pragma: no cover - integration behaviour exercised in higher-level tests + blueprint = (request.blueprint or "").lower() + if blueprint and blueprint in getattr(app, "_engine_api_tracked_blueprints", tracked): + logger.info( + "Engine handling API request via legacy bridge: %s %s", + request.method, + request.path, + ) + + setattr(app, "_engine_api_logging_installed", True) def create_app(config: Optional[Mapping[str, Any]] = None) -> Tuple[Flask, SocketIO, EngineContext]: @@ -115,8 +154,6 @@ def create_app(config: Optional[Mapping[str, Any]] = None) -> Tuple[Flask, Socke settings: EngineSettings = load_runtime_config(config) logger = initialise_engine_logger(settings) - database_path = settings.database_path - static_folder = settings.static_folder app = Flask(__name__, static_folder=static_folder, static_url_path="") app.wsgi_app = ProxyFix(app.wsgi_app, x_proto=1, x_host=1) @@ -141,22 +178,7 @@ def create_app(config: Optional[Mapping[str, Any]] = None) -> Tuple[Flask, Socke }, ) - tls_cert_path, tls_key_path, tls_bundle_path = ( - settings.tls_cert_path, - settings.tls_key_path, - settings.tls_bundle_path, - ) - - context = EngineContext( - database_path=database_path, - logger=logger, - scheduler=None, - tls_cert_path=tls_cert_path, - tls_key_path=tls_key_path, - tls_bundle_path=tls_bundle_path, - config=settings.as_dict(), - api_groups=settings.api_groups, - ) + context = _build_engine_context(settings, logger) from .services import API, WebSocket, WebUI # Local import to avoid circular deps during bootstrap @@ -167,3 +189,21 @@ def create_app(config: Optional[Mapping[str, Any]] = None) -> Tuple[Flask, Socke logger.debug("Engine application factory completed initialisation.") return app, socketio, context + + +def register_engine_api(app: Flask, *, config: Optional[Mapping[str, Any]] = None) -> EngineContext: + """Register Engine-managed API blueprints onto an existing Flask app.""" + + settings: EngineSettings = load_runtime_config(config) + logger = initialise_engine_logger(settings) + context = _build_engine_context(settings, logger) + + from .services import API # Local import avoids circular dependency at module import time + + API.register_api(app, context) + _attach_transition_logging(app, context, logger) + + groups_display = ", ".join(context.api_groups) if context.api_groups else "none" + logger.info("Engine API delegation activated for groups: %s", groups_display) + + return context diff --git a/Data/Server/server.py b/Data/Server/server.py index 07dcd40..67806c5 100644 --- a/Data/Server/server.py +++ b/Data/Server/server.py @@ -210,6 +210,13 @@ def _infer_server_scope(message: str, explicit: Optional[str]) -> Optional[str]: return None +def _env_flag(name: str, *, default: bool = False) -> bool: + raw = os.environ.get(name) + if raw is None: + return default + return raw.strip().lower() in {"1", "true", "yes", "on"} + + def _is_internal_request(req: Request) -> bool: """Return True if the HTTP request originated from the local server host.""" try: @@ -326,6 +333,8 @@ AUTH_RATE_LIMITER = SlidingWindowRateLimiter() ENROLLMENT_NONCE_CACHE = NonceCache() DPOP_VALIDATOR = DPoPValidator() DEVICE_AUTH_MANAGER: Optional[DeviceAuthManager] = None +ENGINE_API_ENABLED = _env_flag("BOREALIS_ENGINE_API") +ENGINE_API_GROUPS: Tuple[str, ...] = tuple() def _set_cached_github_token(token: Optional[str]) -> None: @@ -5088,24 +5097,57 @@ def init_db(): init_db() -enrollment_routes.register( - app, - db_conn_factory=_db_conn, - log=_write_service_log, - jwt_service=JWT_SERVICE, - tls_bundle_path=TLS_BUNDLE_PATH, - ip_rate_limiter=IP_RATE_LIMITER, - fp_rate_limiter=FP_RATE_LIMITER, - nonce_cache=ENROLLMENT_NONCE_CACHE, - script_signer=SCRIPT_SIGNER, -) +if ENGINE_API_ENABLED: + _engine_api_config: Dict[str, Any] = { + "DATABASE_PATH": DB_PATH, + "TLS_CERT_PATH": TLS_CERT_PATH, + "TLS_KEY_PATH": TLS_KEY_PATH, + "TLS_BUNDLE_PATH": TLS_BUNDLE_PATH, + } + api_groups_override = os.environ.get("BOREALIS_API_GROUPS") + if api_groups_override: + _engine_api_config["API_GROUPS"] = api_groups_override -token_routes.register( - app, - db_conn_factory=_db_conn, - jwt_service=JWT_SERVICE, - dpop_validator=DPOP_VALIDATOR, -) + try: + from Data.Engine.server import register_engine_api + + _engine_context = register_engine_api(app, config=_engine_api_config) + except Exception: + ENGINE_API_ENABLED = False + ENGINE_API_GROUPS = tuple() + _write_service_log( + "server", + "Engine API delegation failed; continuing with legacy API registration.", + level="ERROR", + ) + else: + ENGINE_API_GROUPS = tuple(_engine_context.api_groups) + _write_service_log( + "server", + "Engine API delegation enabled for groups: {}".format( + ", ".join(ENGINE_API_GROUPS) or "default" + ), + ) + +if not ENGINE_API_ENABLED: + enrollment_routes.register( + app, + db_conn_factory=_db_conn, + log=_write_service_log, + jwt_service=JWT_SERVICE, + tls_bundle_path=TLS_BUNDLE_PATH, + ip_rate_limiter=IP_RATE_LIMITER, + fp_rate_limiter=FP_RATE_LIMITER, + nonce_cache=ENROLLMENT_NONCE_CACHE, + script_signer=SCRIPT_SIGNER, + ) + + token_routes.register( + app, + db_conn_factory=_db_conn, + jwt_service=JWT_SERVICE, + dpop_validator=DPOP_VALIDATOR, + ) agent_routes.register( app,