From fea9d055e3a22092496ad066d8aa87d855602644 Mon Sep 17 00:00:00 2001 From: Cole Date: Tue, 18 May 2021 23:19:41 -0700 Subject: [PATCH] New module: azure_rm_adgroup (#423) * init * functional. need to document and clean * documentation * functionally complete. Needs cleanup * Add tests and update documentation * Update tests * update test spacing * Update azure_rm_adgroup.py remove metadata block * Update azure_rm_adgroup_info.py remove metadata block * Apply suggestions from code review Co-authored-by: Fred-sun <37327967+Fred-sun@users.noreply.github.com> * Update tests/integration/targets/azure_rm_adgroup/tasks/main.yml Co-authored-by: Fred-sun <37327967+Fred-sun@users.noreply.github.com> * Update plugins/modules/azure_rm_adgroup.py Co-authored-by: Fred-sun <37327967+Fred-sun@users.noreply.github.com> * Update plugins/modules/azure_rm_adgroup.py Co-authored-by: Fred-sun <37327967+Fred-sun@users.noreply.github.com> * Update plugins/modules/azure_rm_adgroup_info.py Co-authored-by: Fred-sun <37327967+Fred-sun@users.noreply.github.com> * Update plugins/modules/azure_rm_adgroup.py Co-authored-by: Fred-sun <37327967+Fred-sun@users.noreply.github.com> * Update plugins/modules/azure_rm_adgroup_info.py Co-authored-by: Fred-sun <37327967+Fred-sun@users.noreply.github.com> * Update tests/integration/targets/azure_rm_adgroup/tasks/main.yml Co-authored-by: Fred-sun <37327967+Fred-sun@users.noreply.github.com> * Update main.yml * Update azure_rm_adgroup_info.py remove log references * Update azure_rm_adgroup.py remove log references * Update azure_rm_adgroup.py Update return * Update azure_rm_adgroup_info.py Update return * Update azure_rm_adgroup.py Update punctuation of description statements * Update azure_rm_adgroup_info.py Update punctuation of descriptive statements * Update plugins/modules/azure_rm_adgroup_info.py Co-authored-by: Fred-sun <37327967+Fred-sun@users.noreply.github.com> * Update plugins/modules/azure_rm_adgroup.py Co-authored-by: Fred-sun <37327967+Fred-sun@users.noreply.github.com> * Update plugins/modules/azure_rm_adgroup_info.py Co-authored-by: Fred-sun <37327967+Fred-sun@users.noreply.github.com> * Update plugins/modules/azure_rm_adgroup_info.py Co-authored-by: Fred-sun <37327967+Fred-sun@users.noreply.github.com> * Update plugins/modules/azure_rm_adgroup_info.py Co-authored-by: Fred-sun <37327967+Fred-sun@users.noreply.github.com> * Update plugins/modules/azure_rm_adgroup_info.py Co-authored-by: Fred-sun <37327967+Fred-sun@users.noreply.github.com> * Update plugins/modules/azure_rm_adgroup_info.py Co-authored-by: Fred-sun <37327967+Fred-sun@users.noreply.github.com> * Update plugins/modules/azure_rm_adgroup.py Co-authored-by: Fred-sun <37327967+Fred-sun@users.noreply.github.com> * Update plugins/modules/azure_rm_adgroup.py Co-authored-by: Fred-sun <37327967+Fred-sun@users.noreply.github.com> * Update plugins/modules/azure_rm_adgroup.py Co-authored-by: Fred-sun <37327967+Fred-sun@users.noreply.github.com> * Apply suggestions from code review Co-authored-by: Fred-sun <37327967+Fred-sun@users.noreply.github.com> * Update plugins/modules/azure_rm_adgroup_info.py Co-authored-by: Fred-sun <37327967+Fred-sun@users.noreply.github.com> * Update tests/integration/targets/azure_rm_adgroup/tasks/main.yml Co-authored-by: Fred-sun <37327967+Fred-sun@users.noreply.github.com> Co-authored-by: Fred-sun <37327967+Fred-sun@users.noreply.github.com> Co-authored-by: haiyuan_zhang --- plugins/modules/azure_rm_adgroup.py | 443 ++++++++++++++++++ plugins/modules/azure_rm_adgroup_info.py | 323 +++++++++++++ pr-pipelines.yml | 1 + .../targets/azure_rm_adgroup/aliases | 4 + .../targets/azure_rm_adgroup/meta/main.yml | 2 + .../targets/azure_rm_adgroup/tasks/main.yml | 239 ++++++++++ 6 files changed, 1012 insertions(+) create mode 100644 plugins/modules/azure_rm_adgroup.py create mode 100644 plugins/modules/azure_rm_adgroup_info.py create mode 100644 tests/integration/targets/azure_rm_adgroup/aliases create mode 100644 tests/integration/targets/azure_rm_adgroup/meta/main.yml create mode 100644 tests/integration/targets/azure_rm_adgroup/tasks/main.yml diff --git a/plugins/modules/azure_rm_adgroup.py b/plugins/modules/azure_rm_adgroup.py new file mode 100644 index 000000000..092ed8c78 --- /dev/null +++ b/plugins/modules/azure_rm_adgroup.py @@ -0,0 +1,443 @@ +#!/usr/bin/python +# +# Copyright (c) 2021 Cole Neubauer, (@coleneubauer) +# +# 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 = ''' +module: azure_rm_adgroup +version_added: "1.6.0" +short_description: Manage Azure Active Directory group +description: + - Create, update or delete Azure Active Directory group. +options: + tenant: + description: + - The tenant ID. + type: str + required: True + state: + description: + - Assert the state of the resource group. Use C(present) to create or update and C(absent) to delete. + default: present + choices: + - absent + - present + type: str + object_id: + description: + - The object id for the ad group. + - Can be used to reference when updating an existing group. + - Ignored when attempting to create a group. + type: str + display_name: + description: + - The display name of the ad group. + - Can be used with I(mail_nickname) instead of I(object_id) to reference existing group. + - Required when creating a new ad group. + type: str + mail_nickname: + description: + - The mail nickname of the ad group. + - Can be used with I(display_name) instead of I(object_id) to reference existing group. + - Required when creating a new ad group. + type: str + present_members: + description: + - The azure ad objects asserted to be members of the group. + - This list does not need to be all inclusive. Objects that are members and not on this list remain members. + type: list + elements: str + absent_members: + description: + - The azure ad objects asserted to not be members of the group. + type: list + elements: str + present_owners: + description: + - The azure ad objects asserted to be owners of the group. + - This list does not need to be all inclusive. Objects that are owners and not on this list remain members. + type: list + elements: str + absent_owners: + description: + - The azure ad objects asserted to not be owners of the group. + type: list + elements: str +extends_documentation_fragment: + - azure.azcollection.azure +author: + - Cole Neubauer(@coleneubauer) +''' + +EXAMPLES = ''' + - name: Create Group + azure_rm_adgroup: + tenant: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx + display_name: "Group-Name" + mail_nickname: "Group-Mail-Nickname" + state: 'present' + + - name: Delete Group using display_name and mail_nickname + azure_rm_adgroup: + tenant: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx + display_name: "Group-Name" + mail_nickname: "Group-Mail-Nickname" + state: 'absent' + + - name: Delete Group using object_id + azure_rm_adgroup: + tenant: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx + object_id: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx + state: 'absent' + + - name: Ensure Users are Members of a Group using display_name and mail_nickname + azure_rm_adgroup: + tenant: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx + display_name: "Group-Name" + mail_nickname: "Group-Mail-Nickname" + state: 'present' + present_members: + - "https://graph.windows.net/{{ tenant_id }}/directoryObjects/{{ ad_object_1_object_id }}" + - "https://graph.windows.net/{{ tenant_id }}/directoryObjects/{{ ad_object_2_object_id }}" + + - name: Ensure Users are Members of a Group using object_id + azure_rm_adgroup: + tenant: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx + object_id: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx + state: 'present' + present_members: + - "https://graph.windows.net/{{ ad_object_1_tenant_id }}/directoryObjects/{{ ad_object_1_object_id }}" + - "https://graph.windows.net/{{ ad_object_2_tenant_id }}/directoryObjects/{{ ad_object_2_object_id }}" + + - name: Ensure Users are not Members of a Group using display_name and mail_nickname + azure_rm_adgroup: + tenant: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx + display_name: "Group-Name" + mail_nickname: "Group-Mail-Nickname" + state: 'present' + absent_members: + - "{{ ad_object_1_object_id }}" + + - name: Ensure Users are Members of a Group using object_id + azure_rm_adgroup: + tenant: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx + object_id: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx + state: 'present' + absent_members: + - "{{ ad_object_1_object_id }}" + + - name: Ensure Users are Owners of a Group using display_name and mail_nickname + azure_rm_adgroup: + tenant: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx + display_name: "Group-Name" + mail_nickname: "Group-Mail-Nickname" + state: 'present' + present_owners: + - "https://graph.windows.net/{{ tenant_id }}/directoryObjects/{{ ad_object_1_object_id }}" + - "https://graph.windows.net/{{ tenant_id }}/directoryObjects/{{ ad_object_2_object_id }}" + + - name: Ensure Users are Owners of a Group using object_id + azure_rm_adgroup: + tenant: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx + object_id: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx + state: 'present' + present_owners: + - "https://graph.windows.net/{{ ad_object_1_tenant_id }}/directoryObjects/{{ ad_object_1_object_id }}" + - "https://graph.windows.net/{{ ad_object_2_tenant_id }}/directoryObjects/{{ ad_object_2_object_id }}" + + - name: Ensure Users are not Owners of a Group using display_name and mail_nickname + azure_rm_adgroup: + tenant: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx + display_name: "Group-Name" + mail_nickname: "Group-Mail-Nickname" + state: 'present' + absent_owners: + - "{{ ad_object_1_object_id }}" + - "{{ ad_object_2_object_id }}" + + - name: Ensure Users are Owners of a Group using object_id + azure_rm_adgroup: + tenant: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx + object_id: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx + state: 'present' + absent_owners: + - "{{ ad_object_1_object_id }}" + - "{{ ad_object_2_object_id }}" + +''' + +RETURN = ''' +object_id: + description: + - The object_id for the group. + type: str + returned: always + sample: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx +display_name: + description: + - The display name of the group. + returned: always + type: str + sample: GroupName +mail_nickname: + description: + - The mail alias for the group. + returned: always + type: str + sample: groupname +mail_enabled: + description: + - Whether the group is mail-enabled. Must be false. This is because only pure security groups can be created using the Graph API. + returned: always + type: bool + sample: False +security_enabled: + description: + - Whether the group is security-enable. + returned: always + type: bool + sample: False +mail: + description: + - The primary email address of the group. + returned: always + type: str + sample: group@contoso.com +group_owners: + description: + - The owners of the group. + returned: always + type: list +group_members: + description: + - The members of the group. + returned: always + type: list +''' + +from ansible_collections.azure.azcollection.plugins.module_utils.azure_rm_common_ext import AzureRMModuleBase + +try: + from msrestazure.azure_exceptions import CloudError + from azure.graphrbac.models import GraphErrorException + from azure.graphrbac.models import GroupCreateParameters +except ImportError: + # This is handled in azure_rm_common + pass + + +class AzureRMADGroup(AzureRMModuleBase): + def __init__(self): + + self.module_arg_spec = dict( + object_id=dict(type='str'), + display_name=dict(type='str'), + mail_nickname=dict(type='str'), + present_members=dict(type='list', elements='str'), + present_owners=dict(type='list', elements='str'), + absent_members=dict(type='list', elements='str'), + absent_owners=dict(type='list', elements='str'), + tenant=dict(type='str', required=True), + state=dict( + type='str', + default='present', + choices=['present', 'absent'] + ), + ) + + self.tenant = None + self.display_name = None + self.mail_nickname = None + self.object_id = None + self.present_members = [] + self.present_owners = [] + self.absent_members = [] + self.absent_owners = [] + self.state = None + self.results = dict(changed=False) + + super(AzureRMADGroup, self).__init__(derived_arg_spec=self.module_arg_spec, + supports_check_mode=False, + supports_tags=False, + is_ad_resource=True) + + def exec_module(self, **kwargs): + + for key in list(self.module_arg_spec.keys()): + setattr(self, key, kwargs[key]) + + # TODO remove ad_groups return. Returns as one object always + ad_groups = [] + + try: + client = self.get_graphrbac_client(self.tenant) + ad_groups = [] + + if self.display_name and self.mail_nickname: + ad_groups = list(client.groups.list(filter="displayName eq '{0}' and mailNickname eq '{1}'".format(self.display_name, self.mail_nickname))) + + if ad_groups: + self.object_id = ad_groups[0].object_id + + elif self.object_id: + ad_groups = [client.groups.get(self.object_id)] + + if ad_groups: + if self.state == "present": + self.results["changed"] = False + elif self.state == "absent": + ad_groups = [client.groups.delete(self.object_id)] + self.results["changed"] = True + else: + if self.state == "present": + if self.display_name and self.mail_nickname: + ad_groups = [client.groups.create(GroupCreateParameters(display_name=self.display_name, mail_nickname=self.mail_nickname))] + self.results["changed"] = True + else: + raise ValueError('The group does not exist. Both display_name : {0} and mail_nickname : {1} must be passed to create a new group' + .format(self.display_name, self.mail_nickname)) + elif self.state == "absent": + self.results["changed"] = False + + if ad_groups[0] is not None: + self.update_members(ad_groups[0].object_id, client) + self.update_owners(ad_groups[0].object_id, client) + self.results.update(self.set_results(ad_groups[0], client)) + + except GraphErrorException as e: + self.fail(e) + except ValueError as e: + self.fail(e) + + return self.results + + def update_members(self, group_id, client): + + current_members = [] + + if self.present_members or self.absent_members: + current_members = [object.object_id for object in list(client.groups.get_group_members(group_id))] + + if self.present_members: + present_members_by_object_id = self.dictionary_from_object_urls(self.present_members) + + members_to_add = list(set(present_members_by_object_id.keys()) - set(current_members)) + + if members_to_add: + for member_object_id in members_to_add: + client.groups.add_member(group_id, present_members_by_object_id[member_object_id]) + + self.results["changed"] = True + + if self.absent_members: + members_to_remove = list(set(self.absent_members).intersection(set(current_members))) + + if members_to_remove: + for member in members_to_remove: + client.groups.remove_member(group_id, member) + self.results["changed"] = True + + def update_owners(self, group_id, client): + current_owners = [] + + if self.present_owners or self.absent_owners: + current_owners = [object.object_id for object in list(client.groups.list_owners(group_id))] + + if self.present_owners: + + present_owners_by_object_id = self.dictionary_from_object_urls(self.present_owners) + + owners_to_add = list(set(present_owners_by_object_id.keys()) - set(current_owners)) + + if owners_to_add: + for owner_object_id in owners_to_add: + client.groups.add_owner(group_id, present_owners_by_object_id[owner_object_id]) + self.results["changed"] = True + + if self.absent_owners: + owners_to_remove = list(set(self.absent_owners).intersection(set(current_owners))) + + if owners_to_remove: + for owner in owners_to_remove: + client.groups.remove_owner(group_id, owner) + self.results["changed"] = True + + def dictionary_from_object_urls(self, object_urls): + objects_by_object_id = {} + + for urls in object_urls: + object_id = urls.split("/")[-1] + objects_by_object_id[object_id] = urls + + return objects_by_object_id + + def application_to_dict(self, object): + return dict( + app_id=object.app_id, + object_id=object.object_id, + display_name=object.display_name, + ) + + def serviceprincipal_to_dict(self, object): + return dict( + app_id=object.app_id, + object_id=object.object_id, + app_display_name=object.display_name, + app_role_assignment_required=object.app_role_assignment_required + ) + + def group_to_dict(self, object): + return dict( + object_id=object.object_id, + display_name=object.display_name, + mail_nickname=object.mail_nickname, + mail_enabled=object.mail_enabled, + security_enabled=object.security_enabled, + mail=object.mail + ) + + def user_to_dict(self, object): + return dict( + object_id=object.object_id, + display_name=object.display_name, + user_principal_name=object.user_principal_name, + mail_nickname=object.mail_nickname, + mail=object.mail, + account_enabled=object.account_enabled, + user_type=object.user_type + ) + + def result_to_dict(self, object): + if object.object_type == "Group": + return self.group_to_dict(object) + elif object.object_type == "User": + return self.user_to_dict(object) + elif object.object_type == "Application": + return self.application_to_dict(object) + elif object.object_type == "ServicePrincipal": + return self.serviceprincipal_to_dict(object) + else: + return object.object_type + + def set_results(self, object, client): + results = self.group_to_dict(object) + + if results["object_id"] and (self.present_owners or self.absent_owners): + results["group_owners"] = [self.result_to_dict(object) for object in list(client.groups.list_owners(results["object_id"]))] + + if results["object_id"] and (self.present_members or self.absent_members): + results["group_members"] = [self.result_to_dict(object) for object in list(client.groups.get_group_members(results["object_id"]))] + + return results + + +def main(): + AzureRMADGroup() + + +if __name__ == '__main__': + main() diff --git a/plugins/modules/azure_rm_adgroup_info.py b/plugins/modules/azure_rm_adgroup_info.py new file mode 100644 index 000000000..de7fe1630 --- /dev/null +++ b/plugins/modules/azure_rm_adgroup_info.py @@ -0,0 +1,323 @@ +#!/usr/bin/python +# +# Copyright (c) 2021 Cole Neubauer, (@coleneubauer) +# +# 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 = ''' +module: azure_rm_adgroup_info +version_added: "1.6.0" +short_description: Get Azure Active Directory group info +description: + - Get Azure Active Directory group info. +options: + tenant: + description: + - The tenant ID. + type: str + required: True + object_id: + description: + - The object id for the ad group. + - returns the group which has this object ID. + type: str + attribute_name: + description: + - The name of an attribute that you want to match to attribute_value. + - If attribute_name is not a collection type it will return groups where attribute_name is equal to attribute_value. + - If attribute_name is a collection type it will return groups where attribute_value is in attribute_name. + type: str + attribute_value: + description: + - The value to match attribute_name to. + - If attribute_name is not a collection type it will return groups where attribute_name is equal to attribute_value. + - If attribute_name is a collection type it will groups users where attribute_value is in attribute_name. + type: str + odata_filter: + description: + - returns groups based on the the OData filter passed into this parameter. + type: str + check_membership: + description: + - The object ID of the contact, group, user, or service principal to check for membership against returned groups. + type: str + return_owners: + description: + - Indicate whether the owners of a group should be returned with the returned groups. + default: False + type: bool + return_group_members: + description: + - Indicate whether the members of a group should be returned with the returned groups. + default: False + type: bool + return_member_groups: + description: + - Indicate whether the groups in which a groups is a member should be returned with the returned groups. + default: False + type: bool + all: + description: + - If True, will return all groups in tenant. + - If False will return no users. + - It is recommended that you instead identify a subset of groups and use filter. + default: False + type: bool +extends_documentation_fragment: + - azure.azcollection.azure +author: + - Cole Neubauer(@coleneubauer) +''' + +EXAMPLES = ''' + - name: Return a specific group using object_id + azure_rm_adgroup_info: + object_id: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx + tenant: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx + + - name: Return a specific group using object_id and return the owners of the group + azure_rm_adgroup_info: + object_id: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx + return_owners: True + tenant: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx + + - name: Return a specific group using object_id and return the owners and members of the group + azure_rm_adgroup_info: + object_id: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx + return_owners: True + return_group_members: True + tenant: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx + + - name: Return a specific group using object_id and return the groups the group is a member of + azure_rm_adgroup_info: + object_id: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx + return_member_groups: True + tenant: "{{ tenant_id }}" + + - name: Return a specific group using object_id and check an ID for membership + azure_rm_adgroup_info: + object_id: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx + check_membership: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx + tenant: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx + + - name: Return a specific group using displayName for attribute_name + azure_rm_adgroup_info: + attribute_name: "displayName" + attribute_value: "Display-Name-Of-AD-Group" + tenant: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx + + - name: Return groups matching odata_filter + azure_rm_adgroup_info: + odata_filter: "mailNickname eq 'Mail-Nickname-Of-AD-Group'" + tenant: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx + + - name: Return all groups + azure_rm_adgroup_info: + tenant: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx + all: True + +''' + +RETURN = ''' +object_id: + description: + - The object_id for the group. + type: str + returned: always + sample: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx +display_name: + description: + - The display name of the group. + returned: always + type: str + sample: GroupName +mail_nickname: + description: + - The mail alias for the group. + returned: always + type: str + sample: groupname +mail_enabled: + description: + - Whether the group is mail-enabled. Must be false. This is because only pure security groups can be created using the Graph API. + returned: always + type: bool + sample: False +security_enabled: + description: + - Whether the group is security-enable. + returned: always + type: bool + sample: False +mail: + description: + - The primary email address of the group. + returned: always + type: str + sample: group@contoso.com +group_owners: + description: + - The owners of the group. + returned: always + type: list +group_members: + description: + - The members of the group. + returned: always + type: list +''' + +from ansible_collections.azure.azcollection.plugins.module_utils.azure_rm_common_ext import AzureRMModuleBase + +try: + from msrestazure.azure_exceptions import CloudError + from azure.graphrbac.models import GraphErrorException + from azure.graphrbac.models import CheckGroupMembershipParameters +except ImportError: + # This is handled in azure_rm_common + pass + + +class AzureRMADGroupInfo(AzureRMModuleBase): + def __init__(self): + + self.module_arg_spec = dict( + object_id=dict(type='str'), + attribute_name=dict(type='str'), + attribute_value=dict(type='str'), + odata_filter=dict(type='str'), + check_membership=dict(type='str'), + return_owners=dict(type='bool', default=False), + return_group_members=dict(type='bool', default=False), + return_member_groups=dict(type='bool', default=False), + all=dict(type='bool', default=False), + tenant=dict(type='str', required=True), + ) + + self.tenant = None + self.object_id = None + self.attribute_name = None + self.attribute_value = None + self.odata_filter = None + self.check_membership = None + self.return_owners = False + self.return_group_members = False + self.return_member_groups = False + self.all = False + + self.results = dict(changed=False) + + mutually_exclusive = [['odata_filter', 'attribute_name', 'object_id', 'all']] + required_together = [['attribute_name', 'attribute_value']] + required_one_of = [['odata_filter', 'attribute_name', 'object_id', 'all']] + + super(AzureRMADGroupInfo, self).__init__(derived_arg_spec=self.module_arg_spec, + supports_check_mode=False, + supports_tags=False, + mutually_exclusive=mutually_exclusive, + required_together=required_together, + required_one_of=required_one_of, + is_ad_resource=True) + + def exec_module(self, **kwargs): + + for key in list(self.module_arg_spec.keys()): + setattr(self, key, kwargs[key]) + + ad_groups = [] + + try: + client = self.get_graphrbac_client(self.tenant) + + if self.object_id is not None: + ad_groups = [client.groups.get(self.object_id)] + elif self.attribute_name is not None and self.attribute_value is not None: + ad_groups = list(client.groups.list(filter="{0} eq '{1}'".format(self.attribute_name, self.attribute_value))) + elif self.odata_filter is not None: # run a filter based on user input + ad_groups = list(client.groups.list(filter=self.odata_filter)) + elif self.all: + ad_groups = list(client.groups.list()) + + self.results['ad_groups'] = [self.set_results(group, client) for group in ad_groups] + + except GraphErrorException as e: + self.fail("failed to get ad group info {0}".format(str(e))) + + return self.results + + def application_to_dict(self, object): + return dict( + app_id=object.app_id, + object_id=object.object_id, + display_name=object.display_name, + ) + + def serviceprincipal_to_dict(self, object): + return dict( + app_id=object.app_id, + object_id=object.object_id, + app_display_name=object.display_name, + app_role_assignment_required=object.app_role_assignment_required + ) + + def group_to_dict(self, object): + return dict( + object_id=object.object_id, + display_name=object.display_name, + mail_nickname=object.mail_nickname, + mail_enabled=object.mail_enabled, + security_enabled=object.security_enabled, + mail=object.mail + ) + + def user_to_dict(self, object): + return dict( + object_id=object.object_id, + display_name=object.display_name, + user_principal_name=object.user_principal_name, + mail_nickname=object.mail_nickname, + mail=object.mail, + account_enabled=object.account_enabled, + user_type=object.user_type + ) + + def result_to_dict(self, object): + if object.object_type == "Group": + return self.group_to_dict(object) + elif object.object_type == "User": + return self.user_to_dict(object) + elif object.object_type == "Application": + return self.application_to_dict(object) + elif object.object_type == "ServicePrincipal": + return self.serviceprincipal_to_dict(object) + else: + return object.object_type + + def set_results(self, object, client): + results = self.group_to_dict(object) + + if results["object_id"] and self.return_owners: + results["group_owners"] = [self.result_to_dict(object) for object in list(client.groups.list_owners(results["object_id"]))] + + if results["object_id"] and self.return_group_members: + results["group_members"] = [self.result_to_dict(object) for object in list(client.groups.get_group_members(results["object_id"]))] + + if results["object_id"] and self.return_member_groups: + results["member_groups"] = [self.result_to_dict(object) for object in list(client.groups.get_member_groups(results["object_id"], False))] + + if results["object_id"] and self.check_membership: + results["is_member_of"] = client.groups.is_member_of( + CheckGroupMembershipParameters(group_id=results["object_id"], member_id=self.check_membership)).value + + return results + + +def main(): + AzureRMADGroupInfo() + + +if __name__ == '__main__': + main() diff --git a/pr-pipelines.yml b/pr-pipelines.yml index c14c90c91..77b85f40e 100644 --- a/pr-pipelines.yml +++ b/pr-pipelines.yml @@ -25,6 +25,7 @@ parameters: - 'sanity' - 'azure_rm_adapplication' - "azure_rm_acs" + - "azure_rm_adgroup" - "azure_rm_aduser" - "azure_rm_aks" - "azure_rm_appgateway" diff --git a/tests/integration/targets/azure_rm_adgroup/aliases b/tests/integration/targets/azure_rm_adgroup/aliases new file mode 100644 index 000000000..fc8bf1e71 --- /dev/null +++ b/tests/integration/targets/azure_rm_adgroup/aliases @@ -0,0 +1,4 @@ +cloud/azure +shippable/azure/group10 +disabled +destructive diff --git a/tests/integration/targets/azure_rm_adgroup/meta/main.yml b/tests/integration/targets/azure_rm_adgroup/meta/main.yml new file mode 100644 index 000000000..95e1952f9 --- /dev/null +++ b/tests/integration/targets/azure_rm_adgroup/meta/main.yml @@ -0,0 +1,2 @@ +dependencies: + - setup_azure diff --git a/tests/integration/targets/azure_rm_adgroup/tasks/main.yml b/tests/integration/targets/azure_rm_adgroup/tasks/main.yml new file mode 100644 index 000000000..09b3f686e --- /dev/null +++ b/tests/integration/targets/azure_rm_adgroup/tasks/main.yml @@ -0,0 +1,239 @@ +- set_fact: + tenant_id: "{{ azure_tenant }}" + resource_prefix: "{{ 999999999999999999994 | random | to_uuid }}" + run_once: yes + +- name: Try to return non-existent group using display name + azure_rm_adgroup_info: + attribute_name: "displayName" + attribute_value: "{{ resource_prefix }}-Group-Root" + tenant: "{{ tenant_id }}" + register: get_nonexistent_group_display_name_ShouldFail + failed_when: + - get_nonexistent_group_display_name_ShouldFail.ad_groups != [] + +- name: Create Group Root + azure_rm_adgroup: + tenant: "{{ tenant_id }}" + display_name: "{{ resource_prefix }}-Group-Root" + mail_nickname: "{{ resource_prefix }}-Group-Root" + state: 'present' + register: group_create_changed_ShouldPass + +- name: Create Group Should Return Not Changed + azure_rm_adgroup: + tenant: "{{ tenant_id }}" + display_name: "{{ resource_prefix }}-Group-Root" + mail_nickname: "{{ resource_prefix }}-Group-Root" + state: 'present' + register: group_create_unchanged_ShouldPass + +- name: Assert Otherwise Changed Returns are Equal + assert: + that: + - group_create_changed_ShouldPass.changed == True + - group_create_unchanged_ShouldPass.changed == False + - group_create_changed_ShouldPass.display_name == group_create_unchanged_ShouldPass.display_name + - group_create_changed_ShouldPass.mail_enabled == group_create_unchanged_ShouldPass.mail_enabled + - group_create_changed_ShouldPass.mail_nickname == group_create_unchanged_ShouldPass.mail_nickname + - group_create_changed_ShouldPass.object_id == group_create_unchanged_ShouldPass.object_id + - group_create_changed_ShouldPass.security_enabled == group_create_unchanged_ShouldPass.security_enabled + +- name: Return previously created group using object_id + azure_rm_adgroup_info: + object_id: "{{ group_create_unchanged_ShouldPass.object_id }}" + tenant: "{{ tenant_id }}" + register: get_created_object_id_ShouldPass + +- name: Assert Returns are Equal to Created Group + assert: + that: + - get_created_object_id_ShouldPass.ad_groups[0].object_id == group_create_unchanged_ShouldPass.object_id + +- name: Create Group Member 1 + azure_rm_adgroup: + tenant: "{{ tenant_id }}" + display_name: "{{ resource_prefix }}-Group-Member-1" + mail_nickname: "{{ resource_prefix }}-Group-Member-1" + state: 'present' + register: create_group_member_1_ShouldPass + +- name: Create Group Member 2 + azure_rm_adgroup: + tenant: "{{ tenant_id }}" + display_name: "{{ resource_prefix }}-Group-Member-2" + mail_nickname: "{{ resource_prefix }}-Group-Member-2" + state: 'present' + register: create_group_member_2_ShouldPass + +- name: Ensure member is in group using display_name and mail_nickname + azure_rm_adgroup: + tenant: "{{ tenant_id }}" + display_name: "{{ resource_prefix }}-Group-Root" + mail_nickname: "{{ resource_prefix }}-Group-Root" + state: 'present' + present_members: + - "https://graph.windows.net/{{ tenant_id }}/directoryObjects/{{ create_group_member_1_ShouldPass.object_id }}" + - "https://graph.windows.net/{{ tenant_id }}/directoryObjects/{{ create_group_member_2_ShouldPass.object_id }}" + register: add_members_to_group_ShouldPass + +- name: Validate members are in the group + assert: + that: + - add_members_to_group_ShouldPass.group_members[0].object_id == create_group_member_1_ShouldPass.object_id or add_members_to_group_ShouldPass.group_members[1].object_id == create_group_member_1_ShouldPass.object_id + - add_members_to_group_ShouldPass.group_members[1].object_id == create_group_member_2_ShouldPass.object_id or add_members_to_group_ShouldPass.group_members[0].object_id == create_group_member_2_ShouldPass.object_id + +- name: Ensure member is in group that is already present using object_id + azure_rm_adgroup: + tenant: "{{ tenant_id }}" + object_id: "{{ group_create_changed_ShouldPass.object_id }}" + state: 'present' + present_members: + - "https://graph.windows.net/{{ tenant_id }}/directoryObjects/{{ create_group_member_1_ShouldPass.object_id }}" + register: add_already_present_member_to_group_ShouldPass + +- name: Validate nothing changed from already present member + assert: + that: + - add_already_present_member_to_group_ShouldPass.changed == false + +- name: Ensure member is not in group using object_id + azure_rm_adgroup: + tenant: "{{ tenant_id }}" + object_id: "{{ group_create_changed_ShouldPass.object_id }}" + state: 'present' + absent_members: + - "{{ create_group_member_2_ShouldPass.object_id }}" + register: remove_member_from_group_ShouldPass + +- name: Validate Group Member 1 is in the group and Group Member 2 is not + assert: + that: + - remove_member_from_group_ShouldPass.group_members[0].object_id == create_group_member_1_ShouldPass.object_id + - remove_member_from_group_ShouldPass.group_members | length == 1 + +- name: Ensure member is not in group that is already not in group using display_name and mail_nickname + azure_rm_adgroup: + tenant: "{{ tenant_id }}" + display_name: "{{ resource_prefix }}-Group-Root" + mail_nickname: "{{ resource_prefix }}-Group-Root" + state: 'present' + absent_members: + - "{{ create_group_member_2_ShouldPass.object_id }}" + register: remove_already_absent_member_from_group_ShouldPass + +- name: Validate nothing changed from already absent member + assert: + that: + - remove_already_absent_member_from_group_ShouldPass.changed == false + +- name: Return a specific group using object_id + azure_rm_adgroup_info: + object_id: "{{ group_create_changed_ShouldPass.object_id }}" + tenant: "{{ tenant_id }}" + register: object_id_ShouldPass + +- name: Return a specific group using object_id and return_owners + azure_rm_adgroup_info: + object_id: "{{ group_create_changed_ShouldPass.object_id }}" + return_owners: True + tenant: "{{ tenant_id }}" + register: object_id_return_owners_ShouldPass + +- name: Return a specific group using object_id and return_owners and return_group_members + azure_rm_adgroup_info: + object_id: "{{ group_create_changed_ShouldPass.object_id }}" + return_owners: True + return_group_members: True + tenant: "{{ tenant_id }}" + register: object_id_return_owners_and_group_members_ShouldPass + +- name: Return a specific group using object_id and member_groups + azure_rm_adgroup_info: + object_id: "{{ group_create_changed_ShouldPass.object_id }}" + return_member_groups: True + tenant: "{{ tenant_id }}" + register: object_id_return_member_groups_ShouldPass + +- name: Return a specific group using object_id and check_membership + azure_rm_adgroup_info: + object_id: "{{ group_create_changed_ShouldPass.object_id }}" + check_membership: "{{ create_group_member_1_ShouldPass.object_id }}" + tenant: "{{ tenant_id }}" + register: object_id_return_check_membership_ShouldPass + +- name: Return a specific group using displayName attribute + azure_rm_adgroup_info: + attribute_name: "displayName" + attribute_value: "{{ group_create_changed_ShouldPass.display_name }}" + tenant: "{{ tenant_id }}" + register: displayName_attribute_ShouldPass + +- name: Return a specific group using mailNickname filter + azure_rm_adgroup_info: + odata_filter: "mailNickname eq '{{ group_create_changed_ShouldPass.mail_nickname }}'" + tenant: "{{ tenant_id }}" + register: mailNickname_filter_ShouldPass + +- name: Return a different group using displayName attribute + azure_rm_adgroup_info: + attribute_name: "displayName" + attribute_value: "{{ create_group_member_2_ShouldPass.display_name }}" + tenant: "{{ tenant_id }}" + register: displayName_attribute_different_ShouldPass + +- name: Assert All Returns Are Equal + assert: + that: + - object_id_ShouldPass == displayName_attribute_ShouldPass + - object_id_ShouldPass == mailNickname_filter_ShouldPass + +- name: Assert Returns Are Not Equal + assert: + that: + - object_id_ShouldPass != displayName_attribute_different_ShouldPass + +- name: Delete group Group Root on object_id + azure_rm_adgroup: + tenant: "{{ tenant_id }}" + object_id: "{{ group_create_unchanged_ShouldPass.object_id }}" + state: 'absent' + register: group_delete_group_root_ShouldPass + +- name: Try to return now deleted group Group Root using object_id + azure_rm_adgroup_info: + object_id: "{{ group_create_unchanged_ShouldPass.object_id }}" + tenant: "{{ tenant_id }}" + register: get_deleted_object_group_root_ShouldFail + failed_when: + - '"failed to get ad group info Resource" not in get_deleted_object_group_root_ShouldFail.msg' + +- name: Delete group Group Member 1 on object_id + azure_rm_adgroup: + tenant: "{{ tenant_id }}" + object_id: "{{ create_group_member_1_ShouldPass.object_id }}" + state: 'absent' + register: group_delete_group_member_1_ShouldPass + +- name: Try to return now deleted group Group Member 1 using object_id + azure_rm_adgroup_info: + object_id: "{{ create_group_member_1_ShouldPass.object_id }}" + tenant: "{{ tenant_id }}" + register: get_deleted_object_group_member_1_ShouldFail + failed_when: + - '"failed to get ad group info Resource" not in get_deleted_object_group_member_1_ShouldFail.msg' + +- name: Delete group Group Member 2 on object_id + azure_rm_adgroup: + tenant: "{{ tenant_id }}" + object_id: "{{ create_group_member_2_ShouldPass.object_id }}" + state: 'absent' + register: group_delete_group_member_2_ShouldPass + +- name: Try to return now deleted group Group Member 2 using object_id + azure_rm_adgroup_info: + object_id: "{{ create_group_member_2_ShouldPass.object_id }}" + tenant: "{{ tenant_id }}" + register: get_deleted_object_group_member_2_ShouldFail + failed_when: + - '"failed to get ad group info Resource" not in get_deleted_object_group_member_2_ShouldFail.msg'