Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 32 additions & 0 deletions app/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,15 @@
DEFAULT_ATTEMPT_TTL_SECONDS = 24 * 60 * 60
MIN_ATTEMPT_TTL_SECONDS = 60
MAX_ATTEMPT_TTL_SECONDS = 7 * 24 * 60 * 60
WEBHOOK_OUTCOME_SCAN_ORDER = {
"missing_submitter": 0,
"bounty_not_found": 1,
"exhausted_bounty": 2,
"duplicate_delivery": 3,
"delivery_payload_mismatch": 4,
"already_paid": 5,
"paid": 6,
}


def _request_was_forwarded_https(request: Request) -> bool:
Expand Down Expand Up @@ -247,6 +256,27 @@ def _existing_payout_proof_for_submission(
)


def webhook_status_summary(session: Session) -> list[dict[str, Any]]:
status_expr = func.lower(WebhookEvent.processed_status)
count_expr = func.count(WebhookEvent.delivery_id)
rows = session.execute(
select(status_expr, count_expr)
.group_by(status_expr)
.order_by(count_expr.desc(), status_expr.asc())
).all()
summary = [
{"processed_status": str(status), "count": int(count)} for status, count in rows if status
]
return sorted(
summary,
key=lambda item: (
WEBHOOK_OUTCOME_SCAN_ORDER.get(str(item["processed_status"]), 100),
-int(item["count"]),
str(item["processed_status"]),
),
)


def _host_without_port(request: Request) -> str:
return request.headers.get("host", "").split(":", 1)[0].lower()

Expand Down Expand Up @@ -1432,13 +1462,15 @@ def admin_page(
WebhookEvent.created_at.desc(), WebhookEvent.delivery_id.desc()
).limit(webhook_limit)
).all()
webhook_summary = webhook_status_summary(session)
return templates.TemplateResponse(
request,
"admin.html",
{
"login": login,
"csrf_token": _csrf_token("admin-bounty", login, settings.cookie_secret),
"webhook_events": webhook_events,
"webhook_status_summary": webhook_summary,
"webhook_limit": webhook_limit,
"webhook_limit_options": [10, 25, 50, 100],
"webhook_status": normalized_status,
Expand Down
10 changes: 10 additions & 0 deletions app/templates/admin.html
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,16 @@ <h1>Post a bounty</h1>
<section class="detail">
<p class="eyebrow">Webhooks</p>
<h2>Recent delivery outcomes</h2>
{% if webhook_status_summary %}
<div class="bounty-list-summary" aria-label="Webhook processed status summary">
{% for item in webhook_status_summary %}
<article>
<span><code>{{ item.processed_status }}</code></span>
<strong>{{ item.count }}</strong>
</article>
{% endfor %}
</div>
{% endif %}
<form method="get" action="/admin" class="inline-form">
<label>Processed status
<input name="webhook_status" value="{{ webhook_status }}" placeholder="missing_submitter">
Expand Down
34 changes: 34 additions & 0 deletions tests/test_security.py
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,22 @@ def test_admin_page_renders_safe_webhook_events_for_cookie_admin(
processed_status="bounty_not_found",
)
)
session.add(
WebhookEvent(
delivery_id="delivery-missing-submitter-uppercase",
event_type="pull_request",
payload_hash="d" * 64,
processed_status="Missing_Submitter",
)
)
session.add(
WebhookEvent(
delivery_id="delivery-exhausted-bounty",
event_type="pull_request",
payload_hash="e" * 64,
processed_status="exhausted_bounty",
)
)
session.add(
WebhookEvent(
delivery_id="delivery-paid-secret-payload-body",
Expand All @@ -181,11 +197,29 @@ def test_admin_page_renders_safe_webhook_events_for_cookie_admin(
assert all_events.status_code == 200
assert "missing_submitter" in all_events.text
assert "bounty_not_found" in all_events.text
assert "exhausted_bounty" in all_events.text
assert re.search(
r"<code>missing_submitter</code>\s*</span>\s*<strong>2</strong>", all_events.text
)
summary_match = re.search(
r'<div class="bounty-list-summary"[^>]*>.*?</div>', all_events.text, flags=re.S
)
assert summary_match is not None
summary_html = summary_match.group(0)
status_summary_positions = [
summary_html.index("<code>missing_submitter</code>"),
summary_html.index("<code>bounty_not_found</code>"),
summary_html.index("<code>exhausted_bounty</code>"),
summary_html.index("<code>paid</code>"),
]
assert status_summary_positions == sorted(status_summary_positions)
assert "paid" in all_events.text
assert filtered.status_code == 200
assert "delivery-missing-submitter" in filtered.text
assert "delivery-missing-submitter-uppercase" in filtered.text
assert "missing_submitter" in filtered.text
assert "delivery-bounty-not-found" not in filtered.text
assert "bounty_not_found" in filtered.text
Comment thread
coderabbitai[bot] marked this conversation as resolved.
assert "a" * 64 in filtered.text
assert "secret-payload-body" not in filtered.text
assert limited.status_code == 200
Expand Down
Loading