From 677eb7e5c1dad4f7d9baefb525864d997b866863 Mon Sep 17 00:00:00 2001 From: Yuriy Krysko Date: Tue, 21 Dec 2021 14:28:00 -0500 Subject: [PATCH 1/9] Added ability to manage resource policy for AWS Secrets Manager secrets --- plugins/modules/aws_secret.py | 77 ++++++++++++++++++++++++++++++++--- 1 file changed, 72 insertions(+), 5 deletions(-) diff --git a/plugins/modules/aws_secret.py b/plugins/modules/aws_secret.py index dfe1013194d..3aae0b230f2 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: 2.2.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,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 @@ -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 @@ -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: @@ -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) @@ -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: + 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 +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") + + 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 +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: + 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 +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), @@ -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') @@ -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"): @@ -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: From 73169e76339f0d0846bccecde8e6097ab92d804b Mon Sep 17 00:00:00 2001 From: Yuri Krysko Date: Tue, 21 Dec 2021 19:54:10 -0500 Subject: [PATCH 2/9] Added basic secret resource policy tests --- .../targets/aws_secret/tasks/basic.yml | 36 +++++++++++++++++++ .../aws_secret/templates/secret-policy.j2 | 11 ++++++ 2 files changed, 47 insertions(+) create mode 100644 tests/integration/targets/aws_secret/templates/secret-policy.j2 diff --git a/tests/integration/targets/aws_secret/tasks/basic.yml b/tests/integration/targets/aws_secret/tasks/basic.yml index 884fdc40d36..0ae875c02fd 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,35 @@ 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 }}" + 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 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..d0fbaca17bb --- /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:GetSecretValue", + "Resource" : "*" + } ] +} \ No newline at end of file From 6396a4f3eab5ad2f191eb5459aeedc8ecdab4e74 Mon Sep 17 00:00:00 2001 From: Yuri Krysko Date: Tue, 21 Dec 2021 20:01:49 -0500 Subject: [PATCH 3/9] corrected test parameter name --- tests/integration/targets/aws_secret/tasks/basic.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/integration/targets/aws_secret/tasks/basic.yml b/tests/integration/targets/aws_secret/tasks/basic.yml index 0ae875c02fd..65e464cd467 100644 --- a/tests/integration/targets/aws_secret/tasks/basic.yml +++ b/tests/integration/targets/aws_secret/tasks/basic.yml @@ -115,7 +115,7 @@ state: present secret_type: 'string' secret: "{{ super_secret_string }}" - policy: "{{ lookup('template', 'secret-policy.j2', convert_data=False) | string }}" + resource_policy: "{{ lookup('template', 'secret-policy.j2', convert_data=False) | string }}" register: result - name: assert correct keys are returned From 855e54717f8b84a4f4199c1596e8ab00c1ffc381 Mon Sep 17 00:00:00 2001 From: Yuri Krysko Date: Wed, 22 Dec 2021 08:54:35 -0500 Subject: [PATCH 4/9] Update basic.yml Added more tests --- .../integration/targets/aws_secret/tasks/basic.yml | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/tests/integration/targets/aws_secret/tasks/basic.yml b/tests/integration/targets/aws_secret/tasks/basic.yml index 65e464cd467..ffe5314c148 100644 --- a/tests/integration/targets/aws_secret/tasks/basic.yml +++ b/tests/integration/targets/aws_secret/tasks/basic.yml @@ -137,6 +137,20 @@ 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 }}" From 9c0bedf14572aab1674c03a2d2190caad54eb995 Mon Sep 17 00:00:00 2001 From: Yuri Krysko Date: Wed, 22 Dec 2021 09:26:50 -0500 Subject: [PATCH 5/9] Update secret-policy.j2 Relaxed secret resource policy permissions for tests to work from the same account --- tests/integration/targets/aws_secret/templates/secret-policy.j2 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/integration/targets/aws_secret/templates/secret-policy.j2 b/tests/integration/targets/aws_secret/templates/secret-policy.j2 index d0fbaca17bb..77438091b25 100644 --- a/tests/integration/targets/aws_secret/templates/secret-policy.j2 +++ b/tests/integration/targets/aws_secret/templates/secret-policy.j2 @@ -5,7 +5,7 @@ "Principal" : { "AWS" : "arn:aws:iam::{{ aws_caller_info.account }}:root" }, - "Action" : "secretsmanager:GetSecretValue", + "Action" : "secretsmanager:*", "Resource" : "*" } ] } \ No newline at end of file From a38cc24511c3cc6445b34362b2f8242a3df3055b Mon Sep 17 00:00:00 2001 From: Yuri Krysko Date: Fri, 14 Jan 2022 07:03:06 -0500 Subject: [PATCH 6/9] Update aws_secret.py Removed trailing whitespace --- plugins/modules/aws_secret.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/modules/aws_secret.py b/plugins/modules/aws_secret.py index 3aae0b230f2..6e61f12f9ff 100644 --- a/plugins/modules/aws_secret.py +++ b/plugins/modules/aws_secret.py @@ -55,7 +55,7 @@ type: str resource_policy: description: - - Specifies JSON-formatted resource policy to attach to the secret. Useful when granting cross-account access + - Specifies JSON-formatted resource policy to attach to the secret. Useful when granting cross-account access to secrets. required: false type: json From 8e13c8d42c417438489ad9740f6284a8bb2cb389 Mon Sep 17 00:00:00 2001 From: Yuri Krysko Date: Sat, 15 Jan 2022 10:08:41 -0500 Subject: [PATCH 7/9] Create 843-add_aws_secret_resource_policy_support.yml Added changelog fragment --- .../fragments/843-add_aws_secret_resource_policy_support.yml | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 changelogs/fragments/843-add_aws_secret_resource_policy_support.yml 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 From 650e4b3423590564814b6f6831b830fbe8371256 Mon Sep 17 00:00:00 2001 From: Yuri Krysko Date: Thu, 20 Jan 2022 08:31:05 -0500 Subject: [PATCH 8/9] Update aws_secret.py version_added bump --- plugins/modules/aws_secret.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/modules/aws_secret.py b/plugins/modules/aws_secret.py index 6e61f12f9ff..51a78ec7028 100644 --- a/plugins/modules/aws_secret.py +++ b/plugins/modules/aws_secret.py @@ -59,7 +59,7 @@ to secrets. required: false type: json - version_added: 2.2.0 + version_added: 3.1.0 tags: description: - Specifies a list of user-defined tags that are attached to the secret. From 9e16c707aa3ce004483091ccf815b7dbb1550dd7 Mon Sep 17 00:00:00 2001 From: Yuri Krysko Date: Mon, 24 Jan 2022 20:58:30 -0500 Subject: [PATCH 9/9] Update aws_secret.py Added fixes per the review --- plugins/modules/aws_secret.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/plugins/modules/aws_secret.py b/plugins/modules/aws_secret.py index 51a78ec7028..050b00f5ae8 100644 --- a/plugins/modules/aws_secret.py +++ b/plugins/modules/aws_secret.py @@ -146,6 +146,7 @@ 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: @@ -240,7 +241,7 @@ def get_resource_policy(self, name): resource_policy = self.client.get_resource_policy(SecretId=name) except self.client.exceptions.ResourceNotFoundException: resource_policy = None - except Exception as e: + except (BotoCoreError, ClientError) as e: self.module.fail_json_aws(e, msg="Failed to get secret resource policy") return resource_policy @@ -272,7 +273,7 @@ def put_resource_policy(self, secret): 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") + 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) @@ -306,7 +307,7 @@ def delete_resource_policy(self, name): self.module.exit_json(changed=True) try: response = self.client.delete_resource_policy(SecretId=name) - except Exception as e: + except (BotoCoreError, ClientError) as e: self.module.fail_json_aws(e, msg="Failed to delete secret resource policy") return response