Bridge legacy API registration through Engine

This commit is contained in:
2025-10-26 01:38:39 -06:00
parent 7a3db6cbb0
commit 01ea3ca4a4
3 changed files with 124 additions and 42 deletions

View File

@@ -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] Provide fixtures that mirror the legacy SQLite schema and seed data.
- [x] Assert HTTP status codes, payloads, and side effects for parity. - [x] Assert HTTP status codes, payloads, and side effects for parity.
- [x] Integrate Engine API tests into CI/local workflows. - [x] Integrate Engine API tests into CI/local workflows.
- [ ] **Stage 5 — Bridge the legacy server to Engine APIs** - [x] **Stage 5 — Bridge the legacy server to Engine APIs**
- [ ] Delegate API blueprint registration to the Engine factory from the legacy server. - [x] Delegate API blueprint registration to the Engine factory from the legacy server.
- [ ] Replace legacy API routes with Engine-provided blueprints gated by a flag. - [x] Replace legacy API routes with Engine-provided blueprints gated by a flag.
- [ ] Emit transitional logging when Engine handles requests. - [x] Emit transitional logging when Engine handles requests.
- [ ] **Stage 6 — Plan WebUI migration** - [ ] **Stage 6 — Plan WebUI migration**
- [ ] Move static/template handling into Data/Engine/services/WebUI. - [ ] Move static/template handling into Data/Engine/services/WebUI.
- [ ] Preserve TLS-aware URL generation and caching. - [ ] 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. - [ ] Update legacy server to consume Engine WebSocket registration.
## Current Status ## 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. - **Active Task:** Awaiting next stage instructions.

View File

@@ -17,7 +17,7 @@ from pathlib import Path
from typing import Any, Mapping, Optional, Sequence, Tuple from typing import Any, Mapping, Optional, Sequence, Tuple
import eventlet import eventlet
from flask import Flask from flask import Flask, request
from flask_cors import CORS from flask_cors import CORS
from flask_socketio import SocketIO from flask_socketio import SocketIO
from werkzeug.middleware.proxy_fix import ProxyFix from werkzeug.middleware.proxy_fix import ProxyFix
@@ -106,7 +106,46 @@ class EngineContext:
api_groups: Sequence[str] 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]: 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) settings: EngineSettings = load_runtime_config(config)
logger = initialise_engine_logger(settings) logger = initialise_engine_logger(settings)
database_path = settings.database_path
static_folder = settings.static_folder static_folder = settings.static_folder
app = Flask(__name__, static_folder=static_folder, static_url_path="") app = Flask(__name__, static_folder=static_folder, static_url_path="")
app.wsgi_app = ProxyFix(app.wsgi_app, x_proto=1, x_host=1) 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 = ( context = _build_engine_context(settings, logger)
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,
)
from .services import API, WebSocket, WebUI # Local import to avoid circular deps during bootstrap 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.") logger.debug("Engine application factory completed initialisation.")
return app, socketio, context 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

View File

@@ -210,6 +210,13 @@ def _infer_server_scope(message: str, explicit: Optional[str]) -> Optional[str]:
return None 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: def _is_internal_request(req: Request) -> bool:
"""Return True if the HTTP request originated from the local server host.""" """Return True if the HTTP request originated from the local server host."""
try: try:
@@ -326,6 +333,8 @@ AUTH_RATE_LIMITER = SlidingWindowRateLimiter()
ENROLLMENT_NONCE_CACHE = NonceCache() ENROLLMENT_NONCE_CACHE = NonceCache()
DPOP_VALIDATOR = DPoPValidator() DPOP_VALIDATOR = DPoPValidator()
DEVICE_AUTH_MANAGER: Optional[DeviceAuthManager] = None 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: def _set_cached_github_token(token: Optional[str]) -> None:
@@ -5088,24 +5097,57 @@ def init_db():
init_db() init_db()
enrollment_routes.register( if ENGINE_API_ENABLED:
app, _engine_api_config: Dict[str, Any] = {
db_conn_factory=_db_conn, "DATABASE_PATH": DB_PATH,
log=_write_service_log, "TLS_CERT_PATH": TLS_CERT_PATH,
jwt_service=JWT_SERVICE, "TLS_KEY_PATH": TLS_KEY_PATH,
tls_bundle_path=TLS_BUNDLE_PATH, "TLS_BUNDLE_PATH": TLS_BUNDLE_PATH,
ip_rate_limiter=IP_RATE_LIMITER, }
fp_rate_limiter=FP_RATE_LIMITER, api_groups_override = os.environ.get("BOREALIS_API_GROUPS")
nonce_cache=ENROLLMENT_NONCE_CACHE, if api_groups_override:
script_signer=SCRIPT_SIGNER, _engine_api_config["API_GROUPS"] = api_groups_override
)
token_routes.register( try:
app, from Data.Engine.server import register_engine_api
db_conn_factory=_db_conn,
jwt_service=JWT_SERVICE, _engine_context = register_engine_api(app, config=_engine_api_config)
dpop_validator=DPOP_VALIDATOR, 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( agent_routes.register(
app, app,