diff --git a/apps/agent-coordinator/src/app/main.py b/apps/agent-coordinator/src/app/main.py index 9db612a2..957285f9 100644 --- a/apps/agent-coordinator/src/app/main.py +++ b/apps/agent-coordinator/src/app/main.py @@ -4,7 +4,7 @@ from fastapi.middleware.cors import CORSMiddleware from aitbc.rate_limiting import RateLimitMiddleware -from .config import settings +from .config import settings, validated_cors_origins from .exceptions import register_exception_handlers from .lifespan import lifespan from .middleware import register_middleware @@ -21,7 +21,7 @@ def create_app() -> FastAPI: app.add_middleware( CORSMiddleware, - allow_origins=["*"], + allow_origins=_validated_cors_origins(settings.cors_origins), allow_credentials=True, allow_methods=["*"], allow_headers=["*"], diff --git a/apps/blockchain-node/src/aitbc_chain/rpc/router.py b/apps/blockchain-node/src/aitbc_chain/rpc/router.py index 8af087b3..836d85a9 100644 --- a/apps/blockchain-node/src/aitbc_chain/rpc/router.py +++ b/apps/blockchain-node/src/aitbc_chain/rpc/router.py @@ -10,7 +10,7 @@ import uuid from typing import Any, Dict, Optional, List from datetime import datetime, timezone, timedelta -from fastapi import APIRouter, HTTPException, status, Request +from fastapi import APIRouter, Depends, HTTPException, status, Request from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials from pydantic import BaseModel, Field, model_validator from sqlmodel import select, delete @@ -52,34 +52,32 @@ def get_authenticated_address(request: Request, credentials: Optional[HTTPAuthor # Check for X-Wallet-Address header (API key authentication) wallet_address = request.headers.get("X-Wallet-Address") if wallet_address: - # Validate address format (basic check) if not wallet_address.startswith("0x") or len(wallet_address) != 42: _logger.warning(f"Invalid wallet address format in X-Wallet-Address header: {wallet_address}") raise HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid wallet address format" ) + if os.getenv("TRUST_X_WALLET_ADDRESS", "false").lower() != "true": + _logger.warning("Rejected untrusted X-Wallet-Address header") + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail="X-Wallet-Address header is not trusted without explicit server configuration" + ) _logger.debug(f"Authenticated via X-Wallet-Address header: {wallet_address}") return wallet_address # Check for JWT Bearer token if credentials and credentials.scheme == "Bearer": - # In a full implementation, this would validate the JWT token - # For now, we'll extract a wallet address from the token if present - # This is a placeholder for proper JWT validation - token = credentials.credentials - _logger.debug(f"JWT token provided (validation not yet implemented)") - # TODO: Implement proper JWT validation and address extraction - # For now, raise an error to require proper implementation + _logger.warning("JWT authentication attempted but not supported") raise HTTPException( - status_code=status.HTTP_501_NOT_IMPLEMENTED, - detail="JWT authentication not yet implemented. Use X-Wallet-Address header." + status_code=status.HTTP_401_UNAUTHORIZED, + detail="JWT authentication is not supported. Use X-Wallet-Address header with TRUST_X_WALLET_ADDRESS=true for trusted internal requests." ) # Development mode fallback if os.getenv("DEV_MODE", "false").lower() == "true": - _logger.warning("Using development mode fallback for authentication - returning zero address") - return "0x0000000000000000000000000000000000000000" + _logger.warning("Rejected unauthenticated request in development mode") # No valid authentication found raise HTTPException( @@ -1739,17 +1737,22 @@ async def submit_arbitration_vote( try: # Get authenticated address from request arbitrator_address = get_authenticated_address(http_request, credentials) - - # TODO: Implement actual smart contract interaction with arbitrator authorization check - # For now, validate that we have a real address (not zero address unless in dev mode) + + # Reject zero address in all modes - this is a sensitive arbitration operation if arbitrator_address == "0x0000000000000000000000000000000000000000": - _logger.warning("Vote submission attempted with zero address - may be in dev mode") - + _logger.error("Vote submission attempted with zero address - rejected") + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail="Zero address is not allowed for arbitration operations" + ) + return SubmitArbitrationVoteResponse( success=True, status="Submitted", message=f"Vote submitted successfully for dispute {request.dispute_id}" ) + except HTTPException: + raise except Exception as e: _logger.error(f"Error submitting arbitration vote: {e}") raise HTTPException(status_code=500, detail=f"Failed to submit vote: {str(e)}") diff --git a/apps/coordinator-api/src/app/contexts/hermes/routers/hermes_enhanced_app.py b/apps/coordinator-api/src/app/contexts/hermes/routers/hermes_enhanced_app.py index 7d876edd..7ee67022 100755 --- a/apps/coordinator-api/src/app/contexts/hermes/routers/hermes_enhanced_app.py +++ b/apps/coordinator-api/src/app/contexts/hermes/routers/hermes_enhanced_app.py @@ -22,7 +22,16 @@ app = FastAPI( app.add_middleware( CORSMiddleware, - allow_origins=["*"], + allow_origins=[ + "http://localhost:8001", + "http://localhost:8011", + "http://localhost:8016", + "http://localhost:9001", + "http://127.0.0.1:8001", + "http://127.0.0.1:8011", + "http://127.0.0.1:8016", + "http://127.0.0.1:9001", + ], allow_credentials=True, allow_methods=["GET", "POST", "PUT", "DELETE", "OPTIONS"], allow_headers=["*"], diff --git a/apps/coordinator-api/src/app/routers/marketplace_enhanced_app.py b/apps/coordinator-api/src/app/routers/marketplace_enhanced_app.py index 77a86be1..41999d61 100755 --- a/apps/coordinator-api/src/app/routers/marketplace_enhanced_app.py +++ b/apps/coordinator-api/src/app/routers/marketplace_enhanced_app.py @@ -20,7 +20,16 @@ app = FastAPI( app.add_middleware( CORSMiddleware, - allow_origins=["*"], + allow_origins=[ + "http://localhost:8001", + "http://localhost:8011", + "http://localhost:8016", + "http://localhost:9001", + "http://127.0.0.1:8001", + "http://127.0.0.1:8011", + "http://127.0.0.1:8016", + "http://127.0.0.1:9001", + ], allow_credentials=True, allow_methods=["GET", "POST", "PUT", "DELETE", "OPTIONS"], allow_headers=["*"], diff --git a/apps/coordinator-api/src/app/services/adaptive_learning_app.py b/apps/coordinator-api/src/app/services/adaptive_learning_app.py index ab483391..4adc8490 100755 --- a/apps/coordinator-api/src/app/services/adaptive_learning_app.py +++ b/apps/coordinator-api/src/app/services/adaptive_learning_app.py @@ -21,7 +21,16 @@ app = FastAPI( app.add_middleware( CORSMiddleware, - allow_origins=["*"], + allow_origins=[ + "http://localhost:8001", + "http://localhost:8011", + "http://localhost:8016", + "http://localhost:9001", + "http://127.0.0.1:8001", + "http://127.0.0.1:8011", + "http://127.0.0.1:8016", + "http://127.0.0.1:9001", + ], allow_credentials=True, allow_methods=["GET", "POST", "PUT", "DELETE", "OPTIONS"], allow_headers=["*"], diff --git a/apps/coordinator-api/src/app/services/advanced_ai_service.py b/apps/coordinator-api/src/app/services/advanced_ai_service.py index 5826797b..aab5ba47 100755 --- a/apps/coordinator-api/src/app/services/advanced_ai_service.py +++ b/apps/coordinator-api/src/app/services/advanced_ai_service.py @@ -74,7 +74,16 @@ app = FastAPI( # CORS middleware app.add_middleware( CORSMiddleware, - allow_origins=["*"], + allow_origins=[ + "http://localhost:8001", + "http://localhost:8011", + "http://localhost:8016", + "http://localhost:9001", + "http://127.0.0.1:8001", + "http://127.0.0.1:8011", + "http://127.0.0.1:8016", + "http://127.0.0.1:9001", + ], allow_credentials=True, allow_methods=["*"], allow_headers=["*"], diff --git a/apps/coordinator-api/src/app/services/enterprise_integration/api_gateway.py b/apps/coordinator-api/src/app/services/enterprise_integration/api_gateway.py index ed4aa9a1..8c597d2f 100755 --- a/apps/coordinator-api/src/app/services/enterprise_integration/api_gateway.py +++ b/apps/coordinator-api/src/app/services/enterprise_integration/api_gateway.py @@ -466,7 +466,16 @@ app = FastAPI( # CORS middleware app.add_middleware( CORSMiddleware, - allow_origins=["*"], + allow_origins=[ + "http://localhost:8001", + "http://localhost:8011", + "http://localhost:8016", + "http://localhost:9001", + "http://127.0.0.1:8001", + "http://127.0.0.1:8011", + "http://127.0.0.1:8016", + "http://127.0.0.1:9001", + ], allow_credentials=True, allow_methods=["*"], allow_headers=["*"], diff --git a/apps/coordinator-api/src/app/services/gpu_multimodal_app.py b/apps/coordinator-api/src/app/services/gpu_multimodal_app.py index d719667c..545ee389 100755 --- a/apps/coordinator-api/src/app/services/gpu_multimodal_app.py +++ b/apps/coordinator-api/src/app/services/gpu_multimodal_app.py @@ -21,7 +21,16 @@ app = FastAPI( app.add_middleware( CORSMiddleware, - allow_origins=["*"], + allow_origins=[ + "http://localhost:8001", + "http://localhost:8011", + "http://localhost:8016", + "http://localhost:9001", + "http://127.0.0.1:8001", + "http://127.0.0.1:8011", + "http://127.0.0.1:8016", + "http://127.0.0.1:9001", + ], allow_credentials=True, allow_methods=["GET", "POST", "PUT", "DELETE", "OPTIONS"], allow_headers=["*"], diff --git a/apps/coordinator-api/src/app/services/modality_optimization_app.py b/apps/coordinator-api/src/app/services/modality_optimization_app.py index 6a4971cb..d7b7923b 100755 --- a/apps/coordinator-api/src/app/services/modality_optimization_app.py +++ b/apps/coordinator-api/src/app/services/modality_optimization_app.py @@ -21,7 +21,16 @@ app = FastAPI( app.add_middleware( CORSMiddleware, - allow_origins=["*"], + allow_origins=[ + "http://localhost:8001", + "http://localhost:8011", + "http://localhost:8016", + "http://localhost:9001", + "http://127.0.0.1:8001", + "http://127.0.0.1:8011", + "http://127.0.0.1:8016", + "http://127.0.0.1:9001", + ], allow_credentials=True, allow_methods=["GET", "POST", "PUT", "DELETE", "OPTIONS"], allow_headers=["*"], diff --git a/apps/coordinator-api/src/app/services/multimodal_app.py b/apps/coordinator-api/src/app/services/multimodal_app.py index ae16bda0..ea163ae5 100755 --- a/apps/coordinator-api/src/app/services/multimodal_app.py +++ b/apps/coordinator-api/src/app/services/multimodal_app.py @@ -21,7 +21,16 @@ app = FastAPI( app.add_middleware( CORSMiddleware, - allow_origins=["*"], + allow_origins=[ + "http://localhost:8001", + "http://localhost:8011", + "http://localhost:8016", + "http://localhost:9001", + "http://127.0.0.1:8001", + "http://127.0.0.1:8011", + "http://127.0.0.1:8016", + "http://127.0.0.1:9001", + ], allow_credentials=True, allow_methods=["GET", "POST", "PUT", "DELETE", "OPTIONS"], allow_headers=["*"], diff --git a/apps/marketplace/agent_marketplace.py b/apps/marketplace/agent_marketplace.py index 9e779086..a51a969e 100755 --- a/apps/marketplace/agent_marketplace.py +++ b/apps/marketplace/agent_marketplace.py @@ -5,6 +5,7 @@ Miners register GPU offerings, choose chains, and confirm deals """ import json +import os import uuid from datetime import datetime, timedelta from typing import Dict, List, Any, Optional @@ -14,6 +15,28 @@ from fastapi.responses import JSONResponse from pydantic import BaseModel import uvicorn +DEFAULT_CORS_ORIGINS = [ + "http://localhost:8001", + "http://localhost:8011", + "http://localhost:8016", + "http://localhost:9001", + "http://127.0.0.1:8001", + "http://127.0.0.1:8011", + "http://127.0.0.1:8016", + "http://127.0.0.1:9001", +] + + +def get_cors_origins() -> List[str]: + raw_origins = os.getenv("AITBC_MARKETPLACE_CORS_ORIGINS") + if not raw_origins: + return DEFAULT_CORS_ORIGINS + origins = [origin.strip() for origin in raw_origins.split(",") if origin.strip()] + if "*" in origins: + raise ValueError("Wildcard CORS origins are not allowed when credentials are enabled") + return origins + + app = FastAPI( title="AITBC Agent-First GPU Marketplace", description="GPU trading marketplace where miners register offerings and confirm deals", @@ -23,7 +46,7 @@ app = FastAPI( # Add CORS middleware app.add_middleware( CORSMiddleware, - allow_origins=["*"], + allow_origins=get_cors_origins(), allow_credentials=True, allow_methods=["*"], allow_headers=["*"], diff --git a/tests/contract_tests/test_dispute_auth.py b/tests/contract_tests/test_dispute_auth.py index c20a307a..0bab555e 100644 --- a/tests/contract_tests/test_dispute_auth.py +++ b/tests/contract_tests/test_dispute_auth.py @@ -5,18 +5,26 @@ Tests for missing authentication, unauthorized access, and invalid tokens. import pytest import os +import sys +from pathlib import Path +import pytest_asyncio from httpx import AsyncClient, ASGITransport from fastapi import status +repo_root = Path(__file__).resolve().parents[2] +blockchain_src = repo_root / "apps" / "blockchain-node" / "src" +if str(blockchain_src) not in sys.path: + sys.path.insert(0, str(blockchain_src)) + @pytest.mark.asyncio class TestDisputeAuthentication: """Test authentication requirements for dispute endpoints""" - @pytest.fixture + @pytest_asyncio.fixture async def client(self): """Create test client for blockchain node RPC""" - from apps.blockchain_node.src.aitbc_chain.rpc.router import router + from aitbc_chain.rpc.router import router from fastapi import FastAPI app = FastAPI() @@ -24,7 +32,9 @@ class TestDisputeAuthentication: # Set DEV_MODE to false for production-like testing original_dev_mode = os.getenv("DEV_MODE") + original_trust_header = os.getenv("TRUST_X_WALLET_ADDRESS") os.environ["DEV_MODE"] = "false" + os.environ["TRUST_X_WALLET_ADDRESS"] = "false" transport = ASGITransport(app=app) async with AsyncClient(transport=transport, base_url="http://test") as ac: @@ -41,23 +51,23 @@ class TestDisputeAuthentication: response = await client.post( "/rpc/disputes/file", json={ - "agreement_id": "test_agreement_1", + "agreement_id": 1, "respondent": "0x1234567890123456789012345678901234567890", "dispute_type": "payment_dispute", "reason": "Test dispute", "evidence_hash": "0xabcdef" } ) - + assert response.status_code == status.HTTP_401_UNAUTHORIZED assert "Authentication required" in response.json()["detail"] - + async def test_file_dispute_with_invalid_wallet_address(self, client): """Test that filing a dispute with invalid wallet address format returns 401""" response = await client.post( "/rpc/disputes/file", json={ - "agreement_id": "test_agreement_1", + "agreement_id": 1, "respondent": "0x1234567890123456789012345678901234567890", "dispute_type": "payment_dispute", "reason": "Test dispute", @@ -65,10 +75,10 @@ class TestDisputeAuthentication: }, headers={"X-Wallet-Address": "invalid_address_format"} ) - + assert response.status_code == status.HTTP_401_UNAUTHORIZED assert "Invalid wallet address format" in response.json()["detail"] - + async def test_submit_evidence_missing_auth(self, client): """Test that submitting evidence without authentication returns 401""" response = await client.post( @@ -79,10 +89,10 @@ class TestDisputeAuthentication: "evidence_data": "test_evidence_data" } ) - + assert response.status_code == status.HTTP_401_UNAUTHORIZED assert "Authentication required" in response.json()["detail"] - + async def test_verify_evidence_missing_auth(self, client): """Test that verifying evidence without authentication returns 401""" response = await client.post( @@ -94,10 +104,10 @@ class TestDisputeAuthentication: "verification_score": 95 } ) - + assert response.status_code == status.HTTP_401_UNAUTHORIZED assert "Authentication required" in response.json()["detail"] - + async def test_authorize_arbitrator_missing_auth(self, client): """Test that authorizing an arbitrator without authentication returns 401""" response = await client.post( @@ -107,10 +117,10 @@ class TestDisputeAuthentication: "reputation_score": 85 } ) - + assert response.status_code == status.HTTP_401_UNAUTHORIZED assert "Authentication required" in response.json()["detail"] - + async def test_submit_vote_missing_auth(self, client): """Test that submitting a vote without authentication returns 401""" response = await client.post( @@ -122,16 +132,17 @@ class TestDisputeAuthentication: "reasoning": "Test reasoning" } ) - + assert response.status_code == status.HTTP_401_UNAUTHORIZED assert "Authentication required" in response.json()["detail"] - + async def test_file_dispute_with_valid_wallet_address(self, client): """Test that filing a dispute with valid wallet address header succeeds (or returns expected error)""" + os.environ["TRUST_X_WALLET_ADDRESS"] = "true" response = await client.post( "/rpc/disputes/file", json={ - "agreement_id": "test_agreement_1", + "agreement_id": 1, "respondent": "0x1234567890123456789012345678901234567890", "dispute_type": "payment_dispute", "reason": "Test dispute", @@ -139,17 +150,34 @@ class TestDisputeAuthentication: }, headers={"X-Wallet-Address": "0x1234567890123456789012345678901234567890"} ) - + # Should not be 401 (authentication passed) # May be 500 if dispute service is not available, which is acceptable assert response.status_code != status.HTTP_401_UNAUTHORIZED - - async def test_jwt_token_not_implemented(self, client): - """Test that JWT token authentication returns 501 (not yet implemented)""" + + async def test_valid_wallet_address_header_is_not_trusted_by_default(self, client): + """Test that filing a dispute with an untrusted wallet address header returns 401""" response = await client.post( "/rpc/disputes/file", json={ - "agreement_id": "test_agreement_1", + "agreement_id": 1, + "respondent": "0x1234567890123456789012345678901234567890", + "dispute_type": "payment_dispute", + "reason": "Test dispute", + "evidence_hash": "0xabcdef" + }, + headers={"X-Wallet-Address": "0x1234567890123456789012345678901234567890"} + ) + + assert response.status_code == status.HTTP_401_UNAUTHORIZED + assert "not trusted" in response.json()["detail"] + + async def test_jwt_token_not_configured(self, client): + """Test that JWT token authentication fails closed with clear error""" + response = await client.post( + "/rpc/disputes/file", + json={ + "agreement_id": 1, "respondent": "0x1234567890123456789012345678901234567890", "dispute_type": "payment_dispute", "reason": "Test dispute", @@ -157,19 +185,19 @@ class TestDisputeAuthentication: }, headers={"Authorization": "Bearer test_token"} ) - - assert response.status_code == status.HTTP_501_NOT_IMPLEMENTED - assert "JWT authentication not yet implemented" in response.json()["detail"] + + assert response.status_code == status.HTTP_401_UNAUTHORIZED + assert "JWT authentication is not supported" in response.json()["detail"] @pytest.mark.asyncio class TestDisputeAuthDevMode: """Test authentication behavior in development mode""" - @pytest.fixture + @pytest_asyncio.fixture async def dev_client(self): """Create test client with DEV_MODE enabled""" - from apps.blockchain_node.src.aitbc_chain.rpc.router import router + from aitbc_chain.rpc.router import router from fastapi import FastAPI app = FastAPI() @@ -177,7 +205,9 @@ class TestDisputeAuthDevMode: # Set DEV_MODE to true original_dev_mode = os.getenv("DEV_MODE") + original_trust_header = os.getenv("TRUST_X_WALLET_ADDRESS") os.environ["DEV_MODE"] = "true" + os.environ["TRUST_X_WALLET_ADDRESS"] = "false" transport = ASGITransport(app=app) async with AsyncClient(transport=transport, base_url="http://test") as ac: @@ -189,19 +219,35 @@ class TestDisputeAuthDevMode: else: os.environ["DEV_MODE"] = original_dev_mode - async def test_file_dispute_dev_mode_fallback(self, dev_client): - """Test that in dev mode, missing auth uses zero address fallback""" + async def test_file_dispute_dev_mode_fails_closed(self, dev_client): + """Test that dev mode no longer uses a zero address fallback""" response = await dev_client.post( "/rpc/disputes/file", json={ - "agreement_id": "test_agreement_1", + "agreement_id": 1, "respondent": "0x1234567890123456789012345678901234567890", "dispute_type": "payment_dispute", "reason": "Test dispute", "evidence_hash": "0xabcdef" } ) - - # In dev mode, should not return 401 (uses zero address fallback) - # May return 500 if dispute service is not available - assert response.status_code != status.HTTP_401_UNAUTHORIZED + + assert response.status_code == status.HTTP_401_UNAUTHORIZED + assert "Authentication required" in response.json()["detail"] + + async def test_arbitration_vote_zero_address_rejected(self, dev_client): + """Test that zero address is rejected in arbitration vote submission""" + os.environ["TRUST_X_WALLET_ADDRESS"] = "true" + response = await dev_client.post( + "/rpc/disputes/vote", + json={ + "dispute_id": 1, + "vote_in_favor_of_initiator": True, + "confidence": 90, + "reasoning": "Test reasoning" + }, + headers={"X-Wallet-Address": "0x0000000000000000000000000000000000000000"} + ) + + assert response.status_code == status.HTTP_401_UNAUTHORIZED + assert "Zero address is not allowed" in response.json()["detail"] diff --git a/tests/security/test_cors_configuration.py b/tests/security/test_cors_configuration.py new file mode 100644 index 00000000..18f63528 --- /dev/null +++ b/tests/security/test_cors_configuration.py @@ -0,0 +1,135 @@ +""" +CORS configuration security tests. +Validates that wildcard CORS is not used with allow_credentials=True. +""" + +import pytest +import os +import sys +from pathlib import Path + + +def test_agent_coordinator_cors_rejects_wildcard(): + """Test that agent-coordinator config rejects wildcard origins""" + repo_root = Path(__file__).resolve().parents[2] + agent_coordinator_src = repo_root / "apps" / "agent-coordinator" / "src" + + if str(agent_coordinator_src) not in sys.path: + sys.path.insert(0, str(agent_coordinator_src)) + + # Set required secret_key to avoid validation error + os.environ["SECRET_KEY"] = "test_secret_key_for_testing" + + from app.config import validated_cors_origins + + with pytest.raises(ValueError, match="Wildcard CORS origins are not allowed"): + validated_cors_origins(["*"]) + + # Clean up + os.environ.pop("SECRET_KEY", None) + + +def test_agent_coordinator_cors_accepts_localhost(): + """Test that agent-coordinator config accepts localhost origins""" + repo_root = Path(__file__).resolve().parents[2] + agent_coordinator_src = repo_root / "apps" / "agent-coordinator" / "src" + + if str(agent_coordinator_src) not in sys.path: + sys.path.insert(0, str(agent_coordinator_src)) + + # Set required secret_key to avoid validation error + os.environ["SECRET_KEY"] = "test_secret_key_for_testing" + + from app.config import validated_cors_origins + + origins = [ + "http://localhost:8001", + "http://localhost:9001", + "http://127.0.0.1:8001", + ] + result = validated_cors_origins(origins) + assert result == origins + + # Clean up + os.environ.pop("SECRET_KEY", None) + + +def test_marketplace_cors_rejects_wildcard(): + """Test that marketplace rejects wildcard origins via environment variable""" + repo_root = Path(__file__).resolve().parents[2] + marketplace_src = repo_root / "apps" / "marketplace" + + if str(marketplace_src) not in sys.path: + sys.path.insert(0, str(marketplace_src)) + + # Set environment variable with wildcard + os.environ["AITBC_MARKETPLACE_CORS_ORIGINS"] = "*" + + # The marketplace module raises ValueError on import when wildcard is set + # This is the expected behavior + with pytest.raises(ValueError, match="Wildcard CORS origins are not allowed"): + import importlib.util + spec = importlib.util.spec_from_file_location("agent_marketplace", marketplace_src / "agent_marketplace.py") + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) + + # Clean up + os.environ.pop("AITBC_MARKETPLACE_CORS_ORIGINS", None) + + +def test_marketplace_cors_accepts_localhost(): + """Test that marketplace accepts localhost origins via environment variable""" + repo_root = Path(__file__).resolve().parents[2] + marketplace_src = repo_root / "apps" / "marketplace" + + if str(marketplace_src) not in sys.path: + sys.path.insert(0, str(marketplace_src)) + + os.environ["AITBC_MARKETPLACE_CORS_ORIGINS"] = "http://localhost:8001,http://localhost:9001" + + # Import the function directly from the file + import importlib.util + spec = importlib.util.spec_from_file_location("agent_marketplace", marketplace_src / "agent_marketplace.py") + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) + + result = module.get_cors_origins() + assert "http://localhost:8001" in result + assert "http://localhost:9001" in result + + # Clean up + os.environ.pop("AITBC_MARKETPLACE_CORS_ORIGINS", None) + + +def test_no_wildcard_cors_in_coordinator_api_apps(): + """Scan coordinator-api apps for wildcard CORS with credentials""" + import re + + repo_root = Path(__file__).resolve().parents[2] + coordinator_src = repo_root / "apps" / "coordinator-api" / "src" + + files_to_check = [ + coordinator_src / "app" / "contexts" / "hermes" / "routers" / "hermes_enhanced_app.py", + coordinator_src / "app" / "services" / "enterprise_integration" / "api_gateway.py", + coordinator_src / "app" / "services" / "modality_optimization_app.py", + coordinator_src / "app" / "services" / "multimodal_app.py", + coordinator_src / "app" / "services" / "gpu_multimodal_app.py", + coordinator_src / "app" / "routers" / "marketplace_enhanced_app.py", + coordinator_src / "app" / "services" / "advanced_ai_service.py", + coordinator_src / "app" / "services" / "adaptive_learning_app.py", + ] + + wildcard_pattern = re.compile(r'allow_origins\s*=\s*\["\*"\]') + credentials_pattern = re.compile(r'allow_credentials\s*=\s*True') + + for file_path in files_to_check: + if not file_path.exists(): + continue + + content = file_path.read_text() + has_wildcard = wildcard_pattern.search(content) is not None + has_credentials = credentials_pattern.search(content) is not None + + # If both wildcard and credentials are present, fail the test + if has_wildcard and has_credentials: + pytest.fail(f"File {file_path} contains wildcard CORS with credentials enabled")