projects/:id/members/all/ API returns duplicate member when member is in a group and project with the same id

Summary

A client reported a duplicated member in the results for the /members/all API endpoint for a project. This member should only show once because they were a only member of one ancestral group of the project, and did not have membership to the project through any other way. We found that they were a member of a project that had the same ID as the ancestral group, which was causing the duplication.

Steps to reproduce

Note: IDs listed here are for example, could be any ID

  1. You have a project with ID 6588 that has one ancestral group with ID 25.
  2. You have a user with ID of 4 that is a member of the ancestral group 25, and is also a member of a project with ID 25.
  3. You query the /projects/6588/members/all endpoint, and you see user 4 show two times in the results, when they should show once.

What is the current bug behavior?

When querying /projects/:id/members/all you can get a duplicate member in the results, if that member is from an ancestral group with the same ID as another project.

What is the expected correct behavior?

When querying /projects/:id/members/all you shouldn't get a duplicate user in this case.

Relevant logs and/or screenshots

(Paste any relevant logs - please use code blocks (```) to format console output, logs, and code as it's very hard to read otherwise.)

Output of checks

(If you are reporting a bug on GitLab.com, write: This bug happens on GitLab.com)

Results of GitLab environment info

Expand for output related to GitLab environment info

System information System: Proxy: no Current User: git Using RVM: no Ruby Version: 2.4.4p296 Gem Version: 2.7.6 Bundler Version:1.16.2 Rake Version: 12.3.1 Redis Version: 3.2.11 Git Version: 2.18.0 Sidekiq Version:5.1.3 Go Version: unknown

GitLab information Version: 11.2.3-ee Revision: aadca99 Directory: /opt/gitlab/embedded/service/gitlab-rails DB Adapter: postgresql DB Version: 9.6.8 URL: https://gitlab-dev-env.eng.vmware.com HTTP Clone URL: https://gitlab-dev-env.eng.vmware.com/some-group/some-project.git SSH Clone URL: git@gitlab-dev-env.eng.vmware.com:some-group/some-project.git Elasticsearch: yes Geo: no Using LDAP: yes Using Omniauth: no

GitLab Shell Version: 8.1.1 Repository storage paths:

  • default: /gitlab-data/git-data/repositories Hooks: /opt/gitlab/embedded/service/gitlab-shell/hooks Git: /opt/gitlab/embedded/bin/git

Results of GitLab application Check

Expand for output related to the GitLab application check

Checking GitLab Shell ...

GitLab Shell version >= 8.1.1 ? ... OK (8.1.1) Repo base directory exists? default... yes Repo storage directories are symlinks? default... no Repo paths owned by git:root, or git:git? default... yes Repo paths access is drwxrws---? default... yes hooks directories in repos are links: ... 2/1 ... ok 4/2 ... ok 4/5 ... repository is empty 12/6 ... repository is empty 13/7 ... ok 6/8 ... ok . . . Redis version >= 2.8.0? ... yes Ruby version >= 2.3.5 ? ... yes (2.4.4) Git version >= 2.9.5 ? ... yes (2.18.0) Git user has default SSH configuration? ... no Try fixing it: mkdir ~/gitlab-check-backup-1538149648 sudo mv /gitlab-data/home/.ssh/id_rsa.pub ~/gitlab-check-backup-1538149648 sudo mv /gitlab-data/home/.ssh/id_rsa ~/gitlab-check-backup-1538149648 For more information see: doc/ssh/README.md in section "SSH on the GitLab server" Please fix the error above and rerun the checks. Active users: ... 43 Elasticsearch version 5.1 - 5.5? ... yes (5.5.3)

Checking GitLab ... Finished

Possible fixes

Looks to be a corner case for the find_all_members_for_project method

The source IDs are in an array but they aren't associated with the type of source they are coming from. So, if a member of the project being queried is a member of an ancestral group that has the same ID as another project, there will be two members returned, one for the ancestral group (which is correct) and one for the project with that ID (which is incorrect).

One possibility is to get the members from the project separately from getting members from the ancestral groups and shared groups. For the members from the project, you can utilize the call from the /members/ endpoint:

members = project.members.where.not(user_id: nil).includes(:user)

Then, you can have the source type to be 'Namespace' when retrieving members for the ancestral groups and shared groups:

Member.includes(:user)
 .joins(user: :project_authorizations)
 .where(project_authorizations: { project_id: project.id })
 .where(source_id: source_ids)
 .where(source_type: 'Namespace')
Edited by Blair Lunceford