On this issue, we will work on creating the controllers that will be used by the improved user mapping to accept or reject an assignment.
Two types of users will interact with the feature: group owners and assigned users.
Initially, group owners will use the User reassignment UI. These actions will be supported by GraphQL endpoints implemented in another issue: #457066 (closed)
To support the invitation email actions, we need to create another controller that meets the following criteria:
All actions should only be accessible to the user assigned to the Import::SourceUser
The Import::SourceUser should have the status awaiting_approval
The controller should implement the following actions:
GET accept
GET reject
When using GET accept, the Import::SourceUser status should be changed to complete. In this case, the reassign worker should be enqueued. If it hasn't been implemented yet, just set the status to complete and leave a placeholder note where the worker should be enqueued.
When using GET reject, the Import::SourceUser status should be changed to rejected.
Note: The controller should only be available when the feature flag improved_user_mapping is enabled. The feature flag will need to be created in this issue if not already done
I have a minor nitpick about the statuses: I thought pending and pending_approval were the same thing at first. Maybe one of the "pending"s should be called something else to avoid confusion? Perhaps pending_approval could be awaiting_approval?
When using GET accept, the Import::SourceUser status should be changed to complete. In this case, the reassign worker has already been implemented. The action should enqueue the worker.
@wortschi@m_frankiewicz I think this might be the only reason this issue is blocked. If so, I think it's worth just leaving a comment in the code that shows where we'd start the reassignment process, and set the status to complete manually for now. At worst, we'd need a follow-up MR to fix that one endpoint once the reassign worker is implemented so that more can be worked on simultaneously
@m_frankiewicz@wortschi Initially, we're planning to allow group owners to reassign users via the User reassignment UI, but are there any requirements on what endpoints we need to make so that this can be done via the API? Should all of the controller actions listed above be available as API endpoints as well?
@SamWord If I understand your question correctly, you are asking if there's a need to enable users to make reassignments via the API rather then the UI?
Regardless of whether we allow this workflow to be done via the API or not, I believe all the following controller actions should be implemented as API endpoints anyway for the UI to consume?
GET index (to list all import source users)
POST reassign (to assign the user that the contributions will go to)
DELETE reassign (to remove the user that the contributions were assigned to)
@SamWord we talked about it during design session, if I remember well, there are recording of those meetings. Rodrigo said I'd possible to have in API the bulk reassignment action, the one that in UI will be done with CSV upload.
I have a question to
DELETE reassign (to remove the user that the contributions were assigned to)
does that mean cancelling re-assignment? When I read this:
When using DELETE reassign, the Import::SourceUser status should be changed to pending_assignment, and the reassign_to_user should be unset.
it seems like it's about cancelling reassignment, that should be possible before user accepts or rejects it.
I noticed that we're using a mix of both the internal and the public API for bulk import features. For example, the history and the details page are using the public API, whereas the status information is being retrieved from the internal API. What's your preference here - should we implement the following controller actions under the public API?
GET index (to list all import source users)
POST reassign (to assign the user that the contributions will go to)
DELETE reassign (to remove the user that the contributions were assigned to)
/cc @georgekoltsov in case you have any thoughts/suggestions
I might need a bit more research on this to form an opinion. Currently, the group members page uses data from a helper at doesn't even use any APIs to fetch data / for pagination. For example, when you click on page 2 on the gitlab-org members page, the whole page refreshes and gets its data from the helper.
I am not sure if we will follow this convention or use something custom. At the very least, we will need to update the helper to include some pagination data, as it's used to show the tab and the users count.
@georgekoltsov mentioned that this API and controller implementations are usually separate, so I'll make a separate issue for API endpoints then for another milestone. This issue should be ready for development then, but please reach out if there's any questions!
GitLab often has our Vue.js frontend make GraphQL calls, in which case the backend implementation happens once in the GraphQL API. GraphQL is well suited for frontend needs. I wonder if we could implement it once in GraphQL, rather than twice in a controller and REST API.
I think in Import and Integrate because much of our product is from the pre-GraphQL era, the frontend tends not to be written to call GraphQL. And since GraphQL has no real support for our product we also select the REST API over GraphQL when adding endpoints for consistency, as the existing endpoints for the particular features are also in REST. Generally the frontend doesn't call our REST API but is supplied data through controllers and our own serializers. We don't tend to call our controllers a "private/internal API" because that can confuse it with our real internal REST API endpoints (most are here), so we tend to just call those endpoints just "controllers".
The question is whether this is greenfield enough to be implemented in GraphQL in the backend without introducing an inconsistency with other import-related endpoints being in the REST API? And then also how ready our frontend is to call GraphQL instead of the more traditional (for our domain) ways of being passed its data from a controller.
I think this feature isn't tied to any particular importer, but stands as its own adjacent-feature so it could be done in GraphQL and not REST without it feeling like we're being inconsistent.
Another thing to weigh is that generally, GitLab prefers we implement API support in GraphQL where possible, rather than REST, if we pick just one. (There's been a long history of this discussion, but recently #446241 (comment 1806802463)).
There are pragmatics of our domain's frontend readiness/support for GraphQL that I don't understand, which are also a factor in where the backend should implement support.
@.luke Thank you for raising this! My perception of this flow is that we anticipate users will primarily interact with it via GitLab's UI, even if public API endpoints are technically available. Is that correct?
If so, and it's the case that (essentially re-phrasing to make sure I understood correctly):
These endpoints share no significant existing with other endpoints in the domain.
Implementing them as GraphQL endpoints is more consistent with the preferred way of fetching data for Vue.
Implementing as GraphQL endpoints potentially avoids duplication between controllers and API endpoints.
On balance, it sounds like GraphQL would be the way to go.
Ok so before I go into the discussion on GraphQL, I should mention a few things:
The Group members page (on which we want to render the new UI) uses a quite complex app that is used for a few other member types (invited users, banner users, etc). With the current setup, it's likely that controller + helper changes are non-negotiable as we need at least the pagination info, which is used by the frontend to determine whether to render our "Placeholders" tab or not (it only renders if there are more than 0 items). I found this recent backend MR that does what we plan to add but for another member type: !146678 (diffs).
Once we have the tab rendered with the above conditions, we can fetch more data (for example, paginated results) pretty much any way we want as it's a fully custom app.
Generally the frontend doesn't call our REST API but is supplied data through controllers and our own serializers.
This is kinda true for simple apps but of course there are a bunch of frontend apps that call either GraphQL / REST API. For this particular page, we use data provided directly from helpers.
Back to the GraphQL / REST API discussion, I think other than our frontend we also need to consider the needs of our customers. I often see requests to add (REST) API endpoints after we rollout a feature, even if there is already UI that works just fine.
Overall, the simplest solution would be to fully fetch data from controllers + helpers (including pagination and filtering; similar to this code) and then the frontend won't even need to use any of GraphQL / REST API. Otherwise if we want to handle pagination and filtering separately, I don't mind using GraphQL if that's the preference.
Thank you @justin_ho! So there's a hard requirement to support some paginated data to the frontend via a helper to render the tab, and then in terms of data served within the new Vue.js app when the tab is clicked we can use whatever source we decide?
There will be some other backend interactions besides just fetching data. (I'm not sure all of them because I'm unable to see the Figma design mentioned in &12378 (closed) and the user flow design in the epic doesn't go into detail - @m_frankiewicz do we know more detail of what exactly the user will see and do when they're mapping users?). Another mutation would at least be needed to persist the mapping. Perhaps we need to allow people to search for users to map to, and could use existing endpoint?
Another angle is backend developer productivity: For backend engineers, on the one hand, a GraphQL implementation may involve some learning. On the other, implementing it once in the backend instead of twice pays off in terms of time saved. So perhaps the backend experience would be about even. There's also a potential benefit to backend engineers to learn new skills.
It sounds like in terms of frontend productivity, it might be easier for the endpoints to be in controllers. @justin_ho do you think a frontend implementation with GraphQL would take away from your productivity?
Ah yes one of the things I wanted to mention but totally forgot. So there are a few actions in the user mapping flow that will not be backed by any UI. Namely the Approve / Reject contributions by individual users (the mapped user) which they click from within an email. This will likely link to some controller that stores that action, redirects them and shows a flash message with the result (see this design #451028[Email_-_Destination_User_Re-Assignment.png]).
do you think a frontend implementation with GraphQL would take away from your productivity?
If the question is about comparing REST API and GraphQL, I would (personally) say it's about the same for me. It's just a different way of fetching data and I've worked with both in different features, so I'm good with either.
It sounds like in terms of frontend productivity, it might be easier for the endpoints to be in controllers.
Yes but mostly because this is how the other member types on the same page do it and it's always easier to copy that approach. Also to make sure we are on the same page, what I am referring to is adding code to existing controllers (the same ones that render the page) and passing data via helpers, not adding new API endpoints.
@gitlab-org/manage/import-and-integrate/dev How does this sound?
A controller for:
Handling the approve/reject actions by individual users (the mapped user) that they click from within an email.
GraphQL for other backend work to support the UI to map users:
Return paginated results of Import::SourceUser records, filterable by its status (design). Possibly filterable by search on placeholder username (design) which might be a fun SQL query.
Mutation to reassign the Import::SourceUser record.
Have we missed any?
Frontend will still need some data supplied through helpers to initialize the tabs #443555 (comment 1875250638).
@.luke@SamWord If we have the accept/reject endpoints on GET actions, does that open us up to CSRF? A malicious actor could create a contribution for a victim user and then send them an email with <img src="https://gitlab.com/assignments/12/accept"> or similar. Do we have any protections in place against that?
Would it be safer to have the link in the email direct the user to a show action from which they can POST/DELETE to accept or reject?
@jnutt Good idea! We could present accept and decline buttons. We use a similar kind of 2-step flow when a user uses a Slack Slash Command for the first time. What do you think @m_frankiewicz?
@justin_ho@jnutt Thinking about the GraphQL design for this, we could add a field to Group and do something like (not all arguments would need to be supported in the first iteration):
{group(fullPath:"my-group"){# `importUsers` field returns a collection of `Import::SourceUser` records# `assignmentStatus` argument (optional), an array of reassignment statuses# (one of https://gitlab.com/gitlab-org/gitlab/-/issues/451028/designs/Reassignment_Statuses.png)# https://gitlab.com/gitlab-org/gitlab/-/issues/451028/designs/User_Mapping_-_Keep_as_Placeholder.png#note_1877820974# `search` argument (optional), text to filter associated user records by # https://gitlab.com/gitlab-org/gitlab/-/issues/451028/designs/User_Mapping_-_Keep_as_Placeholder.png#note_1877821427importUsers(reassignmentStatus:[],search:""){nodes{sourceUserNamesourceNameassignmentStatus# Enum, one of https://gitlab.com/gitlab-org/gitlab/-/issues/451028/designs/Reassignment_Statuses.pngassignedUser{# Types::User, the associated User recordidname}}}}}
We could have fine-grained mutations to set assignment, and unset assignment, and also set assignment status to "keep as placeholder" (see design). Our docs suggest not to use fine-grained mutations though. I don't have strong feelings about which way we take because both have benefits and downsides. It feels like the non-fine-grained would be harder for a member of the public to work with, as some combinations of argument values are invalid, whereas the fine-grained we'd be holding their hand better.
Non-fine grained:
mutation{# `id` required, all others optional# when `assignedUserId` provided and `null` it would be unset# if `keepAsPlaceholder` was provided as `true`, and also `assignedUserId` was present, this would be an errorimportUserUpdate(input:{id:"<Global ID of Import::SourceUser>",assignedUserId:"<Global ID of User>",keepAsPlaceholder:"<Boolean>"}){errorsimportUser{...ImportUsertype}}}
Fine-grained:
mutation{# `id` and `assignedUserId` requiredimportUserAssign(input:{id:"<Global ID of Import::SourceUser>",assignedUserId:"<Global ID of User>"}){errorsimportUser{...ImportUsertype}}}}
mutation{# `id` requiredimportUserUnassign(input:{id:"<Global ID of Import::SourceUser>"}){errorsimportUser{...ImportUsertype}}}
mutation{# `id` requiredimportUserSetAsPlaceholder(input:{id:"<Global ID of Import::SourceUser>"}){errorsimportUser{...ImportUsertype}}}
A malicious actor could create a contribution for a victim user
Yes, they could create a contribution on their SM instance, but not on an actual SM instance the victim user would contribute on and expect migration from. Also, how would they intercept/change the email that is sent during contribution mapping? Or you mean attacker would be sending random emails to people with fake buttons making them look like the legitimate emails sent during contribution reassignment?
@jnutt Good idea! We could present accept and decline buttons. We use a similar kind of 2-step flow when a user uses a Slack Slash Command for the first time. What do you think @m_frankiewicz?
@m_frankiewicz@.luke - I actually like having this intermediary step to confirm the reassignment. I don't think it'll be that hard to adjust from a design POV.
@m_frankiewicz I was thinking along the lines of creating a page with an image tag or similar that caused the accept URL to be loaded with the victim session with no further interaction. It wouldn't require a malicious person to intercept the email, but thinking about it, it would require them to guess the invite ID.
I'm not sure it's a concern in this case. Looking at the way invites are handled, it looks like the invite prompt is skipped so long as the invite is valid. @.luke, am I just being paranoid?
@emilybauman In the above design, should that first entry be the hostname, or the group being imported? I don't think we track the source group.
@SamWord Is the "reassigned by" information available anywhere? Is it always the owner of the destination namespace, or are others able to initiate this step?
@m_frankiewicz@jnutt - Thanks for the info. I just had copied over this content from the email, so perhaps we should have another look at #451028[Email_-_Destination_User_Re-Assignment.png] to ensure we can show all the data proposed. I also adjusted the content a little of the placeholder links so it's easier to understand.
@jnutt We don't have a reassigned_by on Import::SourceUser, so no we don't have that information anywhere. We'll need to add that column.
I think it would be a relatively easy to add as a part of the reassignment process though: when a user assigns a real user to the placeholder user, we can just log who did that assignment at the same time we update reassign_to_user_id on the Import::SourceUser.
@m_frankiewicz Just a small thought from this comment #451028[User_Mapping_-_CSV.png] (comment 1895017411) I wonder if when a user chooses to Reject the assignment whether we mention somewhere small on the page the option to report abuse? This is based on the idea that even scoping who can receive an invite email to group membership doesn't prevent people from adding users as members of their group in a spammy way, and with CSV upload as an option perhaps the invite email could be used in a way to cause nuisance?
@m_frankiewicz@.luke - Do we want to add it to the email, banner or both? The next page after a user hits reject would show the banner in GitLab, but we could also add it to the email so they could report without having to take any action at all.
MR for accept/decline controller actions - awaiting final approval/merge after losing approvals due to broken pipeline - hopefully this will be closed out today
MR to add in-app confirmation step - broadly complete, just waiting for previous MR to be merged.
Since the confirmation step was outside of the original scope, we could split it out into a separate issue (weight: 1) for the next milestone and mark this as complete when !150736 (merged) is merged.
Otherwise, this will carry over with weight: 1 remaining.
@jnutt Thanks for the clarfication. Can you please open a separate issue for the confirmation scope and add it to gitlab-org/manage/general-discussion#17673 under the Direct Transfer bullet item?
In the discussion on this issue, we had 1 point which I haven't seen implemented (around adding the initial data in helpers). I've created a separate issue for this which should be implemented as milestonep1#468437 (closed).
It's dependent on some work (specifically the SourceUsersFinder) by @rodrigo.tomonari in his MR !156687 (merged), so backend can add it once that gets merged.