#!/usr/bin/python # -*- coding: utf-8 -*- # Copyright: Contributors to the Ansible project # GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) DOCUMENTATION = r""" --- module: iam_group version_added: 1.0.0 version_added_collection: community.aws short_description: Manage AWS IAM groups description: - Manage AWS IAM groups. author: - Nick Aslanidis (@naslanidis) - Maksym Postument (@infectsoldier) options: name: description: - The name of the group. - >- Note: Group names are unique within an account. Paths (I(path)) do B(not) affect the uniqueness requirements of I(name). For example it is not permitted to have both C(/Path1/MyGroup) and C(/Path2/MyGroup) in the same account. - The alias C(group_name) was added in release 7.2.0. required: true aliases: ['group_name'] type: str path: description: - The group path. - For more information about IAM paths, see the AWS IAM identifiers documentation U(https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_identifiers.html). aliases: ['prefix', 'path_prefix'] version_added: 7.1.0 type: str managed_policies: description: - A list of managed policy ARNs or friendly names to attach to the role. - If known, it is recommended to use ARNs rather than friendly names to avoid additional lookups. - To embed an inline policy, use M(amazon.aws.iam_policy). required: false type: list elements: str default: [] aliases: ['managed_policy'] users: description: - A list of existing users to add as members of the group. required: false type: list elements: str default: [] state: description: - Create or remove the IAM group. required: true choices: [ 'present', 'absent' ] type: str purge_policies: description: - When I(purge_policies=true) any managed policies not listed in I(managed_policies) will be detatched. required: false default: false type: bool aliases: ['purge_policy', 'purge_managed_policies'] purge_users: description: - When I(purge_users=true) users which are not included in I(users) will be detached. required: false default: false type: bool extends_documentation_fragment: - amazon.aws.common.modules - amazon.aws.region.modules - amazon.aws.boto3 """ EXAMPLES = r""" # Note: These examples do not set authentication details, see the AWS Guide for details. - name: Create a group amazon.aws.iam_group: name: testgroup1 state: present - name: Create a group and attach a managed policy using its ARN amazon.aws.iam_group: name: testgroup1 managed_policies: - arn:aws:iam::aws:policy/AmazonSNSFullAccess state: present - name: Create a group with users as members and attach a managed policy using its ARN amazon.aws.iam_group: name: testgroup1 managed_policies: - arn:aws:iam::aws:policy/AmazonSNSFullAccess users: - test_user1 - test_user2 state: present - name: Remove all managed policies from an existing group with an empty list amazon.aws.iam_group: name: testgroup1 state: present purge_policies: true - name: Remove all group members from an existing group amazon.aws.iam_group: name: testgroup1 managed_policies: - arn:aws:iam::aws:policy/AmazonSNSFullAccess purge_users: true state: present - name: Delete the group amazon.aws.iam_group: name: testgroup1 state: absent """ RETURN = r""" iam_group: description: dictionary containing all the group information including group membership returned: success type: complex contains: group: description: dictionary containing all the group information returned: success type: complex contains: arn: description: the Amazon Resource Name (ARN) specifying the group type: str sample: "arn:aws:iam::1234567890:group/testgroup1" create_date: description: the date and time, in ISO 8601 date-time format, when the group was created type: str sample: "2017-02-08T04:36:28+00:00" group_id: description: the stable and unique string identifying the group type: str sample: AGPA12345EXAMPLE54321 group_name: description: the friendly name that identifies the group type: str sample: testgroup1 path: description: the path to the group type: str sample: / users: description: list containing all the group members returned: success type: complex contains: arn: description: the Amazon Resource Name (ARN) specifying the user type: str sample: "arn:aws:iam::1234567890:user/test_user1" create_date: description: the date and time, in ISO 8601 date-time format, when the user was created type: str sample: "2017-02-08T04:36:28+00:00" user_id: description: the stable and unique string identifying the user type: str sample: AIDA12345EXAMPLE54321 user_name: description: the friendly name that identifies the user type: str sample: testgroup1 path: description: the path to the user type: str sample: / attached_policies: version_added: 7.1.0 description: - list containing basic information about managed policies attached to the group. returned: success type: complex contains: policy_arn: description: the Amazon Resource Name (ARN) specifying the managed policy. type: str sample: "arn:aws:iam::123456789012:policy/test_policy" policy_name: description: the friendly name that identifies the policy. type: str sample: test_policy """ from ansible_collections.amazon.aws.plugins.module_utils.iam import AnsibleIAMError from ansible_collections.amazon.aws.plugins.module_utils.iam import IAMErrorHandler from ansible_collections.amazon.aws.plugins.module_utils.iam import convert_managed_policy_names_to_arns from ansible_collections.amazon.aws.plugins.module_utils.iam import get_iam_group from ansible_collections.amazon.aws.plugins.module_utils.iam import normalize_iam_group from ansible_collections.amazon.aws.plugins.module_utils.iam import validate_iam_identifiers from ansible_collections.amazon.aws.plugins.module_utils.modules import AnsibleAWSModule from ansible_collections.amazon.aws.plugins.module_utils.retries import AWSRetry @IAMErrorHandler.common_error_handler("update group path") def ensure_path(connection, module, group_info, path): if path is None: return False if group_info["Group"].get("Path") == path: return False if module.check_mode: return True connection.update_group( aws_retry=True, GroupName=group_info["Group"]["GroupName"], NewPath=path, ) return True def detach_policies(connection, module, group_name, policies): for policy_arn in policies: IAMErrorHandler.deletion_error_handler(f"detach policy {policy_arn} from group")( connection.detach_group_policy )(aws_retry=True, GroupName=group_name, PolicyArn=policy_arn) def attach_policies(connection, module, group_name, policies): for policy_arn in policies: IAMErrorHandler.common_error_handler(f"attach policy {policy_arn} to group")(connection.attach_group_policy)( aws_retry=True, GroupName=group_name, PolicyArn=policy_arn ) def ensure_managed_policies(connection, module, group_info, managed_policies, purge_policies): if managed_policies is None: return False if managed_policies: managed_policies = convert_managed_policy_names_to_arns(connection, managed_policies) group_name = group_info["Group"]["GroupName"] current_attached_policies_desc = get_attached_policy_list(connection, module, group_name) current_attached_policies = [policy["PolicyArn"] for policy in current_attached_policies_desc] policies_to_add = list(set(managed_policies) - set(current_attached_policies)) policies_to_remove = [] if purge_policies: policies_to_remove = list(set(current_attached_policies) - set(managed_policies)) if not policies_to_add and not policies_to_remove: return False if module.check_mode: return True detach_policies(connection, module, group_name, policies_to_remove) attach_policies(connection, module, group_name, policies_to_add) return True def add_group_members(connection, module, group_name, members): for user in members: IAMErrorHandler.common_error_handler(f"add user {user} to group")(connection.add_user_to_group)( aws_retry=True, GroupName=group_name, UserName=user ) def remove_group_members(connection, module, group_name, members): for user in members: IAMErrorHandler.deletion_error_handler(f"remove user {user} from group")(connection.remove_user_from_group)( aws_retry=True, GroupName=group_name, UserName=user ) def ensure_group_members(connection, module, group_info, users, purge_users): if users is None: return False group_name = group_info["Group"]["GroupName"] current_group_members = [member["UserName"] for member in group_info["Users"]] members_to_add = list(set(users) - set(current_group_members)) members_to_remove = [] if purge_users: members_to_remove = list(set(current_group_members) - set(users)) if not members_to_add and not members_to_remove: return False if module.check_mode: return True add_group_members(connection, module, group_name, members_to_add) remove_group_members(connection, module, group_name, members_to_remove) return True @IAMErrorHandler.common_error_handler("create group") def get_or_create_group(connection, module, group_name, path): group = get_iam_group(connection, group_name) if group: return False, group params = {"GroupName": group_name} if path is not None: params["Path"] = path # Check mode means we would create the group if module.check_mode: module.exit_json(changed=True, create_params=params) group = connection.create_group(aws_retry=True, **params) if "Users" not in group: group["Users"] = [] return True, group def create_or_update_group(connection, module): changed, group_info = get_or_create_group(connection, module, module.params["name"], module.params["path"]) # Update the path if necessary changed |= ensure_path( connection, module, group_info, module.params["path"], ) # Manage managed policies changed |= ensure_managed_policies( connection, module, group_info, module.params["managed_policies"], module.params["purge_policies"], ) # Manage group memberships changed |= ensure_group_members( connection, module, group_info, module.params["users"], module.params["purge_users"], ) if module.check_mode: module.exit_json(changed=changed) # Get the group again group_info = get_iam_group(connection, module.params["name"]) policies = get_attached_policy_list(connection, module, module.params["name"]) group_info["AttachedPolicies"] = policies module.exit_json(changed=changed, iam_group=normalize_iam_group(group_info)) @IAMErrorHandler.deletion_error_handler("delete group") def destroy_group(connection, module): group_name = module.params.get("name") group = get_iam_group(connection, group_name) if not group: module.exit_json(changed=False) # Check mode means we would remove this group if module.check_mode: module.exit_json(changed=True) # Remove any attached policies otherwise deletion fails current_policies_desc = get_attached_policy_list(connection, module, group_name) current_policies = [policy["PolicyArn"] for policy in current_policies_desc] detach_policies(connection, module, group_name, current_policies) # Remove any users in the group otherwise deletion fails current_group_members = [user["UserName"] for user in group["Users"]] remove_group_members(connection, module, group_name, current_group_members) connection.delete_group(aws_retry=True, GroupName=group_name) module.exit_json(changed=True) @IAMErrorHandler.list_error_handler("list policies attached to group") @AWSRetry.jittered_backoff() def get_attached_policy_list(connection, module, name): paginator = connection.get_paginator("list_attached_group_policies") return paginator.paginate(GroupName=name).build_full_result()["AttachedPolicies"] def main(): argument_spec = dict( name=dict(aliases=["group_name"], required=True), path=dict(aliases=["prefix", "path_prefix"]), managed_policies=dict(default=[], type="list", aliases=["managed_policy"], elements="str"), users=dict(default=[], type="list", elements="str"), state=dict(choices=["present", "absent"], required=True), purge_users=dict(default=False, type="bool"), purge_policies=dict(default=False, type="bool", aliases=["purge_policy", "purge_managed_policies"]), ) module = AnsibleAWSModule( argument_spec=argument_spec, supports_check_mode=True, ) identifier_problem = validate_iam_identifiers( "group", name=module.params.get("name"), path=module.params.get("path") ) if identifier_problem: module.fail_json(msg=identifier_problem) connection = module.client("iam", retry_decorator=AWSRetry.jittered_backoff()) state = module.params.get("state") try: if state == "present": create_or_update_group(connection, module) else: destroy_group(connection, module) except AnsibleIAMError as e: module.fail_json_aws_error(e) if __name__ == "__main__": main()