diff --git a/app.py b/app.py index b82ee89..da453f1 100644 --- a/app.py +++ b/app.py @@ -10,6 +10,8 @@ import logging import threading import time +import hmac +import secrets from dotenv import load_dotenv # Load environment variables @@ -47,15 +49,27 @@ def clear(self): # Initialize Flask app app = Flask(__name__) -app.config['SECRET_KEY'] = os.getenv("FLASK_SECRET_KEY", "default-dev-key") +app.config['SECRET_KEY'] = os.getenv("FLASK_SECRET_KEY") or secrets.token_hex(32) -# Basic Auth +# Basic Auth. No hardcoded fallback password: anyone can read this repo, so a +# default here is a public credential for every deployment that forgets to set +# one. When ADMIN_PASS is unset, generate a random one for this run and print +# it once in the startup log so the operator can still log in locally. ADMIN_USER = os.getenv("ADMIN_USER", "admin") -ADMIN_PASS = os.getenv("ADMIN_PASS", "polymarket2024") +ADMIN_PASS = os.getenv("ADMIN_PASS") +if not ADMIN_PASS: + ADMIN_PASS = secrets.token_urlsafe(18) + logger.warning( + "ADMIN_PASS is not set. Generated a random dashboard password for " + f"this run: {ADMIN_PASS} (set ADMIN_PASS in .env to choose your own)" + ) def check_auth(username, password): - return username == ADMIN_USER and password == ADMIN_PASS + return ( + hmac.compare_digest(username or "", ADMIN_USER) + and hmac.compare_digest(password or "", ADMIN_PASS) + ) def authenticate(): @@ -560,6 +574,7 @@ def api_status(): @app.route('/api/analyze') +@requires_auth def api_analyze(): """API endpoint to run AI analysis""" if not analyzer: @@ -702,6 +717,7 @@ def api_positions(): @app.route('/api/add_order', methods=['POST']) +@requires_auth def api_add_order(): """Manually add an order to track (for orders placed before persistence was added)""" try: diff --git a/deploy-tokyo.sh b/deploy-tokyo.sh index e2864c6..bd76436 100755 --- a/deploy-tokyo.sh +++ b/deploy-tokyo.sh @@ -47,9 +47,18 @@ echo "Deploying to Cloud Run..." # Build env vars string ENV_VARS="BLOCKRUN_WALLET_KEY=$BLOCKRUN_WALLET_KEY" ENV_VARS="$ENV_VARS,POLYGON_WALLET_PRIVATE_KEY=$POLYGON_WALLET_PRIVATE_KEY" -ENV_VARS="$ENV_VARS,FLASK_SECRET_KEY=${FLASK_SECRET_KEY:-polymarket-agent-secret}" +if [ -z "$FLASK_SECRET_KEY" ]; then + FLASK_SECRET_KEY=$(openssl rand -hex 32) + echo "FLASK_SECRET_KEY not set, generated one for this deploy." +fi +if [ -z "$ADMIN_PASS" ]; then + ADMIN_PASS=$(openssl rand -hex 12) + echo "ADMIN_PASS not set, generated dashboard password: $ADMIN_PASS" + echo "Save it now. Set ADMIN_PASS in .env to choose your own." +fi +ENV_VARS="$ENV_VARS,FLASK_SECRET_KEY=$FLASK_SECRET_KEY" ENV_VARS="$ENV_VARS,ADMIN_USER=${ADMIN_USER:-admin}" -ENV_VARS="$ENV_VARS,ADMIN_PASS=${ADMIN_PASS:-polymarket2024}" +ENV_VARS="$ENV_VARS,ADMIN_PASS=$ADMIN_PASS" # Add Polymarket API credentials if available (recommended for production) if [ -n "$POLYMARKET_API_KEY" ]; then diff --git a/tests/test_no_default_credentials.py b/tests/test_no_default_credentials.py new file mode 100644 index 0000000..75eb3f9 --- /dev/null +++ b/tests/test_no_default_credentials.py @@ -0,0 +1,62 @@ +"""Regression test: the dashboard must not fall back to known credentials. + +Run: python -m pytest tests/test_no_default_credentials.py +Needs: flask, python-dotenv (already in requirements.txt). + +The app module is imported in a fresh subprocess with ADMIN_PASS and +FLASK_SECRET_KEY removed from the environment. The trading/AI modules are +stubbed so the import needs no wallet keys and no network. + +On the old code this fails: ADMIN_PASS came out as "polymarket2024" and +check_auth("admin", "polymarket2024") returned True. +""" +import os +import subprocess +import sys +import textwrap + +CHILD = textwrap.dedent( + """ + import sys, types + from unittest.mock import MagicMock + + for name in [ + "src", "src.market", "src.market.polymarket", + "src.analysis", "src.analysis.ai_analyzer", + "src.trading", "src.trading.wallet", "src.trading.executor", + "src.storage", + ]: + sys.modules.setdefault(name, types.ModuleType(name)) + sys.modules["src.market.polymarket"].fetch_active_markets = lambda **kw: [] + sys.modules["src.analysis.ai_analyzer"].get_analyzer = lambda: None + sys.modules["src.trading.wallet"].get_wallet = lambda: None + sys.modules["src.trading.executor"].get_executor = lambda: None + sys.modules["src.storage"].get_storage = lambda: None + + import app + + assert app.ADMIN_PASS not in ("", "polymarket2024"), app.ADMIN_PASS + assert app.app.config["SECRET_KEY"] != "default-dev-key" + assert app.check_auth("admin", "polymarket2024") is False + assert app.check_auth(app.ADMIN_USER, app.ADMIN_PASS) is True + assert app.check_auth(app.ADMIN_USER, "wrong-password") is False + + client = app.app.test_client() + # Paid/write endpoints must reject requests with no credentials. + assert client.post("/api/add_order", json={}).status_code == 401 + assert client.get("/api/analyze").status_code == 401 + print("ok") + """ +) + + +def test_no_default_credentials(): + env = {k: v for k, v in os.environ.items() + if k not in ("ADMIN_PASS", "FLASK_SECRET_KEY", "ADMIN_USER")} + result = subprocess.run( + [sys.executable, "-c", CHILD], + capture_output=True, text=True, env=env, + cwd=os.path.join(os.path.dirname(__file__), ".."), + ) + assert result.returncode == 0, result.stderr + assert "ok" in result.stdout