diff --git a/changelogs/fragments/843-add_aws_secret_resource_policy_support.yml b/changelogs/fragments/843-add_aws_secret_resource_policy_support.yml new file mode 100644 index 00000000000..c970a72b8a1 --- /dev/null +++ b/changelogs/fragments/843-add_aws_secret_resource_policy_support.yml @@ -0,0 +1,2 @@ +minor_changes: +- aws_secret - Add ``resource_policy`` parameter (https://github.com/ansible-collections/community.aws/pull/843). \ No newline at end of file diff --git a/plugins/modules/aws_secret.py b/plugins/modules/aws_secret.py index dfe1013194d..050b00f5ae8 100644 --- a/plugins/modules/aws_secret.py +++ b/plugins/modules/aws_secret.py @@ -6,7 +6,6 @@ from __future__ import absolute_import, division, print_function __metaclass__ = type - DOCUMENTATION = r''' --- module: aws_secret @@ -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. @@ -73,7 +79,6 @@ ''' - EXAMPLES = r''' - name: Add string to AWS Secrets Manager community.aws.aws_secret: @@ -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' @@ -90,7 +103,6 @@ secret: "{{ super_secret_string }}" ''' - RETURN = r''' secret: description: The secret information @@ -133,6 +145,9 @@ 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 +from traceback import format_exc +import json try: from botocore.exceptions import BotoCoreError, ClientError @@ -142,7 +157,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 @@ -152,6 +167,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: @@ -185,6 +201,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) @@ -211,6 +236,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 (BotoCoreError, ClientError) as e: + 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) @@ -227,13 +261,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(msg="Failed to parse resource policy as JSON: %s" % (str(e)), exception=format_exc()) + + 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) @@ -255,6 +302,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 (BotoCoreError, ClientError) as e: + 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: @@ -334,6 +390,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), @@ -352,6 +409,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') @@ -374,6 +432,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"): @@ -385,6 +445,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: diff --git a/tests/integration/targets/aws_secret/tasks/basic.yml b/tests/integration/targets/aws_secret/tasks/basic.yml index 884fdc40d36..ffe5314c148 100644 --- a/tests/integration/targets/aws_secret/tasks/basic.yml +++ b/tests/integration/targets/aws_secret/tasks/basic.yml @@ -1,5 +1,12 @@ --- - block: + # ============================================================ + # Preparation + # ============================================================ + - name: 'Retrieve caller facts' + aws_caller_info: + register: aws_caller_info + # ============================================================ # Module parameter testing # ============================================================ @@ -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 }}" diff --git a/tests/integration/targets/aws_secret/templates/secret-policy.j2 b/tests/integration/targets/aws_secret/templates/secret-policy.j2 new file mode 100644 index 00000000000..77438091b25 --- /dev/null +++ b/tests/integration/targets/aws_secret/templates/secret-policy.j2 @@ -0,0 +1,11 @@ +{ + "Version" : "2012-10-17", + "Statement" : [ { + "Effect" : "Allow", + "Principal" : { + "AWS" : "arn:aws:iam::{{ aws_caller_info.account }}:root" + }, + "Action" : "secretsmanager:*", + "Resource" : "*" + } ] +} \ No newline at end of file