diff --git a/README.md b/README.md index 67fd003..fc33d7a 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ RouteForge Logo

- Version + Version License Status Selfhosted @@ -43,7 +43,7 @@ Routing changes often require fast but traceable checks across multiple external ## Current Alpha Status -RouteForge is a **functional beta** release with production-like workflows for read-only validation and demo usage. Current release target: **v0.8.1-beta**. +RouteForge is a **functional beta** release with production-like workflows for read-only validation and demo usage. Current release target: **v0.9.1-rc**. ## Quickstart with Docker Compose diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md index 8fdbfbe..f434aac 100644 --- a/RELEASE_NOTES.md +++ b/RELEASE_NOTES.md @@ -1,3 +1,30 @@ +## v0.9.1-rc (2026-05-20) + +### Motivation +Finalize release-candidate validation for deployment, upgrade discipline, security posture, and role-based UX quality before v1.0. + +### Implemented Changes +- Added deployment health check script (`backend/scripts/check_deployment_health.py`) for API/system/database/build smoke validation. +- Expanded operations docs with explicit release QA, upgrade QA, Docker QA, and security checklist guidance. +- Updated release checklist with role/feature UX validation scenarios for admin/operator/viewer. +- Version bump across backend/frontend/docs to `0.9.1` / `v0.9.1-rc`. + +### Deployment QA Notes +- Use `python backend/scripts/check_deployment_health.py --base-url http://localhost:8000 --check-setup`. +- Ensure `/api/system/status` reports `read_only=true` and `migration_status` not `behind`. + +### Security QA Notes +- Verify `COOKIE_SECURE=true` for HTTPS. +- Use explicit `CORS_ORIGINS`, never `*` in production. +- Ensure Alembic current/head parity before go-live. + +### Testing +- `cd backend && pytest -q` +- `cd frontend && npm run build` + +### Known Limitations +- Deployment smoke script checks endpoint health/state only; it does not perform synthetic business transactions. + ## v0.9.0-rc (2026-05-20) diff --git a/ROADMAP.md b/ROADMAP.md index ada4a67..0f232be 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -1,22 +1,22 @@ # RouteForge Roadmap ## Current Status -v0.9.0-rc, BGP Visibility Details completed, read-only +v0.9.1-rc, BGP Visibility Details completed, read-only -## v0.9.0-rc +## v0.9.1-rc - projects/change cases - grouped preflight reports -## v0.9.0-rc +## v0.9.1-rc - bgp visibility details -## v0.9.0-rc +## v0.9.1-rc - roa planner / roa preflight -## v0.9.0-rc +## v0.9.1-rc - watch mode / scheduled rechecks -## v0.9.0-rc +## v0.9.1-rc - security review - UX review - API stability diff --git a/backend/app/api/routes_health.py b/backend/app/api/routes_health.py index a6ce757..1d16f84 100644 --- a/backend/app/api/routes_health.py +++ b/backend/app/api/routes_health.py @@ -8,4 +8,4 @@ @router.get('/health') def health() -> dict: - return {"status": "ok", "version": "v0.9.0-rc", "database": get_database_status(engine).get("status", "unknown")} + return {"status": "ok", "version": "v0.9.1-rc", "database": get_database_status(engine).get("status", "unknown")} diff --git a/backend/app/api/routes_system.py b/backend/app/api/routes_system.py index c169116..05d7d98 100644 --- a/backend/app/api/routes_system.py +++ b/backend/app/api/routes_system.py @@ -12,7 +12,7 @@ def system_info(): return { 'name': 'RouteForge', - 'version': 'v0.9.0-rc', + 'version': 'v0.9.1-rc', 'demo_mode': settings.demo_mode, 'read_only': True, 'data_sources': ['RIPEstat', 'RIPEstat Whois/Registry'], diff --git a/backend/app/core/system_status.py b/backend/app/core/system_status.py index 37e1b39..574a784 100644 --- a/backend/app/core/system_status.py +++ b/backend/app/core/system_status.py @@ -153,7 +153,7 @@ def build_system_status(engine: Engine | None) -> dict: return { "status": "ok", "name": settings.app_name, - "version": "v0.9.0-rc", + "version": "v0.9.1-rc", "read_only": True, "mode": "demo" if settings.demo_mode else "live", "demo_mode": settings.demo_mode, diff --git a/backend/app/main.py b/backend/app/main.py index fa77fc9..f778e90 100644 --- a/backend/app/main.py +++ b/backend/app/main.py @@ -20,7 +20,7 @@ logging.basicConfig(level=getattr(logging, settings.log_level.upper(), logging.INFO)) logger = logging.getLogger("routeforge") -app = FastAPI(title="RouteForge", version="0.9.0") +app = FastAPI(title="RouteForge", version="0.9.1") app.add_middleware( CORSMiddleware, diff --git a/backend/pyproject.toml b/backend/pyproject.toml index 6e6da93..eabae7a 100644 --- a/backend/pyproject.toml +++ b/backend/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "routeforge-backend" -version = "0.9.0" +version = "0.9.1" description = "RouteForge backend" license = "AGPL-3.0-or-later" requires-python = ">=3.12" diff --git a/backend/scripts/check_deployment_health.py b/backend/scripts/check_deployment_health.py new file mode 100755 index 0000000..2dd1400 --- /dev/null +++ b/backend/scripts/check_deployment_health.py @@ -0,0 +1,87 @@ +#!/usr/bin/env python3 +"""RouteForge deployment smoke/health check for release validation.""" + +from __future__ import annotations + +import argparse +import json +import sys +from pathlib import Path +from urllib.error import HTTPError, URLError +from urllib.request import urlopen + + +def fetch_json(url: str, timeout: float) -> dict: + try: + with urlopen(url, timeout=timeout) as resp: + return json.loads(resp.read().decode("utf-8")) + except HTTPError as exc: + raise RuntimeError(f"HTTP {exc.code} for {url}") from exc + except URLError as exc: + raise RuntimeError(f"Connection error for {url}: {exc.reason}") from exc + + +def check(condition: bool, message: str, failures: list[str]) -> None: + prefix = "[OK]" if condition else "[FAIL]" + print(f"{prefix} {message}") + if not condition: + failures.append(message) + + +def main() -> int: + parser = argparse.ArgumentParser() + parser.add_argument("--base-url", default="http://localhost:8000", help="Backend base URL") + parser.add_argument("--frontend-dist", default="frontend/dist", help="Frontend build artifact directory") + parser.add_argument("--timeout", type=float, default=5.0) + parser.add_argument("--check-setup", action="store_true", help="Also validate /api/setup/required endpoint") + args = parser.parse_args() + + base = args.base_url.rstrip("/") + failures: list[str] = [] + + try: + health = fetch_json(f"{base}/health", timeout=args.timeout) + except RuntimeError as exc: + check(False, str(exc), failures) + print(f"\nDeployment check failed with {len(failures)} issue(s).") + return 1 + check(health.get("status") == "ok", "Backend /health is reachable and status=ok", failures) + + try: + status = fetch_json(f"{base}/api/system/status", timeout=args.timeout) + except RuntimeError as exc: + check(False, str(exc), failures) + print(f"\nDeployment check failed with {len(failures)} issue(s).") + return 1 + check(status.get("status") == "ok", "System status endpoint reachable", failures) + check(bool(status.get("version")), "System status contains version", failures) + check(status.get("read_only") is True, "System status read_only=true", failures) + + database = status.get("database") if isinstance(status, dict) else None + check(isinstance(database, dict), "System status contains database payload", failures) + if isinstance(database, dict): + check(bool(database.get("status")), "Database status is present", failures) + check(bool(database.get("schema_version")), "Database schema_version is present", failures) + check(bool(database.get("migration_head")), "Database migration_head is present", failures) + check(database.get("migration_status") != "behind", "Migration status is not behind", failures) + + dist = Path(args.frontend_dist) + check(dist.exists(), f"Frontend artifacts directory exists ({dist})", failures) + check((dist / "index.html").exists(), f"Frontend build artifact exists ({dist / 'index.html'})", failures) + + if args.check_setup: + try: + setup = fetch_json(f"{base}/api/setup/required", timeout=args.timeout) + check("required" in setup, "Setup endpoint reachable and contains required field", failures) + except RuntimeError as exc: + check(False, str(exc), failures) + + if failures: + print(f"\nDeployment check failed with {len(failures)} issue(s).") + return 1 + print("\nDeployment check completed successfully.") + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/backend/tests/test_api_smoke.py b/backend/tests/test_api_smoke.py index a1a4b6e..8a7f78b 100644 --- a/backend/tests/test_api_smoke.py +++ b/backend/tests/test_api_smoke.py @@ -177,7 +177,7 @@ def test_system_status_endpoint() -> None: response = client.get('/api/system/status') assert response.status_code == 200 payload = response.json() - assert payload.get('version') == 'v0.9.0-rc' + assert payload.get('version') == 'v0.9.1-rc' assert payload.get('read_only') is True assert payload.get('database', {}).get('status') assert payload.get('ripestat', {}).get('cache_ttl_seconds') is not None diff --git a/docs/operations/database-migrations.md b/docs/operations/database-migrations.md index b6e7a53..56610b3 100644 --- a/docs/operations/database-migrations.md +++ b/docs/operations/database-migrations.md @@ -2,15 +2,6 @@ RouteForge uses Alembic for schema lifecycle tracking from `v0.5.3-beta`. -## Why -- Traceable schema history -- Safe, repeatable upgrades -- Visible migration state in system status - -## Engines -- Production: PostgreSQL (recommended) -- Dev/demo: SQLite supported - ## Commands ```bash docker compose exec backend alembic current @@ -18,44 +9,30 @@ docker compose exec backend alembic heads docker compose exec backend alembic upgrade head ``` -## Existing pre-baseline databases -If the schema already exists from historical `create_all`, mark baseline first: -```bash -docker compose exec backend alembic stamp 0001_initial_schema -docker compose exec backend alembic upgrade head -``` - -## Auto migration -`ROUTEFORGE_AUTO_MIGRATE` is not enabled by default in this beta. -Run migrations manually before production upgrades. - -## Troubleshooting: missing `created_by_user_id` after v0.6 upgrade - -Fehlerbild: - -`sqlite3.OperationalError: table checks has no column named created_by_user_id` - -Ursache: - -Die Migration `0002_users_and_report_ownership` wurde auf einer bestehenden Datenbank nicht ausgeführt. - -Fix: +## Docker QA runbook (v0.9.1-rc) ```bash +docker compose up -d +docker compose logs backend docker compose exec backend alembic current docker compose exec backend alembic heads docker compose exec backend alembic upgrade head +docker compose restart backend ``` -Wenn bestehende Pre-Alembic DB: +Validation goals: +- backend comes up without migration crash +- `alembic current` equals `alembic heads` +- after restart, `/api/system/status` remains `migration_status=up_to_date` -```bash -docker compose exec backend alembic stamp 0001_initial_schema -docker compose exec backend alembic upgrade head -``` +## Reverse proxy notes -Vorher Backup: +Use a reverse proxy for production TLS termination and same-origin `/api` forwarding. +See `docs/operations/reverse-proxy.md` for deployment examples. +## Existing pre-baseline databases +If schema already exists from historical `create_all`, mark baseline first only after schema verification: ```bash -docker compose exec backend sh -c 'cp /app/data/routeforge.db /app/data/routeforge.db.bak.$(date +%s)' +docker compose exec backend alembic stamp 0001_initial_schema +docker compose exec backend alembic upgrade head ``` diff --git a/docs/operations/release-checklist.md b/docs/operations/release-checklist.md index 43c83bc..4f721dd 100644 --- a/docs/operations/release-checklist.md +++ b/docs/operations/release-checklist.md @@ -1,26 +1,58 @@ # Release Checklist -## Before release +## v0.9.1-rc: Deployment QA & UX Validation + +### 1) Pre-release validation - `git pull` - Run backend tests (`cd backend && pytest -q`) - Run frontend production build (`cd frontend && npm run build`) - Validate Compose (`docker compose config` and production config) -- Check system status endpoint (`/api/system/status`) -- Check migration status visibility (`database.schema_version`, `migration_head`, `migration_status`) -## Tagging +### 2) Deployment smoke check + +Run after backend/frontend are up and frontend artifacts were built: + +```bash +python backend/scripts/check_deployment_health.py --base-url http://localhost:8000 --check-setup +``` + +The script validates: +- Backend reachable (`/health`) +- `/api/system/status` reachable +- `version` present +- `read_only=true` +- `database.status`, `schema_version`, `migration_head` present +- `migration_status` is not `behind` +- Frontend build artifacts exist (`frontend/dist/index.html`) +- Optional `/api/setup/required` reachability + +### 3) UX QA checklist + +- [ ] Setup Flow works and initial admin creation succeeds +- [ ] Login/Logout works end-to-end +- [ ] User Management roles behave correctly (admin/operator/viewer) +- [ ] Audit Log is visible only for admin users +- [ ] Change Case create/edit/delete works for authorized roles +- [ ] BGP Visibility check works and stores report +- [ ] ROA Planner check works and stores report +- [ ] Watch Target create/edit/run-due works for authorized roles +- [ ] Report export works for Markdown/HTML/Summary +- [ ] Viewer can read data but cannot execute checks/watch/change operations +- [ ] Operator can execute checks/watch/change cases, but cannot access user/audit admin-only features + +### 4) Tagging -- `git tag -a vX.Y.Z[-beta] -m "RouteForge vX.Y.Z[-beta]"` -- `git push origin vX.Y.Z[-beta]` +- `git tag -a v0.9.1-rc -m "RouteForge v0.9.1-rc"` +- `git push origin v0.9.1-rc` -## GitHub Release +### 5) GitHub Release -- Use release title matching the version and sprint scope -- Enable **prerelease** checkbox for beta versions -- Include structured release notes with highlights and known limitations +- Release title aligned with `v0.9.1-rc` +- Mark as prerelease +- Include deployment, upgrade, and security QA notes -## Post-release smoke test +### 6) Post-release smoke test - `GET /health` - `GET /api/system/info` diff --git a/docs/operations/security.md b/docs/operations/security.md index 5a454ca..790b16f 100644 --- a/docs/operations/security.md +++ b/docs/operations/security.md @@ -1,5 +1,18 @@ # Security Baseline +## Security QA checklist (v0.9.1-rc) + +- [ ] `SECRET_KEY` changed from any default/dev value +- [ ] `COOKIE_SECURE=true` when running behind HTTPS +- [ ] `COOKIE_SAMESITE` is plausible (`lax` recommended, `none` only with `COOKIE_SECURE=true`) +- [ ] `CORS_ORIGINS` is explicit and not `*` +- [ ] Admin password is strong and unique +- [ ] Demo mode is disabled for production usage +- [ ] Backup exists and restore procedure is tested +- [ ] Alembic is current (`alembic current == alembic heads`) +- [ ] Watch Mode external cron/triggering is access-restricted and authenticated +- [ ] No public write-endpoints exist to RIPE/RPKI/router systems + ## Read-only safety model RouteForge is read-only by design. It validates routing state and preflight conditions but does not push configuration changes to external systems. @@ -14,13 +27,12 @@ Use RouteForge behind a reverse proxy with TLS termination in production (for ex - Never commit `.env` files to git. - Change `POSTGRES_PASSWORD` from the example default. -## Default password warning - -`/api/system/status` exposes `security_warnings` when a risky default is detected (currently: `POSTGRES_PASSWORD=change-me`). No secret values are returned. +## HTTPS cookie and CORS guidance -## CORS guidance - -In the standard Docker setup, frontend and API are same-origin via nginx (`/api` proxy). CORS is mainly relevant for split deployments where frontend and backend are served from different origins. +- `COOKIE_SECURE=true` for HTTPS deployments. +- `COOKIE_SAMESITE=lax` is the recommended default. +- `COOKIE_SAMESITE=none` requires `COOKIE_SECURE=true`. +- In split-origin deployments set explicit `CORS_ORIGINS` to frontend domains. ## Security headers @@ -32,39 +44,8 @@ Frontend nginx sets baseline headers: - `Permissions-Policy: geolocation=(), microphone=(), camera=()` - A conservative initial Content-Security-Policy for SPA usage -## Container hardening - -- Backend container starts through an entrypoint as root only long enough to normalize `/app/data` ownership, then drops privileges to non-root user `routeforge` for runtime. - -- Backend container runs as non-root user `routeforge`. -- Backend image keeps only runtime-relevant files. -- Frontend image is multi-stage (build + runtime). -- Nginx container still runs with default upstream behavior; strict non-root nginx runtime can be added later as a dedicated hardening step. - -## SQLite volume permissions (Docker Compose) - -In the standard `docker-compose.yml` setup with SQLite, the database file is stored at `/app/data/routeforge.db` via the named volume mount `routeforge_data:/app/data`. - -The backend entrypoint ensures `/app/data` is writable for the non-root runtime user `routeforge` on container start. This prevents SQLite write failures such as `sqlite3.OperationalError: attempt to write a readonly database` when a mounted volume is root-owned. - ## What RouteForge does not do - no ROA creation - no RIPE DB writes - no router deployment - -## Current limitations - -- Role model: admin/operator/viewer -- Admin-only user management -- Inactive users cannot log in -- Password reset is admin-driven (set new password in user management) -- No external auth/SSO in this version - - -## v0.9.0-rc baseline - -- Session cookies use `HttpOnly`, configurable `COOKIE_SAMESITE` (default `lax`) and `COOKIE_SECURE` policy. -- Password hashing uses `pbkdf2_sha256` with legacy `sha256` verify compatibility for existing users. -- Viewer role remains strictly read-only for checks and watch execution/mutation endpoints. -- RouteForge remains read-only to external systems (no write operations to RIPE DB/RPKI/router APIs). diff --git a/docs/operations/upgrades.md b/docs/operations/upgrades.md index a01d48d..aea7efc 100644 --- a/docs/operations/upgrades.md +++ b/docs/operations/upgrades.md @@ -9,6 +9,62 @@ git pull docker compose -f docker-compose.prod.yml build ``` +## Upgrade QA matrix (v0.9.1-rc) + +### A) Fresh SQLite DB + +```bash +rm -f backend/data/routeforge.db +cd backend && alembic upgrade head +cd .. && docker compose up -d +``` + +Expected: +- backend starts cleanly +- `/api/system/status` shows `migration_status=up_to_date` + +### B) Existing SQLite DB from older revision + +```bash +cd backend && alembic current +cd backend && alembic heads +cd backend && alembic upgrade head +cd .. && docker compose restart backend +``` + +Expected: +- `alembic current` matches `alembic heads` +- application starts without migration warnings + +### C) Behavior when DB is behind + +Expected behavior: +- `/api/system/status` returns database migration status `behind` +- operational warnings include upgrade command hints +- release/deployment checks must fail until DB is upgraded + +### D) `ALLOW_SQLITE_CREATE_ALL=false` + +Expected behavior: +- SQLite startup must not silently create schema via `create_all` +- migration discipline remains Alembic-first + +### E) Safe `alembic stamp` usage + +Use `alembic stamp ` only when: +- schema was inspected and confirmed equivalent to stamped revision +- DB is a pre-Alembic legacy DB initialized outside Alembic + +Recommended sequence: + +```bash +alembic current +alembic heads +# verify tables/columns align with target baseline revision first +alembic stamp 0001_initial_schema +alembic upgrade head +``` + ## Database migration workflow (production) ```bash @@ -27,27 +83,3 @@ curl http://localhost:3000/api/system/status - `migration_head` - Open frontend and run a sample check. - Verify existing reports can still be viewed. - -## Notes on existing alpha databases -- If tables were created via `create_all` before Alembic baseline, apply baseline carefully: - -```bash -docker compose -f docker-compose.prod.yml run --rm backend alembic stamp 0001_initial_schema -``` - -- Then run normal upgrades (`alembic upgrade head`). - -## SQLite / dev mode note -- In SQLite dev mode, `Base.metadata.create_all()` can create tables before Alembic revision stamping. -- If `/api/system/status` shows tables but unknown/behind migration state, first verify schema, then use `alembic stamp ` only when schema and migration baseline match. -- Recommended diagnostics sequence: - - `alembic current` - - `alembic heads` - - `alembic upgrade head` - - -## v0.9.0-rc upgrade validation - -- Run `python backend/scripts/check_upgrade_path.py` for a CI-friendly SQLite upgrade sanity check (empty DB -> head, current/head checks, simulated behind revision). -- Use `alembic upgrade head` for real schema upgrades. -- Use `alembic stamp ` only to baseline pre-existing schema that already matches the target revision. diff --git a/frontend/package.json b/frontend/package.json index 2382f94..252d44e 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -1,6 +1,6 @@ { "name": "routeforge-frontend", - "version": "0.9.0", + "version": "0.9.1", "private": true, "license": "AGPL-3.0-or-later", "type": "module", diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 4d02e9f..745ab33 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -74,7 +74,7 @@ export default function App() { if (authMode === 'login') return if (authMode === 'error') return

