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
…ansible-collections#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 and abikouo committed Sep 18, 2023
1 parent 21e4dfe commit 827238f
Showing 1 changed file with 73 additions and 5 deletions.
78 changes: 73 additions & 5 deletions 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

0 comments on commit 827238f

Please sign in to comment.