Skip to content

OP-887 Add GET /patients/{code}/export for GDPR data portability - #595

Open
giuseppesorge wants to merge 2 commits into
informatici:developfrom
giuseppesorge:OP-887-patient-data-export
Open

OP-887 Add GET /patients/{code}/export for GDPR data portability#595
giuseppesorge wants to merge 2 commits into
informatici:developfrom
giuseppesorge:OP-887-patient-data-export

Conversation

@giuseppesorge

@giuseppesorge giuseppesorge commented Jul 2, 2026

Copy link
Copy Markdown
Contributor

See OP-887. Depends on informatici/openhospital-core#1633 (same branch name, so this PR's CI cross-builds against that core branch).

GET /patients/{code}/export returns the full patient aggregate as JSON; with Accept: text/csv it returns a CSV rendering — one RFC-4180 section per entity group with # <group> marker lines, CRLF, and an attachment Content-Disposition (patient_<code>_export.csv). Media types are evaluated in header order, JSON winning when both are acceptable (q-values are not weighted; documented in the javadoc). The endpoint requires the new patient.export authority: the SecurityConfig rule is placed before the generic GET /patients/** matcher. @Operation(description = "GDPR Art. 20 - Right to data portability") per the ticket.

openapi/oh.yaml was regenerated and verified byte-identical (sorted-JSON compare) to the springdoc output of the running application.

This PR also fixes a pre-existing production bug surfaced while wiring the mapper: the shared ModelMapper mis-resolved PatientVaccine.vaccine onto vaccineDate, so GET/POST/PUT /patientvaccines failed with a MappingException for any record with a non-null vaccine date. Pinned with an explicit typeMap in PatVacMapper; an audit of other implicit GenericMapper matches may be worth a follow-up ticket.

Verified: controller and CSV tests green, full suite green (237 tests); live end-to-end on the demo database — 200 JSON with all entity groups populated (vaccines included, per the ticket's note), 200 CSV, 404 for an unknown patient, 403 for a user whose group lacks the permission.

New endpoint returning all patient-linked data as JSON or RFC-4180 CSV
sections depending on the Accept header, guarded by the patient.export
authority. Profile photo is stripped from all nested DTOs. Also pins the
PatientVaccine vaccine property mapping in PatVacMapper, fixing a
pre-existing MappingException on /patientvaccines.
Prefix a cell value with a single quote when it starts with =, +, -, @, tab or
carriage return, so a spreadsheet application does not interpret an exported
field as a formula. Applied in escape(), before the RFC 4180 quoting, so it
covers every cell (keys and values, including rendered nested JSON). Adds a
unit test.
@giuseppesorge

Copy link
Copy Markdown
Contributor Author

Hardened the CSV export against formula injection (commit a0ba653).

PatientExportCsv.escape() already applied RFC 4180 quoting, but a cell whose value starts with =, +, -, @, a tab or a carriage return is interpreted as a formula by spreadsheet applications (Excel, LibreOffice, Google Sheets) when the exported CSV is opened — a classic CSV/formula-injection vector, and here the exported fields come from user-controlled patient data.

The fix prefixes any such value with a single quote (') before the RFC 4180 quoting. It lives inside escape(), so it covers every cell — both the header keys and the values, including the values produced by render() for nested JSON objects. Added a neutralizesCsvFormulaInjection unit test; PatientExportCsvTest is green (3/3).

Context: flagged by the automated security review of the pushed commits (MEDIUM). This only affects the CSV variant of the OP-887 export; the JSON body and the OP-888 full-record view (JSON only) are unaffected.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant