Bulk-reassign endpoint, gated on REASSIGN_LEADS_MANUALLY

FeatureSecurityService
Shipped
July 22, 2026 at 8:23 PM UTC
Author
Kamo
Commit
972f4de

POST **************** moves a batch of leads onto one team member and emails that member once for the whole batch. Authorization uses REASSIGN_LEADS_MANUALLY (149) — the right named for exactly this. That is stricter than the single-lead path on PUT /leads/{id}, which checks ASSIGN_LEADS; reassigning a hundred leads is a different blast radius from reassigning one, and the two rights exist so they can be granted apart. The unassigned-visibility tier is enforced here too. Assignment IS a visibility grant: once a lead is assigned to the caller it reads as "assigned to me", which lifts the unassigned-read 403 and skips the contact-info mask. Without this check a caller holding 149 but not VIEW_UNASSIGNED_LEADS could self-assign leads the grid hides from them and launder them into fully readable ones. Skipped leads report NOT_FOUND — matching what their own grid shows — so the response can't enumerate hidden or foreign leads. The batch is deliberately not all-or-nothing: each assignLead runs in its own transaction and a bad id becomes a skip, because discarding nine good reassignments over the tenth is worse than reporting which one didn't take. Capped at 250 per call to bound both the row locks and the realtime fan-out. The notifier resolves its data in a short transaction through a separate bean and sends afterwards, so an outbound call with a 30s read timeout never pins a CRDB connection. Crossing the bean boundary is also what makes @Transactional apply at all — self-invocation would silently run with no transaction and the LAZY user association would throw.

All changes

Like what you see shipping?

Every one of these updates lands in your workspace automatically. Start free and watch it grow week after week.

Start Free ForeverView Pricing