feat(security): add JWT startup guard, configurable CORS, and externalize DB credentials
- Add check_jwt_secret() that refuses default JWT secret when APP_ENV != development - Make CORS origins configurable via CORS_ORIGINS env var (comma-separated) - Replace hardcoded postgres credentials in docker-compose.yml with env var references - Add APP_ENV and cors_origins to config.py - Update .env.example with all required variables and documentation - Add tests for JWT startup guard and CORS configuration Closes leeworks-agents/SPARC#4 Closes leeworks-agents/SPARC#5 Closes leeworks-agents/SPARC#6 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,116 @@
|
||||
"""Tests for security hardening: JWT secret startup check, CORS config, credential handling."""
|
||||
|
||||
import os
|
||||
from unittest.mock import patch
|
||||
|
||||
import pytest
|
||||
|
||||
|
||||
class TestJWTSecretStartupCheck:
|
||||
"""Test the startup guard that refuses default JWT secret in non-dev environments."""
|
||||
|
||||
def test_default_secret_in_production_raises(self):
|
||||
"""Starting with default secret and APP_ENV=production must raise RuntimeError."""
|
||||
with patch.dict(os.environ, {"APP_ENV": "production"}):
|
||||
# Reload config to pick up the new APP_ENV
|
||||
import importlib
|
||||
import SPARC.config
|
||||
importlib.reload(SPARC.config)
|
||||
|
||||
from SPARC.auth import _DEFAULT_JWT_SECRET, check_jwt_secret
|
||||
# Patch JWT_SECRET to the default
|
||||
with patch("SPARC.auth.JWT_SECRET", _DEFAULT_JWT_SECRET):
|
||||
with pytest.raises(RuntimeError, match="FATAL.*JWT_SECRET"):
|
||||
check_jwt_secret()
|
||||
|
||||
# Restore config
|
||||
with patch.dict(os.environ, {"APP_ENV": "development"}):
|
||||
importlib.reload(SPARC.config)
|
||||
|
||||
def test_default_secret_in_development_succeeds(self):
|
||||
"""Starting with default secret and APP_ENV=development must not raise."""
|
||||
with patch.dict(os.environ, {"APP_ENV": "development"}):
|
||||
import importlib
|
||||
import SPARC.config
|
||||
importlib.reload(SPARC.config)
|
||||
|
||||
from SPARC.auth import _DEFAULT_JWT_SECRET, check_jwt_secret
|
||||
with patch("SPARC.auth.JWT_SECRET", _DEFAULT_JWT_SECRET):
|
||||
# Should not raise
|
||||
check_jwt_secret()
|
||||
|
||||
# Restore
|
||||
importlib.reload(SPARC.config)
|
||||
|
||||
def test_custom_secret_in_production_succeeds(self):
|
||||
"""Starting with a custom secret in production must not raise."""
|
||||
with patch.dict(os.environ, {"APP_ENV": "production"}):
|
||||
import importlib
|
||||
import SPARC.config
|
||||
importlib.reload(SPARC.config)
|
||||
|
||||
from SPARC.auth import check_jwt_secret
|
||||
with patch("SPARC.auth.JWT_SECRET", "my-secure-random-secret-abc123"):
|
||||
# Should not raise
|
||||
check_jwt_secret()
|
||||
|
||||
with patch.dict(os.environ, {"APP_ENV": "development"}):
|
||||
importlib.reload(SPARC.config)
|
||||
|
||||
def test_default_secret_unset_env_succeeds(self):
|
||||
"""When APP_ENV is unset (defaults to development), default secret is allowed."""
|
||||
with patch.dict(os.environ, {}, clear=False):
|
||||
# Remove APP_ENV if present
|
||||
env = os.environ.copy()
|
||||
env.pop("APP_ENV", None)
|
||||
with patch.dict(os.environ, env, clear=True):
|
||||
import importlib
|
||||
import SPARC.config
|
||||
importlib.reload(SPARC.config)
|
||||
|
||||
from SPARC.auth import _DEFAULT_JWT_SECRET, check_jwt_secret
|
||||
with patch("SPARC.auth.JWT_SECRET", _DEFAULT_JWT_SECRET):
|
||||
# Should not raise (defaults to development)
|
||||
check_jwt_secret()
|
||||
|
||||
with patch.dict(os.environ, {"APP_ENV": "development"}):
|
||||
importlib.reload(SPARC.config)
|
||||
|
||||
|
||||
class TestCORSConfig:
|
||||
"""Test that CORS origins are configurable via environment variable."""
|
||||
|
||||
def test_default_cors_origins(self):
|
||||
"""When CORS_ORIGINS is unset, defaults to localhost origins."""
|
||||
with patch.dict(os.environ, {"CORS_ORIGINS": ""}):
|
||||
import importlib
|
||||
import SPARC.config
|
||||
importlib.reload(SPARC.config)
|
||||
assert SPARC.config.cors_origins == [
|
||||
"http://localhost:3000",
|
||||
"http://localhost:5173",
|
||||
]
|
||||
|
||||
def test_custom_cors_origins(self):
|
||||
"""Setting CORS_ORIGINS configures allowed origins."""
|
||||
with patch.dict(os.environ, {"CORS_ORIGINS": "https://sparc.example.com,https://app.example.com"}):
|
||||
import importlib
|
||||
import SPARC.config
|
||||
importlib.reload(SPARC.config)
|
||||
assert SPARC.config.cors_origins == [
|
||||
"https://sparc.example.com",
|
||||
"https://app.example.com",
|
||||
]
|
||||
# Restore
|
||||
with patch.dict(os.environ, {"CORS_ORIGINS": ""}):
|
||||
importlib.reload(SPARC.config)
|
||||
|
||||
def test_single_cors_origin(self):
|
||||
"""A single origin without comma works correctly."""
|
||||
with patch.dict(os.environ, {"CORS_ORIGINS": "https://sparc.example.com"}):
|
||||
import importlib
|
||||
import SPARC.config
|
||||
importlib.reload(SPARC.config)
|
||||
assert SPARC.config.cors_origins == ["https://sparc.example.com"]
|
||||
with patch.dict(os.environ, {"CORS_ORIGINS": ""}):
|
||||
importlib.reload(SPARC.config)
|
||||
Reference in New Issue
Block a user