Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add ability to manage resource policy for AWS Secrets Manager secrets #843

Merged
merged 10 commits into from
Jan 25, 2022
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
minor_changes:
- aws_secret - Add ``resource_policy`` parameter (https://github.com/ansible-collections/community.aws/pull/843).
77 changes: 72 additions & 5 deletions plugins/modules/aws_secret.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@
from __future__ import absolute_import, division, print_function
__metaclass__ = type


DOCUMENTATION = r'''
---
module: aws_secret
Expand Down Expand Up @@ -54,6 +53,13 @@
- Specifies string or binary data that you want to encrypt and store in the new version of the secret.
default: ""
type: str
resource_policy:
description:
- Specifies JSON-formatted resource policy to attach to the secret. Useful when granting cross-account access
to secrets.
required: false
type: json
version_added: 3.1.0
tags:
description:
- Specifies a list of user-defined tags that are attached to the secret.
Expand All @@ -73,7 +79,6 @@

'''


EXAMPLES = r'''
- name: Add string to AWS Secrets Manager
community.aws.aws_secret:
Expand All @@ -82,6 +87,14 @@
secret_type: 'string'
secret: "{{ super_secret_string }}"

- name: Add a secret with resource policy attached
community.aws.aws_secret:
name: 'test_secret_string'
state: present
secret_type: 'string'
secret: "{{ super_secret_string }}"
resource_policy: "{{ lookup('template', 'templates/resource_policy.json.j2', convert_data=False) | string }}"

- name: remove string from AWS Secrets Manager
community.aws.aws_secret:
name: 'test_secret_string'
Expand All @@ -90,7 +103,6 @@
secret: "{{ super_secret_string }}"
'''


RETURN = r'''
secret:
description: The secret information
Expand Down Expand Up @@ -133,6 +145,8 @@
from ansible_collections.amazon.aws.plugins.module_utils.core import AnsibleAWSModule
from ansible_collections.amazon.aws.plugins.module_utils.ec2 import snake_dict_to_camel_dict, camel_dict_to_snake_dict
from ansible_collections.amazon.aws.plugins.module_utils.ec2 import boto3_tag_list_to_ansible_dict, compare_aws_tags, ansible_dict_to_boto3_tag_list
from ansible_collections.amazon.aws.plugins.module_utils.ec2 import compare_policies
import json

try:
from botocore.exceptions import BotoCoreError, ClientError
Expand All @@ -142,7 +156,7 @@

class Secret(object):
"""An object representation of the Secret described by the self.module args"""
def __init__(self, name, secret_type, secret, description="", kms_key_id=None,
def __init__(self, name, secret_type, secret, resource_policy=None, description="", kms_key_id=None,
tags=None, lambda_arn=None, rotation_interval=None):
self.name = name
self.description = description
Expand All @@ -152,6 +166,7 @@ def __init__(self, name, secret_type, secret, description="", kms_key_id=None,
else:
self.secret_type = "SecretString"
self.secret = secret
self.resource_policy = resource_policy
self.tags = tags or {}
self.rotation_enabled = False
if lambda_arn:
Expand Down Expand Up @@ -185,6 +200,15 @@ def update_args(self):
args[self.secret_type] = self.secret
return args

@property
def secret_resource_policy_args(self):
args = {
"SecretId": self.name
}
if self.resource_policy:
args["ResourcePolicy"] = self.resource_policy
return args

@property
def boto3_tags(self):
return ansible_dict_to_boto3_tag_list(self.Tags)
Expand All @@ -211,6 +235,15 @@ def get_secret(self, name):
self.module.fail_json_aws(e, msg="Failed to describe secret")
return secret

def get_resource_policy(self, name):
try:
resource_policy = self.client.get_resource_policy(SecretId=name)
except self.client.exceptions.ResourceNotFoundException:
resource_policy = None
except Exception as e:
ykrysko marked this conversation as resolved.
Show resolved Hide resolved
self.module.fail_json_aws(e, msg="Failed to get secret resource policy")
return resource_policy

def create_secret(self, secret):
if self.module.check_mode:
self.module.exit_json(changed=True)
Expand All @@ -227,13 +260,26 @@ def create_secret(self, secret):
def update_secret(self, secret):
if self.module.check_mode:
self.module.exit_json(changed=True)

try:
response = self.client.update_secret(**secret.update_args)
except (BotoCoreError, ClientError) as e:
self.module.fail_json_aws(e, msg="Failed to update secret")
return response

def put_resource_policy(self, secret):
if self.module.check_mode:
self.module.exit_json(changed=True)
try:
json.loads(secret.secret_resource_policy_args.get("ResourcePolicy"))
except (TypeError, ValueError) as e:
self.module.fail_json_aws(e, msg="Failed to parse resource policy as JSON")
ykrysko marked this conversation as resolved.
Show resolved Hide resolved

try:
response = self.client.put_resource_policy(**secret.secret_resource_policy_args)
except (BotoCoreError, ClientError) as e:
self.module.fail_json_aws(e, msg="Failed to update secret resource policy")
return response

def restore_secret(self, name):
if self.module.check_mode:
self.module.exit_json(changed=True)
Expand All @@ -255,6 +301,15 @@ def delete_secret(self, name, recovery_window):
self.module.fail_json_aws(e, msg="Failed to delete secret")
return response

def delete_resource_policy(self, name):
if self.module.check_mode:
self.module.exit_json(changed=True)
try:
response = self.client.delete_resource_policy(SecretId=name)
except Exception as e:
ykrysko marked this conversation as resolved.
Show resolved Hide resolved
self.module.fail_json_aws(e, msg="Failed to delete secret resource policy")
return response

def update_rotation(self, secret):
if secret.rotation_enabled:
try:
Expand Down Expand Up @@ -334,6 +389,7 @@ def main():
'kms_key_id': dict(),
'secret_type': dict(choices=['binary', 'string'], default="string"),
'secret': dict(default="", no_log=True),
'resource_policy': dict(type='json', default=None),
'tags': dict(type='dict', default={}),
'rotation_lambda': dict(),
'rotation_interval': dict(type='int', default=30),
Expand All @@ -352,6 +408,7 @@ def main():
module.params.get('secret'),
description=module.params.get('description'),
kms_key_id=module.params.get('kms_key_id'),
resource_policy=module.params.get('resource_policy'),
tags=module.params.get('tags'),
lambda_arn=module.params.get('rotation_lambda'),
rotation_interval=module.params.get('rotation_interval')
Expand All @@ -374,6 +431,8 @@ def main():
if state == 'present':
if current_secret is None:
result = secrets_mgr.create_secret(secret)
if secret.resource_policy and result.get("ARN"):
result = secrets_mgr.put_resource_policy(secret)
changed = True
else:
if current_secret.get("DeletedDate"):
Expand All @@ -385,6 +444,14 @@ def main():
if not rotation_match(secret, current_secret):
result = secrets_mgr.update_rotation(secret)
changed = True
current_resource_policy_response = secrets_mgr.get_resource_policy(secret.name)
current_resource_policy = current_resource_policy_response.get("ResourcePolicy")
if compare_policies(secret.resource_policy, current_resource_policy):
if secret.resource_policy is None and current_resource_policy:
result = secrets_mgr.delete_resource_policy(secret.name)
else:
result = secrets_mgr.put_resource_policy(secret)
changed = True
current_tags = boto3_tag_list_to_ansible_dict(current_secret.get('Tags', []))
tags_to_add, tags_to_remove = compare_aws_tags(current_tags, secret.tags)
if tags_to_add:
Expand Down
50 changes: 50 additions & 0 deletions tests/integration/targets/aws_secret/tasks/basic.yml
Original file line number Diff line number Diff line change
@@ -1,5 +1,12 @@
---
- block:
# ============================================================
# Preparation
# ============================================================
- name: 'Retrieve caller facts'
aws_caller_info:
register: aws_caller_info

# ============================================================
# Module parameter testing
# ============================================================
Expand Down Expand Up @@ -101,6 +108,49 @@
that:
- result.changed

- name: add resource policy to secret
aws_secret:
name: "{{ secret_name }}"
description: 'this is a change to this secret'
state: present
secret_type: 'string'
secret: "{{ super_secret_string }}"
resource_policy: "{{ lookup('template', 'secret-policy.j2', convert_data=False) | string }}"
register: result

- name: assert correct keys are returned
assert:
that:
- result.changed

- name: remove existing resource policy from secret
aws_secret:
name: "{{ secret_name }}"
description: 'this is a change to this secret'
state: present
secret_type: 'string'
secret: "{{ super_secret_string }}"
register: result

- name: assert correct keys are returned
assert:
that:
- result.changed

- name: remove resource policy from secret (idempotency)
aws_secret:
name: "{{ secret_name }}"
description: 'this is a change to this secret'
state: present
secret_type: 'string'
secret: "{{ super_secret_string }}"
register: result

- name: assert no change happened
assert:
that:
- not result.changed

- name: remove secret
aws_secret:
name: "{{ secret_name }}"
Expand Down
11 changes: 11 additions & 0 deletions tests/integration/targets/aws_secret/templates/secret-policy.j2
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"Version" : "2012-10-17",
"Statement" : [ {
"Effect" : "Allow",
"Principal" : {
"AWS" : "arn:aws:iam::{{ aws_caller_info.account }}:root"
},
"Action" : "secretsmanager:*",
"Resource" : "*"
} ]
}