You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
security(dci): scope notification delivery per subscriber (consent + filter) (#258)
* security(dci): scope notification delivery per subscriber (consent + filter)
The DCI notification pipeline built full DCI person/group records with no
sender context and fanned the identical payload out to every subscription
matched only on event/registry/active/expiry. Stored filter_expression was
never applied, consent was never checked, and auto_approve removed the human
gate -- so any active/auto-approved subscriber to a broad event received full
PII (names, birthdate, address, phone, email, household, programmes) for every
changed registrant, outside its requested filter, consent, or legal basis.
Make delivery per-subscription and sender-scoped:
- spp_dci_server: capture filter_type at subscribe time; add eligibility helpers
on spp.dci.subscription -- _consent_allows_partner (correct primitive:
has_legal_basis_bypass / can_access_registrant; NOT the dead status=="active"
domain), _partner_matches_filter (base fail-closed), _eligible_partner_ids,
_build_notification_records (hook), _delete_records_for. notify_event now takes
(event_type, partner_ids, reg_type, delete_payloads) and, per matching
subscription, delivers only consent-allowed + filter-matching records built
with that subscription's sender context.
- spp_dci_server_social: override _partner_matches_filter (compile the stored
filter via the search service; drop on parse failure) and
_build_notification_records (build with sender_id). Snapshot per-subscription
eligibility at unlink so delete notifications go only to eligible subscribers;
_execute_dci_notification hands off partner_ids / scoped delete payloads
instead of a single global payload.
Decisions: legal-basis-bypass senders pass consent (filter still applies),
documented; delete scoped via unlink-time eligibility snapshot. Deferred to
follow-ups: field minimization (response_fields / layer C), and the parallel
dead consent filter on the search path (build_consented_domain status=="active").
Tests: spp_dci_server 320/320, spp_dci_server_social 82/82.
* security(dci): batch filter eval + cover notification scoping helpers
Address review on PR #258:
- Gemini (perf): replace per-partner filter evaluation (O(partners x
subscriptions) queries) with a batched `_filter_matching_partners(partner_ids)`
that evaluates the filter in a single query per subscription. The social
override now intersects partner_ids with the compiled filter domain in one
search; `_partner_matches_filter` is a thin wrapper; `_eligible_partner_ids`
applies consent per record then filters in one batch.
- Codecov: add generic unit tests for the new subscription helpers
(_filter_matching_partners base policy, _partner_matches_filter wrapper,
_consent_allows_partner bypass, _eligible_partner_ids, _delete_records_for,
notify_event delete scoping) that the generic spp_dci_server test DB exercises.
Tests: spp_dci_server 326/326, spp_dci_server_social 82/82.
* security(dci): fail closed on missing filter_type (staff-review fix)
Staff review of the implemented branch caught a HIGH over-delivery regression:
the social filter evaluator defaulted query_type to "expression" when
filter_type was unset. The DCI client never sends filter_type, so a stored
idtype-value filter (a specific person) was parsed as an expression with no
seq/or keys -> empty domain -> matched EVERY registrant in the batch. For
legal-basis-bypass senders (consent does not backstop) this re-introduced the
full unscoped PII fan-out the PR set out to fix.
Fix: in _filter_matching_partners, treat the SPDCI {"type":"*","value":"*"}
wildcard as match-all (the client's "subscribe to all" default; consent still
gates), but for any other filter require a known filter_type
(idtype-value/expression/predicate) and fail closed (match nothing) when the
discriminator is missing/unknown rather than guessing.
Tests: add regressions for the two cases that would have caught this -
real idtype-value filter without filter_type -> matches nothing; wildcard
filter without filter_type -> matches all. spp_dci_server_social 84/84.
0 commit comments