Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

aws_secrets.py: add on_missing and on_denied option #122

Merged
merged 16 commits into from
Nov 20, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
rene1977 marked this conversation as resolved.
Show resolved Hide resolved


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'
rene1977 marked this conversation as resolved.
Show resolved Hide resolved
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 == [])