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

  1. In rails console enable the user mapping feature

    Feature.enable(:importer_user_mapping)
    Feature.enable(:bulk_import_importer_user_mapping)
  2. Import a group with some user contributions

  3. Wait until the import is done (An email is sent to confirm it's finished)

  4. Visit http://127.0.0.1:3000/groups/<group-name>/-/group_members?tab=placeholders

  5. Reassign a placeholder to your username

  6. Visit http://127.0.0.1:3000/rails/letter_opener and click on "Review reassignment details"

  7. Press "Approve"

  8. 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 the csrf-token from the meta tag, in the source code of any logged in page.
  • For <COOKIES> you can copy these by finding a POST request in the network tab on the homepage and copying the request "Cookie" value. For example from the http://172.16.123.1:3000/-/peek/results request 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 with api permissions.

Related to #493405

Edited by Keeyan Nejad

Merge request reports

Loading