From 1b2e0cb8af1c870ccbb2c92abcc1532204d6a47a Mon Sep 17 00:00:00 2001 From: Beda Schmid Date: Sun, 1 Mar 2026 17:08:50 -0300 Subject: [PATCH] Allow private-network CORS origins in development --- .env.example | 2 ++ README.md | 2 +- backend/.env.example | 1 + backend/app/core/config.py | 1 + backend/app/main.py | 23 +++++++++++++++++++++++ doc/operations-and-configuration.md | 2 ++ docker-compose.yml | 1 + 7 files changed, 31 insertions(+), 1 deletion(-) diff --git a/.env.example b/.env.example index 7a288f1..091002c 100644 --- a/.env.example +++ b/.env.example @@ -36,6 +36,7 @@ PROVIDER_BASE_URL_ALLOWLIST=[] PUBLIC_BASE_URL=http://localhost:8000 CORS_ORIGINS=["http://localhost:5173","http://localhost:3000"] CORS_ALLOW_CREDENTIALS=false +CORS_ALLOW_DEVELOPMENT_PRIVATE_NETWORK_ORIGINS=true VITE_API_BASE= # Production baseline overrides (set explicitly for live deployments): @@ -50,4 +51,5 @@ VITE_API_BASE= # PUBLIC_BASE_URL=https://api.example.com # CORS_ORIGINS=["https://app.example.com"] # CORS_ALLOW_CREDENTIALS=false +# CORS_ALLOW_DEVELOPMENT_PRIVATE_NETWORK_ORIGINS=false # VITE_API_BASE=https://api.example.com/api/v1 diff --git a/README.md b/README.md index b93861e..47568a5 100644 --- a/README.md +++ b/README.md @@ -121,7 +121,7 @@ cd frontend && npm run preview Main runtime variables are defined in `docker-compose.yml`: -- API and worker: `DATABASE_URL`, `REDIS_URL`, `REDIS_SECURITY_MODE`, `REDIS_TLS_MODE`, `STORAGE_ROOT`, `PUBLIC_BASE_URL`, `CORS_ORIGINS`, `CORS_ALLOW_CREDENTIALS`, `AUTH_BOOTSTRAP_*`, `PROCESSING_LOG_STORE_*`, `CONTENT_EXPORT_*`, `TYPESENSE_*`, `APP_SETTINGS_ENCRYPTION_KEY` +- API and worker: `DATABASE_URL`, `REDIS_URL`, `REDIS_SECURITY_MODE`, `REDIS_TLS_MODE`, `STORAGE_ROOT`, `PUBLIC_BASE_URL`, `CORS_ORIGINS`, `CORS_ALLOW_CREDENTIALS`, `CORS_ALLOW_DEVELOPMENT_PRIVATE_NETWORK_ORIGINS`, `AUTH_BOOTSTRAP_*`, `PROCESSING_LOG_STORE_*`, `CONTENT_EXPORT_*`, `TYPESENSE_*`, `APP_SETTINGS_ENCRYPTION_KEY` - Frontend: optional `VITE_API_BASE` When `VITE_API_BASE` is unset, the frontend uses `http://:8000/api/v1`. diff --git a/backend/.env.example b/backend/.env.example index 1ede9c1..7180f38 100644 --- a/backend/.env.example +++ b/backend/.env.example @@ -38,3 +38,4 @@ TYPESENSE_API_KEY=replace-with-random-typesense-api-key TYPESENSE_COLLECTION_NAME=documents PUBLIC_BASE_URL=http://localhost:8000 CORS_ALLOW_CREDENTIALS=false +CORS_ALLOW_DEVELOPMENT_PRIVATE_NETWORK_ORIGINS=true diff --git a/backend/app/core/config.py b/backend/app/core/config.py index f4d9ed0..1b4d7e1 100644 --- a/backend/app/core/config.py +++ b/backend/app/core/config.py @@ -73,6 +73,7 @@ class Settings(BaseSettings): public_base_url: str = "http://localhost:8000" cors_origins: list[str] = Field(default_factory=lambda: ["http://localhost:5173", "http://localhost:3000"]) cors_allow_credentials: bool = False + cors_allow_development_private_network_origins: bool = True LOCAL_HOSTNAME_SUFFIXES = (".local", ".internal", ".home.arpa") diff --git a/backend/app/main.py b/backend/app/main.py index 4e6ab2b..239237d 100644 --- a/backend/app/main.py +++ b/backend/app/main.py @@ -19,6 +19,15 @@ from app.services.typesense_index import ensure_typesense_collection settings = get_settings() UPLOAD_ENDPOINT_PATH = "/api/v1/documents/upload" UPLOAD_ENDPOINT_METHOD = "POST" +CORS_DEVELOPMENT_PRIVATE_ORIGIN_REGEX = ( + r"^https?://(" + r"localhost" + r"|127\.0\.0\.1" + r"|10\.\d{1,3}\.\d{1,3}\.\d{1,3}" + r"|192\.168\.\d{1,3}\.\d{1,3}" + r"|172\.(1[6-9]|2\d|3[0-1])\.\d{1,3}\.\d{1,3}" + r")(?::\d{1,5})?$" +) def _is_upload_size_guard_target(request: Request) -> bool: @@ -31,14 +40,28 @@ def _is_upload_size_guard_target(request: Request) -> bool: return request.method.upper() == UPLOAD_ENDPOINT_METHOD and request.url.path == UPLOAD_ENDPOINT_PATH +def _resolve_cors_origin_regex() -> str | None: + """Returns development-only private-network origin regex when explicitly enabled.""" + + app_env = settings.app_env.strip().lower() + if app_env not in {"development", "dev"}: + return None + allow_private_dev_origins = bool(getattr(settings, "cors_allow_development_private_network_origins", False)) + if not allow_private_dev_origins: + return None + return CORS_DEVELOPMENT_PRIVATE_ORIGIN_REGEX + + def create_app() -> FastAPI: """Builds and configures the FastAPI application instance.""" app = FastAPI(title="DCM DMS API", version="0.1.0") allowed_origins = [origin.strip() for origin in settings.cors_origins if isinstance(origin, str) and origin.strip()] + allowed_origin_regex = _resolve_cors_origin_regex() app.add_middleware( CORSMiddleware, allow_origins=allowed_origins, + allow_origin_regex=allowed_origin_regex, allow_credentials=bool(getattr(settings, "cors_allow_credentials", False)), allow_methods=["*"], allow_headers=["*"], diff --git a/doc/operations-and-configuration.md b/doc/operations-and-configuration.md index 5232f85..81ccb85 100644 --- a/doc/operations-and-configuration.md +++ b/doc/operations-and-configuration.md @@ -65,6 +65,7 @@ Use `.env.example` as baseline. The table below documents user-managed settings | `VITE_API_BASE` | empty for host-derived `http://:8000/api/v1`, or explicit local URL | `https://api.example.com/api/v1` | | `CORS_ORIGINS` | `["http://localhost:5173","http://localhost:3000"]` | exact frontend origins only, for example `["https://app.example.com"]` | | `CORS_ALLOW_CREDENTIALS` | `false` | `false` (Authorization header flow does not need credentialed CORS) | +| `CORS_ALLOW_DEVELOPMENT_PRIVATE_NETWORK_ORIGINS` | `true` to allow LAN origins such as `http://192.168.x.x:5173` during dev | `false` | | `REDIS_URL` | `redis://:@redis:6379/0` in isolated local network | `rediss://:@redis.internal:6379/0` | | `REDIS_SECURITY_MODE` | `compat` or `auto` | `strict` | | `REDIS_TLS_MODE` | `allow_insecure` or `auto` | `required` | @@ -93,6 +94,7 @@ Recommended LIVE pattern: ## Security Controls - CORS uses explicit origin allowlist only; broad origin regex matching is removed. +- Development mode can additionally allow private-network HTTP(S) origins when `CORS_ALLOW_DEVELOPMENT_PRIVATE_NETWORK_ORIGINS=true`. - Worker Redis startup validates URL auth and TLS policy before consuming jobs. - Provider API keys are encrypted at rest with standard AEAD (`cryptography` Fernet). - legacy `enc-v1` payloads are read for backward compatibility diff --git a/docker-compose.yml b/docker-compose.yml index f3f4b9c..a3b1006 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -52,6 +52,7 @@ services: PROVIDER_BASE_URL_ALLOW_HTTP: ${PROVIDER_BASE_URL_ALLOW_HTTP:-true} PROVIDER_BASE_URL_ALLOW_PRIVATE_NETWORK: ${PROVIDER_BASE_URL_ALLOW_PRIVATE_NETWORK:-true} CORS_ALLOW_CREDENTIALS: ${CORS_ALLOW_CREDENTIALS:-false} + CORS_ALLOW_DEVELOPMENT_PRIVATE_NETWORK_ORIGINS: ${CORS_ALLOW_DEVELOPMENT_PRIVATE_NETWORK_ORIGINS:-true} PROCESSING_LOG_STORE_MODEL_IO_TEXT: ${PROCESSING_LOG_STORE_MODEL_IO_TEXT:-false} PROCESSING_LOG_STORE_PAYLOAD_TEXT: ${PROCESSING_LOG_STORE_PAYLOAD_TEXT:-false} CONTENT_EXPORT_MAX_DOCUMENTS: ${CONTENT_EXPORT_MAX_DOCUMENTS:-250}