Repository: ansible-collections/community.general

Issue:
GitLab API searches always return first found match
### Summary

When trying to add gitlab users to a gitlab group using the `gitlab_group_members` we found that the users were not assigned to the correct given group. Instead they were assigned on a totally different subgroup.
We traced down the problem and found that every time the API is asked to search a specific group the module only returns the first match instead of the "exact" match.

This might be the same behavior for groups as @btck30351 described in #2767 for projects in `gitlab_project_members`.

Findings during our trace:

- `gitlab_group_members` doesn't change the permissions of the group that was given but a different one
- https://github.com/ansible-collections/community.general/blob/7a2efb4775af4296bf4dfe640cbc0ae52c87d5dd/plugins/modules/source_control/gitlab/gitlab_group_members.py#L182 searches for the given group value using the `python-gitlab` 
- The [corresponding code in `python-gitlab`](https://github.com/python-gitlab/python-gitlab/blob/e19314dcc481b045ba7a12dd76abedc08dbdf032/gitlab/v4/objects/groups.py#L93) calls the GitLab API and searches for groups containing the `value` that was given
- As the [GitLab API docs for group searching](https://docs.gitlab.com/ee/api/groups.html#search-for-group) says this will return **all** groups that match the given `value` in name or path
- https://github.com/ansible-collections/community.general/blob/7a2efb4775af4296bf4dfe640cbc0ae52c87d5dd/plugins/modules/source_control/gitlab/gitlab_group_members.py#L184 returns only the first item of the result list of the API call

So we think that the code of `get_group_id` https://github.com/ansible-collections/community.general/blob/7a2efb4775af4296bf4dfe640cbc0ae52c87d5dd/plugins/modules/source_control/gitlab/gitlab_group_members.py#L181

Should further filter the returned result. Not picking the first found but the exact match.
To implement such functionality (and backwards compatibility) it is crucial to know which value is expected here. The [docs](https://docs.ansible.com/ansible/latest/collections/community/general/gitlab_group_members_module.html) say:
| Parameter | Type | Comment |
|------------|-------|------------|
|`gitlab_group` | `string/required` | The name of the GitLab group the member is added to/removed from.|

Combining this with the official API docs that state:
> Get all groups that match your string in their name or path.

We are left with the valid options `name` or `path`.

Looking in the examples of the docs there are only single-worded lowercased group names used so we didn't get further. In our opinion using the path here makes the most sense as it is unique and can therefore be matched exactly.

cc @sh0shin

### Issue Type

Bug Report

### Component Name

`gitlab_group_members`

### Ansible Version

```console (paste below)
$ ansible --version
ansible [core 2.11.5]
  config file = /Users/sh0/Devel/cancom/gitlab/hybrid-it/projects/gitlab/gitlab-bootstrap-ansible/ansible.cfg
  configured module search path = ['/Users/sh0/Devel/cancom/gitlab/hybrid-it/projects/gitlab/gitlab-bootstrap-ansible/library']
  ansible python module location = /Users/sh0/.pyenv/versions/3.8.12/envs/ansible4.5@python3.8.12/lib/python3.8/site-packages/ansible
  ansible collection location = /Users/sh0/Devel/cancom/gitlab/hybrid-it/projects/gitlab/gitlab-bootstrap-ansible/collections
  executable location = /Users/sh0/.pyenv/versions/ansible4.5@python3.8.12/bin/ansible
  python version = 3.8.12 (default, Sep 16 2021, 08:52:19) [Clang 12.0.5 (clang-1205.0.22.9)]
  jinja version = 3.0.1
  libyaml = True
```


### Configuration

```console (paste below)
$ ansible-config dump --only-changed

ANSIBLE_NOCOWS(/Users/sh0/Devel/cancom/gitlab/hybrid-it/projects/gitlab/gitlab-bootstrap-ansible/ansible.cfg) = True
CACHE_PLUGIN(/Users/sh0/Devel/cancom/gitlab/hybrid-it/projects/gitlab/gitlab-bootstrap-ansible/ansible.cfg) = jsonfile
CACHE_PLUGIN_CONNECTION(/Users/sh0/Devel/cancom/gitlab/hybrid-it/projects/gitlab/gitlab-bootstrap-ansible/ansible.cfg) = fact
CACHE_PLUGIN_TIMEOUT(/Users/sh0/Devel/cancom/gitlab/hybrid-it/projects/gitlab/gitlab-bootstrap-ansible/ansible.cfg) = 3600
COLLECTIONS_PATHS(/Users/sh0/Devel/cancom/gitlab/hybrid-it/projects/gitlab/gitlab-bootstrap-ansible/ansible.cfg) = ['/Users/sh0/Devel/cancom/gitlab/hybrid-it/projects/gitlab/gitlab-bootstrap-ansible/collections']
DEFAULT_GATHERING(/Users/sh0/Devel/cancom/gitlab/hybrid-it/projects/gitlab/gitlab-bootstrap-ansible/ansible.cfg) = smart
DEFAULT_GATHER_SUBSET(/Users/sh0/Devel/cancom/gitlab/hybrid-it/projects/gitlab/gitlab-bootstrap-ansible/ansible.cfg) = ['all']
DEFAULT_HOST_LIST(/Users/sh0/Devel/cancom/gitlab/hybrid-it/projects/gitlab/gitlab-bootstrap-ansible/ansible.cfg) = ['/Users/sh0/Devel/cancom/gitlab/hybrid-it/projects/gitlab/gitlab-bootstrap-ansible/inventory/hosts.ini']
DEFAULT_KEEP_REMOTE_FILES(/Users/sh0/Devel/cancom/gitlab/hybrid-it/projects/gitlab/gitlab-bootstrap-ansible/ansible.cfg) = False
DEFAULT_LOG_PATH(/Users/sh0/Devel/cancom/gitlab/hybrid-it/projects/gitlab/gitlab-bootstrap-ansible/ansible.cfg) = /Users/sh0/Devel/cancom/gitlab/hybrid-it/projects/gitlab/gitlab-bootstrap-ansible/log/ansible.log
DEFAULT_MODULE_PATH(/Users/sh0/Devel/cancom/gitlab/hybrid-it/projects/gitlab/gitlab-bootstrap-ansible/ansible.cfg) = ['/Users/sh0/Devel/cancom/gitlab/hybrid-it/projects/gitlab/gitlab-bootstrap-ansible/library']
DEFAULT_NO_TARGET_SYSLOG(/Users/sh0/Devel/cancom/gitlab/hybrid-it/projects/gitlab/gitlab-bootstrap-ansible/ansible.cfg) = True
DEFAULT_ROLES_PATH(/Users/sh0/Devel/cancom/gitlab/hybrid-it/projects/gitlab/gitlab-bootstrap-ansible/ansible.cfg) = ['/Users/sh0/Devel/cancom/gitlab/hybrid-it/projects/gitlab/gitlab-bootstrap-ansible/roles']
DEFAULT_TRANSPORT(/Users/sh0/Devel/cancom/gitlab/hybrid-it/projects/gitlab/gitlab-bootstrap-ansible/ansible.cfg) = smart
DEFAULT_VAULT_PASSWORD_FILE(/Users/sh0/Devel/cancom/gitlab/hybrid-it/projects/gitlab/gitlab-bootstrap-ansible/ansible.cfg) = /Users/sh0/.ansible/vault
DISPLAY_SKIPPED_HOSTS(/Users/sh0/Devel/cancom/gitlab/hybrid-it/projects/gitlab/gitlab-bootstrap-ansible/ansible.cfg) = True
HOST_KEY_CHECKING(/Users/sh0/Devel/cancom/gitlab/hybrid-it/projects/gitlab/gitlab-bootstrap-ansible/ansible.cfg) = False
INTERPRETER_PYTHON(/Users/sh0/Devel/cancom/gitlab/hybrid-it/projects/gitlab/gitlab-bootstrap-ansible/ansible.cfg) = /usr/bin/env python3
INVENTORY_CACHE_ENABLED(/Users/sh0/Devel/cancom/gitlab/hybrid-it/projects/gitlab/gitlab-bootstrap-ansible/ansible.cfg) = True
INVENTORY_CACHE_PLUGIN(/Users/sh0/Devel/cancom/gitlab/hybrid-it/projects/gitlab/gitlab-bootstrap-ansible/ansible.cfg) = jsonfile
LOCALHOST_WARNING(/Users/sh0/Devel/cancom/gitlab/hybrid-it/projects/gitlab/gitlab-bootstrap-ansible/ansible.cfg) = False
RETRY_FILES_ENABLED(/Users/sh0/Devel/cancom/gitlab/hybrid-it/projects/gitlab/gitlab-bootstrap-ansible/ansible.cfg) = False
USE_PERSISTENT_CONNECTIONS(/Users/sh0/Devel/cancom/gitlab/hybrid-it/projects/gitlab/gitlab-bootstrap-ansible/ansible.cfg) = True

```


### OS / Environment

Orchestrate Gitlab Instance with the following Version:
- GitLab Community Edition `14.1.1` 

Run playbooks (`delegate_to: localhost`) on the following client:
- MacOS Big Sur 11.5.2 (`Darwin canton 20.6.0 Darwin Kernel Version 20.6.0: Wed Jun 23 00:26:31 PDT 2021; root:xnu-7195.141.2~5/RELEASE_X86_64 x86_64`)


### Steps to Reproduce

1. Create a group with a subgroup:
```yaml
- name: "Create GitLab Group"
  community.general.gitlab_group:
    api_url: https://gitlab.example.com/
    api_token: 'Your-Private-Token'
    name: parentgroup
    path: parentgroup
    state: present

- name: "Create GitLab SubGroup"
  community.general.gitlab_group:
    api_url: https://gitlab.example.com/
    api_token: 'Your-Private-Token'
    name: ansible-boombox
    path: ansible-boombox
    state: present
    parent: "parentgroup"

```

2. Create a group membership assignment:
```yaml
- name: Add a user to a GitLab Group
  community.general.gitlab_group_members:
    api_url: 'https://gitlab.example.com'
    api_token: 'Your-Private-Token'
    gitlab_group: parentgroup
    gitlab_user: username
    access_level: developer
    state: present
```


### Expected Results

We expect the user to be added to the `parentgroup` with access level "developer"

### Actual Results

The user `username` gets added to the subgroup `ansible-boombox` with access level developer

<details>
<summary>API output for group search</summary>

This happens because if we query the API to search for the group `parentgroup` we get this as a result:

```
curl -s "https://gitlab.example.com/api/v4/groups?search=parentgroup&private_token=XXX&simple=true" | jq -r

[
  {
    "id": 623,
    "web_url": "https://gitlab.example.com/groups/parentgroup/ansible-boombox",
    "name": "ansible-boombox",
    "path": "ansible-boombox",
    "description": "",
    "visibility": "private",
    "share_with_group_lock": true,
    "require_two_factor_authentication": true,
    "two_factor_grace_period": 48,
    "project_creation_level": "maintainer",
    "auto_devops_enabled": null,
    "subgroup_creation_level": "owner",
    "emails_disabled": null,
    "mentions_disabled": null,
    "lfs_enabled": true,
    "default_branch_protection": 2,
    "avatar_url": null,
    "request_access_enabled": true,
    "full_name": "parentgroup / ansible-boombox",
    "full_path": "parentgroup/ansible-boombox",
    "created_at": "2021-09-17T11:21:58.791+02:00",
    "parent_id": 532
  },
  {
    "id": 532,
    "web_url": "https://gitlab.example.com/groups/parentgroup",
    "name": "parentgroup",
    "path": "parentgroup",
    "description": "",
    "visibility": "private",
    "share_with_group_lock": true,
    "require_two_factor_authentication": true,
    "two_factor_grace_period": 48,
    "project_creation_level": "maintainer",
    "auto_devops_enabled": null,
    "subgroup_creation_level": "owner",
    "emails_disabled": null,
    "mentions_disabled": null,
    "lfs_enabled": true,
    "default_branch_protection": 2,
    "avatar_url": null,
    "request_access_enabled": false,
    "full_name": "parentgroup",
    "full_path": "parentgroup",
    "created_at": "2021-08-25T13:37:41.186+02:00",
    "parent_id": 528
  },
]
```

`ansible-boombox` is listed first and the `gitlab_group_members` module picks the first returned result instead of the exact match of `parentgroup`
</details>


### Code of Conduct

- [X] I agree to follow the Ansible Code of Conduct


Discussion:
Files identified in the description:
* [`plugins/modules/source_control/gitlab/gitlab_group_members.py`](https://github.com/ansible-collections/community.general/blob/main/plugins/modules/source_control/gitlab/gitlab_group_members.py)

If these files are incorrect, please update the `component name` section of the description or use the `!component` bot command.

[click here for bot help](https://github.com/ansible/ansibullbot/blob/devel/ISSUE_HELP.md)
<!--- boilerplate: components_banner --->
CC @Lunik @Shaps @dj-wasabi @marwatk @waheedi @zanssa @scodeman @metanovii
bot_status

(Felix noticed the bot isn't pinging maintainers)
<details><summary><b>Components</b></summary>

[plugins/modules/source_control/gitlab/gitlab_group_members.py](https://github.com/ansible/ansible/blob/devel/plugins/modules/source_control/gitlab/gitlab_group_members.py)
support: core
maintainers: Lunik Shaps dj-wasabi marwatk metanovii scodeman waheedi zanssa

</details>

<details><summary><b>Metadata</b></summary>

**waiting_on**: maintainer
[needs_info](https://github.com/ansible/ansibullbot/blob/devel/ISSUE_HELP.md#label-needs_info): False
</details>

[click here for bot help](https://github.com/ansible/ansibullbot/blob/devel/ISSUE_HELP.md)
<!--- boilerplate: bot_status --->

Resolving patch:
diff --git a/plugins/modules/source_control/gitlab/gitlab_group_members.py b/plugins/modules/source_control/gitlab/gitlab_group_members.py
--- a/plugins/modules/source_control/gitlab/gitlab_group_members.py
+++ b/plugins/modules/source_control/gitlab/gitlab_group_members.py
@@ -102,9 +102,13 @@ def get_user_id(self, gitlab_user):
 
     # get group id if group exists
     def get_group_id(self, gitlab_group):
-        group_exists = self._gitlab.groups.list(search=gitlab_group)
-        if group_exists:
-            return group_exists[0].id
+        groups = self._gitlab.groups.list(search=gitlab_group)
+        for group in groups:
+            if group.full_path == gitlab_group:
+                return group.id
+        for group in groups:
+            if group.path == gitlab_group or group.name == gitlab_group:
+                return group.id
 
     # get all members in a group
     def get_members_in_a_group(self, gitlab_group_id):


Test patch: