// Package adminnotify owns the shared flood cap for the DB-backed admin // notification centre (admin_notifications). That table is the single // operator's ONLY pager for money events, so every site that inserts an // operator-facing alert must bound its unacknowledged queue — a hostile flood // (attacker-registered accounts triggering refresh_token_reuse / reissue-fail / // webhook alerts) must not be able to bury the notification centre under rows // the operator can never work through. // // Coordination contract (Round 2 Loop B finding 1): the cap is applied // atomically at every insert site in this codebase: // // - handlers/payments/twofa.go — the reissue-fail alert (per-issue capped, // see finding 2; NOT globally capped). // - auth/jwt.go VerifyRefreshToken — the 'refresh_token_reuse' alert. // - handlers/webhooks/square.go — the three critical_payment_log inserts // (dispute, booking, unknown-event, orphan-replay). // - handlers/user/account.go InsertSquareErasureCriticalNotification. // // Sites owned by OTHER agents that still need the fold (coordination notes): // // - handlers/payments/sweep.go:1556 insertCriticalPaymentNotification (money // agent) — its INSERT ... SELECT ... WHERE NOT EXISTS is the same // unbounded-across-accounts shape; fold `AND (SELECT COUNT(*) FROM // admin_notifications _an WHERE _an.reason = 'critical_payment_log' AND // _an.acknowledged_at IS NULL) < $N` (N = MaxUnacknowledgedCriticalLogs) // into its WHERE clause. // - handlers/scheduling/time-blockers.go:511 insertSquareCleanupCriticalNotification // and internal/jobs/cleanup.go:333 ScanCriticalPaymentLogs — the same // 'critical_payment_log' insert shape on the GDPR-cleanup and log-scan // paths (owner: the jobs/scheduling agent). // - main.go has NO admin_notifications insert sites (it only mounts the // notification read/ack routes), so nothing to cap there. // // Every site folds the cap INTO its INSERT (a conditional // `INSERT ... SELECT ... WHERE (SELECT COUNT(*) ...) < $cap`) so the // count-then-insert is ATOMIC — closing the TOCTOU where two concurrent // inserts both read a below-cap count and overshoot together (finding 2). package adminnotify import ( "context" "log" "crussell/db" ) // MaxUnacknowledgedCriticalLogs is the GLOBAL cap on unacknowledged // admin_notifications rows for one reason (named after the money-critical // reason 'critical_payment_log' that the cap exists to protect). Once the // unacknowledged queue for a reason reaches the cap, further inserts for that // reason are dropped (with a suppression log for operator visibility) until // the operator acknowledges outstanding rows. 100 is far beyond anything a // single salon produces legitimately, so it only ever suppresses an abnormal // flood. const MaxUnacknowledgedCriticalLogs = 100 // CriticalLogsCapExceeded reports whether the unacknowledged admin-notification // queue for reason has reached MaxUnacknowledgedCriticalLogs. q routes through // the caller's transaction when one is active (the ctx-routed db.Conn proxy, or // an explicit pgx.Tx). // // Best-effort and fail-OPEN: a count error is logged and false is returned, so // a money alert is never dropped because the count query failed (the insert's // own atomic cap condition below still guards the row in that case — the // pre-check only decides whether to log a suppression). func CriticalLogsCapExceeded(ctx context.Context, q db.Querier, reason string) bool { var n int err := q.QueryRow(ctx, ` SELECT COUNT(*) FROM admin_notifications WHERE reason = $1::admin_notification_reason AND acknowledged_at IS NULL `, reason).Scan(&n) if err != nil { log.Printf("adminnotify: failed to count unacknowledged %s admin notifications: %v", reason, err) return false } return n >= MaxUnacknowledgedCriticalLogs }