Repository navigation
[PM-44104] Save auth grant on change - #8450
Conversation
|
Thank you for your contribution! We've added this to our internal tracking system for review. Details on our contribution process can be found here: https://contributing.bitwarden.com/contributing/pull-requests/community-pr-process. |
f6e6cc5 to
b41a669
Compare
ike-kottlowski
left a comment
There was a problem hiding this comment.
The most elegant of changes are always single lines.
We would like for you to add integration tests please.
Cover all three SaveAsync paths against every configured database provider: insert, update, and the non-Grant type guard. The update test is the regression test for the missing SaveChangesAsync on the update branch: IdentityServer stores refresh tokens with ReUse + Sliding semantics, so every token refresh re-saves the grant under the same key with a reset creation time and a new expiration. When that update is dropped, the row is frozen at issuance and continuously active clients are forced into a full re- authentication once the original window lapses.
Three defects on the EF grant repository (PostgreSQL, MySQL, SQLite self-hosted), all dormant because nothing in Bitwarden's IdentityServer wiring exercises these paths today: - GetManyAsync threw InvalidCastException on every call: List<Grant> cannot cast to ICollection<IGrant> because ICollection<T> is invariant. Materialize with ToList<IGrant>() like the Dapper implementation. - GetManyAsync and DeleteManyAsync treated null filter arguments as "the column must be NULL" (EF's null-semantics translation of plain equality), while the Grant_Read and Grant_Delete stored procedures treat NULL parameters as wildcards. For Bitwarden data, where SubjectId/ClientId/Type are always populated, a partial filter such as subject+type with a null clientId matched nothing on EF providers but everything on MSSQL. Align the LINQ predicates to the stored procedures' wildcard semantics, which is also what Duende's PersistedGrantFilter produces. Integration tests cover SaveAsync (type guard, insert, update), GetManyAsync (non-null filtering with distractors and empty results, null filters as wildcards), and DeleteManyAsync (null client filter deletes across clients while other types and subjects survive).
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## main #8450 +/- ##
==========================================
+ Coverage 74.76% 74.83% +0.07%
==========================================
Files 2750 2750
Lines 117617 117618 +1
Branches 10781 10781
==========================================
+ Hits 87937 88024 +87
+ Misses 27022 26937 -85
+ Partials 2658 2657 -1 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
dotnet format's CHARSET check requires C# files to be UTF-8 with BOM (.editorconfig); the test file was written without the BOM and failed dotnet format --verify-no-changes in CI.
|
I will update when QA has had a chance to take a look on our end. |
| await dbContext.Grants.Where(g => | ||
| g.SubjectId == subjectId && | ||
| g.ClientId == clientId && | ||
| g.SessionId == sessionId && | ||
| g.Type == type).ExecuteDeleteAsync(); | ||
| (subjectId == null || g.SubjectId == subjectId) && | ||
| (clientId == null || g.ClientId == clientId) && | ||
| (sessionId == null || g.SessionId == sessionId) && | ||
| (type == null || g.Type == type)).ExecuteDeleteAsync(); |
There was a problem hiding this comment.
The subjectId predicate combines || and && on one line, increasing the logical complexity of the filter.
Show fix
| await dbContext.Grants.Where(g => | |
| g.SubjectId == subjectId && | |
| g.ClientId == clientId && | |
| g.SessionId == sessionId && | |
| g.Type == type).ExecuteDeleteAsync(); | |
| (subjectId == null || g.SubjectId == subjectId) && | |
| (clientId == null || g.ClientId == clientId) && | |
| (sessionId == null || g.SessionId == sessionId) && | |
| (type == null || g.Type == type)).ExecuteDeleteAsync(); | |
| var grants = dbContext.Grants.Where(g => | |
| (subjectId == null || g.SubjectId == subjectId) && | |
| (clientId == null || g.ClientId == clientId) && | |
| (sessionId == null || g.SessionId == sessionId) && | |
| (type == null || g.Type == type)); | |
| await grants.ExecuteDeleteAsync(); |
Details
✨ AI Reasoning
The line combines a null check and a field comparison with two different logical operators. Readers must track both the grouped alternatives and the outer conjunction.
Reply on this comment to give feedback or ignore the issue.
More info
|
|
|
@alexyao2015 great news, the change passed QA! Thank you for finding this bug and iterating on the PR with me. 🚀 🚀 |
📔 Objective
The authorization grants were not being updated if they exist. This means for any non MSSQL db, users will experience timeouts after a set period of time. In practice, a user's client will be forcibly kicked out and forced to perform a relogin even if they are actively using it.
The database entry was updated, but this change was not saved.