Account import: an archive from a newer Postulo is not refused by format, and fails with a traceback #561

Closed
opened 2026-10-01 10:47:39 +00:00 by tiagoagueda · 0 comments
Owner

Found on 2026-09-29 in a read-only review of postulo at 95348908d, comparing the account importer's manifest check with the backup reader's.

Checked again on 2026-10-01 against main at 0a5cad587: still present.

The account importer checks only that the manifest has a format key. It never compares the number with the formats it knows, so an archive written by a newer Postulo is read as if it were current. What happens next depends on what the newer format added: a new key on a record ends the import in a TypeError traceback, and a new top-level section is dropped without a word while the command reports success. The backup reader already refuses an archive outside the range it reads, with a sentence saying which range that is.

  • Where: core/importer.py:114-127 (read_manifest: if "format" not in header), against core/backup.py:405-427 (read_manifest, which refuses anything outside 1 <= fmt <= BACKUP_FORMAT). The records go straight into constructors at core/importer.py:442 (model.objects.create(owner=user, **values)) and in the skills, contacts and companies loops. core/management/commands/import_data.py:35 catches only ArchiveError and zipfile.BadZipFile.
  • What happens: the format history in core/export.py:27-63 shows both shapes of change. Format 24 added esco_uri on a skill: a Postulo that reads format 23 passes that key to resume.Skill.objects.create(..., **entry), and Django raises TypeError for an unexpected keyword argument. import_data does not catch it, so the operator sees a traceback rather than "this archive is newer than this Postulo". Format 25 added remembered_places at the top of the file: a Postulo that reads format 24 never looks for it, reports the import as done, and the section is gone without a line in report.skipped. The transaction in load() means the first case leaves the account untouched; the second loses data silently.
  • Why it matters: moving an account from a newer instance to an older one (or restoring into an instance that has not been upgraded yet) is an ordinary thing to try, and the answer should be a clear refusal before anything is written, not a crash or a partial import that says it succeeded.
  • Fix: in importer.read_manifest, refuse any format that is not an int (and not a bool) from 1 to export.FORMAT_VERSION, with a sentence that mirrors the backup reader's ("This archive is export format 26; this version of Postulo reads 1 to 25."). Ideally one small helper serves both readers.

Done when

  • A test in tests/test_export.py builds an archive whose manifest says FORMAT_VERSION + 1 and asserts that load() raises ArchiveError naming both numbers, and that import_data exits with that message rather than a traceback.
  • A manifest whose format is a string or true is refused the same way.
Found on 2026-09-29 in a read-only review of postulo at 95348908d, comparing the account importer's manifest check with the backup reader's. Checked again on 2026-10-01 against `main` at `0a5cad587`: still present. The account importer checks only that the manifest has a `format` key. It never compares the number with the formats it knows, so an archive written by a newer Postulo is read as if it were current. What happens next depends on what the newer format added: a new key on a record ends the import in a `TypeError` traceback, and a new top-level section is dropped without a word while the command reports success. The backup reader already refuses an archive outside the range it reads, with a sentence saying which range that is. - **Where:** `core/importer.py:114-127` (`read_manifest`: `if "format" not in header`), against `core/backup.py:405-427` (`read_manifest`, which refuses anything outside `1 <= fmt <= BACKUP_FORMAT`). The records go straight into constructors at `core/importer.py:442` (`model.objects.create(owner=user, **values)`) and in the skills, contacts and companies loops. `core/management/commands/import_data.py:35` catches only `ArchiveError` and `zipfile.BadZipFile`. - **What happens:** the format history in `core/export.py:27-63` shows both shapes of change. Format 24 added `esco_uri` on a skill: a Postulo that reads format 23 passes that key to `resume.Skill.objects.create(..., **entry)`, and Django raises `TypeError` for an unexpected keyword argument. `import_data` does not catch it, so the operator sees a traceback rather than "this archive is newer than this Postulo". Format 25 added `remembered_places` at the top of the file: a Postulo that reads format 24 never looks for it, reports the import as done, and the section is gone without a line in `report.skipped`. The transaction in `load()` means the first case leaves the account untouched; the second loses data silently. - **Why it matters:** moving an account from a newer instance to an older one (or restoring into an instance that has not been upgraded yet) is an ordinary thing to try, and the answer should be a clear refusal before anything is written, not a crash or a partial import that says it succeeded. - **Fix:** in `importer.read_manifest`, refuse any `format` that is not an `int` (and not a `bool`) from 1 to `export.FORMAT_VERSION`, with a sentence that mirrors the backup reader's ("This archive is export format 26; this version of Postulo reads 1 to 25."). Ideally one small helper serves both readers. ## Done when - A test in `tests/test_export.py` builds an archive whose manifest says `FORMAT_VERSION + 1` and asserts that `load()` raises `ArchiveError` naming both numbers, and that `import_data` exits with that message rather than a traceback. - A manifest whose `format` is a string or `true` is refused the same way.
tiagoagueda added this to the 0.6.0 milestone 2026-10-01 10:47:39 +00:00
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
Postulo/postulo#561
No description provided.