Repository navigation
fix(identity): enforce one row per role claim grant - #1458
Merged
Merged
Conversation
Concurrent permission writers (two instances starting at once, or the seeder and the startup sync on a fresh tenant) could both insert the same permission on a role, because identity.RoleClaims had no uniqueness. - Migration removes existing duplicates (keeping the lowest Id per RoleId, ClaimType, ClaimValue) and then adds a unique index on those columns, replacing the RoleId-only index it covers. - RolePermissionSyncer and IdentityDbInitializer treat a unique violation on that index as another writer having inserted the grant first: they clear the change tracker, log at Information and continue. - Role permission updates collapse repeated entries in the request, which would otherwise now hit the index. Closes #1435
iammukeshm
added a commit
to fullstackhero/docs
that referenced
this pull request
Nov 3, 2026
Changelog entry and Authorization note for fullstackhero/dotnet-starter-kit#1458 (closes #1435).
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.
Problem
identity.RoleClaimshad no uniqueness on(RoleId, ClaimType, ClaimValue). Both writers that grant permissions to the built-in roles,RolePermissionSyncer.SyncRoleAsyncandIdentityDbInitializer.AssignPermissionsToRoleAsync, read the role's claims and insert the missing ones without a lock. Two API instances starting at the same time after a release that adds a permission, or the seeder overlapping the startup sync on a new tenant, could both insert the same grant and leave a duplicate row, with nothing in the logs. Authorization was not affected, but the table drifted.Fix
RoleClaims (RoleId, ClaimType, ClaimValue), configured inApplicationRoleClaimConfig.23505) on that index as "another writer already inserted it": they clear the change tracker so the next role's save in the same scope doesn't retry the rejected rows (the same approach fix(identity): make the system-role permission sync remove revoked claims #1426 takes for the lost delete race), log at Information through a[LoggerMessage], and continue. Any other database error still throws.RoleService.UpdatePermissionsAsyncnow collapses a permission repeated in the request. Before,["x", "x"]silently stored two rows; with the index it would have been a 500.Migration notes
AddRoleClaimsUniqueIndex(Identity):Idper(RoleId, ClaimType, ClaimValue), with amigrationBuilder.Sqlstatement that runs before the index is created, in the same migration and transaction.IX_RoleClaims_RoleIdand creates the uniqueIX_RoleClaims_RoleId_ClaimType_ClaimValue. EF drops the old index because the new one leads withRoleIdand covers the foreign key.PostgreSQL treats NULLs as distinct in a unique index, so rows with a NULL
ClaimTypeorClaimValuenever conflict. The dedupe uses plain equality to match exactly what the index enforces, so it leaves those rows alone (the sync already removes null-valued permission claims). The snapshot diff is only the index change plus theProductVersionannotation bump to 10.0.13; no other model drift.I also ran the dedupe + index SQL by hand against a throwaway Postgres 17 with three copies of one grant, a grant on a second role, two NULL-valued rows and a different claim type: it deleted the 2 extra copies, kept the lowest
Id, left the NULL rows, and the index created cleanly.Tests
RoleClaims_Should_Reject_The_Same_Permission_Twice_On_A_Role: a second insert of the same permission fails with23505on the new index, and one row remains.SyncRoleAsync_Should_Treat_A_Lost_Insert_Race_As_Done_And_Keep_Syncing: another writer inserts the same missing permission just before the sync saves; the sync reports no change, exactly one row exists, and the next role in the same scope still syncs.UpdateRolePermissions_Should_Store_A_Repeated_Permission_Once: a repeated permission in the request returns 200 and is stored once.Results:
dotnet build src/FSH.Starter.slnx0 warnings, 0 errors. Integration.Tests 803/803 passed (migrations applied by the harness), Identity.Tests 348/348, Architecture.Tests 55/55, and the rest of the solution's suites all green.Docs: fullstackhero/docs#277
Closes #1435. Thanks @marcelo-maciel for the report and the clear write-up.