{authError}
- const systemLine = system ? `${system.name} ${system.version} · mode=${system.demo_mode ? 'DEMO' : 'LIVE'} · read_only=${String(system.read_only)}` : 'RouteForge v0.8.1-beta · read-only preflight checks' + const systemLine = system ? `${system.name} ${system.version} · mode=${system.demo_mode ? 'DEMO' : 'LIVE'} · read_only=${String(system.read_only)}` : 'RouteForge v0.9.1-rc · read-only preflight checks' const title = { dashboard: 'Dashboard', asn: 'ASN Check', prefix: 'Prefix Check', preflight: 'Preflight Check', 'roa-planner': 'ROA Planner', 'bgp-visibility': 'BGP Visibility', reports: 'Reports', 'watch-mode': 'Watch Mode', 'change-cases': 'Change Cases', system: 'System Status', users: 'User Management', audit: 'Audit Log', about: 'About RouteForge' }[active] const proxyStatus = systemStatusError ? 'ERROR' : 'OK' const migrationStatus = systemStatus?.database?.migration_status || 'unknown' @@ -89,7 +89,7 @@ export default function App() { const allowedActions = role === 'admin' ? 'You can run checks, manage users, view reports and system status.' : role === 'operator' ? 'You can run checks and view reports.' : 'You can view reports and change cases.' return - {active === 'dashboard' &&

RouteForge v0.8.1-beta

Modernes read-only Operator-Tool für Preflight Checks von ASN, Prefix, RPKI und Registry/IRR.

Logged in as: {currentUser?.username}
Role: {currentUser?.role}
Allowed actions: {allowedActions}
{migrationsBlocked &&
Database migrations are required before using RouteForge. Run: alembic current, alembic heads, alembic upgrade head.
}
} + {active === 'dashboard' &&

RouteForge v0.9.1-rc

Modernes read-only Operator-Tool für Preflight Checks von ASN, Prefix, RPKI und Registry/IRR.

Logged in as: {currentUser?.username}
Role: {currentUser?.role}
Allowed actions: {allowedActions}
{migrationsBlocked &&
Database migrations are required before using RouteForge. Run: alembic current, alembic heads, alembic upgrade head.
}
} {!canAccess(active) &&
You do not have permission to access this section.
} {active === 'asn' && canAccess('asn') && } {active === 'prefix' && canAccess('prefix') && }