diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..d21bbcc --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,34 @@ +# Repository Guidelines + +## Project Structure & Module Organization +Core application code lives in `src/aegis_gitea_mcp/`: +- `server.py` contains FastAPI routes and MCP/SSE endpoints. +- `auth.py`, `config.py`, `audit.py`, and `gitea_client.py` handle security, settings, logging, and Gitea API access. +- `tools/` contains MCP tool implementations (for example `tools/repository.py`). + +Tests are in `tests/` and follow module-level coverage for auth, config, server, and integration flows. Utility scripts (key generation/rotation checks) are in `scripts/`. Container assets are in `docker/` with runtime orchestration in `docker-compose.yml`. + +## Build, Test, and Development Commands +- `make install`: install runtime dependencies. +- `make install-dev`: install dev dependencies and pre-commit hooks. +- `make run`: run the server locally (`python -m aegis_gitea_mcp.server`). +- `make test`: run pytest with coverage output. +- `make lint`: run `ruff` + `mypy`. +- `make format`: run `black` and auto-fix lint issues. +- `make docker-up` / `make docker-down`: start/stop local container stack. + +## Coding Style & Naming Conventions +Use Python 3.10+ with 4-space indentation and type hints for production code. Keep lines within 100 chars (Black/Ruff setting). Modules and functions use `snake_case`; classes use `PascalCase`; constants use `UPPER_SNAKE_CASE`. Prefer explicit exceptions and preserve exception chaining (`raise ... from exc`) when wrapping errors. + +## Testing Guidelines +Framework: `pytest` with `pytest-asyncio` and coverage (`--cov=aegis_gitea_mcp`). Place tests under `tests/` using `test_*.py` naming and `test_*` function names. Add or update tests for behavior changes, especially around authentication, API error paths, and MCP tool responses. + +## Commit & Pull Request Guidelines +Prefer Conventional Commit style used in history (`feat:`, `fix:`, `docs:`, `test:`). Keep subjects imperative and specific (avoid messages like `update` or `quick fix`). PRs should include: +- what changed and why, +- linked issue(s) if available, +- test/lint evidence (`make test`, `make lint`), +- notes for config/security impact when touching auth, keys, or `.env` behavior. + +## Security & Configuration Tips +Never commit secrets. Use `.env` (see `.env.example`) for `GITEA_TOKEN` and `MCP_API_KEYS`. Use `scripts/generate_api_key.py`, `scripts/rotate_api_key.py`, and `scripts/check_key_age.py` for API key lifecycle management. diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md deleted file mode 100644 index ae987a0..0000000 --- a/ARCHITECTURE.md +++ /dev/null @@ -1,456 +0,0 @@ -# AegisGitea MCP - Architecture Documentation - ---- - -## System Overview - -``` -┌─────────────────────────────────────────────────────────────────────┐ -│ ChatGPT Business │ -│ (AI Assistant Interface) │ -│ │ -│ User: "Show me the files in my-repo" │ -└────────────────────────────┬────────────────────────────────────────┘ - │ HTTPS (MCP over SSE) - │ Tool: get_file_tree(owner, repo) - ▼ -┌─────────────────────────────────────────────────────────────────────┐ -│ Reverse Proxy (Traefik/Nginx) │ -│ TLS Termination │ -└────────────────────────────┬────────────────────────────────────────┘ - │ HTTP - ▼ -┌─────────────────────────────────────────────────────────────────────┐ -│ AegisGitea MCP Server (Docker) │ -│ │ -│ ┌───────────────────────────────────────────────────────────────┐ │ -│ │ FastAPI Application │ │ -│ │ │ │ -│ │ Endpoints: │ │ -│ │ - GET /health (Health check) │ │ -│ │ - GET /mcp/tools (List available tools) │ │ -│ │ - POST /mcp/tool/call (Execute tool) │ │ -│ │ - GET /mcp/sse (Server-sent events) │ │ -│ └───────────────────────┬───────────────────────────────────────┘ │ -│ │ │ -│ ┌───────────────────────┴───────────────────────────────────────┐ │ -│ │ MCP Protocol Handler │ │ -│ │ - Tool validation │ │ -│ │ - Request/response mapping │ │ -│ │ - Correlation ID management │ │ -│ └───────────────────────┬───────────────────────────────────────┘ │ -│ │ │ -│ ┌───────────────────────┴───────────────────────────────────────┐ │ -│ │ Tool Implementations │ │ -│ │ │ │ -│ │ - list_repositories() - get_repository_info() │ │ -│ │ - get_file_tree() - get_file_contents() │ │ -│ └───────────────────────┬───────────────────────────────────────┘ │ -│ │ │ -│ ┌──────────────┬────────┴────────┬─────────────────────────────┐ │ -│ │ │ │ │ │ -│ │ ┌───────────▼───────┐ ┌─────▼──────┐ ┌────────────────┐ │ │ -│ │ │ Gitea Client │ │ Config │ │ Audit Logger │ │ │ -│ │ │ - Auth │ │ Manager │ │ - Structured │ │ │ -│ │ │ - API calls │ │ - Env vars│ │ - JSON logs │ │ │ -│ │ │ - Error handling│ │ - Defaults│ │ - Correlation │ │ │ -│ │ └───────────┬───────┘ └────────────┘ └────────┬───────┘ │ │ -│ │ │ │ │ │ -│ └──────────────┼────────────────────────────────────┼─────────┘ │ -│ │ │ │ -└─────────────────┼────────────────────────────────────┼───────────┘ - │ Gitea API │ - │ (Authorization: token XXX) │ Audit Logs - ▼ ▼ -┌─────────────────────────────────────┐ ┌──────────────────────────┐ -│ Gitea Instance │ │ Persistent Volume │ -│ (Self-hosted VCS) │ │ /var/log/aegis-mcp/ │ -│ │ │ audit.log │ -│ Repositories: │ └──────────────────────────┘ -│ ┌─────────────────────────────┐ │ -│ │ org/repo-1 (bot has access)│ │ -│ │ org/repo-2 (bot has access)│ │ -│ │ org/private (NO ACCESS) │ │ -│ └─────────────────────────────┘ │ -│ │ -│ Bot User: aegis-bot │ -│ Permissions: Read-only │ -└─────────────────────────────────────┘ -``` - ---- - -## Component Responsibilities - -### 1. ChatGPT (External) -**Responsibility**: Initiate explicit tool calls based on user requests - -- Receives MCP tool definitions -- Constructs tool call requests -- Presents results to user -- Human-in-the-loop decision making - -### 2. Reverse Proxy -**Responsibility**: TLS termination and routing - -- Terminates HTTPS connections -- Routes to MCP server container -- Handles SSL certificates -- Optional: IP filtering, rate limiting - -### 3. AegisGitea MCP Server (Core) -**Responsibility**: MCP protocol implementation and policy enforcement - -#### 3a. FastAPI Application -- HTTP server with async support -- Server-Sent Events endpoint -- Health and status endpoints -- Request routing - -#### 3b. MCP Protocol Handler -- Tool definition management -- Request validation -- Response formatting -- Correlation ID tracking - -#### 3c. Tool Implementations -- Repository discovery -- File tree navigation -- File content retrieval -- Bounded, single-purpose operations - -#### 3d. Gitea Client -- Async HTTP client for Gitea API -- Bot user authentication -- Error handling and retries -- Response parsing - -#### 3e. Config Manager -- Environment variable loading -- Validation with Pydantic -- Default values -- Type safety - -#### 3f. Audit Logger -- Structured JSON logging -- Correlation ID tracking -- Timestamp (UTC) -- Append-only logs - -### 4. Gitea Instance -**Responsibility**: Authorization and data storage - -- Source of truth for permissions -- Repository data storage -- Bot user management -- Access control enforcement - -### 5. Persistent Volume -**Responsibility**: Audit log storage - -- Durable storage for audit logs -- Survives container restarts -- Accessible for review/analysis - ---- - -## Data Flow: Tool Invocation - -``` -1. User Request - ├─> "Show me files in org/my-repo" - └─> ChatGPT decides to call: get_file_tree(owner="org", repo="my-repo") - -2. MCP Request - ├─> POST /mcp/tool/call - ├─> Body: {"tool": "get_file_tree", "arguments": {"owner": "org", "repo": "my-repo"}} - └─> Generate correlation_id: uuid4() - -3. Audit Log (Entry) - ├─> Log: tool_invocation - ├─> tool_name: "get_file_tree" - ├─> repository: "org/my-repo" - └─> status: "pending" - -4. Gitea API Call - ├─> GET /api/v1/repos/org/my-repo/git/trees/main - ├─> Header: Authorization: token XXX - └─> Response: {"tree": [...files...]} - -5. Authorization Check - ├─> 200 OK → Bot has access - ├─> 403 Forbidden → Log access_denied, raise error - └─> 404 Not Found → Repository doesn't exist or no access - -6. Response Processing - ├─> Extract file tree - ├─> Transform to simplified format - └─> Apply size/count limits - -7. Audit Log (Success) - ├─> Log: tool_invocation - ├─> status: "success" - └─> params: {"count": 42} - -8. MCP Response - ├─> 200 OK - ├─> Body: {"success": true, "result": {...files...}} - └─> correlation_id: same as request - -9. ChatGPT Processing - ├─> Receive file tree data - ├─> Format for user presentation - └─> "Here are the files in org/my-repo: ..." -``` - ---- - -## Security Boundaries - -``` -┌───────────────────────────────────────────────────────────────┐ -│ Trust Boundary 1 │ -│ (Internet ↔ MCP Server) │ -│ │ -│ Controls: │ -│ - HTTPS/TLS encryption │ -│ - Reverse proxy authentication (optional) │ -│ - Rate limiting │ -│ - Firewall rules │ -└───────────────────────────────────────────────────────────────┘ - -┌───────────────────────────────────────────────────────────────┐ -│ Trust Boundary 2 │ -│ (MCP Server ↔ Gitea API) │ -│ │ -│ Controls: │ -│ - Bot user token authentication │ -│ - Gitea's access control (authoritative) │ -│ - API request timeouts │ -│ - Input validation │ -└───────────────────────────────────────────────────────────────┘ - -┌───────────────────────────────────────────────────────────────┐ -│ Trust Boundary 3 │ -│ (Container ↔ Host System) │ -│ │ -│ Controls: │ -│ - Non-root container user │ -│ - Resource limits (CPU, memory) │ -│ - No new privileges │ -│ - Read-only filesystem (where possible) │ -└───────────────────────────────────────────────────────────────┘ -``` - ---- - -## Authorization Flow - -``` -┌──────────────────────────────────────────────────────────────────┐ -│ AI requests access to "org/private-repo" │ -└────────────────────────┬─────────────────────────────────────────┘ - │ - ▼ - ┌───────────────────────────────────────┐ - │ MCP Server: Forward to Gitea API │ - │ with bot user token │ - └───────────────┬───────────────────────┘ - │ - ▼ - ┌───────────────────────────────────────┐ - │ Gitea: Check bot user permissions │ - │ for "org/private-repo" │ - └───────────────┬───────────────────────┘ - │ - ┌───────┴────────┐ - │ │ - Bot is collaborator? │ - │ │ - ┌────────▼─────┐ ┌──────▼──────┐ - │ YES │ │ NO │ - │ (Read access)│ │ (No access) │ - └────────┬─────┘ └──────┬──────┘ - │ │ - ▼ ▼ - ┌───────────────┐ ┌─────────────────┐ - │ Return data │ │ Return 403 │ - │ Log: success │ │ Log: denied │ - └───────────────┘ └─────────────────┘ -``` - -**Key Insight**: The MCP server never makes authorization decisions - it only forwards requests and respects Gitea's response. - ---- - -## Failure Modes & Handling - -### 1. Gitea Unavailable -- **Detection**: HTTP connection error -- **Response**: Return error to ChatGPT -- **Logging**: Log connection failure -- **Recovery**: Automatic retry on next request - -### 2. Invalid Bot Token -- **Detection**: 401 Unauthorized from Gitea -- **Response**: Log security event, return auth error -- **Logging**: High-severity security log -- **Recovery**: Operator must rotate token - -### 3. Bot Lacks Permission -- **Detection**: 403 Forbidden from Gitea -- **Response**: Return authorization error -- **Logging**: Access denied event -- **Recovery**: Grant permission in Gitea UI - -### 4. File Too Large -- **Detection**: File size exceeds MAX_FILE_SIZE_BYTES -- **Response**: Return size limit error -- **Logging**: Security event (potential abuse) -- **Recovery**: Increase limit or reject request - -### 5. Network Timeout -- **Detection**: Request exceeds REQUEST_TIMEOUT_SECONDS -- **Response**: Return timeout error -- **Logging**: Log timeout event -- **Recovery**: Automatic retry possible - -### 6. Rate Limit Exceeded -- **Detection**: Too many requests per minute -- **Response**: Return 429 Too Many Requests -- **Logging**: Log rate limit event -- **Recovery**: Wait and retry - ---- - -## Scaling Considerations - -### Vertical Scaling (Single Instance) -- **Current**: 128-512 MB RAM, minimal CPU -- **Bottleneck**: Gitea API response time -- **Max throughput**: ~100-200 requests/second - -### Horizontal Scaling (Multiple Instances) -- **Stateless design**: Each instance independent -- **Load balancing**: Standard HTTP load balancer -- **Shared state**: None (all state in Gitea) -- **Audit logs**: Each instance writes to own log (or use centralized logging) - -### Performance Optimization (Future) -- Add Redis caching layer -- Implement connection pooling -- Use HTTP/2 for Gitea API -- Batch multiple file reads - ---- - -## Observability - -### Metrics to Monitor -1. **Request rate**: Requests per minute -2. **Error rate**: Failed requests / total requests -3. **Response time**: P50, P95, P99 latency -4. **Gitea API health**: Success rate to Gitea -5. **Auth failures**: 401/403 responses - -### Logs to Track -1. **Audit logs**: Every tool invocation -2. **Access denied**: Permission violations -3. **Security events**: Rate limits, size limits -4. **Errors**: Exceptions and failures - -### Alerts to Configure -1. **High error rate**: > 5% errors -2. **Auth failures**: Any 401 responses -3. **Gitea unreachable**: Connection failures -4. **Disk space**: Audit logs filling disk - ---- - -## Future Enhancements - -### Phase 2: Extended Context -``` -New Tools: -├── get_commits(owner, repo, limit) -├── get_commit_diff(owner, repo, sha) -├── list_issues(owner, repo) -├── get_issue(owner, repo, number) -├── list_pull_requests(owner, repo) -└── get_pull_request(owner, repo, number) -``` - -### Phase 3: Advanced Features -``` -Capabilities: -├── Caching layer (Redis) -├── Webhook support for real-time updates -├── OAuth2 flow instead of static tokens -├── Per-client rate limiting -├── Multi-tenant support (multiple bot users) -└── GraphQL API for more efficient queries -``` - ---- - -## Deployment Patterns - -### Pattern 1: Single Homelab Instance -``` -[Homelab Server] -├── Gitea container -├── AegisGitea MCP container -└── Caddy reverse proxy - └── Exposes HTTPS endpoint -``` - -### Pattern 2: Kubernetes Deployment -``` -[Kubernetes Cluster] -├── Namespace: aegis-mcp -├── Deployment: aegis-mcp (3 replicas) -├── Service: ClusterIP -├── Ingress: HTTPS with cert-manager -└── PersistentVolume: Audit logs -``` - -### Pattern 3: Cloud Deployment -``` -[AWS/GCP/Azure] -├── Container service (ECS/Cloud Run/ACI) -├── Load balancer (ALB/Cloud Load Balancing) -├── Secrets manager (Secrets Manager/Secret Manager/Key Vault) -└── Log aggregation (CloudWatch/Cloud Logging/Monitor) -``` - ---- - -## Testing Strategy - -### Unit Tests -- Configuration loading -- Gitea client methods -- Tool implementations -- Audit logging - -### Integration Tests -- Full MCP protocol flow -- Gitea API interactions (mocked) -- Error handling paths - -### End-to-End Tests -- Real Gitea instance -- Real bot user -- Real tool invocations - ---- - -## Conclusion - -This architecture prioritizes: -1. **Security**: Read-only, auditable, fail-safe -2. **Simplicity**: Straightforward data flow -3. **Maintainability**: Clear separation of concerns -4. **Observability**: Comprehensive logging - -The design is intentionally boring and predictable - perfect for a security-critical system. diff --git a/AUTHENTICATION_IMPLEMENTATION_SUMMARY.md b/AUTHENTICATION_IMPLEMENTATION_SUMMARY.md deleted file mode 100644 index 4f7dc24..0000000 --- a/AUTHENTICATION_IMPLEMENTATION_SUMMARY.md +++ /dev/null @@ -1,408 +0,0 @@ -# Authentication System Implementation Summary - -**Branch:** `feature/authentication-system` -**Status:** ✅ Complete and Pushed -**Commit:** `eeaad74` - ---- - -## 🎉 What Was Built - -A complete API key authentication system that ensures only YOUR ChatGPT Business workspace can access your self-hosted Gitea MCP server. - ---- - -## 📦 Files Created - -### Core Authentication Module -- **`src/aegis_gitea_mcp/auth.py`** (215 lines) - - `APIKeyValidator` class with constant-time comparison - - Rate limiting (5 failures per IP per 5 minutes) - - Failed attempt tracking - - Bearer token extraction and validation - - `generate_api_key()` function for secure key generation - -### Key Management Scripts -- **`scripts/generate_api_key.py`** (125 lines) - - Interactive key generation wizard - - Metadata tracking (description, creation date, expiration) - - .env configuration snippet output - - Optional metadata file storage - -- **`scripts/rotate_api_key.py`** (155 lines) - - Guided key rotation with automatic backup - - Grace period support (multi-key transition) - - Old .env backup before changes - - Step-by-step instructions - -- **`scripts/check_key_age.py`** (130 lines) - - Automated expiration monitoring - - Warning system (14 days, 7 days, expired) - - Cron-ready exit codes - - Metadata file parsing - -### Documentation -- **`AUTH_SETUP.md`** (620 lines) - - Complete authentication setup guide - - Security features overview - - Troubleshooting guide - - Monitoring and best practices - -- **`CHATGPT_SETUP.md`** (520 lines) - - ChatGPT Business integration guide - - Step-by-step configuration - - Example commands to try - - Multi-user setup instructions - -- **`KEY_ROTATION.md`** (600 lines) - - Automated rotation procedures - - Manual rotation step-by-step - - Emergency rotation (compromised key) - - Multi-user rotation strategies - ---- - -## 🔧 Files Modified - -### Configuration -- **`src/aegis_gitea_mcp/config.py`** - - Added `auth_enabled: bool` (default: True) - - Added `mcp_api_keys: List[str]` (comma-separated parsing) - - Added `max_auth_failures: int` (default: 5) - - Added `auth_failure_window: int` (default: 300 seconds) - - Validation: Keys must be at least 32 characters - - Validation: At least one key required if auth enabled - -### Server -- **`src/aegis_gitea_mcp/server.py`** - - Added authentication middleware - - Validates all `/mcp/*` endpoints (excludes `/health` and `/`) - - Extracts Bearer token from Authorization header - - Returns 401 with helpful error messages on auth failure - - Logs authentication status on startup - -### Infrastructure -- **`docker-compose.yml`** - - Added Traefik labels for automatic HTTPS - - Added rate limiting middleware (60 req/min per IP) - - Added security headers (HSTS, CSP, X-Frame-Options) - - Connected to external Traefik network - - Added `MCP_DOMAIN` environment variable support - -- **`.env.example`** - - Added `AUTH_ENABLED` (default: true) - - Added `MCP_API_KEYS` (required if auth enabled) - - Added `MCP_DOMAIN` (for Traefik routing) - - Added `MAX_AUTH_FAILURES` (default: 5) - - Added `AUTH_FAILURE_WINDOW` (default: 300) - - Documented multi-key configuration - -- **`.gitignore`** - - Added `keys/` (metadata storage) - - Added `.env.backup-*` (rotation backups) - - Added `*.key` (key files) - -- **`Makefile`** - - Added `make generate-key` command - - Added `make rotate-key` command - - Added `make check-key-age` command - - Updated help text - ---- - -## 🔐 Security Features Implemented - -### 1. Authentication -- ✅ Bearer token validation -- ✅ Constant-time key comparison (prevents timing attacks) -- ✅ Multi-key support (rotation grace periods) -- ✅ Minimum 32-character keys (64 recommended) -- ✅ No authentication on health checks (monitoring-friendly) - -### 2. Rate Limiting -- ✅ 5 failed attempts per IP before blocking -- ✅ 5-minute time window (configurable) -- ✅ In-memory tracking (resets on restart) -- ✅ High-severity security events logged - -### 3. Audit Logging -- ✅ All auth attempts logged (success and failure) -- ✅ Client IP and user agent captured -- ✅ Key hints logged (first 8 + last 4 chars only) -- ✅ Correlation IDs for request tracking -- ✅ Timestamps in UTC - -### 4. Network Security -- ✅ Traefik labels for automatic HTTPS -- ✅ Security headers (HSTS, X-Frame-Options, CSP) -- ✅ Rate limiting at proxy level (60/min per IP) -- ✅ External network isolation - ---- - -## 📊 Statistics - -| Metric | Value | -|--------|-------| -| **Total Lines Added** | ~2,263 | -| **Python Code** | ~900 lines | -| **Documentation** | ~1,740 lines | -| **Scripts** | 3 | -| **Docs** | 3 | -| **Config Changes** | 5 files | -| **New Modules** | 1 (auth.py) | - ---- - -## 🚀 Quick Start (For Users) - -### Step 1: Generate API Key -```bash -make generate-key -``` - -### Step 2: Add to .env -```bash -echo "MCP_API_KEYS=" >> .env -``` - -### Step 3: Restart Server -```bash -docker-compose restart aegis-mcp -``` - -### Step 4: Configure ChatGPT Business -- Go to ChatGPT Settings > MCP Servers -- Add custom header: - ``` - Authorization: Bearer - ``` - -### Step 5: Test -``` -Ask ChatGPT: "List my Gitea repositories" -``` - ---- - -## 🧪 Testing Checklist - -### Manual Testing Required - -Before merging to main: - -- [ ] Generate API key with `make generate-key` -- [ ] Add key to `.env` file -- [ ] Start server: `docker-compose up -d` -- [ ] Test without key (should return 401): - ```bash - curl https://mcp.yourdomain.com/mcp/tools - ``` -- [ ] Test with invalid key (should return 401): - ```bash - curl -H "Authorization: Bearer invalid-key" https://mcp.yourdomain.com/mcp/tools - ``` -- [ ] Test with valid key (should return 200): - ```bash - curl -H "Authorization: Bearer " https://mcp.yourdomain.com/mcp/tools - ``` -- [ ] Test rate limiting (6 failed attempts, should block) -- [ ] Test key rotation with `make rotate-key` -- [ ] Test key age check with `make check-key-age` -- [ ] Configure ChatGPT and test actual usage -- [ ] Check audit logs show auth events: - ```bash - docker-compose exec aegis-mcp cat /var/log/aegis-mcp/audit.log | grep auth - ``` - ---- - -## 📝 Migration Guide (For Existing Installations) - -### Breaking Changes - -1. **Authentication now enabled by default** - - Old installations without `MCP_API_KEYS` will fail to start - - Must generate and configure API key - -2. **Environment variable required** - - `MCP_API_KEYS` must be set if `AUTH_ENABLED=true` (default) - - Minimum 32 characters, 64 recommended - -### Migration Steps - -```bash -# 1. Pull latest code -git pull origin feature/authentication-system - -# 2. Generate API key -make generate-key - -# 3. Update .env -echo "MCP_API_KEYS=" >> .env - -# 4. Restart -docker-compose down -docker-compose up -d - -# 5. Update ChatGPT configuration -# Add Authorization header in ChatGPT settings - -# 6. Verify -curl -H "Authorization: Bearer " https://mcp.yourdomain.com/mcp/tools -``` - -### Rollback Plan - -If issues arise: - -```bash -# Temporarily disable authentication -echo "AUTH_ENABLED=false" >> .env -docker-compose restart aegis-mcp - -# Server will log warning but allow all requests -# Fix configuration, then re-enable auth -``` - ---- - -## 🔍 Code Review Notes - -### Architecture Decisions - -1. **Bearer Token over OAuth2** - - Simpler for single-user/small team - - ChatGPT Business compatible - - Easy to rotate and manage - -2. **In-Memory Rate Limiting** - - Sufficient for single-instance deployment - - Resets on restart (acceptable tradeoff) - - Can be upgraded to Redis if needed - -3. **Plaintext Keys in .env** - - Current: Keys stored in plaintext in `.env` - - Future: Can add hashing with minimal refactoring - - Mitigation: File permissions, container isolation - -4. **No Database for Key Metadata** - - Optional metadata files in `keys/` directory - - Simple, no additional dependencies - - Easy to backup and version control (metadata only) - -### Security Considerations - -- ✅ Constant-time comparison prevents timing attacks -- ✅ Keys never logged in full (only hints) -- ✅ Rate limiting prevents brute force -- ✅ Audit logging for accountability -- ⚠️ Keys in `.env` require filesystem protection -- ⚠️ In-memory rate limits reset on restart - -### Performance Impact - -- Negligible (< 1ms per request for auth check) -- Constant-time comparison slightly slower than `==` but necessary -- No database queries needed - ---- - -## 🎯 Success Criteria - -All objectives met: - -- ✅ Only authorized ChatGPT workspaces can access MCP server -- ✅ API keys easy to generate, rotate, and manage -- ✅ Comprehensive audit logging of all auth attempts -- ✅ Zero downtime key rotation support -- ✅ Rate limiting prevents abuse -- ✅ Complete documentation for setup and usage -- ✅ Traefik integration with HTTPS and security headers -- ✅ Multi-key support for team environments -- ✅ Emergency rotation procedures documented - ---- - -## 🚦 Next Steps - -### Immediate (Before Merge) -1. Manual testing of all features -2. Verify documentation accuracy -3. Test on clean installation -4. Update main README.md to reference AUTH_SETUP.md - -### Future Enhancements (Not in This PR) -1. Discord/Slack webhooks for alerts (`alerts.py`) -2. Prometheus metrics endpoint (`metrics.py`) -3. Automated tests for authentication (`tests/test_auth.py`) -4. Key hashing instead of plaintext storage -5. Redis-based rate limiting for multi-instance -6. OAuth2 flow for enterprise environments -7. Web dashboard for key management - ---- - -## 📞 Pull Request Link - -Create PR at: -``` -https://git.hiddenden.cafe/Hiddenden/AegisGitea-MCP/pulls/new/feature/authentication-system -``` - -**Suggested PR Title:** -``` -feat: Add API key authentication for ChatGPT Business exclusive access -``` - -**Suggested PR Description:** -``` -Implements comprehensive Bearer token authentication to ensure only -authorized ChatGPT workspaces can access the self-hosted Gitea MCP server. - -## Features -- API key validation with rate limiting -- Key management scripts (generate, rotate, check age) -- Traefik integration with HTTPS and security headers -- Comprehensive documentation (AUTH_SETUP.md, CHATGPT_SETUP.md, KEY_ROTATION.md) -- Multi-key support for rotation grace periods - -## Security -- Constant-time key comparison -- Failed attempt tracking (5 per IP per 5 min) -- Comprehensive audit logging -- No keys logged in full - -## Breaking Changes -- `MCP_API_KEYS` environment variable now required -- Authentication enabled by default - -## Migration -See AUTHENTICATION_IMPLEMENTATION_SUMMARY.md for complete migration guide. - -## Testing -Manual testing checklist provided in summary document. -``` - ---- - -## 🙏 Acknowledgments - -Built based on requirements for: -- ChatGPT Business workspace exclusive access -- Self-hosted Gitea private instance -- Traefik (Pangolin) reverse proxy -- Security-first, audit-focused design - ---- - -**Status:** ✅ Ready for Review and Testing - -**Branch:** `feature/authentication-system` -**Commit:** `eeaad74` -**Files Changed:** 13 (8 added, 5 modified) -**Lines Added:** ~2,263 - ---- - -**Great work! The authentication system is complete, documented, and pushed to the remote repository.** 🎉 diff --git a/AUTH_SETUP.md b/AUTH_SETUP.md deleted file mode 100644 index c9db9cc..0000000 --- a/AUTH_SETUP.md +++ /dev/null @@ -1,437 +0,0 @@ -# Authentication Setup Guide - -This guide walks you through setting up API key authentication for AegisGitea MCP to ensure only your ChatGPT workspace can access your self-hosted Gitea instance. - ---- - -## Overview - -AegisGitea MCP uses **Bearer Token authentication** to secure access: - -- **API Keys**: Cryptographically secure 64-character tokens -- **Header-based**: Keys sent via `Authorization: Bearer ` header -- **Multi-key support**: Multiple keys for rotation grace periods -- **Rate limiting**: Failed auth attempts trigger IP-based rate limits -- **Audit logging**: All auth attempts logged for security monitoring - ---- - -## Quick Start (5 minutes) - -### 1. Generate API Key - -```bash -# Using Make -make generate-key - -# Or directly -python3 scripts/generate_api_key.py -``` - -This will: -- Generate a secure 64-character API key -- Show you the key (save it immediately!) -- Provide `.env` configuration snippet -- Optionally save metadata (not the key itself) for tracking - -**Example output:** -``` -✓ API Key Generated Successfully! - -API KEY: - a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6q7r8s9t0u1v2w3x4y5z6a7b8c9d0e1f2 - -📋 Next Steps: -1. Add this key to your .env file -2. Restart the MCP server -3. Configure ChatGPT Business -``` - -### 2. Add Key to .env - -```bash -# Edit .env -nano .env - -# Add/update this line: -MCP_API_KEYS=a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6q7r8s9t0u1v2w3x4y5z6a7b8c9d0e1f2 -``` - -### 3. Restart MCP Server - -```bash -docker-compose restart aegis-mcp - -# Verify authentication is enabled -docker-compose logs aegis-mcp | grep "authentication" -``` - -Expected log output: -``` -API key authentication ENABLED (1 key(s) configured) -``` - -### 4. Test Authentication - -```bash -# Without key - should fail with 401 -curl https://mcp.yourdomain.com/mcp/tools - -# With valid key (header) - should succeed -curl -H "Authorization: Bearer YOUR_KEY_HERE" \ - https://mcp.yourdomain.com/mcp/tools - -# With valid key (query param) - should succeed -curl "https://mcp.yourdomain.com/mcp/tools?api_key=YOUR_KEY_HERE" -``` - ---- - -## Configuration Options - -### Environment Variables - -```bash -# Enable/disable authentication -AUTH_ENABLED=true # Set to false ONLY for testing - -# API keys (comma-separated for multiple) -MCP_API_KEYS=key1 # Single key -MCP_API_KEYS=key1,key2,key3 # Multiple keys (rotation grace period) - -# Rate limiting -MAX_AUTH_FAILURES=5 # Max failed attempts before blocking -AUTH_FAILURE_WINDOW=300 # Time window in seconds (5 min) -``` - -### Multiple Keys (Rotation Grace Period) - -During key rotation, you can temporarily allow multiple keys: - -```bash -# Old key -MCP_API_KEYS=old-key-here - -# Add new key (both work) -MCP_API_KEYS=old-key-here,new-key-here - -# After updating ChatGPT, remove old key -MCP_API_KEYS=new-key-here -``` - ---- - -## Security Features - -### 1. Constant-Time Comparison - -Keys are compared using `hmac.compare_digest()` to prevent timing attacks. - -### 2. Rate Limiting - -- **Threshold**: 5 failed attempts per IP -- **Window**: 5 minutes (configurable) -- **Action**: Reject all requests from that IP until window expires -- **Logging**: High-severity security event logged - -### 3. Audit Logging - -Every authentication attempt logs: - -```json -{ - "timestamp": "2026-01-29T12:34:56.789Z", - "event": "api_authentication", - "status": "success", - "client_ip": "203.0.113.42", - "user_agent": "ChatGPT-User/1.0", - "key_hint": "a1b2c3d4...e1f2" -} -``` - -Failed attempts: - -```json -{ - "timestamp": "2026-01-29T12:34:56.789Z", - "event": "access_denied", - "reason": "invalid_api_key", - "client_ip": "203.0.113.42" -} -``` - -### 4. Key Format Validation - -- Minimum length: 32 characters -- Recommended length: 64 characters -- Format: Hexadecimal string (0-9, a-f) - ---- - -## Troubleshooting - -### Issue: "No API keys configured" error - -**Symptoms:** -``` -docker-compose logs: "No API keys configured in environment" -``` - -**Solution:** -1. Check `.env` file exists and contains `MCP_API_KEYS` -2. Ensure no typos (`MCP_API_KEYS` not `MCP_API_KEY`) -3. Verify key is at least 32 characters -4. Restart container after updating `.env` - -### Issue: "Invalid API key" on valid key - -**Possible causes:** - -1. **Whitespace in .env**: Remove spaces around `=` - ```bash - # Wrong - MCP_API_KEYS = key-here - - # Correct - MCP_API_KEYS=key-here - ``` - -2. **Key truncated**: Ensure entire 64-char key is copied - ```bash - # Check key length - echo -n "your-key-here" | wc -c - # Should output: 64 - ``` - -3. **Container not restarted**: Always restart after changing `.env` - ```bash - docker-compose restart aegis-mcp - ``` - -### Issue: Rate limit blocking legitimate requests - -**Symptoms:** -``` -"Too many failed authentication attempts" -``` - -**Solution:** -1. Check audit logs for failed attempts: - ```bash - docker-compose exec aegis-mcp cat /var/log/aegis-mcp/audit.log | grep "invalid_api_key" - ``` - -2. Wait 5 minutes for rate limit to reset - -3. If accidentally blocked yourself: - ```bash - # Restart container (clears in-memory rate limits) - docker-compose restart aegis-mcp - ``` - -4. Adjust rate limit settings in `.env` if needed: - ```bash - MAX_AUTH_FAILURES=10 - AUTH_FAILURE_WINDOW=600 - ``` - -### Issue: ChatGPT can't connect after adding auth - -See [CHATGPT_SETUP.md](CHATGPT_SETUP.md) for detailed ChatGPT configuration. - -**Quick check:** -1. Verify key in ChatGPT settings matches `.env` -2. Check Authorization header format: `Bearer ` (with space) -3. Test manually with curl first - ---- - -## Monitoring Authentication - -### View All Auth Attempts - -```bash -# All auth events -docker-compose exec aegis-mcp grep "api_authentication" /var/log/aegis-mcp/audit.log - -# Failed attempts only -docker-compose exec aegis-mcp grep "access_denied" /var/log/aegis-mcp/audit.log - -# Rate limit triggers -docker-compose exec aegis-mcp grep "auth_rate_limit_exceeded" /var/log/aegis-mcp/audit.log -``` - -### Real-Time Monitoring - -```bash -# Follow auth events -docker-compose exec aegis-mcp tail -f /var/log/aegis-mcp/audit.log | grep "auth" -``` - -### Weekly Summary Script - -```bash -#!/bin/bash -# Save as scripts/auth_summary.sh - -CONTAINER="aegis-gitea-mcp" -LOG_PATH="/var/log/aegis-mcp/audit.log" - -echo "=== Weekly Auth Summary ===" -echo "" -echo "Total auth attempts:" -docker exec $CONTAINER grep "api_authentication" $LOG_PATH | wc -l - -echo "" -echo "Successful:" -docker exec $CONTAINER grep "api_authentication.*success" $LOG_PATH | wc -l - -echo "" -echo "Failed:" -docker exec $CONTAINER grep "access_denied.*invalid_api_key" $LOG_PATH | wc -l - -echo "" -echo "Rate limited IPs:" -docker exec $CONTAINER grep "auth_rate_limit_exceeded" $LOG_PATH | wc -l -``` - ---- - -## Best Practices - -### 1. Key Storage - -- ✅ Store in `.env` file (never commit to git) -- ✅ Use password manager for backup -- ✅ Save key metadata (not key itself) for tracking -- ❌ Never hardcode in application code -- ❌ Never share via unencrypted channels - -### 2. Key Rotation - -- **Schedule**: Every 90 days (automated check available) -- **Method**: Use `make rotate-key` for guided rotation -- **Grace period**: Temporarily allow both old and new keys -- **Verification**: Test new key before removing old key - -See [KEY_ROTATION.md](KEY_ROTATION.md) for detailed rotation procedures. - -### 3. Access Control - -- **Single workspace**: One key for your ChatGPT Business account -- **Multiple users**: One key per user (track with metadata) -- **Revocation**: Remove key from `.env` and restart - -### 4. Monitoring - -- **Weekly**: Review auth logs for anomalies -- **Monthly**: Check key age with `make check-key-age` -- **Quarterly**: Rotate keys -- **Alerts**: Set up notifications for rate limit events - ---- - -## Advanced Configuration - -### Disable Authentication (Testing Only) - -**⚠️ WARNING: Only for isolated test environments** - -```bash -# .env -AUTH_ENABLED=false -``` - -Server will log critical warning: -``` -API key authentication DISABLED - server is open to all requests! -``` - -### Custom Rate Limiting - -```bash -# More lenient (for high-traffic) -MAX_AUTH_FAILURES=20 -AUTH_FAILURE_WINDOW=600 # 10 minutes - -# More strict (for high-security) -MAX_AUTH_FAILURES=3 -AUTH_FAILURE_WINDOW=1800 # 30 minutes -``` - -### Key Hashing (Future Enhancement) - -Currently keys are stored in plaintext in `.env`. Future versions will support hashed keys in database. - -**Current security model:** -- ✅ Keys never logged in full (only first 8 + last 4 chars) -- ✅ `.env` file protected by filesystem permissions -- ✅ Container runs as non-root user -- ⚠️ Ensure `.env` has restrictive permissions: `chmod 600 .env` - ---- - -## Security Checklist - -Before going to production: - -- [ ] API key generated with `make generate-key` (not manually typed) -- [ ] Key is exactly 64 characters -- [ ] `.env` file has restrictive permissions (`chmod 600 .env`) -- [ ] `.env` is in `.gitignore` (verify: `git status` shows no .env) -- [ ] Container restarted after adding key -- [ ] Authentication enabled (check logs for "ENABLED" message) -- [ ] Test curl command succeeds with key, fails without -- [ ] ChatGPT configured with Authorization header -- [ ] Rate limiting tested (try 6 failed attempts) -- [ ] Audit logs capturing auth events -- [ ] Key rotation reminder set (90 days) -- [ ] Backup key stored securely (password manager) - ---- - -## FAQ - -**Q: Can I use the same key for multiple ChatGPT workspaces?** - -A: Yes, but not recommended. Generate separate keys for auditability: -```bash -MCP_API_KEYS=workspace1-key,workspace2-key -``` - -**Q: How do I revoke access immediately?** - -A: Remove key from `.env` and restart: -```bash -# Edit .env (remove old key) -docker-compose restart aegis-mcp -``` - -Access is revoked instantly after restart. - -**Q: Does rate limiting affect valid keys?** - -A: No. Rate limiting only applies to IPs that fail authentication. Valid keys are never rate limited. - -**Q: Can I see which key was used for each request?** - -A: Yes. Audit logs include `key_hint` (first 8 + last 4 characters): -```json -{"key_hint": "a1b2c3d4...e1f2"} -``` - -**Q: What happens if I lose my API key?** - -A: Generate a new one with `make generate-key`, update `.env`, restart server, update ChatGPT config. The old key stops working immediately after restart. - ---- - -## Next Steps - -- [ChatGPT Setup Guide](CHATGPT_SETUP.md) - Configure ChatGPT Business -- [Key Rotation Guide](KEY_ROTATION.md) - Automated rotation procedures -- [Security Policy](SECURITY.md) - Overall security best practices - ---- - -**Need help?** Open an issue in the Gitea repository. diff --git a/CHATGPT_SETUP.md b/CHATGPT_SETUP.md deleted file mode 100644 index 401658f..0000000 --- a/CHATGPT_SETUP.md +++ /dev/null @@ -1,419 +0,0 @@ -# ChatGPT Business Setup Guide - -Complete guide for connecting ChatGPT Business to your secured AegisGitea MCP server. - ---- - -## Prerequisites - -- ✅ AegisGitea MCP server deployed and running -- ✅ API key generated (see [AUTH_SETUP.md](AUTH_SETUP.md)) -- ✅ Traefik configured with HTTPS (or reverse proxy with TLS) -- ✅ ChatGPT Business or Developer subscription - ---- - -## Setup Steps - -### Step 1: Verify MCP Server is Running - -```bash -# Check container status -docker-compose ps - -# Should show "Up" status -aegis-gitea-mcp Up 0.0.0.0:8080->8080/tcp -``` - -### Step 2: Test Authentication - -```bash -# Replace YOUR_API_KEY with your actual key -curl "https://mcp.yourdomain.com/mcp/tools?api_key=YOUR_API_KEY" - -# Expected: JSON response with available tools -# If error: Check AUTH_SETUP.md troubleshooting -``` - -### Step 3: Configure ChatGPT Business - -1. **Open ChatGPT Settings** - - Click your profile icon (bottom left) - - Select "Settings" - - Navigate to "Beta Features" or "Integrations" - -2. **Add MCP Server** - - Look for "Model Context Protocol" or "MCP Servers" - - Click "Add Server" or "+" - -3. **Enter Server Details** - ``` - Name: AegisGitea MCP - URL: https://mcp.yourdomain.com?api_key=YOUR_API_KEY_HERE - Type: HTTP/SSE (Server-Sent Events) - Auth: None (or Mixed, leave OAuth fields empty) - ``` - -4. **Save Configuration** - -### Step 4: Test Connection - -Start a new ChatGPT conversation and try: - -``` -List my Gitea repositories -``` - -**Expected Response:** -``` -I found X repositories in your Gitea instance: - -1. org/repo-name - Description here -2. org/another-repo - Another description -... -``` - -**If it fails**, see Troubleshooting section below. - ---- - -## Verification Checklist - -After setup, verify: - -- [ ] ChatGPT shows "Connected" status for AegisGitea MCP -- [ ] Test command "List my Gitea repositories" works -- [ ] Audit logs show successful authentication: - ```bash - docker-compose logs aegis-mcp | grep "api_authentication.*success" - ``` -- [ ] Can read file contents: - ``` - Show me the README.md file from org/repo-name - ``` -- [ ] Can browse repository structure: - ``` - What files are in the src/ directory of org/repo-name? - ``` - ---- - -## Example Commands - -Once connected, try these commands in ChatGPT: - -### Repository Discovery -``` -What repositories do I have access to? -List all my Gitea repositories -Show me my private repositories -``` - -### Repository Information -``` -Tell me about the org/my-repo repository -What's the default branch of org/my-repo? -When was org/my-repo last updated? -``` - -### File Operations -``` -Show me the file tree of org/my-repo -What files are in the src/ directory of org/my-repo? -Read the README.md file from org/my-repo -Show me the contents of src/main.py in org/my-repo -``` - -### Code Understanding -``` -Explain what the main function does in org/my-repo/src/main.py -Summarize the architecture of org/my-repo -What dependencies does org/my-repo use? -Find all TODO comments in org/my-repo -``` - ---- - -## Troubleshooting - -### Issue: "Cannot connect to MCP server" - -**Check 1: Server is running** -```bash -docker-compose ps -docker-compose logs aegis-mcp -``` - -**Check 2: Domain/URL is correct** -```bash -curl https://mcp.yourdomain.com/health -# Should return: {"status": "healthy"} -``` - -**Check 3: Firewall/Network** -- Ensure port 443 is open -- Check Traefik is routing correctly -- Verify DNS resolves correctly - -### Issue: "Authentication failed" - -**Check 1: MCP Server URL format** - -Correct: -``` -https://mcp.yourdomain.com?api_key=YOUR_KEY -``` - -Wrong: -``` -https://mcp.yourdomain.com (missing api_key) -https://mcp.yourdomain.com?api_key= (empty key) -https://mcp.yourdomain.com?api_key=short (too short) -``` - -**Check 2: Key matches .env** -```bash -# Show configured keys (first 12 chars only for security) -docker-compose exec aegis-mcp printenv MCP_API_KEYS | cut -c1-12 -``` - -Compare with your ChatGPT key (first 12 chars). - -**Check 3: Server logs** -```bash -docker-compose logs aegis-mcp | tail -50 -``` - -Look for: -- `invalid_api_key` → Key doesn't match -- `missing_api_key` → api_key not provided -- `invalid_key_format` → Key too short - -### Issue: "No repositories visible" - -**Verify bot user has access:** - -1. Log into Gitea -2. Go to repository Settings > Collaborators -3. Confirm bot user is listed with Read permission -4. If not, add bot user as collaborator - -**Test manually:** -```bash -curl "https://mcp.yourdomain.com/mcp/tool/call?api_key=YOUR_KEY" \ - -X POST \ - -H "Content-Type: application/json" \ - -d '{"tool": "list_repositories", "arguments": {}}' -``` - -### Issue: "Rate limited" - -**Symptoms:** -``` -Too many failed authentication attempts -``` - -**Solution:** -1. Wait 5 minutes -2. Verify API key is correct -3. Check audit logs: - ```bash - docker-compose exec aegis-mcp grep "auth_rate_limit" /var/log/aegis-mcp/audit.log - ``` -4. If accidentally locked out: - ```bash - docker-compose restart aegis-mcp - ``` - -### Issue: ChatGPT says "Tool not available" - -**Verify tools are registered:** -```bash -curl "https://mcp.yourdomain.com/mcp/tools?api_key=YOUR_KEY" -``` - -Should return JSON with tools array. - ---- - -## Security Best Practices - -### 1. API Key Management - -- ✅ Store key in password manager -- ✅ Never share key in chat or messages -- ✅ Rotate key every 90 days -- ❌ Don't paste key in public ChatGPT conversations -- ❌ Don't screenshot ChatGPT settings with key visible - -### 2. Workspace Separation - -If multiple team members: - -``` -User A: Use key_a1b2c3... -User B: Use key_b4c5d6... -User C: Use key_c7d8e9... -``` - -Configure in `.env`: -```bash -MCP_API_KEYS=key_a1b2c3...,key_b4c5d6...,key_c7d8e9... -``` - -Each user configures their own ChatGPT with their specific key. - -### 3. Monitoring Usage - -Weekly check: -```bash -# Who's using the MCP server? -docker-compose exec aegis-mcp grep "api_authentication" /var/log/aegis-mcp/audit.log | \ - grep "success" | \ - tail -20 -``` - -Look for `key_hint` to identify which key was used. - -### 4. Incident Response - -If key is compromised: - -1. **Immediate:** Remove from `.env` - ```bash - # Edit .env (remove compromised key) - docker-compose restart aegis-mcp - ``` - -2. **Generate new key** - ```bash - make generate-key - ``` - -3. **Update ChatGPT** with new key - -4. **Check audit logs** for unauthorized access: - ```bash - docker-compose exec aegis-mcp grep "key_hint: " /var/log/aegis-mcp/audit.log - ``` - ---- - -## Advanced Configuration - -### Custom MCP Tool Calls (For Developers) - -You can call tools directly via API: - -```bash -# List repositories -curl -X POST "https://mcp.yourdomain.com/mcp/tool/call?api_key=YOUR_KEY" \ - -H "Content-Type: application/json" \ - -d '{ - "tool": "list_repositories", - "arguments": {} - }' - -# Get file contents -curl -X POST "https://mcp.yourdomain.com/mcp/tool/call?api_key=YOUR_KEY" \ - -H "Content-Type: application/json" \ - -d '{ - "tool": "get_file_contents", - "arguments": { - "owner": "org", - "repo": "my-repo", - "filepath": "README.md" - } - }' -``` - -### Webhook Integration (Future) - -For automated workflows, you can integrate MCP tools into CI/CD: - -```yaml -# Example GitHub Actions (future enhancement) -- name: Sync to Gitea via MCP - run: | - curl -X POST "https://mcp.yourdomain.com/mcp/tool/call?api_key=${{ secrets.MCP_API_KEY }}" \ - -H "Content-Type: application/json" \ - -d '{...}' -``` - ---- - -## Performance Tips - -### 1. Rate Limiting Awareness - -Traefik limits: **60 requests/minute** - -For heavy usage: -```yaml -# docker-compose.yml -labels: - - "traefik.http.middlewares.aegis-ratelimit.ratelimit.average=120" -``` - -### 2. Large Files - -Files >1MB are rejected by default. To increase: - -```bash -# .env -MAX_FILE_SIZE_BYTES=5242880 # 5MB -``` - -### 3. Batch Operations - -Instead of: -``` -Show me file1.py -Show me file2.py -Show me file3.py -``` - -Use: -``` -Show me all Python files in the src/ directory -``` - ---- - -## FAQ - -**Q: Can I use this with Claude or other AI assistants?** - -A: Currently optimized for ChatGPT Business, but the MCP protocol is standard. Other AI assistants with MCP support should work with the same configuration. - -**Q: Do I need a separate key for each ChatGPT conversation?** - -A: No. One key per user/workspace. All conversations in that workspace use the same key. - -**Q: Can ChatGPT modify my repositories?** - -A: **No**. AegisGitea MCP is read-only by design. ChatGPT can only read code, never write/commit/push. - -**Q: What happens if I hit the rate limit?** - -A: You'll receive a 429 error. Wait 1 minute and try again. The limit is per IP, not per key. - -**Q: Can I use this from mobile ChatGPT app?** - -A: Yes, if your ChatGPT Business account syncs to mobile. The MCP configuration follows your account. - -**Q: How do I disconnect ChatGPT?** - -A: Go to ChatGPT Settings > MCP Servers > Remove "AegisGitea MCP" - ---- - -## Next Steps - -- [Key Rotation Guide](KEY_ROTATION.md) - Rotate keys every 90 days -- [Authentication Setup](AUTH_SETUP.md) - Detailed auth configuration -- [Security Policy](SECURITY.md) - Best practices and threat model - ---- - -**Happy coding with AI-assisted Gitea access!** 🚀 diff --git a/DEPLOYMENT.md b/DEPLOYMENT.md deleted file mode 100644 index c4f7657..0000000 --- a/DEPLOYMENT.md +++ /dev/null @@ -1,343 +0,0 @@ -# AegisGitea MCP Deployment Guide - -This guide walks you through deploying AegisGitea MCP in a production environment. - ---- - -## Prerequisites - -1. **Self-hosted Gitea instance** (running and accessible) -2. **Docker and Docker Compose** installed on your server -3. **Reverse proxy** (Traefik, Caddy, or Nginx) for TLS termination -4. **Bot user account** created in Gitea with read-only access - ---- - -## Step 1: Create Gitea Bot User - -1. Log into your Gitea instance as an admin -2. Create a new user account (e.g., `aegis-bot`) -3. **Important**: Do NOT grant admin privileges to this user -4. Generate an access token: - - Go to Settings > Applications - - Generate new token with `read:repository` scope only - - Save the token securely (you'll need it in Step 3) - ---- - -## Step 2: Grant Repository Access - -The bot user can only see repositories where it has been explicitly granted access: - -### Method 1: Add as Collaborator (for individual repos) - -1. Go to repository Settings > Collaborators -2. Add `aegis-bot` user -3. Set permission to **Read** only - -### Method 2: Add to Organization Team (for multiple repos) - -1. Create an organization team (e.g., "AI Reviewers") -2. Add `aegis-bot` to the team -3. Grant team **Read** access to desired repositories - -**Result**: Only repositories where the bot is a collaborator are AI-visible. - ---- - -## Step 3: Configure AegisGitea MCP - -Clone the repository and set up environment: - -```bash -# Clone repository -git clone https://your-gitea.com/your-org/AegisGitea-MCP.git -cd AegisGitea-MCP - -# Copy environment template -cp .env.example .env - -# Edit configuration -nano .env -``` - -### Required Configuration - -```bash -# Gitea instance URL (must be accessible from Docker container) -GITEA_URL=https://gitea.example.com - -# Bot user token from Step 1 -GITEA_TOKEN=your-bot-token-here - -# MCP server configuration -MCP_HOST=0.0.0.0 -MCP_PORT=8080 - -# Logging -LOG_LEVEL=INFO -AUDIT_LOG_PATH=/var/log/aegis-mcp/audit.log -``` - -### Optional Security Configuration - -```bash -# File size limit (bytes) -MAX_FILE_SIZE_BYTES=1048576 # 1MB - -# API request timeout (seconds) -REQUEST_TIMEOUT_SECONDS=30 - -# Rate limiting (requests per minute) -RATE_LIMIT_PER_MINUTE=60 -``` - ---- - -## Step 4: Deploy with Docker Compose - -```bash -# Build and start container -docker-compose up -d - -# Check logs -docker-compose logs -f aegis-mcp - -# Verify health -curl http://localhost:8080/health -``` - -Expected output: -```json -{"status": "healthy"} -``` - ---- - -## Step 5: Configure Reverse Proxy - -**Never expose the MCP server directly to the internet without TLS.** - -### Example: Traefik - -```yaml -# docker-compose.yml (add to aegis-mcp service) -labels: - - "traefik.enable=true" - - "traefik.http.routers.aegis-mcp.rule=Host(`mcp.example.com`)" - - "traefik.http.routers.aegis-mcp.entrypoints=websecure" - - "traefik.http.routers.aegis-mcp.tls.certresolver=letsencrypt" - - "traefik.http.services.aegis-mcp.loadbalancer.server.port=8080" -``` - -### Example: Caddy - -```caddyfile -# Caddyfile -mcp.example.com { - reverse_proxy aegis-mcp:8080 -} -``` - -### Example: Nginx - -```nginx -# /etc/nginx/sites-available/aegis-mcp -server { - listen 443 ssl http2; - server_name mcp.example.com; - - ssl_certificate /path/to/cert.pem; - ssl_certificate_key /path/to/key.pem; - - location / { - proxy_pass http://localhost:8080; - proxy_set_header Host $host; - proxy_set_header X-Real-IP $remote_addr; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - proxy_set_header X-Forwarded-Proto $scheme; - - # SSE support - proxy_buffering off; - proxy_cache off; - proxy_set_header Connection ''; - chunked_transfer_encoding off; - } -} -``` - ---- - -## Step 6: Register with ChatGPT - -1. Go to ChatGPT Settings > MCP Servers -2. Add new MCP server: - - **Name**: AegisGitea MCP - - **URL**: `https://mcp.example.com` - - **Type**: SSE (Server-Sent Events) - -3. Test connection by asking ChatGPT: - ``` - List my Gitea repositories - ``` - ---- - -## Verification Checklist - -- [ ] Bot user created in Gitea -- [ ] Bot user has read-only token -- [ ] Bot user added as collaborator to desired repositories -- [ ] `.env` file configured with correct values -- [ ] Docker container running and healthy -- [ ] Reverse proxy configured with TLS -- [ ] MCP server accessible via HTTPS -- [ ] ChatGPT successfully connects to MCP server -- [ ] Audit logs are being written - ---- - -## Security Best Practices - -### 1. Token Management - -- **Rotate tokens quarterly** or when team members leave -- Store tokens in a secrets manager (Vault, 1Password, etc.) -- Never commit tokens to version control - -### 2. Network Security - -- Use a firewall to restrict MCP server access -- Only allow HTTPS connections (port 443) -- Consider VPN or IP allowlisting for extra security - -### 3. Monitoring - -Monitor audit logs for unexpected activity: - -```bash -# View recent audit events -docker-compose exec aegis-mcp tail -f /var/log/aegis-mcp/audit.log - -# Search for specific repository access -docker-compose exec aegis-mcp grep "repository-name" /var/log/aegis-mcp/audit.log -``` - -### 4. Access Control - -- Review bot user permissions monthly -- Remove access from archived repositories -- Audit which repositories are AI-visible - -### 5. Updates - -```bash -# Pull latest changes -git pull - -# Rebuild container -docker-compose down -docker-compose build --no-cache -docker-compose up -d -``` - ---- - -## Troubleshooting - -### Container won't start - -```bash -# Check logs for errors -docker-compose logs aegis-mcp - -# Verify environment variables -docker-compose config -``` - -### Authentication errors - -```bash -# Test Gitea connection manually -curl -H "Authorization: token YOUR_TOKEN" https://gitea.example.com/api/v1/user - -# If 401: Token is invalid or expired -# If 403: Token lacks necessary permissions -``` - -### ChatGPT can't connect - -1. Verify reverse proxy is working: - ```bash - curl https://mcp.example.com/health - ``` - -2. Check firewall rules: - ```bash - sudo ufw status - ``` - -3. Review reverse proxy logs - -### No repositories visible - -- Verify bot user has been added as collaborator -- Check repository is not archived -- Confirm bot user permissions in Gitea UI - ---- - -## Rollback Plan - -If something goes wrong: - -```bash -# Stop container -docker-compose down - -# Remove container and volumes -docker-compose down -v - -# Restore previous configuration -git checkout HEAD~1 .env - -# Restart -docker-compose up -d -``` - -To completely disable AI access: - -1. Remove bot user token in Gitea -2. Stop MCP container: `docker-compose down` - -**The system is designed to be reversible.** - ---- - -## Production Checklist - -Before going live: - -- [ ] All sensitive data in `.env` (not hardcoded) -- [ ] TLS configured and tested -- [ ] Audit logging enabled and accessible -- [ ] Resource limits set in docker-compose.yml -- [ ] Monitoring and alerting configured -- [ ] Backup strategy for audit logs -- [ ] Incident response plan documented -- [ ] Team trained on emergency procedures - ---- - -## Support - -For deployment issues: - -1. Check logs: `docker-compose logs -f` -2. Review audit logs for access patterns -3. Open an issue in Gitea repository -4. Include sanitized logs (remove tokens!) - ---- - -**Remember**: This system prioritizes security over convenience. When in doubt, restrict access first and expand gradually. diff --git a/KEY_ROTATION.md b/KEY_ROTATION.md deleted file mode 100644 index 195d360..0000000 --- a/KEY_ROTATION.md +++ /dev/null @@ -1,548 +0,0 @@ -# API Key Rotation Guide - -Comprehensive guide for rotating API keys in AegisGitea MCP with zero downtime. - ---- - -## Why Rotate Keys? - -- **Security hygiene**: Regular rotation limits exposure window -- **Compliance**: Many security policies require quarterly rotation -- **Compromise mitigation**: If a key leaks, rotation limits damage -- **Audit trail**: New keys = clear timeline of when access changed - -**Recommended schedule**: Every 90 days - ---- - -## Automated Rotation (Recommended) - -### Step 1: Check Key Age - -```bash -make check-key-age -``` - -**Output:** -``` -Key: a1b2c3d4... - Created: 2026-01-29 (85 days ago) - Expires: 2026-04-29 (in 5 days) - Status: ⚠️ WARNING -``` - -### Step 2: Rotate Key - -```bash -make rotate-key -``` - -This script will: -1. Show current keys -2. Generate new 64-character key -3. Offer rotation strategies (replace or grace period) -4. Backup old `.env` file -5. Update `.env` with new key -6. Provide next steps - -**Example:** -``` -Found 1 existing key(s) - 1. a1b2c3d4...e1f2 - -✓ New API key generated! - -Rotation Strategy: - 1. Replace all keys with new key (recommended) - 2. Add new key, keep old keys (grace period) - 3. Cancel - -Choose option [1/2/3]: 2 - -✓ New key will be added (total: 2 keys) - -⚠️ IMPORTANT: Remove old keys manually after updating ChatGPT config -``` - -### Step 3: Restart Server - -```bash -docker-compose restart aegis-mcp -``` - -Verify both keys work: -```bash -# Test old key (should still work if using grace period) -curl -H "Authorization: Bearer OLD_KEY" \ - https://mcp.yourdomain.com/mcp/tools - -# Test new key (should work) -curl -H "Authorization: Bearer NEW_KEY" \ - https://mcp.yourdomain.com/mcp/tools -``` - -### Step 4: Update ChatGPT - -1. Go to ChatGPT Settings > MCP Servers -2. Edit "AegisGitea MCP" -3. Update Authorization header: - ``` - Authorization: Bearer NEW_KEY_HERE - ``` -4. Save - -### Step 5: Verify & Clean Up - -Test ChatGPT connection: -``` -List my Gitea repositories -``` - -If successful, remove old key: -```bash -# Edit .env - remove old key, keep only new key -nano .env - -# MCP_API_KEYS=old-key,new-key ← Remove old-key -# MCP_API_KEYS=new-key ← Keep only new key - -# Restart -docker-compose restart aegis-mcp -``` - -Verify old key no longer works: -```bash -curl -H "Authorization: Bearer OLD_KEY" \ - https://mcp.yourdomain.com/mcp/tools -# Should return 401 Unauthorized -``` - ---- - -## Manual Rotation (Step-by-Step) - -If you prefer manual control: - -### 1. Generate New Key - -```bash -make generate-key -``` - -Save the output: -``` -API KEY: b9c8d7e6f5g4h3i2... -``` - -### 2. Add to .env (Grace Period) - -```bash -nano .env - -# Before: -MCP_API_KEYS=a1b2c3d4e5f6g7h8... - -# After (both keys work): -MCP_API_KEYS=a1b2c3d4e5f6g7h8...,b9c8d7e6f5g4h3i2... -``` - -### 3. Restart & Verify - -```bash -docker-compose restart aegis-mcp - -# Check logs -docker-compose logs aegis-mcp | grep "authentication" -# Should show: "API key authentication ENABLED (2 key(s) configured)" -``` - -### 4. Update ChatGPT - -Update Authorization header with new key. - -### 5. Remove Old Key - -After confirming ChatGPT works with new key: - -```bash -nano .env - -# Remove old key -MCP_API_KEYS=b9c8d7e6f5g4h3i2... - -docker-compose restart aegis-mcp -``` - ---- - -## Rotation Strategies - -### Strategy 1: Immediate Replacement (No Downtime Risk) - -**Use when:** You can update ChatGPT config immediately - -1. Generate new key -2. Replace old key in `.env` -3. Restart server -4. Update ChatGPT within minutes - -**Pros:** Clean, only one key at a time -**Cons:** Must update ChatGPT immediately or service breaks - -### Strategy 2: Grace Period (Zero Downtime) - -**Use when:** You need time to update ChatGPT config - -1. Generate new key -2. Add new key, keep old key (both work) -3. Restart server -4. Update ChatGPT at your convenience -5. Remove old key after verification - -**Pros:** Zero downtime, test thoroughly -**Cons:** Temporarily allows two keys - -**Recommended grace period:** 24-48 hours max - -### Strategy 3: Scheduled Maintenance Window - -**Use when:** You want to coordinate with team - -1. Announce maintenance window (e.g., Saturday 2 AM) -2. Generate new key -3. During window: - - Replace key in `.env` - - Restart server - - Update all team members' ChatGPT configs -4. Verify all users can access - -**Pros:** Coordinated, everyone updates at once -**Cons:** Requires coordination - ---- - -## Multi-User Rotation - -If multiple team members use different keys: - -### Option A: Rotate One Key at a Time - -```bash -# Current state -MCP_API_KEYS=alice-key,bob-key,charlie-key - -# Rotate Alice's key -MCP_API_KEYS=alice-new-key,bob-key,charlie-key - -# Alice updates her ChatGPT, verify, continue -``` - -### Option B: Rotate All Keys Simultaneously - -```bash -# Generate 3 new keys -make generate-key # Save as alice-new -make generate-key # Save as bob-new -make generate-key # Save as charlie-new - -# Grace period -MCP_API_KEYS=alice-old,alice-new,bob-old,bob-new,charlie-old,charlie-new - -# Each user updates -# After all verified, remove old keys -MCP_API_KEYS=alice-new,bob-new,charlie-new -``` - ---- - -## Automated Rotation Schedule - -### Setup Cron Job - -```bash -# Edit crontab -crontab -e - -# Check key age weekly -0 9 * * MON /path/to/AegisGitea-MCP/scripts/check_key_age.py || echo "Keys need rotation" - -# Or check daily at 2 AM -0 2 * * * cd /path/to/AegisGitea-MCP && make check-key-age -``` - -Exit codes: -- `0` = All keys OK -- `1` = Warning (rotation recommended in 14 days) -- `2` = Critical (rotation needed NOW) - -### Email Notifications - -```bash -#!/bin/bash -# Save as scripts/check_and_notify.sh - -cd /path/to/AegisGitea-MCP - -# Run check -make check-key-age > /tmp/key_status.txt 2>&1 -STATUS=$? - -if [ $STATUS -eq 2 ]; then - # Critical - send email - mail -s "[CRITICAL] AegisGitea API Key Expired" admin@example.com < /tmp/key_status.txt -elif [ $STATUS -eq 1 ]; then - # Warning - send email - mail -s "[WARNING] AegisGitea API Key Expiring Soon" admin@example.com < /tmp/key_status.txt -fi -``` - -### Discord/Slack Webhook - -```bash -#!/bin/bash -# Save as scripts/notify_discord.sh - -WEBHOOK_URL="https://discord.com/api/webhooks/..." -MESSAGE="⚠️ AegisGitea MCP: API key expires in 5 days. Run \`make rotate-key\`" - -curl -X POST "$WEBHOOK_URL" \ - -H "Content-Type: application/json" \ - -d "{\"content\": \"$MESSAGE\"}" -``` - ---- - -## Emergency Rotation (Compromised Key) - -If you suspect a key has been compromised: - -### Immediate Actions (< 5 minutes) - -1. **Revoke compromised key immediately** - ```bash - # Edit .env - remove compromised key - nano .env - - # Before - MCP_API_KEYS=compromised-key - - # After - MCP_API_KEYS= - - # Or temporarily disable auth - AUTH_ENABLED=false - - docker-compose restart aegis-mcp - ``` - -2. **Check audit logs for unauthorized access** - ```bash - docker-compose exec aegis-mcp grep "compromised_key_hint" /var/log/aegis-mcp/audit.log - - # Look for suspicious IPs, unusual times, unexpected repositories - ``` - -3. **Generate new key** - ```bash - make generate-key - ``` - -4. **Update .env with new key** - ```bash - MCP_API_KEYS=new-secure-key - AUTH_ENABLED=true - - docker-compose restart aegis-mcp - ``` - -5. **Update ChatGPT config** - -### Investigation (< 30 minutes) - -```bash -# When was compromised key last used? -docker-compose exec aegis-mcp grep "key_hint: compromised..." /var/log/aegis-mcp/audit.log | tail -1 - -# What repositories were accessed? -docker-compose exec aegis-mcp grep "compromised..." /var/log/aegis-mcp/audit.log | grep "repository" - -# From which IPs? -docker-compose exec aegis-mcp grep "compromised..." /var/log/aegis-mcp/audit.log | grep "client_ip" -``` - -### Post-Incident (< 24 hours) - -1. Document timeline of compromise -2. Review and improve key storage practices -3. Consider additional security measures (IP allowlisting, MFA) -4. Notify team if multi-user setup - ---- - -## Troubleshooting Rotation - -### Issue: Both old and new keys fail after rotation - -**Cause:** Server not restarted or `.env` syntax error - -**Fix:** -```bash -# Check .env syntax -cat .env | grep MCP_API_KEYS -# Should be: MCP_API_KEYS=key1,key2 - -# No spaces around = -# No quotes around keys -# Comma-separated, no spaces - -# Restart -docker-compose restart aegis-mcp - -# Check logs -docker-compose logs aegis-mcp | tail -20 -``` - -### Issue: "No API keys configured" after rotation - -**Cause:** Empty MCP_API_KEYS in `.env` - -**Fix:** -```bash -# Verify .env has key -grep MCP_API_KEYS .env -# Should NOT be empty - -# If empty, add key -echo "MCP_API_KEYS=your-key-here" >> .env - -docker-compose restart aegis-mcp -``` - -### Issue: Old key still works after removal - -**Cause:** Server not restarted or cached config - -**Fix:** -```bash -# Force restart (not just reload) -docker-compose down -docker-compose up -d - -# Verify old key fails -curl -H "Authorization: Bearer OLD_KEY" \ - https://mcp.yourdomain.com/mcp/tools -# Should return 401 -``` - -### Issue: Lost .env.backup file - -**Cause:** Backup not created during rotation - -**Prevention:** -```bash -# Always backup before manual edits -cp .env .env.backup-$(date +%Y%m%d-%H%M%S) - -# The rotate script does this automatically -``` - -**Recovery:** -If you lost your key and no backup: -1. Generate new key -2. Update `.env` -3. Update ChatGPT -4. The old key is lost but system still works - ---- - -## Best Practices Checklist - -Before rotation: -- [ ] Check current key age: `make check-key-age` -- [ ] Backup `.env`: `cp .env .env.backup` -- [ ] Notify team if multi-user -- [ ] Schedule maintenance window if needed - -During rotation: -- [ ] Generate new key: `make generate-key` -- [ ] Save key securely (password manager) -- [ ] Update `.env` with new key -- [ ] Restart server: `docker-compose restart` -- [ ] Verify new key works (curl test) -- [ ] Update ChatGPT config -- [ ] Test ChatGPT connection - -After rotation: -- [ ] Remove old key from `.env` (if using grace period) -- [ ] Verify old key fails (curl test) -- [ ] Check audit logs for successful auth with new key -- [ ] Save key metadata: Save metadata when generating key -- [ ] Set calendar reminder for next rotation (90 days) -- [ ] Document rotation in changelog/runbook - ---- - -## Key Metadata Tracking - -When generating keys, save metadata: - -```bash -make generate-key -# Choose "y" when prompted to save metadata -``` - -This creates `keys/key--.txt`: -``` -API Key Metadata -================ - -Key ID: a1b2c3d4... -Description: Production ChatGPT Key -Created: 2026-01-29T12:00:00+00:00 -Expires: 2026-04-29T12:00:00+00:00 - -NOTE: The actual API key is NOT stored in this file for security. - Only metadata is saved for reference. -``` - -**Benefits:** -- Track key age automatically -- Audit key lifecycle -- Identify which key is which (if multiple) -- Automated expiration warnings - ---- - -## FAQ - -**Q: What's the grace period for rotation?** - -A: 24-48 hours is recommended. Keep both keys working just long enough to update ChatGPT, then remove the old key. - -**Q: Can I rotate keys without downtime?** - -A: Yes! Use Strategy 2 (Grace Period). Both old and new keys work simultaneously during the transition. - -**Q: How do I know which key is oldest?** - -A: Use `make check-key-age` if you saved metadata. Otherwise, check audit logs for last usage. - -**Q: Should I rotate after a team member leaves?** - -A: Yes, if they had access to the shared key. Or better: use per-user keys and just remove their key. - -**Q: Can I automate the entire rotation?** - -A: Partially. You can automate generation and notification, but ChatGPT config update requires manual action. - ---- - -## Next Steps - -- [Authentication Setup](AUTH_SETUP.md) - Initial setup guide -- [ChatGPT Configuration](CHATGPT_SETUP.md) - Update ChatGPT after rotation -- [Security Policy](SECURITY.md) - Overall security best practices - ---- - -**Set a reminder now:** Rotate your keys 90 days from today! 🔑 diff --git a/PROJECT_SUMMARY.md b/PROJECT_SUMMARY.md deleted file mode 100644 index 02cffc4..0000000 --- a/PROJECT_SUMMARY.md +++ /dev/null @@ -1,362 +0,0 @@ -# AegisGitea MCP - Project Summary - -**Status**: Phase 1 Complete - Foundation Implemented - ---- - -## What Was Built - -A complete, production-ready implementation of AegisGitea MCP - a security-first Model Context Protocol server that enables controlled AI access to self-hosted Gitea repositories. - ---- - -## Project Structure - -``` -AegisGitea-MCP/ -├── src/aegis_gitea_mcp/ # Main application code -│ ├── __init__.py # Package initialization -│ ├── server.py # FastAPI server with MCP endpoints -│ ├── mcp_protocol.py # MCP protocol definitions -│ ├── config.py # Configuration management -│ ├── audit.py # Audit logging system -│ ├── gitea_client.py # Gitea API client -│ └── tools/ # MCP tool implementations -│ ├── __init__.py -│ └── repository.py # Repository access tools -│ -├── tests/ # Test suite -│ ├── __init__.py -│ ├── conftest.py # Pytest configuration -│ └── test_config.py # Configuration tests -│ -├── docker/ # Docker configuration -│ ├── Dockerfile # Multi-stage build -│ └── docker-compose.yml # Container orchestration -│ -├── Documentation -│ ├── README.md # Main project documentation -│ ├── QUICKSTART.md # 5-minute setup guide -│ ├── DEPLOYMENT.md # Production deployment guide -│ ├── SECURITY.md # Security policy and best practices -│ └── PROJECT_SUMMARY.md # This file -│ -├── Configuration -│ ├── .env.example # Environment variable template -│ ├── .gitignore # Git ignore patterns -│ ├── pyproject.toml # Python project configuration -│ ├── requirements.txt # Production dependencies -│ ├── requirements-dev.txt # Development dependencies -│ ├── Makefile # Development commands -│ └── docker-compose.yml # Root-level compose file -│ -└── LICENSE # MIT License -``` - ---- - -## Implemented Features - -### Phase 1: Foundation (COMPLETE) - -#### Core Infrastructure -- [x] FastAPI-based MCP server -- [x] Server-Sent Events (SSE) endpoint for real-time communication -- [x] Health check and status endpoints -- [x] Structured logging with configurable levels -- [x] Environment-based configuration management - -#### Security Features -- [x] Bot user authentication via access tokens -- [x] Dynamic authorization via Gitea permissions -- [x] Comprehensive audit logging (timestamp, tool, repo, target, correlation ID) -- [x] File size limits (configurable, default 1MB) -- [x] Request timeout protection -- [x] Input validation and error handling -- [x] Non-root Docker container execution - -#### MCP Tools -- [x] `list_repositories` - List all bot-visible repositories -- [x] `get_repository_info` - Get repository metadata -- [x] `get_file_tree` - Browse repository file structure -- [x] `get_file_contents` - Read file contents with size limits - -#### Gitea Integration -- [x] Async HTTP client with proper error handling -- [x] Bot user authentication and verification -- [x] Repository access control enforcement -- [x] File content retrieval with encoding handling -- [x] Tree/directory listing support - -#### Developer Experience -- [x] Docker containerization with multi-stage builds -- [x] Docker Compose for easy deployment -- [x] Makefile with common development tasks -- [x] Pytest test suite with fixtures -- [x] Type hints and validation with Pydantic -- [x] Code quality tools (black, ruff, mypy) -- [x] Comprehensive documentation - ---- - -## Technical Stack - -| Component | Technology | Purpose | -|-----------|-----------|---------| -| **Server** | FastAPI + Uvicorn | Async HTTP server with SSE support | -| **HTTP Client** | httpx | Async Gitea API communication | -| **Validation** | Pydantic | Type-safe configuration and data models | -| **Logging** | structlog | Structured, machine-readable audit logs | -| **Containerization** | Docker | Isolated, reproducible deployment | -| **Testing** | pytest + pytest-asyncio | Comprehensive test coverage | -| **Code Quality** | black, ruff, mypy | Consistent code style and type safety | - ---- - -## Architecture Highlights - -### Separation of Concerns - -``` -ChatGPT ──HTTP/SSE──> MCP Server ──API──> Gitea - │ - ├──> Audit Logger (all actions logged) - ├──> Config Manager (env-based settings) - └──> Tool Handlers (bounded operations) -``` - -### Security Model - -1. **Authorization**: Fully delegated to Gitea (bot user permissions) -2. **Authentication**: Token-based, rotatable -3. **Auditability**: Every action logged with correlation IDs -4. **Safety**: Read-only, bounded operations, fail-safe defaults - -### Key Design Decisions - -- **No write operations**: Read-only by design, impossible to modify repositories -- **No global search**: All tools require explicit repository targeting -- **Dynamic permissions**: Changes in Gitea take effect immediately -- **Stateless server**: No session management, fully stateless -- **Explicit over implicit**: No hidden or automatic operations - ---- - -## What's NOT Implemented (Future Phases) - -### Phase 2: Extended Context (Planned) -- Commit history and diff viewing -- Issue and pull request access -- Branch listing and comparison -- Tag and release information - -### Phase 3: Advanced Features (Future) -- Rate limiting per client (currently per-server) -- Webhook support for real-time updates -- Caching layer for performance -- Multi-tenant support -- OAuth2 flow instead of static tokens - ---- - -## Testing Status - -### Implemented Tests -- Configuration loading and validation -- Environment variable handling -- Default value verification -- Singleton pattern testing - -### Test Coverage Needed -- Gitea client operations (requires mocking) -- MCP tool implementations -- Audit logging functionality -- Server endpoints and SSE - ---- - -## Deployment Status - -### Ready for Production -- ✅ Docker containerization -- ✅ Environment-based configuration -- ✅ Health checks and monitoring hooks -- ✅ Audit logging enabled -- ✅ Security hardening (non-root, resource limits) -- ✅ Documentation complete - -### Needs Configuration -- ⚠️ Reverse proxy setup (Traefik/Caddy/Nginx) -- ⚠️ TLS certificates -- ⚠️ Bot user creation in Gitea -- ⚠️ Repository access grants -- ⚠️ Production environment variables - ---- - -## Security Posture - -### Implemented Safeguards -- Read-only operations only -- Bot user with minimal permissions -- Comprehensive audit logging -- File size limits -- Request timeouts -- Input validation -- Container security (non-root, no-new-privileges) - -### Recommended Next Steps -- Set up log rotation for audit logs -- Implement monitoring/alerting on audit logs -- Regular token rotation policy -- Periodic access reviews -- Security training for operators - ---- - -## Performance Characteristics - -### Resource Usage (Typical) -- **Memory**: ~128-256 MB -- **CPU**: Minimal (async I/O bound) -- **Disk**: Audit logs grow over time (implement rotation) -- **Network**: Depends on file sizes and request frequency - -### Scalability -- Stateless design allows horizontal scaling -- Async operations handle concurrent requests efficiently -- Rate limiting prevents abuse - ---- - -## Next Steps for Deployment - -1. **Setup Bot User** (5 min) - - Create `aegis-bot` user in Gitea - - Generate read-only access token - -2. **Configure Environment** (2 min) - - Copy `.env.example` to `.env` - - Set `GITEA_URL` and `GITEA_TOKEN` - -3. **Deploy Container** (1 min) - - Run `docker-compose up -d` - - Verify with `curl http://localhost:8080/health` - -4. **Setup Reverse Proxy** (10-30 min) - - Configure Traefik/Caddy/Nginx - - Obtain TLS certificates - - Test HTTPS access - -5. **Grant Repository Access** (2 min per repo) - - Add `aegis-bot` as collaborator - - Set Read permission - -6. **Connect ChatGPT** (5 min) - - Add MCP server in ChatGPT settings - - Test with "List my Gitea repositories" - -**Total time**: ~30-60 minutes for complete setup - ---- - -## Success Criteria - -This implementation successfully meets all Phase 1 objectives: - -- ✅ Secure communication between ChatGPT and Gitea -- ✅ Bot user authentication working -- ✅ Dynamic authorization via Gitea -- ✅ Comprehensive audit logging -- ✅ Read-only operations enforced -- ✅ Production-ready deployment -- ✅ Complete documentation - ---- - -## Maintainability - -### Code Quality -- Type hints throughout -- Docstrings on all public functions -- Pydantic models for validation -- Structured error handling -- Separation of concerns - -### Documentation -- Inline code comments where needed -- Comprehensive README -- Step-by-step deployment guide -- Security policy and best practices -- Quick start guide - -### Testability -- Pytest framework set up -- Fixtures for common test scenarios -- Configuration reset between tests -- Mock-friendly architecture - ---- - -## Known Limitations - -1. **Audit Log Size**: Logs grow unbounded (implement rotation) -2. **Rate Limiting**: Per-server, not per-client -3. **Caching**: No caching layer (every request hits Gitea) -4. **Error Messages**: Could be more user-friendly -5. **Test Coverage**: Core logic tested, tools need more coverage - -None of these are blockers for production use. - ---- - -## Support and Maintenance - -### Regular Maintenance Tasks -- **Weekly**: Review audit logs for anomalies -- **Monthly**: Review bot user permissions -- **Quarterly**: Rotate bot user token -- **As needed**: Update Docker images - -### Monitoring Recommendations -- Track API response times -- Monitor error rates -- Alert on authentication failures -- Watch audit log size - ---- - -## Final Notes - -This project was built with **security, auditability, and simplicity** as the primary goals. The architecture intentionally avoids clever optimizations in favor of straightforward, auditable behavior. - -**It's designed to be boring, predictable, and safe** - exactly what you want in a security-critical system. - ---- - -## Questions for Stakeholders - -Before going live, confirm: - -1. **Bot user naming**: Is `aegis-bot` acceptable? -2. **Token rotation**: What's the policy (recommend: quarterly)? -3. **Audit log retention**: How long to keep logs (recommend: 90 days)? -4. **Access approval**: Who approves new repository access? -5. **Incident response**: Who responds to security alerts? - ---- - -## Conclusion - -**AegisGitea MCP is ready for production deployment.** - -All Phase 1 objectives have been met, the system is fully documented, and security best practices have been implemented throughout. The next steps are configuration and deployment-specific rather than development work. - -The foundation is solid, boring, and secure - ready to enable safe AI access to your private Gitea repositories. - ---- - -**Project Status**: ✅ Phase 1 Complete - Ready for Deployment - -**Last Updated**: January 29, 2026 -**Version**: 0.1.0 diff --git a/QUICKSTART.md b/QUICKSTART.md deleted file mode 100644 index 928e1e3..0000000 --- a/QUICKSTART.md +++ /dev/null @@ -1,131 +0,0 @@ -# Quick Start Guide - -Get AegisGitea MCP running in 5 minutes. - ---- - -## Prerequisites - -- Docker and Docker Compose installed -- Self-hosted Gitea instance -- 5 minutes of your time - ---- - -## Step 1: Create Bot User (2 minutes) - -1. Log into your Gitea instance -2. Create a new user `aegis-bot` (or any name you prefer) -3. Go to Settings > Applications -4. Generate an access token with **read-only** permissions -5. Copy the token - ---- - -## Step 2: Clone and Configure (1 minute) - -```bash -# Clone repository -git clone -cd AegisGitea-MCP - -# Configure environment -cp .env.example .env -nano .env -``` - -Edit `.env`: -```bash -GITEA_URL=https://your-gitea-instance.com -GITEA_TOKEN=your-bot-token-here -``` - ---- - -## Step 3: Start Server (1 minute) - -```bash -docker-compose up -d -``` - -Verify it's running: -```bash -# Check logs -docker-compose logs -f - -# Test health endpoint -curl http://localhost:8080/health -``` - -Expected response: -```json -{"status": "healthy"} -``` - ---- - -## Step 4: Grant Repository Access (1 minute) - -1. Go to a repository in Gitea -2. Settings > Collaborators -3. Add `aegis-bot` user -4. Set permission to **Read** - ---- - -## Step 5: Connect ChatGPT (Optional) - -If using ChatGPT Business/Developer: - -1. Go to ChatGPT Settings -2. Add MCP Server: - - **URL**: `http://localhost:8080` (or your domain) - - **Type**: HTTP/SSE -3. Test by asking: "List my Gitea repositories" - ---- - -## What's Next? - -- Read [DEPLOYMENT.md](DEPLOYMENT.md) for production setup -- Review [SECURITY.md](SECURITY.md) for security best practices -- Check audit logs: `docker-compose exec aegis-mcp cat /var/log/aegis-mcp/audit.log` - ---- - -## Troubleshooting - -### Container won't start - -```bash -docker-compose logs aegis-mcp -``` - -Common issues: -- Invalid `GITEA_URL` or `GITEA_TOKEN` in `.env` -- Port 8080 already in use -- Gitea instance not accessible - -### Bot can't see repositories - -1. Verify bot user is added as collaborator -2. Check bot user has Read permission -3. Confirm repository is not archived - -### ChatGPT can't connect - -- Ensure MCP server is accessible from ChatGPT -- Check firewall rules -- Verify HTTPS is configured (required for production) - ---- - -## Need Help? - -- Check the [README.md](README.md) for detailed documentation -- Review logs for error messages -- Open an issue in the repository - ---- - -**You're all set!** The AI can now securely access your Gitea repositories. diff --git a/README.md b/README.md deleted file mode 100644 index 9b025ba..0000000 --- a/README.md +++ /dev/null @@ -1,322 +0,0 @@ -# AegisGitea MCP - -**A private, security-first MCP server for controlled AI access to self-hosted Gitea** - ---- - -## Overview - -AegisGitea MCP is a Model Context Protocol (MCP) server that enables controlled, auditable, read-only AI access to a self-hosted Gitea environment. - -The system allows ChatGPT (Business / Developer environment) to inspect repositories, code, commits, issues, and pull requests **only through explicit MCP tool calls**, while all access control is dynamically managed through a dedicated bot user inside Gitea itself. - -### Core Principles - -- **Strong separation of concerns**: Clear boundaries between AI, MCP server, and Gitea -- **Least-privilege access**: Bot user has minimal necessary permissions -- **Full auditability**: Every AI action is logged with context -- **Dynamic authorization**: Access control via Gitea permissions (no redeployment needed) -- **Privacy-first**: Designed for homelab and private infrastructure - ---- - -## Architecture - -``` -┌─────────────────────────────────────────────────────────────┐ -│ ChatGPT (Business/Developer) │ -│ - Initiates explicit MCP tool calls │ -│ - Human-in-the-loop decision making │ -└────────────────────┬────────────────────────────────────────┘ - │ HTTPS (MCP over SSE) - ▼ -┌─────────────────────────────────────────────────────────────┐ -│ AegisGitea MCP Server (Python, Docker) │ -│ - Implements MCP protocol │ -│ - Translates tool calls → Gitea API requests │ -│ - Enforces access, logging, and safety constraints │ -│ - Provides bounded, single-purpose tools │ -└────────────────────┬────────────────────────────────────────┘ - │ Gitea API (Bot User Token) - ▼ -┌─────────────────────────────────────────────────────────────┐ -│ Gitea Instance (Docker) │ -│ - Source of truth for authorization │ -│ - Hosts dedicated read-only bot user │ -│ - Determines AI-visible repositories dynamically │ -└─────────────────────────────────────────────────────────────┘ -``` - -### Trust Model - -| Component | Responsibility | -|-----------|----------------| -| **Gitea** | Authorization (what the AI can see) | -| **MCP Server** | Policy enforcement (how the AI accesses data) | -| **ChatGPT** | Decision initiation (when the AI acts) | -| **Human** | Final decision authority (why the AI acts) | - ---- - -## Features - -### Phase 1 — Foundation (Current) - -- MCP protocol handling with SSE lifecycle -- Secure Gitea API communication via bot user token -- Health and readiness endpoints -- ChatGPT MCP registration flow - -### Phase 2 — Authorization & Data Access (Planned) - -- Repository discovery based on bot user permissions -- File tree and content retrieval with size limits -- Dynamic access control (changes in Gitea apply instantly) - -### Phase 3 — Audit & Hardening (Planned) - -- Comprehensive audit logging (timestamp, tool, repo, path, correlation ID) -- Request correlation and tracing -- Input validation and rate limiting -- Defensive bounds on all operations - -### Phase 4 — Extended Context (Future) - -- Commit history and diff inspection -- Issue and pull request visibility -- Full contextual understanding while maintaining read-only guarantees - ---- - -## Authorization Model - -### Bot User Strategy - -A dedicated Gitea bot user represents "the AI": - -- The MCP server authenticates as this user using a read-only token -- The bot user's repository permissions define AI visibility -- **No admin privileges** -- **No write permissions** -- **No implicit access** - -This allows dynamic enable/disable of AI access **without restarting or reconfiguring** the MCP server. - -**Example:** -```bash -# Grant AI access to a repository -git clone https://gitea.example.com/org/repo.git -cd repo -# Add bot user as collaborator with Read permission in Gitea UI - -# Revoke AI access -# Remove bot user from repository in Gitea UI -``` - ---- - -## MCP Tool Design - -All tools are: - -- **Explicit**: Single-purpose, no hidden behavior -- **Deterministic**: Same input always produces same output -- **Bounded**: Size limits, path constraints, no wildcards -- **Auditable**: Full logging of every invocation - -### Tool Categories - -1. **Repository Discovery** - - List repositories visible to bot user - - Get repository metadata - -2. **File Operations** - - Get file tree for a repository - - Read file contents (with size limits) - -3. **Commit History** (Phase 4) - - List commits for a repository - - Get commit details and diffs - -4. **Issues & PRs** (Phase 4) - - List issues and pull requests - - Read issue/PR details and comments - -### Explicit Constraints - -- No wildcard search tools -- No full-text indexing -- No recursive "read everything" operations -- No hidden or implicit data access - ---- - -## Audit & Observability - -Every MCP tool invocation logs: - -- **Timestamp** (UTC) -- **Tool name** -- **Repository identifier** -- **Target** (path / commit / issue) -- **Correlation ID** - -Logs are: - -- Append-only -- Human-readable JSON -- Machine-parseable -- Stored locally by default - -**Audit Philosophy**: The system must answer "What exactly did the AI see, and when?" without ambiguity. - ---- - -## Deployment - -### Prerequisites - -- Docker and Docker Compose -- Self-hosted Gitea instance -- Gitea bot user with read-only access token - -### Quick Start - -```bash -# Clone repository -git clone https://gitea.example.com/your-org/AegisGitea-MCP.git -cd AegisGitea-MCP - -# Configure environment -cp .env.example .env -# Edit .env with your Gitea URL and bot token - -# Start MCP server -docker-compose up -d - -# Check logs -docker-compose logs -f aegis-mcp -``` - -### Environment Variables - -| Variable | Description | Required | -|----------|-------------|----------| -| `GITEA_URL` | Base URL of Gitea instance | Yes | -| `GITEA_TOKEN` | Bot user access token | Yes | -| `MCP_HOST` | MCP server listen host | No (default: 0.0.0.0) | -| `MCP_PORT` | MCP server listen port | No (default: 8080) | -| `LOG_LEVEL` | Logging verbosity | No (default: INFO) | -| `AUDIT_LOG_PATH` | Audit log file path | No (default: /var/log/aegis-mcp/audit.log) | - -### Security Considerations - -1. **Never expose the MCP server publicly** — use a reverse proxy with TLS -2. **Rotate bot tokens regularly** -3. **Monitor audit logs** for unexpected access patterns -4. **Keep Docker images updated** -5. **Use a dedicated bot user** — never use a personal account token - ---- - -## Development - -### Setup - -```bash -# Create virtual environment -python3 -m venv venv -source venv/bin/activate - -# Install dependencies -pip install -r requirements-dev.txt - -# Run tests -pytest tests/ - -# Run server locally -python -m aegis_gitea_mcp.server -``` - -### Project Structure - -``` -AegisGitea-MCP/ -├── src/ -│ └── aegis_gitea_mcp/ -│ ├── __init__.py -│ ├── server.py # MCP server entry point -│ ├── mcp_protocol.py # MCP protocol implementation -│ ├── gitea_client.py # Gitea API client -│ ├── audit.py # Audit logging -│ ├── config.py # Configuration management -│ └── tools/ # MCP tool implementations -│ ├── __init__.py -│ ├── repository.py # Repository discovery tools -│ └── files.py # File access tools -├── tests/ -│ ├── test_mcp_protocol.py -│ ├── test_gitea_client.py -│ └── test_tools.py -├── docker/ -│ ├── Dockerfile -│ └── docker-compose.yml -├── .env.example -├── pyproject.toml -├── requirements.txt -├── requirements-dev.txt -└── README.md -``` - ---- - -## Non-Goals - -Explicitly **out of scope**: - -- No write access to Gitea (no commits, comments, merges, edits) -- No autonomous or background scanning -- No global search or unrestricted crawling -- No public exposure of repositories or credentials -- No coupling to GitHub or external VCS platforms - ---- - -## Roadmap - -- [x] Project initialization and architecture design -- [ ] **Phase 1**: MCP server foundation and Gitea integration -- [ ] **Phase 2**: Repository discovery and file access tools -- [ ] **Phase 3**: Audit logging and security hardening -- [ ] **Phase 4**: Commit history, issues, and PR support - ---- - -## Contributing - -This project prioritizes security and privacy. Contributions should: - -1. Maintain read-only guarantees -2. Add comprehensive audit logging for new tools -3. Include tests for authorization and boundary cases -4. Document security implications - ---- - -## License - -MIT License - See LICENSE file for details - ---- - -## Acknowledgments - -Built on the [Model Context Protocol](https://modelcontextprotocol.io/) by Anthropic. - ---- - -## Support - -For issues, questions, or security concerns, please open an issue in the Gitea repository. - -**Remember**: This is designed to be **boring, predictable, and safe** — not clever, not magical, and not autonomous. diff --git a/SECURITY.md b/SECURITY.md deleted file mode 100644 index be9b61d..0000000 --- a/SECURITY.md +++ /dev/null @@ -1,285 +0,0 @@ -# Security Policy - -## Supported Versions - -| Version | Supported | -| ------- | ------------------ | -| 0.1.x | :white_check_mark: | - ---- - -## Security Principles - -AegisGitea MCP is designed with security as the primary concern: - -1. **Read-Only by Design**: No write operations to Gitea -2. **Least Privilege**: Bot user has minimal necessary permissions -3. **Explicit Access**: No implicit or hidden data access -4. **Full Auditability**: Every action is logged -5. **Fail-Safe**: Errors deny access rather than grant it - ---- - -## Threat Model - -### In Scope - -- Unauthorized access to repositories -- Token compromise and misuse -- Data exfiltration via MCP tools -- Denial of service attacks -- API abuse and rate limiting bypass - -### Out of Scope - -- Physical access to server -- Social engineering attacks -- Compromise of Gitea instance itself -- ChatGPT platform security - ---- - -## Security Features - -### 1. Authorization - -- **Dynamic**: Permissions managed in Gitea, not MCP server -- **Explicit**: Bot user must be added to each repository -- **Reversible**: Removing bot user immediately revokes access - -### 2. Authentication - -- Token-based authentication with Gitea -- No password storage -- Tokens should be rotated regularly - -### 3. Audit Logging - -Every tool invocation logs: -- Timestamp (UTC) -- Tool name -- Repository accessed -- Target file/path -- Request correlation ID -- Success/failure status - -Logs are append-only and tamper-evident. - -### 4. Input Validation - -- File size limits enforced -- Path traversal protection -- Request timeout limits -- Rate limiting per minute - -### 5. Container Security - -- Runs as non-root user -- No unnecessary privileges -- Resource limits enforced -- Read-only filesystem where possible - ---- - -## Reporting a Vulnerability - -**Please do not report security vulnerabilities through public GitHub issues.** - -Instead: - -1. **Email**: security@youromain.com (replace with your actual contact) -2. **Subject**: `[SECURITY] AegisGitea MCP - Brief Description` -3. **Include**: - - Description of vulnerability - - Steps to reproduce - - Potential impact - - Suggested fix (if any) - -### Response Timeline - -- **24 hours**: Acknowledgment of report -- **7 days**: Initial assessment and severity rating -- **30 days**: Fix developed and tested -- **45 days**: Public disclosure (if applicable) - ---- - -## Security Best Practices - -### For Operators - -1. **Token Management** - - Use dedicated bot user (never personal accounts) - - Rotate tokens quarterly - - Store tokens in secrets manager - - Never commit tokens to version control - -2. **Network Security** - - Always use HTTPS with valid TLS certificates - - Never expose MCP server directly to internet - - Use reverse proxy for TLS termination - - Consider VPN or IP allowlisting - -3. **Access Control** - - Review bot user permissions monthly - - Remove access from archived repositories - - Document which repositories are AI-visible - - Implement approval workflow for new access - -4. **Monitoring** - - Review audit logs weekly - - Set up alerts for unusual access patterns - - Monitor for failed authentication attempts - - Track file access frequency - -5. **Updates** - - Keep Docker images updated - - Monitor security advisories - - Test updates in staging first - - Maintain rollback capability - -### For Developers - -1. **Code Review** - - All changes require peer review - - Security-critical changes require 2+ reviewers - - Automated tests must pass - -2. **Dependencies** - - Pin dependency versions - - Review dependency licenses - - Monitor for security advisories - - Use tools like `pip-audit` or `safety` - -3. **Testing** - - Write tests for authorization logic - - Test boundary conditions - - Include negative test cases - - Fuzz test inputs - ---- - -## Known Limitations - -1. **Trust in Gitea**: Authorization depends on Gitea's access control -2. **Token Security**: Compromised token = compromised access until rotated -3. **Rate Limiting**: Current implementation is per-server, not per-client -4. **Audit Log Size**: Logs grow unbounded (implement rotation) - ---- - -## Security Checklist - -Before deploying to production: - -- [ ] Bot user created with minimal permissions -- [ ] Access token generated with read-only scope -- [ ] TLS configured with valid certificate -- [ ] Reverse proxy properly configured -- [ ] Audit logging enabled and tested -- [ ] Resource limits set in Docker -- [ ] Firewall rules configured -- [ ] Monitoring and alerting set up -- [ ] Incident response plan documented -- [ ] Team trained on security procedures - ---- - -## Incident Response - -If you suspect a security breach: - -### Immediate Actions (within 5 minutes) - -1. **Isolate**: Stop the MCP container - ```bash - docker-compose down - ``` - -2. **Revoke**: Delete bot user token in Gitea - - Go to Gitea > Settings > Applications - - Delete the token immediately - -3. **Preserve**: Save audit logs for analysis - ```bash - docker cp aegis-gitea-mcp:/var/log/aegis-mcp/audit.log ./incident-$(date +%Y%m%d-%H%M%S).log - ``` - -### Investigation (within 1 hour) - -1. Review audit logs for unauthorized access -2. Check which repositories were accessed -3. Identify timeframe of suspicious activity -4. Document findings - -### Remediation (within 24 hours) - -1. Generate new bot user token -2. Review and update bot user permissions -3. Deploy updated configuration -4. Monitor for continued suspicious activity -5. Notify affected repository owners if necessary - -### Post-Incident (within 1 week) - -1. Conduct post-mortem analysis -2. Update security procedures -3. Implement additional safeguards -4. Document lessons learned -5. Train team on new procedures - ---- - -## Compliance Considerations - -### GDPR - -- Audit logs may contain personal data (usernames, timestamps) -- Implement log retention policy (recommend 90 days) -- Provide mechanism for data deletion requests - -### SOC 2 - -- Audit logging satisfies monitoring requirements -- Access control model supports least privilege -- Incident response procedures documented - -### Internal Policies - -- Adapt security practices to your organization's policies -- Document any deviations from standard procedures -- Obtain necessary approvals before deployment - ---- - -## Security Roadmap - -Future security enhancements (not yet implemented): - -- [ ] Multi-factor authentication for bot token generation -- [ ] Per-client rate limiting (not just per-server) -- [ ] Automated audit log analysis and anomaly detection -- [ ] Integration with SIEM systems -- [ ] Encrypted audit logs -- [ ] Support for multiple bot users with different permissions -- [ ] OAuth2 flow instead of static tokens -- [ ] Content scanning for sensitive data patterns - ---- - -## Acknowledgments - -Security vulnerabilities responsibly disclosed by: - -- (None yet - be the first!) - ---- - -## Contact - -Security Team: security@yourdomain.com -General Support: issues in Gitea repository - ---- - -**Remember**: Security is a process, not a product. Stay vigilant. diff --git a/TESTING.md b/TESTING.md deleted file mode 100644 index 51d00d4..0000000 --- a/TESTING.md +++ /dev/null @@ -1,441 +0,0 @@ -# Testing Guide - -Comprehensive guide for testing AegisGitea MCP. - ---- - -## Test Suite Overview - -The project includes a complete test suite covering: - -1. **Unit Tests** - Individual components (auth, config) -2. **Integration Tests** - Component interactions -3. **Server Tests** - API endpoints and middleware -4. **Manual Tests** - Real-world scenarios - ---- - -## Quick Start - -### Run All Tests - -```bash -# Using test script (recommended) -./run_tests.sh - -# Or using Make -make test - -# Or directly with pytest -pytest tests/ -v -``` - -### Run Specific Test Files - -```bash -# Authentication tests only -pytest tests/test_auth.py -v - -# Server endpoint tests only -pytest tests/test_server.py -v - -# Integration tests only -pytest tests/test_integration.py -v -``` - -### Run with Coverage - -```bash -pytest tests/ --cov=aegis_gitea_mcp --cov-report=html - -# View coverage report -open htmlcov/index.html # macOS -xdg-open htmlcov/index.html # Linux -``` - ---- - -## Test Modules - -### 1. `test_auth.py` - Authentication Module Tests - -**Coverage:** -- API key generation (length, format) -- Key hashing (SHA256) -- Bearer token extraction -- Constant-time comparison -- Rate limiting (per IP) -- Multiple keys support -- Auth enable/disable - -**Key Tests:** -```python -test_generate_api_key() # 64-char hex key generation -test_validate_api_key_valid() # Valid key acceptance -test_validate_api_key_invalid() # Invalid key rejection -test_rate_limiting() # 5 attempts/5min limit -test_multiple_keys() # Comma-separated keys -``` - -**Run:** -```bash -pytest tests/test_auth.py -v -``` - ---- - -### 2. `test_server.py` - Server Endpoint Tests - -**Coverage:** -- Middleware authentication -- Protected vs public endpoints -- Authorization header formats -- Rate limiting at HTTP level -- Error messages - -**Key Tests:** -```python -test_health_endpoint_no_auth_required() # /health is public -test_list_tools_without_auth() # /mcp/tools requires auth -test_list_tools_with_valid_key() # Valid key works -test_auth_header_formats() # Bearer format validation -test_rate_limiting() # HTTP-level rate limit -``` - -**Run:** -```bash -pytest tests/test_server.py -v -``` - ---- - -### 3. `test_integration.py` - Integration Tests - -**Coverage:** -- Complete authentication flow -- Key rotation simulation -- Multi-tool discovery -- Concurrent requests -- Error handling - -**Key Tests:** -```python -test_complete_authentication_flow() # End-to-end flow -test_key_rotation_simulation() # Grace period handling -test_all_mcp_tools_discoverable() # Tool registration -test_concurrent_requests_different_ips() # Per-IP rate limiting -``` - -**Run:** -```bash -pytest tests/test_integration.py -v -``` - ---- - -### 4. `test_config.py` - Configuration Tests - -**Coverage:** -- Environment variable loading -- Default values -- Validation rules -- Required fields - -**Key Tests:** -```python -test_settings_from_env() # Env var parsing -test_settings_defaults() # Default values -test_settings_invalid_log_level() # Validation -``` - -**Run:** -```bash -pytest tests/test_config.py -v -``` - ---- - -## Manual Testing - -### Prerequisites - -```bash -# 1. Generate API key -make generate-key - -# 2. Add to .env -echo "MCP_API_KEYS=your-key-here" >> .env - -# 3. Build and start -docker-compose build -docker-compose up -d -``` - -### Test 1: Health Check (No Auth) - -```bash -curl http://localhost:8080/health - -# Expected: {"status": "healthy"} -``` - -### Test 2: Protected Endpoint Without Auth - -```bash -curl http://localhost:8080/mcp/tools - -# Expected: 401 Unauthorized -``` - -### Test 3: Protected Endpoint With Invalid Key - -```bash -curl -H "Authorization: Bearer invalid-key-1234567890123456789012345678901234" \ - http://localhost:8080/mcp/tools - -# Expected: 401 Unauthorized with "Invalid API key" message -``` - -### Test 4: Protected Endpoint With Valid Key - -```bash -curl -H "Authorization: Bearer YOUR_API_KEY_HERE" \ - http://localhost:8080/mcp/tools - -# Expected: 200 OK with JSON list of tools -``` - -### Test 5: Rate Limiting - -```bash -# Run this script to trigger rate limit: -for i in {1..6}; do - curl -H "Authorization: Bearer wrong-key-12345678901234567890123456789012345" \ - http://localhost:8080/mcp/tools - echo "" -done - -# 6th request should return "Too many failed authentication attempts" -``` - -### Test 6: Key Rotation - -```bash -# 1. Add second key to .env -MCP_API_KEYS=old-key,new-key - -# 2. Restart -docker-compose restart aegis-mcp - -# 3. Test both keys work -curl -H "Authorization: Bearer old-key" http://localhost:8080/mcp/tools -curl -H "Authorization: Bearer new-key" http://localhost:8080/mcp/tools - -# 4. Remove old key -MCP_API_KEYS=new-key - -# 5. Restart and verify only new key works -docker-compose restart aegis-mcp -curl -H "Authorization: Bearer old-key" http://localhost:8080/mcp/tools # Should fail -curl -H "Authorization: Bearer new-key" http://localhost:8080/mcp/tools # Should work -``` - -### Test 7: ChatGPT Integration - -1. Configure ChatGPT Business with your API key -2. Ask: "List my Gitea repositories" -3. Verify response contains repository list -4. Check audit logs: - ```bash - docker-compose exec aegis-mcp cat /var/log/aegis-mcp/audit.log | grep "api_authentication" - ``` - ---- - -## Test Data - -### Valid API Keys (for testing) - -```python -# 64-character keys -KEY_A = "a" * 64 -KEY_B = "b" * 64 -KEY_C = "c" * 64 - -# Invalid keys (too short) -INVALID_SHORT = "short" -INVALID_32 = "x" * 31 # Less than 32 chars -``` - -### Test Environment Variables - -```bash -# Minimal test environment -GITEA_URL=https://gitea.example.com -GITEA_TOKEN=test-token-12345 -AUTH_ENABLED=true -MCP_API_KEYS=aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa - -# Auth disabled for testing -AUTH_ENABLED=false -MCP_API_KEYS= # Can be empty when disabled -``` - ---- - -## CI/CD Integration - -### GitHub Actions Example - -```yaml -name: Tests - -on: [push, pull_request] - -jobs: - test: - runs-on: ubuntu-latest - - steps: - - uses: actions/checkout@v3 - - - name: Set up Python - uses: actions/setup-python@v4 - with: - python-version: '3.11' - - - name: Install dependencies - run: | - pip install -r requirements-dev.txt - - - name: Run tests - run: | - pytest tests/ -v --cov=aegis_gitea_mcp - - - name: Upload coverage - uses: codecov/codecov-action@v3 -``` - ---- - -## Performance Testing - -### Load Test Example - -```bash -# Install Apache Bench -sudo apt-get install apache2-utils - -# Test authenticated endpoint (100 requests, 10 concurrent) -ab -n 100 -c 10 \ - -H "Authorization: Bearer YOUR_KEY" \ - http://localhost:8080/mcp/tools - -# Check results: -# - Requests per second -# - Mean time per request -# - Longest request -``` - -### Expected Performance - -- **Health check**: < 10ms per request -- **List tools** (authenticated): < 50ms per request -- **Rate limit enforcement**: < 5ms overhead - ---- - -## Troubleshooting Tests - -### Issue: Import errors - -```bash -# Solution: Install dev dependencies -pip install -r requirements-dev.txt -``` - -### Issue: "No module named 'aegis_gitea_mcp'" - -```bash -# Solution: Set PYTHONPATH -export PYTHONPATH="${PYTHONPATH}:$(pwd)/src" - -# Or run from project root -cd /path/to/AegisGitea-MCP -pytest tests/ -``` - -### Issue: Tests pass locally but fail in Docker - -```bash -# Solution: Run tests inside Docker -docker-compose exec aegis-mcp pytest /app/tests/ -``` - -### Issue: Rate limiting tests interfere with each other - -```bash -# Solution: Run tests with fresh validator each time -pytest tests/test_auth.py::test_rate_limiting -v --tb=short -``` - ---- - -## Code Coverage Goals - -| Module | Target Coverage | Current | -|--------|----------------|---------| -| `auth.py` | > 90% | ✅ | -| `config.py` | > 85% | ✅ | -| `server.py` | > 80% | ✅ | -| `mcp_protocol.py` | > 90% | ✅ | -| `gitea_client.py` | > 70% | ⏳ Requires mocking | -| `audit.py` | > 80% | ⏳ Requires file I/O | - ---- - -## Test Maintenance - -### Adding New Tests - -1. Create test file: `tests/test_.py` -2. Follow existing patterns: - ```python - def test_descriptive_name(): - """Clear description of what's being tested.""" - # Arrange - # Act - # Assert - ``` -3. Run tests to ensure they pass -4. Update this doc with new test coverage - -### Updating Tests - -- Run full suite after changes: `./run_tests.sh` -- Check coverage: `pytest --cov` -- Update test data if needed - ---- - -## Best Practices - -1. **Isolation**: Each test should be independent -2. **Fixtures**: Use pytest fixtures for setup/teardown -3. **Naming**: `test___` -4. **Assertions**: One logical assertion per test -5. **Documentation**: Clear docstrings explaining purpose - ---- - -## Next Steps - -- [ ] Add tests for Gitea client (with mocking) -- [ ] Add tests for audit logging (with temp files) -- [ ] Add performance benchmarks -- [ ] Add end-to-end tests with real Gitea instance -- [ ] Set up continuous testing in CI/CD - ---- - -**Test coverage is currently ~85% for core authentication and server modules. All critical paths are covered.** diff --git a/scripts/check_key_age.py b/scripts/check_key_age.py index b5d15b5..94d0933 100755 --- a/scripts/check_key_age.py +++ b/scripts/check_key_age.py @@ -1,10 +1,9 @@ #!/usr/bin/env python3 """Check API key age and alert if rotation needed.""" -import os import re import sys -from datetime import datetime, timedelta, timezone +from datetime import datetime, timezone from pathlib import Path @@ -46,7 +45,7 @@ def main() -> None: critical = [] for metadata_file in sorted(metadata_files): - with open(metadata_file, "r") as f: + with open(metadata_file, "r", encoding="utf-8") as f: content = f.read() # Extract metadata diff --git a/scripts/generate_api_key.py b/scripts/generate_api_key.py index be4aaf5..c91c320 100755 --- a/scripts/generate_api_key.py +++ b/scripts/generate_api_key.py @@ -87,7 +87,7 @@ def main() -> None: key_id = api_key[:12] metadata_file = keys_dir / f"key-{key_id}-{created_at.strftime('%Y%m%d')}.txt" - with open(metadata_file, "w") as f: + with open(metadata_file, "w", encoding="utf-8") as f: f.write(f"API Key Metadata\n") f.write(f"================\n\n") f.write(f"Key ID: {key_id}...\n") diff --git a/scripts/rotate_api_key.py b/scripts/rotate_api_key.py index 6de9794..f9f6c54 100755 --- a/scripts/rotate_api_key.py +++ b/scripts/rotate_api_key.py @@ -1,7 +1,6 @@ #!/usr/bin/env python3 """Rotate API key for AegisGitea MCP.""" -import os import re import sys from datetime import datetime, timedelta, timezone @@ -31,7 +30,7 @@ def main() -> None: sys.exit(1) # Read current .env - with open(env_file, "r") as f: + with open(env_file, "r", encoding="utf-8") as f: env_content = f.read() # Check if MCP_API_KEYS exists @@ -115,13 +114,13 @@ def main() -> None: # Backup old .env backup_file = env_file.with_suffix(f".env.backup-{datetime.now().strftime('%Y%m%d-%H%M%S')}") - with open(backup_file, "w") as f: + with open(backup_file, "w", encoding="utf-8") as f: f.write(env_content) print(f"✓ Backed up old .env to: {backup_file.name}") # Write new .env - with open(env_file, "w") as f: + with open(env_file, "w", encoding="utf-8") as f: f.write(new_env_content) print(f"✓ Updated .env file with new key(s)") diff --git a/src/aegis_gitea_mcp/audit.py b/src/aegis_gitea_mcp/audit.py index e473cff..cc1b538 100644 --- a/src/aegis_gitea_mcp/audit.py +++ b/src/aegis_gitea_mcp/audit.py @@ -25,6 +25,7 @@ class AuditLogger: # Ensure log directory exists self.log_path.parent.mkdir(parents=True, exist_ok=True) + self._log_file = self._get_log_file() # Configure structlog for audit logging structlog.configure( @@ -35,7 +36,7 @@ class AuditLogger: ], wrapper_class=structlog.BoundLogger, context_class=dict, - logger_factory=structlog.PrintLoggerFactory(file=self._get_log_file()), + logger_factory=structlog.PrintLoggerFactory(file=self._log_file), cache_logger_on_first_use=True, ) @@ -45,6 +46,13 @@ class AuditLogger: """Get file handle for audit log.""" return open(self.log_path, "a", encoding="utf-8") + def close(self) -> None: + """Close open audit log resources.""" + try: + self._log_file.close() + except Exception: + pass + def log_tool_invocation( self, tool_name: str, @@ -166,4 +174,6 @@ def get_audit_logger() -> AuditLogger: def reset_audit_logger() -> None: """Reset global audit logger instance (primarily for testing).""" global _audit_logger + if _audit_logger is not None: + _audit_logger.close() _audit_logger = None diff --git a/src/aegis_gitea_mcp/config.py b/src/aegis_gitea_mcp/config.py index f784f44..bbdb526 100644 --- a/src/aegis_gitea_mcp/config.py +++ b/src/aegis_gitea_mcp/config.py @@ -1,7 +1,7 @@ """Configuration management for AegisGitea MCP server.""" from pathlib import Path -from typing import List, Optional +from typing import Optional from pydantic import Field, HttpUrl, field_validator, model_validator from pydantic_settings import BaseSettings, SettingsConfigDict @@ -112,20 +112,20 @@ class Settings(BaseSettings): def validate_and_parse_api_keys(self) -> "Settings": """Parse and validate API keys if authentication is enabled.""" # Parse comma-separated keys into list - keys = [] + keys: list[str] = [] if self.mcp_api_keys_raw and self.mcp_api_keys_raw.strip(): keys = [key.strip() for key in self.mcp_api_keys_raw.split(",") if key.strip()] - + # Store in a property we'll access object.__setattr__(self, "_mcp_api_keys", keys) - + # Validate if auth is enabled if self.auth_enabled and not keys: raise ValueError( "At least one API key must be configured when auth_enabled=True. " "Set MCP_API_KEYS environment variable or disable auth with AUTH_ENABLED=false" ) - + # Validate key format (at least 32 characters for security) for key in keys: if len(key) < 32: @@ -133,11 +133,11 @@ class Settings(BaseSettings): f"API keys must be at least 32 characters long. " f"Use scripts/generate_api_key.py to generate secure keys." ) - + return self - + @property - def mcp_api_keys(self) -> List[str]: + def mcp_api_keys(self) -> list[str]: """Get parsed list of API keys.""" return getattr(self, "_mcp_api_keys", []) @@ -155,7 +155,7 @@ def get_settings() -> Settings: """Get or create global settings instance.""" global _settings if _settings is None: - _settings = Settings() # type: ignore + _settings = Settings() return _settings diff --git a/src/aegis_gitea_mcp/gitea_client.py b/src/aegis_gitea_mcp/gitea_client.py index 1910c50..93e7284 100644 --- a/src/aegis_gitea_mcp/gitea_client.py +++ b/src/aegis_gitea_mcp/gitea_client.py @@ -69,7 +69,7 @@ class GiteaClient: if self.client: await self.client.aclose() - def _handle_response(self, response: Response, correlation_id: str) -> Dict[str, Any]: + def _handle_response(self, response: Response, correlation_id: str) -> Any: """Handle Gitea API response and raise appropriate exceptions. Args: @@ -148,12 +148,12 @@ class GiteaClient: return user_data - except Exception as e: + except Exception as exc: self.audit.log_tool_invocation( tool_name="get_current_user", correlation_id=correlation_id, result_status="error", - error=str(e), + error=str(exc), ) raise @@ -177,7 +177,7 @@ class GiteaClient: try: response = await self.client.get("/api/v1/user/repos") repos_data = self._handle_response(response, correlation_id) - + # Ensure we have a list repos = repos_data if isinstance(repos_data, list) else [] @@ -190,12 +190,12 @@ class GiteaClient: return repos - except Exception as e: + except Exception as exc: self.audit.log_tool_invocation( tool_name="list_repositories", correlation_id=correlation_id, result_status="error", - error=str(e), + error=str(exc), ) raise @@ -236,13 +236,13 @@ class GiteaClient: return repo_data - except Exception as e: + except Exception as exc: self.audit.log_tool_invocation( tool_name="get_repository", repository=repo_id, correlation_id=correlation_id, result_status="error", - error=str(e), + error=str(exc), ) raise @@ -314,14 +314,14 @@ class GiteaClient: return file_data - except Exception as e: + except Exception as exc: self.audit.log_tool_invocation( tool_name="get_file_contents", repository=repo_id, target=filepath, correlation_id=correlation_id, result_status="error", - error=str(e), + error=str(exc), ) raise @@ -370,12 +370,12 @@ class GiteaClient: return tree_data - except Exception as e: + except Exception as exc: self.audit.log_tool_invocation( tool_name="get_tree", repository=repo_id, correlation_id=correlation_id, result_status="error", - error=str(e), + error=str(exc), ) raise diff --git a/src/aegis_gitea_mcp/mcp_protocol.py b/src/aegis_gitea_mcp/mcp_protocol.py index 010cf91..83b2fed 100644 --- a/src/aegis_gitea_mcp/mcp_protocol.py +++ b/src/aegis_gitea_mcp/mcp_protocol.py @@ -2,7 +2,7 @@ from typing import Any, Dict, List, Optional -from pydantic import BaseModel, Field +from pydantic import BaseModel, ConfigDict, Field class MCPTool(BaseModel): @@ -14,9 +14,10 @@ class MCPTool(BaseModel): ..., alias="inputSchema", description="JSON Schema for tool input" ) - class Config: - populate_by_name = True - by_alias = True + model_config = ConfigDict( + populate_by_name=True, + serialize_by_alias=True, + ) class MCPToolCallRequest(BaseModel): diff --git a/src/aegis_gitea_mcp/server.py b/src/aegis_gitea_mcp/server.py index 27bb160..0da0e10 100644 --- a/src/aegis_gitea_mcp/server.py +++ b/src/aegis_gitea_mcp/server.py @@ -40,9 +40,7 @@ app = FastAPI( ) # Global settings and audit logger -# Note: auth_validator is fetched dynamically in middleware to support test resets -settings = get_settings() -audit = get_audit_logger() +# Note: access settings/audit logger dynamically to support test resets. # Tool dispatcher mapping @@ -115,6 +113,7 @@ async def authenticate_request(request: Request, call_next): @app.on_event("startup") async def startup_event() -> None: """Initialize server on startup.""" + settings = get_settings() logger.info(f"Starting AegisGitea MCP Server on {settings.mcp_host}:{settings.mcp_port}") logger.info(f"Connected to Gitea instance: {settings.gitea_base_url}") logger.info(f"Audit logging enabled: {settings.audit_log_path}") @@ -180,6 +179,7 @@ async def call_tool(request: MCPToolCallRequest) -> JSONResponse: Returns: JSON response with tool execution result """ + audit = get_audit_logger() correlation_id = request.correlation_id or audit.log_tool_invocation( tool_name=request.tool, params=request.arguments, @@ -312,6 +312,7 @@ async def sse_message_handler(request: Request) -> JSONResponse: JSON response acknowledging the message """ try: + audit = get_audit_logger() body = await request.json() logger.info(f"Received MCP message via SSE POST: {body}") diff --git a/src/aegis_gitea_mcp/tools/repository.py b/src/aegis_gitea_mcp/tools/repository.py index 1073383..7e7e00b 100644 --- a/src/aegis_gitea_mcp/tools/repository.py +++ b/src/aegis_gitea_mcp/tools/repository.py @@ -40,8 +40,8 @@ async def list_repositories_tool(gitea: GiteaClient, arguments: Dict[str, Any]) "count": len(simplified_repos), } - except GiteaError as e: - raise Exception(f"Failed to list repositories: {str(e)}") + except GiteaError as exc: + raise RuntimeError(f"Failed to list repositories: {exc}") from exc async def get_repository_info_tool( @@ -84,8 +84,8 @@ async def get_repository_info_tool( "clone_url": repo_data.get("clone_url", ""), } - except GiteaError as e: - raise Exception(f"Failed to get repository info: {str(e)}") + except GiteaError as exc: + raise RuntimeError(f"Failed to get repository info: {exc}") from exc async def get_file_tree_tool(gitea: GiteaClient, arguments: Dict[str, Any]) -> Dict[str, Any]: @@ -129,8 +129,8 @@ async def get_file_tree_tool(gitea: GiteaClient, arguments: Dict[str, Any]) -> D "count": len(simplified_tree), } - except GiteaError as e: - raise Exception(f"Failed to get file tree: {str(e)}") + except GiteaError as exc: + raise RuntimeError(f"Failed to get file tree: {exc}") from exc async def get_file_contents_tool(gitea: GiteaClient, arguments: Dict[str, Any]) -> Dict[str, Any]: @@ -185,5 +185,5 @@ async def get_file_contents_tool(gitea: GiteaClient, arguments: Dict[str, Any]) "url": file_data.get("html_url", ""), } - except GiteaError as e: - raise Exception(f"Failed to get file contents: {str(e)}") + except GiteaError as exc: + raise RuntimeError(f"Failed to get file contents: {exc}") from exc diff --git a/tests/conftest.py b/tests/conftest.py index 2dc2258..fa45cf0 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,7 +1,5 @@ """Pytest configuration and fixtures.""" -import os -import tempfile from pathlib import Path from typing import Generator diff --git a/tests/test_auth.py b/tests/test_auth.py index 597a528..693aebc 100644 --- a/tests/test_auth.py +++ b/tests/test_auth.py @@ -5,8 +5,8 @@ import pytest from aegis_gitea_mcp.auth import ( APIKeyValidator, generate_api_key, - hash_api_key, get_validator, + hash_api_key, reset_validator, ) from aegis_gitea_mcp.config import reset_settings @@ -40,7 +40,7 @@ def validator(mock_env_with_key): def test_generate_api_key(): """Test API key generation.""" key = generate_api_key(length=64) - + assert len(key) == 64 assert all(c in "0123456789abcdef" for c in key) @@ -48,7 +48,7 @@ def test_generate_api_key(): def test_generate_api_key_custom_length(): """Test API key generation with custom length.""" key = generate_api_key(length=128) - + assert len(key) == 128 @@ -56,7 +56,7 @@ def test_hash_api_key(): """Test API key hashing.""" key = "test-key-12345" hashed = hash_api_key(key) - + assert len(hashed) == 64 # SHA256 produces 64-char hex string assert hashed == hash_api_key(key) # Deterministic @@ -65,7 +65,7 @@ def test_validator_singleton(): """Test that get_validator returns same instance.""" validator1 = get_validator() validator2 = get_validator() - + assert validator1 is validator2 @@ -73,10 +73,10 @@ def test_constant_time_compare(validator): """Test constant-time string comparison.""" # Same strings assert validator._constant_time_compare("test", "test") - + # Different strings assert not validator._constant_time_compare("test", "fail") - + # Different lengths assert not validator._constant_time_compare("test", "testing") @@ -86,19 +86,19 @@ def test_extract_bearer_token(validator): # Valid bearer token token = validator.extract_bearer_token("Bearer abc123") assert token == "abc123" - + # No header token = validator.extract_bearer_token(None) assert token is None - + # Wrong format token = validator.extract_bearer_token("abc123") assert token is None - + # Wrong scheme token = validator.extract_bearer_token("Basic abc123") assert token is None - + # Too many parts token = validator.extract_bearer_token("Bearer abc 123") assert token is None @@ -107,7 +107,7 @@ def test_extract_bearer_token(validator): def test_validate_api_key_missing(validator): """Test validation with missing API key.""" is_valid, error = validator.validate_api_key(None, "127.0.0.1", "test-agent") - + assert not is_valid assert "Authorization header missing" in error @@ -115,7 +115,7 @@ def test_validate_api_key_missing(validator): def test_validate_api_key_too_short(validator): """Test validation with too-short API key.""" is_valid, error = validator.validate_api_key("short", "127.0.0.1", "test-agent") - + assert not is_valid assert "Invalid API key format" in error @@ -123,7 +123,7 @@ def test_validate_api_key_too_short(validator): def test_validate_api_key_invalid(validator, mock_env_with_key): """Test validation with invalid API key.""" is_valid, error = validator.validate_api_key("b" * 64, "127.0.0.1", "test-agent") - + assert not is_valid assert "Invalid API key" in error @@ -131,7 +131,7 @@ def test_validate_api_key_invalid(validator, mock_env_with_key): def test_validate_api_key_valid(validator, mock_env_with_key): """Test validation with valid API key.""" is_valid, error = validator.validate_api_key("a" * 64, "127.0.0.1", "test-agent") - + assert is_valid assert error is None @@ -139,12 +139,12 @@ def test_validate_api_key_valid(validator, mock_env_with_key): def test_rate_limiting(validator): """Test rate limiting after multiple failures.""" client_ip = "192.168.1.1" - + # First 5 attempts should be allowed - for i in range(5): + for _ in range(5): is_valid, error = validator.validate_api_key("b" * 64, client_ip, "test-agent") assert not is_valid - + # 6th attempt should be rate limited is_valid, error = validator.validate_api_key("b" * 64, client_ip, "test-agent") assert not is_valid @@ -155,15 +155,15 @@ def test_rate_limiting_per_ip(validator): """Test that rate limiting is per IP address.""" ip1 = "192.168.1.1" ip2 = "192.168.1.2" - + # Fail 5 times from IP1 - for i in range(5): + for _ in range(5): validator.validate_api_key("b" * 64, ip1, "test-agent") - + # IP1 should be rate limited is_valid, error = validator.validate_api_key("b" * 64, ip1, "test-agent") assert "Too many failed" in error - + # IP2 should still work is_valid, error = validator.validate_api_key("b" * 64, ip2, "test-agent") assert "Invalid API key" in error # Wrong key, but not rate limited @@ -175,9 +175,9 @@ def test_auth_disabled(monkeypatch): monkeypatch.setenv("GITEA_TOKEN", "test-token-12345") monkeypatch.setenv("AUTH_ENABLED", "false") monkeypatch.setenv("MCP_API_KEYS", "") # No keys needed when disabled - + validator = APIKeyValidator() - + # Should allow access without key is_valid, error = validator.validate_api_key(None, "127.0.0.1", "test-agent") assert is_valid @@ -190,19 +190,19 @@ def test_multiple_keys(monkeypatch): monkeypatch.setenv("GITEA_TOKEN", "test-token-12345") monkeypatch.setenv("AUTH_ENABLED", "true") monkeypatch.setenv("MCP_API_KEYS", f"{'a' * 64},{'b' * 64},{'c' * 64}") - + validator = APIKeyValidator() - + # All three keys should work is_valid, _ = validator.validate_api_key("a" * 64, "127.0.0.1", "test-agent") assert is_valid - + is_valid, _ = validator.validate_api_key("b" * 64, "127.0.0.1", "test-agent") assert is_valid - + is_valid, _ = validator.validate_api_key("c" * 64, "127.0.0.1", "test-agent") assert is_valid - + # Invalid key should fail is_valid, _ = validator.validate_api_key("d" * 64, "127.0.0.1", "test-agent") assert not is_valid diff --git a/tests/test_integration.py b/tests/test_integration.py index 5ad8d58..305a681 100644 --- a/tests/test_integration.py +++ b/tests/test_integration.py @@ -120,7 +120,7 @@ def test_concurrent_requests_different_ips(client): tool_call_data = {"tool": "list_repositories", "arguments": {}} # Make 5 failed attempts on protected endpoint - for i in range(5): + for _ in range(5): response = client.post("/mcp/tool/call", headers=headers_invalid, json=tool_call_data) assert response.status_code == 401 diff --git a/tests/test_server.py b/tests/test_server.py index 209067c..5dc73db 100644 --- a/tests/test_server.py +++ b/tests/test_server.py @@ -203,7 +203,7 @@ def test_rate_limiting(client): tool_data = {"tool": "list_repositories", "arguments": {}} # Make 6 failed attempts on protected endpoint - for i in range(6): + for _ in range(6): response = client.post( "/mcp/tool/call", headers={"Authorization": "Bearer " + "x" * 64}, json=tool_data )