Skip to content

Commit

Permalink
Add ability to manage resource policy for AWS Secrets Manager secrets (
Browse files Browse the repository at this point in the history
…#843)

Add ability to manage resource policy for AWS Secrets Manager secrets

SUMMARY

AWS Secrets Manager secrets support attaching resource policy. The benefit is huge when necessary to access secrets from other AWS accounts. This pull request adds ability to manage (add new/remove or modify existing) secrets resource policy.

ISSUE TYPE


Feature Pull Request

COMPONENT NAME

module: aws_secret
ADDITIONAL INFORMATION

Reviewed-by: Mark Woolley <[email protected]>
Reviewed-by: Yuri Krysko <[email protected]>
Reviewed-by: Alina Buzachis <None>
Reviewed-by: Markus Bergholz <[email protected]>
  • Loading branch information
ykrysko authored Jan 25, 2022
1 parent af6a28f commit 9110162
Show file tree
Hide file tree
Showing 4 changed files with 136 additions and 5 deletions.
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).
78 changes: 73 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,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
Expand All @@ -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
Expand All @@ -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:
Expand Down Expand Up @@ -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)
Expand All @@ -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)
Expand All @@ -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)
Expand All @@ -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:
Expand Down Expand Up @@ -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),
Expand All @@ -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')
Expand All @@ -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"):
Expand All @@ -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:
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" : "*"
} ]
}

0 comments on commit 9110162

Please sign in to comment.