Security hardening release addressing CodeQL and Dependabot alerts: - Fix stack trace exposure in error responses - Add SSRF protection with DNS resolution checking - Implement proper URL hostname validation (replaces substring matching) - Add centralized path sanitization to prevent path traversal - Fix ReDoS vulnerability in email validation regex - Improve HTML sanitization in validation utilities - Fix capability wildcard matching in auth utilities - Update glob dependency to address CVE - Add CodeQL suppression comments for verified false positives 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
411 lines
15 KiB
Python
411 lines
15 KiB
Python
"""
|
|
Integration Proxy API for GT 2.0
|
|
|
|
RESTful API for secure external service integration through the Resource Cluster.
|
|
Provides capability-based access control and sandbox restrictions.
|
|
"""
|
|
|
|
from typing import List, Dict, Any, Optional
|
|
from fastapi import APIRouter, HTTPException, Depends, Header
|
|
from pydantic import BaseModel, Field
|
|
|
|
from app.core.security import verify_capability_token
|
|
from app.services.integration_proxy import (
|
|
IntegrationProxyService, ProxyRequest, ProxyResponse, IntegrationConfig,
|
|
IntegrationType, SandboxLevel
|
|
)
|
|
|
|
router = APIRouter()
|
|
|
|
|
|
# Request/Response Models
|
|
class ExecuteIntegrationRequest(BaseModel):
|
|
"""Request to execute integration"""
|
|
integration_id: str = Field(..., description="Integration ID to execute")
|
|
method: str = Field(..., description="HTTP method (GET, POST, PUT, DELETE)")
|
|
endpoint: str = Field(..., description="Endpoint path or full URL")
|
|
headers: Optional[Dict[str, str]] = Field(None, description="Request headers")
|
|
data: Optional[Dict[str, Any]] = Field(None, description="Request data")
|
|
params: Optional[Dict[str, str]] = Field(None, description="Query parameters")
|
|
timeout_override: Optional[int] = Field(None, description="Override timeout in seconds")
|
|
|
|
|
|
class IntegrationExecutionResponse(BaseModel):
|
|
"""Response from integration execution"""
|
|
success: bool
|
|
status_code: int
|
|
data: Optional[Dict[str, Any]]
|
|
headers: Dict[str, str]
|
|
execution_time_ms: int
|
|
sandbox_applied: bool
|
|
restrictions_applied: List[str]
|
|
error_message: Optional[str]
|
|
|
|
|
|
class CreateIntegrationRequest(BaseModel):
|
|
"""Request to create integration configuration"""
|
|
name: str = Field(..., description="Human-readable integration name")
|
|
integration_type: str = Field(..., description="Type of integration")
|
|
base_url: str = Field(..., description="Base URL for the service")
|
|
authentication_method: str = Field(..., description="Authentication method")
|
|
auth_config: Dict[str, Any] = Field(..., description="Authentication configuration")
|
|
sandbox_level: str = Field("basic", description="Sandbox restriction level")
|
|
max_requests_per_hour: int = Field(1000, description="Rate limit per hour")
|
|
max_response_size_bytes: int = Field(10485760, description="Max response size (10MB default)")
|
|
timeout_seconds: int = Field(30, description="Request timeout")
|
|
allowed_methods: Optional[List[str]] = Field(None, description="Allowed HTTP methods")
|
|
allowed_endpoints: Optional[List[str]] = Field(None, description="Allowed endpoints")
|
|
blocked_endpoints: Optional[List[str]] = Field(None, description="Blocked endpoints")
|
|
allowed_domains: Optional[List[str]] = Field(None, description="Allowed domains")
|
|
|
|
|
|
class IntegrationConfigResponse(BaseModel):
|
|
"""Integration configuration response"""
|
|
id: str
|
|
name: str
|
|
integration_type: str
|
|
base_url: str
|
|
authentication_method: str
|
|
sandbox_level: str
|
|
max_requests_per_hour: int
|
|
max_response_size_bytes: int
|
|
timeout_seconds: int
|
|
allowed_methods: List[str]
|
|
allowed_endpoints: List[str]
|
|
blocked_endpoints: List[str]
|
|
allowed_domains: List[str]
|
|
is_active: bool
|
|
created_at: str
|
|
created_by: str
|
|
|
|
|
|
class IntegrationUsageResponse(BaseModel):
|
|
"""Integration usage analytics response"""
|
|
integration_id: str
|
|
total_requests: int
|
|
successful_requests: int
|
|
error_count: int
|
|
success_rate: float
|
|
avg_execution_time_ms: float
|
|
date_range: Dict[str, str]
|
|
|
|
|
|
# Dependency injection
|
|
async def get_integration_proxy_service() -> IntegrationProxyService:
|
|
"""Get integration proxy service"""
|
|
return IntegrationProxyService()
|
|
|
|
|
|
@router.post("/execute", response_model=IntegrationExecutionResponse)
|
|
async def execute_integration(
|
|
request: ExecuteIntegrationRequest,
|
|
authorization: str = Header(...),
|
|
proxy_service: IntegrationProxyService = Depends(get_integration_proxy_service)
|
|
):
|
|
"""
|
|
Execute external integration with capability-based access control.
|
|
|
|
- **integration_id**: ID of the configured integration
|
|
- **method**: HTTP method (GET, POST, PUT, DELETE)
|
|
- **endpoint**: API endpoint path or full URL
|
|
- **headers**: Optional request headers
|
|
- **data**: Optional request body data
|
|
- **params**: Optional query parameters
|
|
- **timeout_override**: Optional timeout override
|
|
"""
|
|
try:
|
|
# Create proxy request
|
|
proxy_request = ProxyRequest(
|
|
integration_id=request.integration_id,
|
|
method=request.method.upper(),
|
|
endpoint=request.endpoint,
|
|
headers=request.headers,
|
|
data=request.data,
|
|
params=request.params,
|
|
timeout_override=request.timeout_override
|
|
)
|
|
|
|
# Execute integration
|
|
response = await proxy_service.execute_integration(
|
|
request=proxy_request,
|
|
capability_token=authorization
|
|
)
|
|
|
|
return IntegrationExecutionResponse(
|
|
success=response.success,
|
|
status_code=response.status_code,
|
|
data=response.data,
|
|
headers=response.headers,
|
|
execution_time_ms=response.execution_time_ms,
|
|
sandbox_applied=response.sandbox_applied,
|
|
restrictions_applied=response.restrictions_applied,
|
|
error_message=response.error_message
|
|
)
|
|
|
|
except PermissionError as e:
|
|
raise HTTPException(status_code=403, detail=str(e))
|
|
except ValueError as e:
|
|
raise HTTPException(status_code=400, detail=str(e))
|
|
except Exception as e:
|
|
raise HTTPException(status_code=500, detail=f"Integration execution failed: {str(e)}")
|
|
|
|
|
|
@router.get("", response_model=List[IntegrationConfigResponse])
|
|
async def list_integrations(
|
|
authorization: str = Header(...),
|
|
proxy_service: IntegrationProxyService = Depends(get_integration_proxy_service)
|
|
):
|
|
"""
|
|
List available integrations based on user capabilities.
|
|
|
|
Returns only integrations the user has permission to access.
|
|
"""
|
|
try:
|
|
integrations = await proxy_service.list_integrations(authorization)
|
|
|
|
return [
|
|
IntegrationConfigResponse(
|
|
id=config.id,
|
|
name=config.name,
|
|
integration_type=config.integration_type.value,
|
|
base_url=config.base_url,
|
|
authentication_method=config.authentication_method,
|
|
sandbox_level=config.sandbox_level.value,
|
|
max_requests_per_hour=config.max_requests_per_hour,
|
|
max_response_size_bytes=config.max_response_size_bytes,
|
|
timeout_seconds=config.timeout_seconds,
|
|
allowed_methods=config.allowed_methods,
|
|
allowed_endpoints=config.allowed_endpoints,
|
|
blocked_endpoints=config.blocked_endpoints,
|
|
allowed_domains=config.allowed_domains,
|
|
is_active=config.is_active,
|
|
created_at=config.created_at.isoformat(),
|
|
created_by=config.created_by
|
|
)
|
|
for config in integrations
|
|
]
|
|
|
|
except PermissionError as e:
|
|
raise HTTPException(status_code=403, detail=str(e))
|
|
except Exception as e:
|
|
raise HTTPException(status_code=500, detail=f"Failed to list integrations: {str(e)}")
|
|
|
|
|
|
@router.post("", response_model=IntegrationConfigResponse)
|
|
async def create_integration(
|
|
request: CreateIntegrationRequest,
|
|
authorization: str = Header(...),
|
|
proxy_service: IntegrationProxyService = Depends(get_integration_proxy_service)
|
|
):
|
|
"""
|
|
Create new integration configuration (admin only).
|
|
|
|
- **name**: Human-readable name for the integration
|
|
- **integration_type**: Type of integration (communication, development, etc.)
|
|
- **base_url**: Base URL for the external service
|
|
- **authentication_method**: oauth2, api_key, basic_auth, certificate
|
|
- **auth_config**: Authentication details (encrypted storage)
|
|
- **sandbox_level**: none, basic, restricted, strict
|
|
"""
|
|
try:
|
|
# Verify admin capability
|
|
token_data = await verify_capability_token(authorization)
|
|
if not token_data:
|
|
raise HTTPException(status_code=401, detail="Invalid capability token")
|
|
|
|
# Check admin permissions
|
|
if not any("admin" in str(cap) for cap in token_data.get("capabilities", [])):
|
|
raise HTTPException(status_code=403, detail="Admin capability required")
|
|
|
|
# Generate unique ID
|
|
import uuid
|
|
integration_id = str(uuid.uuid4())
|
|
|
|
# Create integration config
|
|
config = IntegrationConfig(
|
|
id=integration_id,
|
|
name=request.name,
|
|
integration_type=IntegrationType(request.integration_type.lower()),
|
|
base_url=request.base_url,
|
|
authentication_method=request.authentication_method,
|
|
auth_config=request.auth_config,
|
|
sandbox_level=SandboxLevel(request.sandbox_level.lower()),
|
|
max_requests_per_hour=request.max_requests_per_hour,
|
|
max_response_size_bytes=request.max_response_size_bytes,
|
|
timeout_seconds=request.timeout_seconds,
|
|
allowed_methods=request.allowed_methods or ["GET", "POST"],
|
|
allowed_endpoints=request.allowed_endpoints or [],
|
|
blocked_endpoints=request.blocked_endpoints or [],
|
|
allowed_domains=request.allowed_domains or [],
|
|
created_by=token_data.get("sub", "unknown")
|
|
)
|
|
|
|
# Store configuration
|
|
success = await proxy_service.store_integration_config(config)
|
|
if not success:
|
|
raise HTTPException(status_code=500, detail="Failed to store integration configuration")
|
|
|
|
return IntegrationConfigResponse(
|
|
id=config.id,
|
|
name=config.name,
|
|
integration_type=config.integration_type.value,
|
|
base_url=config.base_url,
|
|
authentication_method=config.authentication_method,
|
|
sandbox_level=config.sandbox_level.value,
|
|
max_requests_per_hour=config.max_requests_per_hour,
|
|
max_response_size_bytes=config.max_response_size_bytes,
|
|
timeout_seconds=config.timeout_seconds,
|
|
allowed_methods=config.allowed_methods,
|
|
allowed_endpoints=config.allowed_endpoints,
|
|
blocked_endpoints=config.blocked_endpoints,
|
|
allowed_domains=config.allowed_domains,
|
|
is_active=config.is_active,
|
|
created_at=config.created_at.isoformat(),
|
|
created_by=config.created_by
|
|
)
|
|
|
|
except ValueError as e:
|
|
raise HTTPException(status_code=400, detail=str(e))
|
|
except HTTPException:
|
|
raise
|
|
except Exception as e:
|
|
raise HTTPException(status_code=500, detail=f"Failed to create integration: {str(e)}")
|
|
|
|
|
|
@router.get("/{integration_id}/usage", response_model=IntegrationUsageResponse)
|
|
async def get_integration_usage(
|
|
integration_id: str,
|
|
days: int = 30,
|
|
authorization: str = Header(...),
|
|
proxy_service: IntegrationProxyService = Depends(get_integration_proxy_service)
|
|
):
|
|
"""
|
|
Get usage analytics for specific integration.
|
|
|
|
- **days**: Number of days to analyze (default 30)
|
|
"""
|
|
try:
|
|
# Verify capability for this integration
|
|
token_data = await verify_capability_token(authorization)
|
|
if not token_data:
|
|
raise HTTPException(status_code=401, detail="Invalid capability token")
|
|
|
|
# Get usage analytics
|
|
usage = await proxy_service.get_integration_usage_analytics(integration_id, days)
|
|
|
|
return IntegrationUsageResponse(
|
|
integration_id=usage["integration_id"],
|
|
total_requests=usage["total_requests"],
|
|
successful_requests=usage["successful_requests"],
|
|
error_count=usage["error_count"],
|
|
success_rate=usage["success_rate"],
|
|
avg_execution_time_ms=usage["avg_execution_time_ms"],
|
|
date_range=usage["date_range"]
|
|
)
|
|
|
|
except PermissionError as e:
|
|
raise HTTPException(status_code=403, detail=str(e))
|
|
except Exception as e:
|
|
raise HTTPException(status_code=500, detail=f"Failed to get usage analytics: {str(e)}")
|
|
|
|
|
|
# Integration type and sandbox level catalogs
|
|
@router.get("/catalog/types")
|
|
async def get_integration_types():
|
|
"""Get available integration types for UI builders"""
|
|
return {
|
|
"integration_types": [
|
|
{
|
|
"value": "communication",
|
|
"label": "Communication",
|
|
"description": "Slack, Teams, Discord integration"
|
|
},
|
|
{
|
|
"value": "development",
|
|
"label": "Development",
|
|
"description": "GitHub, GitLab, Jira integration"
|
|
},
|
|
{
|
|
"value": "project_management",
|
|
"label": "Project Management",
|
|
"description": "Asana, Monday.com integration"
|
|
},
|
|
{
|
|
"value": "database",
|
|
"label": "Database",
|
|
"description": "PostgreSQL, MySQL, MongoDB connectors"
|
|
},
|
|
{
|
|
"value": "custom_api",
|
|
"label": "Custom API",
|
|
"description": "Custom REST/GraphQL APIs"
|
|
},
|
|
{
|
|
"value": "webhook",
|
|
"label": "Webhook",
|
|
"description": "Outbound webhook calls"
|
|
}
|
|
]
|
|
}
|
|
|
|
|
|
@router.get("/catalog/sandbox-levels")
|
|
async def get_sandbox_levels():
|
|
"""Get available sandbox levels for UI builders"""
|
|
return {
|
|
"sandbox_levels": [
|
|
{
|
|
"value": "none",
|
|
"label": "No Restrictions",
|
|
"description": "Trusted integrations with full access"
|
|
},
|
|
{
|
|
"value": "basic",
|
|
"label": "Basic Restrictions",
|
|
"description": "Basic timeout and size limits"
|
|
},
|
|
{
|
|
"value": "restricted",
|
|
"label": "Restricted Access",
|
|
"description": "Limited API calls and data access"
|
|
},
|
|
{
|
|
"value": "strict",
|
|
"label": "Maximum Security",
|
|
"description": "Strict restrictions and monitoring"
|
|
}
|
|
]
|
|
}
|
|
|
|
|
|
@router.get("/catalog/auth-methods")
|
|
async def get_authentication_methods():
|
|
"""Get available authentication methods for UI builders"""
|
|
return {
|
|
"auth_methods": [
|
|
{
|
|
"value": "api_key",
|
|
"label": "API Key",
|
|
"description": "Simple API key authentication",
|
|
"fields": ["api_key", "key_header", "key_prefix"]
|
|
},
|
|
{
|
|
"value": "basic_auth",
|
|
"label": "Basic Authentication",
|
|
"description": "Username and password authentication",
|
|
"fields": ["username", "password"]
|
|
},
|
|
{
|
|
"value": "oauth2",
|
|
"label": "OAuth 2.0",
|
|
"description": "OAuth 2.0 bearer token authentication",
|
|
"fields": ["access_token", "refresh_token", "client_id", "client_secret"]
|
|
},
|
|
{
|
|
"value": "certificate",
|
|
"label": "Certificate",
|
|
"description": "Client certificate authentication",
|
|
"fields": ["cert_path", "key_path", "ca_path"]
|
|
}
|
|
]
|
|
} |