**feat(git-ops): enhance MCP server with Git provider updates and SSRF protection**
- Added `mcp-git-ops` service to `docker-compose.dev.yml` with health checks and configurations. - Integrated SSRF protection in repository URL validation for enhanced security. - Expanded `pyproject.toml` mypy settings and adjusted code to meet stricter type checking. - Improved workspace management and GitWrapper operations with error handling refinements. - Updated input validation, branching, and repository operations to align with new error structure. - Shut down thread pool executor gracefully during server cleanup.
This commit is contained in:
@@ -13,7 +13,7 @@ from datetime import UTC, datetime, timedelta
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
import aiofiles
|
||||
import aiofiles # type: ignore[import-untyped]
|
||||
from filelock import FileLock, Timeout
|
||||
|
||||
from config import Settings, get_settings
|
||||
@@ -54,10 +54,25 @@ class WorkspaceManager:
|
||||
self.base_path.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
def _get_workspace_path(self, project_id: str) -> Path:
|
||||
"""Get the path for a project workspace."""
|
||||
"""Get the path for a project workspace with path traversal protection."""
|
||||
# Sanitize project ID for filesystem
|
||||
safe_id = "".join(c if c.isalnum() or c in "-_" else "_" for c in project_id)
|
||||
return self.base_path / safe_id
|
||||
|
||||
# Reject reserved names
|
||||
reserved_names = {".", "..", "con", "prn", "aux", "nul"}
|
||||
if safe_id.lower() in reserved_names:
|
||||
raise ValueError(f"Invalid project ID: reserved name '{project_id}'")
|
||||
|
||||
# Construct path and verify it's within base_path (prevent path traversal)
|
||||
workspace_path = (self.base_path / safe_id).resolve()
|
||||
base_resolved = self.base_path.resolve()
|
||||
|
||||
if not workspace_path.is_relative_to(base_resolved):
|
||||
raise ValueError(
|
||||
f"Invalid project ID: path traversal detected '{project_id}'"
|
||||
)
|
||||
|
||||
return workspace_path
|
||||
|
||||
def _get_lock_path(self, project_id: str) -> Path:
|
||||
"""Get the lock file path for a workspace."""
|
||||
@@ -230,10 +245,7 @@ class WorkspaceManager:
|
||||
raise WorkspaceNotFoundError(project_id)
|
||||
|
||||
# Check if already locked by someone else
|
||||
if (
|
||||
workspace.state == WorkspaceState.LOCKED
|
||||
and workspace.lock_holder != holder
|
||||
):
|
||||
if workspace.state == WorkspaceState.LOCKED and workspace.lock_holder != holder:
|
||||
# Check if lock expired
|
||||
if workspace.lock_expires and workspace.lock_expires > datetime.now(UTC):
|
||||
raise WorkspaceLockedError(project_id, workspace.lock_holder)
|
||||
@@ -275,11 +287,7 @@ class WorkspaceManager:
|
||||
raise WorkspaceNotFoundError(project_id)
|
||||
|
||||
# Verify holder
|
||||
if (
|
||||
not force
|
||||
and workspace.lock_holder
|
||||
and workspace.lock_holder != holder
|
||||
):
|
||||
if not force and workspace.lock_holder and workspace.lock_holder != holder:
|
||||
raise WorkspaceLockedError(project_id, workspace.lock_holder)
|
||||
|
||||
# Clear lock
|
||||
@@ -341,7 +349,7 @@ class WorkspaceManager:
|
||||
return True
|
||||
|
||||
size_bytes = await self._calculate_size(workspace_path)
|
||||
size_gb = size_bytes / (1024 ** 3)
|
||||
size_gb = size_bytes / (1024**3)
|
||||
max_size_gb = self.settings.workspace_max_size_gb
|
||||
|
||||
if size_gb > max_size_gb:
|
||||
@@ -362,7 +370,7 @@ class WorkspaceManager:
|
||||
Returns:
|
||||
List of WorkspaceInfo
|
||||
"""
|
||||
workspaces = []
|
||||
workspaces: list[WorkspaceInfo] = []
|
||||
|
||||
if not self.base_path.exists():
|
||||
return workspaces
|
||||
@@ -532,9 +540,7 @@ class WorkspaceLock:
|
||||
self.holder,
|
||||
)
|
||||
except Exception as e:
|
||||
logger.warning(
|
||||
f"Failed to release lock for {self.project_id}: {e}"
|
||||
)
|
||||
logger.warning(f"Failed to release lock for {self.project_id}: {e}")
|
||||
|
||||
|
||||
class FileLockManager:
|
||||
|
||||
Reference in New Issue
Block a user