Skip to content

enhance: serialize user update methods - #1042

Open
eternal-flame-AD wants to merge 1 commit into
masterfrom
api-user-txn
Open

eternal-flame-AD wants to merge 1 commit into
masterfrom
api-user-txn

Conversation

@eternal-flame-AD

Copy link
Copy Markdown
Member

Serializes user update actions to prevent race conditions leading to unexpected results.

I removed the 'Test_UpdateUserByID_EmptyPassword_Expect400' test as it seemed to be a mistake - it should return 200, it returned 400 in the test because there wasn't a second admin.

@eternal-flame-AD
eternal-flame-AD requested a review from a team as a code owner September 2, 2026 09:00
@codecov

codecov Bot commented Sep 2, 2026 •

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 82.60870% with 28 lines in your changes missing coverage. Please review.
✅ Project coverage is 76.21%. Comparing base (f77d8a0) to head (60ec466).

Files with missing lines Patch % Lines
plugin/manager.go 86.66% 5 Missing and 7 partials ⚠️
api/user.go 74.41% 6 Missing and 5 partials ⚠️
database/database.go 75.00% 1 Missing and 1 partial ⚠️
database/user.go 60.00% 1 Missing and 1 partial ⚠️
api/oidc.go 0.00% 0 Missing and 1 partial ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##           master    #1042      +/-   ##
==========================================
+ Coverage   76.12%   76.21%   +0.09%     
==========================================
  Files          67       67              
  Lines        3619     3679      +60     
==========================================
+ Hits         2755     2804      +49     
- Misses        653      659       +6     
- Partials      211      216       +5     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

Comment thread api/user.go Outdated
Comment thread api/user.go Outdated
Comment thread api/user.go Outdated
Comment thread api/user.go
Comment thread api/user.go Outdated
@eternal-flame-AD
eternal-flame-AD force-pushed the api-user-txn branch 5 times, most recently from d486c1b to 6e4f37c Compare September 6, 2026 14:06
Comment thread api/user.go Outdated
@eternal-flame-AD
eternal-flame-AD force-pushed the api-user-txn branch 2 times, most recently from 3421a40 to 6445b84 Compare September 28, 2026 09:03
Comment thread api/user.go
@eternal-flame-AD
eternal-flame-AD force-pushed the api-user-txn branch 2 times, most recently from 1728f14 to d7f2d0b Compare October 2, 2026 14:14
Comment thread plugin/manager.go Outdated
}
if compat.HasSupport(instance, compat.Storager) {
instance.SetStorageHandler(dbStorageHandler{pluginConf.ID, m.db})
instance.SetStorageHandler(dbStorageHandler{pluginConf.ID, tx})

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This shouldn't use the transaction, as the transaction is closed after the http requests finishes, but the plugin instance keeps going in the background.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Good catch, but we need to defer this in a gorountine just in case the plugin side blocks on this call. I will add a sync barrier for unit tests to make sure the test has no races.

Comment thread plugin/manager.go Outdated
defer m.mutex.Unlock()
maps.DeleteFunc(m.instances, func(id uint, instance *InstanceWrapper) (delete bool) {
delete = instance.userID == userID
if delete {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Suggested change
if delete {
if delete && instance.enabled {

I think .Disable should only be called if the plugin was enabled. E.g. a plugin could create some maps, bigger resources in the Enable method, and then free these resources in Disable method, but in this case disable could try to free uninitialized resources which could produce errors.

THe instance should be removed from the instances map regardless if the plugin is enabled or not.

Comment thread api/user.go
// $ref: "#/definitions/Error"
func (a *UserAPI) DeleteUserByID(ctx *gin.Context) {
withID(ctx, "id", func(id uint) {
user, err := a.DB.GetUserByID(id)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

(optional) Can we move this into the transaction too? Otherwise they can still be some race here. Similar for the UpdateUser api.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

I think this is fine as is, it's just an early filter making sure the ID exists. The risk would be someone deleted the user right before the check and your transaction runs, in such case returning a success instead of "user does not exist" is probably acceptable.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

For the update endpoint it caches oidc_id before the transaction and then set's it inside the transaction. There probably could be a race condition where a user is linked to an oidc identity & maybe updates the admin permissions, and at the same time the update method is called, this could unset the oidc_id and the change of permission.

For the delete endpoint it could be that at the same time another admin changes the admin flag of the user, so it's the last admin. The admin flag is false on the read outside of the transaction, but before the transaction is called, it was actually updated, and then the admin check isn't done correctly.

But yeah, it's optional, but it seems like a similar problem we are trying to fix here.

Comment thread plugin/manager.go Outdated
i.enabled = false
err := i.instance.Disable()
if err != nil {
i.enabled = true

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I'm not sure this is true. Disable could be called without the plugin being active. Mabye we could guard here against disabling a plugin that is already disabled. Or implement it as the other comment describes.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

2 participants