initial commit after one day coding agent session
This commit is contained in:
commit
372af75b90
88 changed files with 22694 additions and 0 deletions
433
tests/podman/test_containers.py
Normal file
433
tests/podman/test_containers.py
Normal file
|
|
@ -0,0 +1,433 @@
|
|||
"""
|
||||
Tests for Secure Container Manager.
|
||||
|
||||
Following TDD approach - these tests are written before implementation.
|
||||
Tests cover all requirements from todo.md section 1.3.2.
|
||||
All tests use mocked Podman client (no actual containers needed).
|
||||
"""
|
||||
|
||||
import pytest
|
||||
from datetime import datetime, timedelta
|
||||
from unittest.mock import MagicMock, Mock, patch, call
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def test_create_container_with_valid_params_succeeds():
|
||||
"""Test that container creation with valid params succeeds."""
|
||||
from mcp_forge.podman.containers import ContainerConfig, SecureContainerManager
|
||||
from mcp_forge.podman.client import PodmanClient
|
||||
from mcp_forge.security.allowlist import OperationValidator
|
||||
from mcp_forge.security.audit import AuditLogger, AuditEventType
|
||||
from mcp_forge.security.resource_limits import ResourceLimits
|
||||
from mcp_forge.config.schema import SecurityConfig
|
||||
|
||||
# Setup mocks
|
||||
mock_podman_client = MagicMock(spec=PodmanClient)
|
||||
mock_container = MagicMock()
|
||||
mock_container.id = "abc123"
|
||||
mock_podman_client.client.containers.create.return_value = mock_container
|
||||
|
||||
audit_logger = AuditLogger(Path("/tmp/test_audit.log"))
|
||||
validator = OperationValidator(SecurityConfig(audit_log=Path("/tmp/test_audit.log")))
|
||||
|
||||
manager = SecureContainerManager(
|
||||
podman_client=mock_podman_client,
|
||||
validator=validator,
|
||||
audit_logger=audit_logger
|
||||
)
|
||||
|
||||
config = ContainerConfig(
|
||||
image="mcp-forge/python:3.11",
|
||||
command=["python", "-c", "print('hello')"],
|
||||
resource_limits=ResourceLimits(memory="512m", storage="1g", cpu_quota=100000)
|
||||
)
|
||||
|
||||
container_id = manager.create_container(config, session_id="test-session-1")
|
||||
|
||||
assert container_id == "abc123"
|
||||
mock_podman_client.client.containers.create.assert_called_once()
|
||||
|
||||
|
||||
def test_create_container_with_forbidden_params_raises_security_error():
|
||||
"""Test that forbidden parameters raise SecurityError."""
|
||||
from mcp_forge.podman.containers import ContainerConfig, SecureContainerManager
|
||||
from mcp_forge.podman.client import PodmanClient
|
||||
from mcp_forge.security.allowlist import OperationValidator, SecurityError
|
||||
from mcp_forge.security.audit import AuditLogger
|
||||
from mcp_forge.config.schema import SecurityConfig
|
||||
|
||||
mock_podman_client = MagicMock(spec=PodmanClient)
|
||||
audit_logger = AuditLogger(Path("/tmp/test_audit.log"))
|
||||
validator = OperationValidator(SecurityConfig(audit_log=Path("/tmp/test_audit.log")))
|
||||
|
||||
manager = SecureContainerManager(
|
||||
podman_client=mock_podman_client,
|
||||
validator=validator,
|
||||
audit_logger=audit_logger
|
||||
)
|
||||
|
||||
config = ContainerConfig(
|
||||
image="mcp-forge/python:3.11",
|
||||
command=["python", "-c", "print('hello')"]
|
||||
)
|
||||
|
||||
# Try to override security params (should be caught in to_podman_params or validation)
|
||||
with pytest.raises(SecurityError):
|
||||
# This should fail validation
|
||||
manager.create_container(config, session_id="test-session", privileged=True)
|
||||
|
||||
|
||||
def test_create_container_with_invalid_image_raises_security_error():
|
||||
"""Test that invalid/disallowed images raise SecurityError."""
|
||||
from mcp_forge.podman.containers import ContainerConfig, SecureContainerManager
|
||||
from mcp_forge.podman.client import PodmanClient
|
||||
from mcp_forge.security.allowlist import OperationValidator, SecurityError
|
||||
from mcp_forge.security.audit import AuditLogger
|
||||
from mcp_forge.config.schema import SecurityConfig
|
||||
|
||||
mock_podman_client = MagicMock(spec=PodmanClient)
|
||||
audit_logger = AuditLogger(Path("/tmp/test_audit.log"))
|
||||
validator = OperationValidator(SecurityConfig(audit_log=Path("/tmp/test_audit.log")))
|
||||
|
||||
manager = SecureContainerManager(
|
||||
podman_client=mock_podman_client,
|
||||
validator=validator,
|
||||
audit_logger=audit_logger
|
||||
)
|
||||
|
||||
config = ContainerConfig(
|
||||
image="evil/malicious:latest",
|
||||
command=["python", "-c", "print('hello')"]
|
||||
)
|
||||
|
||||
with pytest.raises(SecurityError) as exc_info:
|
||||
manager.create_container(config, session_id="test-session")
|
||||
assert "image" in str(exc_info.value).lower()
|
||||
|
||||
|
||||
def test_create_container_enforces_required_parameters():
|
||||
"""Test that required security parameters are enforced."""
|
||||
from mcp_forge.podman.containers import ContainerConfig
|
||||
|
||||
config = ContainerConfig(
|
||||
image="mcp-forge/python:3.11",
|
||||
command=["python", "-c", "print('hello')"]
|
||||
)
|
||||
|
||||
params = config.to_podman_params()
|
||||
|
||||
# Check required security parameters
|
||||
assert params["network_mode"] == "none"
|
||||
assert params["read_only"] is True
|
||||
assert "no-new-privileges" in params["security_opt"]
|
||||
assert params["user"] == "1000:1000"
|
||||
|
||||
|
||||
def test_resource_limits_are_applied_correctly():
|
||||
"""Test that resource limits are correctly applied."""
|
||||
from mcp_forge.podman.containers import ContainerConfig
|
||||
from mcp_forge.security.resource_limits import ResourceLimits
|
||||
|
||||
limits = ResourceLimits(
|
||||
memory="1g",
|
||||
storage="2g",
|
||||
cpu_quota=200000
|
||||
)
|
||||
|
||||
config = ContainerConfig(
|
||||
image="mcp-forge/python:3.11",
|
||||
resource_limits=limits
|
||||
)
|
||||
|
||||
params = config.to_podman_params()
|
||||
|
||||
assert params["mem_limit"] == "1073741824" # 1GB in bytes as string
|
||||
assert params["cpu_quota"] == 200000
|
||||
assert params["storage_opt"]["size"] == "2147483648" # 2GB in bytes as string
|
||||
|
||||
|
||||
def test_volume_mounts_are_validated():
|
||||
"""Test that volume mounts are validated against allowlist."""
|
||||
from mcp_forge.podman.containers import ContainerConfig, SecureContainerManager
|
||||
from mcp_forge.podman.client import PodmanClient
|
||||
from mcp_forge.security.allowlist import OperationValidator, SecurityError
|
||||
from mcp_forge.security.audit import AuditLogger
|
||||
from mcp_forge.config.schema import SecurityConfig
|
||||
|
||||
mock_podman_client = MagicMock(spec=PodmanClient)
|
||||
audit_logger = AuditLogger(Path("/tmp/test_audit.log"))
|
||||
validator = OperationValidator(SecurityConfig(audit_log=Path("/tmp/test_audit.log")))
|
||||
|
||||
manager = SecureContainerManager(
|
||||
podman_client=mock_podman_client,
|
||||
validator=validator,
|
||||
audit_logger=audit_logger
|
||||
)
|
||||
|
||||
# Valid mount (session path)
|
||||
config = ContainerConfig(
|
||||
image="mcp-forge/python:3.11",
|
||||
volumes={"/mcp-forge/sessions/test-session-1/workspace": {"bind": "/workspace", "mode": "rw"}}
|
||||
)
|
||||
|
||||
# This should succeed (valid session path)
|
||||
mock_container = MagicMock()
|
||||
mock_container.id = "abc123"
|
||||
mock_podman_client.client.containers.create.return_value = mock_container
|
||||
container_id = manager.create_container(config, session_id="test-session-1")
|
||||
assert container_id == "abc123"
|
||||
|
||||
# Invalid mount (forbidden path)
|
||||
config_bad = ContainerConfig(
|
||||
image="mcp-forge/python:3.11",
|
||||
volumes={"/etc/passwd": {"bind": "/tmp/passwd", "mode": "r"}}
|
||||
)
|
||||
|
||||
with pytest.raises(SecurityError):
|
||||
manager.create_container(config_bad, session_id="test-session-1")
|
||||
|
||||
|
||||
def test_start_container_on_session_container_succeeds():
|
||||
"""Test that starting a session container succeeds."""
|
||||
from mcp_forge.podman.containers import SecureContainerManager
|
||||
from mcp_forge.podman.client import PodmanClient
|
||||
from mcp_forge.security.allowlist import OperationValidator
|
||||
from mcp_forge.security.audit import AuditLogger
|
||||
from mcp_forge.config.schema import SecurityConfig
|
||||
|
||||
mock_podman_client = MagicMock(spec=PodmanClient)
|
||||
mock_container = MagicMock()
|
||||
mock_podman_client.client.containers.get.return_value = mock_container
|
||||
|
||||
audit_logger = AuditLogger(Path("/tmp/test_audit.log"))
|
||||
validator = OperationValidator(SecurityConfig(audit_log=Path("/tmp/test_audit.log")))
|
||||
|
||||
# Register container with validator
|
||||
validator.register_session_container("abc123")
|
||||
|
||||
manager = SecureContainerManager(
|
||||
podman_client=mock_podman_client,
|
||||
validator=validator,
|
||||
audit_logger=audit_logger
|
||||
)
|
||||
|
||||
manager.start_container("abc123")
|
||||
mock_container.start.assert_called_once()
|
||||
|
||||
|
||||
def test_start_container_on_non_session_container_raises_security_error():
|
||||
"""Test that starting a non-session container raises SecurityError."""
|
||||
from mcp_forge.podman.containers import SecureContainerManager
|
||||
from mcp_forge.podman.client import PodmanClient
|
||||
from mcp_forge.security.allowlist import OperationValidator, SecurityError
|
||||
from mcp_forge.security.audit import AuditLogger
|
||||
from mcp_forge.config.schema import SecurityConfig
|
||||
|
||||
mock_podman_client = MagicMock(spec=PodmanClient)
|
||||
audit_logger = AuditLogger(Path("/tmp/test_audit.log"))
|
||||
validator = OperationValidator(SecurityConfig(audit_log=Path("/tmp/test_audit.log")))
|
||||
|
||||
manager = SecureContainerManager(
|
||||
podman_client=mock_podman_client,
|
||||
validator=validator,
|
||||
audit_logger=audit_logger
|
||||
)
|
||||
|
||||
# Try to start container not registered with validator
|
||||
with pytest.raises(SecurityError):
|
||||
manager.start_container("unknown123")
|
||||
|
||||
|
||||
def test_stop_container_works():
|
||||
"""Test that stopping a container works."""
|
||||
from mcp_forge.podman.containers import SecureContainerManager
|
||||
from mcp_forge.podman.client import PodmanClient
|
||||
from mcp_forge.security.allowlist import OperationValidator
|
||||
from mcp_forge.security.audit import AuditLogger
|
||||
from mcp_forge.config.schema import SecurityConfig
|
||||
|
||||
mock_podman_client = MagicMock(spec=PodmanClient)
|
||||
mock_container = MagicMock()
|
||||
mock_podman_client.client.containers.get.return_value = mock_container
|
||||
|
||||
audit_logger = AuditLogger(Path("/tmp/test_audit.log"))
|
||||
validator = OperationValidator(SecurityConfig(audit_log=Path("/tmp/test_audit.log")))
|
||||
|
||||
# Register container
|
||||
validator.register_session_container("abc123")
|
||||
|
||||
manager = SecureContainerManager(
|
||||
podman_client=mock_podman_client,
|
||||
validator=validator,
|
||||
audit_logger=audit_logger
|
||||
)
|
||||
|
||||
manager.stop_container("abc123", timeout=10)
|
||||
mock_container.stop.assert_called_once_with(timeout=10)
|
||||
|
||||
|
||||
def test_remove_container_works():
|
||||
"""Test that removing a container works."""
|
||||
from mcp_forge.podman.containers import SecureContainerManager
|
||||
from mcp_forge.podman.client import PodmanClient
|
||||
from mcp_forge.security.allowlist import OperationValidator
|
||||
from mcp_forge.security.audit import AuditLogger
|
||||
from mcp_forge.config.schema import SecurityConfig
|
||||
|
||||
mock_podman_client = MagicMock(spec=PodmanClient)
|
||||
mock_container = MagicMock()
|
||||
mock_podman_client.client.containers.get.return_value = mock_container
|
||||
|
||||
audit_logger = AuditLogger(Path("/tmp/test_audit.log"))
|
||||
validator = OperationValidator(SecurityConfig(audit_log=Path("/tmp/test_audit.log")))
|
||||
|
||||
# Register container
|
||||
validator.register_session_container("abc123")
|
||||
|
||||
manager = SecureContainerManager(
|
||||
podman_client=mock_podman_client,
|
||||
validator=validator,
|
||||
audit_logger=audit_logger
|
||||
)
|
||||
|
||||
manager.remove_container("abc123", force=True)
|
||||
mock_container.remove.assert_called_once_with(force=True)
|
||||
|
||||
|
||||
def test_cleanup_old_containers():
|
||||
"""Test cleanup of old containers."""
|
||||
from mcp_forge.podman.containers import SecureContainerManager
|
||||
from mcp_forge.podman.client import PodmanClient
|
||||
from mcp_forge.security.allowlist import OperationValidator
|
||||
from mcp_forge.security.audit import AuditLogger
|
||||
from mcp_forge.config.schema import SecurityConfig
|
||||
|
||||
mock_podman_client = MagicMock(spec=PodmanClient)
|
||||
|
||||
# Create mock old and new containers
|
||||
old_container = MagicMock()
|
||||
old_container.id = "old123"
|
||||
old_container.attrs = {
|
||||
"Created": (datetime.now() - timedelta(hours=25)).isoformat(),
|
||||
"Labels": {"mcp-forge.session": "old-session"}
|
||||
}
|
||||
|
||||
new_container = MagicMock()
|
||||
new_container.id = "new123"
|
||||
new_container.attrs = {
|
||||
"Created": datetime.now().isoformat(),
|
||||
"Labels": {"mcp-forge.session": "new-session"}
|
||||
}
|
||||
|
||||
mock_podman_client.client.containers.list.return_value = [old_container, new_container]
|
||||
|
||||
audit_logger = AuditLogger(Path("/tmp/test_audit.log"))
|
||||
validator = OperationValidator(SecurityConfig(audit_log=Path("/tmp/test_audit.log")))
|
||||
|
||||
manager = SecureContainerManager(
|
||||
podman_client=mock_podman_client,
|
||||
validator=validator,
|
||||
audit_logger=audit_logger
|
||||
)
|
||||
|
||||
count = manager.cleanup_old_containers(max_age=timedelta(hours=24))
|
||||
|
||||
assert count == 1
|
||||
old_container.remove.assert_called_once_with(force=True)
|
||||
new_container.remove.assert_not_called()
|
||||
|
||||
|
||||
def test_get_container_logs():
|
||||
"""Test getting container logs."""
|
||||
from mcp_forge.podman.containers import SecureContainerManager
|
||||
from mcp_forge.podman.client import PodmanClient
|
||||
from mcp_forge.security.allowlist import OperationValidator
|
||||
from mcp_forge.security.audit import AuditLogger
|
||||
from mcp_forge.config.schema import SecurityConfig
|
||||
|
||||
mock_podman_client = MagicMock(spec=PodmanClient)
|
||||
mock_container = MagicMock()
|
||||
mock_container.logs.return_value = b"stdout output\nstderr output"
|
||||
mock_podman_client.client.containers.get.return_value = mock_container
|
||||
|
||||
audit_logger = AuditLogger(Path("/tmp/test_audit.log"))
|
||||
validator = OperationValidator(SecurityConfig(audit_log=Path("/tmp/test_audit.log")))
|
||||
|
||||
# Register container
|
||||
validator.register_session_container("abc123")
|
||||
|
||||
manager = SecureContainerManager(
|
||||
podman_client=mock_podman_client,
|
||||
validator=validator,
|
||||
audit_logger=audit_logger
|
||||
)
|
||||
|
||||
stdout, stderr = manager.get_container_logs("abc123", tail=100)
|
||||
|
||||
assert "output" in stdout or "output" in stderr
|
||||
mock_container.logs.assert_called()
|
||||
|
||||
|
||||
def test_wait_for_container():
|
||||
"""Test waiting for container to exit."""
|
||||
from mcp_forge.podman.containers import SecureContainerManager
|
||||
from mcp_forge.podman.client import PodmanClient
|
||||
from mcp_forge.security.allowlist import OperationValidator
|
||||
from mcp_forge.security.audit import AuditLogger
|
||||
from mcp_forge.config.schema import SecurityConfig
|
||||
|
||||
mock_podman_client = MagicMock(spec=PodmanClient)
|
||||
mock_container = MagicMock()
|
||||
mock_container.wait.return_value = {"StatusCode": 0}
|
||||
mock_podman_client.client.containers.get.return_value = mock_container
|
||||
|
||||
audit_logger = AuditLogger(Path("/tmp/test_audit.log"))
|
||||
validator = OperationValidator(SecurityConfig(audit_log=Path("/tmp/test_audit.log")))
|
||||
|
||||
# Register container
|
||||
validator.register_session_container("abc123")
|
||||
|
||||
manager = SecureContainerManager(
|
||||
podman_client=mock_podman_client,
|
||||
validator=validator,
|
||||
audit_logger=audit_logger
|
||||
)
|
||||
|
||||
exit_code = manager.wait_for_container("abc123", timeout=300)
|
||||
|
||||
assert exit_code == 0
|
||||
mock_container.wait.assert_called_once_with(timeout=300)
|
||||
|
||||
|
||||
def test_container_config_to_podman_params_includes_all_security_settings():
|
||||
"""Test that ContainerConfig.to_podman_params includes all required settings."""
|
||||
from mcp_forge.podman.containers import ContainerConfig
|
||||
from mcp_forge.security.resource_limits import ResourceLimits
|
||||
|
||||
config = ContainerConfig(
|
||||
image="mcp-forge/python:3.11",
|
||||
command=["python", "-m", "test"],
|
||||
environment={"VAR1": "value1"},
|
||||
volumes={"/mcp-forge/sessions/sess-1/work": {"bind": "/workspace", "mode": "rw"}},
|
||||
resource_limits=ResourceLimits(memory="512m", storage="1g", cpu_quota=100000),
|
||||
working_dir="/workspace",
|
||||
user="1000:1000"
|
||||
)
|
||||
|
||||
params = config.to_podman_params()
|
||||
|
||||
# Required security settings
|
||||
assert params["network_mode"] == "none"
|
||||
assert params["read_only"] is True
|
||||
assert "no-new-privileges" in params["security_opt"]
|
||||
assert params["user"] == "1000:1000"
|
||||
|
||||
# Configuration passthrough
|
||||
assert params["image"] == "mcp-forge/python:3.11"
|
||||
assert params["command"] == ["python", "-m", "test"]
|
||||
assert params["environment"] == {"VAR1": "value1"}
|
||||
assert params["working_dir"] == "/workspace"
|
||||
|
||||
# Resource limits
|
||||
assert "mem_limit" in params
|
||||
assert "cpu_quota" in params
|
||||
Loading…
Add table
Add a link
Reference in a new issue