feat(phases): implement webhooks (6.1), email (7.1), user preferences (7.2)
Some checks failed
Aegis CI / lint-and-test (push) Has been cancelled

- Phase 6.1: WebhookConfig model, CRUD router (/api/v1/webhooks, admin-only),
  dispatch_webhook() with HMAC signing; integrated into test validation,
  campaign completion, and MITRE sync job
- Phase 7.1: SMTP email service with send_test_validated_email,
  send_campaign_completed_email, send_new_mitre_techniques_email;
  notify_role_with_email() added to notification_service
- Phase 7.2: notification_preferences and jira_account_id on User model;
  PATCH /users/me/preferences endpoint; Alembic migrations b031phase6 and b032phase7

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
kitos
2026-05-19 13:40:45 +02:00
parent d6df7fdc09
commit c1e06d4c0a
16 changed files with 590 additions and 2 deletions

View File

@@ -0,0 +1,149 @@
"""Webhook configuration CRUD router — admin only.
Endpoints
---------
GET /webhooks — list all webhook configs
POST /webhooks — create a new webhook config
GET /webhooks/{id} — get a single webhook config
PATCH /webhooks/{id} — update a webhook config
DELETE /webhooks/{id} — hard-delete a webhook config
POST /webhooks/{id}/test — send a test ping
"""
import uuid
from fastapi import APIRouter, Depends, status
from sqlalchemy.orm import Session
from app.database import get_db
from app.dependencies.auth import require_role
from app.domain.unit_of_work import UnitOfWork
from app.models.user import User
from app.schemas.webhook import WebhookConfigCreate, WebhookConfigOut, WebhookConfigUpdate
from app.services.webhook_service import (
create_webhook,
delete_webhook,
dispatch_webhook,
get_webhook_or_raise,
list_webhooks,
update_webhook,
)
router = APIRouter(prefix="/webhooks", tags=["webhooks"])
def _mask_secret(wh) -> WebhookConfigOut:
"""Return a WebhookConfigOut with the secret masked."""
out = WebhookConfigOut.model_validate(wh)
if wh.secret:
out.secret = "***"
else:
out.secret = None
return out
# ---------------------------------------------------------------------------
# GET /webhooks
# ---------------------------------------------------------------------------
@router.get("", response_model=list[WebhookConfigOut])
def list_webhooks_route(
offset: int = 0,
limit: int = 50,
db: Session = Depends(get_db),
current_user: User = Depends(require_role("admin")),
):
"""Return all webhook configurations. **Requires admin role.**"""
webhooks = list_webhooks(db, offset=offset, limit=limit)
return [_mask_secret(wh) for wh in webhooks]
# ---------------------------------------------------------------------------
# POST /webhooks
# ---------------------------------------------------------------------------
@router.post("", response_model=WebhookConfigOut, status_code=status.HTTP_201_CREATED)
def create_webhook_route(
payload: WebhookConfigCreate,
db: Session = Depends(get_db),
current_user: User = Depends(require_role("admin")),
):
"""Create a new webhook configuration. **Requires admin role.**"""
with UnitOfWork(db) as uow:
wh = create_webhook(db, created_by=current_user.id, payload=payload)
uow.commit()
db.refresh(wh)
return _mask_secret(wh)
# ---------------------------------------------------------------------------
# GET /webhooks/{id}
# ---------------------------------------------------------------------------
@router.get("/{webhook_id}", response_model=WebhookConfigOut)
def get_webhook_route(
webhook_id: uuid.UUID,
db: Session = Depends(get_db),
current_user: User = Depends(require_role("admin")),
):
"""Return a single webhook configuration. **Requires admin role.**"""
wh = get_webhook_or_raise(db, webhook_id)
return _mask_secret(wh)
# ---------------------------------------------------------------------------
# PATCH /webhooks/{id}
# ---------------------------------------------------------------------------
@router.patch("/{webhook_id}", response_model=WebhookConfigOut)
def update_webhook_route(
webhook_id: uuid.UUID,
payload: WebhookConfigUpdate,
db: Session = Depends(get_db),
current_user: User = Depends(require_role("admin")),
):
"""Update one or more fields of a webhook configuration. **Requires admin role.**"""
with UnitOfWork(db) as uow:
wh = update_webhook(db, webhook_id, payload)
uow.commit()
db.refresh(wh)
return _mask_secret(wh)
# ---------------------------------------------------------------------------
# DELETE /webhooks/{id}
# ---------------------------------------------------------------------------
@router.delete("/{webhook_id}", status_code=status.HTTP_204_NO_CONTENT)
def delete_webhook_route(
webhook_id: uuid.UUID,
db: Session = Depends(get_db),
current_user: User = Depends(require_role("admin")),
):
"""Hard-delete a webhook configuration. **Requires admin role.**"""
with UnitOfWork(db) as uow:
delete_webhook(db, webhook_id)
uow.commit()
# ---------------------------------------------------------------------------
# POST /webhooks/{id}/test
# ---------------------------------------------------------------------------
@router.post("/{webhook_id}/test", status_code=status.HTTP_202_ACCEPTED)
def test_webhook_route(
webhook_id: uuid.UUID,
db: Session = Depends(get_db),
current_user: User = Depends(require_role("admin")),
):
"""Send a test ping to the webhook endpoint. **Requires admin role.**"""
# Verify the webhook exists before dispatching
get_webhook_or_raise(db, webhook_id)
dispatch_webhook("webhook.test", {"webhook_id": str(webhook_id), "message": "Test ping from Aegis"})
return {"detail": "Test ping dispatched"}