Skip to content

Issue #1300: fix: reject identity-mapping fields wider than their column with a 422 - #1313

Merged
bjagg merged 3 commits into
mainfrom
issue-1300-identity-mapper-422-oversized
Sep 26, 2026
Merged

bjagg merged 3 commits into
mainfrom
issue-1300-identity-mapper-422-oversized

Conversation

@dereck-symmetry

Copy link
Copy Markdown
Contributor
Description of Change

Problem. IdentityMapping's string fields have no max_length. As #1300 describes, a value longer than its column reaches MariaDB, fails with 1406 Data too long in strict mode, and returns to the caller as a generic 500 through DataStoreException. #1258 lowered the threshold for three fields, from 255 to 191.

Solution: enforce the widths at the API boundary. The issue left the placement open; this puts it at the API boundary rather than on the shared DTO.

  • A new IdentityMappingRequest(IdentityMapping) in bases/lif/identity_mapper_restapi/core.py is the POST .../mappings body type. Each of the five string fields carries max_length read from the SQLAlchemy column (IdentityMappingModel.__table__.c[name].type.length). The model already mirrors 02-ddl.sql, so there's no third copy of the numbers.
  • The endpoint converts the validated items back to plain IdentityMapping before calling the service. The request model only validates; the service and storage receive exactly what they did before. This also keeps list[IdentityMapping] type-correct for ty (list is invariant) without widening the service's signature.
  • An oversized field now gets FastAPI's standard 422, with loc naming it, e.g. ["body", 0, "target_system_id"].

Why not max_length on the DTO in components/lif/datatypes. Only the Identity Mapper bricks use IdentityMapping, but datatypes is packaged by 12 projects and watched by 9 deploy workflows. Putting it there would redeploy about nine services for a class one of them uses, and it would be a third copy of the widths. Here, only the Identity Mapper redeploys.

Limitations. max_length counts characters, the same unit as MariaDB's VARCHAR(n). The 500 this replaces is as the issue describes it; I didn't reproduce it against a real MariaDB for this PR. The tests prove the 422 at the API, which is the part this change owns. The GET/DELETE path parameters aren't limited: an over-long one just matches no row.

How reviewers should test it.

uv run pytest test/bases/lif/identity_mapper_restapi/ -q -k column
Related Issues

Closes #1300
Refs #1131

Type of Change
  • Bug fix (non-breaking change which fixes an issue)
Project Area(s) Affected
  • bases/
  • test/ or e2e/
  • API endpoints
  • Documentation (docs/, READMEs, ARCHITECTURE.md, CLAUDE.md)

Checklist
  • commit message follows commit guidelines (see commitlint.config.mjs)
  • tests are included (unit and/or integration tests)
  • code passes linting checks (uv run ruff check)
  • code passes formatting checks (uv run ruff format)
  • code passes type checking (uv run ty check)
  • pre-commit hooks have been run successfully
  • API changes: base README updated (bases/lif/identity_mapper_restapi/README.md). No page
    under docs/ documents the Identity Mapper's status codes
Testing
  • Automated tests added/updated

Two tests, each parametrized over all five fields, with the widths pinned as literals from the issue's table (191 / 191 / 191 / 100 / 255). A width change then shows up as a deliberate test edit.

Test main This branch
One character over the width → 422, error loc is exactly ["body", 0, <field>], service never called fails ×5 (reaches the service, 200) passes ×5
Exactly at the width → 200, service receives a plain IdentityMapping passes ×5 passes ×5

With only core.py reverted to main, the five "over" cases fail again. The three existing save tests, which compare the service's call arguments against IdentityMapping(...), pass unchanged, which confirms the service still gets the plain DTO. The identity-mapper base, service and storage suites pass (86 tests). pre-commit run --files on all four changed files is green, including the full pytest suite.

Additional Notes

Merge order with #1312 (#1261, the 409 on a persistent collision). I trial-merged the two branches:

  • core.py and the test file merge cleanly.
  • CHANGELOG.md and bases/lif/identity_mapper_restapi/README.md conflict: both PRs add a paragraph at the same spot. Keep both.

CHANGELOG.md is also shared with #1310, #1311 and #1148, under the same keep-both rule.

🤖 Generated with Claude Code

…umn with a 422

A value longer than its column reached MariaDB and came back as a generic
500. The save endpoint's body is now an IdentityMappingRequest whose fields
carry max_length read from the SQLAlchemy model's column widths, so an
oversized value is a 422 naming the field. Enforced at the API boundary
rather than on the shared datatypes DTO, which a dozen projects package for
a class only the Identity Mapper uses; the service still receives plain
IdentityMapping objects.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

@bjagg bjagg left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Approving. 86 pass, and reverting core.py fails exactly the five over-width cases. I mutation-tested the limits: dropping one field's max_length, off-by-one in either direction, a single width for every field, skipping the conversion back to plain IdentityMapping, and reverting the body type are all caught. Pinning the widths as literals is what catches the subtle ones. The model, 02-ddl.sql and #1300's table agree, and the service's body-vs-path match means an over-long path segment can't bypass the check. Good call keeping this off the shared datatypes DTO.

@bjagg
bjagg merged commit 19c5d75 into main Sep 26, 2026
4 checks passed
@bjagg
bjagg deleted the issue-1300-identity-mapper-422-oversized branch September 26, 2026 01:34
bjagg added a commit that referenced this pull request Sep 26, 2026
Resolve CHANGELOG.md with #1310 and #1313 by keeping every entry. In
test_core.py, keep main's file plus this branch's
TestMutationFailureDoesNotLeakBody, minus its
monkeypatch.setattr(type_factory, "input_type_cache", {}) line: #1314
removed that module attribute, so the line raised AttributeError (the
step #1314's PR body describes).
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

Identity Mapper: reject oversized identity-mapping fields with 422 instead of a 500 from the database

2 participants