Skip to content

Commit

Permalink
aws_secrets.py: add on_missing and on_denied option (#122)
Browse files Browse the repository at this point in the history
* aws_secrets.py: add on_missing and on_denied option
* - add changelog fragment
* - fix syntax error in example section
  • Loading branch information
rene1977 authored Nov 20, 2020
1 parent aa5fc97 commit 50c4d5f
Show file tree
Hide file tree
Showing 3 changed files with 88 additions and 7 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
minor_changes:
- aws_secret - add "on_missing" and "on_denied" option (https://github.com/ansible-collections/amazon.aws/pull/122).
46 changes: 45 additions & 1 deletion plugins/lookup/aws_secret.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,24 @@
- This is useful for overcoming the 4096 character limit imposed by AWS.
type: boolean
default: false
on_missing:
description:
- Action to take if the secret is missing.
- C(error) will raise a fatal error when the secret is missing.
- C(skip) will silently ignore the missing secret.
- C(warn) will skip over the missing secret but issue a warning.
default: error
type: string
choices: ['error', 'skip', 'warn']
on_denied:
description:
- Action to take if access to the secret is denied.
- C(error) will raise a fatal error when access to the secret is denied.
- C(skip) will silently ignore the denied secret.
- C(warn) will skip over the denied secret but issue a warning.
default: error
type: string
choices: ['error', 'skip', 'warn']
'''

EXAMPLES = r"""
Expand All @@ -51,6 +69,12 @@
password: "{{ lookup('aws_secret', 'DbSecret') }}"
tags:
Environment: staging
- name: skip if secret does not exist
debug: msg="{{ lookup('aws_secret', 'secret-not-exist', on_missing='skip')}}"
- name: warn if access to the secret is denied
debug: msg="{{ lookup('aws_secret', 'secret-denied', on_denied='warn')}}"
"""

RETURN = r"""
Expand All @@ -60,6 +84,7 @@
"""

from ansible.errors import AnsibleError
from ansible.module_utils.six import string_types

try:
import boto3
Expand All @@ -70,6 +95,7 @@
from ansible.plugins import AnsiblePlugin
from ansible.plugins.lookup import LookupBase
from ansible.module_utils._text import to_native
from ansible_collections.amazon.aws.plugins.module_utils.core import is_boto3_error_code


def _boto3_conn(region, credentials):
Expand Down Expand Up @@ -108,6 +134,14 @@ def _get_credentials(self):

def run(self, terms, variables, **kwargs):

missing = kwargs.get('on_missing', 'error').lower()
if not isinstance(missing, string_types) or missing not in ['error', 'warn', 'skip']:
raise AnsibleError('"on_missing" must be a string and one of "error", "warn" or "skip", not %s' % missing)

denied = kwargs.get('on_denied', 'error').lower()
if not isinstance(denied, string_types) or denied not in ['error', 'warn', 'skip']:
raise AnsibleError('"on_denied" must be a string and one of "error", "warn" or "skip", not %s' % denied)

self.set_options(var_options=variables, direct=kwargs)
boto_credentials = self._get_credentials()

Expand All @@ -129,7 +163,17 @@ def run(self, terms, variables, **kwargs):
secrets.append(response['SecretBinary'])
if 'SecretString' in response:
secrets.append(response['SecretString'])
except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e:
except is_boto3_error_code('ResourceNotFoundException'):
if missing == 'error':
raise AnsibleError("Failed to find secret %s (ResourceNotFound)" % term)
elif missing == 'warn':
self._display.warning('Skipping, did not find secret %s' % term)
except is_boto3_error_code('AccessDeniedException'): # pylint: disable=duplicate-except
if denied == 'error':
raise AnsibleError("Failed to access secret %s (AccessDenied)" % term)
elif denied == 'warn':
self._display.warning('Skipping, access denied for secret %s' % term)
except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: # pylint: disable=duplicate-except
raise AnsibleError("Failed to retrieve secret: %s" % to_native(e))

if kwargs.get('join'):
Expand Down
47 changes: 41 additions & 6 deletions tests/unit/plugins/lookup/test_aws_secret.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,10 @@

import pytest
import datetime
import sys
from copy import copy

from ansible.errors import AnsibleError

from ansible.plugins.loader import lookup_loader

try:
Expand Down Expand Up @@ -77,14 +78,48 @@ def test_lookup_variable(mocker, dummy_credentials):
aws_secret_access_key="notasecret", aws_session_token=None)


error_response = {'Error': {'Code': 'ResourceNotFoundException', 'Message': 'Fake Testing Error'}}
error_response_missing = {'Error': {'Code': 'ResourceNotFoundException', 'Message': 'Fake Not Found Error'}}
error_response_denied = {'Error': {'Code': 'AccessDeniedException', 'Message': 'Fake Denied Error'}}
operation_name = 'FakeOperation'


def test_warn_denied_variable(mocker, dummy_credentials):
def test_on_missing_option(mocker, dummy_credentials):
boto3_double = mocker.MagicMock()
boto3_double.Session.return_value.client.return_value.get_secret_value.side_effect = ClientError(error_response_missing, operation_name)

with pytest.raises(AnsibleError, match="ResourceNotFound"):
mocker.patch.object(boto3, 'session', boto3_double)
lookup_loader.get('amazon.aws.aws_secret').run(["missing_secret"], None, **dummy_credentials)

mocker.patch.object(boto3, 'session', boto3_double)
args = copy(dummy_credentials)
args["on_missing"] = 'skip'
retval = lookup_loader.get('amazon.aws.aws_secret').run(["missing_secret"], None, **args)
assert(retval == [])

mocker.patch.object(boto3, 'session', boto3_double)
args = copy(dummy_credentials)
args["on_missing"] = 'warn'
retval = lookup_loader.get('amazon.aws.aws_secret').run(["missing_secret"], None, **args)
assert(retval == [])


def test_on_denied_option(mocker, dummy_credentials):
boto3_double = mocker.MagicMock()
boto3_double.Session.return_value.client.return_value.get_secret_value.side_effect = ClientError(error_response, operation_name)
boto3_double.Session.return_value.client.return_value.get_secret_value.side_effect = ClientError(error_response_denied, operation_name)

with pytest.raises(AnsibleError):
with pytest.raises(AnsibleError, match="AccessDenied"):
mocker.patch.object(boto3, 'session', boto3_double)
lookup_loader.get('amazon.aws.aws_secret').run(["denied_variable"], None, **dummy_credentials)
lookup_loader.get('amazon.aws.aws_secret').run(["denied_secret"], None, **dummy_credentials)

mocker.patch.object(boto3, 'session', boto3_double)
args = copy(dummy_credentials)
args["on_denied"] = 'skip'
retval = lookup_loader.get('amazon.aws.aws_secret').run(["denied_secret"], None, **args)
assert(retval == [])

mocker.patch.object(boto3, 'session', boto3_double)
args = copy(dummy_credentials)
args["on_denied"] = 'warn'
retval = lookup_loader.get('amazon.aws.aws_secret').run(["denied_secret"], None, **args)
assert(retval == [])

0 comments on commit 50c4d5f

Please sign in to comment.