Data protection: a contact's document omits the applications and interviews that name them; erasure and the dry run miss some #370
Labels
No labels
accessibility
authentication
breaking change
bug
documentation
enhancement
interface
internationalisation
observability
performance
security
tier
1
tier
2
tier
3
tier/4
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set
Reference
Postulo/postulo#370
Loading…
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Found on 2026-09-29 in a read-only review of postulo at
95348908d, by comparing what each of the data-protection functions and the contact merge treat as "pointing at a contact".Checked again on 2026-10-01 against
mainat0a5cad587: still present.#297 built a data-subject export of "everything the instance holds on one other person" and an erasure that says what it removed. Four functions each decide for themselves what refers to a contact, and they disagree. The merge moves four kinds of reference; the subject's document carries only one of them, and the erasure report and the retention dry run each leave out a different one.
core/gdpr.py:150-175(contact_document): the contact row, numbers, addresses, links,listing_eventsand plugin rows. Nothing for the applications where the person is the main contact (Application.contact), the ones they referred (Application.referred_by), or the interviews they were at (Interview.contacts,applications/models.py:722-724).core/gdpr.py:279-309(erase_contact):unlinkedcounts applications, referrals and listing entries, not interview seats; the M2M rows go with the delete and the report never says so.core/gdpr.py:331-363(retention_dry_run):would_removecounts numbers, addresses, links and applications (as contact or referrer), not listing entries or interviews.jobs/merging.py:548-621(plan_contacts) and:645-701(merge_contacts): the one place that handles all four, and backs itself with a collector check (_refuse_what_is_left).core/gdpr.py:81-123: the subject document's number, address and link rows restate the archive's field lists by hand (core/export.py:149-167) and already differ from them:"primary"where the archive writesis_primary.jobs/views.py:826) hascontact,phone_numbers,postal_addresses,web_links,listing_eventsandplugins; neither application and not the interview appear.applications_unlinked: 2and nothing for the listing entry or the interview.jobs/views.py:800) does not mention it.core.gdpr.references(contact), that returns every row pointing at a contact: applications as contact and as referrer, interview seats, listing entries.contact_document(new sectionsapplications,referralsandinterviews, named by role and company the way_listing_event_rowsnames a listing, bumpingDOCUMENT_VERSION), inerase_contact(aninterviewscount inunlinkedand a sentence for it), inretention_dry_run, and inplan_contacts.export._fieldsand the archive'sPHONE_NUMBER_FIELDS,POSTAL_ADDRESS_FIELDSandWEB_LINK_FIELDS, so the two cannot drift.Related to #297 (closed), which built the export, the erasure and the dry run.
Done when
tests/test_gdpr.pyreferences one contact in all four ways and asserts that the document, the erasure report and the dry run each account for every one.tests/test_gdpr.pywalksContact._meta.related_objects(asmerging._refuse_what_is_leftdoes with the collector) and fails when a relation toContactexists thatreferences()does not cover.