Files
mimic-big/backend/src/mimic/app.py

82 lines
2.6 KiB
Python
Raw Normal View History

"""Flask application factory."""
from __future__ import annotations
from datetime import timedelta
from flask import Flask, jsonify
from flask.typing import ResponseReturnValue
from mimic.api import register_blueprints
from mimic.auth.identity import load_user
from mimic.config import Settings, get_settings
from mimic.extensions import db, login_manager, migrate, socketio
from mimic.logging import configure_logging
def create_app(settings: Settings | None = None) -> Flask:
settings = settings or get_settings()
configure_logging(settings.log_level, as_json=settings.log_json)
app = Flask(__name__)
app.config.update(
SECRET_KEY=settings.secret_key.get_secret_value(),
SQLALCHEMY_DATABASE_URI=settings.database_url,
SQLALCHEMY_TRACK_MODIFICATIONS=False,
SESSION_COOKIE_SECURE=settings.session_cookie_secure,
SESSION_COOKIE_SAMESITE=settings.session_cookie_samesite,
SESSION_COOKIE_HTTPONLY=True,
PERMANENT_SESSION_LIFETIME=timedelta(minutes=settings.session_lifetime_minutes),
MIMIC_SETTINGS=settings,
)
db.init_app(app)
migrate.init_app(app, db, directory="src/mimic/db/migrations")
login_manager.init_app(app)
chore(backend): mypy strict clean + ruff format pass Pre-merge sanity per devops checklist (ruff format --check, mypy --strict). Type fixes: - ORM models: `Mapped[dict]` → `Mapped[dict[str, Any]]` (audit, scenario, run, report, ttp, detection.artifact_files_json). Equivalent on Pydantic DTOs (TtpBase.params_schema_json, ScenarioStepBase.params_override_json). - Rename `TtpRead.current_version` → `TtpRead.version` to mirror the ORM column (which itself was renamed in D-009 cleanup). - Flask blueprints: add `-> ResponseReturnValue` to every view, plus typed UUID params on `_validate_step_consistency`. - `templating/filters.py`: rewrite the conditional re2 import so mypy can narrow the union (`ModuleType | None`); the runtime branch on `_re2 is not None` removes the unused-ignore that was triggered by warn_unused_ignores. - `pyproject.toml`: add `flask_login.*` and `pythonjsonlogger.*` to the `[[tool.mypy.overrides]]` `ignore_missing_imports` list (both ship without typed marker). - Misc: drop stale `# type: ignore` comments (`app.py:36`, `rbac/decorators.py:35`) flagged by `warn_unused_ignores`. Keep `logging.JsonFormatter` ignore because the symbol exists at runtime but is not re-exported through the typed surface. Formatting: - `ruff format` applied (15 files normalized; line-length unchanged at 100). Verification on this commit: - `ruff check` → All checks passed. - `ruff format --check` → 68 files already formatted. - `mypy --strict src` → Success: no issues found in 54 source files. - `pytest tests/unit` → 49 passed.
2026-05-22 05:10:51 +02:00
login_manager.user_loader(load_user)
feat(backend): wire auth endpoints + dev CORS (sprint 1) Three login endpoints under /api/v1/auth/ + dev-only CORS so the Vite frontend can drive the session cookie. - POST /login validates local credentials and sets a Flask session cookie. Returns the CurrentUser shape on 200 (user_id, username=email, display_name, role, permissions, groups). Uniform 401 invalid_credentials on bad password or unknown user; a bcrypt round against a dummy hash runs even on unknown users so the request timing does not enumerate accounts. Audits an auth.login row and bumps user.last_login_at. - POST /logout (login_required) clears the session, returns 204, audits an auth.logout row. - GET /me returns the current principal or 401 not_authenticated. Used by the frontend at boot to rehydrate state. Side wiring: - LoginManager.unauthorized_handler emits the same {error, message} JSON envelope so @login_required 401s match the rest of the API surface. - api/_helpers gains `serialize_current_user(AuthUser) -> CurrentUser` and `api_error(code, message, status)` — used by the auth blueprint and available to follow-up endpoints. - AuthUser carries display_name + user_type now; identity.load_user routes through a new `authuser_from_orm()` helper that the login endpoint also uses so /login and the user_loader produce identical shapes. - Dev-only CORS via flask-cors on /api/*, gated on MIMIC_ENV=development AND MIMIC_CORS_ORIGINS non-empty. Prod keeps same-origin (reverse proxy fronts the SPA + API). - LoginRequest + CurrentUser DTOs added to mimic.schemas. No frontend-visible change to engagements (sprint-0 already shipped created_by_id, audit log, F11 scope).
2026-05-23 04:21:44 +02:00
@login_manager.unauthorized_handler # type: ignore[untyped-decorator]
def _unauthorized() -> ResponseReturnValue:
# API returns JSON; never redirect to a login page.
return (
jsonify({"error": "not_authenticated", "message": "no active session"}),
401,
)
socketio.init_app(
app,
cors_allowed_origins=settings.cors_origins or "*",
async_mode="gevent",
)
feat(backend): wire auth endpoints + dev CORS (sprint 1) Three login endpoints under /api/v1/auth/ + dev-only CORS so the Vite frontend can drive the session cookie. - POST /login validates local credentials and sets a Flask session cookie. Returns the CurrentUser shape on 200 (user_id, username=email, display_name, role, permissions, groups). Uniform 401 invalid_credentials on bad password or unknown user; a bcrypt round against a dummy hash runs even on unknown users so the request timing does not enumerate accounts. Audits an auth.login row and bumps user.last_login_at. - POST /logout (login_required) clears the session, returns 204, audits an auth.logout row. - GET /me returns the current principal or 401 not_authenticated. Used by the frontend at boot to rehydrate state. Side wiring: - LoginManager.unauthorized_handler emits the same {error, message} JSON envelope so @login_required 401s match the rest of the API surface. - api/_helpers gains `serialize_current_user(AuthUser) -> CurrentUser` and `api_error(code, message, status)` — used by the auth blueprint and available to follow-up endpoints. - AuthUser carries display_name + user_type now; identity.load_user routes through a new `authuser_from_orm()` helper that the login endpoint also uses so /login and the user_loader produce identical shapes. - Dev-only CORS via flask-cors on /api/*, gated on MIMIC_ENV=development AND MIMIC_CORS_ORIGINS non-empty. Prod keeps same-origin (reverse proxy fronts the SPA + API). - LoginRequest + CurrentUser DTOs added to mimic.schemas. No frontend-visible change to engagements (sprint-0 already shipped created_by_id, audit log, F11 scope).
2026-05-23 04:21:44 +02:00
_enable_cors_in_dev(app, settings)
register_blueprints(app)
@app.get("/healthz")
def healthz() -> ResponseReturnValue:
return jsonify(status="ok"), 200
return app
feat(backend): wire auth endpoints + dev CORS (sprint 1) Three login endpoints under /api/v1/auth/ + dev-only CORS so the Vite frontend can drive the session cookie. - POST /login validates local credentials and sets a Flask session cookie. Returns the CurrentUser shape on 200 (user_id, username=email, display_name, role, permissions, groups). Uniform 401 invalid_credentials on bad password or unknown user; a bcrypt round against a dummy hash runs even on unknown users so the request timing does not enumerate accounts. Audits an auth.login row and bumps user.last_login_at. - POST /logout (login_required) clears the session, returns 204, audits an auth.logout row. - GET /me returns the current principal or 401 not_authenticated. Used by the frontend at boot to rehydrate state. Side wiring: - LoginManager.unauthorized_handler emits the same {error, message} JSON envelope so @login_required 401s match the rest of the API surface. - api/_helpers gains `serialize_current_user(AuthUser) -> CurrentUser` and `api_error(code, message, status)` — used by the auth blueprint and available to follow-up endpoints. - AuthUser carries display_name + user_type now; identity.load_user routes through a new `authuser_from_orm()` helper that the login endpoint also uses so /login and the user_loader produce identical shapes. - Dev-only CORS via flask-cors on /api/*, gated on MIMIC_ENV=development AND MIMIC_CORS_ORIGINS non-empty. Prod keeps same-origin (reverse proxy fronts the SPA + API). - LoginRequest + CurrentUser DTOs added to mimic.schemas. No frontend-visible change to engagements (sprint-0 already shipped created_by_id, audit log, F11 scope).
2026-05-23 04:21:44 +02:00
def _enable_cors_in_dev(app: Flask, settings: Settings) -> None:
"""Dev-only CORS for the Vite frontend on http://localhost:5173.
In production, the reverse proxy (Caddy + same-origin) terminates this
concern; enabling CORS there would expand the CSRF surface for no benefit.
"""
if settings.env != "development":
return
if not settings.cors_origins:
return
from flask_cors import CORS # noqa: PLC0415 — keeps the prod import path lean
CORS(
app,
resources={r"/api/*": {"origins": settings.cors_origins}},
supports_credentials=True,
methods=["GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"],
allow_headers=["Content-Type", "X-Requested-With"],
max_age=600,
)