fix(jdbi): make username uniqueness case-insensitive to match DynamoDB - #150
Merged
Conversation
wolpert
force-pushed
the
fix/jdbi-username-case-insensitive
branch
from
August 5, 2026 15:12
8e05feb to
0a89956
Compare
wolpert
enabled auto-merge (rebase)
August 5, 2026 15:19
wolpert
force-pushed
the
fix/jdbi-username-case-insensitive
branch
from
August 5, 2026 15:21
0a89956 to
23ec0dd
Compare
DynamoDbUserLookup keys identity on `USERNAME#<lower(username)>`, so "Admin" and "admin" are one account on that backend. JdbiUserLookup matched exactly against V5's plain UNIQUE constraint, so on Postgres they were two. Same library, same SPI, two identity models — a host that assumed the DynamoDB semantics, or moved between backends, could end up with look-alike accounts. Flyway V12 adds a unique index on lower(username) and the lookup queries fold case to match. getOrCreateHandle's ON CONFLICT now infers that expression index, so a racing insert of any case variant resolves to the existing row instead of minting a second handle. The stored username keeps its original casing — only uniqueness and lookup fold — mirroring DynamoDB's UserItem, which stores the supplied form and lower-cases only the key. The migration refuses to run on a database that already holds case-duplicate usernames, and names the offending groups. Which of "Admin" and "admin" is authoritative, and what becomes of the other's credentials, is a business decision; a schema change should not make it silently, nor should the operator meet it as an opaque duplicate-key error from the index build. That guard only executes on a dirty database, which is exactly the case that would otherwise ship untested, so V12UsernameCaseMigrationGuardIntegrationTest drives it on real Postgres: seeded conflicts are refused with all variants named and no index created, clean data migrates and is then enforced by the index, and the documented remediation (drop the loser, re-run) succeeds. Also refreshes the DESIGN.md schema paragraph, which had drifted to "10". Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
wolpert
force-pushed
the
fix/jdbi-username-case-insensitive
branch
from
August 5, 2026 15:26
23ec0dd to
3937b71
Compare
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.



Closes finding 5 of the full-project security audit.
The problem
DynamoDbUserLookupkeys identity onUSERNAME#<lower(username)>, soAdminandadminare one account on that backend.JdbiUserLookupmatched exactly against V5's plainUNIQUEconstraint, so on Postgres they were two.Same library, same SPI, two identity models. A host that assumed the DynamoDB semantics — or moved between backends — could end up with look-alike accounts.
The fix
Flyway V12 adds a unique index on
lower(username); the lookup queries fold case to match.getOrCreateHandle'sON CONFLICTnow infers that expression index, so a racing insert of any case variant resolves to the existing row instead of minting a second handle.The stored username keeps its original casing — only uniqueness and lookup fold — mirroring DynamoDB's
UserItem, which stores the supplied form and lower-cases only the key. Display values round-trip unchanged.PkAuthJdbiSchema.CURRENT_SCHEMA_VERSION→12.V12 runs a pre-flight check and refuses to migrate, naming the conflicting groups, if the database already holds usernames differing only by case:
Which of
Adminandadminis authoritative, and what becomes of the other's credentials, is a business decision. A schema change shouldn't make it silently — nor should the operator meet it as an opaque duplicate-key error from the index build.Testing
The guard only executes on a dirty database, which is exactly the code that would otherwise ship untested.
V12UsernameCaseMigrationGuardIntegrationTestdrives it on real Postgres (its own container, migrated to V11 then seeded):Plus case-insensitivity coverage on
JdbiUserLookupitself../gradlew :pk-auth-persistence-jdbi:checkpasses (Docker required).Also refreshes the
DESIGN.mdschema paragraph, which had drifted to"10".🤖 Generated with Claude Code