Add deployment-safe backend config and production-safe CORS defaults
This commit is contained in:
@@ -8,6 +8,12 @@ BANGUI_DATABASE_PATH=bangui.db
|
||||
# Path to the fail2ban Unix domain socket.
|
||||
BANGUI_FAIL2BAN_SOCKET=/var/run/fail2ban/fail2ban.sock
|
||||
|
||||
# Path to the fail2ban configuration directory used by the web UI.
|
||||
BANGUI_FAIL2BAN_CONFIG_DIR=/config/fail2ban
|
||||
|
||||
# Shell command used to start fail2ban during recovery operations.
|
||||
BANGUI_FAIL2BAN_START_COMMAND=fail2ban-client start
|
||||
|
||||
# Secret key used to sign session tokens. Use a long, random string.
|
||||
# Generate with: python -c "import secrets; print(secrets.token_hex(64))"
|
||||
BANGUI_SESSION_SECRET=replace-this-with-a-long-random-secret
|
||||
@@ -23,4 +29,5 @@ BANGUI_LOG_LEVEL=info
|
||||
|
||||
# Comma-separated list of allowed CORS origins when the frontend is served
|
||||
# from a different origin than the backend.
|
||||
# Leave this blank in production when the UI is served from the same origin.
|
||||
BANGUI_CORS_ALLOWED_ORIGINS=http://localhost:5173
|
||||
|
||||
@@ -4,9 +4,15 @@ Follows pydantic-settings patterns: all values are prefixed with BANGUI_
|
||||
and validated at startup via the Settings singleton.
|
||||
"""
|
||||
|
||||
from pydantic import Field
|
||||
from pydantic import Field, field_validator
|
||||
from pydantic_settings import BaseSettings, SettingsConfigDict
|
||||
|
||||
from app.utils.constants import (
|
||||
DEFAULT_DATABASE_PATH,
|
||||
DEFAULT_FAIL2BAN_SOCKET,
|
||||
DEFAULT_SESSION_DURATION_MINUTES,
|
||||
)
|
||||
|
||||
|
||||
class Settings(BaseSettings):
|
||||
"""BanGUI runtime configuration.
|
||||
@@ -18,11 +24,11 @@ class Settings(BaseSettings):
|
||||
"""
|
||||
|
||||
database_path: str = Field(
|
||||
default="bangui.db",
|
||||
default=DEFAULT_DATABASE_PATH,
|
||||
description="Filesystem path to the BanGUI SQLite application database.",
|
||||
)
|
||||
fail2ban_socket: str = Field(
|
||||
default="/var/run/fail2ban/fail2ban.sock",
|
||||
default=DEFAULT_FAIL2BAN_SOCKET,
|
||||
description="Path to the fail2ban Unix domain socket.",
|
||||
)
|
||||
session_secret: str = Field(
|
||||
@@ -33,7 +39,7 @@ class Settings(BaseSettings):
|
||||
),
|
||||
)
|
||||
session_duration_minutes: int = Field(
|
||||
default=60,
|
||||
default=DEFAULT_SESSION_DURATION_MINUTES,
|
||||
ge=1,
|
||||
description="Number of minutes a session token remains valid after creation.",
|
||||
)
|
||||
@@ -41,14 +47,24 @@ class Settings(BaseSettings):
|
||||
default="UTC",
|
||||
description="IANA timezone name used when displaying timestamps in the UI.",
|
||||
)
|
||||
cors_allowed_origins: list[str] = Field(
|
||||
default_factory=lambda: ["http://localhost:5173"],
|
||||
cors_allowed_origins: str | list[str] = Field(
|
||||
default_factory=list,
|
||||
description=(
|
||||
"Comma-separated list of allowed CORS origins when the frontend is "
|
||||
"served from a different origin than the backend. "
|
||||
"Leave empty to disable cross-origin requests in production."
|
||||
),
|
||||
)
|
||||
|
||||
@field_validator("cors_allowed_origins", mode="before")
|
||||
@classmethod
|
||||
def _normalize_cors_origins(cls, value: str | list[str] | None) -> list[str]:
|
||||
if value is None:
|
||||
return []
|
||||
if isinstance(value, str):
|
||||
return [origin.strip() for origin in value.split(",") if origin.strip()]
|
||||
return value
|
||||
|
||||
log_level: str = Field(
|
||||
default="info",
|
||||
description="Application log level: debug | info | warning | error | critical.",
|
||||
@@ -72,8 +88,8 @@ class Settings(BaseSettings):
|
||||
default="fail2ban-client start",
|
||||
description=(
|
||||
"Shell command used to start (not reload) the fail2ban daemon during "
|
||||
"recovery rollback. Split by whitespace to build the argument list — "
|
||||
"no shell interpretation is performed. "
|
||||
"recovery rollback. Split by whitespace to build the argument list — "
|
||||
"no shell interpretation is performed. "
|
||||
"Example: 'systemctl start fail2ban' or 'fail2ban-client start'."
|
||||
),
|
||||
)
|
||||
|
||||
@@ -48,3 +48,23 @@ def test_create_app_skips_cors_when_no_origins_are_configured() -> None:
|
||||
]
|
||||
|
||||
assert cors_middleware == []
|
||||
|
||||
|
||||
def test_create_app_disables_cors_by_default() -> None:
|
||||
"""The FastAPI app does not add CORS middleware when no origins are configured by environment."""
|
||||
settings = Settings(
|
||||
database_path="/tmp/test.db",
|
||||
fail2ban_socket="/tmp/fake_fail2ban.sock",
|
||||
fail2ban_config_dir="/tmp/fail2ban",
|
||||
session_secret="test-secret-key-do-not-use-in-production",
|
||||
session_duration_minutes=60,
|
||||
timezone="UTC",
|
||||
log_level="debug",
|
||||
)
|
||||
|
||||
app = create_app(settings=settings)
|
||||
cors_middleware = [
|
||||
middleware for middleware in app.user_middleware if middleware.cls is CORSMiddleware
|
||||
]
|
||||
|
||||
assert cors_middleware == []
|
||||
|
||||
Reference in New Issue
Block a user