User mapping - Prevent concurrent reassignments modifications
What does this MR do and why?
Prevents a race condition where a reassignment could be accepted, and then immediately updated to a different user. This could cause contributions to be mapped to the wrong user.
2 steps were taken to prevent this:
- Wrap source user reassignments and permission checking in a database lock. That way we can't have 2 queries interfering with each other
- Add a special token which needs to be present when accepting a reassignment. This will be included in the emails
MR acceptance checklist
Please evaluate this MR against the MR acceptance checklist. It helps you analyze changes to reduce risks in quality, performance, reliability, security, and maintainability.
Screenshots or screen recordings
Screenshots are required for UI changes, and strongly recommended for all other merge requests.
| Before | After |
|---|---|
How to set up and validate locally
-
In rails console enable the user mapping feature
Feature.enable(:importer_user_mapping) Feature.enable(:bulk_import_importer_user_mapping) -
Import a group with some user contributions
-
Wait until the import is done (An email is sent to confirm it's finished)
-
Visit
http://127.0.0.1:3000/groups/<group-name>/-/group_members?tab=placeholders -
Reassign a placeholder to your username
-
Visit http://127.0.0.1:3000/rails/letter_opener and click on "Review reassignment details"
-
Press "Approve"
-
The contributions should be reassigned to your user
To test you are no longer able to change user to reassign to after approving an assignment, you can run this script (I found setting a 10 second sleep after https://gitlab.com/gitlab-org/gitlab/-/blob/493405-user-mapping-prevent-concurrent-reassignment-modifications/app/services/import/source_users/accept_reassignment_service.rb#L17 would be needed to reproduce the issue locally):
curl --request POST \
--url http://172.16.123.1:3000/import/source_users/<SOURCE_USER_ID>/accept?token=<TOKEN_FROM_EMAIL> \
--header 'X-CSRF-Token: <CSRF_TOKEN>' \
--cookie '<COOKIES>' & \
curl --request POST \
--url http://172.16.123.1:3000/api/graphql \
--header 'Content-Type: application/json' \
--header 'PRIVATE-TOKEN: <PRIVATE_TOKEN>' \
--data '{"query":"mutation {\n importSourceUserCancelReassignment(\n input: {id: \"gid://gitlab/Import::SourceUser/<SOURCE_USER_ID>\"}\n ) {\n errors\n }\n importSourceUserReassign(\n input: {id: \"gid://gitlab/Import::SourceUser/<SOURCE_USER_ID>\", assigneeUserId: \"gid://gitlab/User/2\"}\n ) {\n errors\n }\n}"}' \
You must replace the values in brackets as below:
- To get the
<CSRF_TOKEN>, you can get the value of thecsrf-tokenfrom the meta tag, in the source code of any logged in page. - For
<COOKIES>you can copy these by finding aPOSTrequest in the network tab on the homepage and copying the request "Cookie" value. For example from thehttp://172.16.123.1:3000/-/peek/resultsrequest on the homepage. - The
<TOKEN_FROM_EMAIL>and<SOURCE_USER_ID>can both be found in the accept reassignments email. - Finally for the
<PRIVATE_TOKEN>you just need to generate a token withapipermissions.
Related to #493405