Skip to content

Commit 65f1ba3

Browse files
authored
Promote iam_managed_policy module (#1762)
Promote iam_managed_policy module SUMMARY Migrate iam_managed_policy module from community.aws ISSUE TYPE Bugfix Pull Request Docs Pull Request Feature Pull Request New Module Pull Request COMPONENT NAME ADDITIONAL INFORMATION Reviewed-by: Helen Bailey <[email protected]> Reviewed-by: Bikouo Aubin
1 parent 269f08b commit 65f1ba3

File tree

9 files changed

+556
-2
lines changed

9 files changed

+556
-2
lines changed
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
major_changes:
2+
- iam_managed_policy - The module has been migrated from the ``community.aws`` collection.
3+
Playbooks using the Fully Qualified Collection Name for this module should be updated
4+
to use ``amazon.aws.iam_managed_policy`` (https://github.com/ansible-collections/amazon.aws/pull/1762).

meta/runtime.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,7 @@ action_groups:
7171
- iam_group
7272
- iam_instance_profile
7373
- iam_instance_profile_info
74+
- iam_managed_policy
7475
- iam_policy
7576
- iam_policy_info
7677
- iam_role
Lines changed: 380 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,380 @@
1+
#!/usr/bin/python
2+
# -*- coding: utf-8 -*-
3+
4+
# Copyright: Ansible Project
5+
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
6+
7+
DOCUMENTATION = r"""
8+
---
9+
module: iam_managed_policy
10+
version_added: 1.0.0
11+
version_added_collection: community.aws
12+
short_description: Manage User Managed IAM policies
13+
description:
14+
- Allows creating and removing managed IAM policies
15+
options:
16+
policy_name:
17+
description:
18+
- The name of the managed policy.
19+
required: True
20+
type: str
21+
policy_description:
22+
description:
23+
- A helpful description of this policy, this value is immutable and only set when creating a new policy.
24+
default: ''
25+
type: str
26+
policy:
27+
description:
28+
- A properly json formatted policy
29+
type: json
30+
make_default:
31+
description:
32+
- Make this revision the default revision.
33+
default: True
34+
type: bool
35+
only_version:
36+
description:
37+
- Remove all other non default revisions, if this is used with C(make_default) it will result in all other versions of this policy being deleted.
38+
type: bool
39+
default: false
40+
state:
41+
description:
42+
- Should this managed policy be present or absent. Set to absent to detach all entities from this policy and remove it if found.
43+
default: present
44+
choices: [ "present", "absent" ]
45+
type: str
46+
47+
author:
48+
- "Dan Kozlowski (@dkhenry)"
49+
extends_documentation_fragment:
50+
- amazon.aws.common.modules
51+
- amazon.aws.region.modules
52+
- amazon.aws.boto3
53+
"""
54+
55+
EXAMPLES = r"""
56+
# Create a policy
57+
- name: Create IAM Managed Policy
58+
amazon.aws.iam_managed_policy:
59+
policy_name: "ManagedPolicy"
60+
policy_description: "A Helpful managed policy"
61+
policy: "{{ lookup('template', 'managed_policy.json.j2') }}"
62+
state: present
63+
64+
# Update a policy with a new default version
65+
- name: Update an IAM Managed Policy with new default version
66+
amazon.aws.iam_managed_policy:
67+
policy_name: "ManagedPolicy"
68+
policy: "{{ lookup('file', 'managed_policy_update.json') }}"
69+
state: present
70+
71+
# Update a policy with a new non default version
72+
- name: Update an IAM Managed Policy with a non default version
73+
amazon.aws.iam_managed_policy:
74+
policy_name: "ManagedPolicy"
75+
policy:
76+
Version: "2012-10-17"
77+
Statement:
78+
- Effect: "Allow"
79+
Action: "logs:CreateLogGroup"
80+
Resource: "*"
81+
make_default: false
82+
state: present
83+
84+
# Update a policy and make it the only version and the default version
85+
- name: Update an IAM Managed Policy with default version as the only version
86+
amazon.aws.iam_managed_policy:
87+
policy_name: "ManagedPolicy"
88+
policy: |
89+
{
90+
"Version": "2012-10-17",
91+
"Statement":[{
92+
"Effect": "Allow",
93+
"Action": "logs:PutRetentionPolicy",
94+
"Resource": "*"
95+
}]
96+
}
97+
only_version: true
98+
state: present
99+
100+
# Remove a policy
101+
- name: Remove an existing IAM Managed Policy
102+
amazon.aws.iam_managed_policy:
103+
policy_name: "ManagedPolicy"
104+
state: absent
105+
"""
106+
107+
RETURN = r"""
108+
policy:
109+
description: Returns the policy json structure, when state == absent this will return the value of the removed policy.
110+
returned: success
111+
type: complex
112+
contains: {}
113+
sample: '{
114+
"arn": "arn:aws:iam::aws:policy/AdministratorAccess "
115+
"attachment_count": 0,
116+
"create_date": "2017-03-01T15:42:55.981000+00:00",
117+
"default_version_id": "v1",
118+
"is_attachable": true,
119+
"path": "/",
120+
"policy_id": "ANPA1245EXAMPLE54321",
121+
"policy_name": "AdministratorAccess",
122+
"update_date": "2017-03-01T15:42:55.981000+00:00"
123+
}'
124+
"""
125+
126+
import json
127+
128+
try:
129+
import botocore
130+
except ImportError:
131+
pass # Handled by AnsibleAWSModule
132+
133+
from ansible.module_utils._text import to_native
134+
from ansible.module_utils.common.dict_transformations import camel_dict_to_snake_dict
135+
136+
from ansible_collections.amazon.aws.plugins.module_utils.botocore import is_boto3_error_code
137+
from ansible_collections.amazon.aws.plugins.module_utils.policy import compare_policies
138+
from ansible_collections.amazon.aws.plugins.module_utils.retries import AWSRetry
139+
140+
from ansible_collections.amazon.aws.plugins.module_utils.modules import AnsibleAWSModule
141+
142+
143+
@AWSRetry.jittered_backoff(retries=5, delay=5, backoff=2.0)
144+
def list_policies_with_backoff():
145+
paginator = client.get_paginator("list_policies")
146+
return paginator.paginate(Scope="Local").build_full_result()
147+
148+
149+
def get_policy_by_name(name):
150+
try:
151+
response = list_policies_with_backoff()
152+
except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e:
153+
module.fail_json_aws(e, msg="Couldn't list policies")
154+
for policy in response["Policies"]:
155+
if policy["PolicyName"] == name:
156+
return policy
157+
return None
158+
159+
160+
def delete_oldest_non_default_version(policy):
161+
try:
162+
versions = [
163+
v for v in client.list_policy_versions(PolicyArn=policy["Arn"])["Versions"] if not v["IsDefaultVersion"]
164+
]
165+
except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e:
166+
module.fail_json_aws(e, msg="Couldn't list policy versions")
167+
versions.sort(key=lambda v: v["CreateDate"], reverse=True)
168+
for v in versions[-1:]:
169+
try:
170+
client.delete_policy_version(PolicyArn=policy["Arn"], VersionId=v["VersionId"])
171+
except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e:
172+
module.fail_json_aws(e, msg="Couldn't delete policy version")
173+
174+
175+
# This needs to return policy_version, changed
176+
def get_or_create_policy_version(policy, policy_document):
177+
try:
178+
versions = client.list_policy_versions(PolicyArn=policy["Arn"])["Versions"]
179+
except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e:
180+
module.fail_json_aws(e, msg="Couldn't list policy versions")
181+
182+
for v in versions:
183+
try:
184+
document = client.get_policy_version(PolicyArn=policy["Arn"], VersionId=v["VersionId"])["PolicyVersion"][
185+
"Document"
186+
]
187+
except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e:
188+
module.fail_json_aws(e, msg=f"Couldn't get policy version {v['VersionId']}")
189+
190+
if module.check_mode and compare_policies(document, json.loads(to_native(policy_document))):
191+
return v, True
192+
193+
# If the current policy matches the existing one
194+
if not compare_policies(document, json.loads(to_native(policy_document))):
195+
return v, False
196+
197+
# No existing version so create one
198+
# There is a service limit (typically 5) of policy versions.
199+
#
200+
# Rather than assume that it is 5, we'll try to create the policy
201+
# and if that doesn't work, delete the oldest non default policy version
202+
# and try again.
203+
try:
204+
version = client.create_policy_version(PolicyArn=policy["Arn"], PolicyDocument=policy_document)["PolicyVersion"]
205+
return version, True
206+
except is_boto3_error_code("LimitExceeded"):
207+
delete_oldest_non_default_version(policy)
208+
try:
209+
version = client.create_policy_version(PolicyArn=policy["Arn"], PolicyDocument=policy_document)[
210+
"PolicyVersion"
211+
]
212+
return version, True
213+
except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as second_e:
214+
module.fail_json_aws(second_e, msg="Couldn't create policy version")
215+
except (
216+
botocore.exceptions.ClientError,
217+
botocore.exceptions.BotoCoreError,
218+
) as e: # pylint: disable=duplicate-except
219+
module.fail_json_aws(e, msg="Couldn't create policy version")
220+
221+
222+
def set_if_default(policy, policy_version, is_default):
223+
if is_default and not policy_version["IsDefaultVersion"]:
224+
try:
225+
client.set_default_policy_version(PolicyArn=policy["Arn"], VersionId=policy_version["VersionId"])
226+
except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e:
227+
module.fail_json_aws(e, msg="Couldn't set default policy version")
228+
return True
229+
return False
230+
231+
232+
def set_if_only(policy, policy_version, is_only):
233+
if is_only:
234+
try:
235+
versions = [
236+
v for v in client.list_policy_versions(PolicyArn=policy["Arn"])["Versions"] if not v["IsDefaultVersion"]
237+
]
238+
except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e:
239+
module.fail_json_aws(e, msg="Couldn't list policy versions")
240+
for v in versions:
241+
try:
242+
client.delete_policy_version(PolicyArn=policy["Arn"], VersionId=v["VersionId"])
243+
except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e:
244+
module.fail_json_aws(e, msg="Couldn't delete policy version")
245+
return len(versions) > 0
246+
return False
247+
248+
249+
def detach_all_entities(policy, **kwargs):
250+
try:
251+
entities = client.list_entities_for_policy(PolicyArn=policy["Arn"], **kwargs)
252+
except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e:
253+
module.fail_json_aws(e, msg=f"Couldn't detach list entities for policy {policy['PolicyName']}")
254+
255+
for g in entities["PolicyGroups"]:
256+
try:
257+
client.detach_group_policy(PolicyArn=policy["Arn"], GroupName=g["GroupName"])
258+
except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e:
259+
module.fail_json_aws(e, msg=f"Couldn't detach group policy {g['GroupName']}")
260+
for u in entities["PolicyUsers"]:
261+
try:
262+
client.detach_user_policy(PolicyArn=policy["Arn"], UserName=u["UserName"])
263+
except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e:
264+
module.fail_json_aws(e, msg=f"Couldn't detach user policy {u['UserName']}")
265+
for r in entities["PolicyRoles"]:
266+
try:
267+
client.detach_role_policy(PolicyArn=policy["Arn"], RoleName=r["RoleName"])
268+
except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e:
269+
module.fail_json_aws(e, msg=f"Couldn't detach role policy {r['RoleName']}")
270+
if entities["IsTruncated"]:
271+
detach_all_entities(policy, marker=entities["Marker"])
272+
273+
274+
def create_or_update_policy(existing_policy):
275+
name = module.params.get("policy_name")
276+
description = module.params.get("policy_description")
277+
default = module.params.get("make_default")
278+
only = module.params.get("only_version")
279+
280+
policy = None
281+
282+
if module.params.get("policy") is not None:
283+
policy = json.dumps(json.loads(module.params.get("policy")))
284+
285+
if existing_policy is None:
286+
if module.check_mode:
287+
module.exit_json(changed=True)
288+
289+
# Create policy when none already exists
290+
try:
291+
rvalue = client.create_policy(PolicyName=name, Path="/", PolicyDocument=policy, Description=description)
292+
except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e:
293+
module.fail_json_aws(e, msg=f"Couldn't create policy {name}")
294+
295+
module.exit_json(changed=True, policy=camel_dict_to_snake_dict(rvalue["Policy"]))
296+
else:
297+
policy_version, changed = get_or_create_policy_version(existing_policy, policy)
298+
changed = set_if_default(existing_policy, policy_version, default) or changed
299+
changed = set_if_only(existing_policy, policy_version, only) or changed
300+
301+
# If anything has changed we need to refresh the policy
302+
if changed:
303+
try:
304+
updated_policy = client.get_policy(PolicyArn=existing_policy["Arn"])["Policy"]
305+
except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e:
306+
module.fail_json(msg="Couldn't get policy")
307+
308+
module.exit_json(changed=changed, policy=camel_dict_to_snake_dict(updated_policy))
309+
else:
310+
module.exit_json(changed=changed, policy=camel_dict_to_snake_dict(existing_policy))
311+
312+
313+
def delete_policy(existing_policy):
314+
# Check for existing policy
315+
if existing_policy:
316+
if module.check_mode:
317+
module.exit_json(changed=True)
318+
319+
# Detach policy
320+
detach_all_entities(existing_policy)
321+
# Delete Versions
322+
try:
323+
versions = client.list_policy_versions(PolicyArn=existing_policy["Arn"])["Versions"]
324+
except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e:
325+
module.fail_json_aws(e, msg="Couldn't list policy versions")
326+
for v in versions:
327+
if not v["IsDefaultVersion"]:
328+
try:
329+
client.delete_policy_version(PolicyArn=existing_policy["Arn"], VersionId=v["VersionId"])
330+
except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e:
331+
module.fail_json_aws(e, msg=f"Couldn't delete policy version {v['VersionId']}")
332+
# Delete policy
333+
try:
334+
client.delete_policy(PolicyArn=existing_policy["Arn"])
335+
except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e:
336+
module.fail_json_aws(e, msg=f"Couldn't delete policy {existing_policy['PolicyName']}")
337+
338+
# This is the one case where we will return the old policy
339+
module.exit_json(changed=True, policy=camel_dict_to_snake_dict(existing_policy))
340+
else:
341+
module.exit_json(changed=False, policy=None)
342+
343+
344+
def main():
345+
global module
346+
global client
347+
348+
argument_spec = dict(
349+
policy_name=dict(required=True),
350+
policy_description=dict(default=""),
351+
policy=dict(type="json"),
352+
make_default=dict(type="bool", default=True),
353+
only_version=dict(type="bool", default=False),
354+
state=dict(default="present", choices=["present", "absent"]),
355+
)
356+
357+
module = AnsibleAWSModule(
358+
argument_spec=argument_spec,
359+
required_if=[["state", "present", ["policy"]]],
360+
supports_check_mode=True,
361+
)
362+
363+
name = module.params.get("policy_name")
364+
state = module.params.get("state")
365+
366+
try:
367+
client = module.client("iam", retry_decorator=AWSRetry.jittered_backoff())
368+
except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e:
369+
module.fail_json_aws(e, msg="Failed to connect to AWS")
370+
371+
existing_policy = get_policy_by_name(name)
372+
373+
if state == "present":
374+
create_or_update_policy(existing_policy)
375+
else:
376+
delete_policy(existing_policy)
377+
378+
379+
if __name__ == "__main__":
380+
main()

0 commit comments

Comments
 (0)