Files
stack/apps/coordinator/src/security.py
Jason Woltje e23c09f1f2 feat(#157): Set up webhook receiver endpoint
Implement FastAPI webhook receiver for Gitea issue assignment events
with HMAC SHA256 signature verification and event routing.

Implementation details:
- FastAPI application with /webhook/gitea POST endpoint
- HMAC SHA256 signature verification in security.py
- Event routing for assigned, unassigned, closed actions
- Comprehensive logging for all webhook events
- Health check endpoint at /health
- Docker containerization with health checks
- 91% test coverage (exceeds 85% requirement)

TDD workflow followed:
- Wrote 16 tests first (RED phase)
- Implemented features to pass tests (GREEN phase)
- All tests passing with 91% coverage
- Type checking with mypy: success
- Linting with ruff: success

Files created:
- apps/coordinator/src/main.py - FastAPI application
- apps/coordinator/src/webhook.py - Webhook handlers
- apps/coordinator/src/security.py - HMAC verification
- apps/coordinator/src/config.py - Configuration management
- apps/coordinator/tests/ - Comprehensive test suite
- apps/coordinator/Dockerfile - Production container
- apps/coordinator/pyproject.toml - Python project config

Configuration:
- Updated .env.example with GITEA_WEBHOOK_SECRET
- Updated docker-compose.yml with coordinator service

Testing:
- 16 unit and integration tests
- Security tests for signature verification
- Event handler tests for all supported actions
- Health check endpoint tests
- All tests passing with 91% coverage

This unblocks issue #158 (issue parser).

Fixes #157

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-02-01 17:41:46 -06:00

36 lines
1015 B
Python

"""Security utilities for webhook signature verification."""
import hashlib
import hmac
def verify_signature(payload: bytes, signature: str, secret: str) -> bool:
"""
Verify HMAC SHA256 signature of webhook payload.
Args:
payload: Raw request body as bytes
signature: Signature from X-Gitea-Signature header
secret: Webhook secret configured in Gitea
Returns:
True if signature is valid, False otherwise
Example:
>>> payload = b'{"action": "assigned"}'
>>> secret = "my-webhook-secret"
>>> sig = hmac.new(secret.encode(), payload, "sha256").hexdigest()
>>> verify_signature(payload, sig, secret)
True
"""
if not signature:
return False
# Compute expected signature
expected_signature = hmac.new(
secret.encode("utf-8"), payload, hashlib.sha256
).hexdigest()
# Use timing-safe comparison to prevent timing attacks
return hmac.compare_digest(signature, expected_signature)