[PM-39454] feat(km): add UserKeyId column to the user row - #8108
Conversation
Codecov Report❌ Patch coverage is Additional details and impacted files@@ Coverage Diff @@
## km/user-key-id-02-keyid-type #8108 +/- ##
================================================================
+ Coverage 62.89% 67.37% +4.47%
================================================================
Files 2303 2303
Lines 100279 100288 +9
Branches 9030 9032 +2
================================================================
+ Hits 63071 67569 +4498
+ Misses 35023 30438 -4585
- Partials 2185 2281 +96 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
🤖 Bitwarden Claude Code ReviewOverall Assessment: APPROVE Reviewed the addition of the Code Review DetailsNo new findings. Context on the two open items from earlier review rounds:
Informational, not reopening the resolved thread: |
30f5cea to
a251250
Compare
| Email = $"{id}@example.com", | ||
| ApiKey = "TEST", | ||
| SecurityStamp = "stamp", | ||
| AccountRevisionDate = DateTime.UtcNow.AddMinutes(-10), |
There was a problem hiding this comment.
Not sure why this was necessary, but it was making tests fail 🤷
There was a problem hiding this comment.
@quexten This is a pre-existing flake, not caused by your PR. User_BumpAccountRevisionDate uses GETUTCDATE(), which has about 3.3 ms precision, so on a fast run it can land before the full-precision DateTime.UtcNow set during creation. The same strict > assertion at OrganizationUserReplaceTests.cs:90 failed on main in run 30025134779, so backdating the test value here is a valid fix.
The real fix is to update the ten User_BumpAccountRevisionDate* procedures to use SYSUTCDATETIME(), the datetime2(7)-precision equivalent, but that requires a migration.
Adds the nullable UserKeyId column to [dbo].[User] and its view, and threads it through the three procedures that write a user row wholesale: User_Create, User_Update and User_UpdateKeys. The column is nullable and every new procedure parameter defaults to NULL, so an older server that omits it keeps working during a rolling deploy. User_UpdateKeys assigns the column unconditionally, so both rotation paths have to supply the value. The Dapper path passes @UserKeyId; the Entity Framework path was not assigning it at all, which would have left a stale key id behind on the non-MSSQL providers once rotations start carrying one. Both now behave the same. KeyIdCustomization teaches AutoFixture to build a KeyId — it has a private constructor — and pins User.UserKeyId to valid hex so that any test whose subject reads the id does not throw on a random string. It is applied to every BitAutoData fixture because users are generated throughout the suite. Nothing sets a key id yet; this only gives it somewhere to live.
a251250 to
b17a1f0
Compare
| [MaxLength(32)] | ||
| public string? UserKeyId { get; set; } | ||
|
|
||
| public void SetUserKeyId(KeyId userKeyId) |
There was a problem hiding this comment.
Since KeyId might come null and I assume we will use this function from command, i think it make sense to make it nullable too.
| public void SetUserKeyId(KeyId userKeyId) | |
| public void SetUserKeyId(KeyId? userKeyId) |
|
|
||
| public void SetUserKeyId(KeyId userKeyId) | ||
| { | ||
| UserKeyId = userKeyId.ToString(); |
There was a problem hiding this comment.
| UserKeyId = userKeyId.ToString(); | |
| UserKeyId = userKeyId?.ToString(); |
| /// A key rotation will set a new key id. Account registrations will carry a key id. | ||
| /// </summary> | ||
| [MaxLength(32)] | ||
| public string? UserKeyId { get; set; } |
There was a problem hiding this comment.
Should we make it more restrictive ?
| public string? UserKeyId { get; set; } | |
| public string? UserKeyId { get; private set; } |
| [V2UpgradeToken] = @V2UpgradeToken, | ||
| [MasterPasswordSalt] = @MasterPasswordSalt, | ||
| [LastApiKeyRotationDate] = @LastApiKeyRotationDate, | ||
| [UserKeyId] = @UserKeyId |
There was a problem hiding this comment.
Just confirming the rollout order here: the intent is for #8110 to ship at least one release after this change, correct? Older binaries will omit @UserKeyId, so landing both together would allow User_Update to overwrite an existing value with NULL during deployment or rollback. If they do need to land together, [UserKeyId] = COALESCE(@UserKeyId, [UserKeyId]) closes that compatibility gap — same applies to User_UpdateKeys below, and the SSDT copies need to match either way.
There was a problem hiding this comment.
One addition, and one thing to confirm.
Release ordering doesn’t fully close this on its own. #8112 adds a backfill endpoint that writes UserKeyId out of band, so a request holding a User loaded before the backfill ran could still null the column on its next ReplaceAsync, even with no version skew. Since actual writes go through User_SetUserKeyId / User_TrySetUserKeyId, having User_Update preserve the existing value rather than assign NULL closes both cases.
Adds the user-id column, which is the authoritative ID of the user key. The column contains 32 characters (16 bytes but in hex).