Skip to content
Open
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
2 changes: 2 additions & 0 deletions Justfile
Original file line number Diff line number Diff line change
Expand Up @@ -591,6 +591,8 @@ test-unit:
+ test(=state::tests::disconnect_community_wins_reason_losing_nip_fi_does_not_enqueue_frame)
+ test(=state::tests::manager_disconnect_sets_reason_enqueues_frame_then_cancels)
+ test(/^api::nip_fi::/)'
# Retain partially consumed mesh receives across housekeeping ticks.
cargo nextest run -p buzz-relay --lib -E 'test(=mesh_boot::tests::demo_echo_retains_pending_receive_across_housekeeping_ticks)'
# boot_lifecycle spawns the real relay binary and asserts its startup
# lifecycle, including that buzz_startup_phase_* reaches /metrics. Its
# non-ignored tests need no Postgres or Redis; the Postgres cases are
Expand Down
19 changes: 18 additions & 1 deletion crates/buzz-db/src/store/event_follow_up_postgres_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1018,13 +1018,30 @@ async fn assert_async_isolation(db: &Db, community: CommunityId, keys: &Keys) {
endpoint_grant, max_class, subscriptions, expires_at, updated_at) \
SELECT $1, author, installation_id, source_event_id, source_created_at, generation, \
active, endpoint_enabled, endpoint_hash, endpoint_grant, max_class, \
subscriptions, expires_at, clock_timestamp() FROM push_leases WHERE community_id=$2",
subscriptions, expires_at, \
(SELECT received_at + interval '1 second' FROM events WHERE community_id=$1 AND id=$3) \
FROM push_leases WHERE community_id=$2",
)
.bind(later.as_uuid())
.bind(community.as_uuid())
.bind(before_enrollment.id.as_bytes().as_slice())
.execute(&mut *activation)
.await
.unwrap();
let leases_strictly_newer: bool = sqlx::query_scalar(
"SELECT count(*) > 0 AND bool_and(l.updated_at > e.received_at) \
FROM push_leases l JOIN events e ON e.community_id=l.community_id \
WHERE l.community_id=$1 AND e.id=$2",
)
.bind(later.as_uuid())
.bind(before_enrollment.id.as_bytes().as_slice())
.fetch_one(&mut *activation)
.await
.unwrap();
assert!(
leases_strictly_newer,
"fixture leases must follow recorded receipt"
);
activation.commit().await.unwrap();
assert_eq!(match_count(&isolated, later, &before_enrollment).await, 0);
assert_eq!(
Expand Down
25 changes: 24 additions & 1 deletion crates/buzz-relay/src/api/bridge.rs
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,10 @@ use super::{api_error, db_read_error, internal_error, not_found, parse_query_or_
mod thread_roots;
mod thread_window;

#[cfg(test)]
#[path = "bridge/workflow_webhook_postgres_tests.rs"]
mod workflow_webhook_postgres_tests;

pub(crate) async fn enforce_http_admission(
state: &AppState,
tenant: &TenantContext,
Expand Down Expand Up @@ -2409,7 +2413,11 @@ pub struct WebhookQuery {
/// Webhook trigger endpoint. No user auth — the webhook secret authenticates the caller.
///
/// Prefers `X-Webhook-Secret` header over `?secret=` query param (headers aren't logged
/// by most proxies). Returns 202 Accepted; execution is async.
/// by most proxies). Returns 202 Accepted; execution is async. A run is admitted
/// when its `workflow_runs` row is inserted; immediately before that insert,
/// admission checks the workflow owner's active ban in the host-bound community.
/// This point-in-time check does not cancel already-created runs, and a
/// concurrent ban committed after the lookup can race with row insertion.
pub async fn workflow_webhook(
State(state): State<Arc<AppState>>,
Path(id_str): Path<String>,
Expand Down Expand Up @@ -2525,6 +2533,21 @@ pub async fn workflow_webhook(
.await
.map_err(|_| not_found("workflow not found"))?;

// The workflow_runs insert is the admission boundary. This is the last
// authorization gate before it. The restriction read is authoritative and
// tenant-scoped; lookup failures deny admission just like an active ban. It
// is intentionally a point-in-time check rather than a transaction with ban
// writes, so a ban racing after this read may follow the run insertion. Runs
// already inserted are unaffected.
let restrictions = state
.db
.moderation_restriction_state(community_id, &workflow.owner_pubkey)
.await
.map_err(|_| not_found("workflow not found"))?;
if restrictions.banned {
return Err(not_found("workflow not found"));
}

let run_id = state
.db
.create_workflow_run(community_id, id, None, trigger_ctx_json.as_ref())
Expand Down
Loading
Loading