From 905239f530b712c3609ef375a2587dfe48d0a846 Mon Sep 17 00:00:00 2001 From: Zainab Alsaffar Date: Wed, 16 Sep 2020 01:24:37 -0400 Subject: [PATCH] add a custom module for managing group membership in gitlab (#844) * add a custom module for managing group membership in gitlab * add integration test & modify the module * modify the module * modify the module * remove whitespace * add aliases file & modify the module * minor and suggested modifications * suggested modifications * more minor modifications * modified the module to use gitlabAuth * removed api_url from the doc * remove api_token * add update access level for an existing user * remove access level if statement --- plugins/modules/gitlab_group_members.py | 1 + .../gitlab/gitlab_group_members.py | 263 ++++++++++++++++++ .../targets/gitlab_group_members/aliases | 1 + .../gitlab_group_members/tasks/main.yml | 25 ++ .../gitlab_group_members/vars/main.yml | 5 + 5 files changed, 295 insertions(+) create mode 120000 plugins/modules/gitlab_group_members.py create mode 100644 plugins/modules/source_control/gitlab/gitlab_group_members.py create mode 100644 tests/integration/targets/gitlab_group_members/aliases create mode 100644 tests/integration/targets/gitlab_group_members/tasks/main.yml create mode 100644 tests/integration/targets/gitlab_group_members/vars/main.yml diff --git a/plugins/modules/gitlab_group_members.py b/plugins/modules/gitlab_group_members.py new file mode 120000 index 0000000000..ab80ca08ba --- /dev/null +++ b/plugins/modules/gitlab_group_members.py @@ -0,0 +1 @@ +./source_control/gitlab/gitlab_group_members.py \ No newline at end of file diff --git a/plugins/modules/source_control/gitlab/gitlab_group_members.py b/plugins/modules/source_control/gitlab/gitlab_group_members.py new file mode 100644 index 0000000000..8a3da2a41b --- /dev/null +++ b/plugins/modules/source_control/gitlab/gitlab_group_members.py @@ -0,0 +1,263 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Copyright: (c) 2020, Zainab Alsaffar +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + +DOCUMENTATION = r''' +--- +module: gitlab_group_members +short_description: Manage group members on GitLab Server +description: + - This module allows to add and remove members to/from a group, or change a member's access level in a group on GitLab. +version_added: '1.2.0' +author: Zainab Alsaffar (@zanssa) +requirements: + - python-gitlab python module <= 1.15.0 + - administrator rights on the GitLab server +extends_documentation_fragment: community.general.auth_basic +options: + api_token: + description: + - A personal access token to authenticate with the GitLab API. + required: true + type: str + gitlab_group: + description: + - The name of the GitLab group the member is added to/removed from. + required: true + type: str + gitlab_user: + description: + - The username of the member to add to/remove from the GitLab group. + required: true + type: str + access_level: + description: + - The access level for the user. + - Required if I(state=present), user state is set to present. + type: str + choices: ['guest', 'reporter', 'developer', 'maintainer', 'owner'] + state: + description: + - State of the member in the group. + - On C(present), it adds a user to a GitLab group. + - On C(absent), it removes a user from a GitLab group. + choices: ['present', 'absent'] + default: 'present' + type: str +notes: + - Supports C(check_mode). +''' + +EXAMPLES = r''' +- 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: groupname + gitlab_user: username + access_level: developer + state: present + +- name: Remove a user from a GitLab Group + community.general.gitlab_group_members: + api_url: 'https://gitlab.example.com' + api_token: 'Your-Private-Token' + gitlab_group: groupname + gitlab_user: username + state: absent +''' + +RETURN = r''' # ''' + +from ansible.module_utils.api import basic_auth_argument_spec +from ansible.module_utils.basic import AnsibleModule, missing_required_lib + +from ansible_collections.community.general.plugins.module_utils.gitlab import gitlabAuthentication + +import traceback + +try: + import gitlab + HAS_PY_GITLAB = True +except ImportError: + GITLAB_IMP_ERR = traceback.format_exc() + HAS_PY_GITLAB = False + + +class GitLabGroup(object): + def __init__(self, module, gl): + self._module = module + self._gitlab = gl + + # get user id if the user exists + def get_user_id(self, gitlab_user): + user_exists = self._gitlab.users.list(username=gitlab_user) + if user_exists: + return user_exists[0].id + + # 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 + + # get all members in a group + def get_members_in_a_group(self, gitlab_group_id): + group = self._gitlab.groups.get(gitlab_group_id) + return group.members.list() + + # check if the user is a member of the group + def is_user_a_member(self, members, gitlab_user_id): + for member in members: + if member.id == gitlab_user_id: + return True + return False + + # add user to a group + def add_member_to_group(self, gitlab_user_id, gitlab_group_id, access_level): + try: + group = self._gitlab.groups.get(gitlab_group_id) + add_member = group.members.create( + {'user_id': gitlab_user_id, 'access_level': access_level}) + + if add_member: + return add_member.username + + except (gitlab.exceptions.GitlabCreateError) as e: + self._module.fail_json( + msg="Failed to add member to the Group, Group ID %s: %s" % (gitlab_group_id, e)) + + # remove user from a group + def remove_user_from_group(self, gitlab_user_id, gitlab_group_id): + try: + group = self._gitlab.groups.get(gitlab_group_id) + group.members.delete(gitlab_user_id) + + except (gitlab.exceptions.GitlabDeleteError) as e: + self._module.fail_json( + msg="Failed to remove member from GitLab group, ID %s: %s" % (gitlab_group_id, e)) + + # get user's access level + def get_user_access_level(self, members, gitlab_user_id): + for member in members: + if member.id == gitlab_user_id: + return member.access_level + + # update user's access level in a group + def update_user_access_level(self, members, gitlab_user_id, access_level): + for member in members: + if member.id == gitlab_user_id: + try: + member.access_level = access_level + member.save() + except (gitlab.exceptions.GitlabCreateError) as e: + self._module.fail_json( + msg="Failed to update the access level for the member, %s: %s" % (gitlab_user_id, e)) + + +def main(): + argument_spec = basic_auth_argument_spec() + argument_spec.update(dict( + api_token=dict(type='str', required=True, no_log=True), + gitlab_group=dict(type='str', required=True), + gitlab_user=dict(type='str', required=True), + state=dict(type='str', default='present', choices=['present', 'absent']), + access_level=dict(type='str', required=False, choices=['guest', 'reporter', 'developer', 'maintainer', 'owner']) + )) + + module = AnsibleModule( + argument_spec=argument_spec, + mutually_exclusive=[ + ['api_username', 'api_token'], + ['api_password', 'api_token'], + ], + required_together=[ + ['api_username', 'api_password'], + ], + required_one_of=[ + ['api_username', 'api_token'], + ], + required_if=[ + ['state', 'present', ['access_level']], + ], + supports_check_mode=True, + ) + + if not HAS_PY_GITLAB: + module.fail_json(msg=missing_required_lib('python-gitlab', url='https://python-gitlab.readthedocs.io/en/stable/'), exception=GITLAB_IMP_ERR) + + gitlab_group = module.params['gitlab_group'] + gitlab_user = module.params['gitlab_user'] + state = module.params['state'] + access_level = module.params['access_level'] + + # convert access level string input to int + if access_level: + access_level_int = { + 'guest': gitlab.GUEST_ACCESS, + 'reporter': gitlab.REPORTER_ACCESS, + 'developer': gitlab.DEVELOPER_ACCESS, + 'maintainer': gitlab.MAINTAINER_ACCESS, + 'owner': gitlab.OWNER_ACCESS + } + + access_level = access_level_int[access_level] + + # connect to gitlab server + gl = gitlabAuthentication(module) + + group = GitLabGroup(module, gl) + + gitlab_user_id = group.get_user_id(gitlab_user) + gitlab_group_id = group.get_group_id(gitlab_group) + + # group doesn't exist + if not gitlab_group_id: + module.fail_json(msg="group '%s' not found." % gitlab_group) + + # user doesn't exist + if not gitlab_user_id: + if state == 'absent': + module.exit_json(changed=False, result="user '%s' not found, and thus also not part of the group" % gitlab_user) + else: + module.fail_json(msg="user '%s' not found." % gitlab_user) + + members = group.get_members_in_a_group(gitlab_group_id) + is_user_a_member = group.is_user_a_member(members, gitlab_user_id) + + # check if the user is a member in the group + if not is_user_a_member: + if state == 'present': + # add user to the group + if not module.check_mode: + group.add_member_to_group(gitlab_user_id, gitlab_group_id, access_level) + module.exit_json(changed=True, result="Successfully added user '%s' to the group." % gitlab_user) + # state as absent + else: + module.exit_json(changed=False, result="User, '%s', is not a member in the group. No change to report" % gitlab_user) + # in case that a user is a member + else: + if state == 'present': + # compare the access level + user_access_level = group.get_user_access_level(members, gitlab_user_id) + if user_access_level == access_level: + module.exit_json(changed=False, result="User, '%s', is already a member in the group. No change to report" % gitlab_user) + else: + # update the access level for the user + if not module.check_mode: + group.update_user_access_level(members, gitlab_user_id, access_level) + module.exit_json(changed=True, result="Successfully updated the access level for the user, '%s'" % gitlab_user) + else: + # remove the user from the group + if not module.check_mode: + group.remove_user_from_group(gitlab_user_id, gitlab_group_id) + module.exit_json(changed=True, result="Successfully removed user, '%s', from the group" % gitlab_user) + + +if __name__ == '__main__': + main() diff --git a/tests/integration/targets/gitlab_group_members/aliases b/tests/integration/targets/gitlab_group_members/aliases new file mode 100644 index 0000000000..89aea537d1 --- /dev/null +++ b/tests/integration/targets/gitlab_group_members/aliases @@ -0,0 +1 @@ +unsupported \ No newline at end of file diff --git a/tests/integration/targets/gitlab_group_members/tasks/main.yml b/tests/integration/targets/gitlab_group_members/tasks/main.yml new file mode 100644 index 0000000000..dc7b2489be --- /dev/null +++ b/tests/integration/targets/gitlab_group_members/tasks/main.yml @@ -0,0 +1,25 @@ +# Test code for gitlab_group_members module +# +# Copyright: (c) 2020, Zainab Alsaffar +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) +- name: Install required library + pip: + name: python-gitlab + state: present + +- name: Add a User to A GitLab Group + gitlab_group_members: + api_url: '{{ gitlab_server_url }}' + api_token: '{{ gitlab_api_access_token }}' + gitlab_group: '{{ gitlab_group_name }}' + gitlab_user: '{{ username }}' + access_level: '{{ gitlab_access_level }}' + state: present + +- name: Remove a User from A GitLab Group + gitlab_group_members: + api_url: '{{ gitlab_server_url }}' + api_token: '{{ gitlab_api_access_token }}' + gitlab_group: '{{ gitlab_group_name }}' + gitlab_user: '{{ username }}' + state: absent \ No newline at end of file diff --git a/tests/integration/targets/gitlab_group_members/vars/main.yml b/tests/integration/targets/gitlab_group_members/vars/main.yml new file mode 100644 index 0000000000..7f68893cf9 --- /dev/null +++ b/tests/integration/targets/gitlab_group_members/vars/main.yml @@ -0,0 +1,5 @@ +gitlab_server_url: https://gitlabserver.example.com +gitlab_api_access_token: 126hngbscx890cv09b +gitlab_group_name: groupname1 +username: username1 +gitlab_access_level: developer \ No newline at end of file