Adding in Ansible (still a WIP).
This commit is contained in:
+33
@@ -0,0 +1,33 @@
|
||||
# (c) 2014, Toshio Kuratomi <tkuratomi@ansible.com>
|
||||
#
|
||||
# This file is part of Ansible
|
||||
#
|
||||
# Ansible is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# Ansible is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
# Make coding more python3-ish
|
||||
from __future__ import (absolute_import, division, print_function)
|
||||
__metaclass__ = type
|
||||
|
||||
#
|
||||
# Compat for python2.7
|
||||
#
|
||||
|
||||
# One unittest needs to import builtins via __import__() so we need to have
|
||||
# the string that represents it
|
||||
try:
|
||||
import __builtin__ # pylint: disable=unused-import
|
||||
except ImportError:
|
||||
BUILTINS = 'builtins'
|
||||
else:
|
||||
BUILTINS = '__builtin__'
|
||||
+28
@@ -0,0 +1,28 @@
|
||||
# Copyright (c) 2014, Toshio Kuratomi <tkuratomi@ansible.com>
|
||||
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
# Make coding more python3-ish
|
||||
from __future__ import (absolute_import, division, print_function)
|
||||
__metaclass__ = type
|
||||
|
||||
'''
|
||||
Compat module for Python3.x's unittest.mock module
|
||||
'''
|
||||
# Python 2.7
|
||||
|
||||
# Note: Could use the pypi mock library on python3.x as well as python2.x. It
|
||||
# is the same as the python3 stdlib mock library
|
||||
|
||||
try:
|
||||
# Allow wildcard import because we really do want to import all of mock's
|
||||
# symbols into this compat shim
|
||||
# pylint: disable=wildcard-import,unused-wildcard-import
|
||||
from unittest.mock import *
|
||||
except ImportError:
|
||||
# Python 2
|
||||
# pylint: disable=wildcard-import,unused-wildcard-import
|
||||
try:
|
||||
from mock import *
|
||||
except ImportError:
|
||||
print('You need the mock library installed on python2.x to run tests')
|
||||
+38
@@ -0,0 +1,38 @@
|
||||
# (c) 2014, Toshio Kuratomi <tkuratomi@ansible.com>
|
||||
#
|
||||
# This file is part of Ansible
|
||||
#
|
||||
# Ansible is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# Ansible is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
# Make coding more python3-ish
|
||||
from __future__ import (absolute_import, division, print_function)
|
||||
__metaclass__ = type
|
||||
|
||||
'''
|
||||
Compat module for Python2.7's unittest module
|
||||
'''
|
||||
|
||||
import sys
|
||||
|
||||
# Allow wildcard import because we really do want to import all of
|
||||
# unittests's symbols into this compat shim
|
||||
# pylint: disable=wildcard-import,unused-wildcard-import
|
||||
if sys.version_info < (2, 7):
|
||||
try:
|
||||
# Need unittest2 on python2.6
|
||||
from unittest2 import *
|
||||
except ImportError:
|
||||
print('You need unittest2 installed on python2.6.x to run tests')
|
||||
else:
|
||||
from unittest import *
|
||||
@@ -0,0 +1,92 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright (c) 2021 Brian Scholer (@briantist)
|
||||
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
from __future__ import (absolute_import, division, print_function)
|
||||
__metaclass__ = type
|
||||
|
||||
import sys
|
||||
import os
|
||||
import json
|
||||
import pytest
|
||||
|
||||
from .compat import mock
|
||||
|
||||
from ...plugins.module_utils._authenticator import HashiVaultAuthenticator
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def skip_python():
|
||||
if sys.version_info < (3, 6):
|
||||
pytest.skip('Skipping on Python %s. community.hashi_vault supports Python 3.6 and higher.' % sys.version)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def fixture_loader():
|
||||
def _loader(name, parse='json'):
|
||||
here = os.path.dirname(os.path.realpath(__file__))
|
||||
fixture = os.path.join(here, 'fixtures', name)
|
||||
|
||||
if parse == 'path':
|
||||
return fixture
|
||||
|
||||
with open(fixture, 'r') as f:
|
||||
if parse == 'json':
|
||||
d = json.load(f)
|
||||
elif parse == 'lines':
|
||||
d = f.readlines()
|
||||
elif parse == 'raw':
|
||||
d = f.read()
|
||||
else:
|
||||
raise ValueError("Unknown value '%s' for parse" % parse)
|
||||
|
||||
return d
|
||||
|
||||
return _loader
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def vault_client():
|
||||
return mock.MagicMock()
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def authenticator():
|
||||
authenticator = HashiVaultAuthenticator
|
||||
authenticator.validate = mock.Mock(wraps=lambda: True)
|
||||
authenticator.authenticate = mock.Mock(wraps=lambda client: 'throwaway')
|
||||
|
||||
return authenticator
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def patch_authenticator(authenticator):
|
||||
with mock.patch('ansible_collections.community.hashi_vault.plugins.module_utils._hashi_vault_module.HashiVaultAuthenticator', new=authenticator):
|
||||
yield
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def patch_get_vault_client(vault_client):
|
||||
with mock.patch(
|
||||
'ansible_collections.community.hashi_vault.plugins.module_utils._hashi_vault_common.HashiVaultHelper.get_vault_client', return_value=vault_client
|
||||
):
|
||||
yield
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def requests_unparseable_response():
|
||||
r = mock.MagicMock()
|
||||
r.json.side_effect = json.JSONDecodeError
|
||||
return r
|
||||
|
||||
|
||||
# https://github.com/hvac/hvac/issues/797
|
||||
@pytest.fixture
|
||||
def empty_response(requests_unparseable_response):
|
||||
r = requests_unparseable_response
|
||||
r.status_code = 204
|
||||
r._content = b""
|
||||
r.content = b""
|
||||
r.text = ""
|
||||
return r
|
||||
+64
@@ -0,0 +1,64 @@
|
||||
coverage >= 4.2, < 5.0.0, != 4.3.2 ; python_version <= '3.7' # features in 4.2+ required, avoid known bug in 4.3.2 on python 2.6, coverage 5.0+ incompatible
|
||||
coverage >= 4.5.4, < 5.0.0 ; python_version > '3.7' # coverage had a bug in < 4.5.4 that would cause unit tests to hang in Python 3.8, coverage 5.0+ incompatible
|
||||
cryptography < 2.2 ; python_version < '2.7' # cryptography 2.2 drops support for python 2.6
|
||||
deepdiff < 4.0.0 ; python_version < '3' # deepdiff 4.0.0 and later require python 3
|
||||
jinja2 < 2.11 ; python_version < '2.7' # jinja2 2.11 and later require python 2.7 or later
|
||||
urllib3 < 1.24 ; python_version < '2.7' # urllib3 1.24 and later require python 2.7 or later
|
||||
pywinrm >= 0.3.0 # message encryption support
|
||||
sphinx < 1.6 ; python_version < '2.7' # sphinx 1.6 and later require python 2.7 or later
|
||||
sphinx < 1.8 ; python_version >= '2.7' # sphinx 1.8 and later are currently incompatible with rstcheck 3.3
|
||||
pygments >= 2.4.0 # Pygments 2.4.0 includes bugfixes for YAML and YAML+Jinja lexers
|
||||
wheel < 0.30.0 ; python_version < '2.7' # wheel 0.30.0 and later require python 2.7 or later
|
||||
yamllint != 1.8.0, < 1.14.0 ; python_version < '2.7' # yamllint 1.8.0 and 1.14.0+ require python 2.7+
|
||||
pycrypto >= 2.6 # Need features found in 2.6 and greater
|
||||
ncclient >= 0.5.2 # Need features added in 0.5.2 and greater
|
||||
idna < 2.6, >= 2.5 # linode requires idna < 2.9, >= 2.5, requests requires idna < 2.6, but cryptography will cause the latest version to be installed instead
|
||||
paramiko < 2.4.0 ; python_version < '2.7' # paramiko 2.4.0 drops support for python 2.6
|
||||
pytest < 3.3.0 ; python_version < '2.7' # pytest 3.3.0 drops support for python 2.6
|
||||
pytest < 5.0.0 ; python_version == '2.7' # pytest 5.0.0 and later will no longer support python 2.7
|
||||
pytest-forked < 1.0.2 ; python_version < '2.7' # pytest-forked 1.0.2 and later require python 2.7 or later
|
||||
pytest-forked >= 1.0.2 ; python_version >= '2.7' # pytest-forked before 1.0.2 does not work with pytest 4.2.0+ (which requires python 2.7+)
|
||||
ntlm-auth >= 1.3.0 # message encryption support using cryptography
|
||||
requests < 2.20.0 ; python_version < '2.7' # requests 2.20.0 drops support for python 2.6
|
||||
requests-ntlm >= 1.1.0 # message encryption support
|
||||
requests-credssp >= 0.1.0 # message encryption support
|
||||
voluptuous >= 0.11.0 # Schema recursion via Self
|
||||
openshift >= 0.6.2, < 0.9.0 # merge_type support
|
||||
virtualenv < 16.0.0 ; python_version < '2.7' # virtualenv 16.0.0 and later require python 2.7 or later
|
||||
pathspec < 0.6.0 ; python_version < '2.7' # pathspec 0.6.0 and later require python 2.7 or later
|
||||
pyopenssl < 18.0.0 ; python_version < '2.7' # pyOpenSSL 18.0.0 and later require python 2.7 or later
|
||||
pyfmg == 0.6.1 # newer versions do not pass current unit tests
|
||||
pyyaml < 5.1 ; python_version < '2.7' # pyyaml 5.1 and later require python 2.7 or later
|
||||
pycparser < 2.19 ; python_version < '2.7' # pycparser 2.19 and later require python 2.7 or later
|
||||
mock >= 2.0.0 # needed for features backported from Python 3.6 unittest.mock (assert_called, assert_called_once...)
|
||||
pytest-mock >= 1.4.0 # needed for mock_use_standalone_module pytest option
|
||||
xmltodict < 0.12.0 ; python_version < '2.7' # xmltodict 0.12.0 and later require python 2.7 or later
|
||||
lxml < 4.3.0 ; python_version < '2.7' # lxml 4.3.0 and later require python 2.7 or later
|
||||
pyvmomi < 6.0.0 ; python_version < '2.7' # pyvmomi 6.0.0 and later require python 2.7 or later
|
||||
pyone == 1.1.9 # newer versions do not pass current integration tests
|
||||
boto3 < 1.11 ; python_version < '2.7' # boto3 1.11 drops Python 2.6 support
|
||||
botocore >= 1.10.0, < 1.14 ; python_version < '2.7' # adds support for the following AWS services: secretsmanager, fms, and acm-pca; botocore 1.14 drops Python 2.6 support
|
||||
botocore >= 1.10.0 ; python_version >= '2.7' # adds support for the following AWS services: secretsmanager, fms, and acm-pca
|
||||
setuptools < 45 ; python_version <= '2.7' # setuptools 45 and later require python 3.5 or later
|
||||
cffi >= 1.14.2, != 1.14.3 # Yanked version which older versions of pip will still install:
|
||||
|
||||
# freeze pylint and its requirements for consistent test results
|
||||
astroid == 2.2.5
|
||||
isort == 4.3.15
|
||||
lazy-object-proxy == 1.3.1
|
||||
mccabe == 0.6.1
|
||||
pylint == 2.3.1
|
||||
typed-ast == 1.4.0 # 1.4.0 is required to compile on Python 3.8
|
||||
wrapt == 1.11.1
|
||||
|
||||
# hvac
|
||||
hvac >= 1.2.1 ; python_version >= '3.6'
|
||||
|
||||
# urllib3
|
||||
# these should be satisfied naturally by the requests versions required by hvac anyway
|
||||
urllib3 >= 1.15 ; python_version >= '3.6' # we need raise_on_status for retry support to raise the correct exceptions https://github.com/urllib3/urllib3/blob/main/CHANGES.rst#115-2016-04-06
|
||||
|
||||
# requests
|
||||
# https://github.com/psf/requests/pull/6356
|
||||
requests >= 2.29 ; python_version >= '3.7'
|
||||
requests < 2.28 ; python_version < '3.7'
|
||||
+31
@@ -0,0 +1,31 @@
|
||||
{
|
||||
"auth": {
|
||||
"accessor": "zFP4VJtZFNGuzRsbYH8ham5E",
|
||||
"client_token": "s.urjjEppAAXAOL2EWLCXgS4CY",
|
||||
"entity_id": "fa3741ea-ad23-6557-9bc7-18a86dcaf3eb",
|
||||
"lease_duration": 3600,
|
||||
"metadata": {
|
||||
"role_name": "req-secret-id-role"
|
||||
},
|
||||
"orphan": true,
|
||||
"policies": [
|
||||
"alt-policy",
|
||||
"approle-policy",
|
||||
"default"
|
||||
],
|
||||
"renewable": true,
|
||||
"token_policies": [
|
||||
"alt-policy",
|
||||
"approle-policy",
|
||||
"default"
|
||||
],
|
||||
"token_type": "service"
|
||||
},
|
||||
"data": null,
|
||||
"lease_duration": 0,
|
||||
"lease_id": "",
|
||||
"renewable": false,
|
||||
"request_id": "b35b7ff6-c1ce-f61d-deef-805ac3ae13dc",
|
||||
"warnings": null,
|
||||
"wrap_info": null
|
||||
}
|
||||
+14
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"auth": null,
|
||||
"data": {
|
||||
"secret_id": "41b12758-8c6f-0896-c761-92e05675023c",
|
||||
"secret_id_accessor": "b0ab25c8-a8eb-3b31-3830-663840d5f504",
|
||||
"secret_id_ttl": 3600
|
||||
},
|
||||
"lease_duration": 0,
|
||||
"lease_id": "",
|
||||
"renewable": false,
|
||||
"request_id": "5e461200-18f2-0f18-4601-6bf2b9368cb5",
|
||||
"warnings": null,
|
||||
"wrap_info": null
|
||||
}
|
||||
+30
@@ -0,0 +1,30 @@
|
||||
{
|
||||
"request_id": "ec0d300f-ac44-4f5b-9feb-282d3a6686a7",
|
||||
"lease_id": "",
|
||||
"lease_duration": 0,
|
||||
"renewable": false,
|
||||
"data": null,
|
||||
"warnings": null,
|
||||
"auth": {
|
||||
"client_token": "s.YXZDqrOgv3mhlcPXpRBrS2cE",
|
||||
"accessor": "Xkad5E1bHRBJApR03pGrp1a0",
|
||||
"policies": [
|
||||
"default",
|
||||
"aws-sample-policy"
|
||||
],
|
||||
"token_policies": [
|
||||
"default",
|
||||
"aws-sample-policy"
|
||||
],
|
||||
"identity_policies": null,
|
||||
"metadata": {
|
||||
"account_id": "064281349855",
|
||||
"auth_type": "iam",
|
||||
"role_id": "b9462e71-e600-418d-b14e-fa69627470ec"
|
||||
},
|
||||
"orphan": true,
|
||||
"entity_id": "e23d3bad-7485-4330-bf74-d64fc1e774e4",
|
||||
"lease_duration": 1800,
|
||||
"renewable": true
|
||||
}
|
||||
}
|
||||
+33
@@ -0,0 +1,33 @@
|
||||
{
|
||||
"request_id": "cbfb16b9-4cf6-917d-182b-170801fc5a4e",
|
||||
"lease_id": "",
|
||||
"renewable": false,
|
||||
"lease_duration": 0,
|
||||
"data": null,
|
||||
"wrap_info": null,
|
||||
"warnings": null,
|
||||
"auth": {
|
||||
"client_token": "hvs.CAESIH6iy4yyvKMpk-vcaaVvU8nGfZFRCcH92hVa24lGNxHNGh4KHGh2cy5qU29Ua1FscTJIQ3BBY1AwTDM4dzNpR0E",
|
||||
"accessor": "60U0DvUOIMOIGI7kzAneeD2x",
|
||||
"policies": [
|
||||
"default",
|
||||
"azure-sample-policy"
|
||||
],
|
||||
"token_policies": [
|
||||
"default",
|
||||
"azure-sample-policy"
|
||||
],
|
||||
"metadata": {
|
||||
"resource_group_name": "",
|
||||
"role": "msi-vault",
|
||||
"subscription_id": ""
|
||||
},
|
||||
"lease_duration": 2764800,
|
||||
"renewable": true,
|
||||
"entity_id": "ff6a9d66-c2eb-6b78-e463-b3192243b5c1",
|
||||
"token_type": "service",
|
||||
"orphan": true,
|
||||
"mfa_requirement": null,
|
||||
"num_uses": 0
|
||||
}
|
||||
}
|
||||
+35
@@ -0,0 +1,35 @@
|
||||
{
|
||||
"auth": {
|
||||
"accessor": "f69aXFTLzAE1e5pPDpAqNAFW",
|
||||
"client_token": "s.bJ8UmS3NbYH3XJD7P70Yiyml",
|
||||
"entity_id": "84590d6b-54a2-7d81-201c-6107353169fb",
|
||||
"lease_duration": 3600,
|
||||
"metadata": {
|
||||
"authority_key_id": "66:45:2e:ae:d1:39:c8:d8:0d:fd:e7:d8:0f:8a:49:ee:f7:cc:53:ae",
|
||||
"cert_name": "vault_test",
|
||||
"common_name": "vault-test",
|
||||
"serial_number": "657513290402968240784573665154053221879835701422",
|
||||
"subject_key_id": "66:45:2e:ae:d1:39:c8:d8:0d:fd:e7:d8:0f:8a:49:ee:f7:cc:53:ae"
|
||||
},
|
||||
"orphan": true,
|
||||
"policies": [
|
||||
"approle-policy",
|
||||
"default",
|
||||
"test-policy"
|
||||
],
|
||||
"renewable": true,
|
||||
"token_policies": [
|
||||
"approle-policy",
|
||||
"default",
|
||||
"test-policy"
|
||||
],
|
||||
"token_type": "service"
|
||||
},
|
||||
"data": null,
|
||||
"lease_duration": 0,
|
||||
"lease_id": "",
|
||||
"renewable": false,
|
||||
"request_id": "9016334e-8bbb-4390-5512-c9b526b39bd3",
|
||||
"warnings": null,
|
||||
"wrap_info": null
|
||||
}
|
||||
+20
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"auth": null,
|
||||
"data": {
|
||||
"allowed_roles": [],
|
||||
"connection_details": {
|
||||
"connection_url": "postgresql://{{username}}:{{password}}@postgres:5432/postgres?sslmode=disable",
|
||||
"username": "UserName"
|
||||
},
|
||||
"password_policy": "",
|
||||
"plugin_name": "postgresql-database-plugin",
|
||||
"plugin_version": "",
|
||||
"root_credentials_rotate_statements": []
|
||||
},
|
||||
"lease_duration": 0,
|
||||
"lease_id": "",
|
||||
"renewable": false,
|
||||
"request_id": "91909ec0-cd89-489c-a7cf-2a82d2258b4d",
|
||||
"warnings": null,
|
||||
"wrap_info": null
|
||||
}
|
||||
+21
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"auth": null,
|
||||
"data": {
|
||||
"keys": [
|
||||
"con1",
|
||||
"con2",
|
||||
"con3"
|
||||
]
|
||||
},
|
||||
"connections": [
|
||||
"con1",
|
||||
"con2",
|
||||
"con3"
|
||||
],
|
||||
"lease_duration": 0,
|
||||
"lease_id": "",
|
||||
"renewable": false,
|
||||
"request_id": "91909ec0-cd89-489c-a7cf-2a82d2258b4d",
|
||||
"warnings": null,
|
||||
"wrap_info": null
|
||||
}
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"auth": null,
|
||||
"data": {
|
||||
"creation_statements": [
|
||||
"CREATE ROLE \"{{name}}\" WITH LOGIN PASSWORD '{{password}}' VALID UNTIL '{{expiration}}';",
|
||||
"GRANT SELECT ON ALL TABLES IN SCHEMA public TO \"{{name}}\";"
|
||||
],
|
||||
"credential_type": "password",
|
||||
"db_name": "SomeConnection",
|
||||
"default_ttl": 3600,
|
||||
"max_ttl": 86400,
|
||||
"renew_statements": [],
|
||||
"revocation_statements": [],
|
||||
"rollback_statements": []
|
||||
},
|
||||
"lease_duration": 0,
|
||||
"lease_id": "",
|
||||
"renewable": false,
|
||||
"request_id": "91909ec0-cd89-489c-a7cf-2a82d2258b4d",
|
||||
"warnings": null,
|
||||
"wrap_info": null
|
||||
}
|
||||
+21
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"auth": null,
|
||||
"data": {
|
||||
"keys": [
|
||||
"dyn_role1",
|
||||
"dyn_role2",
|
||||
"dyn_role3"
|
||||
]
|
||||
},
|
||||
"roles": [
|
||||
"dyn_role1",
|
||||
"dyn_role2",
|
||||
"dyn_role3"
|
||||
],
|
||||
"lease_duration": 0,
|
||||
"lease_id": "",
|
||||
"renewable": false,
|
||||
"request_id": "91909ec0-cd89-489c-a7cf-2a82d2258b4d",
|
||||
"warnings": null,
|
||||
"wrap_info": null
|
||||
}
|
||||
+25
@@ -0,0 +1,25 @@
|
||||
{
|
||||
"data": {
|
||||
"last_vault_rotation": "2024-01-01T09:00:00+01:00",
|
||||
"password": "Th3_$3cr3t_P@ss!",
|
||||
"rotation_period": 86400,
|
||||
"ttl": 123456,
|
||||
"username": "SomeUser"
|
||||
},
|
||||
"raw": {
|
||||
"auth": null,
|
||||
"data": {
|
||||
"last_vault_rotation": "2024-01-01T09:00:00+01:00",
|
||||
"password": "Th3_$3cr3t_P@ss!",
|
||||
"rotation_period": 86400,
|
||||
"ttl": 123456,
|
||||
"username": "SomeUser"
|
||||
},
|
||||
"lease_duration": 0,
|
||||
"lease_id": "",
|
||||
"renewable": false,
|
||||
"request_id": "91909ec0-cd89-489c-a7cf-2a82d2258b4d",
|
||||
"warnings": null,
|
||||
"wrap_info": null
|
||||
}
|
||||
}
|
||||
+18
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"auth": null,
|
||||
"data": {
|
||||
"credential_type": "password",
|
||||
"db_name": "SomeConnection",
|
||||
"last_vault_rotation": "2024-01-01T09:00:00 +01:00",
|
||||
"rotation_period": 86400,
|
||||
"rotation_statements": [
|
||||
"ALTER USER \"{{name}}\" WITH PASSWORD '{{password}}';"
|
||||
]
|
||||
},
|
||||
"lease_duration": 0,
|
||||
"lease_id": "",
|
||||
"renewable": false,
|
||||
"request_id": "91909ec0-cd89-489c-a7cf-2a82d2258b4d",
|
||||
"warnings": null,
|
||||
"wrap_info": null
|
||||
}
|
||||
+21
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"auth": null,
|
||||
"data": {
|
||||
"keys": [
|
||||
"role1",
|
||||
"role2",
|
||||
"role3"
|
||||
]
|
||||
},
|
||||
"roles": [
|
||||
"role1",
|
||||
"role2",
|
||||
"role3"
|
||||
],
|
||||
"lease_duration": 0,
|
||||
"lease_id": "",
|
||||
"renewable": false,
|
||||
"request_id": "91909ec0-cd89-489c-a7cf-2a82d2258b4d",
|
||||
"warnings": null,
|
||||
"wrap_info": null
|
||||
}
|
||||
+29
@@ -0,0 +1,29 @@
|
||||
{
|
||||
"auth": {
|
||||
"accessor": "3QbZdd50wZFaUqBVb6v6vXhG",
|
||||
"client_token": "s.8PtJkzREM9ZIYWQ28cSGqtP6",
|
||||
"entity_id": "b708d9c6-38fa-2f45-0cfd-1f36c11f3acb",
|
||||
"lease_duration": 3600,
|
||||
"metadata": {
|
||||
"role": "test-role"
|
||||
},
|
||||
"orphan": true,
|
||||
"policies": [
|
||||
"default",
|
||||
"test-policy"
|
||||
],
|
||||
"renewable": true,
|
||||
"token_policies": [
|
||||
"default",
|
||||
"test-policy"
|
||||
],
|
||||
"token_type": "service"
|
||||
},
|
||||
"data": null,
|
||||
"lease_duration": 0,
|
||||
"lease_id": "",
|
||||
"renewable": false,
|
||||
"request_id": "797bbe1d-4a95-c078-ecd2-2eff4c4fdaed",
|
||||
"warnings": null,
|
||||
"wrap_info": null
|
||||
}
|
||||
+13
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"auth": null,
|
||||
"data": {
|
||||
"Key1": "val1",
|
||||
"Key2": "val2"
|
||||
},
|
||||
"lease_duration": 2764800,
|
||||
"lease_id": "",
|
||||
"renewable": false,
|
||||
"request_id": "e26a7521-e512-82f1-3998-7cc494f14e86",
|
||||
"warnings": null,
|
||||
"wrap_info": null
|
||||
}
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"auth": null,
|
||||
"data": {
|
||||
"data": {
|
||||
"Key1": "val1",
|
||||
"Key2": "val2"
|
||||
},
|
||||
"metadata": {
|
||||
"created_time": "2022-04-21T15:56:58.8525402Z",
|
||||
"custom_metadata": null,
|
||||
"deletion_time": "",
|
||||
"destroyed": false,
|
||||
"version": 2
|
||||
}
|
||||
},
|
||||
"lease_duration": 0,
|
||||
"lease_id": "",
|
||||
"renewable": false,
|
||||
"request_id": "15538d55-0ad9-1c39-2f4b-dcbb982f13cc",
|
||||
"warnings": null,
|
||||
"wrap_info": null
|
||||
}
|
||||
+15
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"auth": null,
|
||||
"data": {
|
||||
"keys": [
|
||||
"Secret1",
|
||||
"Secret2"
|
||||
]
|
||||
},
|
||||
"lease_duration": 0,
|
||||
"lease_id": "",
|
||||
"renewable": false,
|
||||
"request_id": "02e4b52a-23b1-9a1c-cf2b-3799edb17fed",
|
||||
"warnings": null,
|
||||
"wrap_info": null
|
||||
}
|
||||
+28
@@ -0,0 +1,28 @@
|
||||
{
|
||||
"request_id": "30fd9f34-83af-4921-be0c-b93e41dc3959",
|
||||
"lease_id": "",
|
||||
"lease_duration": 0,
|
||||
"renewable": false,
|
||||
"data": {},
|
||||
"warnings": null,
|
||||
"auth": {
|
||||
"client_token": "s.fjXSOvsGY3Q95XGyJKnDw7OC",
|
||||
"accessor": "VnnNWBasAnVn1YO4cVL9jJei",
|
||||
"policies": [
|
||||
"default",
|
||||
"test-policy"
|
||||
],
|
||||
"token_policies": [
|
||||
"default",
|
||||
"test-policy"
|
||||
],
|
||||
"identity_policies": null,
|
||||
"metadata": {
|
||||
"username": "ldapuser"
|
||||
},
|
||||
"orphan": true,
|
||||
"entity_id": "08e5b262-7dc2-4edd-8fc7-77882ca7cc1b",
|
||||
"lease_duration": 3600,
|
||||
"renewable": true
|
||||
}
|
||||
}
|
||||
+24
@@ -0,0 +1,24 @@
|
||||
{
|
||||
"auth": null,
|
||||
"data": {
|
||||
"accessor": "8609694a-cdbc-db9b-d345-e782dbb562ed",
|
||||
"creation_time": 1523979354,
|
||||
"creation_ttl": 2764800,
|
||||
"display_name": "ldap2-tesla",
|
||||
"entity_id": "7d2e3179-f69b-450c-7179-ac8ee8bd8ca9",
|
||||
"expire_time": "2018-05-19T11:35:54.466476215-04:00",
|
||||
"explicit_max_ttl": 0,
|
||||
"id": "cf64a70f-3a12-3f6c-791d-6cef6d390eed",
|
||||
"identity_policies": ["dev-group-policy"],
|
||||
"issue_time": "2018-04-17T11:35:54.466476078-04:00",
|
||||
"meta": {
|
||||
"username": "tesla"
|
||||
},
|
||||
"num_uses": 0,
|
||||
"orphan": true,
|
||||
"path": "auth/ldap2/login/tesla",
|
||||
"policies": ["default", "testgroup2-policy"],
|
||||
"renewable": true,
|
||||
"ttl": 2764790
|
||||
}
|
||||
}
|
||||
+21
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"auth": null,
|
||||
"data": {
|
||||
"accessor": "8609694a-cdbc-db9b-d345-e782dbb562ed",
|
||||
"creation_time": 1523979354,
|
||||
"creation_ttl": 2764800,
|
||||
"display_name": "ldap2-tesla",
|
||||
"entity_id": "7d2e3179-f69b-450c-7179-ac8ee8bd8ca9",
|
||||
"expire_time": "2018-05-19T11:35:54.466476215-04:00",
|
||||
"explicit_max_ttl": 0,
|
||||
"id": "cf64a70f-3a12-3f6c-791d-6cef6d390eed",
|
||||
"identity_policies": ["dev-group-policy"],
|
||||
"issue_time": "2018-04-17T11:35:54.466476078-04:00",
|
||||
"num_uses": 0,
|
||||
"orphan": true,
|
||||
"path": "auth/ldap2/login/tesla",
|
||||
"policies": ["default", "testgroup2-policy"],
|
||||
"renewable": true,
|
||||
"ttl": 2764790
|
||||
}
|
||||
}
|
||||
+17
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"auth": null,
|
||||
"data": {
|
||||
"certificate": "-----BEGIN CERTIFICATE-----\nMIID/jCCAuagAwIBAgIUGlFiKFaKT3YFut6MIAEBjxdERtUwDQYJKoZIhvcNAQEL\nBQAwGTEXMBUGA1UEAxMOY2EuZXhhbXBsZS5vcmcwHhcNMjIwMjEzMTgwNzU2WhcN\nMjIwMjE0MTgwODI2WjAhMR8wHQYDVQQDExZkdW1teS50ZXN0LmV4YW1wbGUub3Jn\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAxVgdHP1hWPSisopSyAG5\nX6PzH0pvOd5DU1HNFwE6OELzj3LlWnvMhoPNI5YAjLX+R0z461YfuWmWDwRvwMXu\nb3ErZWcB04+9iZ2zIcpq2Bc3GzVWWRl3uB8pNDYN2EWwgc14z71yxD4A0mVBR9GG\nloP0ntLSfKAccdsEQ8Pd5WJKLN6QcaQ6nO2oc4qJT6F19c27nElpuq0Xd0j5hZg9\nfi+SMA/PZ+p6Ego46Wm5gGkD/AzIQ0ElSnQrw0dLc0t6ktis0Ln3IqW4SbooWqLU\nE5+30T/fLlnIoJpmjQqj4Gh96wKpOuQ+9vMA+0ODuFfMrV5cHCxwoPteJ/EXUSzr\n2wIDAQABo4IBNDCCATAwDgYDVR0PAQH/BAQDAgOoMB0GA1UdJQQWMBQGCCsGAQUF\nBwMBBggrBgEFBQcDAjAdBgNVHQ4EFgQUsUQZM+FDLd6A4AzMJ9l/FITktF8wHwYD\nVR0jBBgwFoAU2MqIFsUSq2DSn6Bps5AKsVRven8wOQYIKwYBBQUHAQEELTArMCkG\nCCsGAQUFBzAChh1odHRwOi8vbXl2YXVsdDo4MjAwL3YxL3BraS9jYTBTBgNVHREE\nTDBKghZkdW1teS50ZXN0LmV4YW1wbGUub3JnghdkdW1teTIudGVzdC5leGFtcGxl\nLm9yZ4IXZHVtbXkzLnRlc3QuZXhhbXBsZS5vcmcwLwYDVR0fBCgwJjAkoCKgIIYe\naHR0cDovL215dmF1bHQ6ODIwMC92MS9wa2kvY3JsMA0GCSqGSIb3DQEBCwUAA4IB\nAQB68sBRsYnAlZqwypVoJlvRqtqwvgdQF9tgTol+fHrKDFSeFDJbhQZY9QRI+juZ\n1CAWClCK5O4f0PGfozaDfn+Iph6wuc+H49MY3Z/wgwSvg2sQYOvUP6HZMk0XajDQ\ntJP1F/yPQrZ7e7WVQy9SdvLd/QwGjwCyRFvK2DS5IzImUzTreycUK7Fr7Vy+Rlj0\n1O5JMMJen1z2G5lqdeW3dthMM+LH2o7gSgms9RLd66Y/p+eCyXhPxI9TlJx84kqw\nu8MPJoEz9x2oX2bxuTLw6pmV7W7zH9YB5pZm2q9k5sDyFX4khvUTmBuOTQdcYO4W\nsgvZp3hDe9Hjh6WrQrxVUNfO\n-----END CERTIFICATE-----",
|
||||
"expiration": 1644862106,
|
||||
"issuing_ca": "-----BEGIN CERTIFICATE-----\nMIIDPjCCAiagAwIBAgIUD2KlA6b1Dgd0db97iymVMC8kG64wDQYJKoZIhvcNAQEL\nBQAwGTEXMBUGA1UEAxMOY2EuZXhhbXBsZS5vcmcwHhcNMjIwMjEzMTgwNzI4WhcN\nMjIwMzE3MTgwNzU4WjAZMRcwFQYDVQQDEw5jYS5leGFtcGxlLm9yZzCCASIwDQYJ\nKoZIhvcNAQEBBQADggEPADCCAQoCggEBAKYCpLqrJRbGglK2V0qHFZBd3tJIc3Qv\nMHDXA0SNXiEo+6RzTOqYAh72/7YeXGOTyD2f6wfdcwTuH1NYvfC+Oz1nDubdKn2F\nCdwKtuVAZ9BLSoxHH+NdntYaIVxv/7swpjHHFIxmA0ZN5auGWHfxe9kfeQc/ul3S\nF+bBQpt2syiLn0RheXUsf/2r/LcfZ93W4fD1G8yOU6xdsGpErwbXezfoaz2adDLQ\nXlj0rizpxR5zSKFBjCYMYVZJ06/22Pn1ePsWwGHrdJe1VuVab8k8uGVzvn4rNVOE\n3brfD6Jo3ua+xUIzrOmKQwmQ5GrUCRHvUW3IyecFyauinzopWtmFyGkCAwEAAaN+\nMHwwDgYDVR0PAQH/BAQDAgEGMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFNjK\niBbFEqtg0p+gabOQCrFUb3p/MB8GA1UdIwQYMBaAFNjKiBbFEqtg0p+gabOQCrFU\nb3p/MBkGA1UdEQQSMBCCDmNhLmV4YW1wbGUub3JnMA0GCSqGSIb3DQEBCwUAA4IB\nAQB1ItzmiOYJ70LaAdvzJN3VFexV4ZruM2QTgNbAHkIPgJQpGxoc3Y2+VaX0nWUR\nNWFLvw1vuM+rjjUe87X+mXoc6iPn6yZxmJ4QfaEkDgc+7jlr5T9STKgrnXQGIOF6\nO1pJiLfr+Rq5PpsdaQnN31DOpQIoeEVnf2c+2KmMIYt+E7JNRo+NS7lF3amgY6EZ\npOnbLLGQB+GM/EvPhDlMGrmFcW6Zk2GpyPMq1BtI/OJOcF1SB+R8ZOQ9ygHznt8f\nIs6d9xC0FlBLo901mp3NhrRI7nUJxdtFdZChgqjRYMB4kooQEC5RELbCUC3UvccO\ng9ne4DQhA/CVOOGYUpyHbOcP\n-----END CERTIFICATE-----",
|
||||
"private_key": "-----BEGIN RSA PRIVATE KEY-----\nMIIEpQIBAAKCAQEAxVgdHP1hWPSisopSyAG5X6PzH0pvOd5DU1HNFwE6OELzj3Ll\nWnvMhoPNI5YAjLX+R0z461YfuWmWDwRvwMXub3ErZWcB04+9iZ2zIcpq2Bc3GzVW\nWRl3uB8pNDYN2EWwgc14z71yxD4A0mVBR9GGloP0ntLSfKAccdsEQ8Pd5WJKLN6Q\ncaQ6nO2oc4qJT6F19c27nElpuq0Xd0j5hZg9fi+SMA/PZ+p6Ego46Wm5gGkD/AzI\nQ0ElSnQrw0dLc0t6ktis0Ln3IqW4SbooWqLUE5+30T/fLlnIoJpmjQqj4Gh96wKp\nOuQ+9vMA+0ODuFfMrV5cHCxwoPteJ/EXUSzr2wIDAQABAoIBAQCiI/FXnj9bbTQ3\n6Tp2piP+lp/st6WHMDy0umL9Yb7J9whSdh5HJ6w1YRktAdPVyLnxLybdhNdv6Xan\nRAflpTpwSdVT7Tws7M7XwMArJTp/7SMTsdEOR8R7fO7HvRnG9gs9uupmFMu0vRTD\nyPnH3jjsdeKIk8LpLkvwp/hrDQTFr3aZnIcRz0pmZAAxMxFgmKN/ZR0S5PVjTdIU\nUACPr90vciixITY3u0C5hc5IPNKMLm8D/4E65J9D7Vjcs05bU+6ecfYYFnQtIhaH\ncv+t3nk5sfuGTy6ozBQ820V/YCN5wS840wSrqLFmUz8vdSFTqJUZbMoDYQjcPvxq\nh+0UajJRAoGBAMlfuk++vnGNVL4wTGb6GdUqmExdJ6nnfZ+uq19FgL8Q0zwhEKtQ\nTytkDsmPf+T2NfuEby8xow1hmxzlEUAq70vnvDq6VEqUtcJNVU3TggkLutWmslbY\nTj0mGFLHVoue6L+ElDubjKSklNCZcNXAmbb98dbuuM5mvLLtbJU2Zk1fAoGBAPrg\nixXBolvifJOiNWcJCuTT4K6j8RyKwx7PofGd6T1pIeooi/T4/dLiLB6y9zbwhrZq\nXh9H4kKBZqMp9K+7GVZMZxhYWKCCTpxvpmGrX8hINFm8Rz2KpKW6MvRaUKs2NYsj\npE5r51lJoF8EuQf47nhpufX/C3TTzzS3OKQWMjcFAoGAeUQmhGNPeD4t7CJVwCWY\nbOArusDWY+C9q+2Z0dOfBnBxZGJdEW1ZX73vkb3SvOTv+Tj1Y6w2jpZavHnNe6Df\nXgx9M7iFjiwjkJDVb/qQ8jWYG5U5DEdSRkyslRzpp0bYzoxeX876USOzYjMk2fQU\nHTir7EzyCYmg1PdZTjnmPW0CgYEA+n2Q4dxA3DW75TykzYf91JSpVjZi2/jA8dam\n/7SH2cVLE54AgEzMQu+I1e4jYDuwhhqWd+0yQO0rKecOZRgPKFeI6Intk/YHv7LL\nEeIm9LcDbkXLa+sukjrj/Y7f1NN/irm/qH2ctU4KTlVM2mT21kvaXYCWU8PYs+3t\nJAj1gnECgYEApvShoOEUOQyEgxOoCNr2+tesK7PiLcFTbeDkzc4eXgn8+nzazMMn\nBDr8siHE2GBetcw5e4NkNy5sY+vV09ruLEhLES+QvHB/9+5/HKkx5XXRk0Bkw8DJ\nJx7qr67MRYF1yP242H9Iqf/fbnZepw3lqb7OJkDib8+CXYthMkcffgs=\n-----END RSA PRIVATE KEY-----",
|
||||
"private_key_type": "rsa",
|
||||
"serial_number": "1a:51:62:28:56:8a:4f:76:05:ba:de:8c:20:01:01:8f:17:44:46:d5"
|
||||
},
|
||||
"lease_duration": 0,
|
||||
"lease_id": "",
|
||||
"renewable": false,
|
||||
"request_id": "6a17b0ab-3bfa-a2a4-4b7d-d23aad10e021",
|
||||
"warnings": null,
|
||||
"wrap_info": null
|
||||
}
|
||||
+15
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"auth": null,
|
||||
"data": {
|
||||
"keys": [
|
||||
"Policy1",
|
||||
"Policy2"
|
||||
]
|
||||
},
|
||||
"lease_duration": 0,
|
||||
"lease_id": "",
|
||||
"renewable": false,
|
||||
"request_id": "96f2857e-5e33-1957-ea7e-be58f483faa3",
|
||||
"warnings": null,
|
||||
"wrap_info": null
|
||||
}
|
||||
+27
@@ -0,0 +1,27 @@
|
||||
{
|
||||
"auth": {
|
||||
"accessor": "ag7UbiKYw1HNvkUlz0EAmJF1",
|
||||
"client_token": "s.rlwajI2bblHAWU7uPqZhLru3",
|
||||
"entity_id": "44133048-b0f9-c0b1-29dc-5d2e62f73b0c",
|
||||
"lease_duration": 60,
|
||||
"metadata": null,
|
||||
"orphan": false,
|
||||
"policies": [
|
||||
"test",
|
||||
"default"
|
||||
],
|
||||
"renewable": true,
|
||||
"token_policies": [
|
||||
"test",
|
||||
"default"
|
||||
],
|
||||
"token_type": "service"
|
||||
},
|
||||
"data": null,
|
||||
"lease_duration": 0,
|
||||
"lease_id": "",
|
||||
"renewable": false,
|
||||
"request_id": "61138ea3-a6ff-8735-102f-4e0087e1b3f4",
|
||||
"warnings": null,
|
||||
"wrap_info": null
|
||||
}
|
||||
+15
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"auth": null,
|
||||
"data": {
|
||||
"keys": [
|
||||
"User1",
|
||||
"User2"
|
||||
]
|
||||
},
|
||||
"lease_duration": 0,
|
||||
"lease_id": "",
|
||||
"renewable": false,
|
||||
"request_id": "8b18a5ca-9baf-eb7c-18a6-11be81ed95a6",
|
||||
"warnings": null,
|
||||
"wrap_info": null
|
||||
}
|
||||
+31
@@ -0,0 +1,31 @@
|
||||
{
|
||||
"auth": {
|
||||
"accessor": "mQewzgKRx5Yui1h1eMemJlMu",
|
||||
"client_token": "s.drgLxu6ZtttSVn5Zkoy0huMR",
|
||||
"entity_id": "8a74ffd3-f71b-8ebe-7942-610428051ea9",
|
||||
"lease_duration": 3600,
|
||||
"metadata": {
|
||||
"username": "testuser"
|
||||
},
|
||||
"orphan": true,
|
||||
"policies": [
|
||||
"alt-policy",
|
||||
"default",
|
||||
"userpass-policy"
|
||||
],
|
||||
"renewable": true,
|
||||
"token_policies": [
|
||||
"alt-policy",
|
||||
"default",
|
||||
"userpass-policy"
|
||||
],
|
||||
"token_type": "service"
|
||||
},
|
||||
"data": null,
|
||||
"lease_duration": 0,
|
||||
"lease_id": "",
|
||||
"renewable": false,
|
||||
"request_id": "511e8fba-83f0-4b7e-95ea-770aa19c1957",
|
||||
"warnings": null,
|
||||
"wrap_info": null
|
||||
}
|
||||
+1
@@ -0,0 +1 @@
|
||||
token-value
|
||||
+56
@@ -0,0 +1,56 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright (c) 2021 Brian Scholer (@briantist)
|
||||
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
from __future__ import (absolute_import, division, print_function)
|
||||
__metaclass__ = type
|
||||
|
||||
import pytest
|
||||
|
||||
from ansible.errors import AnsibleError
|
||||
|
||||
from ansible_collections.community.hashi_vault.plugins.filter.vault_login_token import vault_login_token
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def login_response(fixture_loader):
|
||||
return fixture_loader('userpass_login_response.json')
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def module_login_response(login_response):
|
||||
return {
|
||||
"login": login_response
|
||||
}
|
||||
|
||||
|
||||
def test_vault_login_token_login_response(login_response):
|
||||
token = vault_login_token(login_response)
|
||||
|
||||
assert token == login_response['auth']['client_token']
|
||||
|
||||
|
||||
@pytest.mark.parametrize('optional_field', ['other', 'another'])
|
||||
def test_vault_login_token_login_response_alternate_optionals(login_response, optional_field):
|
||||
token = vault_login_token(login_response, optional_field=optional_field)
|
||||
|
||||
assert token == login_response['auth']['client_token']
|
||||
|
||||
|
||||
def test_vault_login_token_module_login_response(module_login_response):
|
||||
token = vault_login_token(module_login_response)
|
||||
|
||||
assert token == module_login_response['login']['auth']['client_token']
|
||||
|
||||
|
||||
@pytest.mark.parametrize('optional_field', ['other', 'another'])
|
||||
def test_vault_login_token_module_wrong_field(module_login_response, optional_field):
|
||||
with pytest.raises(AnsibleError, match=r"Could not find 'auth' or 'auth\.client_token' fields\. Input may not be a Vault login response\."):
|
||||
vault_login_token(module_login_response, optional_field=optional_field)
|
||||
|
||||
|
||||
@pytest.mark.parametrize('input', [1, 'string', ['array'], ('tuple',), False])
|
||||
def test_vault_login_token_wrong_types(input):
|
||||
with pytest.raises(AnsibleError, match=r"The 'vault_login_token' filter expects a dictionary\."):
|
||||
vault_login_token(input)
|
||||
+17
@@ -0,0 +1,17 @@
|
||||
# Copyright (c) 2022 Brian Scholer (@briantist)
|
||||
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
from __future__ import (absolute_import, division, print_function)
|
||||
__metaclass__ = type
|
||||
|
||||
import pytest
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def minimal_vars():
|
||||
return {
|
||||
'ansible_hashi_vault_auth_method': 'token',
|
||||
'ansible_hashi_vault_url': 'http://myvault',
|
||||
'ansible_hashi_vault_token': 'throwaway',
|
||||
}
|
||||
+54
@@ -0,0 +1,54 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright (c) 2021 Brian Scholer (@briantist)
|
||||
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
from __future__ import (absolute_import, division, print_function)
|
||||
__metaclass__ = type
|
||||
|
||||
import os
|
||||
import pytest
|
||||
|
||||
from ansible.plugins.loader import lookup_loader
|
||||
|
||||
from ansible.module_utils.six.moves.urllib.parse import urlparse
|
||||
|
||||
from ansible_collections.community.hashi_vault.tests.unit.compat import mock
|
||||
|
||||
from ansible_collections.community.hashi_vault.plugins.plugin_utils._hashi_vault_lookup_base import HashiVaultLookupBase
|
||||
|
||||
from requests.exceptions import ConnectionError
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def hashi_vault_lookup_module():
|
||||
return lookup_loader.get('community.hashi_vault.hashi_vault')
|
||||
|
||||
|
||||
class TestHashiVaultLookup(object):
|
||||
|
||||
def test_is_hashi_vault_lookup_base(self, hashi_vault_lookup_module):
|
||||
assert issubclass(type(hashi_vault_lookup_module), HashiVaultLookupBase)
|
||||
|
||||
# TODO: this test should be decoupled from the hashi_vault lookup and moved to the connection options tests
|
||||
@pytest.mark.parametrize(
|
||||
'envpatch,expected',
|
||||
[
|
||||
({'VAULT_ADDR': 'http://vault:0'}, 'http://vault:0'),
|
||||
({'ANSIBLE_HASHI_VAULT_ADDR': 'https://vaultalt'}, 'https://vaultalt'),
|
||||
({'VAULT_ADDR': 'https://vaultlow:8443', 'ANSIBLE_HASHI_VAULT_ADDR': 'http://vaulthigh:8200'}, 'http://vaulthigh:8200'),
|
||||
],
|
||||
)
|
||||
def test_vault_addr_low_pref(self, hashi_vault_lookup_module, envpatch, expected):
|
||||
url = urlparse(expected)
|
||||
host = url.hostname
|
||||
port = url.port if url.port is not None else {'http': 80, 'https': 443}[url.scheme]
|
||||
|
||||
with mock.patch.dict(os.environ, envpatch):
|
||||
with pytest.raises(ConnectionError) as e:
|
||||
hashi_vault_lookup_module.run(['secret/fake'], token='fake')
|
||||
|
||||
s_err = str(e.value)
|
||||
|
||||
assert str(host) in s_err, "host '%s' not found in exception: %r" % (host, str(e.value))
|
||||
assert str(port) in s_err, "port '%i' not found in exception: %r" % (port, str(e.value))
|
||||
+157
@@ -0,0 +1,157 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright (c) 2022 Brian Scholer (@briantist)
|
||||
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
from __future__ import (absolute_import, division, print_function)
|
||||
__metaclass__ = type
|
||||
|
||||
import pytest
|
||||
import re
|
||||
|
||||
from ansible.plugins.lookup import LookupBase
|
||||
from ansible.errors import AnsibleError
|
||||
|
||||
from ...compat import mock
|
||||
|
||||
|
||||
OPTIONS = {
|
||||
'_terms': (None, 'default'),
|
||||
'_private1': (1, 'default'),
|
||||
'_private2': (2, 'env'),
|
||||
'_private3': (None, 'variable'),
|
||||
'optionA': ('A', 'env'),
|
||||
'optionB': ('B', 'default'),
|
||||
'optionC': ('C', '/tmp/ansible.cfg'),
|
||||
'Doption': ('D', 'variable'),
|
||||
}
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def sample_options():
|
||||
return OPTIONS.copy()
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def patch_config_manager(sample_options):
|
||||
def _config_value_and_origin(name, *args, **kwargs):
|
||||
return sample_options[name]
|
||||
|
||||
from ansible.constants import config as C
|
||||
config = mock.Mock(wraps=C)
|
||||
config.get_configuration_definitions.return_value = sample_options.copy()
|
||||
config.get_config_value_and_origin = mock.Mock(wraps=_config_value_and_origin)
|
||||
|
||||
with mock.patch('ansible.constants.config', config):
|
||||
yield config
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def vault_ansible_settings_lookup(loader):
|
||||
return loader.get('community.hashi_vault.vault_ansible_settings')
|
||||
|
||||
|
||||
@pytest.fixture(params=[False, True])
|
||||
def loader(request):
|
||||
from ansible.plugins.loader import lookup_loader as orig_loader
|
||||
|
||||
def _legacy_sim(plugin):
|
||||
r = orig_loader.find_plugin_with_context(plugin)
|
||||
return (r.plugin_resolved_name, None)
|
||||
|
||||
loader = mock.Mock(wraps=orig_loader)
|
||||
|
||||
if request.param:
|
||||
loader.find_plugin_with_context.side_effect = AttributeError
|
||||
loader.find_plugin_with_name = mock.Mock(wraps=_legacy_sim)
|
||||
|
||||
with mock.patch('ansible.plugins.loader.lookup_loader', loader):
|
||||
yield loader
|
||||
|
||||
|
||||
class TestVaultAnsibleSettingsLookup(object):
|
||||
|
||||
def test_vault_ansible_settings_is_lookup_base(self, vault_ansible_settings_lookup):
|
||||
assert issubclass(type(vault_ansible_settings_lookup), LookupBase)
|
||||
|
||||
@pytest.mark.parametrize('opt_plugin', ['community.hashi_vault.vault_login', 'vault_login'], ids=['plugin=fqcn', 'plugin=short'])
|
||||
@pytest.mark.parametrize('opt_inc_none', [True, False], ids=lambda x: 'none=%s' % x)
|
||||
@pytest.mark.parametrize('opt_inc_default', [True, False], ids=lambda x: 'default=%s' % x)
|
||||
@pytest.mark.parametrize('opt_inc_private', [True, False], ids=lambda x: 'private=%s' % x)
|
||||
@pytest.mark.parametrize('variables', [
|
||||
{},
|
||||
dict(ansible_hashi_vault_retries=7, ansible_hashi_vault_url='https://the.money.bin'),
|
||||
])
|
||||
@pytest.mark.parametrize(['terms', 'expected'], [
|
||||
([], ['_terms', '_private1', '_private2', '_private3', 'optionA', 'optionB', 'optionC', 'Doption']),
|
||||
(['*'], ['_terms', '_private1', '_private2', '_private3', 'optionA', 'optionB', 'optionC', 'Doption']),
|
||||
(['opt*'], ['optionA', 'optionB', 'optionC']),
|
||||
(['*', '!opt*'], ['_terms', '_private1', '_private2', '_private3', 'Doption']),
|
||||
(['*', '!*opt*', 'option[B-C]'], ['_terms', '_private1', '_private2', '_private3', 'optionB', 'optionC']),
|
||||
])
|
||||
def test_vault_ansible_settings_stuff(
|
||||
self, vault_ansible_settings_lookup,
|
||||
opt_plugin, opt_inc_none, opt_inc_default, opt_inc_private, variables, terms, expected,
|
||||
patch_config_manager, sample_options, loader
|
||||
):
|
||||
kwargs = dict(
|
||||
plugin=opt_plugin,
|
||||
include_default=opt_inc_default,
|
||||
include_none=opt_inc_none,
|
||||
include_private=opt_inc_private
|
||||
)
|
||||
|
||||
result = vault_ansible_settings_lookup.run(terms, variables, **kwargs)
|
||||
|
||||
# this lookup always returns a single dictionary
|
||||
assert isinstance(result, list)
|
||||
assert len(result) == 1
|
||||
deresult = result[0]
|
||||
assert isinstance(deresult, dict)
|
||||
|
||||
patch_config_manager.get_configuration_definitions.assert_called_once()
|
||||
|
||||
fqplugin = re.sub(r'^(?:community\.hashi_vault\.)?(.*?)$', r'community.hashi_vault.\1', opt_plugin)
|
||||
if hasattr(loader, 'find_plugin_with_name'):
|
||||
loader.find_plugin_with_name.assert_called_once_with(fqplugin)
|
||||
else:
|
||||
loader.find_plugin_with_context.assert_called_once_with(fqplugin)
|
||||
|
||||
# the calls to get_config_value_and_origin vary, get the whole list of calls
|
||||
cvocalls = patch_config_manager.get_config_value_and_origin.call_args_list
|
||||
|
||||
for call in cvocalls:
|
||||
# 1) ensure variables are always included in this call
|
||||
# 2) ensure this method is only called for expected keys (after filtering)
|
||||
margs, mkwargs = call
|
||||
assert 'variables' in mkwargs and mkwargs['variables'] == variables, call
|
||||
assert margs[0] in expected
|
||||
|
||||
# go through all expected keys, ensuring they are in the result,
|
||||
# or that they had a reason not to be.
|
||||
for ex in expected:
|
||||
skip_private = not opt_inc_private and ex.startswith('_')
|
||||
skip_none = not opt_inc_none and sample_options[ex][0] is None
|
||||
skip_default = not opt_inc_default and sample_options[ex][1] == 'default'
|
||||
skip = skip_private or skip_none or skip_default
|
||||
|
||||
assert ex in deresult or skip
|
||||
|
||||
# ensure all expected keys (other than skipped private) had their values checked
|
||||
if not skip_private:
|
||||
assert any(call[0][0] == ex for call in cvocalls)
|
||||
|
||||
# now check the results:
|
||||
# 1) ensure private values are not present when they should not be
|
||||
# 2) ensure None values are not present when they should not be
|
||||
# 3) ensure values derived from defaults are not present when they should not be
|
||||
# 4) ensure the value returned is the correct value
|
||||
for k, v in deresult.items():
|
||||
assert opt_inc_private or not k.startswith('_')
|
||||
assert opt_inc_none or v is not None
|
||||
assert opt_inc_default or sample_options[k][1] != 'default'
|
||||
assert v == sample_options[k][0]
|
||||
|
||||
def test_vault_ansible_settings_plugin_not_found(self, vault_ansible_settings_lookup):
|
||||
with pytest.raises(AnsibleError, match=r"'_ns._col._fake' plugin not found\."):
|
||||
vault_ansible_settings_lookup.run([], plugin='_ns._col._fake')
|
||||
+135
@@ -0,0 +1,135 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright (c) 2022 Brian Scholer (@briantist)
|
||||
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
from __future__ import (absolute_import, division, print_function)
|
||||
__metaclass__ = type
|
||||
|
||||
import re
|
||||
import pytest
|
||||
|
||||
from ansible.plugins.loader import lookup_loader
|
||||
from ansible.errors import AnsibleError
|
||||
|
||||
from ...compat import mock
|
||||
|
||||
from .....plugins.plugin_utils._hashi_vault_lookup_base import HashiVaultLookupBase
|
||||
from .....plugins.module_utils._hashi_vault_common import HashiVaultValueError
|
||||
|
||||
from .....plugins.lookup import vault_kv1_get
|
||||
|
||||
|
||||
hvac = pytest.importorskip('hvac')
|
||||
|
||||
|
||||
pytestmark = pytest.mark.usefixtures(
|
||||
'patch_authenticator',
|
||||
'patch_get_vault_client',
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def vault_kv1_get_lookup():
|
||||
return lookup_loader.get('community.hashi_vault.vault_kv1_get')
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def kv1_get_response(fixture_loader):
|
||||
return fixture_loader('kv1_get_response.json')
|
||||
|
||||
|
||||
class TestVaultKv1GetLookup(object):
|
||||
|
||||
def test_vault_kv1_get_is_lookup_base(self, vault_kv1_get_lookup):
|
||||
assert issubclass(type(vault_kv1_get_lookup), HashiVaultLookupBase)
|
||||
|
||||
def test_vault_kv1_get_no_hvac(self, vault_kv1_get_lookup, minimal_vars):
|
||||
with mock.patch.object(vault_kv1_get, 'HVAC_IMPORT_ERROR', new=ImportError()):
|
||||
with pytest.raises(AnsibleError, match=r"This plugin requires the 'hvac' Python library"):
|
||||
vault_kv1_get_lookup.run(terms='fake', variables=minimal_vars)
|
||||
|
||||
@pytest.mark.parametrize('exc', [HashiVaultValueError('throwaway msg'), NotImplementedError('throwaway msg')])
|
||||
def test_vault_kv1_get_authentication_error(self, vault_kv1_get_lookup, minimal_vars, authenticator, exc):
|
||||
authenticator.authenticate.side_effect = exc
|
||||
|
||||
with pytest.raises(AnsibleError, match=r'throwaway msg'):
|
||||
vault_kv1_get_lookup.run(terms='fake', variables=minimal_vars)
|
||||
|
||||
@pytest.mark.parametrize('exc', [HashiVaultValueError('throwaway msg'), NotImplementedError('throwaway msg')])
|
||||
def test_vault_kv1_get_auth_validation_error(self, vault_kv1_get_lookup, minimal_vars, authenticator, exc):
|
||||
authenticator.validate.side_effect = exc
|
||||
|
||||
with pytest.raises(AnsibleError, match=r'throwaway msg'):
|
||||
vault_kv1_get_lookup.run(terms='fake', variables=minimal_vars)
|
||||
|
||||
@pytest.mark.parametrize('paths', [['fake1'], ['fake2', 'fake3']])
|
||||
@pytest.mark.parametrize('engine_mount_point', ['kv', 'other'])
|
||||
def test_vault_kv1_get_return_data(self, vault_kv1_get_lookup, minimal_vars, kv1_get_response, vault_client, paths, engine_mount_point):
|
||||
client = vault_client
|
||||
|
||||
expected_calls = [mock.call(path=p, mount_point=engine_mount_point) for p in paths]
|
||||
|
||||
expected = {}
|
||||
expected['raw'] = kv1_get_response.copy()
|
||||
expected['metadata'] = kv1_get_response.copy()
|
||||
expected['data'] = expected['metadata'].pop('data')
|
||||
expected['secret'] = expected['data']
|
||||
|
||||
def _fake_kv1_get(path, mount_point):
|
||||
r = kv1_get_response.copy()
|
||||
r['data'] = r['data'].copy()
|
||||
r['data'].update({'_path': path})
|
||||
r['data'].update({'_mount': mount_point})
|
||||
return r
|
||||
|
||||
client.secrets.kv.v1.read_secret = mock.Mock(wraps=_fake_kv1_get)
|
||||
|
||||
response = vault_kv1_get_lookup.run(terms=paths, variables=minimal_vars, engine_mount_point=engine_mount_point)
|
||||
|
||||
client.secrets.kv.v1.read_secret.assert_has_calls(expected_calls)
|
||||
|
||||
assert len(response) == len(paths), "%i paths processed but got %i responses" % (len(paths), len(response))
|
||||
|
||||
for p in paths:
|
||||
r = response.pop(0)
|
||||
ins_p = r['secret'].pop('_path')
|
||||
ins_m = r['secret'].pop('_mount')
|
||||
assert p == ins_p, "expected '_path=%s' field was not found in response, got %r" % (p, ins_p)
|
||||
assert engine_mount_point == ins_m, "expected '_mount=%s' field was not found in response, got %r" % (engine_mount_point, ins_m)
|
||||
assert r['raw'] == expected['raw'], (
|
||||
"remaining response did not match expected\nresponse: %r\nexpected: %r" % (r, expected['raw'])
|
||||
)
|
||||
assert r['metadata'] == expected['metadata']
|
||||
assert r['data'] == expected['data']
|
||||
assert r['secret'] == expected['secret']
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
'exc',
|
||||
[
|
||||
(hvac.exceptions.Forbidden, "", r"^Forbidden: Permission Denied to path \['([^']+)'\]"),
|
||||
(
|
||||
hvac.exceptions.InvalidPath,
|
||||
"Invalid path for a versioned K/V secrets engine",
|
||||
r"^Invalid path for a versioned K/V secrets engine \['[^']+'\]. If this is a KV version 2 path, use community.hashi_vault.vault_kv2_get"
|
||||
),
|
||||
(hvac.exceptions.InvalidPath, "", r"^Invalid or missing path \['[^']+'\]"),
|
||||
]
|
||||
)
|
||||
@pytest.mark.parametrize('path', ['path/1', 'second/path'])
|
||||
def test_vault_kv1_get_exceptions(self, vault_kv1_get_lookup, minimal_vars, vault_client, path, exc):
|
||||
client = vault_client
|
||||
|
||||
client.secrets.kv.v1.read_secret.side_effect = exc[0](exc[1])
|
||||
|
||||
with pytest.raises(AnsibleError) as e:
|
||||
vault_kv1_get_lookup.run(terms=[path], variables=minimal_vars)
|
||||
|
||||
match = re.search(exc[2], str(e.value))
|
||||
|
||||
assert match is not None, "result: %r\ndid not match: %s" % (e.value, exc[2])
|
||||
|
||||
try:
|
||||
assert path == match.group(1), "expected: %s\ngot: %s" % (match.group(1), path)
|
||||
except IndexError:
|
||||
pass
|
||||
+139
@@ -0,0 +1,139 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright (c) 2022 Brian Scholer (@briantist)
|
||||
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
from __future__ import (absolute_import, division, print_function)
|
||||
__metaclass__ = type
|
||||
|
||||
import re
|
||||
import pytest
|
||||
|
||||
from ansible.plugins.loader import lookup_loader
|
||||
from ansible.errors import AnsibleError
|
||||
|
||||
from ...compat import mock
|
||||
|
||||
from .....plugins.plugin_utils._hashi_vault_lookup_base import HashiVaultLookupBase
|
||||
from .....plugins.module_utils._hashi_vault_common import HashiVaultValueError
|
||||
|
||||
from .....plugins.lookup import vault_kv2_get
|
||||
|
||||
|
||||
hvac = pytest.importorskip('hvac')
|
||||
|
||||
|
||||
pytestmark = pytest.mark.usefixtures(
|
||||
'patch_authenticator',
|
||||
'patch_get_vault_client',
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def vault_kv2_get_lookup():
|
||||
return lookup_loader.get('community.hashi_vault.vault_kv2_get')
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def kv2_get_response(fixture_loader):
|
||||
return fixture_loader('kv2_get_response.json')
|
||||
|
||||
|
||||
class TestVaultKv2GetLookup(object):
|
||||
|
||||
def test_vault_kv2_get_is_lookup_base(self, vault_kv2_get_lookup):
|
||||
assert issubclass(type(vault_kv2_get_lookup), HashiVaultLookupBase)
|
||||
|
||||
def test_vault_kv2_get_no_hvac(self, vault_kv2_get_lookup, minimal_vars):
|
||||
with mock.patch.object(vault_kv2_get, 'HVAC_IMPORT_ERROR', new=ImportError()):
|
||||
with pytest.raises(AnsibleError, match=r"This plugin requires the 'hvac' Python library"):
|
||||
vault_kv2_get_lookup.run(terms='fake', variables=minimal_vars)
|
||||
|
||||
@pytest.mark.parametrize('exc', [HashiVaultValueError('throwaway msg'), NotImplementedError('throwaway msg')])
|
||||
def test_vault_kv2_get_authentication_error(self, vault_kv2_get_lookup, minimal_vars, authenticator, exc):
|
||||
authenticator.authenticate.side_effect = exc
|
||||
|
||||
with pytest.raises(AnsibleError, match=r'throwaway msg'):
|
||||
vault_kv2_get_lookup.run(terms='fake', variables=minimal_vars)
|
||||
|
||||
@pytest.mark.parametrize('exc', [HashiVaultValueError('throwaway msg'), NotImplementedError('throwaway msg')])
|
||||
def test_vault_kv2_get_auth_validation_error(self, vault_kv2_get_lookup, minimal_vars, authenticator, exc):
|
||||
authenticator.validate.side_effect = exc
|
||||
|
||||
with pytest.raises(AnsibleError, match=r'throwaway msg'):
|
||||
vault_kv2_get_lookup.run(terms='fake', variables=minimal_vars)
|
||||
|
||||
@pytest.mark.parametrize('paths', [['fake1'], ['fake2', 'fake3']])
|
||||
@pytest.mark.parametrize('engine_mount_point', ['secret', 'other'])
|
||||
@pytest.mark.parametrize('version', [None, 2, 10])
|
||||
def test_vault_kv2_get_return_data(self, vault_kv2_get_lookup, minimal_vars, kv2_get_response, vault_client, paths, engine_mount_point, version):
|
||||
client = vault_client
|
||||
rv = kv2_get_response.copy()
|
||||
rv['data']['metadata']['version'] = version
|
||||
|
||||
expected = {}
|
||||
expected['raw'] = rv.copy()
|
||||
expected['metadata'] = expected['raw']['data']['metadata']
|
||||
expected['data'] = expected['raw']['data']
|
||||
expected['secret'] = expected['data']['data']
|
||||
|
||||
expected_calls = [mock.call(path=p, version=version, mount_point=engine_mount_point) for p in paths]
|
||||
|
||||
def _fake_kv2_get(path, version, mount_point):
|
||||
r = rv.copy()
|
||||
r['data']['data'] = r['data']['data'].copy()
|
||||
r['data']['data'].update({'_path': path})
|
||||
r['data']['data'].update({'_mount': mount_point})
|
||||
return r
|
||||
|
||||
client.secrets.kv.v2.read_secret_version = mock.Mock(wraps=_fake_kv2_get)
|
||||
|
||||
response = vault_kv2_get_lookup.run(terms=paths, variables=minimal_vars, version=version, engine_mount_point=engine_mount_point)
|
||||
|
||||
client.secrets.kv.v2.read_secret_version.assert_has_calls(expected_calls)
|
||||
|
||||
assert len(response) == len(paths), "%i paths processed but got %i responses" % (len(paths), len(response))
|
||||
|
||||
for p in paths:
|
||||
r = response.pop(0)
|
||||
ins_p = r['secret'].pop('_path')
|
||||
ins_m = r['secret'].pop('_mount')
|
||||
assert p == ins_p, "expected '_path=%s' field was not found in response, got %r" % (p, ins_p)
|
||||
assert engine_mount_point == ins_m, "expected '_mount=%s' field was not found in response, got %r" % (engine_mount_point, ins_m)
|
||||
assert r['raw'] == expected['raw'], (
|
||||
"remaining response did not match expected\nresponse: %r\nexpected: %r" % (r, expected['raw'])
|
||||
)
|
||||
assert r['metadata'] == expected['metadata']
|
||||
assert r['data'] == expected['data']
|
||||
assert r['secret'] == expected['secret']
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
'exc',
|
||||
[
|
||||
(hvac.exceptions.Forbidden, "", r"^Forbidden: Permission Denied to path \['([^']+)'\]"),
|
||||
(
|
||||
hvac.exceptions.InvalidPath,
|
||||
"",
|
||||
r"^Invalid or missing path \['([^']+)'\] with secret version '(\d+|latest)'. Check the path or secret version"
|
||||
),
|
||||
]
|
||||
)
|
||||
@pytest.mark.parametrize('path', ['path/1', 'second/path'])
|
||||
@pytest.mark.parametrize('version', [None, 2, 10])
|
||||
def test_vault_kv2_get_exceptions(self, vault_kv2_get_lookup, minimal_vars, vault_client, path, version, exc):
|
||||
client = vault_client
|
||||
|
||||
client.secrets.kv.v2.read_secret_version.side_effect = exc[0](exc[1])
|
||||
|
||||
with pytest.raises(AnsibleError) as e:
|
||||
vault_kv2_get_lookup.run(terms=[path], variables=minimal_vars, version=version)
|
||||
|
||||
match = re.search(exc[2], str(e.value))
|
||||
|
||||
assert path == match.group(1), "expected: %s\ngot: %s" % (match.group(1), path)
|
||||
|
||||
try:
|
||||
assert (version is None) == (match.group(2) == 'latest')
|
||||
assert (version is not None) == (match.group(2) == str(version))
|
||||
except IndexError:
|
||||
pass
|
||||
+94
@@ -0,0 +1,94 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright (c) 2023 Tom Kivlin (@tomkivlin)
|
||||
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
from __future__ import (absolute_import, division, print_function)
|
||||
__metaclass__ = type
|
||||
|
||||
import pytest
|
||||
|
||||
from ansible.plugins.loader import lookup_loader
|
||||
from ansible.errors import AnsibleError
|
||||
|
||||
from ...compat import mock
|
||||
|
||||
from .....plugins.plugin_utils._hashi_vault_lookup_base import HashiVaultLookupBase
|
||||
from .....plugins.module_utils._hashi_vault_common import HashiVaultValueError
|
||||
|
||||
from .....plugins.lookup import vault_list
|
||||
|
||||
|
||||
hvac = pytest.importorskip('hvac')
|
||||
|
||||
|
||||
pytestmark = pytest.mark.usefixtures(
|
||||
'patch_authenticator',
|
||||
'patch_get_vault_client',
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def vault_list_lookup():
|
||||
return lookup_loader.get('community.hashi_vault.vault_list')
|
||||
|
||||
|
||||
LIST_FIXTURES = [
|
||||
'kv2_list_response.json',
|
||||
'policy_list_response.json',
|
||||
'userpass_list_response.json',
|
||||
]
|
||||
|
||||
|
||||
@pytest.fixture(params=LIST_FIXTURES)
|
||||
def list_response(request, fixture_loader):
|
||||
return fixture_loader(request.param)
|
||||
|
||||
|
||||
class TestVaultListLookup(object):
|
||||
|
||||
def test_vault_list_is_lookup_base(self, vault_list_lookup):
|
||||
assert issubclass(type(vault_list_lookup), HashiVaultLookupBase)
|
||||
|
||||
def test_vault_list_no_hvac(self, vault_list_lookup, minimal_vars):
|
||||
with mock.patch.object(vault_list, 'HVAC_IMPORT_ERROR', new=ImportError()):
|
||||
with pytest.raises(AnsibleError, match=r"This plugin requires the 'hvac' Python library"):
|
||||
vault_list_lookup.run(terms='fake', variables=minimal_vars)
|
||||
|
||||
@pytest.mark.parametrize('exc', [HashiVaultValueError('throwaway msg'), NotImplementedError('throwaway msg')])
|
||||
def test_vault_list_authentication_error(self, vault_list_lookup, minimal_vars, authenticator, exc):
|
||||
authenticator.authenticate.side_effect = exc
|
||||
|
||||
with pytest.raises(AnsibleError, match=r'throwaway msg'):
|
||||
vault_list_lookup.run(terms='fake', variables=minimal_vars)
|
||||
|
||||
@pytest.mark.parametrize('exc', [HashiVaultValueError('throwaway msg'), NotImplementedError('throwaway msg')])
|
||||
def test_vault_list_auth_validation_error(self, vault_list_lookup, minimal_vars, authenticator, exc):
|
||||
authenticator.validate.side_effect = exc
|
||||
|
||||
with pytest.raises(AnsibleError, match=r'throwaway msg'):
|
||||
vault_list_lookup.run(terms='fake', variables=minimal_vars)
|
||||
|
||||
@pytest.mark.parametrize('paths', [['fake1'], ['fake2', 'fake3']])
|
||||
def test_vault_list_return_data(self, vault_list_lookup, minimal_vars, list_response, vault_client, paths):
|
||||
client = vault_client
|
||||
|
||||
expected_calls = [mock.call(p) for p in paths]
|
||||
|
||||
def _fake_list_operation(path):
|
||||
r = list_response.copy()
|
||||
r.update({'_path': path})
|
||||
return r
|
||||
|
||||
client.list = mock.Mock(wraps=_fake_list_operation)
|
||||
|
||||
response = vault_list_lookup.run(terms=paths, variables=minimal_vars)
|
||||
|
||||
client.list.assert_has_calls(expected_calls)
|
||||
|
||||
assert len(response) == len(paths), "%i paths processed but got %i responses" % (len(paths), len(response))
|
||||
|
||||
for p in paths:
|
||||
r = response.pop(0)
|
||||
ins_p = r.pop('_path')
|
||||
assert p == ins_p, "expected '_path=%s' field was not found in response, got %r" % (p, ins_p)
|
||||
+53
@@ -0,0 +1,53 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright (c) 2021 Brian Scholer (@briantist)
|
||||
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
from __future__ import (absolute_import, division, print_function)
|
||||
__metaclass__ = type
|
||||
|
||||
import pytest
|
||||
|
||||
from ansible.plugins.loader import lookup_loader
|
||||
from ansible.errors import AnsibleError
|
||||
|
||||
from ansible_collections.community.hashi_vault.tests.unit.compat import mock
|
||||
|
||||
from ansible_collections.community.hashi_vault.plugins.plugin_utils._hashi_vault_lookup_base import HashiVaultLookupBase
|
||||
|
||||
from .....plugins.lookup import vault_login
|
||||
|
||||
|
||||
pytest.importorskip('hvac')
|
||||
|
||||
|
||||
pytestmark = pytest.mark.usefixtures(
|
||||
'patch_authenticator',
|
||||
'patch_get_vault_client',
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def vault_login_lookup():
|
||||
return lookup_loader.get('community.hashi_vault.vault_login')
|
||||
|
||||
|
||||
class TestVaultLoginLookup(object):
|
||||
|
||||
def test_vault_login_is_lookup_base(self, vault_login_lookup):
|
||||
assert issubclass(type(vault_login_lookup), HashiVaultLookupBase)
|
||||
|
||||
def test_vault_login_no_hvac(self, vault_login_lookup, minimal_vars):
|
||||
with mock.patch.object(vault_login, 'HVAC_IMPORT_ERROR', new=ImportError()):
|
||||
with pytest.raises(AnsibleError, match=r"This plugin requires the 'hvac' Python library"):
|
||||
vault_login_lookup.run(terms='fake', variables=minimal_vars)
|
||||
|
||||
def test_vault_login_auth_none(self, vault_login_lookup):
|
||||
with pytest.raises(AnsibleError, match=r"The 'none' auth method is not valid for this lookup"):
|
||||
vault_login_lookup.run(terms=[], variables={'ansible_hashi_vault_auth_method': 'none'})
|
||||
|
||||
def test_vault_login_extra_terms(self, vault_login_lookup, authenticator, minimal_vars):
|
||||
with mock.patch('ansible_collections.community.hashi_vault.plugins.lookup.vault_login.display.warning') as warning:
|
||||
with mock.patch.object(vault_login_lookup, 'authenticator', new=authenticator):
|
||||
vault_login_lookup.run(terms=['', ''], variables=minimal_vars)
|
||||
warning.assert_called_once_with("Supplied term strings will be ignored. This lookup does not use term strings.")
|
||||
+87
@@ -0,0 +1,87 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright (c) 2022 Brian Scholer (@briantist)
|
||||
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
from __future__ import (absolute_import, division, print_function)
|
||||
__metaclass__ = type
|
||||
|
||||
import pytest
|
||||
|
||||
from ansible.plugins.loader import lookup_loader
|
||||
from ansible.errors import AnsibleError
|
||||
|
||||
from ...compat import mock
|
||||
|
||||
from .....plugins.plugin_utils._hashi_vault_lookup_base import HashiVaultLookupBase
|
||||
from .....plugins.module_utils._hashi_vault_common import HashiVaultValueError
|
||||
|
||||
from .....plugins.lookup import vault_read
|
||||
|
||||
|
||||
hvac = pytest.importorskip('hvac')
|
||||
|
||||
|
||||
pytestmark = pytest.mark.usefixtures(
|
||||
'patch_authenticator',
|
||||
'patch_get_vault_client',
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def vault_read_lookup():
|
||||
return lookup_loader.get('community.hashi_vault.vault_read')
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def kv1_get_response(fixture_loader):
|
||||
return fixture_loader('kv1_get_response.json')
|
||||
|
||||
|
||||
class TestVaultReadLookup(object):
|
||||
|
||||
def test_vault_read_is_lookup_base(self, vault_read_lookup):
|
||||
assert issubclass(type(vault_read_lookup), HashiVaultLookupBase)
|
||||
|
||||
def test_vault_read_no_hvac(self, vault_read_lookup, minimal_vars):
|
||||
with mock.patch.object(vault_read, 'HVAC_IMPORT_ERROR', new=ImportError()):
|
||||
with pytest.raises(AnsibleError, match=r"This plugin requires the 'hvac' Python library"):
|
||||
vault_read_lookup.run(terms='fake', variables=minimal_vars)
|
||||
|
||||
@pytest.mark.parametrize('exc', [HashiVaultValueError('throwaway msg'), NotImplementedError('throwaway msg')])
|
||||
def test_vault_read_authentication_error(self, vault_read_lookup, minimal_vars, authenticator, exc):
|
||||
authenticator.authenticate.side_effect = exc
|
||||
|
||||
with pytest.raises(AnsibleError, match=r'throwaway msg'):
|
||||
vault_read_lookup.run(terms='fake', variables=minimal_vars)
|
||||
|
||||
@pytest.mark.parametrize('exc', [HashiVaultValueError('throwaway msg'), NotImplementedError('throwaway msg')])
|
||||
def test_vault_read_auth_validation_error(self, vault_read_lookup, minimal_vars, authenticator, exc):
|
||||
authenticator.validate.side_effect = exc
|
||||
|
||||
with pytest.raises(AnsibleError, match=r'throwaway msg'):
|
||||
vault_read_lookup.run(terms='fake', variables=minimal_vars)
|
||||
|
||||
@pytest.mark.parametrize('paths', [['fake1'], ['fake2', 'fake3']])
|
||||
def test_vault_read_return_data(self, vault_read_lookup, minimal_vars, kv1_get_response, vault_client, paths):
|
||||
client = vault_client
|
||||
|
||||
expected_calls = [mock.call(p) for p in paths]
|
||||
|
||||
def _fake_kv1_get(path):
|
||||
r = kv1_get_response.copy()
|
||||
r.update({'_path': path})
|
||||
return r
|
||||
|
||||
client.read = mock.Mock(wraps=_fake_kv1_get)
|
||||
|
||||
response = vault_read_lookup.run(terms=paths, variables=minimal_vars)
|
||||
|
||||
client.read.assert_has_calls(expected_calls)
|
||||
|
||||
assert len(response) == len(paths), "%i paths processed but got %i responses" % (len(paths), len(response))
|
||||
|
||||
for p in paths:
|
||||
r = response.pop(0)
|
||||
ins_p = r.pop('_path')
|
||||
assert p == ins_p, "expected '_path=%s' field was not found in response, got %r" % (p, ins_p)
|
||||
+232
@@ -0,0 +1,232 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright (c) 2022 Brian Scholer (@briantist)
|
||||
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
from __future__ import (absolute_import, division, print_function)
|
||||
__metaclass__ = type
|
||||
|
||||
import pytest
|
||||
|
||||
from ansible.plugins.loader import lookup_loader
|
||||
from ansible.errors import AnsibleError
|
||||
|
||||
from ansible_collections.community.hashi_vault.tests.unit.compat import mock
|
||||
|
||||
from ansible_collections.community.hashi_vault.plugins.plugin_utils._hashi_vault_lookup_base import HashiVaultLookupBase
|
||||
|
||||
from .....plugins.lookup import vault_token_create
|
||||
from .....plugins.module_utils._hashi_vault_common import HashiVaultValueError
|
||||
|
||||
|
||||
pytest.importorskip('hvac')
|
||||
|
||||
|
||||
pytestmark = pytest.mark.usefixtures(
|
||||
'patch_authenticator',
|
||||
'patch_get_vault_client',
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def vault_token_create_lookup():
|
||||
return lookup_loader.get('community.hashi_vault.vault_token_create')
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def pass_thru_options():
|
||||
return {
|
||||
'no_parent': True,
|
||||
'no_default_policy': True,
|
||||
'policies': ['a', 'b'],
|
||||
'id': 'tokenid',
|
||||
'role_name': 'role',
|
||||
'meta': {'a': 'valA', 'b': 'valB'},
|
||||
'renewable': True,
|
||||
'ttl': '1h',
|
||||
'type': 'batch',
|
||||
'explicit_max_ttl': '2h',
|
||||
'display_name': 'kiminonamae',
|
||||
'num_uses': 9,
|
||||
'period': '8h',
|
||||
'entity_alias': 'alias',
|
||||
'wrap_ttl': '60s',
|
||||
}
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def orphan_option_translation():
|
||||
return {
|
||||
'id': 'token_id',
|
||||
'role_name': 'role',
|
||||
'type': 'token_type',
|
||||
}
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def token_create_response(fixture_loader):
|
||||
return fixture_loader('token_create_response.json')
|
||||
|
||||
|
||||
class TestVaultTokenCreateLookup(object):
|
||||
|
||||
def test_vault_token_create_is_lookup_base(self, vault_token_create_lookup):
|
||||
assert issubclass(type(vault_token_create_lookup), HashiVaultLookupBase)
|
||||
|
||||
def test_vault_token_create_no_hvac(self, vault_token_create_lookup, minimal_vars):
|
||||
with mock.patch.object(vault_token_create, 'HVAC_IMPORT_ERROR', new=ImportError()):
|
||||
with pytest.raises(AnsibleError, match=r"This plugin requires the 'hvac' Python library"):
|
||||
vault_token_create_lookup.run(terms='fake', variables=minimal_vars)
|
||||
|
||||
@pytest.mark.parametrize('exc', [HashiVaultValueError('throwaway msg'), NotImplementedError('throwaway msg')])
|
||||
def test_vault_token_create_authentication_error(self, vault_token_create_lookup, minimal_vars, authenticator, exc):
|
||||
authenticator.authenticate.side_effect = exc
|
||||
|
||||
with pytest.raises(AnsibleError, match=r'throwaway msg'):
|
||||
vault_token_create_lookup.run(terms='fake', variables=minimal_vars)
|
||||
|
||||
@pytest.mark.parametrize('exc', [HashiVaultValueError('throwaway msg'), NotImplementedError('throwaway msg')])
|
||||
def test_vault_token_create_auth_validation_error(self, vault_token_create_lookup, minimal_vars, authenticator, exc):
|
||||
authenticator.validate.side_effect = exc
|
||||
|
||||
with pytest.raises(AnsibleError, match=r'throwaway msg'):
|
||||
vault_token_create_lookup.run(terms='fake', variables=minimal_vars)
|
||||
|
||||
def test_vault_token_create_extra_terms(self, vault_token_create_lookup, authenticator, minimal_vars):
|
||||
with mock.patch('ansible_collections.community.hashi_vault.plugins.lookup.vault_token_create.display.warning') as warning:
|
||||
with mock.patch.object(vault_token_create_lookup, 'authenticator', new=authenticator):
|
||||
with mock.patch.object(vault_token_create_lookup.helper, 'get_vault_client'):
|
||||
vault_token_create_lookup.run(terms=['', ''], variables=minimal_vars)
|
||||
warning.assert_called_once_with("Supplied term strings will be ignored. This lookup does not use term strings.")
|
||||
|
||||
def test_vault_token_create_passthru_options_expected(self, vault_token_create_lookup, pass_thru_options):
|
||||
# designed to catch the case where new passthru options differ between tests and lookup
|
||||
|
||||
lookup_set = set(vault_token_create_lookup.PASS_THRU_OPTION_NAMES)
|
||||
test_set = set(pass_thru_options.keys())
|
||||
|
||||
assert sorted(vault_token_create_lookup.PASS_THRU_OPTION_NAMES) == sorted(pass_thru_options.keys()), (
|
||||
"Passthru options in lookup do not match options in test: %r" % (
|
||||
list(lookup_set ^ test_set)
|
||||
)
|
||||
)
|
||||
|
||||
def test_vault_token_create_orphan_options_expected(self, vault_token_create_lookup, orphan_option_translation, pass_thru_options):
|
||||
# designed to catch the case where new orphan translations differ between tests and lookup
|
||||
# and that all listed translations are present in passthru options
|
||||
|
||||
lookup_set = set(vault_token_create_lookup.ORPHAN_OPTION_TRANSLATION.items())
|
||||
test_set = set(orphan_option_translation.items())
|
||||
|
||||
lookup_key_set = set(vault_token_create_lookup.ORPHAN_OPTION_TRANSLATION.keys())
|
||||
pass_thru_key_set = set(pass_thru_options.keys())
|
||||
|
||||
assert lookup_set == test_set, (
|
||||
"Orphan options in lookup do not match orphan options in test:\nlookup: %r\ntest: %r" % (
|
||||
dict(lookup_set - test_set),
|
||||
dict(test_set - lookup_set),
|
||||
)
|
||||
)
|
||||
assert vault_token_create_lookup.ORPHAN_OPTION_TRANSLATION.keys() <= pass_thru_options.keys(), (
|
||||
"Orphan option translation keys must exist in passthru options: %r" % (
|
||||
list(lookup_key_set - pass_thru_key_set),
|
||||
)
|
||||
)
|
||||
|
||||
def test_vault_token_create_passthru_options(self, vault_token_create_lookup, authenticator, minimal_vars, pass_thru_options, token_create_response):
|
||||
|
||||
client = mock.MagicMock()
|
||||
client.auth.token.create.return_value = token_create_response
|
||||
|
||||
with mock.patch.object(vault_token_create_lookup, 'authenticator', new=authenticator):
|
||||
with mock.patch.object(vault_token_create_lookup.helper, 'get_vault_client', return_value=client):
|
||||
result = vault_token_create_lookup.run(terms=[], variables=minimal_vars, **pass_thru_options)
|
||||
|
||||
client.create_token.assert_not_called()
|
||||
client.auth.token.create.assert_called_once()
|
||||
|
||||
assert result[0] == token_create_response, (
|
||||
"lookup result did not match expected result:\nlookup: %r\nexpected: %r" % (result, token_create_response)
|
||||
)
|
||||
|
||||
assert pass_thru_options.items() <= client.auth.token.create.call_args.kwargs.items()
|
||||
|
||||
def test_vault_token_create_orphan_options(
|
||||
self, vault_token_create_lookup, authenticator, minimal_vars, pass_thru_options, orphan_option_translation, token_create_response
|
||||
):
|
||||
|
||||
client = mock.MagicMock()
|
||||
client.auth.token.create_orphan.return_value = token_create_response
|
||||
|
||||
with mock.patch.object(vault_token_create_lookup, 'authenticator', new=authenticator):
|
||||
with mock.patch.object(vault_token_create_lookup.helper, 'get_vault_client', return_value=client):
|
||||
result = vault_token_create_lookup.run(terms=[], variables=minimal_vars, orphan=True, **pass_thru_options)
|
||||
|
||||
client.auth.token.create.assert_not_called()
|
||||
client.auth.token.create_orphan.assert_called_once()
|
||||
client.create_token.assert_not_called()
|
||||
|
||||
assert result[0] == token_create_response, (
|
||||
"lookup result did not match expected result:\nlookup: %r\nexpected: %r" % (result, token_create_response)
|
||||
)
|
||||
|
||||
call_kwargs = client.auth.token.create_orphan.call_args.kwargs
|
||||
|
||||
for name, orphan in orphan_option_translation.items():
|
||||
assert name not in call_kwargs, (
|
||||
"'%s' was found in call to orphan method, should be '%s'" % (name, orphan)
|
||||
)
|
||||
assert orphan in call_kwargs, (
|
||||
"'%s' (from '%s') was not found in call to orphan method" % (orphan, name)
|
||||
)
|
||||
assert call_kwargs[orphan] == pass_thru_options.get(name), (
|
||||
"Expected orphan param '%s' not found or value did not match:\nvalue: %r\nexpected: %r" % (
|
||||
orphan,
|
||||
call_kwargs.get(orphan),
|
||||
pass_thru_options.get(name),
|
||||
)
|
||||
)
|
||||
|
||||
def test_vault_token_create_orphan_fallback(self, vault_token_create_lookup, authenticator, minimal_vars, pass_thru_options, token_create_response):
|
||||
client = mock.MagicMock()
|
||||
client.create_token.return_value = token_create_response
|
||||
client.auth.token.create_orphan.side_effect = AttributeError
|
||||
|
||||
with mock.patch.object(vault_token_create_lookup, 'authenticator', new=authenticator):
|
||||
with mock.patch.object(vault_token_create_lookup.helper, 'get_vault_client', return_value=client):
|
||||
result = vault_token_create_lookup.run(terms=[], variables=minimal_vars, orphan=True, **pass_thru_options)
|
||||
|
||||
client.auth.token.create_orphan.assert_called_once()
|
||||
client.create_token.assert_called_once()
|
||||
|
||||
assert result[0] == token_create_response, (
|
||||
"lookup result did not match expected result:\nlookup: %r\nexpected: %r" % (result, token_create_response)
|
||||
)
|
||||
|
||||
def test_vault_token_create_exception_handling_standard(self, vault_token_create_lookup, authenticator, minimal_vars, pass_thru_options):
|
||||
client = mock.MagicMock()
|
||||
client.auth.token.create.side_effect = Exception('side_effect')
|
||||
|
||||
with mock.patch.object(vault_token_create_lookup, 'authenticator', new=authenticator):
|
||||
with mock.patch.object(vault_token_create_lookup.helper, 'get_vault_client', return_value=client):
|
||||
with pytest.raises(AnsibleError, match=r'^side_effect$'):
|
||||
vault_token_create_lookup.run(terms=[], variables=minimal_vars, **pass_thru_options)
|
||||
|
||||
def test_vault_token_create_exception_handling_orphan(self, vault_token_create_lookup, authenticator, minimal_vars, pass_thru_options):
|
||||
client = mock.MagicMock()
|
||||
client.auth.token.create_orphan.side_effect = Exception('side_effect')
|
||||
|
||||
with mock.patch.object(vault_token_create_lookup, 'authenticator', new=authenticator):
|
||||
with mock.patch.object(vault_token_create_lookup.helper, 'get_vault_client', return_value=client):
|
||||
with pytest.raises(AnsibleError, match=r'^side_effect$'):
|
||||
vault_token_create_lookup.run(terms=[], variables=minimal_vars, orphan=True, **pass_thru_options)
|
||||
|
||||
def test_vault_token_create_exception_handling_orphan_fallback(self, vault_token_create_lookup, authenticator, minimal_vars, pass_thru_options):
|
||||
client = mock.MagicMock()
|
||||
client.create_token.side_effect = Exception('side_effect')
|
||||
client.auth.token.create_orphan.side_effect = AttributeError
|
||||
|
||||
with mock.patch.object(vault_token_create_lookup, 'authenticator', new=authenticator):
|
||||
with mock.patch.object(vault_token_create_lookup.helper, 'get_vault_client', return_value=client):
|
||||
with pytest.raises(AnsibleError, match=r'^side_effect$'):
|
||||
vault_token_create_lookup.run(terms=[], variables=minimal_vars, orphan=True, **pass_thru_options)
|
||||
+166
@@ -0,0 +1,166 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright (c) 2022 Brian Scholer (@briantist)
|
||||
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
from __future__ import (absolute_import, division, print_function)
|
||||
__metaclass__ = type
|
||||
|
||||
import pytest
|
||||
|
||||
from ansible.plugins.loader import lookup_loader
|
||||
from ansible.errors import AnsibleError
|
||||
|
||||
from ...compat import mock
|
||||
|
||||
from .....plugins.plugin_utils._hashi_vault_lookup_base import HashiVaultLookupBase
|
||||
from .....plugins.module_utils._hashi_vault_common import HashiVaultValueError
|
||||
|
||||
from .....plugins.lookup import vault_write
|
||||
|
||||
|
||||
hvac = pytest.importorskip('hvac')
|
||||
|
||||
|
||||
pytestmark = pytest.mark.usefixtures(
|
||||
'patch_authenticator',
|
||||
'patch_get_vault_client',
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def vault_write_lookup():
|
||||
return lookup_loader.get('community.hashi_vault.vault_write')
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def approle_secret_id_write_response(fixture_loader):
|
||||
return fixture_loader('approle_secret_id_write_response.json')
|
||||
|
||||
|
||||
class TestVaultWriteLookup(object):
|
||||
|
||||
def test_vault_write_is_lookup_base(self, vault_write_lookup):
|
||||
assert issubclass(type(vault_write_lookup), HashiVaultLookupBase)
|
||||
|
||||
def test_vault_write_no_hvac(self, vault_write_lookup, minimal_vars):
|
||||
with mock.patch.object(vault_write, 'HVAC_IMPORT_ERROR', new=ImportError()):
|
||||
with pytest.raises(AnsibleError, match=r"This plugin requires the 'hvac' Python library"):
|
||||
vault_write_lookup.run(terms='fake', variables=minimal_vars)
|
||||
|
||||
@pytest.mark.parametrize('exc', [HashiVaultValueError('throwaway msg'), NotImplementedError('throwaway msg')])
|
||||
def test_vault_write_authentication_error(self, vault_write_lookup, minimal_vars, authenticator, exc):
|
||||
authenticator.authenticate.side_effect = exc
|
||||
|
||||
with pytest.raises(AnsibleError, match=r'throwaway msg'):
|
||||
vault_write_lookup.run(terms='fake', variables=minimal_vars)
|
||||
|
||||
@pytest.mark.parametrize('exc', [HashiVaultValueError('throwaway msg'), NotImplementedError('throwaway msg')])
|
||||
def test_vault_write_auth_validation_error(self, vault_write_lookup, minimal_vars, authenticator, exc):
|
||||
authenticator.validate.side_effect = exc
|
||||
|
||||
with pytest.raises(AnsibleError, match=r'throwaway msg'):
|
||||
vault_write_lookup.run(terms='fake', variables=minimal_vars)
|
||||
|
||||
@pytest.mark.parametrize('paths', [['fake1'], ['fake2', 'fake3']])
|
||||
@pytest.mark.parametrize('data', [{}, {'a': 1, 'b': 'two'}])
|
||||
@pytest.mark.parametrize('wrap_ttl', [None, '5m'])
|
||||
def test_vault_write_return_data(self, vault_write_lookup, minimal_vars, approle_secret_id_write_response, vault_client, paths, data, wrap_ttl):
|
||||
client = vault_client
|
||||
|
||||
expected_calls = [mock.call(path=p, wrap_ttl=wrap_ttl, data=data) for p in paths]
|
||||
|
||||
def _fake_write(path, wrap_ttl, data=None):
|
||||
r = approle_secret_id_write_response.copy()
|
||||
r.update({'path': path})
|
||||
return r
|
||||
|
||||
client.write_data = mock.Mock(wraps=_fake_write)
|
||||
|
||||
response = vault_write_lookup.run(terms=paths, variables=minimal_vars, wrap_ttl=wrap_ttl, data=data)
|
||||
|
||||
client.write_data.assert_has_calls(expected_calls)
|
||||
|
||||
assert len(response) == len(paths), "%i paths processed but got %i responses" % (len(paths), len(response))
|
||||
|
||||
for p in paths:
|
||||
r = response.pop(0)
|
||||
m = r.pop('path')
|
||||
assert p == m, "expected 'path=%s' field was not found in response, got %r" % (p, m)
|
||||
assert r == approle_secret_id_write_response, (
|
||||
"remaining response did not match expected\nresponse: %r\nexpected: %r" % (r, approle_secret_id_write_response)
|
||||
)
|
||||
|
||||
def test_vault_write_empty_response(self, vault_write_lookup, minimal_vars, vault_client, requests_unparseable_response):
|
||||
client = vault_client
|
||||
|
||||
requests_unparseable_response.status_code = 204
|
||||
|
||||
client.write_data.return_value = requests_unparseable_response
|
||||
|
||||
response = vault_write_lookup.run(terms=['fake'], variables=minimal_vars)
|
||||
|
||||
assert response[0] == {}
|
||||
|
||||
def test_vault_write_unparseable_response(self, vault_write_lookup, minimal_vars, vault_client, requests_unparseable_response):
|
||||
client = vault_client
|
||||
|
||||
requests_unparseable_response.status_code = 200
|
||||
requests_unparseable_response.content = '﷽'
|
||||
|
||||
client.write_data.return_value = requests_unparseable_response
|
||||
|
||||
with mock.patch('ansible_collections.community.hashi_vault.plugins.lookup.vault_write.display.warning') as warning:
|
||||
response = vault_write_lookup.run(terms=['fake'], variables=minimal_vars)
|
||||
warning.assert_called_once_with('Vault returned status code 200 and an unparsable body.')
|
||||
|
||||
assert response[0] == '﷽'
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
'exc',
|
||||
[
|
||||
(hvac.exceptions.Forbidden, r'^Forbidden: Permission Denied to path'),
|
||||
(hvac.exceptions.InvalidPath, r"^The path '[^']+' doesn't seem to exist"),
|
||||
(hvac.exceptions.InternalServerError, r'^Internal Server Error:'),
|
||||
]
|
||||
)
|
||||
def test_vault_write_exceptions(self, vault_write_lookup, minimal_vars, vault_client, exc):
|
||||
client = vault_client
|
||||
|
||||
client.write_data.side_effect = exc[0]
|
||||
|
||||
with pytest.raises(AnsibleError, match=exc[1]):
|
||||
vault_write_lookup.run(terms=['fake'], variables=minimal_vars)
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
'data',
|
||||
[
|
||||
{"path": mock.sentinel.path_value},
|
||||
{"wrap_ttl": mock.sentinel.wrap_ttl_value},
|
||||
{"path": mock.sentinel.data_value, "wrap_ttl": mock.sentinel.write_ttl_value},
|
||||
],
|
||||
)
|
||||
def test_vault_write_data_fallback_bad_params(self, vault_write_lookup, minimal_vars, vault_client, data):
|
||||
client = vault_client
|
||||
client.mock_add_spec(['write'])
|
||||
|
||||
with pytest.raises(AnsibleError, match=r"To use 'path' or 'wrap_ttl' as data keys, use hvac >= 1\.2"):
|
||||
vault_write_lookup.run(terms=['fake'], variables=minimal_vars, data=data)
|
||||
|
||||
client.write.assert_not_called()
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
'data',
|
||||
[
|
||||
{"item1": mock.sentinel.item1_value},
|
||||
{"item2": mock.sentinel.item2_value},
|
||||
{"item1": mock.sentinel.item1_value, "item2": mock.sentinel.item2_value},
|
||||
],
|
||||
)
|
||||
def test_vault_write_data_fallback_write(self, vault_write_lookup, minimal_vars, vault_client, data):
|
||||
client = vault_client
|
||||
client.mock_add_spec(['write'])
|
||||
|
||||
vault_write_lookup.run(terms=['fake'], variables=minimal_vars, data=data)
|
||||
|
||||
client.write.assert_called_once_with(path='fake', wrap_ttl=None, **data)
|
||||
+84
@@ -0,0 +1,84 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright (c) 2021 Brian Scholer (@briantist)
|
||||
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
from __future__ import (absolute_import, division, print_function)
|
||||
__metaclass__ = type
|
||||
|
||||
import pytest
|
||||
import contextlib
|
||||
|
||||
try:
|
||||
import hvac
|
||||
except ImportError:
|
||||
# python 2.6, which isn't supported anyway
|
||||
pass
|
||||
|
||||
from ansible_collections.community.hashi_vault.tests.unit.compat import mock
|
||||
|
||||
from ansible_collections.community.hashi_vault.plugins.module_utils._hashi_vault_common import (
|
||||
HashiVaultAuthMethodBase,
|
||||
HashiVaultOptionAdapter,
|
||||
)
|
||||
|
||||
|
||||
class HashiVaultAuthMethodFake(HashiVaultAuthMethodBase):
|
||||
NAME = 'fake'
|
||||
OPTIONS = []
|
||||
|
||||
def __init__(self, option_adapter, warning_callback, deprecate_callback):
|
||||
super(HashiVaultAuthMethodFake, self).__init__(option_adapter, warning_callback, deprecate_callback)
|
||||
|
||||
validate = mock.MagicMock()
|
||||
authenticate = mock.MagicMock()
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def option_dict():
|
||||
return {'auth_method': 'fake'}
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def adapter(option_dict):
|
||||
return HashiVaultOptionAdapter.from_dict(option_dict)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def fake_auth_class(adapter, warner, deprecator):
|
||||
return HashiVaultAuthMethodFake(adapter, warner, deprecator)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def client():
|
||||
return hvac.Client()
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def warner():
|
||||
return mock.MagicMock()
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def deprecator():
|
||||
return mock.MagicMock()
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_import_error():
|
||||
@contextlib.contextmanager
|
||||
def _mock_import_error(*names):
|
||||
import builtins
|
||||
|
||||
real_import = builtins.__import__
|
||||
|
||||
def _fake_importer(name, *args, **kwargs):
|
||||
if name in names:
|
||||
raise ImportError
|
||||
|
||||
return real_import(name, *args, **kwargs)
|
||||
|
||||
with mock.patch.object(builtins, '__import__', side_effect=_fake_importer):
|
||||
yield
|
||||
|
||||
return _mock_import_error
|
||||
+99
@@ -0,0 +1,99 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright (c) 2021 Brian Scholer (@briantist)
|
||||
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
from __future__ import (absolute_import, division, print_function)
|
||||
__metaclass__ = type
|
||||
|
||||
import pytest
|
||||
|
||||
from ansible_collections.community.hashi_vault.tests.unit.compat import mock
|
||||
|
||||
from ansible_collections.community.hashi_vault.plugins.module_utils._auth_method_approle import (
|
||||
HashiVaultAuthMethodApprole,
|
||||
)
|
||||
|
||||
from ansible_collections.community.hashi_vault.plugins.module_utils._hashi_vault_common import (
|
||||
HashiVaultAuthMethodBase,
|
||||
HashiVaultValueError,
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def option_dict():
|
||||
return {
|
||||
'auth_method': 'approle',
|
||||
'secret_id': None,
|
||||
'role_id': None,
|
||||
'mount_point': None,
|
||||
}
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def secret_id():
|
||||
return 'opaque'
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def role_id():
|
||||
return 'fake-role'
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def auth_approle(adapter, warner, deprecator):
|
||||
return HashiVaultAuthMethodApprole(adapter, warner, deprecator)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def approle_login_response(fixture_loader):
|
||||
return fixture_loader('approle_login_response.json')
|
||||
|
||||
|
||||
class TestAuthApprole(object):
|
||||
|
||||
def test_auth_approle_is_auth_method_base(self, auth_approle):
|
||||
assert isinstance(auth_approle, HashiVaultAuthMethodApprole)
|
||||
assert issubclass(HashiVaultAuthMethodApprole, HashiVaultAuthMethodBase)
|
||||
|
||||
def test_auth_approle_validate_direct(self, auth_approle, adapter, role_id):
|
||||
adapter.set_option('role_id', role_id)
|
||||
|
||||
auth_approle.validate()
|
||||
|
||||
@pytest.mark.parametrize('opt_patch', [
|
||||
{},
|
||||
{'secret_id': 'secret_id-only'},
|
||||
])
|
||||
def test_auth_approle_validate_xfailures(self, auth_approle, adapter, opt_patch):
|
||||
adapter.set_options(**opt_patch)
|
||||
|
||||
with pytest.raises(HashiVaultValueError, match=r'Authentication method approle requires options .*? to be set, but these are missing:'):
|
||||
auth_approle.validate()
|
||||
|
||||
@pytest.mark.parametrize('use_token', [True, False], ids=lambda x: 'use_token=%s' % x)
|
||||
@pytest.mark.parametrize('mount_point', [None, 'other'], ids=lambda x: 'mount_point=%s' % x)
|
||||
def test_auth_approle_authenticate(self, auth_approle, client, adapter, secret_id, role_id, mount_point, use_token, approle_login_response):
|
||||
adapter.set_option('secret_id', secret_id)
|
||||
adapter.set_option('role_id', role_id)
|
||||
adapter.set_option('mount_point', mount_point)
|
||||
|
||||
expected_login_params = {
|
||||
'secret_id': secret_id,
|
||||
'role_id': role_id,
|
||||
'use_token': use_token,
|
||||
}
|
||||
if mount_point:
|
||||
expected_login_params['mount_point'] = mount_point
|
||||
|
||||
def _set_client_token(*args, **kwargs):
|
||||
if kwargs['use_token']:
|
||||
client.token = approle_login_response['auth']['client_token']
|
||||
return approle_login_response
|
||||
|
||||
with mock.patch.object(client.auth.approle, 'login', side_effect=_set_client_token) as approle_login:
|
||||
response = auth_approle.authenticate(client, use_token=use_token)
|
||||
approle_login.assert_called_once_with(**expected_login_params)
|
||||
|
||||
assert response['auth']['client_token'] == approle_login_response['auth']['client_token']
|
||||
assert (client.token == approle_login_response['auth']['client_token']) is use_token
|
||||
+193
@@ -0,0 +1,193 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright (c) 2021 Brian Scholer (@briantist)
|
||||
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
from __future__ import (absolute_import, division, print_function)
|
||||
__metaclass__ = type
|
||||
|
||||
import pytest
|
||||
|
||||
from ansible_collections.community.hashi_vault.tests.unit.compat import mock
|
||||
|
||||
from ansible_collections.community.hashi_vault.plugins.module_utils._auth_method_aws_iam import (
|
||||
HashiVaultAuthMethodAwsIam,
|
||||
)
|
||||
|
||||
from ansible_collections.community.hashi_vault.plugins.module_utils._hashi_vault_common import (
|
||||
HashiVaultAuthMethodBase,
|
||||
HashiVaultValueError,
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def option_dict():
|
||||
return {
|
||||
'auth_method': 'aws_iam',
|
||||
'aws_access_key': None,
|
||||
'aws_secret_key': None,
|
||||
'aws_profile': None,
|
||||
'aws_security_token': None,
|
||||
'region': None,
|
||||
'aws_iam_server_id': None,
|
||||
'role_id': None,
|
||||
'mount_point': None,
|
||||
}
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def aws_access_key():
|
||||
return 'access-key'
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def aws_secret_key():
|
||||
return 'secret-key'
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def aws_session_token():
|
||||
return 'session-token'
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def auth_aws_iam(adapter, warner, deprecator):
|
||||
return HashiVaultAuthMethodAwsIam(adapter, warner, deprecator)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def aws_iam_login_response(fixture_loader):
|
||||
return fixture_loader('aws_iam_login_response.json')
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def boto_mocks(aws_access_key, aws_secret_key, aws_session_token):
|
||||
class botocore_profile_not_found(Exception):
|
||||
pass
|
||||
|
||||
credentials = mock.MagicMock(access_key=aws_access_key, secret_key=aws_secret_key, token=aws_session_token)
|
||||
mock_session = mock.MagicMock(get_credentials=mock.MagicMock(return_value=credentials))
|
||||
|
||||
def _Session(profile_name):
|
||||
if profile_name == 'missing_profile':
|
||||
raise botocore_profile_not_found
|
||||
|
||||
return mock_session
|
||||
|
||||
boto3 = mock.MagicMock()
|
||||
boto3.session.Session = mock.MagicMock(side_effect=_Session)
|
||||
|
||||
botocore = mock.MagicMock()
|
||||
botocore.exceptions.ProfileNotFound = botocore_profile_not_found
|
||||
|
||||
return mock.MagicMock(
|
||||
botocore=botocore,
|
||||
boto3=boto3,
|
||||
session=mock_session,
|
||||
credentials=credentials
|
||||
)
|
||||
|
||||
|
||||
class TestAuthAwsIam(object):
|
||||
|
||||
def test_auth_aws_iam_is_auth_method_base(self, auth_aws_iam):
|
||||
assert isinstance(auth_aws_iam, HashiVaultAuthMethodAwsIam)
|
||||
assert issubclass(HashiVaultAuthMethodAwsIam, HashiVaultAuthMethodBase)
|
||||
|
||||
@pytest.mark.parametrize('aws_security_token', [None, 'session-token'], ids=lambda x: 'aws_security_token=%s' % x)
|
||||
@pytest.mark.parametrize('region', [None, 'ap-northeast-1'], ids=lambda x: 'region=%s' % x)
|
||||
@pytest.mark.parametrize('aws_iam_server_id', [None, 'server-id'], ids=lambda x: 'aws_iam_server_id=%s' % x)
|
||||
@pytest.mark.parametrize('role_id', [None, 'vault-role'], ids=lambda x: 'role_id=%s' % x)
|
||||
@pytest.mark.parametrize('mount_point', [None, 'other'], ids=lambda x: 'mount_point=%s' % x)
|
||||
def test_auth_aws_iam_validate(
|
||||
self, auth_aws_iam, adapter, aws_access_key, aws_secret_key, aws_security_token,
|
||||
region, aws_iam_server_id, role_id, mount_point
|
||||
):
|
||||
adapter.set_options(
|
||||
aws_access_key=aws_access_key, aws_secret_key=aws_secret_key, aws_security_token=aws_security_token,
|
||||
region=region, aws_iam_server_id=aws_iam_server_id, role_id=role_id, mount_point=mount_point
|
||||
)
|
||||
|
||||
auth_aws_iam.validate()
|
||||
|
||||
login_params = auth_aws_iam._auth_aws_iam_login_params
|
||||
|
||||
assert login_params['access_key'] == aws_access_key
|
||||
assert login_params['secret_key'] == aws_secret_key
|
||||
|
||||
assert (aws_security_token is None and 'session_token' not in login_params) or login_params['session_token'] == aws_security_token
|
||||
assert (mount_point is None and 'mount_point' not in login_params) or login_params['mount_point'] == mount_point
|
||||
assert (role_id is None and 'role' not in login_params) or login_params['role'] == role_id
|
||||
assert (region is None and 'region' not in login_params) or login_params['region'] == region
|
||||
assert (aws_iam_server_id is None and 'header_value' not in login_params) or login_params['header_value'] == aws_iam_server_id
|
||||
|
||||
@pytest.mark.parametrize('use_token', [True, False], ids=lambda x: 'use_token=%s' % x)
|
||||
@pytest.mark.parametrize('mount_point', [None, 'other'], ids=lambda x: 'mount_point=%s' % x)
|
||||
@pytest.mark.parametrize('aws_security_token', [None, 'session-token'], ids=lambda x: 'aws_security_token=%s' % x)
|
||||
@pytest.mark.parametrize('region', [None, 'ap-northeast-1'], ids=lambda x: 'region=%s' % x)
|
||||
@pytest.mark.parametrize('aws_iam_server_id', [None, 'server-id'], ids=lambda x: 'aws_iam_server_id=%s' % x)
|
||||
@pytest.mark.parametrize('role_id', [None, 'vault-role'], ids=lambda x: 'role_id=%s' % x)
|
||||
def test_auth_aws_iam_authenticate(
|
||||
self, auth_aws_iam, client, adapter, aws_access_key, aws_secret_key, aws_security_token,
|
||||
region, aws_iam_server_id, role_id, mount_point, use_token, aws_iam_login_response
|
||||
):
|
||||
adapter.set_options(
|
||||
aws_access_key=aws_access_key, aws_secret_key=aws_secret_key, aws_security_token=aws_security_token,
|
||||
region=region, aws_iam_server_id=aws_iam_server_id, role_id=role_id, mount_point=mount_point
|
||||
)
|
||||
|
||||
auth_aws_iam.validate()
|
||||
|
||||
expected_login_params = auth_aws_iam._auth_aws_iam_login_params.copy()
|
||||
|
||||
with mock.patch.object(client.auth.aws, 'iam_login', return_value=aws_iam_login_response) as aws_iam_login:
|
||||
response = auth_aws_iam.authenticate(client, use_token=use_token)
|
||||
aws_iam_login.assert_called_once_with(use_token=use_token, **expected_login_params)
|
||||
|
||||
assert response['auth']['client_token'] == aws_iam_login_response['auth']['client_token']
|
||||
|
||||
def test_auth_aws_iam_validate_no_creds_no_boto(self, auth_aws_iam, mock_import_error):
|
||||
with mock_import_error('botocore', 'boto3'):
|
||||
with pytest.raises(HashiVaultValueError, match=r'boto3 is required for loading a profile or IAM role credentials'):
|
||||
auth_aws_iam.validate()
|
||||
|
||||
@pytest.mark.parametrize('profile', ['my_aws_profile', None])
|
||||
def test_auth_aws_iam_validate_inferred_creds(self, auth_aws_iam, boto_mocks, adapter, profile, aws_access_key, aws_secret_key, aws_session_token):
|
||||
adapter.set_option('aws_profile', profile)
|
||||
|
||||
botocore = boto_mocks.botocore
|
||||
boto3 = boto_mocks.boto3
|
||||
|
||||
with mock.patch.dict('sys.modules', {'botocore': botocore, 'boto3': boto3}):
|
||||
auth_aws_iam.validate()
|
||||
|
||||
params = auth_aws_iam._auth_aws_iam_login_params
|
||||
|
||||
boto3.session.Session.assert_called_once_with(profile_name=profile)
|
||||
|
||||
assert params['access_key'] == aws_access_key
|
||||
assert params['secret_key'] == aws_secret_key
|
||||
assert params['session_token'] == aws_session_token
|
||||
|
||||
@pytest.mark.parametrize('profile', ['missing_profile'])
|
||||
def test_auth_aws_iam_validate_missing_profile(self, auth_aws_iam, boto_mocks, adapter, profile):
|
||||
adapter.set_option('aws_profile', profile)
|
||||
|
||||
botocore = boto_mocks.botocore
|
||||
boto3 = boto_mocks.boto3
|
||||
|
||||
with mock.patch.dict('sys.modules', {'botocore': botocore, 'boto3': boto3}):
|
||||
with pytest.raises(HashiVaultValueError, match="The AWS profile '%s' was not found" % profile):
|
||||
auth_aws_iam.validate()
|
||||
|
||||
@pytest.mark.parametrize('profile', ['my_aws_profile', None])
|
||||
def test_auth_aws_iam_validate_no_inferred_creds_found(self, auth_aws_iam, boto_mocks, adapter, profile):
|
||||
adapter.set_option('aws_profile', profile)
|
||||
|
||||
botocore = boto_mocks.botocore
|
||||
boto3 = boto_mocks.boto3
|
||||
|
||||
with mock.patch.dict('sys.modules', {'botocore': botocore, 'boto3': boto3}):
|
||||
with mock.patch.object(boto_mocks.session, 'get_credentials', return_value=None):
|
||||
with pytest.raises(HashiVaultValueError, match=r'No AWS credentials supplied or available'):
|
||||
auth_aws_iam.validate()
|
||||
+224
@@ -0,0 +1,224 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright (c) 2022 Junrui Chen (@jchenship)
|
||||
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
from __future__ import absolute_import, division, print_function
|
||||
__metaclass__ = type
|
||||
|
||||
import pytest
|
||||
|
||||
from ansible_collections.community.hashi_vault.tests.unit.compat import mock
|
||||
|
||||
from ansible_collections.community.hashi_vault.plugins.module_utils._auth_method_azure import (
|
||||
HashiVaultAuthMethodAzure,
|
||||
)
|
||||
|
||||
from ansible_collections.community.hashi_vault.plugins.module_utils._hashi_vault_common import (
|
||||
HashiVaultAuthMethodBase,
|
||||
HashiVaultValueError,
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def option_dict():
|
||||
return {
|
||||
'auth_method': 'azure',
|
||||
'role_id': 'vault-role',
|
||||
'mount_point': None,
|
||||
'jwt': None,
|
||||
'azure_tenant_id': None,
|
||||
'azure_client_id': None,
|
||||
'azure_client_secret': None,
|
||||
'azure_resource': 'https://management.azure.com/',
|
||||
}
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def azure_client_id():
|
||||
return 'client-id'
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def azure_client_secret():
|
||||
return 'client-secret'
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def jwt():
|
||||
return 'jwt-token'
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def auth_azure(adapter, warner, deprecator):
|
||||
return HashiVaultAuthMethodAzure(adapter, warner, deprecator)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def azure_login_response(fixture_loader):
|
||||
return fixture_loader('azure_login_response.json')
|
||||
|
||||
|
||||
class TestAuthAzure(object):
|
||||
def test_auth_azure_is_auth_method_base(self, auth_azure):
|
||||
assert isinstance(auth_azure, HashiVaultAuthMethodAzure)
|
||||
assert issubclass(HashiVaultAuthMethodAzure, HashiVaultAuthMethodBase)
|
||||
|
||||
def test_auth_azure_validate_role_id(self, auth_azure, adapter):
|
||||
adapter.set_options(role_id=None)
|
||||
with pytest.raises(HashiVaultValueError, match=r'^role_id is required for azure authentication\.$'):
|
||||
auth_azure.validate()
|
||||
|
||||
@pytest.mark.parametrize('mount_point', [None, 'other'], ids=lambda x: 'mount_point=%s' % x)
|
||||
@pytest.mark.parametrize('role_id', ['role1', 'role2'], ids=lambda x: 'role_id=%s' % x)
|
||||
@pytest.mark.parametrize('jwt', ['jwt1', 'jwt2'], ids=lambda x: 'jwt=%s' % x)
|
||||
def test_auth_azure_validate_use_jwt(
|
||||
self, auth_azure, adapter, role_id, mount_point, jwt
|
||||
):
|
||||
adapter.set_options(
|
||||
role_id=role_id,
|
||||
mount_point=mount_point,
|
||||
jwt=jwt,
|
||||
)
|
||||
|
||||
auth_azure.validate()
|
||||
|
||||
params = auth_azure._auth_azure_login_params
|
||||
|
||||
assert (mount_point is None and 'mount_point' not in params) or params['mount_point'] == mount_point
|
||||
assert params['role'] == role_id
|
||||
assert params['jwt'] == jwt
|
||||
|
||||
@pytest.mark.parametrize('mount_point', [None, 'other'], ids=lambda x: 'mount_point=%s' % x)
|
||||
@pytest.mark.parametrize('use_token', [True, False], ids=lambda x: 'use_token=%s' % x)
|
||||
def test_auth_azure_authenticate_use_jwt(
|
||||
self,
|
||||
auth_azure,
|
||||
client,
|
||||
adapter,
|
||||
mount_point,
|
||||
jwt,
|
||||
use_token,
|
||||
azure_login_response,
|
||||
):
|
||||
adapter.set_options(
|
||||
mount_point=mount_point,
|
||||
jwt=jwt,
|
||||
)
|
||||
|
||||
auth_azure.validate()
|
||||
|
||||
params = auth_azure._auth_azure_login_params.copy()
|
||||
|
||||
with mock.patch.object(
|
||||
client.auth.azure, 'login', return_value=azure_login_response
|
||||
) as azure_login:
|
||||
response = auth_azure.authenticate(client, use_token=use_token)
|
||||
azure_login.assert_called_once_with(use_token=use_token, **params)
|
||||
|
||||
assert (
|
||||
response['auth']['client_token']
|
||||
== azure_login_response['auth']['client_token']
|
||||
)
|
||||
|
||||
def test_auth_azure_validate_use_identity_no_azure_identity_lib(
|
||||
self, auth_azure, mock_import_error, adapter
|
||||
):
|
||||
adapter.set_options()
|
||||
with mock_import_error('azure.identity'):
|
||||
with pytest.raises(
|
||||
HashiVaultValueError, match=r'azure-identity is required'
|
||||
):
|
||||
auth_azure.validate()
|
||||
|
||||
@pytest.mark.parametrize('azure_tenant_id', ['tenant1', 'tenant2'], ids=lambda x: 'azure_tenant_id=%s' % x)
|
||||
@pytest.mark.parametrize('azure_client_id', ['client1', 'client2'], ids=lambda x: 'azure_client_id=%s' % x)
|
||||
@pytest.mark.parametrize('azure_client_secret', ['secret1', 'secret2'], ids=lambda x: 'azure_client_secret=%s' % x)
|
||||
@pytest.mark.parametrize('jwt', ['jwt1', 'jwt2'], ids=lambda x: 'jwt=%s' % x)
|
||||
def test_auth_azure_validate_use_service_principal(
|
||||
self,
|
||||
auth_azure,
|
||||
adapter,
|
||||
jwt,
|
||||
azure_tenant_id,
|
||||
azure_client_id,
|
||||
azure_client_secret,
|
||||
):
|
||||
adapter.set_options(
|
||||
azure_tenant_id=azure_tenant_id,
|
||||
azure_client_id=azure_client_id,
|
||||
azure_client_secret=azure_client_secret,
|
||||
)
|
||||
|
||||
with mock.patch(
|
||||
'azure.identity.ClientSecretCredential'
|
||||
) as mocked_credential_class:
|
||||
credential = mocked_credential_class.return_value
|
||||
credential.get_token.return_value.token = jwt
|
||||
auth_azure.validate()
|
||||
|
||||
mocked_credential_class.assert_called_once_with(
|
||||
azure_tenant_id, azure_client_id, azure_client_secret
|
||||
)
|
||||
credential.get_token.assert_called_once_with(
|
||||
'https://management.azure.com//.default'
|
||||
)
|
||||
|
||||
params = auth_azure._auth_azure_login_params
|
||||
assert params['jwt'] == jwt
|
||||
|
||||
def test_auth_azure_validate_use_service_principal_no_tenant_id(
|
||||
self, auth_azure, adapter, azure_client_id, azure_client_secret
|
||||
):
|
||||
adapter.set_options(
|
||||
azure_client_id=azure_client_id,
|
||||
azure_client_secret=azure_client_secret,
|
||||
)
|
||||
|
||||
with pytest.raises(HashiVaultValueError, match='azure_tenant_id is required'):
|
||||
auth_azure.validate()
|
||||
|
||||
@pytest.mark.parametrize('azure_client_id', ['client1', 'client2'], ids=lambda x: 'azure_client_id=%s' % x)
|
||||
@pytest.mark.parametrize('jwt', ['jwt1', 'jwt2'], ids=lambda x: 'jwt=%s' % x)
|
||||
def test_auth_azure_validate_use_user_managed_identity(
|
||||
self, auth_azure, adapter, jwt, azure_client_id
|
||||
):
|
||||
adapter.set_options(
|
||||
azure_client_id=azure_client_id,
|
||||
)
|
||||
|
||||
with mock.patch(
|
||||
'azure.identity.ManagedIdentityCredential'
|
||||
) as mocked_credential_class:
|
||||
credential = mocked_credential_class.return_value
|
||||
credential.get_token.return_value.token = jwt
|
||||
auth_azure.validate()
|
||||
|
||||
mocked_credential_class.assert_called_once_with(client_id=azure_client_id)
|
||||
credential.get_token.assert_called_once_with(
|
||||
'https://management.azure.com//.default'
|
||||
)
|
||||
|
||||
params = auth_azure._auth_azure_login_params
|
||||
assert params['jwt'] == jwt
|
||||
|
||||
@pytest.mark.parametrize('jwt', ['jwt1', 'jwt2'], ids=lambda x: 'jwt=%s' % x)
|
||||
def test_auth_azure_validate_use_system_managed_identity(
|
||||
self, auth_azure, adapter, jwt
|
||||
):
|
||||
adapter.set_options()
|
||||
|
||||
with mock.patch(
|
||||
'azure.identity.ManagedIdentityCredential'
|
||||
) as mocked_credential_class:
|
||||
credential = mocked_credential_class.return_value
|
||||
credential.get_token.return_value.token = jwt
|
||||
auth_azure.validate()
|
||||
|
||||
mocked_credential_class.assert_called_once_with()
|
||||
credential.get_token.assert_called_once_with(
|
||||
'https://management.azure.com//.default'
|
||||
)
|
||||
|
||||
params = auth_azure._auth_azure_login_params
|
||||
assert params['jwt'] == jwt
|
||||
+89
@@ -0,0 +1,89 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright (c) 2021 Devon Mar (@devon-mar)
|
||||
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
from __future__ import (absolute_import, division, print_function)
|
||||
__metaclass__ = type
|
||||
|
||||
import pytest
|
||||
|
||||
from ansible_collections.community.hashi_vault.tests.unit.compat import mock
|
||||
|
||||
from ansible_collections.community.hashi_vault.plugins.module_utils._auth_method_cert import (
|
||||
HashiVaultAuthMethodCert,
|
||||
)
|
||||
|
||||
from ansible_collections.community.hashi_vault.plugins.module_utils._hashi_vault_common import (
|
||||
HashiVaultAuthMethodBase,
|
||||
HashiVaultValueError,
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def auth_cert(adapter, warner, deprecator):
|
||||
return HashiVaultAuthMethodCert(adapter, warner, deprecator)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def cert_login_response(fixture_loader):
|
||||
return fixture_loader("cert_login_response.json")
|
||||
|
||||
|
||||
class TestAuthCert(object):
|
||||
|
||||
def test_auth_cert_is_auth_method_base(self, auth_cert):
|
||||
assert isinstance(auth_cert, HashiVaultAuthMethodCert)
|
||||
assert issubclass(HashiVaultAuthMethodCert, HashiVaultAuthMethodBase)
|
||||
|
||||
def test_auth_cert_validate_direct(self, auth_cert, adapter):
|
||||
adapter.set_option("cert_auth_public_key", "/fake/path")
|
||||
adapter.set_option("cert_auth_private_key", "/fake/path")
|
||||
|
||||
auth_cert.validate()
|
||||
|
||||
@pytest.mark.parametrize("opt_patch", [
|
||||
{},
|
||||
{"cert_auth_public_key": ""},
|
||||
{"cert_auth_private_key": ""},
|
||||
{"mount_point": ""}
|
||||
])
|
||||
def test_auth_cert_validate_xfailures(self, auth_cert, adapter, opt_patch):
|
||||
adapter.set_options(**opt_patch)
|
||||
|
||||
with pytest.raises(HashiVaultValueError, match=r"Authentication method cert requires options .*? to be set, but these are missing:"):
|
||||
auth_cert.validate()
|
||||
|
||||
@pytest.mark.parametrize("use_token", [True, False], ids=lambda x: "use_token=%s" % x)
|
||||
@pytest.mark.parametrize("mount_point", [None, "other"], ids=lambda x: "mount_point=%s" % x)
|
||||
@pytest.mark.parametrize("role_id", [None, "cert"], ids=lambda x: "role_id=%s" % x)
|
||||
def test_auth_cert_authenticate(self, auth_cert, client, adapter, mount_point, use_token, role_id,
|
||||
cert_login_response):
|
||||
adapter.set_option("cert_auth_public_key", "/fake/path")
|
||||
adapter.set_option("cert_auth_private_key", "/fake/path")
|
||||
adapter.set_option("role_id", role_id)
|
||||
adapter.set_option("mount_point", mount_point)
|
||||
|
||||
expected_login_params = {
|
||||
"cert_pem": "/fake/path",
|
||||
"key_pem": "/fake/path",
|
||||
"use_token": use_token,
|
||||
}
|
||||
|
||||
if role_id:
|
||||
expected_login_params["name"] = role_id
|
||||
|
||||
if mount_point:
|
||||
expected_login_params["mount_point"] = mount_point
|
||||
|
||||
def _set_client_token(*args, **kwargs):
|
||||
if kwargs['use_token']:
|
||||
client.token = cert_login_response['auth']['client_token']
|
||||
return cert_login_response
|
||||
|
||||
with mock.patch.object(client.auth.cert, "login", side_effect=_set_client_token) as cert_login:
|
||||
response = auth_cert.authenticate(client, use_token=use_token)
|
||||
cert_login.assert_called_once_with(**expected_login_params)
|
||||
|
||||
assert response["auth"]["client_token"] == cert_login_response["auth"]["client_token"]
|
||||
assert (client.token == cert_login_response["auth"]["client_token"]) is use_token
|
||||
+95
@@ -0,0 +1,95 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright (c) 2021 Brian Scholer (@briantist)
|
||||
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
from __future__ import (absolute_import, division, print_function)
|
||||
__metaclass__ = type
|
||||
|
||||
import pytest
|
||||
|
||||
from ansible_collections.community.hashi_vault.tests.unit.compat import mock
|
||||
|
||||
from ansible_collections.community.hashi_vault.plugins.module_utils._auth_method_jwt import (
|
||||
HashiVaultAuthMethodJwt,
|
||||
)
|
||||
|
||||
from ansible_collections.community.hashi_vault.plugins.module_utils._hashi_vault_common import (
|
||||
HashiVaultAuthMethodBase,
|
||||
HashiVaultValueError,
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def option_dict():
|
||||
return {
|
||||
'auth_method': 'jwt',
|
||||
'jwt': None,
|
||||
'role_id': None,
|
||||
'mount_point': None,
|
||||
}
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def jwt():
|
||||
return 'opaque'
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def role_id():
|
||||
return 'fake-role'
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def auth_jwt(adapter, warner, deprecator):
|
||||
return HashiVaultAuthMethodJwt(adapter, warner, deprecator)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def jwt_login_response(fixture_loader):
|
||||
return fixture_loader('jwt_login_response.json')
|
||||
|
||||
|
||||
class TestAuthJwt(object):
|
||||
|
||||
def test_auth_jwt_is_auth_method_base(self, auth_jwt):
|
||||
assert isinstance(auth_jwt, HashiVaultAuthMethodJwt)
|
||||
assert issubclass(HashiVaultAuthMethodJwt, HashiVaultAuthMethodBase)
|
||||
|
||||
def test_auth_jwt_validate_direct(self, auth_jwt, adapter, jwt, role_id):
|
||||
adapter.set_option('jwt', jwt)
|
||||
adapter.set_option('role_id', role_id)
|
||||
|
||||
auth_jwt.validate()
|
||||
|
||||
@pytest.mark.parametrize('opt_patch', [
|
||||
{},
|
||||
{'role_id': 'role_id-only'},
|
||||
{'jwt': 'jwt-only'}
|
||||
])
|
||||
def test_auth_jwt_validate_xfailures(self, auth_jwt, adapter, opt_patch):
|
||||
adapter.set_options(**opt_patch)
|
||||
|
||||
with pytest.raises(HashiVaultValueError, match=r'Authentication method jwt requires options .*? to be set, but these are missing:'):
|
||||
auth_jwt.validate()
|
||||
|
||||
@pytest.mark.parametrize('use_token', [True, False], ids=lambda x: 'use_token=%s' % x)
|
||||
@pytest.mark.parametrize('mount_point', [None, 'other'], ids=lambda x: 'mount_point=%s' % x)
|
||||
def test_auth_jwt_authenticate(self, auth_jwt, client, adapter, jwt, role_id, mount_point, use_token, jwt_login_response):
|
||||
adapter.set_option('jwt', jwt)
|
||||
adapter.set_option('role_id', role_id)
|
||||
adapter.set_option('mount_point', mount_point)
|
||||
|
||||
expected_login_params = {
|
||||
'jwt': jwt,
|
||||
'role': role_id,
|
||||
}
|
||||
if mount_point:
|
||||
expected_login_params['path'] = mount_point
|
||||
|
||||
with mock.patch.object(client.auth.jwt, 'jwt_login', return_value=jwt_login_response) as jwt_login:
|
||||
response = auth_jwt.authenticate(client, use_token=use_token)
|
||||
jwt_login.assert_called_once_with(**expected_login_params)
|
||||
|
||||
assert response['auth']['client_token'] == jwt_login_response['auth']['client_token']
|
||||
assert (client.token == jwt_login_response['auth']['client_token']) is use_token
|
||||
+103
@@ -0,0 +1,103 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright (c) 2021 Brian Scholer (@briantist)
|
||||
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
from __future__ import (absolute_import, division, print_function)
|
||||
__metaclass__ = type
|
||||
|
||||
import pytest
|
||||
|
||||
from ansible_collections.community.hashi_vault.tests.unit.compat import mock
|
||||
|
||||
from ansible_collections.community.hashi_vault.plugins.module_utils._auth_method_ldap import (
|
||||
HashiVaultAuthMethodLdap,
|
||||
)
|
||||
|
||||
from ansible_collections.community.hashi_vault.plugins.module_utils._hashi_vault_common import (
|
||||
HashiVaultAuthMethodBase,
|
||||
HashiVaultValueError,
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def option_dict():
|
||||
return {
|
||||
'auth_method': 'ldap',
|
||||
'username': None,
|
||||
'password': None,
|
||||
'mount_point': None,
|
||||
}
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def ldap_username():
|
||||
return 'ldapuser'
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def ldap_password():
|
||||
return 's3cret'
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def auth_ldap(adapter, warner, deprecator):
|
||||
return HashiVaultAuthMethodLdap(adapter, warner, deprecator)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def ldap_login_response(fixture_loader):
|
||||
return fixture_loader('ldap_login_response.json')
|
||||
|
||||
|
||||
class TestAuthLdap(object):
|
||||
|
||||
def test_auth_ldap_is_auth_method_base(self, auth_ldap):
|
||||
assert isinstance(auth_ldap, HashiVaultAuthMethodLdap)
|
||||
assert issubclass(HashiVaultAuthMethodLdap, HashiVaultAuthMethodBase)
|
||||
|
||||
@pytest.mark.parametrize('mount_point', [None, 'other'], ids=lambda x: 'mount_point=%s' % x)
|
||||
def test_auth_ldap_validate(self, auth_ldap, adapter, ldap_username, ldap_password, mount_point):
|
||||
adapter.set_options(username=ldap_username, password=ldap_password, mount_point=mount_point)
|
||||
|
||||
auth_ldap.validate()
|
||||
|
||||
@pytest.mark.parametrize('opt_patch', [
|
||||
{'username': 'user-only'},
|
||||
{'password': 'password-only'},
|
||||
])
|
||||
def test_auth_ldap_validate_xfailures(self, auth_ldap, adapter, opt_patch):
|
||||
adapter.set_options(**opt_patch)
|
||||
|
||||
with pytest.raises(HashiVaultValueError, match=r'Authentication method ldap requires options .*? to be set, but these are missing:'):
|
||||
auth_ldap.validate()
|
||||
|
||||
@pytest.mark.parametrize('use_token', [True, False], ids=lambda x: 'use_token=%s' % x)
|
||||
@pytest.mark.parametrize('mount_point', [None, 'other'], ids=lambda x: 'mount_point=%s' % x)
|
||||
def test_auth_ldap_authenticate(
|
||||
self, auth_ldap, client, adapter, ldap_password, ldap_username, mount_point, use_token, ldap_login_response
|
||||
):
|
||||
adapter.set_option('username', ldap_username)
|
||||
adapter.set_option('password', ldap_password)
|
||||
adapter.set_option('mount_point', mount_point)
|
||||
|
||||
expected_login_params = {
|
||||
'username': ldap_username,
|
||||
'password': ldap_password,
|
||||
}
|
||||
if mount_point:
|
||||
expected_login_params['mount_point'] = mount_point
|
||||
|
||||
auth_ldap.validate()
|
||||
|
||||
def _set_client_token(*args, **kwargs):
|
||||
if kwargs['use_token']:
|
||||
client.token = ldap_login_response['auth']['client_token']
|
||||
return ldap_login_response
|
||||
|
||||
with mock.patch.object(client.auth.ldap, 'login', side_effect=_set_client_token) as ldap_login:
|
||||
response = auth_ldap.authenticate(client, use_token=use_token)
|
||||
ldap_login.assert_called_once_with(use_token=use_token, **expected_login_params)
|
||||
|
||||
assert response['auth']['client_token'] == ldap_login_response['auth']['client_token']
|
||||
assert (client.token == ldap_login_response['auth']['client_token']) is use_token
|
||||
+33
@@ -0,0 +1,33 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright (c) 2021 Brian Scholer (@briantist)
|
||||
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
from __future__ import (absolute_import, division, print_function)
|
||||
__metaclass__ = type
|
||||
|
||||
import pytest
|
||||
|
||||
from ......plugins.module_utils._auth_method_none import HashiVaultAuthMethodNone
|
||||
from ......plugins.module_utils._hashi_vault_common import HashiVaultAuthMethodBase
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def auth_none(adapter, warner, deprecator):
|
||||
return HashiVaultAuthMethodNone(adapter, warner, deprecator)
|
||||
|
||||
|
||||
class TestAuthNone(object):
|
||||
|
||||
def test_auth_none_is_auth_method_base(self, auth_none):
|
||||
assert issubclass(type(auth_none), HashiVaultAuthMethodBase)
|
||||
|
||||
def test_auth_none_validate(self, auth_none):
|
||||
auth_none.validate()
|
||||
|
||||
@pytest.mark.parametrize('use_token', [True, False])
|
||||
def test_auth_none_authenticate(self, auth_none, client, use_token):
|
||||
result = auth_none.authenticate(client, use_token=use_token)
|
||||
|
||||
assert result is None
|
||||
assert client.token is None
|
||||
+182
@@ -0,0 +1,182 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright (c) 2021 Brian Scholer (@briantist)
|
||||
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
from __future__ import (absolute_import, division, print_function)
|
||||
__metaclass__ = type
|
||||
|
||||
import os
|
||||
import pytest
|
||||
|
||||
from ......tests.unit.compat import mock
|
||||
|
||||
import hvac
|
||||
|
||||
from ......plugins.module_utils._auth_method_token import (
|
||||
HashiVaultAuthMethodToken,
|
||||
)
|
||||
|
||||
from ......plugins.module_utils._hashi_vault_common import (
|
||||
HashiVaultAuthMethodBase,
|
||||
HashiVaultValueError,
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def option_dict():
|
||||
return {
|
||||
'auth_method': 'fake',
|
||||
'token': None,
|
||||
'token_path': None,
|
||||
'token_file': '.vault-token',
|
||||
'token_validate': True,
|
||||
}
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def token():
|
||||
return 'opaque'
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def auth_token(adapter, warner, deprecator):
|
||||
return HashiVaultAuthMethodToken(adapter, warner, deprecator)
|
||||
|
||||
|
||||
@pytest.fixture(params=['lookup-self_with_meta.json', 'lookup-self_without_meta.json'])
|
||||
def lookup_self_response(fixture_loader, request):
|
||||
return fixture_loader(request.param)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def token_file_path(fixture_loader):
|
||||
return fixture_loader('vault-token', parse='path')
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def token_file_content(fixture_loader):
|
||||
return fixture_loader('vault-token', parse='raw').strip()
|
||||
|
||||
|
||||
@pytest.fixture(params=[hvac.exceptions.InvalidRequest(), hvac.exceptions.Forbidden(), hvac.exceptions.InvalidPath()])
|
||||
def validation_failure(request):
|
||||
return request.param
|
||||
|
||||
|
||||
class TestAuthToken(object):
|
||||
|
||||
def test_auth_token_is_auth_method_base(self, auth_token):
|
||||
assert isinstance(auth_token, HashiVaultAuthMethodToken)
|
||||
assert issubclass(HashiVaultAuthMethodToken, HashiVaultAuthMethodBase)
|
||||
|
||||
def test_simulate_login_response(self, auth_token, token):
|
||||
response = auth_token._simulate_login_response(token)
|
||||
expected = {
|
||||
'auth': {
|
||||
'client_token': token
|
||||
}
|
||||
}
|
||||
|
||||
assert response == expected
|
||||
|
||||
def test_simulate_login_response_with_lookup(self, auth_token, token, lookup_self_response):
|
||||
response = auth_token._simulate_login_response(token, lookup_self_response)
|
||||
|
||||
assert 'auth' in response
|
||||
assert response['auth']['client_token'] == token
|
||||
|
||||
if 'meta' not in lookup_self_response['data']:
|
||||
return
|
||||
assert 'meta' not in response['auth']
|
||||
assert lookup_self_response['data']['meta'] == response['auth']['metadata']
|
||||
|
||||
def test_auth_token_validate_direct(self, auth_token, adapter, token):
|
||||
adapter.set_option('token', token)
|
||||
|
||||
auth_token.validate()
|
||||
|
||||
assert adapter.get_option('token') == token
|
||||
|
||||
def test_auth_token_validate_by_path(self, auth_token, adapter, token_file_path, token_file_content):
|
||||
head, tail = os.path.split(token_file_path)
|
||||
adapter.set_option('token_path', head)
|
||||
adapter.set_option('token_file', tail)
|
||||
|
||||
auth_token.validate()
|
||||
|
||||
assert adapter.get_option('token') == token_file_content
|
||||
|
||||
@pytest.mark.parametrize('opt_patch', [
|
||||
{},
|
||||
{'token_path': '/tmp', 'token_file': '__fake_no_file'},
|
||||
])
|
||||
def test_auth_token_validate_xfailures(self, auth_token, adapter, opt_patch):
|
||||
adapter.set_options(**opt_patch)
|
||||
|
||||
with pytest.raises(HashiVaultValueError, match=r'No Vault Token specified or discovered'):
|
||||
auth_token.validate()
|
||||
|
||||
def test_auth_token_file_is_directory(self, auth_token, adapter, tmp_path):
|
||||
# ensure that a token_file that exists but is a directory is treated the same as it not being found
|
||||
# see also: https://github.com/ansible-collections/community.hashi_vault/issues/152
|
||||
adapter.set_options(token_path=str(tmp_path.parent), token_file=str(tmp_path))
|
||||
|
||||
with pytest.raises(HashiVaultValueError, match=r"The Vault token file '[^']+' was found but is not a file."):
|
||||
auth_token.validate()
|
||||
|
||||
@pytest.mark.parametrize('use_token', [True, False], ids=lambda x: 'use_token=%s' % x)
|
||||
@pytest.mark.parametrize('lookup_self', [True, False], ids=lambda x: 'lookup_self=%s' % x)
|
||||
@pytest.mark.parametrize('token_validate', [True, False], ids=lambda x: 'token_validate=%s' % x)
|
||||
def test_auth_token_authenticate(self, auth_token, client, adapter, token, use_token, token_validate, lookup_self, lookup_self_response):
|
||||
adapter.set_option('token', token)
|
||||
adapter.set_option('token_validate', token_validate)
|
||||
|
||||
expected_lookup_value = lookup_self_response if use_token and (lookup_self or token_validate) else None
|
||||
|
||||
with mock.patch.object(auth_token, '_simulate_login_response', wraps=auth_token._simulate_login_response) as sim_login:
|
||||
with mock.patch.object(client.auth.token, 'lookup_self', return_value=lookup_self_response):
|
||||
response = auth_token.authenticate(client, use_token=use_token, lookup_self=lookup_self)
|
||||
|
||||
sim_login.assert_called_once_with(token, expected_lookup_value)
|
||||
|
||||
assert response['auth']['client_token'] == token
|
||||
assert (client.token == token) is use_token
|
||||
|
||||
def test_auth_token_authenticate_success_on_no_validate(self, auth_token, adapter, client, token, validation_failure):
|
||||
adapter.set_option('token', token)
|
||||
adapter.set_option('token_validate', False)
|
||||
|
||||
raiser = mock.Mock()
|
||||
raiser.side_effect = validation_failure
|
||||
|
||||
with mock.patch.object(auth_token, '_simulate_login_response', wraps=auth_token._simulate_login_response) as sim_login:
|
||||
with mock.patch.object(client.auth.token, 'lookup_self', raiser):
|
||||
response = auth_token.authenticate(client, use_token=True, lookup_self=True)
|
||||
|
||||
sim_login.assert_called_once_with(token, None)
|
||||
|
||||
assert response['auth']['client_token'] == token
|
||||
assert client.token == token
|
||||
|
||||
def test_auth_token_authenticate_failed_validation(self, auth_token, adapter, client, token, validation_failure):
|
||||
adapter.set_option('token', token)
|
||||
adapter.set_option('token_validate', True)
|
||||
|
||||
raiser = mock.Mock()
|
||||
raiser.side_effect = validation_failure
|
||||
|
||||
with pytest.raises(HashiVaultValueError, match=r'Invalid Vault Token Specified'):
|
||||
with mock.patch.object(client.auth.token, 'lookup_self', raiser):
|
||||
auth_token.authenticate(client, use_token=True, lookup_self=False)
|
||||
|
||||
@pytest.mark.parametrize('exc', [AttributeError, NotImplementedError])
|
||||
def test_auth_token_authenticate_old_lookup_self(self, auth_token, adapter, client, token, exc):
|
||||
adapter.set_option('token', token)
|
||||
|
||||
with mock.patch.object(client, 'lookup_token') as legacy_lookup:
|
||||
with mock.patch.object(client.auth.token, 'lookup_self', side_effect=exc) as lookup:
|
||||
auth_token.authenticate(client, use_token=True, lookup_self=True)
|
||||
|
||||
legacy_lookup.assert_called_once_with()
|
||||
lookup.assert_called_once_with()
|
||||
+99
@@ -0,0 +1,99 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright (c) 2021 Brian Scholer (@briantist)
|
||||
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
from __future__ import (absolute_import, division, print_function)
|
||||
__metaclass__ = type
|
||||
|
||||
import pytest
|
||||
|
||||
from ansible_collections.community.hashi_vault.tests.unit.compat import mock
|
||||
|
||||
from ansible_collections.community.hashi_vault.plugins.module_utils._auth_method_userpass import (
|
||||
HashiVaultAuthMethodUserpass,
|
||||
)
|
||||
|
||||
from ansible_collections.community.hashi_vault.plugins.module_utils._hashi_vault_common import (
|
||||
HashiVaultAuthMethodBase,
|
||||
HashiVaultValueError,
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def option_dict():
|
||||
return {
|
||||
'auth_method': 'userpass',
|
||||
'username': None,
|
||||
'password': None,
|
||||
'mount_point': None,
|
||||
}
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def userpass_password():
|
||||
return 'opaque'
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def userpass_username():
|
||||
return 'fake-user'
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def auth_userpass(adapter, warner, deprecator):
|
||||
return HashiVaultAuthMethodUserpass(adapter, warner, deprecator)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def userpass_login_response(fixture_loader):
|
||||
return fixture_loader('userpass_login_response.json')
|
||||
|
||||
|
||||
class TestAuthUserpass(object):
|
||||
|
||||
def test_auth_userpass_is_auth_method_base(self, auth_userpass):
|
||||
assert isinstance(auth_userpass, HashiVaultAuthMethodUserpass)
|
||||
assert issubclass(HashiVaultAuthMethodUserpass, HashiVaultAuthMethodBase)
|
||||
|
||||
def test_auth_userpass_validate_direct(self, auth_userpass, adapter, userpass_username, userpass_password):
|
||||
adapter.set_option('username', userpass_username)
|
||||
adapter.set_option('password', userpass_password)
|
||||
|
||||
auth_userpass.validate()
|
||||
|
||||
@pytest.mark.parametrize('opt_patch', [
|
||||
{'username': 'user-only'},
|
||||
{'password': 'password-only'},
|
||||
])
|
||||
def test_auth_userpass_validate_xfailures(self, auth_userpass, adapter, opt_patch):
|
||||
adapter.set_options(**opt_patch)
|
||||
|
||||
with pytest.raises(HashiVaultValueError, match=r'Authentication method userpass requires options .*? to be set, but these are missing:'):
|
||||
auth_userpass.validate()
|
||||
|
||||
@pytest.mark.parametrize('use_token', [True, False], ids=lambda x: 'use_token=%s' % x)
|
||||
@pytest.mark.parametrize('mount_point', [None, 'other'], ids=lambda x: 'mount_point=%s' % x)
|
||||
def test_auth_userpass_authenticate(
|
||||
self, auth_userpass, client, adapter, userpass_password, userpass_username, mount_point, use_token, userpass_login_response
|
||||
):
|
||||
adapter.set_option('username', userpass_username)
|
||||
adapter.set_option('password', userpass_password)
|
||||
adapter.set_option('mount_point', mount_point)
|
||||
|
||||
expected_login_params = {
|
||||
'username': userpass_username,
|
||||
'password': userpass_password,
|
||||
}
|
||||
if mount_point:
|
||||
expected_login_params['mount_point'] = mount_point
|
||||
|
||||
def _set_client_token(*args, **kwargs):
|
||||
return userpass_login_response
|
||||
|
||||
with mock.patch.object(client.auth.userpass, 'login', side_effect=_set_client_token) as userpass_login:
|
||||
response = auth_userpass.authenticate(client, use_token=use_token)
|
||||
userpass_login.assert_called_once_with(**expected_login_params)
|
||||
|
||||
assert response['auth']['client_token'] == userpass_login_response['auth']['client_token']
|
||||
assert (client.token == userpass_login_response['auth']['client_token']) is use_token
|
||||
+74
@@ -0,0 +1,74 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright (c) 2021 Brian Scholer (@briantist)
|
||||
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
from __future__ import (absolute_import, division, print_function)
|
||||
__metaclass__ = type
|
||||
|
||||
import pytest
|
||||
|
||||
from ......plugins.module_utils._hashi_vault_common import (
|
||||
HashiVaultAuthMethodBase,
|
||||
HashiVaultOptionGroupBase,
|
||||
HashiVaultValueError,
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def auth_base(adapter, warner, deprecator):
|
||||
return HashiVaultAuthMethodBase(adapter, warner, deprecator)
|
||||
|
||||
|
||||
class TestHashiVaultAuthMethodBase(object):
|
||||
|
||||
def test_auth_method_is_option_group_base(self, fake_auth_class):
|
||||
assert issubclass(type(fake_auth_class), HashiVaultOptionGroupBase)
|
||||
|
||||
def test_base_validate_not_implemented(self, auth_base):
|
||||
with pytest.raises(NotImplementedError):
|
||||
auth_base.validate()
|
||||
|
||||
def test_base_authenticate_not_implemented(self, auth_base, client):
|
||||
with pytest.raises(NotImplementedError):
|
||||
auth_base.authenticate(client)
|
||||
|
||||
@pytest.mark.parametrize('options,required', [
|
||||
({}, []),
|
||||
({'a': 1, 'b': '2'}, ['b']),
|
||||
({'a': 1, 'b': '2'}, ['a', 'b']),
|
||||
({'a': 1, 'b': '2', 'c': 3.0}, ['a', 'c'])
|
||||
])
|
||||
def test_validate_by_required_fields_success(self, auth_base, adapter, options, required):
|
||||
adapter.set_options(**options)
|
||||
|
||||
auth_base.validate_by_required_fields(*required)
|
||||
|
||||
@pytest.mark.parametrize('options,required', [
|
||||
({}, ['a']),
|
||||
({'a': 1, 'b': '2'}, ['c']),
|
||||
({'a': 1, 'b': '2'}, ['a', 'c']),
|
||||
({'a': 1, 'b': '2', 'c': 3.0}, ['a', 'c', 'd'])
|
||||
])
|
||||
def test_validate_by_required_fields_failure(self, fake_auth_class, adapter, options, required):
|
||||
adapter.set_options(**options)
|
||||
|
||||
with pytest.raises(HashiVaultValueError):
|
||||
fake_auth_class.validate_by_required_fields(*required)
|
||||
|
||||
def test_warning_callback(self, auth_base, warner):
|
||||
msg = 'warning msg'
|
||||
|
||||
auth_base.warn(msg)
|
||||
|
||||
warner.assert_called_once_with(msg)
|
||||
|
||||
@pytest.mark.parametrize('version', [None, '0.99.7'])
|
||||
@pytest.mark.parametrize('date', [None, '2022'])
|
||||
@pytest.mark.parametrize('collection_name', [None, 'ns.col'])
|
||||
def test_deprecate_callback(self, auth_base, deprecator, version, date, collection_name):
|
||||
msg = 'warning msg'
|
||||
|
||||
auth_base.deprecate(msg, version, date, collection_name)
|
||||
|
||||
deprecator.assert_called_once_with(msg, version=version, date=date, collection_name=collection_name)
|
||||
+68
@@ -0,0 +1,68 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright (c) 2021 Brian Scholer (@briantist)
|
||||
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
from __future__ import (absolute_import, division, print_function)
|
||||
__metaclass__ = type
|
||||
|
||||
import pytest
|
||||
|
||||
from ......plugins.module_utils._authenticator import HashiVaultAuthenticator
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def authenticator(fake_auth_class, adapter, warner, deprecator):
|
||||
a = HashiVaultAuthenticator(adapter, warner, deprecator)
|
||||
a._selector.update({fake_auth_class.NAME: fake_auth_class})
|
||||
|
||||
return a
|
||||
|
||||
|
||||
class TestHashiVaultAuthenticator(object):
|
||||
def test_method_validate_is_called(self, authenticator, fake_auth_class):
|
||||
authenticator.validate()
|
||||
|
||||
fake_auth_class.validate.assert_called_once()
|
||||
|
||||
def test_validate_not_implemented(self, authenticator, fake_auth_class):
|
||||
with pytest.raises(NotImplementedError):
|
||||
authenticator.validate(method='missing')
|
||||
|
||||
fake_auth_class.validate.assert_not_called()
|
||||
|
||||
@pytest.mark.parametrize('args', [
|
||||
[],
|
||||
['one'],
|
||||
['one', 2, 'three'],
|
||||
])
|
||||
@pytest.mark.parametrize('kwargs', [
|
||||
{},
|
||||
{'one': 1},
|
||||
{'one': '1', 'two': 2},
|
||||
])
|
||||
def test_method_authenticate_is_called(self, authenticator, fake_auth_class, args, kwargs):
|
||||
authenticator.authenticate(*args, **kwargs)
|
||||
|
||||
fake_auth_class.authenticate.assert_called_once_with(*args, **kwargs)
|
||||
|
||||
def test_authenticate_not_implemented(self, authenticator, fake_auth_class):
|
||||
with pytest.raises(NotImplementedError):
|
||||
authenticator.validate(method='missing')
|
||||
|
||||
fake_auth_class.authenticate.assert_not_called()
|
||||
|
||||
def test_get_method_object_explicit(self, authenticator):
|
||||
for auth_method, obj in authenticator._selector.items():
|
||||
assert authenticator._get_method_object(method=auth_method) == obj
|
||||
|
||||
def test_get_method_object_missing(self, authenticator):
|
||||
with pytest.raises(NotImplementedError, match=r"auth method 'missing' is not implemented in HashiVaultAuthenticator"):
|
||||
authenticator._get_method_object(method='missing')
|
||||
|
||||
def test_get_method_object_implicit(self, authenticator, adapter, fake_auth_class):
|
||||
adapter.set_option('auth_method', fake_auth_class.NAME)
|
||||
|
||||
obj = authenticator._get_method_object()
|
||||
|
||||
assert isinstance(obj, type(fake_auth_class))
|
||||
+45
@@ -0,0 +1,45 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright (c) 2021 Brian Scholer (@briantist)
|
||||
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
from __future__ import (absolute_import, division, print_function)
|
||||
__metaclass__ = type
|
||||
|
||||
# this file must define the "adapter" fixture at a minimum,
|
||||
# and anything else that it needs or depends on that isn't already defined in in the test files themselves.
|
||||
|
||||
# Keep in mind that this one is for module_utils and so it cannot depend on or import any controller-side code.
|
||||
|
||||
from ansible_collections.community.hashi_vault.plugins.module_utils._hashi_vault_common import HashiVaultOptionAdapter
|
||||
|
||||
import pytest
|
||||
|
||||
|
||||
class FakeAnsibleModule:
|
||||
'''HashiVaultOptionAdapter.from_ansible_module() only cares about the AnsibleModule.params dict'''
|
||||
|
||||
def __init__(self, params):
|
||||
self.params = params
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def ansible_module(sample_dict):
|
||||
return FakeAnsibleModule(sample_dict)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def adapter_from_ansible_module(ansible_module):
|
||||
def _create_adapter_from_ansible_module():
|
||||
return HashiVaultOptionAdapter.from_ansible_module(ansible_module)
|
||||
|
||||
return _create_adapter_from_ansible_module
|
||||
|
||||
|
||||
@pytest.fixture(params=['dict', 'dict_defaults', 'ansible_module'])
|
||||
def adapter(request, adapter_from_dict, adapter_from_dict_defaults, adapter_from_ansible_module):
|
||||
return {
|
||||
'dict': adapter_from_dict,
|
||||
'dict_defaults': adapter_from_dict_defaults,
|
||||
'ansible_module': adapter_from_ansible_module,
|
||||
}[request.param]()
|
||||
+247
@@ -0,0 +1,247 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright (c) 2021 Brian Scholer (@briantist)
|
||||
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
from __future__ import (absolute_import, division, print_function)
|
||||
__metaclass__ = type
|
||||
|
||||
import pytest
|
||||
|
||||
from ......plugins.module_utils._hashi_vault_common import HashiVaultOptionAdapter
|
||||
|
||||
|
||||
SAMPLE_DICT = {
|
||||
'key1': 'val1',
|
||||
'key2': 2,
|
||||
'key3': 'three',
|
||||
'key4': 'iiii',
|
||||
'key5': None,
|
||||
}
|
||||
|
||||
SAMPLE_KEYS = sorted(list(SAMPLE_DICT.keys()))
|
||||
|
||||
MISSING_KEYS = ['no', 'nein', 'iie']
|
||||
|
||||
|
||||
class SentinelMarker():
|
||||
pass
|
||||
|
||||
|
||||
MARKER = SentinelMarker()
|
||||
|
||||
|
||||
@pytest.fixture()
|
||||
def sample_dict():
|
||||
return SAMPLE_DICT.copy()
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def adapter_from_dict(sample_dict):
|
||||
def _create_adapter_from_dict():
|
||||
return HashiVaultOptionAdapter.from_dict(sample_dict)
|
||||
|
||||
return _create_adapter_from_dict
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def adapter_from_dict_defaults(sample_dict):
|
||||
# the point of this one is to test the "default" methods provided by the adapter
|
||||
# for everything except getter and setter, so we only supply those two required methods
|
||||
def _create_adapter_from_dict_defaults():
|
||||
return HashiVaultOptionAdapter(getter=sample_dict.__getitem__, setter=sample_dict.__setitem__)
|
||||
|
||||
return _create_adapter_from_dict_defaults
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def filter_all():
|
||||
return lambda k, v: True
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def filter_none():
|
||||
return lambda k, v: False
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def filter_value_not_none():
|
||||
return lambda k, v: v is not None
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def filter_key_in_range():
|
||||
return lambda k, v: k in SAMPLE_KEYS[1:3]
|
||||
|
||||
|
||||
class TestHashiVaultOptionAdapter(object):
|
||||
|
||||
@pytest.mark.parametrize('option', SAMPLE_KEYS)
|
||||
def test_get_option_succeeds(self, adapter, option):
|
||||
value = adapter.get_option(option)
|
||||
|
||||
assert value == SAMPLE_DICT[option]
|
||||
|
||||
@pytest.mark.parametrize('option', MISSING_KEYS)
|
||||
def test_get_option_missing_raises(self, adapter, option):
|
||||
with pytest.raises(KeyError):
|
||||
adapter.get_option(option)
|
||||
|
||||
@pytest.mark.parametrize('option', SAMPLE_KEYS)
|
||||
def test_get_option_default_succeeds(self, adapter, option):
|
||||
value = adapter.get_option_default(option, MARKER)
|
||||
|
||||
assert value == SAMPLE_DICT[option]
|
||||
|
||||
@pytest.mark.parametrize('option', MISSING_KEYS)
|
||||
def test_get_option_default_missing_returns_default(self, adapter, option):
|
||||
value = adapter.get_option_default(option, MARKER)
|
||||
|
||||
assert isinstance(value, SentinelMarker)
|
||||
|
||||
@pytest.mark.parametrize('option,expected', [(o, False) for o in MISSING_KEYS] + [(o, True) for o in SAMPLE_KEYS])
|
||||
def test_has_option(self, adapter, option, expected):
|
||||
assert adapter.has_option(option) == expected
|
||||
|
||||
@pytest.mark.parametrize('option', SAMPLE_KEYS)
|
||||
def test_set_option_existing(self, adapter, option, sample_dict):
|
||||
value = type(sample_dict.get(option, ""))()
|
||||
adapter.set_option(option, value)
|
||||
# first check the underlying data, then ensure the adapter refelcts the change too
|
||||
assert sample_dict[option] == value
|
||||
assert adapter.get_option(option) == value
|
||||
|
||||
@pytest.mark.parametrize('option', MISSING_KEYS)
|
||||
def test_set_option_missing(self, request, adapter, option, sample_dict):
|
||||
value = MARKER
|
||||
|
||||
for mark in request.node.own_markers:
|
||||
if mark.name == 'option_adapter_raise_on_missing':
|
||||
from ansible.errors import AnsibleError
|
||||
with pytest.raises(AnsibleError, match=rf"^Requested entry.*?setting: {option}.*?was not defined in configuration"):
|
||||
adapter.set_option(option, value)
|
||||
break
|
||||
else:
|
||||
adapter.set_option(option, value)
|
||||
assert sample_dict[option] == value
|
||||
assert adapter.get_option(option) == value
|
||||
|
||||
@pytest.mark.parametrize('default', [MARKER])
|
||||
@pytest.mark.parametrize('option,expected', [(o, SAMPLE_DICT[o]) for o in SAMPLE_KEYS])
|
||||
def test_set_option_default_existing(self, adapter, option, default, expected, sample_dict):
|
||||
value = adapter.set_option_default(option, default)
|
||||
|
||||
# check return data, underlying data structure, and adapter retrieval
|
||||
assert value == expected
|
||||
assert sample_dict[option] == expected
|
||||
assert adapter.get_option(option) == expected
|
||||
|
||||
@pytest.mark.parametrize('default', [MARKER])
|
||||
@pytest.mark.parametrize('option,expected', [(o, MARKER) for o in MISSING_KEYS])
|
||||
def test_set_option_default_missing(self, request, adapter, option, default, expected, sample_dict):
|
||||
for mark in request.node.own_markers:
|
||||
if mark.name == 'option_adapter_raise_on_missing':
|
||||
from ansible.errors import AnsibleError
|
||||
with pytest.raises(AnsibleError, match=rf"^Requested entry.*?setting: {option}.*?was not defined in configuration"):
|
||||
adapter.set_option_default(option, default)
|
||||
break
|
||||
else:
|
||||
value = adapter.set_option_default(option, default)
|
||||
# check return data, underlying data structure, and adapter retrieval
|
||||
assert value == expected
|
||||
assert sample_dict[option] == expected
|
||||
assert adapter.get_option(option) == expected
|
||||
|
||||
@pytest.mark.parametrize('options', [[SAMPLE_KEYS[0], MISSING_KEYS[0]]])
|
||||
def test_set_options(self, request, adapter, options, sample_dict):
|
||||
update = dict([(o, type(sample_dict.get(o, ""))(i)) for i, o in enumerate(options)])
|
||||
|
||||
for mark in request.node.own_markers:
|
||||
if mark.name == 'option_adapter_raise_on_missing':
|
||||
from ansible.errors import AnsibleError
|
||||
with pytest.raises(AnsibleError, match=r"^Requested entry.*?setting:.*?was not defined in configuration"):
|
||||
adapter.set_options(**update)
|
||||
break
|
||||
else:
|
||||
adapter.set_options(**update)
|
||||
for k in MISSING_KEYS:
|
||||
if k in update:
|
||||
assert sample_dict[k] == update[k]
|
||||
assert adapter.get_option(k) == update[k]
|
||||
else:
|
||||
assert k not in sample_dict
|
||||
assert not adapter.has_option(k)
|
||||
|
||||
for k in SAMPLE_KEYS:
|
||||
expected = update[k] if k in update else SAMPLE_DICT[k]
|
||||
assert sample_dict[k] == expected
|
||||
assert adapter.get_option(k) == expected
|
||||
|
||||
@pytest.mark.parametrize('options', [[SAMPLE_KEYS[0], MISSING_KEYS[0]]])
|
||||
def test_get_options_mixed(self, adapter, options):
|
||||
with pytest.raises(KeyError):
|
||||
adapter.get_options(*options)
|
||||
|
||||
@pytest.mark.parametrize('options', [MISSING_KEYS[0:2]])
|
||||
def test_get_options_missing(self, adapter, options):
|
||||
with pytest.raises(KeyError):
|
||||
adapter.get_options(*options)
|
||||
|
||||
@pytest.mark.parametrize('options', [SAMPLE_KEYS[0:2]])
|
||||
def test_get_options_exists(self, adapter, options):
|
||||
expected = dict([(k, SAMPLE_DICT[k]) for k in options])
|
||||
|
||||
result = adapter.get_options(*options)
|
||||
|
||||
assert result == expected
|
||||
|
||||
@pytest.mark.parametrize('options', [[SAMPLE_KEYS[0], MISSING_KEYS[0]]])
|
||||
def test_get_filtered_options_mixed(self, adapter, options, filter_all):
|
||||
with pytest.raises(KeyError):
|
||||
adapter.get_filtered_options(filter_all, *options)
|
||||
|
||||
@pytest.mark.parametrize('options', [MISSING_KEYS[0:2]])
|
||||
def test_get_filtered_options_missing(self, adapter, options, filter_all):
|
||||
with pytest.raises(KeyError):
|
||||
adapter.get_filtered_options(filter_all, *options)
|
||||
|
||||
@pytest.mark.parametrize('options', [SAMPLE_KEYS])
|
||||
def test_get_filtered_options_all(self, adapter, options, filter_all):
|
||||
expected = dict([(k, SAMPLE_DICT[k]) for k in options])
|
||||
|
||||
result = adapter.get_filtered_options(filter_all, *options)
|
||||
|
||||
assert result == expected
|
||||
assert result == adapter.get_options(*options)
|
||||
|
||||
@pytest.mark.parametrize('options', [SAMPLE_KEYS])
|
||||
def test_get_filtered_options_none(self, adapter, options, filter_none):
|
||||
expected = {}
|
||||
|
||||
result = adapter.get_filtered_options(filter_none, *options)
|
||||
|
||||
assert result == expected
|
||||
|
||||
@pytest.mark.parametrize('options', [SAMPLE_KEYS])
|
||||
def test_get_filtered_options_by_value(self, adapter, options, filter_value_not_none):
|
||||
expected = dict([(k, SAMPLE_DICT[k]) for k in options if SAMPLE_DICT[k] is not None])
|
||||
|
||||
result = adapter.get_filtered_options(filter_value_not_none, *options)
|
||||
|
||||
assert result == expected
|
||||
|
||||
@pytest.mark.parametrize('options', [SAMPLE_KEYS])
|
||||
def test_get_filtered_options_by_key(self, adapter, options, filter_key_in_range):
|
||||
expected = dict([(k, SAMPLE_DICT[k]) for k in options if k in SAMPLE_KEYS[1:3]])
|
||||
|
||||
result = adapter.get_filtered_options(filter_key_in_range, *options)
|
||||
|
||||
assert result == expected
|
||||
|
||||
@pytest.mark.parametrize('options', [SAMPLE_KEYS])
|
||||
def test_get_filled_options(self, adapter, options):
|
||||
expected = dict([(k, SAMPLE_DICT[k]) for k in options if SAMPLE_DICT[k] is not None])
|
||||
|
||||
result = adapter.get_filled_options(*options)
|
||||
|
||||
assert result == expected
|
||||
+272
@@ -0,0 +1,272 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright (c) 2021 Brian Scholer (@briantist)
|
||||
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
from __future__ import (absolute_import, division, print_function)
|
||||
__metaclass__ = type
|
||||
|
||||
import os
|
||||
import pytest
|
||||
|
||||
from ansible_collections.community.hashi_vault.tests.unit.compat import mock
|
||||
|
||||
from ansible_collections.community.hashi_vault.plugins.module_utils._hashi_vault_common import (
|
||||
HashiVaultOptionGroupBase,
|
||||
HashiVaultOptionAdapter,
|
||||
HashiVaultValueError,
|
||||
)
|
||||
|
||||
from ansible_collections.community.hashi_vault.plugins.module_utils._connection_options import HashiVaultConnectionOptions
|
||||
|
||||
from requests import Session
|
||||
|
||||
|
||||
CONNECTION_OPTIONS = {
|
||||
'url': 'url-is-required',
|
||||
'proxies': None,
|
||||
'namespace': None,
|
||||
'validate_certs': None,
|
||||
'ca_cert': None,
|
||||
'timeout': None,
|
||||
'retries': None,
|
||||
'retry_action': 'warn',
|
||||
}
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def predefined_options():
|
||||
return CONNECTION_OPTIONS.copy()
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def adapter(predefined_options):
|
||||
return HashiVaultOptionAdapter.from_dict(predefined_options)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def retry_callback_generator():
|
||||
def _cb(retry_action):
|
||||
pass
|
||||
return _cb
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def connection_options(adapter, retry_callback_generator):
|
||||
return HashiVaultConnectionOptions(adapter, retry_callback_generator)
|
||||
|
||||
|
||||
class TestHashiVaultConnectionOptions(object):
|
||||
def test_connection_options_is_option_group(self, connection_options):
|
||||
assert issubclass(type(connection_options), HashiVaultOptionGroupBase)
|
||||
|
||||
# _boolean_or_cacert tests
|
||||
# this method is the intersection of the validate_certs and ca_cert parameter
|
||||
# along with the VAULT_SKIP_VERIFY environment variable (see the function defintion).
|
||||
# The result is either a boolean, or a string, to be passed to the hvac client's
|
||||
# verify parameter.
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
'optpatch,envpatch,expected',
|
||||
[
|
||||
({}, {}, True),
|
||||
({}, {'VAULT_SKIP_VERIFY': 'true'}, False),
|
||||
({}, {'VAULT_SKIP_VERIFY': 'false'}, True),
|
||||
({}, {'VAULT_SKIP_VERIFY': 'invalid'}, True),
|
||||
({'validate_certs': True}, {}, True),
|
||||
({'validate_certs': True}, {'VAULT_SKIP_VERIFY': 'false'}, True),
|
||||
({'validate_certs': True}, {'VAULT_SKIP_VERIFY': 'true'}, True),
|
||||
({'validate_certs': True}, {'VAULT_SKIP_VERIFY': 'invalid'}, True),
|
||||
({'validate_certs': False}, {}, False),
|
||||
({'validate_certs': False}, {'VAULT_SKIP_VERIFY': 'false'}, False),
|
||||
({'validate_certs': False}, {'VAULT_SKIP_VERIFY': 'true'}, False),
|
||||
({'validate_certs': False}, {'VAULT_SKIP_VERIFY': 'invalid'}, False),
|
||||
({'ca_cert': '/tmp/fake'}, {}, '/tmp/fake'),
|
||||
({'ca_cert': '/tmp/fake'}, {'VAULT_SKIP_VERIFY': 'true'}, False),
|
||||
({'ca_cert': '/tmp/fake'}, {'VAULT_SKIP_VERIFY': 'false'}, '/tmp/fake'),
|
||||
({'ca_cert': '/tmp/fake'}, {'VAULT_SKIP_VERIFY': 'invalid'}, '/tmp/fake'),
|
||||
({'ca_cert': '/tmp/fake', 'validate_certs': True}, {}, '/tmp/fake'),
|
||||
({'ca_cert': '/tmp/fake', 'validate_certs': True}, {'VAULT_SKIP_VERIFY': 'false'}, '/tmp/fake'),
|
||||
({'ca_cert': '/tmp/fake', 'validate_certs': True}, {'VAULT_SKIP_VERIFY': 'true'}, '/tmp/fake'),
|
||||
({'ca_cert': '/tmp/fake', 'validate_certs': True}, {'VAULT_SKIP_VERIFY': 'invalid'}, '/tmp/fake'),
|
||||
({'ca_cert': '/tmp/fake', 'validate_certs': False}, {}, False),
|
||||
({'ca_cert': '/tmp/fake', 'validate_certs': False}, {'VAULT_SKIP_VERIFY': 'false'}, False),
|
||||
({'ca_cert': '/tmp/fake', 'validate_certs': False}, {'VAULT_SKIP_VERIFY': 'true'}, False),
|
||||
({'ca_cert': '/tmp/fake', 'validate_certs': False}, {'VAULT_SKIP_VERIFY': 'invalid'}, False),
|
||||
]
|
||||
)
|
||||
def test_boolean_or_cacert(self, connection_options, predefined_options, adapter, optpatch, envpatch, expected):
|
||||
adapter.set_options(**optpatch)
|
||||
|
||||
with mock.patch.dict(os.environ, envpatch):
|
||||
connection_options._boolean_or_cacert()
|
||||
|
||||
assert connection_options._conopt_verify == expected
|
||||
|
||||
# _process_option_proxies
|
||||
# proxies can be specified as a dictionary where key is protocol/scheme
|
||||
# and value is the proxy address. A dictionary can also be supplied as a string
|
||||
# representation of a dictionary in JSON format.
|
||||
# If a string is supplied that cannot be interpreted as a JSON dictionary, then it
|
||||
# is assumed to be a proxy address, and will be used as proxy for both the
|
||||
# http and https protocols.
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
'optproxies,expected',
|
||||
[
|
||||
(None, None),
|
||||
('socks://thecat', {'http': 'socks://thecat', 'https': 'socks://thecat'}),
|
||||
('{"http": "gopher://it"}', {'http': 'gopher://it'}),
|
||||
({'https': "smtp://mail.aol.com"}, {'https': "smtp://mail.aol.com"}),
|
||||
({'protoa': 'proxya', 'protob': 'proxyb', 'protoc': 'proxyc'}, {'protoa': 'proxya', 'protob': 'proxyb', 'protoc': 'proxyc'}),
|
||||
('{"protoa": "proxya", "protob": "proxyb", "protoc": "proxyc"}', {'protoa': 'proxya', 'protob': 'proxyb', 'protoc': 'proxyc'}),
|
||||
('{"protoa":"proxya","protob":"proxyb","protoc":"proxyc"}', {'protoa': 'proxya', 'protob': 'proxyb', 'protoc': 'proxyc'}),
|
||||
]
|
||||
)
|
||||
def test_process_option_proxies(self, connection_options, predefined_options, adapter, optproxies, expected):
|
||||
adapter.set_option('proxies', optproxies)
|
||||
|
||||
connection_options._process_option_proxies()
|
||||
|
||||
assert predefined_options['proxies'] == expected
|
||||
|
||||
# _process_option_retries
|
||||
# can be specified as a positive int or a dict
|
||||
# (or any string that can be interpreted as one of those)
|
||||
|
||||
@pytest.mark.parametrize('opt_retries', ['plz retry', ('1', '1'), [True], -1, 1.0])
|
||||
def test_process_option_retries_invalid(self, connection_options, predefined_options, adapter, opt_retries):
|
||||
adapter.set_option('retries', opt_retries)
|
||||
|
||||
with pytest.raises((TypeError, ValueError)):
|
||||
connection_options._process_option_retries()
|
||||
|
||||
@pytest.mark.parametrize('opt_retries', [None, 0, '0'])
|
||||
def test_process_option_retries_none_result(self, connection_options, predefined_options, adapter, opt_retries):
|
||||
adapter.set_option('retries', opt_retries)
|
||||
|
||||
connection_options._process_option_retries()
|
||||
|
||||
assert predefined_options['retries'] is None
|
||||
|
||||
@pytest.mark.parametrize('opt_retries', [1, '1', 10, '30'])
|
||||
def test_process_option_retries_from_number(self, connection_options, predefined_options, adapter, opt_retries):
|
||||
expected = connection_options._RETRIES_DEFAULT_PARAMS.copy()
|
||||
expected['total'] = int(float(opt_retries))
|
||||
|
||||
adapter.set_option('retries', opt_retries)
|
||||
|
||||
connection_options._process_option_retries()
|
||||
|
||||
assert predefined_options['retries'] == expected
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
'opt_retries,expected',
|
||||
[
|
||||
({}, {}),
|
||||
('{}', {}),
|
||||
({'total': 5}, {'total': 5}),
|
||||
('{"total": 9}', {'total': 9}),
|
||||
]
|
||||
)
|
||||
def test_process_option_retries_from_dict(self, connection_options, predefined_options, adapter, opt_retries, expected):
|
||||
adapter.set_option('retries', opt_retries)
|
||||
|
||||
connection_options._process_option_retries()
|
||||
|
||||
assert predefined_options['retries'] == expected
|
||||
|
||||
# process_connection_options
|
||||
# this is the public function of the class meant to ensure all option processing is complete
|
||||
|
||||
def test_process_connection_options(self, mocker, connection_options, adapter):
|
||||
# mock the internal methods we expect to be called
|
||||
f_process_late_binding_env_vars = mocker.patch.object(connection_options, 'process_late_binding_env_vars')
|
||||
f_boolean_or_cacert = mocker.patch.object(connection_options, '_boolean_or_cacert')
|
||||
f_process_option_proxies = mocker.patch.object(connection_options, '_process_option_proxies')
|
||||
f_process_option_retries = mocker.patch.object(connection_options, '_process_option_retries')
|
||||
|
||||
# mock the adapter itself, so we can spy on adapter interactions
|
||||
# since we're mocking out the methods we expect to call, we shouldn't see any
|
||||
mock_adapter = mock.create_autospec(adapter)
|
||||
connection_options._options = mock_adapter
|
||||
|
||||
connection_options.process_connection_options()
|
||||
|
||||
# assert the expected methods have been called once
|
||||
f_process_late_binding_env_vars.assert_called_once()
|
||||
f_boolean_or_cacert.assert_called_once()
|
||||
f_process_option_proxies.assert_called_once()
|
||||
f_process_option_retries.assert_called_once()
|
||||
|
||||
# aseert that the adapter had no interactions (because we mocked out everything we knew about)
|
||||
# the intention here is to catch a situation where process_connection_options has been modified
|
||||
# to do some new behavior, without modifying this test.
|
||||
assert mock_adapter.method_calls == [], 'Unexpected adapter interaction: %r' % mock_adapter.method_calls
|
||||
|
||||
# get_hvac_connection_options
|
||||
# gets the dict of params to pass to the hvac Client constructor
|
||||
# based on the connection options we have in Ansible
|
||||
|
||||
@pytest.mark.parametrize('opt_ca_cert', [None, '/tmp/fake'])
|
||||
@pytest.mark.parametrize('opt_validate_certs', [None, True, False])
|
||||
@pytest.mark.parametrize('opt_namespace', [None, 'namepsace1'])
|
||||
@pytest.mark.parametrize('opt_timeout', [None, 30])
|
||||
@pytest.mark.parametrize('opt_retries', [None, 0, 2, {'total': 3}, '{"total": 3}'])
|
||||
@pytest.mark.parametrize('opt_retry_action', ['ignore', 'warn'])
|
||||
@pytest.mark.parametrize('opt_proxies', [
|
||||
None, 'socks://noshow', '{"https": "https://prox", "http": "http://other"}', {'http': 'socks://one', 'https': 'socks://two'}
|
||||
])
|
||||
def test_get_hvac_connection_options(
|
||||
self, connection_options, predefined_options, adapter,
|
||||
opt_ca_cert, opt_validate_certs, opt_proxies, opt_namespace, opt_timeout, opt_retries, opt_retry_action,
|
||||
):
|
||||
|
||||
option_set = {
|
||||
'ca_cert': opt_ca_cert,
|
||||
'validate_certs': opt_validate_certs,
|
||||
'proxies': opt_proxies,
|
||||
'namespace': opt_namespace,
|
||||
'timeout': opt_timeout,
|
||||
'retries': opt_retries,
|
||||
'retry_action': opt_retry_action,
|
||||
}
|
||||
adapter.set_options(**option_set)
|
||||
|
||||
connection_options.process_connection_options()
|
||||
opts = connection_options.get_hvac_connection_options()
|
||||
|
||||
# these two will get swallowed up to become 'verify'
|
||||
assert 'validate_certs' not in opts
|
||||
assert 'ca_cert' not in opts
|
||||
|
||||
# retry_action is used/removed in the configuration of retries (session)
|
||||
assert 'retry_action' not in opts
|
||||
|
||||
# retries will become session
|
||||
assert 'retries' not in opts
|
||||
|
||||
# these should always be returned
|
||||
assert 'url' in opts and opts['url'] == predefined_options['url']
|
||||
assert 'verify' in opts and opts['verify'] == connection_options._conopt_verify
|
||||
|
||||
# these are optional
|
||||
assert 'proxies' not in opts or opts['proxies'] == predefined_options['proxies']
|
||||
assert 'namespace' not in opts or opts['namespace'] == predefined_options['namespace']
|
||||
assert 'timeout' not in opts or opts['timeout'] == predefined_options['timeout']
|
||||
assert 'session' not in opts or isinstance(opts['session'], Session)
|
||||
|
||||
@mock.patch('ansible_collections.community.hashi_vault.plugins.module_utils._connection_options.HAS_RETRIES', new=False)
|
||||
def test_get_hvac_connection_options_retry_not_available(self, connection_options, adapter):
|
||||
adapter.set_option('retries', 2)
|
||||
|
||||
connection_options.process_connection_options()
|
||||
|
||||
with pytest.raises(NotImplementedError):
|
||||
connection_options.get_hvac_connection_options()
|
||||
|
||||
def test_url_is_required(self, connection_options, adapter):
|
||||
adapter.set_option('url', None)
|
||||
|
||||
with pytest.raises(HashiVaultValueError, match=r'Required option url was not set'):
|
||||
connection_options.process_connection_options()
|
||||
+49
@@ -0,0 +1,49 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright (c) 2021 Brian Scholer (@briantist)
|
||||
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
from __future__ import (absolute_import, division, print_function)
|
||||
__metaclass__ = type
|
||||
|
||||
import os
|
||||
import pytest
|
||||
|
||||
from .....tests.unit.compat import mock
|
||||
from .....plugins.module_utils._hashi_vault_common import (
|
||||
HashiVaultHelper,
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def hashi_vault_helper():
|
||||
return HashiVaultHelper()
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def vault_token():
|
||||
return 'fake123'
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def vault_token_via_env(vault_token):
|
||||
with mock.patch.dict(os.environ, {'VAULT_TOKEN': vault_token}):
|
||||
yield
|
||||
|
||||
|
||||
class TestHashiVaultHelper(object):
|
||||
|
||||
def test_get_vault_client_without_logout_explicit_token(self, hashi_vault_helper, vault_token):
|
||||
client = hashi_vault_helper.get_vault_client(token=vault_token)
|
||||
|
||||
assert client.token == vault_token
|
||||
|
||||
def test_get_vault_client_without_logout_implicit_token(self, hashi_vault_helper, vault_token, vault_token_via_env):
|
||||
client = hashi_vault_helper.get_vault_client(hashi_vault_logout_inferred_token=False)
|
||||
|
||||
assert client.token == vault_token
|
||||
|
||||
def test_get_vault_client_with_logout_implicit_token(self, hashi_vault_helper, vault_token_via_env):
|
||||
client = hashi_vault_helper.get_vault_client(hashi_vault_logout_inferred_token=True)
|
||||
|
||||
assert client.token is None
|
||||
+82
@@ -0,0 +1,82 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright (c) 2021 Brian Scholer (@briantist)
|
||||
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
from __future__ import (absolute_import, division, print_function)
|
||||
__metaclass__ = type
|
||||
|
||||
import os
|
||||
import pytest
|
||||
|
||||
from ansible_collections.community.hashi_vault.tests.unit.compat import mock
|
||||
|
||||
from ansible_collections.community.hashi_vault.plugins.module_utils._hashi_vault_common import (
|
||||
HashiVaultOptionGroupBase,
|
||||
HashiVaultOptionAdapter,
|
||||
)
|
||||
|
||||
|
||||
PREREAD_OPTIONS = {
|
||||
'opt1': 'val1',
|
||||
'opt2': None,
|
||||
'opt3': 'val3',
|
||||
'opt4': None,
|
||||
# no opt5
|
||||
'opt6': None,
|
||||
}
|
||||
|
||||
LOW_PREF_DEF = {
|
||||
'opt1': dict(env=['_ENV_1A'], default='never'),
|
||||
'opt2': dict(env=['_ENV_2A', '_ENV_2B']),
|
||||
'opt4': dict(env=['_ENV_4A', '_ENV_4B', '_ENV_4C']),
|
||||
'opt5': dict(env=['_ENV_5A']),
|
||||
'opt6': dict(env=['_ENV_6A'], default='mosdefault'),
|
||||
}
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def preread_options():
|
||||
return PREREAD_OPTIONS.copy()
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def adapter(preread_options):
|
||||
return HashiVaultOptionAdapter.from_dict(preread_options)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def option_group_base(adapter):
|
||||
return HashiVaultOptionGroupBase(adapter)
|
||||
|
||||
|
||||
@pytest.fixture(params=[
|
||||
# first dict is used to patch the environment vars
|
||||
# second dict is used to patch the current options to get them to the expected state
|
||||
#
|
||||
# envpatch, expatch
|
||||
({}, {'opt6': 'mosdefault'}),
|
||||
({'_ENV_1A': 'alt1a'}, {'opt6': 'mosdefault'}),
|
||||
({'_ENV_3X': 'noop3x'}, {'opt6': 'mosdefault'}),
|
||||
({'_ENV_2B': 'alt2b'}, {'opt2': 'alt2b', 'opt6': 'mosdefault'}),
|
||||
({'_ENV_2A': 'alt2a', '_ENV_2B': 'alt2b'}, {'opt2': 'alt2a', 'opt6': 'mosdefault'}),
|
||||
({'_ENV_4B': 'alt4b', '_ENV_6A': 'defnot', '_ENV_4C': 'alt4c'}, {'opt4': 'alt4b', 'opt6': 'defnot'}),
|
||||
({'_ENV_1A': 'alt1a', '_ENV_4A': 'alt4a', '_ENV_1B': 'noop1b', '_ENV_4C': 'alt4c'}, {'opt4': 'alt4a', 'opt6': 'mosdefault'}),
|
||||
({'_ENV_5A': 'noop5a', '_ENV_4C': 'alt4c', '_ENV_2A': 'alt2a'}, {'opt2': 'alt2a', 'opt4': 'alt4c', 'opt6': 'mosdefault'}),
|
||||
])
|
||||
def with_env(request, preread_options):
|
||||
envpatch, expatch = request.param
|
||||
|
||||
expected = preread_options.copy()
|
||||
expected.update(expatch)
|
||||
|
||||
with mock.patch.dict(os.environ, envpatch):
|
||||
yield expected
|
||||
|
||||
|
||||
class TestHashiVaultOptionGroupBase(object):
|
||||
|
||||
def test_process_late_binding_env_vars(self, option_group_base, with_env, preread_options):
|
||||
option_group_base.process_late_binding_env_vars(LOW_PREF_DEF)
|
||||
|
||||
assert preread_options == with_env, "Expected: %r\nGot: %r" % (with_env, preread_options)
|
||||
+76
@@ -0,0 +1,76 @@
|
||||
# Copyright (c) 2022 Brian Scholer (@briantist)
|
||||
# Copyright (c) 2017 Ansible Project
|
||||
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
from __future__ import (absolute_import, division, print_function)
|
||||
__metaclass__ = type
|
||||
|
||||
import json
|
||||
|
||||
import pytest
|
||||
|
||||
from ansible.module_utils.six import string_types
|
||||
from ansible.module_utils.common.text.converters import to_bytes
|
||||
from ansible.module_utils.common._collections_compat import MutableMapping, Sequence
|
||||
|
||||
from ...compat import mock
|
||||
|
||||
|
||||
def pytest_configure(config):
|
||||
config.addinivalue_line(
|
||||
"markers", "no_ansible_module_patch: causes the patch_ansible_module fixture to have no effect"
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def module_warn():
|
||||
return mock.MagicMock()
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def patch_ansible_module(request, module_warn):
|
||||
def _process(param):
|
||||
if isinstance(param, string_types):
|
||||
args = param
|
||||
_yield = args
|
||||
return (args, _yield)
|
||||
elif isinstance(param, MutableMapping):
|
||||
if '_yield' in param:
|
||||
y = param.pop('_yield')
|
||||
_yield = dict((k, v) for k, v in param.items() if k in y)
|
||||
else:
|
||||
_yield = param
|
||||
|
||||
if 'ANSIBLE_MODULE_ARGS' not in param:
|
||||
param = {'ANSIBLE_MODULE_ARGS': param}
|
||||
if '_ansible_remote_tmp' not in param['ANSIBLE_MODULE_ARGS']:
|
||||
param['ANSIBLE_MODULE_ARGS']['_ansible_remote_tmp'] = '/tmp'
|
||||
if '_ansible_keep_remote_files' not in param['ANSIBLE_MODULE_ARGS']:
|
||||
param['ANSIBLE_MODULE_ARGS']['_ansible_keep_remote_files'] = False
|
||||
args = json.dumps(param)
|
||||
return (args, _yield)
|
||||
elif isinstance(param, Sequence):
|
||||
# First item should be a dict that serves as the base of options,
|
||||
# use it for things that aren't being parametrized.
|
||||
# Each of the remaining items is the name of a fixture whose name
|
||||
# begins with opt_ (but without the opt_ prefix), and we will look those up.
|
||||
if not isinstance(param[0], MutableMapping):
|
||||
raise Exception('First value in patch_ansible_module array param must be a dict')
|
||||
|
||||
margs = param[0]
|
||||
for fixt in param[1:]:
|
||||
margs[fixt] = request.getfixturevalue('opt_' + fixt)
|
||||
|
||||
return _process(margs)
|
||||
else:
|
||||
raise Exception('Malformed data to the patch_ansible_module pytest fixture')
|
||||
|
||||
if 'no_ansible_module_patch' in request.keywords:
|
||||
yield
|
||||
else:
|
||||
args, _yield = _process(request.param)
|
||||
with mock.patch('ansible.module_utils.basic._ANSIBLE_ARGS', to_bytes(args)):
|
||||
# TODO: in 2.10+ we can patch basic.warn instead of basic.AnsibleModule.warn
|
||||
with mock.patch('ansible.module_utils.basic.AnsibleModule.warn', module_warn):
|
||||
yield _yield
|
||||
+195
@@ -0,0 +1,195 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright (c) 2024 Brian Scholer (@briantist)
|
||||
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
from __future__ import absolute_import, division, print_function
|
||||
|
||||
__metaclass__ = type
|
||||
|
||||
import pytest
|
||||
import re
|
||||
import json
|
||||
|
||||
from ansible.module_utils.basic import missing_required_lib
|
||||
|
||||
from ...compat import mock
|
||||
from .....plugins.modules import vault_database_connection_configure
|
||||
from .....plugins.module_utils._hashi_vault_common import HashiVaultValueError
|
||||
|
||||
|
||||
hvac = pytest.importorskip("hvac")
|
||||
|
||||
|
||||
pytestmark = pytest.mark.usefixtures(
|
||||
"patch_ansible_module",
|
||||
"patch_authenticator",
|
||||
"patch_get_vault_client",
|
||||
)
|
||||
|
||||
|
||||
def _connection_options():
|
||||
return {
|
||||
"auth_method": "token",
|
||||
"url": "http://myvault",
|
||||
"token": "beep-boop",
|
||||
}
|
||||
|
||||
|
||||
def _sample_options():
|
||||
return {
|
||||
"engine_mount_point": "dbmount",
|
||||
}
|
||||
|
||||
|
||||
def _sample_connection():
|
||||
return {
|
||||
"connection_name": "foo",
|
||||
"plugin_name": "bar",
|
||||
"allowed_roles": [
|
||||
"baz",
|
||||
],
|
||||
"connection_url": "postgresql://{{'{{username}}'}}:{{'{{password}}'}}@postgres:5432/postgres?sslmode=disable",
|
||||
"connection_username": "SomeUser",
|
||||
"connection_password": "SomePass",
|
||||
}
|
||||
|
||||
|
||||
def _combined_options(**kwargs):
|
||||
opt = _connection_options()
|
||||
opt.update(_sample_options())
|
||||
opt.update(_sample_connection())
|
||||
opt.update(kwargs)
|
||||
return opt
|
||||
|
||||
|
||||
class TestModuleVaultDatabaseConnectionConfigure:
|
||||
@pytest.mark.parametrize(
|
||||
"patch_ansible_module", [_combined_options()], indirect=True
|
||||
)
|
||||
@pytest.mark.parametrize(
|
||||
"exc",
|
||||
[HashiVaultValueError("throwaway msg"), NotImplementedError("throwaway msg")],
|
||||
)
|
||||
def test_vault_database_connection_configure_authentication_error(
|
||||
self, authenticator, exc, capfd
|
||||
):
|
||||
authenticator.authenticate.side_effect = exc
|
||||
|
||||
with pytest.raises(SystemExit) as e:
|
||||
vault_database_connection_configure.main()
|
||||
|
||||
out, err = capfd.readouterr()
|
||||
result = json.loads(out)
|
||||
|
||||
assert e.value.code != 0, "result: %r" % (result,)
|
||||
assert result["msg"] == "throwaway msg", "result: %r" % result
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"patch_ansible_module", [_combined_options()], indirect=True
|
||||
)
|
||||
@pytest.mark.parametrize(
|
||||
"exc",
|
||||
[HashiVaultValueError("throwaway msg"), NotImplementedError("throwaway msg")],
|
||||
)
|
||||
def test_vault_database_connection_configure_auth_validation_error(
|
||||
self, authenticator, exc, capfd
|
||||
):
|
||||
authenticator.validate.side_effect = exc
|
||||
|
||||
with pytest.raises(SystemExit) as e:
|
||||
vault_database_connection_configure.main()
|
||||
|
||||
out, err = capfd.readouterr()
|
||||
result = json.loads(out)
|
||||
|
||||
assert e.value.code != 0, "result: %r" % (result,)
|
||||
assert result["msg"] == "throwaway msg"
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"patch_ansible_module", [_combined_options()], indirect=True
|
||||
)
|
||||
def test_vault_database_connection_configure_no_hvac(self, capfd):
|
||||
with mock.patch.multiple(
|
||||
vault_database_connection_configure,
|
||||
HAS_HVAC=False,
|
||||
HVAC_IMPORT_ERROR=None,
|
||||
create=True,
|
||||
):
|
||||
with pytest.raises(SystemExit) as e:
|
||||
vault_database_connection_configure.main()
|
||||
|
||||
out, err = capfd.readouterr()
|
||||
result = json.loads(out)
|
||||
|
||||
assert e.value.code != 0, "result: %r" % (result,)
|
||||
assert result["msg"] == missing_required_lib("hvac")
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"exc",
|
||||
[
|
||||
(
|
||||
hvac.exceptions.Forbidden,
|
||||
"",
|
||||
r"^Forbidden: Permission Denied to path \['([^']+)'\]",
|
||||
),
|
||||
(
|
||||
hvac.exceptions.InvalidPath,
|
||||
"",
|
||||
r"^Invalid or missing path \['([^']+)/config/([^']+)'\]",
|
||||
),
|
||||
],
|
||||
)
|
||||
@pytest.mark.parametrize(
|
||||
"patch_ansible_module",
|
||||
[[_combined_options(), "engine_mount_point"]],
|
||||
indirect=True,
|
||||
)
|
||||
@pytest.mark.parametrize("opt_engine_mount_point", ["path/1", "second/path"])
|
||||
def test_vault_database_connection_configure_vault_exception(
|
||||
self, vault_client, exc, opt_engine_mount_point, capfd
|
||||
):
|
||||
|
||||
client = vault_client
|
||||
client.secrets.database.configure.side_effect = exc[0](exc[1])
|
||||
|
||||
with pytest.raises(SystemExit) as e:
|
||||
vault_database_connection_configure.main()
|
||||
|
||||
out, err = capfd.readouterr()
|
||||
result = json.loads(out)
|
||||
|
||||
assert e.value.code != 0, "result: %r" % (result,)
|
||||
match = re.search(exc[2], result["msg"])
|
||||
assert match is not None, "result: %r\ndid not match: %s" % (result, exc[2])
|
||||
|
||||
assert opt_engine_mount_point == match.group(1)
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"patch_ansible_module", [_combined_options()], indirect=True
|
||||
)
|
||||
def test_vault_database_connection_configure_success(
|
||||
self, patch_ansible_module, empty_response, vault_client, capfd
|
||||
):
|
||||
client = vault_client
|
||||
client.secrets.database.configure.return_value = empty_response
|
||||
|
||||
with pytest.raises(SystemExit) as e:
|
||||
vault_database_connection_configure.main()
|
||||
|
||||
out, err = capfd.readouterr()
|
||||
result = json.loads(out)
|
||||
|
||||
assert e.value.code == 0, "result: %r" % (result,)
|
||||
|
||||
client.secrets.database.configure.assert_called_once_with(
|
||||
mount_point=patch_ansible_module["engine_mount_point"],
|
||||
name=patch_ansible_module["connection_name"],
|
||||
plugin_name=patch_ansible_module["plugin_name"],
|
||||
allowed_roles=patch_ansible_module["allowed_roles"],
|
||||
connection_url=patch_ansible_module["connection_url"],
|
||||
username=patch_ansible_module["connection_username"],
|
||||
password=patch_ansible_module["connection_password"],
|
||||
)
|
||||
|
||||
assert result["changed"] is True
|
||||
+181
@@ -0,0 +1,181 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright (c) 2024 Brian Scholer (@briantist)
|
||||
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
from __future__ import absolute_import, division, print_function
|
||||
|
||||
__metaclass__ = type
|
||||
|
||||
import pytest
|
||||
import re
|
||||
import json
|
||||
|
||||
from ansible.module_utils.basic import missing_required_lib
|
||||
|
||||
from ...compat import mock
|
||||
from .....plugins.modules import vault_database_connection_delete
|
||||
from .....plugins.module_utils._hashi_vault_common import HashiVaultValueError
|
||||
|
||||
|
||||
hvac = pytest.importorskip("hvac")
|
||||
|
||||
|
||||
pytestmark = pytest.mark.usefixtures(
|
||||
"patch_ansible_module",
|
||||
"patch_authenticator",
|
||||
"patch_get_vault_client",
|
||||
)
|
||||
|
||||
|
||||
def _connection_options():
|
||||
return {
|
||||
"auth_method": "token",
|
||||
"url": "http://myvault",
|
||||
"token": "beep-boop",
|
||||
}
|
||||
|
||||
|
||||
def _sample_options():
|
||||
return {
|
||||
"engine_mount_point": "dbmount",
|
||||
}
|
||||
|
||||
|
||||
def _sample_connection():
|
||||
return {"connection_name": "foo"}
|
||||
|
||||
|
||||
def _combined_options(**kwargs):
|
||||
opt = _connection_options()
|
||||
opt.update(_sample_options())
|
||||
opt.update(_sample_connection())
|
||||
opt.update(kwargs)
|
||||
return opt
|
||||
|
||||
|
||||
class TestModuleVaultDatabaseConnectionDelete:
|
||||
@pytest.mark.parametrize(
|
||||
"patch_ansible_module", [_combined_options()], indirect=True
|
||||
)
|
||||
@pytest.mark.parametrize(
|
||||
"exc",
|
||||
[HashiVaultValueError("throwaway msg"), NotImplementedError("throwaway msg")],
|
||||
)
|
||||
def test_vault_database_connection_delete_authentication_error(
|
||||
self, authenticator, exc, capfd
|
||||
):
|
||||
authenticator.authenticate.side_effect = exc
|
||||
|
||||
with pytest.raises(SystemExit) as e:
|
||||
vault_database_connection_delete.main()
|
||||
|
||||
out, err = capfd.readouterr()
|
||||
result = json.loads(out)
|
||||
|
||||
assert e.value.code != 0, "result: %r" % (result,)
|
||||
assert result["msg"] == "throwaway msg", "result: %r" % result
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"patch_ansible_module", [_combined_options()], indirect=True
|
||||
)
|
||||
@pytest.mark.parametrize(
|
||||
"exc",
|
||||
[HashiVaultValueError("throwaway msg"), NotImplementedError("throwaway msg")],
|
||||
)
|
||||
def test_vault_database_connection_delete_auth_validation_error(
|
||||
self, authenticator, exc, capfd
|
||||
):
|
||||
authenticator.validate.side_effect = exc
|
||||
|
||||
with pytest.raises(SystemExit) as e:
|
||||
vault_database_connection_delete.main()
|
||||
|
||||
out, err = capfd.readouterr()
|
||||
result = json.loads(out)
|
||||
|
||||
assert e.value.code != 0, "result: %r" % (result,)
|
||||
assert result["msg"] == "throwaway msg"
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"patch_ansible_module", [_combined_options()], indirect=True
|
||||
)
|
||||
def test_vault_database_connection_delete_success(
|
||||
self, patch_ansible_module, empty_response, vault_client, capfd
|
||||
):
|
||||
client = vault_client
|
||||
client.secrets.database.delete_connection.return_value = empty_response
|
||||
|
||||
with pytest.raises(SystemExit) as e:
|
||||
vault_database_connection_delete.main()
|
||||
|
||||
out, err = capfd.readouterr()
|
||||
result = json.loads(out)
|
||||
|
||||
assert e.value.code == 0, "result: %r" % (result,)
|
||||
|
||||
client.secrets.database.delete_connection.assert_called_once_with(
|
||||
mount_point=patch_ansible_module["engine_mount_point"],
|
||||
name=patch_ansible_module["connection_name"],
|
||||
)
|
||||
|
||||
assert result["changed"] is True
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"patch_ansible_module", [_combined_options()], indirect=True
|
||||
)
|
||||
def test_vault_database_connection_delete_no_hvac(self, capfd):
|
||||
with mock.patch.multiple(
|
||||
vault_database_connection_delete,
|
||||
HAS_HVAC=False,
|
||||
HVAC_IMPORT_ERROR=None,
|
||||
create=True,
|
||||
):
|
||||
with pytest.raises(SystemExit) as e:
|
||||
vault_database_connection_delete.main()
|
||||
|
||||
out, err = capfd.readouterr()
|
||||
result = json.loads(out)
|
||||
|
||||
assert e.value.code != 0, "result: %r" % (result,)
|
||||
assert result["msg"] == missing_required_lib("hvac")
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"exc",
|
||||
[
|
||||
(
|
||||
hvac.exceptions.Forbidden,
|
||||
"",
|
||||
r"^Forbidden: Permission Denied to path \['([^']+)'\]",
|
||||
),
|
||||
(
|
||||
hvac.exceptions.InvalidPath,
|
||||
"",
|
||||
r"^Invalid or missing path \['([^']+)/config/([^']+)'\]",
|
||||
),
|
||||
],
|
||||
)
|
||||
@pytest.mark.parametrize(
|
||||
"patch_ansible_module",
|
||||
[[_combined_options(), "engine_mount_point"]],
|
||||
indirect=True,
|
||||
)
|
||||
@pytest.mark.parametrize("opt_engine_mount_point", ["path/1", "second/path"])
|
||||
def test_vault_database_connection_delete_vault_exception(
|
||||
self, vault_client, exc, opt_engine_mount_point, capfd
|
||||
):
|
||||
|
||||
client = vault_client
|
||||
client.secrets.database.delete_connection.side_effect = exc[0](exc[1])
|
||||
|
||||
with pytest.raises(SystemExit) as e:
|
||||
vault_database_connection_delete.main()
|
||||
|
||||
out, err = capfd.readouterr()
|
||||
result = json.loads(out)
|
||||
|
||||
assert e.value.code != 0, "result: %r" % (result,)
|
||||
match = re.search(exc[2], result["msg"])
|
||||
assert match is not None, "result: %r\ndid not match: %s" % (result, exc[2])
|
||||
|
||||
assert opt_engine_mount_point == match.group(1)
|
||||
+200
@@ -0,0 +1,200 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright (c) 2024 Brian Scholer (@briantist)
|
||||
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
from __future__ import absolute_import, division, print_function
|
||||
|
||||
__metaclass__ = type
|
||||
|
||||
import pytest
|
||||
import re
|
||||
import json
|
||||
|
||||
from ansible.module_utils.basic import missing_required_lib
|
||||
|
||||
from ...compat import mock
|
||||
from .....plugins.modules import vault_database_connection_read
|
||||
from .....plugins.module_utils._hashi_vault_common import HashiVaultValueError
|
||||
|
||||
|
||||
hvac = pytest.importorskip("hvac")
|
||||
|
||||
|
||||
pytestmark = pytest.mark.usefixtures(
|
||||
"patch_ansible_module",
|
||||
"patch_authenticator",
|
||||
"patch_get_vault_client",
|
||||
)
|
||||
|
||||
|
||||
def _connection_options():
|
||||
return {
|
||||
"auth_method": "token",
|
||||
"url": "http://myvault",
|
||||
"token": "beep-boop",
|
||||
}
|
||||
|
||||
|
||||
def _sample_options():
|
||||
return {
|
||||
"engine_mount_point": "dbmount",
|
||||
}
|
||||
|
||||
|
||||
def _sample_connection():
|
||||
return {"connection_name": "foo"}
|
||||
|
||||
|
||||
def _combined_options(**kwargs):
|
||||
opt = _connection_options()
|
||||
opt.update(_sample_options())
|
||||
opt.update(_sample_connection())
|
||||
opt.update(kwargs)
|
||||
return opt
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def list_response(fixture_loader):
|
||||
return fixture_loader("database_connection_read_response.json")
|
||||
|
||||
|
||||
class TestModuleVaultDatabaseConnectionRead:
|
||||
@pytest.mark.parametrize(
|
||||
"patch_ansible_module", [_combined_options()], indirect=True
|
||||
)
|
||||
@pytest.mark.parametrize(
|
||||
"exc",
|
||||
[HashiVaultValueError("throwaway msg"), NotImplementedError("throwaway msg")],
|
||||
)
|
||||
def test_vault_database_connection_read_authentication_error(
|
||||
self, authenticator, exc, capfd
|
||||
):
|
||||
authenticator.authenticate.side_effect = exc
|
||||
|
||||
with pytest.raises(SystemExit) as e:
|
||||
vault_database_connection_read.main()
|
||||
|
||||
out, err = capfd.readouterr()
|
||||
result = json.loads(out)
|
||||
|
||||
assert e.value.code != 0, "result: %r" % (result,)
|
||||
assert result["msg"] == "throwaway msg", "result: %r" % result
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"patch_ansible_module", [_combined_options()], indirect=True
|
||||
)
|
||||
@pytest.mark.parametrize(
|
||||
"exc",
|
||||
[HashiVaultValueError("throwaway msg"), NotImplementedError("throwaway msg")],
|
||||
)
|
||||
def test_vault_database_connection_read_auth_validation_error(
|
||||
self, authenticator, exc, capfd
|
||||
):
|
||||
authenticator.validate.side_effect = exc
|
||||
|
||||
with pytest.raises(SystemExit) as e:
|
||||
vault_database_connection_read.main()
|
||||
|
||||
out, err = capfd.readouterr()
|
||||
result = json.loads(out)
|
||||
|
||||
assert e.value.code != 0, "result: %r" % (result,)
|
||||
assert result["msg"] == "throwaway msg"
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"patch_ansible_module", [_combined_options()], indirect=True
|
||||
)
|
||||
def test_vault_database_connection_read_return_data(
|
||||
self, patch_ansible_module, list_response, vault_client, capfd
|
||||
):
|
||||
client = vault_client
|
||||
client.secrets.database.read_connection.return_value = list_response.copy()
|
||||
|
||||
with pytest.raises(SystemExit) as e:
|
||||
vault_database_connection_read.main()
|
||||
|
||||
out, err = capfd.readouterr()
|
||||
result = json.loads(out)
|
||||
|
||||
assert e.value.code == 0, "result: %r" % (result,)
|
||||
|
||||
client.secrets.database.read_connection.assert_called_once_with(
|
||||
mount_point=patch_ansible_module["engine_mount_point"],
|
||||
name=patch_ansible_module["connection_name"],
|
||||
)
|
||||
|
||||
raw = list_response.copy()
|
||||
data = raw["data"]
|
||||
|
||||
assert (
|
||||
result["raw"] == raw
|
||||
), "module result did not match expected result:\nexpected: %r\ngot: %r" % (
|
||||
list_response,
|
||||
result,
|
||||
)
|
||||
assert (
|
||||
result["data"] == data
|
||||
), "module result did not match expected result:\nexpected: %r\ngot: %r" % (
|
||||
list_response,
|
||||
result,
|
||||
)
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"patch_ansible_module", [_combined_options()], indirect=True
|
||||
)
|
||||
def test_vault_database_connection_read_no_hvac(self, capfd):
|
||||
with mock.patch.multiple(
|
||||
vault_database_connection_read,
|
||||
HAS_HVAC=False,
|
||||
HVAC_IMPORT_ERROR=None,
|
||||
create=True,
|
||||
):
|
||||
with pytest.raises(SystemExit) as e:
|
||||
vault_database_connection_read.main()
|
||||
|
||||
out, err = capfd.readouterr()
|
||||
result = json.loads(out)
|
||||
|
||||
assert e.value.code != 0, "result: %r" % (result,)
|
||||
assert result["msg"] == missing_required_lib("hvac")
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"exc",
|
||||
[
|
||||
(
|
||||
hvac.exceptions.Forbidden,
|
||||
"",
|
||||
r"^Forbidden: Permission Denied to path \['([^']+)'\]",
|
||||
),
|
||||
(
|
||||
hvac.exceptions.InvalidPath,
|
||||
"",
|
||||
r"^Invalid or missing path \['([^']+)/config/([^']+)'\]",
|
||||
),
|
||||
],
|
||||
)
|
||||
@pytest.mark.parametrize(
|
||||
"patch_ansible_module",
|
||||
[[_combined_options(), "engine_mount_point"]],
|
||||
indirect=True,
|
||||
)
|
||||
@pytest.mark.parametrize("opt_engine_mount_point", ["path/1", "second/path"])
|
||||
def test_vault_database_connection_read_vault_exception(
|
||||
self, vault_client, exc, opt_engine_mount_point, capfd
|
||||
):
|
||||
|
||||
client = vault_client
|
||||
client.secrets.database.read_connection.side_effect = exc[0](exc[1])
|
||||
|
||||
with pytest.raises(SystemExit) as e:
|
||||
vault_database_connection_read.main()
|
||||
|
||||
out, err = capfd.readouterr()
|
||||
result = json.loads(out)
|
||||
|
||||
assert e.value.code != 0, "result: %r" % (result,)
|
||||
match = re.search(exc[2], result["msg"])
|
||||
assert match is not None, "result: %r\ndid not match: %s" % (result, exc[2])
|
||||
|
||||
assert opt_engine_mount_point == match.group(1)
|
||||
+181
@@ -0,0 +1,181 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright (c) 2024 Brian Scholer (@briantist)
|
||||
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
from __future__ import absolute_import, division, print_function
|
||||
|
||||
__metaclass__ = type
|
||||
|
||||
import pytest
|
||||
import re
|
||||
import json
|
||||
|
||||
from ansible.module_utils.basic import missing_required_lib
|
||||
|
||||
from ...compat import mock
|
||||
from .....plugins.modules import vault_database_connection_reset
|
||||
from .....plugins.module_utils._hashi_vault_common import HashiVaultValueError
|
||||
|
||||
|
||||
hvac = pytest.importorskip("hvac")
|
||||
|
||||
|
||||
pytestmark = pytest.mark.usefixtures(
|
||||
"patch_ansible_module",
|
||||
"patch_authenticator",
|
||||
"patch_get_vault_client",
|
||||
)
|
||||
|
||||
|
||||
def _connection_options():
|
||||
return {
|
||||
"auth_method": "token",
|
||||
"url": "http://myvault",
|
||||
"token": "beep-boop",
|
||||
}
|
||||
|
||||
|
||||
def _sample_options():
|
||||
return {
|
||||
"engine_mount_point": "dbmount",
|
||||
}
|
||||
|
||||
|
||||
def _sample_connection():
|
||||
return {"connection_name": "foo"}
|
||||
|
||||
|
||||
def _combined_options(**kwargs):
|
||||
opt = _connection_options()
|
||||
opt.update(_sample_options())
|
||||
opt.update(_sample_connection())
|
||||
opt.update(kwargs)
|
||||
return opt
|
||||
|
||||
|
||||
class TestModuleVaultDatabaseConnectionReset:
|
||||
@pytest.mark.parametrize(
|
||||
"patch_ansible_module", [_combined_options()], indirect=True
|
||||
)
|
||||
@pytest.mark.parametrize(
|
||||
"exc",
|
||||
[HashiVaultValueError("throwaway msg"), NotImplementedError("throwaway msg")],
|
||||
)
|
||||
def test_vault_database_connection_reset_authentication_error(
|
||||
self, authenticator, exc, capfd
|
||||
):
|
||||
authenticator.authenticate.side_effect = exc
|
||||
|
||||
with pytest.raises(SystemExit) as e:
|
||||
vault_database_connection_reset.main()
|
||||
|
||||
out, err = capfd.readouterr()
|
||||
result = json.loads(out)
|
||||
|
||||
assert e.value.code != 0, "result: %r" % (result,)
|
||||
assert result["msg"] == "throwaway msg", "result: %r" % result
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"patch_ansible_module", [_combined_options()], indirect=True
|
||||
)
|
||||
@pytest.mark.parametrize(
|
||||
"exc",
|
||||
[HashiVaultValueError("throwaway msg"), NotImplementedError("throwaway msg")],
|
||||
)
|
||||
def test_vault_database_connection_reset_auth_validation_error(
|
||||
self, authenticator, exc, capfd
|
||||
):
|
||||
authenticator.validate.side_effect = exc
|
||||
|
||||
with pytest.raises(SystemExit) as e:
|
||||
vault_database_connection_reset.main()
|
||||
|
||||
out, err = capfd.readouterr()
|
||||
result = json.loads(out)
|
||||
|
||||
assert e.value.code != 0, "result: %r" % (result,)
|
||||
assert result["msg"] == "throwaway msg"
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"patch_ansible_module", [_combined_options()], indirect=True
|
||||
)
|
||||
def test_vault_database_connection_reset_success(
|
||||
self, patch_ansible_module, empty_response, vault_client, capfd
|
||||
):
|
||||
client = vault_client
|
||||
client.secrets.database.reset_connection.return_value = empty_response
|
||||
|
||||
with pytest.raises(SystemExit) as e:
|
||||
vault_database_connection_reset.main()
|
||||
|
||||
out, err = capfd.readouterr()
|
||||
result = json.loads(out)
|
||||
|
||||
assert e.value.code == 0, "result: %r" % (result,)
|
||||
|
||||
client.secrets.database.reset_connection.assert_called_once_with(
|
||||
mount_point=patch_ansible_module["engine_mount_point"],
|
||||
name=patch_ansible_module["connection_name"],
|
||||
)
|
||||
|
||||
assert result["changed"] is True
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"patch_ansible_module", [_combined_options()], indirect=True
|
||||
)
|
||||
def test_vault_database_connection_reset_no_hvac(self, capfd):
|
||||
with mock.patch.multiple(
|
||||
vault_database_connection_reset,
|
||||
HAS_HVAC=False,
|
||||
HVAC_IMPORT_ERROR=None,
|
||||
create=True,
|
||||
):
|
||||
with pytest.raises(SystemExit) as e:
|
||||
vault_database_connection_reset.main()
|
||||
|
||||
out, err = capfd.readouterr()
|
||||
result = json.loads(out)
|
||||
|
||||
assert e.value.code != 0, "result: %r" % (result,)
|
||||
assert result["msg"] == missing_required_lib("hvac")
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"exc",
|
||||
[
|
||||
(
|
||||
hvac.exceptions.Forbidden,
|
||||
"",
|
||||
r"^Forbidden: Permission Denied to path \['([^']+)'\]",
|
||||
),
|
||||
(
|
||||
hvac.exceptions.InvalidPath,
|
||||
"",
|
||||
r"^Invalid or missing path \['([^']+)/reset/([^']+)'\]",
|
||||
),
|
||||
],
|
||||
)
|
||||
@pytest.mark.parametrize(
|
||||
"patch_ansible_module",
|
||||
[[_combined_options(), "engine_mount_point"]],
|
||||
indirect=True,
|
||||
)
|
||||
@pytest.mark.parametrize("opt_engine_mount_point", ["path/1", "second/path"])
|
||||
def test_vault_database_connection_reset_vault_exception(
|
||||
self, vault_client, exc, opt_engine_mount_point, capfd
|
||||
):
|
||||
|
||||
client = vault_client
|
||||
client.secrets.database.reset_connection.side_effect = exc[0](exc[1])
|
||||
|
||||
with pytest.raises(SystemExit) as e:
|
||||
vault_database_connection_reset.main()
|
||||
|
||||
out, err = capfd.readouterr()
|
||||
result = json.loads(out)
|
||||
|
||||
assert e.value.code != 0, "result: %r" % (result,)
|
||||
match = re.search(exc[2], result["msg"])
|
||||
assert match is not None, "result: %r\ndid not match: %s" % (result, exc[2])
|
||||
|
||||
assert opt_engine_mount_point == match.group(1)
|
||||
+228
@@ -0,0 +1,228 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright (c) 2024 Martin Chmielewski (@M4rt1nCh)
|
||||
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
from __future__ import absolute_import, division, print_function
|
||||
|
||||
__metaclass__ = type
|
||||
|
||||
import pytest
|
||||
import re
|
||||
import json
|
||||
|
||||
from ansible.module_utils.basic import missing_required_lib
|
||||
|
||||
from ...compat import mock
|
||||
from .....plugins.modules import vault_database_connections_list
|
||||
from .....plugins.module_utils._hashi_vault_common import HashiVaultValueError
|
||||
|
||||
|
||||
hvac = pytest.importorskip("hvac")
|
||||
|
||||
|
||||
pytestmark = pytest.mark.usefixtures(
|
||||
"patch_ansible_module",
|
||||
"patch_authenticator",
|
||||
"patch_get_vault_client",
|
||||
)
|
||||
|
||||
|
||||
def _connection_options():
|
||||
return {
|
||||
"auth_method": "token",
|
||||
"url": "http://myvault",
|
||||
"token": "beep-boop",
|
||||
}
|
||||
|
||||
|
||||
def _sample_options():
|
||||
return {
|
||||
"engine_mount_point": "dbmount",
|
||||
}
|
||||
|
||||
|
||||
def _combined_options(**kwargs):
|
||||
opt = _connection_options()
|
||||
opt.update(_sample_options())
|
||||
opt.update(kwargs)
|
||||
return opt
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def list_response(fixture_loader):
|
||||
return fixture_loader("database_connections_list_response.json")
|
||||
|
||||
|
||||
class TestModuleVaultDatabaseConnectionsList:
|
||||
@pytest.mark.parametrize(
|
||||
"patch_ansible_module", [_combined_options()], indirect=True
|
||||
)
|
||||
@pytest.mark.parametrize(
|
||||
"exc",
|
||||
[HashiVaultValueError("throwaway msg"), NotImplementedError("throwaway msg")],
|
||||
)
|
||||
def test_vault_database_connections_list_authentication_error(
|
||||
self, authenticator, exc, capfd
|
||||
):
|
||||
authenticator.authenticate.side_effect = exc
|
||||
|
||||
with pytest.raises(SystemExit) as e:
|
||||
vault_database_connections_list.main()
|
||||
|
||||
out, err = capfd.readouterr()
|
||||
result = json.loads(out)
|
||||
|
||||
assert e.value.code != 0, "result: %r" % (result,)
|
||||
assert result["msg"] == "throwaway msg", "result: %r" % result
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"patch_ansible_module", [_combined_options()], indirect=True
|
||||
)
|
||||
@pytest.mark.parametrize(
|
||||
"exc",
|
||||
[HashiVaultValueError("throwaway msg"), NotImplementedError("throwaway msg")],
|
||||
)
|
||||
def test_vault_database_connections_list_auth_validation_error(
|
||||
self, authenticator, exc, capfd
|
||||
):
|
||||
authenticator.validate.side_effect = exc
|
||||
|
||||
with pytest.raises(SystemExit) as e:
|
||||
vault_database_connections_list.main()
|
||||
|
||||
out, err = capfd.readouterr()
|
||||
result = json.loads(out)
|
||||
|
||||
assert e.value.code != 0, "result: %r" % (result,)
|
||||
assert result["msg"] == "throwaway msg"
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"patch_ansible_module", [_combined_options()], indirect=True
|
||||
)
|
||||
def test_vault_database_connections_list_return_data(
|
||||
self, patch_ansible_module, list_response, vault_client, capfd
|
||||
):
|
||||
client = vault_client
|
||||
client.secrets.database.list_connections.return_value = list_response.copy()
|
||||
|
||||
with pytest.raises(SystemExit) as e:
|
||||
vault_database_connections_list.main()
|
||||
|
||||
out, err = capfd.readouterr()
|
||||
result = json.loads(out)
|
||||
|
||||
assert e.value.code == 0, "result: %r" % (result,)
|
||||
|
||||
client.secrets.database.list_connections.assert_called_once_with(
|
||||
mount_point=patch_ansible_module["engine_mount_point"]
|
||||
)
|
||||
|
||||
raw = list_response.copy()
|
||||
data = raw["data"]
|
||||
roles = data["keys"]
|
||||
|
||||
assert (
|
||||
result["raw"] == raw
|
||||
), "module result did not match expected result:\nexpected: %r\ngot: %r" % (
|
||||
list_response,
|
||||
result,
|
||||
)
|
||||
assert (
|
||||
result["data"] == data
|
||||
), "module result did not match expected result:\nexpected: %r\ngot: %r" % (
|
||||
list_response,
|
||||
result,
|
||||
)
|
||||
assert (
|
||||
result["connections"] == roles
|
||||
), "module result did not match expected result:\nexpected: %r\ngot: %r" % (
|
||||
list_response,
|
||||
result,
|
||||
)
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"patch_ansible_module", [_combined_options()], indirect=True
|
||||
)
|
||||
def test_vault_database_connections_list_no_data(
|
||||
self, patch_ansible_module, vault_client, capfd
|
||||
):
|
||||
client = vault_client
|
||||
raw = {"errors": []}
|
||||
client.secrets.database.list_connections.return_value = raw
|
||||
|
||||
with pytest.raises(SystemExit) as e:
|
||||
vault_database_connections_list.main()
|
||||
|
||||
out, err = capfd.readouterr()
|
||||
result = json.loads(out)
|
||||
|
||||
assert e.value.code == 0, "result: %r" % (result,)
|
||||
|
||||
client.secrets.database.list_connections.assert_called_once_with(
|
||||
mount_point=patch_ansible_module["engine_mount_point"]
|
||||
)
|
||||
|
||||
empty_data = {"keys": []}
|
||||
assert result["raw"] == raw
|
||||
assert result["data"] == empty_data
|
||||
assert result["connections"] == empty_data["keys"]
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"patch_ansible_module", [_combined_options()], indirect=True
|
||||
)
|
||||
def test_vault_database_connections_list_no_hvac(self, capfd):
|
||||
with mock.patch.multiple(
|
||||
vault_database_connections_list,
|
||||
HAS_HVAC=False,
|
||||
HVAC_IMPORT_ERROR=None,
|
||||
create=True,
|
||||
):
|
||||
with pytest.raises(SystemExit) as e:
|
||||
vault_database_connections_list.main()
|
||||
|
||||
out, err = capfd.readouterr()
|
||||
result = json.loads(out)
|
||||
|
||||
assert e.value.code != 0, "result: %r" % (result,)
|
||||
assert result["msg"] == missing_required_lib("hvac")
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"exc",
|
||||
[
|
||||
(
|
||||
hvac.exceptions.Forbidden,
|
||||
"",
|
||||
r"^Forbidden: Permission Denied to path \['([^']+)'\]",
|
||||
),
|
||||
(
|
||||
hvac.exceptions.InvalidPath,
|
||||
"",
|
||||
r"^Invalid or missing path \['([^']+)/config'\]",
|
||||
),
|
||||
],
|
||||
)
|
||||
@pytest.mark.parametrize(
|
||||
"patch_ansible_module",
|
||||
[[_combined_options(), "engine_mount_point"]],
|
||||
indirect=True,
|
||||
)
|
||||
@pytest.mark.parametrize("opt_engine_mount_point", ["path/1", "second/path"])
|
||||
def test_vault_database_connections_list_vault_exception(
|
||||
self, vault_client, exc, opt_engine_mount_point, capfd
|
||||
):
|
||||
|
||||
client = vault_client
|
||||
client.secrets.database.list_connections.side_effect = exc[0](exc[1])
|
||||
|
||||
with pytest.raises(SystemExit) as e:
|
||||
vault_database_connections_list.main()
|
||||
|
||||
out, err = capfd.readouterr()
|
||||
result = json.loads(out)
|
||||
|
||||
assert e.value.code != 0, "result: %r" % (result,)
|
||||
match = re.search(exc[2], result["msg"])
|
||||
assert match is not None, "result: %r\ndid not match: %s" % (result, exc[2])
|
||||
|
||||
assert opt_engine_mount_point == match.group(1)
|
||||
+197
@@ -0,0 +1,197 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright (c) 2024 Brian Scholer (@briantist)
|
||||
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
from __future__ import absolute_import, division, print_function
|
||||
|
||||
__metaclass__ = type
|
||||
|
||||
import pytest
|
||||
import re
|
||||
import json
|
||||
|
||||
from ansible.module_utils.basic import missing_required_lib
|
||||
|
||||
from ...compat import mock
|
||||
from .....plugins.modules import vault_database_role_create
|
||||
from .....plugins.module_utils._hashi_vault_common import HashiVaultValueError
|
||||
|
||||
|
||||
hvac = pytest.importorskip("hvac")
|
||||
|
||||
|
||||
pytestmark = pytest.mark.usefixtures(
|
||||
"patch_ansible_module",
|
||||
"patch_authenticator",
|
||||
"patch_get_vault_client",
|
||||
)
|
||||
|
||||
|
||||
def _connection_options():
|
||||
return {
|
||||
"auth_method": "token",
|
||||
"url": "http://myvault",
|
||||
"token": "beep-boop",
|
||||
}
|
||||
|
||||
|
||||
def _sample_options():
|
||||
return {
|
||||
"engine_mount_point": "dbmount",
|
||||
}
|
||||
|
||||
|
||||
def _sample_role():
|
||||
return {
|
||||
"role_name": "foo",
|
||||
"connection_name": "bar",
|
||||
"creation_statements": [
|
||||
"CREATE ROLE \"{{name}}\" WITH LOGIN PASSWORD '{{password}}' VALID UNTIL '{{expiration}}';",
|
||||
'GRANT SELECT ON ALL TABLES IN SCHEMA public TO "{{name}}";',
|
||||
],
|
||||
"default_ttl": 3600,
|
||||
"max_ttl": 86400,
|
||||
}
|
||||
|
||||
|
||||
def _combined_options(**kwargs):
|
||||
opt = _connection_options()
|
||||
opt.update(_sample_options())
|
||||
opt.update(_sample_role())
|
||||
opt.update(kwargs)
|
||||
return opt
|
||||
|
||||
|
||||
class TestModuleVaultDatabaseRoleCreate:
|
||||
@pytest.mark.parametrize(
|
||||
"patch_ansible_module", [_combined_options()], indirect=True
|
||||
)
|
||||
@pytest.mark.parametrize(
|
||||
"exc",
|
||||
[HashiVaultValueError("throwaway msg"), NotImplementedError("throwaway msg")],
|
||||
)
|
||||
def test_vault_database_role_create_authentication_error(
|
||||
self, authenticator, exc, capfd
|
||||
):
|
||||
authenticator.authenticate.side_effect = exc
|
||||
|
||||
with pytest.raises(SystemExit) as e:
|
||||
vault_database_role_create.main()
|
||||
|
||||
out, err = capfd.readouterr()
|
||||
result = json.loads(out)
|
||||
|
||||
assert e.value.code != 0, "result: %r" % (result,)
|
||||
assert result["msg"] == "throwaway msg", "result: %r" % result
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"patch_ansible_module", [_combined_options()], indirect=True
|
||||
)
|
||||
@pytest.mark.parametrize(
|
||||
"exc",
|
||||
[HashiVaultValueError("throwaway msg"), NotImplementedError("throwaway msg")],
|
||||
)
|
||||
def test_vault_database_role_create_auth_validation_error(
|
||||
self, authenticator, exc, capfd
|
||||
):
|
||||
authenticator.validate.side_effect = exc
|
||||
|
||||
with pytest.raises(SystemExit) as e:
|
||||
vault_database_role_create.main()
|
||||
|
||||
out, err = capfd.readouterr()
|
||||
result = json.loads(out)
|
||||
|
||||
assert e.value.code != 0, "result: %r" % (result,)
|
||||
assert result["msg"] == "throwaway msg"
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"patch_ansible_module", [_combined_options()], indirect=True
|
||||
)
|
||||
def test_vault_database_role_create_success(
|
||||
self, patch_ansible_module, empty_response, vault_client, capfd
|
||||
):
|
||||
client = vault_client
|
||||
client.secrets.database.create_role.return_value = empty_response
|
||||
|
||||
with pytest.raises(SystemExit) as e:
|
||||
vault_database_role_create.main()
|
||||
|
||||
out, err = capfd.readouterr()
|
||||
result = json.loads(out)
|
||||
|
||||
assert e.value.code == 0, "result: %r" % (result,)
|
||||
|
||||
client.secrets.database.create_role.assert_called_once_with(
|
||||
name=patch_ansible_module["role_name"],
|
||||
db_name=patch_ansible_module["connection_name"],
|
||||
creation_statements=patch_ansible_module["creation_statements"],
|
||||
revocation_statements=patch_ansible_module.get("revocation_statements"),
|
||||
rollback_statements=patch_ansible_module.get("rollback_statements"),
|
||||
renew_statements=patch_ansible_module.get("renew_statements"),
|
||||
default_ttl=patch_ansible_module["default_ttl"],
|
||||
max_ttl=patch_ansible_module["max_ttl"],
|
||||
mount_point=patch_ansible_module["engine_mount_point"],
|
||||
)
|
||||
|
||||
assert result["changed"] is True
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"patch_ansible_module", [_combined_options()], indirect=True
|
||||
)
|
||||
def test_vault_database_role_create_no_hvac(self, capfd):
|
||||
with mock.patch.multiple(
|
||||
vault_database_role_create,
|
||||
HAS_HVAC=False,
|
||||
HVAC_IMPORT_ERROR=None,
|
||||
create=True,
|
||||
):
|
||||
with pytest.raises(SystemExit) as e:
|
||||
vault_database_role_create.main()
|
||||
|
||||
out, err = capfd.readouterr()
|
||||
result = json.loads(out)
|
||||
|
||||
assert e.value.code != 0, "result: %r" % (result,)
|
||||
assert result["msg"] == missing_required_lib("hvac")
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"exc",
|
||||
[
|
||||
(
|
||||
hvac.exceptions.Forbidden,
|
||||
"",
|
||||
r"^Forbidden: Permission Denied to path \['([^']+)'\]",
|
||||
),
|
||||
(
|
||||
hvac.exceptions.InvalidPath,
|
||||
"",
|
||||
r"^Invalid or missing path \['([^']+)/roles/([^']+)'\]",
|
||||
),
|
||||
],
|
||||
)
|
||||
@pytest.mark.parametrize(
|
||||
"patch_ansible_module",
|
||||
[[_combined_options(), "engine_mount_point"]],
|
||||
indirect=True,
|
||||
)
|
||||
@pytest.mark.parametrize("opt_engine_mount_point", ["path/1", "second/path"])
|
||||
def test_vault_database_role_create_vault_exception(
|
||||
self, vault_client, exc, opt_engine_mount_point, capfd
|
||||
):
|
||||
|
||||
client = vault_client
|
||||
client.secrets.database.create_role.side_effect = exc[0](exc[1])
|
||||
|
||||
with pytest.raises(SystemExit) as e:
|
||||
vault_database_role_create.main()
|
||||
|
||||
out, err = capfd.readouterr()
|
||||
result = json.loads(out)
|
||||
|
||||
assert e.value.code != 0, "result: %r" % (result,)
|
||||
match = re.search(exc[2], result["msg"])
|
||||
assert match is not None, "result: %r\ndid not match: %s" % (result, exc[2])
|
||||
|
||||
assert opt_engine_mount_point == match.group(1)
|
||||
+180
@@ -0,0 +1,180 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright (c) 2024 Brian Scholer (@briantist)
|
||||
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
from __future__ import absolute_import, division, print_function
|
||||
|
||||
__metaclass__ = type
|
||||
|
||||
import pytest
|
||||
import re
|
||||
import json
|
||||
|
||||
from ansible.module_utils.basic import missing_required_lib
|
||||
|
||||
from ...compat import mock
|
||||
from .....plugins.modules import vault_database_role_delete
|
||||
from .....plugins.module_utils._hashi_vault_common import HashiVaultValueError
|
||||
|
||||
|
||||
hvac = pytest.importorskip("hvac")
|
||||
|
||||
pytestmark = pytest.mark.usefixtures(
|
||||
"patch_ansible_module",
|
||||
"patch_authenticator",
|
||||
"patch_get_vault_client",
|
||||
)
|
||||
|
||||
|
||||
def _connection_options():
|
||||
return {
|
||||
"auth_method": "token",
|
||||
"url": "http://myvault",
|
||||
"token": "beep-boop",
|
||||
}
|
||||
|
||||
|
||||
def _sample_options():
|
||||
return {
|
||||
"engine_mount_point": "dbmount",
|
||||
}
|
||||
|
||||
|
||||
def _sample_role():
|
||||
return {"role_name": "foo"}
|
||||
|
||||
|
||||
def _combined_options(**kwargs):
|
||||
opt = _connection_options()
|
||||
opt.update(_sample_options())
|
||||
opt.update(_sample_role())
|
||||
opt.update(kwargs)
|
||||
return opt
|
||||
|
||||
|
||||
class TestModuleVaultDatabaseRoleDelete:
|
||||
@pytest.mark.parametrize(
|
||||
"patch_ansible_module", [_combined_options()], indirect=True
|
||||
)
|
||||
@pytest.mark.parametrize(
|
||||
"exc",
|
||||
[HashiVaultValueError("throwaway msg"), NotImplementedError("throwaway msg")],
|
||||
)
|
||||
def test_vault_database_role_delete_authentication_error(
|
||||
self, authenticator, exc, capfd
|
||||
):
|
||||
authenticator.authenticate.side_effect = exc
|
||||
|
||||
with pytest.raises(SystemExit) as e:
|
||||
vault_database_role_delete.main()
|
||||
|
||||
out, err = capfd.readouterr()
|
||||
result = json.loads(out)
|
||||
|
||||
assert e.value.code != 0, "result: %r" % (result,)
|
||||
assert result["msg"] == "throwaway msg", "result: %r" % result
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"patch_ansible_module", [_combined_options()], indirect=True
|
||||
)
|
||||
@pytest.mark.parametrize(
|
||||
"exc",
|
||||
[HashiVaultValueError("throwaway msg"), NotImplementedError("throwaway msg")],
|
||||
)
|
||||
def test_vault_database_role_delete_auth_validation_error(
|
||||
self, authenticator, exc, capfd
|
||||
):
|
||||
authenticator.validate.side_effect = exc
|
||||
|
||||
with pytest.raises(SystemExit) as e:
|
||||
vault_database_role_delete.main()
|
||||
|
||||
out, err = capfd.readouterr()
|
||||
result = json.loads(out)
|
||||
|
||||
assert e.value.code != 0, "result: %r" % (result,)
|
||||
assert result["msg"] == "throwaway msg"
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"patch_ansible_module", [_combined_options()], indirect=True
|
||||
)
|
||||
def test_vault_database_role_delete_success(
|
||||
self, patch_ansible_module, empty_response, vault_client, capfd
|
||||
):
|
||||
client = vault_client
|
||||
client.secrets.database.delete_role.return_value = empty_response
|
||||
|
||||
with pytest.raises(SystemExit) as e:
|
||||
vault_database_role_delete.main()
|
||||
|
||||
out, err = capfd.readouterr()
|
||||
result = json.loads(out)
|
||||
|
||||
assert e.value.code == 0, "result: %r" % (result,)
|
||||
|
||||
client.secrets.database.delete_role.assert_called_once_with(
|
||||
mount_point=patch_ansible_module["engine_mount_point"],
|
||||
name=patch_ansible_module["role_name"],
|
||||
)
|
||||
|
||||
assert result["changed"] is True
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"patch_ansible_module", [_combined_options()], indirect=True
|
||||
)
|
||||
def test_vault_database_role_delete_no_hvac(self, capfd):
|
||||
with mock.patch.multiple(
|
||||
vault_database_role_delete,
|
||||
HAS_HVAC=False,
|
||||
HVAC_IMPORT_ERROR=None,
|
||||
create=True,
|
||||
):
|
||||
with pytest.raises(SystemExit) as e:
|
||||
vault_database_role_delete.main()
|
||||
|
||||
out, err = capfd.readouterr()
|
||||
result = json.loads(out)
|
||||
|
||||
assert e.value.code != 0, "result: %r" % (result,)
|
||||
assert result["msg"] == missing_required_lib("hvac")
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"exc",
|
||||
[
|
||||
(
|
||||
hvac.exceptions.Forbidden,
|
||||
"",
|
||||
r"^Forbidden: Permission Denied to path \['([^']+)'\]",
|
||||
),
|
||||
(
|
||||
hvac.exceptions.InvalidPath,
|
||||
"",
|
||||
r"^Invalid or missing path \['([^']+)/roles/([^']+)'\]",
|
||||
),
|
||||
],
|
||||
)
|
||||
@pytest.mark.parametrize(
|
||||
"patch_ansible_module",
|
||||
[[_combined_options(), "engine_mount_point"]],
|
||||
indirect=True,
|
||||
)
|
||||
@pytest.mark.parametrize("opt_engine_mount_point", ["path/1", "second/path"])
|
||||
def test_vault_database_role_delete_vault_exception(
|
||||
self, vault_client, exc, opt_engine_mount_point, capfd
|
||||
):
|
||||
|
||||
client = vault_client
|
||||
client.secrets.database.delete_role.side_effect = exc[0](exc[1])
|
||||
|
||||
with pytest.raises(SystemExit) as e:
|
||||
vault_database_role_delete.main()
|
||||
|
||||
out, err = capfd.readouterr()
|
||||
result = json.loads(out)
|
||||
|
||||
assert e.value.code != 0, "result: %r" % (result,)
|
||||
match = re.search(exc[2], result["msg"])
|
||||
assert match is not None, "result: %r\ndid not match: %s" % (result, exc[2])
|
||||
|
||||
assert opt_engine_mount_point == match.group(1)
|
||||
+200
@@ -0,0 +1,200 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright (c) 2024 Brian Scholer (@briantist)
|
||||
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
from __future__ import absolute_import, division, print_function
|
||||
|
||||
__metaclass__ = type
|
||||
|
||||
import pytest
|
||||
import re
|
||||
import json
|
||||
|
||||
from ansible.module_utils.basic import missing_required_lib
|
||||
|
||||
from ...compat import mock
|
||||
from .....plugins.modules import vault_database_role_read
|
||||
from .....plugins.module_utils._hashi_vault_common import HashiVaultValueError
|
||||
|
||||
|
||||
hvac = pytest.importorskip("hvac")
|
||||
|
||||
|
||||
pytestmark = pytest.mark.usefixtures(
|
||||
"patch_ansible_module",
|
||||
"patch_authenticator",
|
||||
"patch_get_vault_client",
|
||||
)
|
||||
|
||||
|
||||
def _connection_options():
|
||||
return {
|
||||
"auth_method": "token",
|
||||
"url": "http://myvault",
|
||||
"token": "beep-boop",
|
||||
}
|
||||
|
||||
|
||||
def _sample_options():
|
||||
return {
|
||||
"engine_mount_point": "dbmount",
|
||||
}
|
||||
|
||||
|
||||
def _sample_role():
|
||||
return {"role_name": "foo"}
|
||||
|
||||
|
||||
def _combined_options(**kwargs):
|
||||
opt = _connection_options()
|
||||
opt.update(_sample_options())
|
||||
opt.update(_sample_role())
|
||||
opt.update(kwargs)
|
||||
return opt
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def list_response(fixture_loader):
|
||||
return fixture_loader("database_role_read_response.json")
|
||||
|
||||
|
||||
class TestModuleVaultDatabaseRoleRead:
|
||||
@pytest.mark.parametrize(
|
||||
"patch_ansible_module", [_combined_options()], indirect=True
|
||||
)
|
||||
@pytest.mark.parametrize(
|
||||
"exc",
|
||||
[HashiVaultValueError("throwaway msg"), NotImplementedError("throwaway msg")],
|
||||
)
|
||||
def test_vault_database_role_read_authentication_error(
|
||||
self, authenticator, exc, capfd
|
||||
):
|
||||
authenticator.authenticate.side_effect = exc
|
||||
|
||||
with pytest.raises(SystemExit) as e:
|
||||
vault_database_role_read.main()
|
||||
|
||||
out, err = capfd.readouterr()
|
||||
result = json.loads(out)
|
||||
|
||||
assert e.value.code != 0, "result: %r" % (result,)
|
||||
assert result["msg"] == "throwaway msg", "result: %r" % result
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"patch_ansible_module", [_combined_options()], indirect=True
|
||||
)
|
||||
@pytest.mark.parametrize(
|
||||
"exc",
|
||||
[HashiVaultValueError("throwaway msg"), NotImplementedError("throwaway msg")],
|
||||
)
|
||||
def test_vault_database_role_read_auth_validation_error(
|
||||
self, authenticator, exc, capfd
|
||||
):
|
||||
authenticator.validate.side_effect = exc
|
||||
|
||||
with pytest.raises(SystemExit) as e:
|
||||
vault_database_role_read.main()
|
||||
|
||||
out, err = capfd.readouterr()
|
||||
result = json.loads(out)
|
||||
|
||||
assert e.value.code != 0, "result: %r" % (result,)
|
||||
assert result["msg"] == "throwaway msg"
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"patch_ansible_module", [_combined_options()], indirect=True
|
||||
)
|
||||
def test_vault_database_role_read_return_data(
|
||||
self, patch_ansible_module, list_response, vault_client, capfd
|
||||
):
|
||||
client = vault_client
|
||||
client.secrets.database.read_role.return_value = list_response.copy()
|
||||
|
||||
with pytest.raises(SystemExit) as e:
|
||||
vault_database_role_read.main()
|
||||
|
||||
out, err = capfd.readouterr()
|
||||
result = json.loads(out)
|
||||
|
||||
assert e.value.code == 0, "result: %r" % (result,)
|
||||
|
||||
client.secrets.database.read_role.assert_called_once_with(
|
||||
mount_point=patch_ansible_module["engine_mount_point"],
|
||||
name=patch_ansible_module["role_name"],
|
||||
)
|
||||
|
||||
raw = list_response.copy()
|
||||
data = raw["data"]
|
||||
|
||||
assert (
|
||||
result["raw"] == raw
|
||||
), "module result did not match expected result:\nexpected: %r\ngot: %r" % (
|
||||
list_response,
|
||||
result,
|
||||
)
|
||||
assert (
|
||||
result["data"] == data
|
||||
), "module result did not match expected result:\nexpected: %r\ngot: %r" % (
|
||||
list_response,
|
||||
result,
|
||||
)
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"patch_ansible_module", [_combined_options()], indirect=True
|
||||
)
|
||||
def test_vault_database_role_read_no_hvac(self, capfd):
|
||||
with mock.patch.multiple(
|
||||
vault_database_role_read,
|
||||
HAS_HVAC=False,
|
||||
HVAC_IMPORT_ERROR=None,
|
||||
create=True,
|
||||
):
|
||||
with pytest.raises(SystemExit) as e:
|
||||
vault_database_role_read.main()
|
||||
|
||||
out, err = capfd.readouterr()
|
||||
result = json.loads(out)
|
||||
|
||||
assert e.value.code != 0, "result: %r" % (result,)
|
||||
assert result["msg"] == missing_required_lib("hvac")
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"exc",
|
||||
[
|
||||
(
|
||||
hvac.exceptions.Forbidden,
|
||||
"",
|
||||
r"^Forbidden: Permission Denied to path \['([^']+)'\]",
|
||||
),
|
||||
(
|
||||
hvac.exceptions.InvalidPath,
|
||||
"",
|
||||
r"^Invalid or missing path \['([^']+)/roles/([^']+)'\]",
|
||||
),
|
||||
],
|
||||
)
|
||||
@pytest.mark.parametrize(
|
||||
"patch_ansible_module",
|
||||
[[_combined_options(), "engine_mount_point"]],
|
||||
indirect=True,
|
||||
)
|
||||
@pytest.mark.parametrize("opt_engine_mount_point", ["path/1", "second/path"])
|
||||
def test_vault_database_role_read_vault_exception(
|
||||
self, vault_client, exc, opt_engine_mount_point, capfd
|
||||
):
|
||||
|
||||
client = vault_client
|
||||
client.secrets.database.read_role.side_effect = exc[0](exc[1])
|
||||
|
||||
with pytest.raises(SystemExit) as e:
|
||||
vault_database_role_read.main()
|
||||
|
||||
out, err = capfd.readouterr()
|
||||
result = json.loads(out)
|
||||
|
||||
assert e.value.code != 0, "result: %r" % (result,)
|
||||
match = re.search(exc[2], result["msg"])
|
||||
assert match is not None, "result: %r\ndid not match: %s" % (result, exc[2])
|
||||
|
||||
assert opt_engine_mount_point == match.group(1)
|
||||
+228
@@ -0,0 +1,228 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright (c) 2024 Martin Chmielewski (@M4rt1nCh)
|
||||
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
from __future__ import absolute_import, division, print_function
|
||||
|
||||
__metaclass__ = type
|
||||
|
||||
import pytest
|
||||
import re
|
||||
import json
|
||||
|
||||
from ansible.module_utils.basic import missing_required_lib
|
||||
|
||||
from ...compat import mock
|
||||
from .....plugins.modules import vault_database_roles_list
|
||||
from .....plugins.module_utils._hashi_vault_common import HashiVaultValueError
|
||||
|
||||
|
||||
hvac = pytest.importorskip("hvac")
|
||||
|
||||
|
||||
pytestmark = pytest.mark.usefixtures(
|
||||
"patch_ansible_module",
|
||||
"patch_authenticator",
|
||||
"patch_get_vault_client",
|
||||
)
|
||||
|
||||
|
||||
def _connection_options():
|
||||
return {
|
||||
"auth_method": "token",
|
||||
"url": "http://myvault",
|
||||
"token": "beep-boop",
|
||||
}
|
||||
|
||||
|
||||
def _sample_options():
|
||||
return {
|
||||
"engine_mount_point": "dbmount",
|
||||
}
|
||||
|
||||
|
||||
def _combined_options(**kwargs):
|
||||
opt = _connection_options()
|
||||
opt.update(_sample_options())
|
||||
opt.update(kwargs)
|
||||
return opt
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def list_response(fixture_loader):
|
||||
return fixture_loader("database_roles_list_response.json")
|
||||
|
||||
|
||||
class TestModuleVaultDatabaseRolesList:
|
||||
@pytest.mark.parametrize(
|
||||
"patch_ansible_module", [_combined_options()], indirect=True
|
||||
)
|
||||
@pytest.mark.parametrize(
|
||||
"exc",
|
||||
[HashiVaultValueError("throwaway msg"), NotImplementedError("throwaway msg")],
|
||||
)
|
||||
def test_vault_database_roles_list_authentication_error(
|
||||
self, authenticator, exc, capfd
|
||||
):
|
||||
authenticator.authenticate.side_effect = exc
|
||||
|
||||
with pytest.raises(SystemExit) as e:
|
||||
vault_database_roles_list.main()
|
||||
|
||||
out, err = capfd.readouterr()
|
||||
result = json.loads(out)
|
||||
|
||||
assert e.value.code != 0, "result: %r" % (result,)
|
||||
assert result["msg"] == "throwaway msg", "result: %r" % result
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"patch_ansible_module", [_combined_options()], indirect=True
|
||||
)
|
||||
@pytest.mark.parametrize(
|
||||
"exc",
|
||||
[HashiVaultValueError("throwaway msg"), NotImplementedError("throwaway msg")],
|
||||
)
|
||||
def test_vault_database_roles_list_auth_validation_error(
|
||||
self, authenticator, exc, capfd
|
||||
):
|
||||
authenticator.validate.side_effect = exc
|
||||
|
||||
with pytest.raises(SystemExit) as e:
|
||||
vault_database_roles_list.main()
|
||||
|
||||
out, err = capfd.readouterr()
|
||||
result = json.loads(out)
|
||||
|
||||
assert e.value.code != 0, "result: %r" % (result,)
|
||||
assert result["msg"] == "throwaway msg"
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"patch_ansible_module", [_combined_options()], indirect=True
|
||||
)
|
||||
def test_vault_database_roles_list_return_data(
|
||||
self, patch_ansible_module, list_response, vault_client, capfd
|
||||
):
|
||||
client = vault_client
|
||||
client.secrets.database.list_roles.return_value = list_response.copy()
|
||||
|
||||
with pytest.raises(SystemExit) as e:
|
||||
vault_database_roles_list.main()
|
||||
|
||||
out, err = capfd.readouterr()
|
||||
result = json.loads(out)
|
||||
|
||||
assert e.value.code == 0, "result: %r" % (result,)
|
||||
|
||||
client.secrets.database.list_roles.assert_called_once_with(
|
||||
mount_point=patch_ansible_module["engine_mount_point"]
|
||||
)
|
||||
|
||||
raw = list_response.copy()
|
||||
data = raw["data"]
|
||||
roles = data["keys"]
|
||||
|
||||
assert (
|
||||
result["raw"] == raw
|
||||
), "module result did not match expected result:\nexpected: %r\ngot: %r" % (
|
||||
list_response,
|
||||
result,
|
||||
)
|
||||
assert (
|
||||
result["data"] == data
|
||||
), "module result did not match expected result:\nexpected: %r\ngot: %r" % (
|
||||
list_response,
|
||||
result,
|
||||
)
|
||||
assert (
|
||||
result["roles"] == roles
|
||||
), "module result did not match expected result:\nexpected: %r\ngot: %r" % (
|
||||
list_response,
|
||||
result,
|
||||
)
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"patch_ansible_module", [_combined_options()], indirect=True
|
||||
)
|
||||
def test_vault_database_roles_list_no_data(
|
||||
self, patch_ansible_module, vault_client, capfd
|
||||
):
|
||||
client = vault_client
|
||||
raw = {"errors": []}
|
||||
client.secrets.database.list_roles.return_value = raw
|
||||
|
||||
with pytest.raises(SystemExit) as e:
|
||||
vault_database_roles_list.main()
|
||||
|
||||
out, err = capfd.readouterr()
|
||||
result = json.loads(out)
|
||||
|
||||
assert e.value.code == 0, "result: %r" % (result,)
|
||||
|
||||
client.secrets.database.list_roles.assert_called_once_with(
|
||||
mount_point=patch_ansible_module["engine_mount_point"]
|
||||
)
|
||||
|
||||
empty_data = {"keys": []}
|
||||
assert result["raw"] == raw
|
||||
assert result["data"] == empty_data
|
||||
assert result["roles"] == empty_data["keys"]
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"patch_ansible_module", [_combined_options()], indirect=True
|
||||
)
|
||||
def test_vault_database_roles_list_no_hvac(self, capfd):
|
||||
with mock.patch.multiple(
|
||||
vault_database_roles_list,
|
||||
HAS_HVAC=False,
|
||||
HVAC_IMPORT_ERROR=None,
|
||||
create=True,
|
||||
):
|
||||
with pytest.raises(SystemExit) as e:
|
||||
vault_database_roles_list.main()
|
||||
|
||||
out, err = capfd.readouterr()
|
||||
result = json.loads(out)
|
||||
|
||||
assert e.value.code != 0, "result: %r" % (result,)
|
||||
assert result["msg"] == missing_required_lib("hvac")
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"exc",
|
||||
[
|
||||
(
|
||||
hvac.exceptions.Forbidden,
|
||||
"",
|
||||
r"^Forbidden: Permission Denied to path \['([^']+)'\]",
|
||||
),
|
||||
(
|
||||
hvac.exceptions.InvalidPath,
|
||||
"",
|
||||
r"^Invalid or missing path \['([^']+)/roles'\]",
|
||||
),
|
||||
],
|
||||
)
|
||||
@pytest.mark.parametrize(
|
||||
"patch_ansible_module",
|
||||
[[_combined_options(), "engine_mount_point"]],
|
||||
indirect=True,
|
||||
)
|
||||
@pytest.mark.parametrize("opt_engine_mount_point", ["path/1", "second/path"])
|
||||
def test_vault_database_roles_list_vault_exception(
|
||||
self, vault_client, exc, opt_engine_mount_point, capfd
|
||||
):
|
||||
|
||||
client = vault_client
|
||||
client.secrets.database.list_roles.side_effect = exc[0](exc[1])
|
||||
|
||||
with pytest.raises(SystemExit) as e:
|
||||
vault_database_roles_list.main()
|
||||
|
||||
out, err = capfd.readouterr()
|
||||
result = json.loads(out)
|
||||
|
||||
assert e.value.code != 0, "result: %r" % (result,)
|
||||
match = re.search(exc[2], result["msg"])
|
||||
assert match is not None, "result: %r\ndid not match: %s" % (result, exc[2])
|
||||
|
||||
assert opt_engine_mount_point == match.group(1)
|
||||
+197
@@ -0,0 +1,197 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright (c) 2024 Brian Scholer (@briantist)
|
||||
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
from __future__ import absolute_import, division, print_function
|
||||
|
||||
__metaclass__ = type
|
||||
|
||||
import pytest
|
||||
import re
|
||||
import json
|
||||
|
||||
from ansible.module_utils.basic import missing_required_lib
|
||||
|
||||
from ...compat import mock
|
||||
from .....plugins.modules import vault_database_rotate_root_credentials
|
||||
from .....plugins.module_utils._hashi_vault_common import HashiVaultValueError
|
||||
|
||||
|
||||
hvac = pytest.importorskip("hvac")
|
||||
|
||||
|
||||
pytestmark = pytest.mark.usefixtures(
|
||||
"patch_ansible_module",
|
||||
"patch_authenticator",
|
||||
"patch_get_vault_client",
|
||||
)
|
||||
|
||||
|
||||
def _connection_options():
|
||||
return {
|
||||
"auth_method": "token",
|
||||
"url": "http://myvault",
|
||||
"token": "beep-boop",
|
||||
}
|
||||
|
||||
|
||||
def _sample_options():
|
||||
return {
|
||||
"engine_mount_point": "dbmount",
|
||||
}
|
||||
|
||||
|
||||
def _sample_connection():
|
||||
return {"connection_name": "foo"}
|
||||
|
||||
|
||||
def _combined_options(**kwargs):
|
||||
opt = _connection_options()
|
||||
opt.update(_sample_options())
|
||||
opt.update(_sample_connection())
|
||||
opt.update(kwargs)
|
||||
return opt
|
||||
|
||||
|
||||
class TestModuleVaultDatabaseRotateRootCredentials:
|
||||
@pytest.mark.parametrize(
|
||||
"patch_ansible_module", [_combined_options()], indirect=True
|
||||
)
|
||||
@pytest.mark.parametrize(
|
||||
"exc",
|
||||
[HashiVaultValueError("throwaway msg"), NotImplementedError("throwaway msg")],
|
||||
)
|
||||
def test_vault_database_rotate_root_credentials_authentication_error(
|
||||
self, authenticator, exc, capfd
|
||||
):
|
||||
authenticator.authenticate.side_effect = exc
|
||||
|
||||
with pytest.raises(SystemExit) as e:
|
||||
vault_database_rotate_root_credentials.main()
|
||||
|
||||
out, err = capfd.readouterr()
|
||||
result = json.loads(out)
|
||||
|
||||
assert e.value.code != 0, "result: %r" % (result,)
|
||||
assert result["msg"] == "throwaway msg", "result: %r" % result
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"patch_ansible_module", [_combined_options()], indirect=True
|
||||
)
|
||||
@pytest.mark.parametrize(
|
||||
"exc",
|
||||
[HashiVaultValueError("throwaway msg"), NotImplementedError("throwaway msg")],
|
||||
)
|
||||
def test_vault_database_rotate_root_credentials_auth_validation_error(
|
||||
self, authenticator, exc, capfd
|
||||
):
|
||||
authenticator.validate.side_effect = exc
|
||||
|
||||
with pytest.raises(SystemExit) as e:
|
||||
vault_database_rotate_root_credentials.main()
|
||||
|
||||
out, err = capfd.readouterr()
|
||||
result = json.loads(out)
|
||||
|
||||
assert e.value.code != 0, "result: %r" % (result,)
|
||||
assert result["msg"] == "throwaway msg"
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"patch_ansible_module", [_combined_options()], indirect=True
|
||||
)
|
||||
def test_vault_database_rotate_root_credentials_success(
|
||||
self, patch_ansible_module, empty_response, vault_client, capfd
|
||||
):
|
||||
client = vault_client
|
||||
client.secrets.database.rotate_root_credentials.return_value = empty_response
|
||||
|
||||
with pytest.raises(SystemExit) as e:
|
||||
vault_database_rotate_root_credentials.main()
|
||||
|
||||
out, err = capfd.readouterr()
|
||||
result = json.loads(out)
|
||||
|
||||
assert e.value.code == 0, "result: %r" % (result,)
|
||||
|
||||
client.secrets.database.rotate_root_credentials.assert_called_once_with(
|
||||
mount_point=patch_ansible_module["engine_mount_point"],
|
||||
name=patch_ansible_module["connection_name"],
|
||||
)
|
||||
|
||||
assert result["changed"] is True
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"patch_ansible_module", [_combined_options()], indirect=True
|
||||
)
|
||||
def test_vault_database_rotate_root_credentials_no_hvac(self, capfd):
|
||||
with mock.patch.multiple(
|
||||
vault_database_rotate_root_credentials,
|
||||
HAS_HVAC=False,
|
||||
HVAC_IMPORT_ERROR=None,
|
||||
create=True,
|
||||
):
|
||||
with pytest.raises(SystemExit) as e:
|
||||
vault_database_rotate_root_credentials.main()
|
||||
|
||||
out, err = capfd.readouterr()
|
||||
result = json.loads(out)
|
||||
|
||||
assert e.value.code != 0, "result: %r" % (result,)
|
||||
assert result["msg"] == missing_required_lib("hvac")
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"patch_ansible_module", [_combined_options()], indirect=True
|
||||
)
|
||||
def test_vault_database_rotate_root_credentials_old_hvac(self, vault_client, capfd):
|
||||
client = vault_client
|
||||
client.secrets.database.rotate_root_credentials.side_effect = AttributeError
|
||||
|
||||
with pytest.raises(SystemExit) as e:
|
||||
vault_database_rotate_root_credentials.main()
|
||||
|
||||
out, err = capfd.readouterr()
|
||||
result = json.loads(out)
|
||||
|
||||
assert e.value.code != 0, "result: %r" % (result,)
|
||||
assert result["msg"] == "hvac>=2.0.0 is required"
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"exc",
|
||||
[
|
||||
(
|
||||
hvac.exceptions.Forbidden,
|
||||
"",
|
||||
r"^Forbidden: Permission Denied to path \['([^']+)'\]",
|
||||
),
|
||||
(
|
||||
hvac.exceptions.InvalidPath,
|
||||
"",
|
||||
r"^Invalid or missing path \['([^']+)/rotate-root/([^']+)'\]",
|
||||
),
|
||||
],
|
||||
)
|
||||
@pytest.mark.parametrize(
|
||||
"patch_ansible_module",
|
||||
[[_combined_options(), "engine_mount_point"]],
|
||||
indirect=True,
|
||||
)
|
||||
@pytest.mark.parametrize("opt_engine_mount_point", ["path/1", "second/path"])
|
||||
def test_vault_database_rotate_root_credentials_vault_exception(
|
||||
self, vault_client, exc, opt_engine_mount_point, capfd
|
||||
):
|
||||
|
||||
client = vault_client
|
||||
client.secrets.database.rotate_root_credentials.side_effect = exc[0](exc[1])
|
||||
|
||||
with pytest.raises(SystemExit) as e:
|
||||
vault_database_rotate_root_credentials.main()
|
||||
|
||||
out, err = capfd.readouterr()
|
||||
result = json.loads(out)
|
||||
|
||||
assert e.value.code != 0, "result: %r" % (result,)
|
||||
match = re.search(exc[2], result["msg"])
|
||||
assert match is not None, "result: %r\ndid not match: %s" % (result, exc[2])
|
||||
|
||||
assert opt_engine_mount_point == match.group(1)
|
||||
+192
@@ -0,0 +1,192 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright (c) 2024 Brian Scholer (@briantist)
|
||||
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
from __future__ import absolute_import, division, print_function
|
||||
|
||||
__metaclass__ = type
|
||||
|
||||
import pytest
|
||||
import re
|
||||
import json
|
||||
|
||||
from ansible.module_utils.basic import missing_required_lib
|
||||
|
||||
from ...compat import mock
|
||||
from .....plugins.modules import vault_database_static_role_create
|
||||
from .....plugins.module_utils._hashi_vault_common import HashiVaultValueError
|
||||
|
||||
|
||||
hvac = pytest.importorskip("hvac")
|
||||
|
||||
pytestmark = pytest.mark.usefixtures(
|
||||
"patch_ansible_module",
|
||||
"patch_authenticator",
|
||||
"patch_get_vault_client",
|
||||
)
|
||||
|
||||
|
||||
def _connection_options():
|
||||
return {
|
||||
"auth_method": "token",
|
||||
"url": "http://myvault",
|
||||
"token": "beep-boop",
|
||||
}
|
||||
|
||||
|
||||
def _sample_options():
|
||||
return {
|
||||
"engine_mount_point": "dbmount",
|
||||
}
|
||||
|
||||
|
||||
def _sample_role():
|
||||
return {
|
||||
"role_name": "foo",
|
||||
"connection_name": "bar",
|
||||
"db_username": "baz",
|
||||
"rotation_statements": [
|
||||
"ALTER USER \"{{name}}\" WITH PASSWORD '{{password}}';"
|
||||
],
|
||||
"rotation_period": 86400,
|
||||
}
|
||||
|
||||
|
||||
def _combined_options(**kwargs):
|
||||
opt = _connection_options()
|
||||
opt.update(_sample_options())
|
||||
opt.update(_sample_role())
|
||||
opt.update(kwargs)
|
||||
return opt
|
||||
|
||||
|
||||
class TestModuleVaultDatabaseStaticRoleCreate:
|
||||
@pytest.mark.parametrize(
|
||||
"patch_ansible_module", [_combined_options()], indirect=True
|
||||
)
|
||||
@pytest.mark.parametrize(
|
||||
"exc",
|
||||
[HashiVaultValueError("throwaway msg"), NotImplementedError("throwaway msg")],
|
||||
)
|
||||
def test_vault_database_static_role_create_authentication_error(
|
||||
self, authenticator, exc, capfd
|
||||
):
|
||||
authenticator.authenticate.side_effect = exc
|
||||
|
||||
with pytest.raises(SystemExit) as e:
|
||||
vault_database_static_role_create.main()
|
||||
|
||||
out, err = capfd.readouterr()
|
||||
result = json.loads(out)
|
||||
|
||||
assert e.value.code != 0, "result: %r" % (result,)
|
||||
assert result["msg"] == "throwaway msg", "result: %r" % result
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"patch_ansible_module", [_combined_options()], indirect=True
|
||||
)
|
||||
@pytest.mark.parametrize(
|
||||
"exc",
|
||||
[HashiVaultValueError("throwaway msg"), NotImplementedError("throwaway msg")],
|
||||
)
|
||||
def test_vault_database_static_role_create_auth_validation_error(
|
||||
self, authenticator, exc, capfd
|
||||
):
|
||||
authenticator.validate.side_effect = exc
|
||||
|
||||
with pytest.raises(SystemExit) as e:
|
||||
vault_database_static_role_create.main()
|
||||
|
||||
out, err = capfd.readouterr()
|
||||
result = json.loads(out)
|
||||
|
||||
assert e.value.code != 0, "result: %r" % (result,)
|
||||
assert result["msg"] == "throwaway msg"
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"patch_ansible_module", [_combined_options()], indirect=True
|
||||
)
|
||||
def test_vault_database_static_role_create_success(
|
||||
self, patch_ansible_module, empty_response, vault_client, capfd
|
||||
):
|
||||
client = vault_client
|
||||
client.secrets.database.create_static_role.return_value = empty_response
|
||||
|
||||
with pytest.raises(SystemExit) as e:
|
||||
vault_database_static_role_create.main()
|
||||
|
||||
out, err = capfd.readouterr()
|
||||
result = json.loads(out)
|
||||
|
||||
assert e.value.code == 0, "result: %r" % (result,)
|
||||
|
||||
client.secrets.database.create_static_role.assert_called_once_with(
|
||||
mount_point=patch_ansible_module["engine_mount_point"],
|
||||
name=patch_ansible_module["role_name"],
|
||||
db_name=patch_ansible_module["connection_name"],
|
||||
username=patch_ansible_module["db_username"],
|
||||
rotation_statements=patch_ansible_module["rotation_statements"],
|
||||
rotation_period=patch_ansible_module["rotation_period"],
|
||||
)
|
||||
|
||||
assert result["changed"] is True
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"patch_ansible_module", [_combined_options()], indirect=True
|
||||
)
|
||||
def test_vault_database_static_role_create_no_hvac(self, capfd):
|
||||
with mock.patch.multiple(
|
||||
vault_database_static_role_create,
|
||||
HAS_HVAC=False,
|
||||
HVAC_IMPORT_ERROR=None,
|
||||
create=True,
|
||||
):
|
||||
with pytest.raises(SystemExit) as e:
|
||||
vault_database_static_role_create.main()
|
||||
|
||||
out, err = capfd.readouterr()
|
||||
result = json.loads(out)
|
||||
|
||||
assert e.value.code != 0, "result: %r" % (result,)
|
||||
assert result["msg"] == missing_required_lib("hvac")
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"exc",
|
||||
[
|
||||
(
|
||||
hvac.exceptions.Forbidden,
|
||||
"",
|
||||
r"^Forbidden: Permission Denied to path \['([^']+)'\]",
|
||||
),
|
||||
(
|
||||
hvac.exceptions.InvalidPath,
|
||||
"",
|
||||
r"^Invalid or missing path \['([^']+)/static-roles/([^']+)'\]",
|
||||
),
|
||||
],
|
||||
)
|
||||
@pytest.mark.parametrize(
|
||||
"patch_ansible_module",
|
||||
[[_combined_options(), "engine_mount_point"]],
|
||||
indirect=True,
|
||||
)
|
||||
@pytest.mark.parametrize("opt_engine_mount_point", ["path/1", "second/path"])
|
||||
def test_vault_database_static_role_create_vault_exception(
|
||||
self, vault_client, exc, opt_engine_mount_point, capfd
|
||||
):
|
||||
|
||||
client = vault_client
|
||||
client.secrets.database.create_static_role.side_effect = exc[0](exc[1])
|
||||
|
||||
with pytest.raises(SystemExit) as e:
|
||||
vault_database_static_role_create.main()
|
||||
|
||||
out, err = capfd.readouterr()
|
||||
result = json.loads(out)
|
||||
|
||||
assert e.value.code != 0, "result: %r" % (result,)
|
||||
match = re.search(exc[2], result["msg"])
|
||||
assert match is not None, "result: %r\ndid not match: %s" % (result, exc[2])
|
||||
|
||||
assert opt_engine_mount_point == match.group(1)
|
||||
+202
@@ -0,0 +1,202 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright (c) 2024 Brian Scholer (@briantist)
|
||||
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
from __future__ import absolute_import, division, print_function
|
||||
|
||||
__metaclass__ = type
|
||||
|
||||
import pytest
|
||||
import re
|
||||
import json
|
||||
|
||||
from ansible.module_utils.basic import missing_required_lib
|
||||
|
||||
from ...compat import mock
|
||||
from .....plugins.modules import vault_database_static_role_get_credentials
|
||||
from .....plugins.module_utils._hashi_vault_common import HashiVaultValueError
|
||||
|
||||
|
||||
hvac = pytest.importorskip("hvac")
|
||||
|
||||
|
||||
pytestmark = pytest.mark.usefixtures(
|
||||
"patch_ansible_module",
|
||||
"patch_authenticator",
|
||||
"patch_get_vault_client",
|
||||
)
|
||||
|
||||
|
||||
def _connection_options():
|
||||
return {
|
||||
"auth_method": "token",
|
||||
"url": "http://myvault",
|
||||
"token": "beep-boop",
|
||||
}
|
||||
|
||||
|
||||
def _sample_options():
|
||||
return {
|
||||
"engine_mount_point": "dbmount",
|
||||
}
|
||||
|
||||
|
||||
def _sample_role():
|
||||
return {"role_name": "foo"}
|
||||
|
||||
|
||||
def _combined_options(**kwargs):
|
||||
opt = _connection_options()
|
||||
opt.update(_sample_options())
|
||||
opt.update(_sample_role())
|
||||
opt.update(kwargs)
|
||||
return opt
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def list_response(fixture_loader):
|
||||
return fixture_loader("database_static_role_get_credentials_response.json")
|
||||
|
||||
|
||||
class TestModuleVaultDatabaseStaticRoleGetCredentials:
|
||||
@pytest.mark.parametrize(
|
||||
"patch_ansible_module", [_combined_options()], indirect=True
|
||||
)
|
||||
@pytest.mark.parametrize(
|
||||
"exc",
|
||||
[HashiVaultValueError("throwaway msg"), NotImplementedError("throwaway msg")],
|
||||
)
|
||||
def test_vault_database_static_role_get_credentials_authentication_error(
|
||||
self, authenticator, exc, capfd
|
||||
):
|
||||
authenticator.authenticate.side_effect = exc
|
||||
|
||||
with pytest.raises(SystemExit) as e:
|
||||
vault_database_static_role_get_credentials.main()
|
||||
|
||||
out, err = capfd.readouterr()
|
||||
result = json.loads(out)
|
||||
|
||||
assert e.value.code != 0, "result: %r" % (result,)
|
||||
assert result["msg"] == "throwaway msg", "result: %r" % result
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"patch_ansible_module", [_combined_options()], indirect=True
|
||||
)
|
||||
@pytest.mark.parametrize(
|
||||
"exc",
|
||||
[HashiVaultValueError("throwaway msg"), NotImplementedError("throwaway msg")],
|
||||
)
|
||||
def test_vault_database_static_role_get_credentials_auth_validation_error(
|
||||
self, authenticator, exc, capfd
|
||||
):
|
||||
authenticator.validate.side_effect = exc
|
||||
|
||||
with pytest.raises(SystemExit) as e:
|
||||
vault_database_static_role_get_credentials.main()
|
||||
|
||||
out, err = capfd.readouterr()
|
||||
result = json.loads(out)
|
||||
|
||||
assert e.value.code != 0, "result: %r" % (result,)
|
||||
assert result["msg"] == "throwaway msg"
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"patch_ansible_module", [_combined_options()], indirect=True
|
||||
)
|
||||
def test_vault_database_static_role_get_credentials_return_data(
|
||||
self, patch_ansible_module, list_response, vault_client, capfd
|
||||
):
|
||||
client = vault_client
|
||||
client.secrets.database.get_static_credentials.return_value = (
|
||||
list_response.copy()
|
||||
)
|
||||
|
||||
with pytest.raises(SystemExit) as e:
|
||||
vault_database_static_role_get_credentials.main()
|
||||
|
||||
out, err = capfd.readouterr()
|
||||
result = json.loads(out)
|
||||
|
||||
assert e.value.code == 0, "result: %r" % (result,)
|
||||
|
||||
client.secrets.database.get_static_credentials.assert_called_once_with(
|
||||
mount_point=patch_ansible_module["engine_mount_point"],
|
||||
name=patch_ansible_module["role_name"],
|
||||
)
|
||||
|
||||
raw = list_response.copy()
|
||||
data = raw["data"]
|
||||
|
||||
assert (
|
||||
result["raw"] == raw
|
||||
), "module result did not match expected result:\nexpected: %r\ngot: %r" % (
|
||||
list_response,
|
||||
result,
|
||||
)
|
||||
assert (
|
||||
result["data"] == data
|
||||
), "module result did not match expected result:\nexpected: %r\ngot: %r" % (
|
||||
list_response,
|
||||
result,
|
||||
)
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"patch_ansible_module", [_combined_options()], indirect=True
|
||||
)
|
||||
def test_vault_database_static_role_get_credentials_no_hvac(self, capfd):
|
||||
with mock.patch.multiple(
|
||||
vault_database_static_role_get_credentials,
|
||||
HAS_HVAC=False,
|
||||
HVAC_IMPORT_ERROR=None,
|
||||
create=True,
|
||||
):
|
||||
with pytest.raises(SystemExit) as e:
|
||||
vault_database_static_role_get_credentials.main()
|
||||
|
||||
out, err = capfd.readouterr()
|
||||
result = json.loads(out)
|
||||
|
||||
assert e.value.code != 0, "result: %r" % (result,)
|
||||
assert result["msg"] == missing_required_lib("hvac")
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"exc",
|
||||
[
|
||||
(
|
||||
hvac.exceptions.Forbidden,
|
||||
"",
|
||||
r"^Forbidden: Permission Denied to path \['([^']+)'\]",
|
||||
),
|
||||
(
|
||||
hvac.exceptions.InvalidPath,
|
||||
"",
|
||||
r"^Invalid or missing path \['([^']+)/static-creds/([^']+)'\]",
|
||||
),
|
||||
],
|
||||
)
|
||||
@pytest.mark.parametrize(
|
||||
"patch_ansible_module",
|
||||
[[_combined_options(), "engine_mount_point"]],
|
||||
indirect=True,
|
||||
)
|
||||
@pytest.mark.parametrize("opt_engine_mount_point", ["path/1", "second/path"])
|
||||
def test_vault_database_static_role_get_credentials_vault_exception(
|
||||
self, vault_client, exc, opt_engine_mount_point, capfd
|
||||
):
|
||||
|
||||
client = vault_client
|
||||
client.secrets.database.get_static_credentials.side_effect = exc[0](exc[1])
|
||||
|
||||
with pytest.raises(SystemExit) as e:
|
||||
vault_database_static_role_get_credentials.main()
|
||||
|
||||
out, err = capfd.readouterr()
|
||||
result = json.loads(out)
|
||||
|
||||
assert e.value.code != 0, "result: %r" % (result,)
|
||||
match = re.search(exc[2], result["msg"])
|
||||
assert match is not None, "result: %r\ndid not match: %s" % (result, exc[2])
|
||||
|
||||
assert opt_engine_mount_point == match.group(1)
|
||||
+200
@@ -0,0 +1,200 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright (c) 2024 Brian Scholer (@briantist)
|
||||
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
from __future__ import absolute_import, division, print_function
|
||||
|
||||
__metaclass__ = type
|
||||
|
||||
import pytest
|
||||
import re
|
||||
import json
|
||||
|
||||
from ansible.module_utils.basic import missing_required_lib
|
||||
|
||||
from ...compat import mock
|
||||
from .....plugins.modules import vault_database_static_role_read
|
||||
from .....plugins.module_utils._hashi_vault_common import HashiVaultValueError
|
||||
|
||||
|
||||
hvac = pytest.importorskip("hvac")
|
||||
|
||||
|
||||
pytestmark = pytest.mark.usefixtures(
|
||||
"patch_ansible_module",
|
||||
"patch_authenticator",
|
||||
"patch_get_vault_client",
|
||||
)
|
||||
|
||||
|
||||
def _connection_options():
|
||||
return {
|
||||
"auth_method": "token",
|
||||
"url": "http://myvault",
|
||||
"token": "beep-boop",
|
||||
}
|
||||
|
||||
|
||||
def _sample_options():
|
||||
return {
|
||||
"engine_mount_point": "dbmount",
|
||||
}
|
||||
|
||||
|
||||
def _sample_role():
|
||||
return {"role_name": "foo"}
|
||||
|
||||
|
||||
def _combined_options(**kwargs):
|
||||
opt = _connection_options()
|
||||
opt.update(_sample_options())
|
||||
opt.update(_sample_role())
|
||||
opt.update(kwargs)
|
||||
return opt
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def list_response(fixture_loader):
|
||||
return fixture_loader("database_static_role_read_response.json")
|
||||
|
||||
|
||||
class TestModuleVaultDatabaseStaticRoleRead:
|
||||
@pytest.mark.parametrize(
|
||||
"patch_ansible_module", [_combined_options()], indirect=True
|
||||
)
|
||||
@pytest.mark.parametrize(
|
||||
"exc",
|
||||
[HashiVaultValueError("throwaway msg"), NotImplementedError("throwaway msg")],
|
||||
)
|
||||
def test_vault_database_static_role_read_authentication_error(
|
||||
self, authenticator, exc, capfd
|
||||
):
|
||||
authenticator.authenticate.side_effect = exc
|
||||
|
||||
with pytest.raises(SystemExit) as e:
|
||||
vault_database_static_role_read.main()
|
||||
|
||||
out, err = capfd.readouterr()
|
||||
result = json.loads(out)
|
||||
|
||||
assert e.value.code != 0, "result: %r" % (result,)
|
||||
assert result["msg"] == "throwaway msg", "result: %r" % result
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"patch_ansible_module", [_combined_options()], indirect=True
|
||||
)
|
||||
@pytest.mark.parametrize(
|
||||
"exc",
|
||||
[HashiVaultValueError("throwaway msg"), NotImplementedError("throwaway msg")],
|
||||
)
|
||||
def test_vault_database_static_role_read_auth_validation_error(
|
||||
self, authenticator, exc, capfd
|
||||
):
|
||||
authenticator.validate.side_effect = exc
|
||||
|
||||
with pytest.raises(SystemExit) as e:
|
||||
vault_database_static_role_read.main()
|
||||
|
||||
out, err = capfd.readouterr()
|
||||
result = json.loads(out)
|
||||
|
||||
assert e.value.code != 0, "result: %r" % (result,)
|
||||
assert result["msg"] == "throwaway msg"
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"patch_ansible_module", [_combined_options()], indirect=True
|
||||
)
|
||||
def test_vault_database_static_role_read_return_data(
|
||||
self, patch_ansible_module, list_response, vault_client, capfd
|
||||
):
|
||||
client = vault_client
|
||||
client.secrets.database.read_static_role.return_value = list_response.copy()
|
||||
|
||||
with pytest.raises(SystemExit) as e:
|
||||
vault_database_static_role_read.main()
|
||||
|
||||
out, err = capfd.readouterr()
|
||||
result = json.loads(out)
|
||||
|
||||
assert e.value.code == 0, "result: %r" % (result,)
|
||||
|
||||
client.secrets.database.read_static_role.assert_called_once_with(
|
||||
mount_point=patch_ansible_module["engine_mount_point"],
|
||||
name=patch_ansible_module["role_name"],
|
||||
)
|
||||
|
||||
raw = list_response.copy()
|
||||
data = raw["data"]
|
||||
|
||||
assert (
|
||||
result["raw"] == raw
|
||||
), "module result did not match expected result:\nexpected: %r\ngot: %r" % (
|
||||
list_response,
|
||||
result,
|
||||
)
|
||||
assert (
|
||||
result["data"] == data
|
||||
), "module result did not match expected result:\nexpected: %r\ngot: %r" % (
|
||||
list_response,
|
||||
result,
|
||||
)
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"patch_ansible_module", [_combined_options()], indirect=True
|
||||
)
|
||||
def test_vault_database_static_role_read_no_hvac(self, capfd):
|
||||
with mock.patch.multiple(
|
||||
vault_database_static_role_read,
|
||||
HAS_HVAC=False,
|
||||
HVAC_IMPORT_ERROR=None,
|
||||
create=True,
|
||||
):
|
||||
with pytest.raises(SystemExit) as e:
|
||||
vault_database_static_role_read.main()
|
||||
|
||||
out, err = capfd.readouterr()
|
||||
result = json.loads(out)
|
||||
|
||||
assert e.value.code != 0, "result: %r" % (result,)
|
||||
assert result["msg"] == missing_required_lib("hvac")
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"exc",
|
||||
[
|
||||
(
|
||||
hvac.exceptions.Forbidden,
|
||||
"",
|
||||
r"^Forbidden: Permission Denied to path \['([^']+)'\]",
|
||||
),
|
||||
(
|
||||
hvac.exceptions.InvalidPath,
|
||||
"",
|
||||
r"^Invalid or missing path \['([^']+)/static-roles/([^']+)'\]",
|
||||
),
|
||||
],
|
||||
)
|
||||
@pytest.mark.parametrize(
|
||||
"patch_ansible_module",
|
||||
[[_combined_options(), "engine_mount_point"]],
|
||||
indirect=True,
|
||||
)
|
||||
@pytest.mark.parametrize("opt_engine_mount_point", ["path/1", "second/path"])
|
||||
def test_vault_database_static_role_read_vault_exception(
|
||||
self, vault_client, exc, opt_engine_mount_point, capfd
|
||||
):
|
||||
|
||||
client = vault_client
|
||||
client.secrets.database.read_static_role.side_effect = exc[0](exc[1])
|
||||
|
||||
with pytest.raises(SystemExit) as e:
|
||||
vault_database_static_role_read.main()
|
||||
|
||||
out, err = capfd.readouterr()
|
||||
result = json.loads(out)
|
||||
|
||||
assert e.value.code != 0, "result: %r" % (result,)
|
||||
match = re.search(exc[2], result["msg"])
|
||||
assert match is not None, "result: %r\ndid not match: %s" % (result, exc[2])
|
||||
|
||||
assert opt_engine_mount_point == match.group(1)
|
||||
+199
@@ -0,0 +1,199 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright (c) 2024 Brian Scholer (@briantist)
|
||||
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
from __future__ import absolute_import, division, print_function
|
||||
|
||||
__metaclass__ = type
|
||||
|
||||
import pytest
|
||||
import re
|
||||
import json
|
||||
|
||||
from ansible.module_utils.basic import missing_required_lib
|
||||
|
||||
from ...compat import mock
|
||||
from .....plugins.modules import vault_database_static_role_rotate_credentials
|
||||
from .....plugins.module_utils._hashi_vault_common import HashiVaultValueError
|
||||
|
||||
|
||||
hvac = pytest.importorskip("hvac")
|
||||
|
||||
|
||||
pytestmark = pytest.mark.usefixtures(
|
||||
"patch_ansible_module",
|
||||
"patch_authenticator",
|
||||
"patch_get_vault_client",
|
||||
)
|
||||
|
||||
|
||||
def _connection_options():
|
||||
return {
|
||||
"auth_method": "token",
|
||||
"url": "http://myvault",
|
||||
"token": "beep-boop",
|
||||
}
|
||||
|
||||
|
||||
def _sample_options():
|
||||
return {
|
||||
"engine_mount_point": "dbmount",
|
||||
}
|
||||
|
||||
|
||||
def _sample_role():
|
||||
return {"role_name": "foo"}
|
||||
|
||||
|
||||
def _combined_options(**kwargs):
|
||||
opt = _connection_options()
|
||||
opt.update(_sample_options())
|
||||
opt.update(_sample_role())
|
||||
opt.update(kwargs)
|
||||
return opt
|
||||
|
||||
|
||||
class TestModuleVaultDatabaseStaticRoleRotateCredentials:
|
||||
@pytest.mark.parametrize(
|
||||
"patch_ansible_module", [_combined_options()], indirect=True
|
||||
)
|
||||
@pytest.mark.parametrize(
|
||||
"exc",
|
||||
[HashiVaultValueError("throwaway msg"), NotImplementedError("throwaway msg")],
|
||||
)
|
||||
def test_vault_database_static_role_rotate_credentials_authentication_error(
|
||||
self, authenticator, exc, capfd
|
||||
):
|
||||
authenticator.authenticate.side_effect = exc
|
||||
|
||||
with pytest.raises(SystemExit) as e:
|
||||
vault_database_static_role_rotate_credentials.main()
|
||||
|
||||
out, err = capfd.readouterr()
|
||||
result = json.loads(out)
|
||||
|
||||
assert e.value.code != 0, "result: %r" % (result,)
|
||||
assert result["msg"] == "throwaway msg", "result: %r" % result
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"patch_ansible_module", [_combined_options()], indirect=True
|
||||
)
|
||||
@pytest.mark.parametrize(
|
||||
"exc",
|
||||
[HashiVaultValueError("throwaway msg"), NotImplementedError("throwaway msg")],
|
||||
)
|
||||
def test_vault_database_static_role_rotate_credentials_auth_validation_error(
|
||||
self, authenticator, exc, capfd
|
||||
):
|
||||
authenticator.validate.side_effect = exc
|
||||
|
||||
with pytest.raises(SystemExit) as e:
|
||||
vault_database_static_role_rotate_credentials.main()
|
||||
|
||||
out, err = capfd.readouterr()
|
||||
result = json.loads(out)
|
||||
|
||||
assert e.value.code != 0, "result: %r" % (result,)
|
||||
assert result["msg"] == "throwaway msg"
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"patch_ansible_module", [_combined_options()], indirect=True
|
||||
)
|
||||
def test_vault_database_static_role_rotate_credentials_success(
|
||||
self, patch_ansible_module, empty_response, vault_client, capfd
|
||||
):
|
||||
client = vault_client
|
||||
client.secrets.database.rotate_static_role_credentials.return_value = empty_response
|
||||
|
||||
with pytest.raises(SystemExit) as e:
|
||||
vault_database_static_role_rotate_credentials.main()
|
||||
|
||||
out, err = capfd.readouterr()
|
||||
result = json.loads(out)
|
||||
|
||||
assert e.value.code == 0, "result: %r" % (result,)
|
||||
|
||||
client.secrets.database.rotate_static_role_credentials.assert_called_once_with(
|
||||
mount_point=patch_ansible_module["engine_mount_point"],
|
||||
name=patch_ansible_module["role_name"],
|
||||
)
|
||||
|
||||
assert result["changed"] is True
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"patch_ansible_module", [_combined_options()], indirect=True
|
||||
)
|
||||
def test_vault_database_static_role_rotate_credentials_no_hvac(self, capfd):
|
||||
with mock.patch.multiple(
|
||||
vault_database_static_role_rotate_credentials,
|
||||
HAS_HVAC=False,
|
||||
HVAC_IMPORT_ERROR=None,
|
||||
create=True,
|
||||
):
|
||||
with pytest.raises(SystemExit) as e:
|
||||
vault_database_static_role_rotate_credentials.main()
|
||||
|
||||
out, err = capfd.readouterr()
|
||||
result = json.loads(out)
|
||||
|
||||
assert e.value.code != 0, "result: %r" % (result,)
|
||||
assert result["msg"] == missing_required_lib("hvac")
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"patch_ansible_module", [_combined_options()], indirect=True
|
||||
)
|
||||
def test_vault_database_static_role_rotate_credentials_old_hvac(self, vault_client, capfd):
|
||||
client = vault_client
|
||||
client.secrets.database.rotate_static_role_credentials.side_effect = AttributeError
|
||||
|
||||
with pytest.raises(SystemExit) as e:
|
||||
vault_database_static_role_rotate_credentials.main()
|
||||
|
||||
out, err = capfd.readouterr()
|
||||
result = json.loads(out)
|
||||
|
||||
assert e.value.code != 0, "result: %r" % (result,)
|
||||
assert result["msg"] == "hvac>=2.0.0 is required"
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"exc",
|
||||
[
|
||||
(
|
||||
hvac.exceptions.Forbidden,
|
||||
"",
|
||||
r"^Forbidden: Permission Denied to path \['([^']+)'\]",
|
||||
),
|
||||
(
|
||||
hvac.exceptions.InvalidPath,
|
||||
"",
|
||||
r"^Invalid or missing path \['([^']+)/rotate-role/([^']+)'\]",
|
||||
),
|
||||
],
|
||||
)
|
||||
@pytest.mark.parametrize(
|
||||
"patch_ansible_module",
|
||||
[[_combined_options(), "engine_mount_point"]],
|
||||
indirect=True,
|
||||
)
|
||||
@pytest.mark.parametrize("opt_engine_mount_point", ["path/1", "second/path"])
|
||||
def test_vault_database_static_role_rotate_credentials_vault_exception(
|
||||
self, vault_client, exc, opt_engine_mount_point, capfd
|
||||
):
|
||||
|
||||
client = vault_client
|
||||
client.secrets.database.rotate_static_role_credentials.side_effect = exc[0](
|
||||
exc[1]
|
||||
)
|
||||
|
||||
with pytest.raises(SystemExit) as e:
|
||||
vault_database_static_role_rotate_credentials.main()
|
||||
|
||||
out, err = capfd.readouterr()
|
||||
result = json.loads(out)
|
||||
|
||||
assert e.value.code != 0, "result: %r" % (result,)
|
||||
match = re.search(exc[2], result["msg"])
|
||||
assert match is not None, "result: %r\ndid not match: %s" % (result, exc[2])
|
||||
|
||||
assert opt_engine_mount_point == match.group(1)
|
||||
+228
@@ -0,0 +1,228 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright (c) 2024 Brian Scholer (@briantist)
|
||||
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
from __future__ import absolute_import, division, print_function
|
||||
|
||||
__metaclass__ = type
|
||||
|
||||
import pytest
|
||||
import re
|
||||
import json
|
||||
|
||||
from ansible.module_utils.basic import missing_required_lib
|
||||
|
||||
from ...compat import mock
|
||||
from .....plugins.modules import vault_database_static_roles_list
|
||||
from .....plugins.module_utils._hashi_vault_common import HashiVaultValueError
|
||||
|
||||
|
||||
hvac = pytest.importorskip("hvac")
|
||||
|
||||
|
||||
pytestmark = pytest.mark.usefixtures(
|
||||
"patch_ansible_module",
|
||||
"patch_authenticator",
|
||||
"patch_get_vault_client",
|
||||
)
|
||||
|
||||
|
||||
def _connection_options():
|
||||
return {
|
||||
"auth_method": "token",
|
||||
"url": "http://myvault",
|
||||
"token": "beep-boop",
|
||||
}
|
||||
|
||||
|
||||
def _sample_options():
|
||||
return {
|
||||
"engine_mount_point": "dbmount",
|
||||
}
|
||||
|
||||
|
||||
def _combined_options(**kwargs):
|
||||
opt = _connection_options()
|
||||
opt.update(_sample_options())
|
||||
opt.update(kwargs)
|
||||
return opt
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def list_response(fixture_loader):
|
||||
return fixture_loader("database_static_roles_list_response.json")
|
||||
|
||||
|
||||
class TestModuleVaultDatabaseStaticRolesList:
|
||||
@pytest.mark.parametrize(
|
||||
"patch_ansible_module", [_combined_options()], indirect=True
|
||||
)
|
||||
@pytest.mark.parametrize(
|
||||
"exc",
|
||||
[HashiVaultValueError("throwaway msg"), NotImplementedError("throwaway msg")],
|
||||
)
|
||||
def test_vault_database_static_roles_list_authentication_error(
|
||||
self, authenticator, exc, capfd
|
||||
):
|
||||
authenticator.authenticate.side_effect = exc
|
||||
|
||||
with pytest.raises(SystemExit) as e:
|
||||
vault_database_static_roles_list.main()
|
||||
|
||||
out, err = capfd.readouterr()
|
||||
result = json.loads(out)
|
||||
|
||||
assert e.value.code != 0, "result: %r" % (result,)
|
||||
assert result["msg"] == "throwaway msg", "result: %r" % result
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"patch_ansible_module", [_combined_options()], indirect=True
|
||||
)
|
||||
@pytest.mark.parametrize(
|
||||
"exc",
|
||||
[HashiVaultValueError("throwaway msg"), NotImplementedError("throwaway msg")],
|
||||
)
|
||||
def test_vault_database_static_roles_list_auth_validation_error(
|
||||
self, authenticator, exc, capfd
|
||||
):
|
||||
authenticator.validate.side_effect = exc
|
||||
|
||||
with pytest.raises(SystemExit) as e:
|
||||
vault_database_static_roles_list.main()
|
||||
|
||||
out, err = capfd.readouterr()
|
||||
result = json.loads(out)
|
||||
|
||||
assert e.value.code != 0, "result: %r" % (result,)
|
||||
assert result["msg"] == "throwaway msg"
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"patch_ansible_module", [_combined_options()], indirect=True
|
||||
)
|
||||
def test_vault_database_static_roles_list_return_data(
|
||||
self, patch_ansible_module, list_response, vault_client, capfd
|
||||
):
|
||||
client = vault_client
|
||||
client.secrets.database.list_static_roles.return_value = list_response.copy()
|
||||
|
||||
with pytest.raises(SystemExit) as e:
|
||||
vault_database_static_roles_list.main()
|
||||
|
||||
out, err = capfd.readouterr()
|
||||
result = json.loads(out)
|
||||
|
||||
assert e.value.code == 0, "result: %r" % (result,)
|
||||
|
||||
client.secrets.database.list_static_roles.assert_called_once_with(
|
||||
mount_point=patch_ansible_module["engine_mount_point"]
|
||||
)
|
||||
|
||||
raw = list_response.copy()
|
||||
data = raw["data"]
|
||||
roles = data["keys"]
|
||||
|
||||
assert (
|
||||
result["raw"] == raw
|
||||
), "module result did not match expected result:\nexpected: %r\ngot: %r" % (
|
||||
list_response,
|
||||
result,
|
||||
)
|
||||
assert (
|
||||
result["data"] == data
|
||||
), "module result did not match expected result:\nexpected: %r\ngot: %r" % (
|
||||
list_response,
|
||||
result,
|
||||
)
|
||||
assert (
|
||||
result["roles"] == roles
|
||||
), "module result did not match expected result:\nexpected: %r\ngot: %r" % (
|
||||
list_response,
|
||||
result,
|
||||
)
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"patch_ansible_module", [_combined_options()], indirect=True
|
||||
)
|
||||
def test_vault_database_static_roles_list_no_data(
|
||||
self, patch_ansible_module, vault_client, capfd
|
||||
):
|
||||
client = vault_client
|
||||
raw = {"errors": []}
|
||||
client.secrets.database.list_static_roles.return_value = raw
|
||||
|
||||
with pytest.raises(SystemExit) as e:
|
||||
vault_database_static_roles_list.main()
|
||||
|
||||
out, err = capfd.readouterr()
|
||||
result = json.loads(out)
|
||||
|
||||
assert e.value.code == 0, "result: %r" % (result,)
|
||||
|
||||
client.secrets.database.list_static_roles.assert_called_once_with(
|
||||
mount_point=patch_ansible_module["engine_mount_point"]
|
||||
)
|
||||
|
||||
empty_data = {"keys": []}
|
||||
assert result["raw"] == raw
|
||||
assert result["data"] == empty_data
|
||||
assert result["roles"] == empty_data["keys"]
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"patch_ansible_module", [_combined_options()], indirect=True
|
||||
)
|
||||
def test_vault_database_static_roles_list_no_hvac(self, capfd):
|
||||
with mock.patch.multiple(
|
||||
vault_database_static_roles_list,
|
||||
HAS_HVAC=False,
|
||||
HVAC_IMPORT_ERROR=None,
|
||||
create=True,
|
||||
):
|
||||
with pytest.raises(SystemExit) as e:
|
||||
vault_database_static_roles_list.main()
|
||||
|
||||
out, err = capfd.readouterr()
|
||||
result = json.loads(out)
|
||||
|
||||
assert e.value.code != 0, "result: %r" % (result,)
|
||||
assert result["msg"] == missing_required_lib("hvac")
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"exc",
|
||||
[
|
||||
(
|
||||
hvac.exceptions.Forbidden,
|
||||
"",
|
||||
r"^Forbidden: Permission Denied to path \['([^']+)'\]",
|
||||
),
|
||||
(
|
||||
hvac.exceptions.InvalidPath,
|
||||
"",
|
||||
r"^Invalid or missing path \['([^']+)/static-roles'\]",
|
||||
),
|
||||
],
|
||||
)
|
||||
@pytest.mark.parametrize(
|
||||
"patch_ansible_module",
|
||||
[[_combined_options(), "engine_mount_point"]],
|
||||
indirect=True,
|
||||
)
|
||||
@pytest.mark.parametrize("opt_engine_mount_point", ["path/1", "second/path"])
|
||||
def test_vault_database_static_roles_list_vault_exception(
|
||||
self, vault_client, exc, opt_engine_mount_point, capfd
|
||||
):
|
||||
|
||||
client = vault_client
|
||||
client.secrets.database.list_static_roles.side_effect = exc[0](exc[1])
|
||||
|
||||
with pytest.raises(SystemExit) as e:
|
||||
vault_database_static_roles_list.main()
|
||||
|
||||
out, err = capfd.readouterr()
|
||||
result = json.loads(out)
|
||||
|
||||
assert e.value.code != 0, "result: %r" % (result,)
|
||||
match = re.search(exc[2], result["msg"])
|
||||
assert match is not None, "result: %r\ndid not match: %s" % (result, exc[2])
|
||||
|
||||
assert opt_engine_mount_point == match.group(1)
|
||||
+158
@@ -0,0 +1,158 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright (c) 2022 Brian Scholer (@briantist)
|
||||
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
from __future__ import (absolute_import, division, print_function)
|
||||
__metaclass__ = type
|
||||
|
||||
import pytest
|
||||
import re
|
||||
import json
|
||||
|
||||
from ansible.module_utils.basic import missing_required_lib
|
||||
|
||||
from ...compat import mock
|
||||
from .....plugins.modules import vault_kv1_get
|
||||
from .....plugins.module_utils._hashi_vault_common import HashiVaultValueError
|
||||
|
||||
|
||||
hvac = pytest.importorskip('hvac')
|
||||
|
||||
|
||||
pytestmark = pytest.mark.usefixtures(
|
||||
'patch_ansible_module',
|
||||
'patch_authenticator',
|
||||
'patch_get_vault_client',
|
||||
)
|
||||
|
||||
|
||||
def _connection_options():
|
||||
return {
|
||||
'auth_method': 'token',
|
||||
'url': 'http://myvault',
|
||||
'token': 'beep-boop',
|
||||
}
|
||||
|
||||
|
||||
def _sample_options():
|
||||
return {
|
||||
'engine_mount_point': 'kv',
|
||||
'path': 'endpoint',
|
||||
}
|
||||
|
||||
|
||||
def _combined_options(**kwargs):
|
||||
opt = _connection_options()
|
||||
opt.update(_sample_options())
|
||||
opt.update(kwargs)
|
||||
return opt
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def kv1_get_response(fixture_loader):
|
||||
return fixture_loader('kv1_get_response.json')
|
||||
|
||||
|
||||
class TestModuleVaultKv1Get():
|
||||
|
||||
@pytest.mark.parametrize('patch_ansible_module', [_combined_options()], indirect=True)
|
||||
@pytest.mark.parametrize('exc', [HashiVaultValueError('throwaway msg'), NotImplementedError('throwaway msg')])
|
||||
def test_vault_kv1_get_authentication_error(self, authenticator, exc, capfd):
|
||||
authenticator.authenticate.side_effect = exc
|
||||
|
||||
with pytest.raises(SystemExit) as e:
|
||||
vault_kv1_get.main()
|
||||
|
||||
out, err = capfd.readouterr()
|
||||
result = json.loads(out)
|
||||
|
||||
assert e.value.code != 0, "result: %r" % (result,)
|
||||
assert result['msg'] == 'throwaway msg', "result: %r" % result
|
||||
|
||||
@pytest.mark.parametrize('patch_ansible_module', [_combined_options()], indirect=True)
|
||||
@pytest.mark.parametrize('exc', [HashiVaultValueError('throwaway msg'), NotImplementedError('throwaway msg')])
|
||||
def test_vault_kv1_get_auth_validation_error(self, authenticator, exc, capfd):
|
||||
authenticator.validate.side_effect = exc
|
||||
|
||||
with pytest.raises(SystemExit) as e:
|
||||
vault_kv1_get.main()
|
||||
|
||||
out, err = capfd.readouterr()
|
||||
result = json.loads(out)
|
||||
|
||||
assert e.value.code != 0, "result: %r" % (result,)
|
||||
assert result['msg'] == 'throwaway msg'
|
||||
|
||||
@pytest.mark.parametrize('opt_engine_mount_point', ['kv', 'other'])
|
||||
@pytest.mark.parametrize('patch_ansible_module', [[_combined_options(), 'engine_mount_point']], indirect=True)
|
||||
def test_vault_kv1_get_return_data(self, patch_ansible_module, kv1_get_response, vault_client, opt_engine_mount_point, capfd):
|
||||
client = vault_client
|
||||
client.secrets.kv.v1.read_secret.return_value = kv1_get_response.copy()
|
||||
|
||||
expected = {}
|
||||
expected['raw'] = kv1_get_response.copy()
|
||||
expected['metadata'] = kv1_get_response.copy()
|
||||
expected['data'] = expected['metadata'].pop('data')
|
||||
expected['secret'] = expected['data']
|
||||
|
||||
with pytest.raises(SystemExit) as e:
|
||||
vault_kv1_get.main()
|
||||
|
||||
out, err = capfd.readouterr()
|
||||
result = json.loads(out)
|
||||
|
||||
assert e.value.code == 0, "result: %r" % (result,)
|
||||
|
||||
client.secrets.kv.v1.read_secret.assert_called_once_with(path=patch_ansible_module['path'], mount_point=patch_ansible_module['engine_mount_point'])
|
||||
|
||||
for k, v in expected.items():
|
||||
assert result[k] == v, (
|
||||
"module result did not match expected result:\nmodule: %r\nkey: %s\nexpected: %r" % (result[k], k, v)
|
||||
)
|
||||
|
||||
@pytest.mark.parametrize('patch_ansible_module', [_combined_options()], indirect=True)
|
||||
def test_vault_kv1_get_no_hvac(self, capfd):
|
||||
with mock.patch.multiple(vault_kv1_get, HAS_HVAC=False, HVAC_IMPORT_ERROR=None, create=True):
|
||||
with pytest.raises(SystemExit) as e:
|
||||
vault_kv1_get.main()
|
||||
|
||||
out, err = capfd.readouterr()
|
||||
result = json.loads(out)
|
||||
|
||||
assert e.value.code != 0, "result: %r" % (result,)
|
||||
assert result['msg'] == missing_required_lib('hvac')
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
'exc',
|
||||
[
|
||||
(hvac.exceptions.Forbidden, "", r"^Forbidden: Permission Denied to path \['([^']+)'\]"),
|
||||
(
|
||||
hvac.exceptions.InvalidPath,
|
||||
"Invalid path for a versioned K/V secrets engine",
|
||||
r"^Invalid path for a versioned K/V secrets engine \['[^']+'\]. If this is a KV version 2 path, use community.hashi_vault.vault_kv2_get"
|
||||
),
|
||||
(hvac.exceptions.InvalidPath, "", r"^Invalid or missing path \['[^']+'\]"),
|
||||
]
|
||||
)
|
||||
@pytest.mark.parametrize('patch_ansible_module', [[_combined_options(), 'path']], indirect=True)
|
||||
@pytest.mark.parametrize('opt_path', ['path/1', 'second/path'])
|
||||
def test_vault_kv1_get_vault_exception(self, vault_client, exc, opt_path, capfd):
|
||||
|
||||
client = vault_client
|
||||
client.secrets.kv.v1.read_secret.side_effect = exc[0](exc[1])
|
||||
|
||||
with pytest.raises(SystemExit) as e:
|
||||
vault_kv1_get.main()
|
||||
|
||||
out, err = capfd.readouterr()
|
||||
result = json.loads(out)
|
||||
|
||||
assert e.value.code != 0, "result: %r" % (result,)
|
||||
match = re.search(exc[2], result['msg'])
|
||||
assert match is not None, "result: %r\ndid not match: %s" % (result, exc[2])
|
||||
|
||||
try:
|
||||
assert opt_path == match.group(1)
|
||||
except IndexError:
|
||||
pass
|
||||
+221
@@ -0,0 +1,221 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright (c) 2022 Isaac Wagner (@idwagner)
|
||||
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
from __future__ import (absolute_import, division, print_function)
|
||||
__metaclass__ = type
|
||||
|
||||
import pytest
|
||||
import re
|
||||
import json
|
||||
|
||||
from ansible.module_utils.basic import missing_required_lib
|
||||
|
||||
from ...compat import mock
|
||||
from .....plugins.modules import vault_kv2_delete
|
||||
from .....plugins.module_utils._hashi_vault_common import HashiVaultValueError
|
||||
|
||||
|
||||
hvac = pytest.importorskip('hvac')
|
||||
|
||||
|
||||
pytestmark = pytest.mark.usefixtures(
|
||||
'patch_ansible_module',
|
||||
'patch_authenticator',
|
||||
'patch_get_vault_client',
|
||||
)
|
||||
|
||||
|
||||
def _connection_options():
|
||||
return {
|
||||
'auth_method': 'token',
|
||||
'url': 'http://myvault',
|
||||
'token': 'beep-boop',
|
||||
}
|
||||
|
||||
|
||||
def _sample_options():
|
||||
return {
|
||||
'engine_mount_point': 'secret',
|
||||
'path': 'endpoint',
|
||||
}
|
||||
|
||||
|
||||
def _combined_options(**kwargs):
|
||||
opt = _connection_options()
|
||||
opt.update(_sample_options())
|
||||
opt.update(kwargs)
|
||||
return opt
|
||||
|
||||
|
||||
class TestModuleVaultKv2Delete():
|
||||
|
||||
@pytest.mark.parametrize('patch_ansible_module', [_combined_options()], indirect=True)
|
||||
@pytest.mark.parametrize('exc', [HashiVaultValueError('throwaway msg'), NotImplementedError('throwaway msg')])
|
||||
def test_vault_kv2_delete_authentication_error(self, authenticator, exc, capfd):
|
||||
authenticator.authenticate.side_effect = exc
|
||||
|
||||
with pytest.raises(SystemExit) as e:
|
||||
vault_kv2_delete.main()
|
||||
|
||||
out, err = capfd.readouterr()
|
||||
result = json.loads(out)
|
||||
|
||||
assert e.value.code != 0, "result: %r" % (result,)
|
||||
assert result['msg'] == 'throwaway msg', "result: %r" % result
|
||||
|
||||
@pytest.mark.parametrize('patch_ansible_module', [_combined_options()], indirect=True)
|
||||
@pytest.mark.parametrize('exc', [HashiVaultValueError('throwaway msg'), NotImplementedError('throwaway msg')])
|
||||
def test_vault_kv2_delete_auth_validation_error(self, authenticator, exc, capfd):
|
||||
authenticator.validate.side_effect = exc
|
||||
|
||||
with pytest.raises(SystemExit) as e:
|
||||
vault_kv2_delete.main()
|
||||
|
||||
out, err = capfd.readouterr()
|
||||
result = json.loads(out)
|
||||
|
||||
assert e.value.code != 0, "result: %r" % (result,)
|
||||
assert result['msg'] == 'throwaway msg'
|
||||
|
||||
@pytest.mark.parametrize('opt_versions', [None, [1, 3]])
|
||||
@pytest.mark.parametrize('patch_ansible_module', [[_combined_options(), 'versions']], indirect=True)
|
||||
def test_vault_kv2_delete_empty_response(self, patch_ansible_module, opt_versions, requests_unparseable_response, vault_client, capfd):
|
||||
client = vault_client
|
||||
|
||||
requests_unparseable_response.status_code = 204
|
||||
|
||||
if opt_versions:
|
||||
client.secrets.kv.v2.delete_secret_versions.return_value = requests_unparseable_response
|
||||
else:
|
||||
client.secrets.kv.v2.delete_latest_version_of_secret.return_value = requests_unparseable_response
|
||||
|
||||
with pytest.raises(SystemExit) as e:
|
||||
vault_kv2_delete.main()
|
||||
|
||||
out, err = capfd.readouterr()
|
||||
result = json.loads(out)
|
||||
|
||||
assert e.value.code == 0, "result: %r" % (result,)
|
||||
|
||||
assert result['data'] == {}
|
||||
|
||||
@pytest.mark.parametrize('opt_versions', [None, [1, 3]])
|
||||
@pytest.mark.parametrize('patch_ansible_module', [[_combined_options(), 'versions']], indirect=True)
|
||||
def test_vault_kv2_delete_unparseable_response(self, vault_client, opt_versions, requests_unparseable_response, module_warn, capfd):
|
||||
client = vault_client
|
||||
|
||||
requests_unparseable_response.status_code = 200
|
||||
requests_unparseable_response.content = '(☞゚ヮ゚)☞ ┻━┻'
|
||||
|
||||
if opt_versions:
|
||||
client.secrets.kv.v2.delete_secret_versions.return_value = requests_unparseable_response
|
||||
else:
|
||||
client.secrets.kv.v2.delete_latest_version_of_secret.return_value = requests_unparseable_response
|
||||
|
||||
with pytest.raises(SystemExit) as e:
|
||||
vault_kv2_delete.main()
|
||||
|
||||
out, err = capfd.readouterr()
|
||||
result = json.loads(out)
|
||||
|
||||
assert e.value.code == 0, "result: %r" % (result,)
|
||||
assert result['data'] == '(☞゚ヮ゚)☞ ┻━┻'
|
||||
|
||||
module_warn.assert_called_once_with(
|
||||
'Vault returned status code 200 and an unparsable body.')
|
||||
|
||||
@pytest.mark.parametrize('patch_ansible_module', [_combined_options()], indirect=True)
|
||||
def test_vault_kv2_delete_no_hvac(self, capfd):
|
||||
with mock.patch.multiple(vault_kv2_delete, HAS_HVAC=False, HVAC_IMPORT_ERROR=None, create=True):
|
||||
with pytest.raises(SystemExit) as e:
|
||||
vault_kv2_delete.main()
|
||||
|
||||
out, err = capfd.readouterr()
|
||||
result = json.loads(out)
|
||||
|
||||
assert e.value.code != 0, "result: %r" % (result,)
|
||||
assert result['msg'] == missing_required_lib('hvac')
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
'exc',
|
||||
[
|
||||
(hvac.exceptions.Forbidden, "",
|
||||
r"^Forbidden: Permission Denied to path \['([^']+)'\]"),
|
||||
]
|
||||
)
|
||||
@pytest.mark.parametrize('opt_versions', [None, [1, 3]])
|
||||
@pytest.mark.parametrize('opt_path', ['path/1', 'second/path'])
|
||||
@pytest.mark.parametrize('patch_ansible_module', [[_combined_options(), 'path', 'versions']], indirect=True)
|
||||
def test_vault_kv2_delete_vault_exception(self, vault_client, exc, opt_versions, opt_path, capfd):
|
||||
|
||||
client = vault_client
|
||||
|
||||
if opt_versions:
|
||||
client.secrets.kv.v2.delete_secret_versions.side_effect = exc[0](
|
||||
exc[1])
|
||||
else:
|
||||
client.secrets.kv.v2.delete_latest_version_of_secret.side_effect = exc[0](
|
||||
exc[1])
|
||||
|
||||
with pytest.raises(SystemExit) as e:
|
||||
vault_kv2_delete.main()
|
||||
|
||||
out, err = capfd.readouterr()
|
||||
result = json.loads(out)
|
||||
|
||||
assert e.value.code != 0, "result: %r" % (result,)
|
||||
match = re.search(exc[2], result['msg'])
|
||||
assert match is not None, "result: %r\ndid not match: %s" % (
|
||||
result, exc[2])
|
||||
|
||||
assert opt_path == match.group(1)
|
||||
|
||||
@pytest.mark.parametrize('opt__ansible_check_mode', [False, True])
|
||||
@pytest.mark.parametrize('opt_versions', [None])
|
||||
@pytest.mark.parametrize('patch_ansible_module', [[
|
||||
_combined_options(),
|
||||
'_ansible_check_mode',
|
||||
'versions'
|
||||
]], indirect=True)
|
||||
def test_vault_kv2_delete_latest_version_call(self, vault_client, opt__ansible_check_mode, opt_versions, capfd):
|
||||
|
||||
client = vault_client
|
||||
client.secrets.kv.v2.delete_latest_version_of_secret.return_value = {}
|
||||
|
||||
with pytest.raises(SystemExit) as e:
|
||||
vault_kv2_delete.main()
|
||||
|
||||
out, err = capfd.readouterr()
|
||||
result = json.loads(out)
|
||||
|
||||
if opt__ansible_check_mode:
|
||||
client.secrets.kv.v2.delete_latest_version_of_secret.assert_not_called()
|
||||
else:
|
||||
client.secrets.kv.v2.delete_latest_version_of_secret.assert_called_once_with(
|
||||
path='endpoint', mount_point='secret')
|
||||
|
||||
@pytest.mark.parametrize('opt__ansible_check_mode', [False, True])
|
||||
@pytest.mark.parametrize('opt_versions', [[1, 3]])
|
||||
@pytest.mark.parametrize('patch_ansible_module', [[
|
||||
_combined_options(),
|
||||
'_ansible_check_mode',
|
||||
'versions'
|
||||
]], indirect=True)
|
||||
def test_vault_kv2_delete_specific_versions_call(self, vault_client, opt__ansible_check_mode, opt_versions, capfd):
|
||||
|
||||
client = vault_client
|
||||
client.secrets.kv.v2.delete_secret_versions.return_value = {}
|
||||
|
||||
with pytest.raises(SystemExit) as e:
|
||||
vault_kv2_delete.main()
|
||||
|
||||
out, err = capfd.readouterr()
|
||||
result = json.loads(out)
|
||||
|
||||
if opt__ansible_check_mode:
|
||||
client.secrets.kv.v2.delete_secret_versions.assert_not_called()
|
||||
else:
|
||||
client.secrets.kv.v2.delete_secret_versions.assert_called_once_with(
|
||||
path='endpoint', mount_point='secret', versions=[1, 3])
|
||||
+168
@@ -0,0 +1,168 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright (c) 2022 Brian Scholer (@briantist)
|
||||
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
from __future__ import (absolute_import, division, print_function)
|
||||
__metaclass__ = type
|
||||
|
||||
import pytest
|
||||
import re
|
||||
import json
|
||||
|
||||
from ansible.module_utils.basic import missing_required_lib
|
||||
|
||||
from ...compat import mock
|
||||
from .....plugins.modules import vault_kv2_get
|
||||
from .....plugins.module_utils._hashi_vault_common import HashiVaultValueError
|
||||
|
||||
|
||||
hvac = pytest.importorskip('hvac')
|
||||
|
||||
|
||||
pytestmark = pytest.mark.usefixtures(
|
||||
'patch_ansible_module',
|
||||
'patch_authenticator',
|
||||
'patch_get_vault_client',
|
||||
)
|
||||
|
||||
|
||||
def _connection_options():
|
||||
return {
|
||||
'auth_method': 'token',
|
||||
'url': 'http://myvault',
|
||||
'token': 'beep-boop',
|
||||
}
|
||||
|
||||
|
||||
def _sample_options():
|
||||
return {
|
||||
'engine_mount_point': 'secret',
|
||||
'path': 'endpoint',
|
||||
}
|
||||
|
||||
|
||||
def _combined_options(**kwargs):
|
||||
opt = _connection_options()
|
||||
opt.update(_sample_options())
|
||||
opt.update(kwargs)
|
||||
return opt
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def kv2_get_response(fixture_loader):
|
||||
return fixture_loader('kv2_get_response.json')
|
||||
|
||||
|
||||
class TestModuleVaultKv2Get():
|
||||
|
||||
@pytest.mark.parametrize('patch_ansible_module', [_combined_options()], indirect=True)
|
||||
@pytest.mark.parametrize('exc', [HashiVaultValueError('throwaway msg'), NotImplementedError('throwaway msg')])
|
||||
def test_vault_kv2_get_authentication_error(self, authenticator, exc, capfd):
|
||||
authenticator.authenticate.side_effect = exc
|
||||
|
||||
with pytest.raises(SystemExit) as e:
|
||||
vault_kv2_get.main()
|
||||
|
||||
out, err = capfd.readouterr()
|
||||
result = json.loads(out)
|
||||
|
||||
assert e.value.code != 0, "result: %r" % (result,)
|
||||
assert result['msg'] == 'throwaway msg', "result: %r" % result
|
||||
|
||||
@pytest.mark.parametrize('patch_ansible_module', [_combined_options()], indirect=True)
|
||||
@pytest.mark.parametrize('exc', [HashiVaultValueError('throwaway msg'), NotImplementedError('throwaway msg')])
|
||||
def test_vault_kv2_get_auth_validation_error(self, authenticator, exc, capfd):
|
||||
authenticator.validate.side_effect = exc
|
||||
|
||||
with pytest.raises(SystemExit) as e:
|
||||
vault_kv2_get.main()
|
||||
|
||||
out, err = capfd.readouterr()
|
||||
result = json.loads(out)
|
||||
|
||||
assert e.value.code != 0, "result: %r" % (result,)
|
||||
assert result['msg'] == 'throwaway msg'
|
||||
|
||||
@pytest.mark.parametrize('opt_engine_mount_point', ['secret', 'other'])
|
||||
@pytest.mark.parametrize('opt_version', [None, 2, 10])
|
||||
@pytest.mark.parametrize('patch_ansible_module', [[_combined_options(), 'engine_mount_point', 'version']], indirect=True)
|
||||
def test_vault_kv2_get_return_data(self, patch_ansible_module, kv2_get_response, vault_client, opt_engine_mount_point, opt_version, capfd):
|
||||
client = vault_client
|
||||
rv = kv2_get_response.copy()
|
||||
rv['data']['metadata']['version'] = opt_version
|
||||
client.secrets.kv.v2.read_secret_version.return_value = rv
|
||||
|
||||
expected = {}
|
||||
expected['raw'] = rv.copy()
|
||||
expected['metadata'] = expected['raw']['data']['metadata']
|
||||
expected['data'] = expected['raw']['data']
|
||||
expected['secret'] = expected['data']['data']
|
||||
|
||||
with pytest.raises(SystemExit) as e:
|
||||
vault_kv2_get.main()
|
||||
|
||||
out, err = capfd.readouterr()
|
||||
result = json.loads(out)
|
||||
|
||||
assert e.value.code == 0, "result: %r" % (result,)
|
||||
|
||||
client.secrets.kv.v2.read_secret_version.assert_called_once_with(
|
||||
path=patch_ansible_module['path'],
|
||||
mount_point=patch_ansible_module['engine_mount_point'],
|
||||
version=opt_version
|
||||
)
|
||||
|
||||
for k, v in expected.items():
|
||||
assert result[k] == v, (
|
||||
"module result did not match expected result:\nmodule: %r\nkey: %s\nexpected: %r" % (result[k], k, v)
|
||||
)
|
||||
|
||||
@pytest.mark.parametrize('patch_ansible_module', [_combined_options()], indirect=True)
|
||||
def test_vault_kv2_get_no_hvac(self, capfd):
|
||||
with mock.patch.multiple(vault_kv2_get, HAS_HVAC=False, HVAC_IMPORT_ERROR=None, create=True):
|
||||
with pytest.raises(SystemExit) as e:
|
||||
vault_kv2_get.main()
|
||||
|
||||
out, err = capfd.readouterr()
|
||||
result = json.loads(out)
|
||||
|
||||
assert e.value.code != 0, "result: %r" % (result,)
|
||||
assert result['msg'] == missing_required_lib('hvac')
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
'exc',
|
||||
[
|
||||
(hvac.exceptions.Forbidden, "", r"^Forbidden: Permission Denied to path \['([^']+)'\]"),
|
||||
(
|
||||
hvac.exceptions.InvalidPath,
|
||||
"",
|
||||
r"^Invalid or missing path \['([^']+)'\] with secret version '(\d+|latest)'. Check the path or secret version"
|
||||
),
|
||||
]
|
||||
)
|
||||
@pytest.mark.parametrize('patch_ansible_module', [[_combined_options(), 'path', 'version']], indirect=True)
|
||||
@pytest.mark.parametrize('opt_path', ['path/1', 'second/path'])
|
||||
@pytest.mark.parametrize('opt_version', [None, 2, 10])
|
||||
def test_vault_kv2_get_vault_exception(self, vault_client, exc, opt_version, opt_path, capfd):
|
||||
|
||||
client = vault_client
|
||||
client.secrets.kv.v2.read_secret_version.side_effect = exc[0](exc[1])
|
||||
|
||||
with pytest.raises(SystemExit) as e:
|
||||
vault_kv2_get.main()
|
||||
|
||||
out, err = capfd.readouterr()
|
||||
result = json.loads(out)
|
||||
|
||||
assert e.value.code != 0, "result: %r" % (result,)
|
||||
match = re.search(exc[2], result['msg'])
|
||||
assert match is not None, "result: %r\ndid not match: %s" % (result, exc[2])
|
||||
|
||||
assert opt_path == match.group(1)
|
||||
|
||||
try:
|
||||
assert (opt_version is None) == (match.group(2) == 'latest')
|
||||
assert (opt_version is not None) == (match.group(2) == str(opt_version))
|
||||
except IndexError:
|
||||
pass
|
||||
+163
@@ -0,0 +1,163 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright (c) 2023 Devon Mar (@devon-mar)
|
||||
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
from __future__ import absolute_import, division, print_function
|
||||
|
||||
__metaclass__ = type
|
||||
|
||||
import json
|
||||
|
||||
import pytest
|
||||
from ansible.module_utils.basic import missing_required_lib
|
||||
|
||||
from .....plugins.module_utils._hashi_vault_common import HashiVaultValueError
|
||||
from .....plugins.modules import vault_kv2_write
|
||||
from ...compat import mock
|
||||
|
||||
hvac = pytest.importorskip("hvac")
|
||||
|
||||
pytestmark = pytest.mark.usefixtures(
|
||||
"patch_ansible_module",
|
||||
"patch_authenticator",
|
||||
"patch_get_vault_client",
|
||||
)
|
||||
|
||||
|
||||
def _connection_options():
|
||||
return {
|
||||
"auth_method": "token",
|
||||
"url": "http://myvault",
|
||||
"token": "beep-boop",
|
||||
}
|
||||
|
||||
|
||||
def _sample_options():
|
||||
return {
|
||||
"engine_mount_point": "secret",
|
||||
"path": "endpoint",
|
||||
"data": {"foo": "bar"},
|
||||
}
|
||||
|
||||
|
||||
def _combined_options(**kwargs):
|
||||
opt = _connection_options()
|
||||
opt.update(_sample_options())
|
||||
opt.update(kwargs)
|
||||
return opt
|
||||
|
||||
|
||||
class TestModuleVaultKv2Write:
|
||||
@pytest.mark.parametrize(
|
||||
"patch_ansible_module", [_combined_options()], indirect=True
|
||||
)
|
||||
@pytest.mark.parametrize(
|
||||
"exc",
|
||||
[HashiVaultValueError("throwaway msg"), NotImplementedError("throwaway msg")],
|
||||
)
|
||||
def test_vault_kv2_write_authentication_error(self, authenticator, exc, capfd):
|
||||
authenticator.authenticate.side_effect = exc
|
||||
|
||||
with pytest.raises(SystemExit) as e:
|
||||
vault_kv2_write.main()
|
||||
|
||||
out, err = capfd.readouterr()
|
||||
result = json.loads(out)
|
||||
|
||||
assert e.value.code != 0, "result: %r" % (result,)
|
||||
assert result["msg"] == "throwaway msg", "result: %r" % result
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"patch_ansible_module", [_combined_options()], indirect=True
|
||||
)
|
||||
@pytest.mark.parametrize(
|
||||
"exc",
|
||||
[HashiVaultValueError("throwaway msg"), NotImplementedError("throwaway msg")],
|
||||
)
|
||||
def test_vault_kv2_write_auth_validation_error(self, authenticator, exc, capfd):
|
||||
authenticator.validate.side_effect = exc
|
||||
|
||||
with pytest.raises(SystemExit) as e:
|
||||
vault_kv2_write.main()
|
||||
|
||||
out, err = capfd.readouterr()
|
||||
result = json.loads(out)
|
||||
|
||||
assert e.value.code != 0, "result: %r" % (result,)
|
||||
assert result["msg"] == "throwaway msg"
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"patch_ansible_module", [_combined_options()], indirect=True
|
||||
)
|
||||
def test_vault_kv2_write_get_no_hvac(self, capfd):
|
||||
with mock.patch.multiple(
|
||||
vault_kv2_write, HAS_HVAC=False, HVAC_IMPORT_ERROR=None, create=True
|
||||
):
|
||||
with pytest.raises(SystemExit) as e:
|
||||
vault_kv2_write.main()
|
||||
|
||||
out, err = capfd.readouterr()
|
||||
result = json.loads(out)
|
||||
|
||||
assert e.value.code != 0, "result: %r" % (result,)
|
||||
assert result["msg"] == missing_required_lib("hvac")
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"patch_ansible_module", [[_combined_options(read_before_write=True)]], indirect=True
|
||||
)
|
||||
@pytest.mark.parametrize(
|
||||
"response",
|
||||
({"thishasnodata": {}}, {"data": {"not data": {}}}),
|
||||
)
|
||||
def test_vault_kv2_write_read_responses_invalid(
|
||||
self, vault_client, capfd, response
|
||||
):
|
||||
client = vault_client
|
||||
|
||||
client.secrets.kv.v2.read_secret_version.return_value = response
|
||||
|
||||
with pytest.raises(SystemExit) as e:
|
||||
vault_kv2_write.main()
|
||||
|
||||
out, err = capfd.readouterr()
|
||||
result = json.loads(out)
|
||||
|
||||
assert e.value.code != 0, "result: %r" % (result,)
|
||||
assert "Vault response did not contain data" in result["msg"]
|
||||
|
||||
@pytest.mark.parametrize("exc", [hvac.exceptions.VaultError("throwaway msg")])
|
||||
@pytest.mark.parametrize(
|
||||
"patch_ansible_module", [_combined_options(read_before_write=True)], indirect=True
|
||||
)
|
||||
def test_vault_kv2_write_read_vault_error(self, vault_client, capfd, exc):
|
||||
client = vault_client
|
||||
|
||||
client.secrets.kv.v2.read_secret_version.side_effect = exc
|
||||
|
||||
with pytest.raises(SystemExit) as e:
|
||||
vault_kv2_write.main()
|
||||
|
||||
out, err = capfd.readouterr()
|
||||
result = json.loads(out)
|
||||
|
||||
assert e.value.code != 0, "result: %r" % (result,)
|
||||
assert "VaultError reading" in result["msg"], "result: %r" % (result,)
|
||||
|
||||
@pytest.mark.parametrize("exc", [hvac.exceptions.InvalidPath("throwaway msg")])
|
||||
@pytest.mark.parametrize(
|
||||
"patch_ansible_module", [_combined_options()], indirect=True
|
||||
)
|
||||
def test_vault_kv2_write_write_invalid_path(self, vault_client, capfd, exc):
|
||||
client = vault_client
|
||||
|
||||
client.secrets.kv.v2.create_or_update_secret.side_effect = exc
|
||||
|
||||
with pytest.raises(SystemExit) as e:
|
||||
vault_kv2_write.main()
|
||||
|
||||
out, err = capfd.readouterr()
|
||||
result = json.loads(out)
|
||||
|
||||
assert e.value.code != 0, "result: %r" % (result,)
|
||||
assert "InvalidPath writing to" in result["msg"], "result: %r" % (result,)
|
||||
+164
@@ -0,0 +1,164 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright (c) 2023 Tom Kivlin (@tomkivlin)
|
||||
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
from __future__ import (absolute_import, division, print_function)
|
||||
__metaclass__ = type
|
||||
|
||||
import pytest
|
||||
import re
|
||||
import json
|
||||
|
||||
from ansible.module_utils.basic import missing_required_lib
|
||||
|
||||
from ...compat import mock
|
||||
from .....plugins.modules import vault_list
|
||||
from .....plugins.module_utils._hashi_vault_common import HashiVaultValueError
|
||||
|
||||
|
||||
hvac = pytest.importorskip('hvac')
|
||||
|
||||
|
||||
pytestmark = pytest.mark.usefixtures(
|
||||
'patch_ansible_module',
|
||||
'patch_authenticator',
|
||||
'patch_get_vault_client',
|
||||
)
|
||||
|
||||
|
||||
def _connection_options():
|
||||
return {
|
||||
'auth_method': 'token',
|
||||
'url': 'http://myvault',
|
||||
'token': 'beep-boop',
|
||||
}
|
||||
|
||||
|
||||
def _sample_options():
|
||||
return {
|
||||
'path': 'endpoint',
|
||||
}
|
||||
|
||||
|
||||
def _combined_options(**kwargs):
|
||||
opt = _connection_options()
|
||||
opt.update(_sample_options())
|
||||
opt.update(kwargs)
|
||||
return opt
|
||||
|
||||
|
||||
LIST_FIXTURES = [
|
||||
'kv2_list_response.json',
|
||||
'policy_list_response.json',
|
||||
'userpass_list_response.json',
|
||||
]
|
||||
|
||||
|
||||
@pytest.fixture(params=LIST_FIXTURES)
|
||||
def list_response(request, fixture_loader):
|
||||
return fixture_loader(request.param)
|
||||
|
||||
|
||||
class TestModuleVaultList():
|
||||
|
||||
@pytest.mark.parametrize('patch_ansible_module', [_combined_options()], indirect=True)
|
||||
@pytest.mark.parametrize('exc', [HashiVaultValueError('throwaway msg'), NotImplementedError('throwaway msg')])
|
||||
def test_vault_list_authentication_error(self, authenticator, exc, capfd):
|
||||
authenticator.authenticate.side_effect = exc
|
||||
|
||||
with pytest.raises(SystemExit) as e:
|
||||
vault_list.main()
|
||||
|
||||
out, err = capfd.readouterr()
|
||||
result = json.loads(out)
|
||||
|
||||
assert e.value.code != 0, "result: %r" % (result,)
|
||||
assert result['msg'] == 'throwaway msg', "result: %r" % result
|
||||
|
||||
@pytest.mark.parametrize('patch_ansible_module', [_combined_options()], indirect=True)
|
||||
@pytest.mark.parametrize('exc', [HashiVaultValueError('throwaway msg'), NotImplementedError('throwaway msg')])
|
||||
def test_vault_list_auth_validation_error(self, authenticator, exc, capfd):
|
||||
authenticator.validate.side_effect = exc
|
||||
|
||||
with pytest.raises(SystemExit) as e:
|
||||
vault_list.main()
|
||||
|
||||
out, err = capfd.readouterr()
|
||||
result = json.loads(out)
|
||||
|
||||
assert e.value.code != 0, "result: %r" % (result,)
|
||||
assert result['msg'] == 'throwaway msg'
|
||||
|
||||
@pytest.mark.parametrize('patch_ansible_module', [_combined_options()], indirect=True)
|
||||
def test_vault_list_return_data(self, patch_ansible_module, list_response, vault_client, capfd):
|
||||
client = vault_client
|
||||
client.list.return_value = list_response.copy()
|
||||
|
||||
with pytest.raises(SystemExit) as e:
|
||||
vault_list.main()
|
||||
|
||||
out, err = capfd.readouterr()
|
||||
result = json.loads(out)
|
||||
|
||||
assert e.value.code == 0, "result: %r" % (result,)
|
||||
|
||||
client.list.assert_called_once_with(patch_ansible_module['path'])
|
||||
|
||||
assert result['data'] == list_response, "module result did not match expected result:\nexpected: %r\ngot: %r" % (list_response, result)
|
||||
|
||||
@pytest.mark.parametrize('patch_ansible_module', [_combined_options()], indirect=True)
|
||||
def test_vault_list_no_data(self, patch_ansible_module, vault_client, capfd):
|
||||
client = vault_client
|
||||
client.list.return_value = None
|
||||
|
||||
with pytest.raises(SystemExit) as e:
|
||||
vault_list.main()
|
||||
|
||||
out, err = capfd.readouterr()
|
||||
result = json.loads(out)
|
||||
|
||||
assert e.value.code != 0, "result: %r" % (result,)
|
||||
|
||||
client.list.assert_called_once_with(patch_ansible_module['path'])
|
||||
|
||||
match = re.search(r"The path '[^']+' doesn't seem to exist", result['msg'])
|
||||
|
||||
assert match is not None, "Unexpected msg: %s" % result['msg']
|
||||
|
||||
@pytest.mark.parametrize('patch_ansible_module', [_combined_options()], indirect=True)
|
||||
def test_vault_list_no_hvac(self, capfd):
|
||||
with mock.patch.multiple(vault_list, HAS_HVAC=False, HVAC_IMPORT_ERROR=None, create=True):
|
||||
with pytest.raises(SystemExit) as e:
|
||||
vault_list.main()
|
||||
|
||||
out, err = capfd.readouterr()
|
||||
result = json.loads(out)
|
||||
|
||||
assert e.value.code != 0, "result: %r" % (result,)
|
||||
assert result['msg'] == missing_required_lib('hvac')
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
'exc',
|
||||
[
|
||||
(hvac.exceptions.Forbidden, "", r"^Forbidden: Permission Denied to path '([^']+)'"),
|
||||
]
|
||||
)
|
||||
@pytest.mark.parametrize('patch_ansible_module', [[_combined_options(), 'path']], indirect=True)
|
||||
@pytest.mark.parametrize('opt_path', ['path/1', 'second/path'])
|
||||
def test_vault_list_vault_exception(self, vault_client, exc, opt_path, capfd):
|
||||
|
||||
client = vault_client
|
||||
client.list.side_effect = exc[0](exc[1])
|
||||
|
||||
with pytest.raises(SystemExit) as e:
|
||||
vault_list.main()
|
||||
|
||||
out, err = capfd.readouterr()
|
||||
result = json.loads(out)
|
||||
|
||||
assert e.value.code != 0, "result: %r" % (result,)
|
||||
match = re.search(exc[2], result['msg'])
|
||||
assert match is not None, "result: %r\ndid not match: %s" % (result, exc[2])
|
||||
|
||||
assert opt_path == match.group(1)
|
||||
+133
@@ -0,0 +1,133 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright (c) 2022 Brian Scholer (@briantist)
|
||||
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
from __future__ import (absolute_import, division, print_function)
|
||||
__metaclass__ = type
|
||||
|
||||
import pytest
|
||||
import json
|
||||
|
||||
from ansible.module_utils.basic import missing_required_lib
|
||||
|
||||
from ...compat import mock
|
||||
from .....plugins.modules import vault_login
|
||||
from .....plugins.module_utils._hashi_vault_common import HashiVaultValueError
|
||||
|
||||
|
||||
hvac = pytest.importorskip('hvac')
|
||||
|
||||
|
||||
pytestmark = pytest.mark.usefixtures(
|
||||
'patch_ansible_module',
|
||||
'patch_authenticator',
|
||||
'patch_get_vault_client',
|
||||
)
|
||||
|
||||
|
||||
def _connection_options():
|
||||
return {
|
||||
'auth_method': 'token',
|
||||
'url': 'http://myvault',
|
||||
'token': 'beep-boop',
|
||||
}
|
||||
|
||||
|
||||
def _sample_options():
|
||||
return {}
|
||||
|
||||
|
||||
def _combined_options(**kwargs):
|
||||
opt = _connection_options()
|
||||
opt.update(_sample_options())
|
||||
opt.update(kwargs)
|
||||
return opt
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def token_lookup_full_response(fixture_loader):
|
||||
return fixture_loader('lookup-self_with_meta.json')
|
||||
|
||||
|
||||
class TestModuleVaultLogin():
|
||||
|
||||
@pytest.mark.parametrize('patch_ansible_module', [_combined_options()], indirect=True)
|
||||
@pytest.mark.parametrize('exc', [HashiVaultValueError('throwaway msg'), NotImplementedError('throwaway msg')])
|
||||
def test_vault_login_authentication_error(self, authenticator, exc, capfd):
|
||||
authenticator.authenticate.side_effect = exc
|
||||
|
||||
with pytest.raises(SystemExit) as e:
|
||||
vault_login.main()
|
||||
|
||||
out, err = capfd.readouterr()
|
||||
result = json.loads(out)
|
||||
|
||||
assert e.value.code != 0, "result: %r" % (result,)
|
||||
assert result['msg'] == 'throwaway msg', "result: %r" % result
|
||||
|
||||
@pytest.mark.parametrize('patch_ansible_module', [_combined_options()], indirect=True)
|
||||
@pytest.mark.parametrize('exc', [HashiVaultValueError('throwaway msg'), NotImplementedError('throwaway msg')])
|
||||
def test_vault_login_auth_validation_error(self, authenticator, exc, capfd):
|
||||
authenticator.validate.side_effect = exc
|
||||
|
||||
with pytest.raises(SystemExit) as e:
|
||||
vault_login.main()
|
||||
|
||||
out, err = capfd.readouterr()
|
||||
result = json.loads(out)
|
||||
|
||||
assert e.value.code != 0, "result: %r" % (result,)
|
||||
assert result['msg'] == 'throwaway msg'
|
||||
|
||||
@pytest.mark.parametrize('opt__ansible_check_mode', [False, True])
|
||||
@pytest.mark.parametrize(
|
||||
['opt_auth_method', 'opt_token', 'opt_role_id'],
|
||||
[
|
||||
('token', 'beep-boop-bloop', None),
|
||||
('approle', None, 'not-used'),
|
||||
]
|
||||
)
|
||||
@pytest.mark.parametrize('patch_ansible_module', [[
|
||||
_combined_options(),
|
||||
'_ansible_check_mode',
|
||||
'auth_method',
|
||||
'token',
|
||||
'role_id',
|
||||
]], indirect=True)
|
||||
def test_vault_login_return_data(
|
||||
self, patch_ansible_module, token_lookup_full_response, authenticator, vault_client,
|
||||
opt__ansible_check_mode, opt_auth_method, opt_token, opt_role_id, capfd
|
||||
):
|
||||
authenticator.authenticate.return_value = token_lookup_full_response
|
||||
|
||||
with pytest.raises(SystemExit) as e:
|
||||
vault_login.main()
|
||||
|
||||
out, err = capfd.readouterr()
|
||||
result = json.loads(out)
|
||||
|
||||
assert e.value.code == 0, "result: %r" % (result,)
|
||||
|
||||
authenticator.validate.assert_called_once()
|
||||
|
||||
assert result['changed'] == (opt_auth_method != 'token')
|
||||
|
||||
if opt__ansible_check_mode:
|
||||
authenticator.authenticate.assert_not_called()
|
||||
assert result['login'] == {'auth': {'client_token': None}}
|
||||
else:
|
||||
authenticator.authenticate.assert_called_once_with(vault_client)
|
||||
assert result['login'] == token_lookup_full_response, "expected: %r\ngot: %r" % (token_lookup_full_response, result['login'])
|
||||
|
||||
@pytest.mark.parametrize('patch_ansible_module', [_combined_options()], indirect=True)
|
||||
def test_vault_login_no_hvac(self, capfd):
|
||||
with mock.patch.multiple(vault_login, HAS_HVAC=False, HVAC_IMPORT_ERROR=None, create=True):
|
||||
with pytest.raises(SystemExit) as e:
|
||||
vault_login.main()
|
||||
|
||||
out, err = capfd.readouterr()
|
||||
result = json.loads(out)
|
||||
|
||||
assert e.value.code != 0, "result: %r" % (result,)
|
||||
assert result['msg'] == missing_required_lib('hvac')
|
||||
+131
@@ -0,0 +1,131 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright (c) 2022 Brian Scholer (@briantist)
|
||||
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
from __future__ import (absolute_import, division, print_function)
|
||||
__metaclass__ = type
|
||||
|
||||
import pytest
|
||||
|
||||
import json
|
||||
|
||||
from ansible.module_utils.basic import missing_required_lib
|
||||
|
||||
from ...compat import mock
|
||||
from .....plugins.modules import vault_pki_generate_certificate
|
||||
|
||||
pytestmark = pytest.mark.usefixtures(
|
||||
'patch_ansible_module',
|
||||
'patch_authenticator',
|
||||
'patch_get_vault_client',
|
||||
)
|
||||
|
||||
|
||||
def _connection_options():
|
||||
return {
|
||||
'auth_method': 'token',
|
||||
'url': 'http://myvault',
|
||||
'token': 'throwaway',
|
||||
}
|
||||
|
||||
|
||||
def _sample_options():
|
||||
return {
|
||||
'role_name': 'some_role',
|
||||
'common_name': 'common_name',
|
||||
'alt_names': ['a', 'b'],
|
||||
'ip_sans': ['c', 'd'],
|
||||
'uri_sans': ['e', 'f'],
|
||||
'other_sans': ['g', 'h'],
|
||||
'ttl': '1h',
|
||||
'format': 'der',
|
||||
'private_key_format': 'pkcs8',
|
||||
'exclude_cn_from_sans': True,
|
||||
'engine_mount_point': 'alt',
|
||||
}
|
||||
|
||||
|
||||
def _combined_options(**kwargs):
|
||||
opt = _connection_options()
|
||||
opt.update(_sample_options())
|
||||
opt.update(kwargs)
|
||||
return opt
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def sample_options():
|
||||
return _sample_options()
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def translated_options(sample_options):
|
||||
toplevel = {
|
||||
'role_name': 'name',
|
||||
'engine_mount_point': 'mount_point',
|
||||
'common_name': 'common_name',
|
||||
}
|
||||
|
||||
opt = {'extra_params': {}}
|
||||
for k, v in sample_options.items():
|
||||
if k in toplevel:
|
||||
opt[toplevel[k]] = v
|
||||
else:
|
||||
if isinstance(v, list):
|
||||
val = ','.join(v)
|
||||
else:
|
||||
val = v
|
||||
|
||||
opt['extra_params'][k] = val
|
||||
|
||||
return opt
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def pki_generate_certificate_response(fixture_loader):
|
||||
return fixture_loader('pki_generate_certificate_response.json')
|
||||
|
||||
|
||||
class TestModuleVaultPkiGenerateCertificate():
|
||||
|
||||
@pytest.mark.parametrize('patch_ansible_module', [_combined_options()], indirect=True)
|
||||
def test_vault_pki_generate_certificate_options(self, pki_generate_certificate_response, translated_options, vault_client, capfd):
|
||||
client = vault_client
|
||||
client.secrets.pki.generate_certificate.return_value = pki_generate_certificate_response
|
||||
|
||||
with pytest.raises(SystemExit) as e:
|
||||
vault_pki_generate_certificate.main()
|
||||
|
||||
out, err = capfd.readouterr()
|
||||
result = json.loads(out)
|
||||
|
||||
client.secrets.pki.generate_certificate.assert_called_once_with(**translated_options)
|
||||
|
||||
assert result['data'] == pki_generate_certificate_response, (
|
||||
"module result did not match expected result:\nmodule: %r\nexpected: %r" % (result['data'], pki_generate_certificate_response)
|
||||
)
|
||||
assert e.value.code == 0
|
||||
|
||||
@pytest.mark.parametrize('patch_ansible_module', [_combined_options()], indirect=True)
|
||||
def test_vault_pki_generate_certificate_no_hvac(self, capfd):
|
||||
with mock.patch.multiple(vault_pki_generate_certificate, HAS_HVAC=False, HVAC_IMPORT_ERROR=None, create=True):
|
||||
with pytest.raises(SystemExit) as e:
|
||||
vault_pki_generate_certificate.main()
|
||||
|
||||
out, err = capfd.readouterr()
|
||||
result = json.loads(out)
|
||||
|
||||
assert result['msg'] == missing_required_lib('hvac')
|
||||
assert e.value.code != 0
|
||||
|
||||
@pytest.mark.parametrize('patch_ansible_module', [_combined_options()], indirect=True)
|
||||
def test_vault_pki_generate_certificate_vault_exception(self, vault_client, capfd):
|
||||
hvac = pytest.importorskip('hvac')
|
||||
|
||||
client = vault_client
|
||||
client.secrets.pki.generate_certificate.side_effect = hvac.exceptions.VaultError
|
||||
|
||||
with pytest.raises(SystemExit) as e:
|
||||
vault_pki_generate_certificate.main()
|
||||
|
||||
assert e.value.code != 0
|
||||
+157
@@ -0,0 +1,157 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright (c) 2022 Brian Scholer (@briantist)
|
||||
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
from __future__ import (absolute_import, division, print_function)
|
||||
__metaclass__ = type
|
||||
|
||||
import pytest
|
||||
import re
|
||||
import json
|
||||
|
||||
from ansible.module_utils.basic import missing_required_lib
|
||||
|
||||
from ...compat import mock
|
||||
from .....plugins.modules import vault_read
|
||||
from .....plugins.module_utils._hashi_vault_common import HashiVaultValueError
|
||||
|
||||
|
||||
hvac = pytest.importorskip('hvac')
|
||||
|
||||
|
||||
pytestmark = pytest.mark.usefixtures(
|
||||
'patch_ansible_module',
|
||||
'patch_authenticator',
|
||||
'patch_get_vault_client',
|
||||
)
|
||||
|
||||
|
||||
def _connection_options():
|
||||
return {
|
||||
'auth_method': 'token',
|
||||
'url': 'http://myvault',
|
||||
'token': 'beep-boop',
|
||||
}
|
||||
|
||||
|
||||
def _sample_options():
|
||||
return {
|
||||
'path': 'endpoint',
|
||||
}
|
||||
|
||||
|
||||
def _combined_options(**kwargs):
|
||||
opt = _connection_options()
|
||||
opt.update(_sample_options())
|
||||
opt.update(kwargs)
|
||||
return opt
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def kv1_get_response(fixture_loader):
|
||||
return fixture_loader('kv1_get_response.json')
|
||||
|
||||
|
||||
class TestModuleVaultRead():
|
||||
|
||||
@pytest.mark.parametrize('patch_ansible_module', [_combined_options()], indirect=True)
|
||||
@pytest.mark.parametrize('exc', [HashiVaultValueError('throwaway msg'), NotImplementedError('throwaway msg')])
|
||||
def test_vault_read_authentication_error(self, authenticator, exc, capfd):
|
||||
authenticator.authenticate.side_effect = exc
|
||||
|
||||
with pytest.raises(SystemExit) as e:
|
||||
vault_read.main()
|
||||
|
||||
out, err = capfd.readouterr()
|
||||
result = json.loads(out)
|
||||
|
||||
assert e.value.code != 0, "result: %r" % (result,)
|
||||
assert result['msg'] == 'throwaway msg', "result: %r" % result
|
||||
|
||||
@pytest.mark.parametrize('patch_ansible_module', [_combined_options()], indirect=True)
|
||||
@pytest.mark.parametrize('exc', [HashiVaultValueError('throwaway msg'), NotImplementedError('throwaway msg')])
|
||||
def test_vault_read_auth_validation_error(self, authenticator, exc, capfd):
|
||||
authenticator.validate.side_effect = exc
|
||||
|
||||
with pytest.raises(SystemExit) as e:
|
||||
vault_read.main()
|
||||
|
||||
out, err = capfd.readouterr()
|
||||
result = json.loads(out)
|
||||
|
||||
assert e.value.code != 0, "result: %r" % (result,)
|
||||
assert result['msg'] == 'throwaway msg'
|
||||
|
||||
@pytest.mark.parametrize('patch_ansible_module', [_combined_options()], indirect=True)
|
||||
def test_vault_read_return_data(self, patch_ansible_module, kv1_get_response, vault_client, capfd):
|
||||
client = vault_client
|
||||
client.read.return_value = kv1_get_response.copy()
|
||||
|
||||
with pytest.raises(SystemExit) as e:
|
||||
vault_read.main()
|
||||
|
||||
out, err = capfd.readouterr()
|
||||
result = json.loads(out)
|
||||
|
||||
assert e.value.code == 0, "result: %r" % (result,)
|
||||
|
||||
client.read.assert_called_once_with(patch_ansible_module['path'])
|
||||
|
||||
assert result['data'] == kv1_get_response, "module result did not match expected result:\nexpected: %r\ngot: %r" % (kv1_get_response, result)
|
||||
|
||||
@pytest.mark.parametrize('patch_ansible_module', [_combined_options()], indirect=True)
|
||||
def test_vault_read_no_data(self, patch_ansible_module, vault_client, capfd):
|
||||
client = vault_client
|
||||
client.read.return_value = None
|
||||
|
||||
with pytest.raises(SystemExit) as e:
|
||||
vault_read.main()
|
||||
|
||||
out, err = capfd.readouterr()
|
||||
result = json.loads(out)
|
||||
|
||||
assert e.value.code != 0, "result: %r" % (result,)
|
||||
|
||||
client.read.assert_called_once_with(patch_ansible_module['path'])
|
||||
|
||||
match = re.search(r"The path '[^']+' doesn't seem to exist", result['msg'])
|
||||
|
||||
assert match is not None, "Unexpected msg: %s" % result['msg']
|
||||
|
||||
@pytest.mark.parametrize('patch_ansible_module', [_combined_options()], indirect=True)
|
||||
def test_vault_read_no_hvac(self, capfd):
|
||||
with mock.patch.multiple(vault_read, HAS_HVAC=False, HVAC_IMPORT_ERROR=None, create=True):
|
||||
with pytest.raises(SystemExit) as e:
|
||||
vault_read.main()
|
||||
|
||||
out, err = capfd.readouterr()
|
||||
result = json.loads(out)
|
||||
|
||||
assert e.value.code != 0, "result: %r" % (result,)
|
||||
assert result['msg'] == missing_required_lib('hvac')
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
'exc',
|
||||
[
|
||||
(hvac.exceptions.Forbidden, "", r"^Forbidden: Permission Denied to path '([^']+)'"),
|
||||
]
|
||||
)
|
||||
@pytest.mark.parametrize('patch_ansible_module', [[_combined_options(), 'path']], indirect=True)
|
||||
@pytest.mark.parametrize('opt_path', ['path/1', 'second/path'])
|
||||
def test_vault_read_vault_exception(self, vault_client, exc, opt_path, capfd):
|
||||
|
||||
client = vault_client
|
||||
client.read.side_effect = exc[0](exc[1])
|
||||
|
||||
with pytest.raises(SystemExit) as e:
|
||||
vault_read.main()
|
||||
|
||||
out, err = capfd.readouterr()
|
||||
result = json.loads(out)
|
||||
|
||||
assert e.value.code != 0, "result: %r" % (result,)
|
||||
match = re.search(exc[2], result['msg'])
|
||||
assert match is not None, "result: %r\ndid not match: %s" % (result, exc[2])
|
||||
|
||||
assert opt_path == match.group(1)
|
||||
+269
@@ -0,0 +1,269 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright (c) 2022 Brian Scholer (@briantist)
|
||||
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
from __future__ import (absolute_import, division, print_function)
|
||||
__metaclass__ = type
|
||||
|
||||
import sys
|
||||
import pytest
|
||||
|
||||
import json
|
||||
|
||||
from .....plugins.modules import vault_token_create
|
||||
from .....plugins.module_utils._hashi_vault_common import HashiVaultValueError
|
||||
|
||||
pytestmark = pytest.mark.usefixtures(
|
||||
'patch_ansible_module',
|
||||
'patch_authenticator',
|
||||
'patch_get_vault_client',
|
||||
)
|
||||
|
||||
|
||||
def _connection_options():
|
||||
return {
|
||||
'auth_method': 'token',
|
||||
'url': 'http://myvault',
|
||||
'token': 'rando',
|
||||
}
|
||||
|
||||
|
||||
def _pass_thru_options():
|
||||
return {
|
||||
'no_parent': True,
|
||||
'no_default_policy': True,
|
||||
'policies': ['a', 'b'],
|
||||
'id': 'tokenid',
|
||||
'role_name': 'role',
|
||||
'meta': {'a': 'valA', 'b': 'valB'},
|
||||
'renewable': True,
|
||||
'ttl': '1h',
|
||||
'type': 'batch',
|
||||
'explicit_max_ttl': '2h',
|
||||
'display_name': 'kiminonamae',
|
||||
'num_uses': 9,
|
||||
'period': '8h',
|
||||
'entity_alias': 'alias',
|
||||
'wrap_ttl': '60s',
|
||||
}
|
||||
|
||||
|
||||
def _combined_options(**kwargs):
|
||||
opt = _connection_options()
|
||||
opt.update(_pass_thru_options())
|
||||
opt.update(kwargs)
|
||||
return opt
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def pass_thru_options():
|
||||
return _pass_thru_options()
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def orphan_option_translation():
|
||||
return {
|
||||
'id': 'token_id',
|
||||
'role_name': 'role',
|
||||
'type': 'token_type',
|
||||
}
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def token_create_response(fixture_loader):
|
||||
return fixture_loader('token_create_response.json')
|
||||
|
||||
|
||||
class TestModuleVaultTokenCreate():
|
||||
|
||||
@pytest.mark.parametrize('patch_ansible_module', [_combined_options()], indirect=True)
|
||||
@pytest.mark.parametrize('exc', [HashiVaultValueError('throwaway msg'), NotImplementedError('throwaway msg')])
|
||||
def test_vault_token_create_authentication_error(self, authenticator, exc, capfd):
|
||||
authenticator.authenticate.side_effect = exc
|
||||
|
||||
with pytest.raises(SystemExit) as e:
|
||||
vault_token_create.main()
|
||||
|
||||
out, err = capfd.readouterr()
|
||||
result = json.loads(out)
|
||||
|
||||
assert e.value.code != 0, "result: %r" % (result,)
|
||||
assert result['msg'] == 'throwaway msg', "result: %r" % result
|
||||
|
||||
@pytest.mark.parametrize('patch_ansible_module', [_combined_options()], indirect=True)
|
||||
@pytest.mark.parametrize('exc', [HashiVaultValueError('throwaway msg'), NotImplementedError('throwaway msg')])
|
||||
def test_vault_token_create_auth_validation_error(self, authenticator, exc, capfd):
|
||||
authenticator.validate.side_effect = exc
|
||||
|
||||
with pytest.raises(SystemExit) as e:
|
||||
vault_token_create.main()
|
||||
|
||||
out, err = capfd.readouterr()
|
||||
result = json.loads(out)
|
||||
|
||||
assert e.value.code != 0, "result: %r" % (result,)
|
||||
assert result['msg'] == 'throwaway msg'
|
||||
|
||||
@pytest.mark.no_ansible_module_patch
|
||||
def test_vault_token_create_passthru_options_expected(self, pass_thru_options):
|
||||
# designed to catch the case where new passthru options differ between tests and module
|
||||
|
||||
module_set = set(vault_token_create.PASS_THRU_OPTION_NAMES)
|
||||
test_set = set(pass_thru_options.keys())
|
||||
|
||||
assert sorted(vault_token_create.PASS_THRU_OPTION_NAMES) == sorted(pass_thru_options.keys()), (
|
||||
"Passthru options in module do not match options in test: %r" % (
|
||||
list(module_set ^ test_set)
|
||||
)
|
||||
)
|
||||
|
||||
@pytest.mark.no_ansible_module_patch
|
||||
def test_vault_token_create_orphan_options_expected(self, orphan_option_translation, pass_thru_options):
|
||||
# designed to catch the case where new orphan translations differ between tests and module
|
||||
# and that all listed translations are present in passthru options
|
||||
|
||||
module_set = set(vault_token_create.ORPHAN_OPTION_TRANSLATION.items())
|
||||
test_set = set(orphan_option_translation.items())
|
||||
|
||||
module_key_set = set(vault_token_create.ORPHAN_OPTION_TRANSLATION.keys())
|
||||
pass_thru_key_set = set(pass_thru_options.keys())
|
||||
|
||||
assert module_set == test_set, (
|
||||
"Orphan options in module do not match orphan options in test:\nmodule: %r\ntest: %r" % (
|
||||
dict(module_set - test_set),
|
||||
dict(test_set - module_set),
|
||||
)
|
||||
)
|
||||
assert vault_token_create.ORPHAN_OPTION_TRANSLATION.keys() <= pass_thru_options.keys(), (
|
||||
"Orphan option translation keys must exist in passthru options: %r" % (
|
||||
list(module_key_set - pass_thru_key_set),
|
||||
)
|
||||
)
|
||||
|
||||
@pytest.mark.parametrize('patch_ansible_module', [_combined_options()], indirect=True)
|
||||
def test_vault_token_create_passthru_options(self, pass_thru_options, token_create_response, vault_client, capfd):
|
||||
client = vault_client
|
||||
client.auth.token.create.return_value = token_create_response
|
||||
|
||||
with pytest.raises(SystemExit):
|
||||
vault_token_create.main()
|
||||
|
||||
out, err = capfd.readouterr()
|
||||
result = json.loads(out)
|
||||
|
||||
client.create_token.assert_not_called()
|
||||
client.auth.token.create_orphan.assert_not_called()
|
||||
client.auth.token.create.assert_called_once()
|
||||
|
||||
assert result['login'] == token_create_response, (
|
||||
"module result did not match expected result:\nmodule: %r\nexpected: %r" % (result['login'], token_create_response)
|
||||
)
|
||||
|
||||
if sys.version_info < (3, 8):
|
||||
# TODO: remove when python < 3.8 is dropped
|
||||
assert pass_thru_options.items() <= client.auth.token.create.call_args[1].items()
|
||||
else:
|
||||
assert pass_thru_options.items() <= client.auth.token.create.call_args.kwargs.items()
|
||||
|
||||
@pytest.mark.parametrize('patch_ansible_module', [_combined_options(orphan=True)], indirect=True)
|
||||
def test_vault_token_create_orphan_options(self, pass_thru_options, orphan_option_translation, token_create_response, vault_client, capfd):
|
||||
client = vault_client
|
||||
client.auth.token.create_orphan.return_value = token_create_response
|
||||
|
||||
with pytest.raises(SystemExit):
|
||||
vault_token_create.main()
|
||||
|
||||
out, err = capfd.readouterr()
|
||||
result = json.loads(out)
|
||||
|
||||
client.create_token.assert_not_called()
|
||||
client.auth.token.create.assert_not_called()
|
||||
client.auth.token.create_orphan.assert_called_once()
|
||||
|
||||
assert result['login'] == token_create_response, (
|
||||
"module result did not match expected result:\nmodule: %r\nexpected: %r" % (result['module'], token_create_response)
|
||||
)
|
||||
|
||||
if sys.version_info < (3, 8):
|
||||
# TODO: remove when python < 3.8 is dropped
|
||||
call_kwargs = client.auth.token.create_orphan.call_args[1]
|
||||
else:
|
||||
call_kwargs = client.auth.token.create_orphan.call_args.kwargs
|
||||
|
||||
for name, orphan in orphan_option_translation.items():
|
||||
assert name not in call_kwargs, (
|
||||
"'%s' was found in call to orphan method, should be '%s'" % (name, orphan)
|
||||
)
|
||||
assert orphan in call_kwargs, (
|
||||
"'%s' (from '%s') was not found in call to orphan method" % (orphan, name)
|
||||
)
|
||||
assert call_kwargs[orphan] == pass_thru_options.get(name), (
|
||||
"Expected orphan param '%s' not found or value did not match:\nvalue: %r\nexpected: %r" % (
|
||||
orphan,
|
||||
call_kwargs.get(orphan),
|
||||
pass_thru_options.get(name),
|
||||
)
|
||||
)
|
||||
|
||||
@pytest.mark.parametrize('patch_ansible_module', [_combined_options(orphan=True)], indirect=True)
|
||||
def test_vault_token_create_orphan_fallback(self, token_create_response, vault_client, capfd):
|
||||
client = vault_client
|
||||
client.create_token.return_value = token_create_response
|
||||
client.auth.token.create_orphan.side_effect = AttributeError
|
||||
|
||||
with pytest.raises(SystemExit):
|
||||
vault_token_create.main()
|
||||
|
||||
out, err = capfd.readouterr()
|
||||
result = json.loads(out)
|
||||
|
||||
client.auth.token.create_orphan.assert_called_once()
|
||||
client.create_token.assert_called_once()
|
||||
|
||||
assert result['login'] == token_create_response, (
|
||||
"module result did not match expected result:\nmodule: %r\nexpected: %r" % (result['login'], token_create_response)
|
||||
)
|
||||
|
||||
@pytest.mark.parametrize('patch_ansible_module', [_combined_options()], indirect=True)
|
||||
def test_vault_token_create_exception_handling_standard(self, vault_client, capfd):
|
||||
client = vault_client
|
||||
client.auth.token.create.side_effect = Exception('side_effect')
|
||||
|
||||
with pytest.raises(SystemExit) as e:
|
||||
vault_token_create.main()
|
||||
|
||||
out, err = capfd.readouterr()
|
||||
result = json.loads(out)
|
||||
|
||||
assert e.value.code != 0, "result: %r" % (result,)
|
||||
assert result['msg'] == 'side_effect'
|
||||
|
||||
@pytest.mark.parametrize('patch_ansible_module', [_combined_options(orphan=True)], indirect=True)
|
||||
def test_vault_token_create_exception_handling_orphan(self, vault_client, capfd):
|
||||
client = vault_client
|
||||
client.auth.token.create_orphan.side_effect = Exception('side_effect')
|
||||
|
||||
with pytest.raises(SystemExit) as e:
|
||||
vault_token_create.main()
|
||||
|
||||
out, err = capfd.readouterr()
|
||||
result = json.loads(out)
|
||||
|
||||
assert e.value.code != 0, "result: %r" % (result,)
|
||||
assert result['msg'] == 'side_effect'
|
||||
|
||||
@pytest.mark.parametrize('patch_ansible_module', [_combined_options(orphan=True)], indirect=True)
|
||||
def test_vault_token_create_exception_handling_orphan_fallback(self, vault_client, capfd):
|
||||
client = vault_client
|
||||
client.create_token.side_effect = Exception('side_effect')
|
||||
client.auth.token.create_orphan.side_effect = AttributeError
|
||||
|
||||
with pytest.raises(SystemExit) as e:
|
||||
vault_token_create.main()
|
||||
|
||||
out, err = capfd.readouterr()
|
||||
result = json.loads(out)
|
||||
|
||||
assert e.value.code != 0, "result: %r" % (result,)
|
||||
assert result['msg'] == 'side_effect'
|
||||
+226
@@ -0,0 +1,226 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright (c) 2022 Brian Scholer (@briantist)
|
||||
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
from __future__ import (absolute_import, division, print_function)
|
||||
__metaclass__ = type
|
||||
|
||||
import pytest
|
||||
import re
|
||||
import json
|
||||
|
||||
from ansible.module_utils.basic import missing_required_lib
|
||||
|
||||
from ...compat import mock
|
||||
from .....plugins.modules import vault_write
|
||||
from .....plugins.module_utils._hashi_vault_common import HashiVaultValueError
|
||||
|
||||
|
||||
hvac = pytest.importorskip('hvac')
|
||||
|
||||
|
||||
pytestmark = pytest.mark.usefixtures(
|
||||
'patch_ansible_module',
|
||||
'patch_authenticator',
|
||||
'patch_get_vault_client',
|
||||
)
|
||||
|
||||
|
||||
def _connection_options():
|
||||
return {
|
||||
'auth_method': 'token',
|
||||
'url': 'http://myvault',
|
||||
'token': 'beep-boop',
|
||||
}
|
||||
|
||||
|
||||
def _sample_options():
|
||||
return {
|
||||
'path': 'endpoint',
|
||||
}
|
||||
|
||||
|
||||
def _combined_options(**kwargs):
|
||||
opt = _connection_options()
|
||||
opt.update(_sample_options())
|
||||
opt.update(kwargs)
|
||||
return opt
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def approle_secret_id_write_response(fixture_loader):
|
||||
return fixture_loader('approle_secret_id_write_response.json')
|
||||
|
||||
|
||||
class TestModuleVaultWrite():
|
||||
|
||||
@pytest.mark.parametrize('patch_ansible_module', [_combined_options()], indirect=True)
|
||||
@pytest.mark.parametrize('exc', [HashiVaultValueError('throwaway msg'), NotImplementedError('throwaway msg')])
|
||||
def test_vault_write_authentication_error(self, authenticator, exc, capfd):
|
||||
authenticator.authenticate.side_effect = exc
|
||||
|
||||
with pytest.raises(SystemExit) as e:
|
||||
vault_write.main()
|
||||
|
||||
out, err = capfd.readouterr()
|
||||
result = json.loads(out)
|
||||
|
||||
assert e.value.code != 0, "result: %r" % (result,)
|
||||
assert result['msg'] == 'throwaway msg', "result: %r" % result
|
||||
|
||||
@pytest.mark.parametrize('patch_ansible_module', [_combined_options()], indirect=True)
|
||||
@pytest.mark.parametrize('exc', [HashiVaultValueError('throwaway msg'), NotImplementedError('throwaway msg')])
|
||||
def test_vault_write_auth_validation_error(self, authenticator, exc, capfd):
|
||||
authenticator.validate.side_effect = exc
|
||||
|
||||
with pytest.raises(SystemExit) as e:
|
||||
vault_write.main()
|
||||
|
||||
out, err = capfd.readouterr()
|
||||
result = json.loads(out)
|
||||
|
||||
assert e.value.code != 0, "result: %r" % (result,)
|
||||
assert result['msg'] == 'throwaway msg'
|
||||
|
||||
@pytest.mark.parametrize('opt_data', [{}, {'thing': 'one', 'thang': 'two'}])
|
||||
@pytest.mark.parametrize('opt_wrap_ttl', [None, '5m'])
|
||||
@pytest.mark.parametrize('patch_ansible_module', [[_combined_options(), 'data', 'wrap_ttl']], indirect=True)
|
||||
def test_vault_write_return_data(self, patch_ansible_module, approle_secret_id_write_response, vault_client, opt_wrap_ttl, opt_data, capfd):
|
||||
client = vault_client
|
||||
client.write_data.return_value = approle_secret_id_write_response
|
||||
|
||||
with pytest.raises(SystemExit) as e:
|
||||
vault_write.main()
|
||||
|
||||
out, err = capfd.readouterr()
|
||||
result = json.loads(out)
|
||||
|
||||
assert e.value.code == 0, "result: %r" % (result,)
|
||||
|
||||
client.write_data.assert_called_once_with(path=patch_ansible_module['path'], wrap_ttl=opt_wrap_ttl, data=opt_data)
|
||||
|
||||
assert result['data'] == approle_secret_id_write_response, (
|
||||
"module result did not match expected result:\nmodule: %r\nexpected: %r" % (result['data'], approle_secret_id_write_response)
|
||||
)
|
||||
|
||||
@pytest.mark.parametrize('patch_ansible_module', [_combined_options()], indirect=True)
|
||||
def test_vault_write_empty_response(self, vault_client, requests_unparseable_response, capfd):
|
||||
client = vault_client
|
||||
|
||||
requests_unparseable_response.status_code = 204
|
||||
|
||||
client.write_data.return_value = requests_unparseable_response
|
||||
|
||||
with pytest.raises(SystemExit) as e:
|
||||
vault_write.main()
|
||||
|
||||
out, err = capfd.readouterr()
|
||||
result = json.loads(out)
|
||||
|
||||
assert e.value.code == 0, "result: %r" % (result,)
|
||||
|
||||
assert result['data'] == {}
|
||||
|
||||
@pytest.mark.parametrize('patch_ansible_module', [_combined_options()], indirect=True)
|
||||
def test_vault_write_unparseable_response(self, vault_client, requests_unparseable_response, module_warn, capfd):
|
||||
client = vault_client
|
||||
|
||||
requests_unparseable_response.status_code = 200
|
||||
requests_unparseable_response.content = '﷽'
|
||||
|
||||
client.write_data.return_value = requests_unparseable_response
|
||||
|
||||
with pytest.raises(SystemExit) as e:
|
||||
vault_write.main()
|
||||
|
||||
out, err = capfd.readouterr()
|
||||
result = json.loads(out)
|
||||
|
||||
assert e.value.code == 0, "result: %r" % (result,)
|
||||
assert result['data'] == '﷽'
|
||||
|
||||
module_warn.assert_called_once_with('Vault returned status code 200 and an unparsable body.')
|
||||
|
||||
@pytest.mark.parametrize('patch_ansible_module', [_combined_options()], indirect=True)
|
||||
def test_vault_write_no_hvac(self, capfd):
|
||||
with mock.patch.multiple(vault_write, HAS_HVAC=False, HVAC_IMPORT_ERROR=None, create=True):
|
||||
with pytest.raises(SystemExit) as e:
|
||||
vault_write.main()
|
||||
|
||||
out, err = capfd.readouterr()
|
||||
result = json.loads(out)
|
||||
|
||||
assert e.value.code != 0, "result: %r" % (result,)
|
||||
assert result['msg'] == missing_required_lib('hvac')
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
'exc',
|
||||
[
|
||||
(hvac.exceptions.Forbidden, r'^Forbidden: Permission Denied to path'),
|
||||
(hvac.exceptions.InvalidPath, r"^The path '[^']+' doesn't seem to exist"),
|
||||
(hvac.exceptions.InternalServerError, r'^Internal Server Error:'),
|
||||
]
|
||||
)
|
||||
@pytest.mark.parametrize('patch_ansible_module', [_combined_options()], indirect=True)
|
||||
def test_vault_write_vault_exception(self, vault_client, exc, capfd):
|
||||
|
||||
client = vault_client
|
||||
client.write_data.side_effect = exc[0]
|
||||
|
||||
with pytest.raises(SystemExit) as e:
|
||||
vault_write.main()
|
||||
|
||||
out, err = capfd.readouterr()
|
||||
result = json.loads(out)
|
||||
|
||||
assert e.value.code != 0, "result: %r" % (result,)
|
||||
assert re.search(exc[1], result['msg']) is not None
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
'opt_data',
|
||||
[
|
||||
{"path": 'path_value'},
|
||||
{"wrap_ttl": 'wrap_ttl_value'},
|
||||
{"path": 'data_value', "wrap_ttl": 'write_ttl_value'},
|
||||
],
|
||||
)
|
||||
@pytest.mark.parametrize('patch_ansible_module', [[_combined_options(), 'data']], indirect=True)
|
||||
def test_vault_write_data_fallback_bad_params(self, vault_client, opt_data, capfd):
|
||||
client = vault_client
|
||||
client.mock_add_spec(['write'])
|
||||
|
||||
with pytest.raises(SystemExit) as e:
|
||||
vault_write.main()
|
||||
|
||||
out, err = capfd.readouterr()
|
||||
result = json.loads(out)
|
||||
|
||||
assert e.value.code != 0, "result: %r" % (result,)
|
||||
assert re.search(r"To use 'path' or 'wrap_ttl' as data keys, use hvac >= 1\.2", result['msg']) is not None
|
||||
|
||||
client.write.assert_not_called()
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
'opt_data',
|
||||
[
|
||||
{"item1": 'item1_value'},
|
||||
{"item2": 'item2_value'},
|
||||
{"item1": 'item1_value', "item2": 'item2_value'},
|
||||
],
|
||||
)
|
||||
@pytest.mark.parametrize('patch_ansible_module', [[_combined_options(), 'data']], indirect=True)
|
||||
def test_vault_write_data_fallback_write(self, vault_client, opt_data, patch_ansible_module, approle_secret_id_write_response, capfd):
|
||||
client = vault_client
|
||||
client.mock_add_spec(['write'])
|
||||
client.write.return_value = approle_secret_id_write_response
|
||||
|
||||
with pytest.raises(SystemExit) as e:
|
||||
vault_write.main()
|
||||
|
||||
out, err = capfd.readouterr()
|
||||
result = json.loads(out)
|
||||
|
||||
assert e.value.code == 0, "result: %r" % (result,)
|
||||
|
||||
client.write.assert_called_once_with(path=patch_ansible_module['path'], wrap_ttl=None, **opt_data)
|
||||
+89
@@ -0,0 +1,89 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright (c) 2021 Brian Scholer (@briantist)
|
||||
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
from __future__ import (absolute_import, division, print_function)
|
||||
__metaclass__ = type
|
||||
|
||||
import pytest
|
||||
|
||||
from re import escape as re_escape
|
||||
|
||||
from ansible.errors import AnsibleError, AnsibleOptionsError
|
||||
from ansible.plugins.lookup import LookupBase
|
||||
|
||||
from ......plugins.plugin_utils._hashi_vault_plugin import HashiVaultPlugin
|
||||
from ......plugins.plugin_utils._hashi_vault_lookup_base import HashiVaultLookupBase
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def hashi_vault_lookup_module():
|
||||
return FakeLookupModule()
|
||||
|
||||
|
||||
class FakeLookupModule(HashiVaultLookupBase):
|
||||
def run(self, terms, variables=None, **kwargs):
|
||||
pass
|
||||
|
||||
|
||||
class TestHashiVaultLookupBase(object):
|
||||
|
||||
def test_is_hashi_vault_plugin(self, hashi_vault_lookup_module):
|
||||
assert issubclass(type(hashi_vault_lookup_module), HashiVaultPlugin)
|
||||
|
||||
def test_is_ansible_lookup_base(self, hashi_vault_lookup_module):
|
||||
assert issubclass(type(hashi_vault_lookup_module), LookupBase)
|
||||
hashi_vault_lookup_module.run([]) # run this for "coverage"
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
'term,unqualified',
|
||||
[
|
||||
('value1 key2=value2 key3=val_w/=in_it key4=value4', 'key1'),
|
||||
('key1=value1 key2=value2 key3=val_w/=in_it key4=value4', None),
|
||||
('key1=value1 key2=value2 key3=val_w/=in_it key4=value4', 'NotReal'),
|
||||
]
|
||||
)
|
||||
def test_parse_kev_term_success(self, term, unqualified, hashi_vault_lookup_module):
|
||||
EXPECTED = {
|
||||
'key1': 'value1',
|
||||
'key2': 'value2',
|
||||
'key3': 'val_w/=in_it',
|
||||
'key4': 'value4',
|
||||
}
|
||||
parsed = hashi_vault_lookup_module.parse_kev_term(term, plugin_name='fake', first_unqualified=unqualified)
|
||||
|
||||
assert parsed == EXPECTED
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
'term,unqualified',
|
||||
[
|
||||
('value1 key2=value2 key3=val_w/=in_it key4=value4', None),
|
||||
('key1=value1 value2 key3=val_w/=in_it key4=value4', None),
|
||||
('key1=value1 value2 key3=val_w/=in_it key4=value4', 'key2'),
|
||||
('key1=val1 invalid key3=val3', None),
|
||||
('key1=val1 invalid key3=val3', 'key1'),
|
||||
]
|
||||
)
|
||||
def test_parse_kev_term_invalid_term_strings(self, term, unqualified, hashi_vault_lookup_module):
|
||||
with pytest.raises(AnsibleError):
|
||||
parsed = hashi_vault_lookup_module.parse_kev_term(term, plugin_name='fake', first_unqualified=unqualified)
|
||||
|
||||
def test_parse_kev_term_plugin_name_required(self, hashi_vault_lookup_module):
|
||||
with pytest.raises(TypeError):
|
||||
parsed = hashi_vault_lookup_module.parse_kev_term('key1=value1', first_unqualified='fake')
|
||||
|
||||
@pytest.mark.parametrize('term', [
|
||||
'one secret=two a=1 b=2',
|
||||
'a=1 secret=one b=2 secret=two',
|
||||
'secret=one secret=two a=z b=y',
|
||||
])
|
||||
def test_parse_kev_term_duplicate_option(self, term, hashi_vault_lookup_module):
|
||||
dup_key = 'secret'
|
||||
expected_template = "Duplicate key '%s' in the term string '%s'."
|
||||
expected_msg = expected_template % (dup_key, term)
|
||||
expected_re = re_escape(expected_msg)
|
||||
expected_match = "^%s$" % (expected_re,)
|
||||
|
||||
with pytest.raises(AnsibleOptionsError, match=expected_match):
|
||||
hashi_vault_lookup_module.parse_kev_term(term, plugin_name='fake', first_unqualified=dup_key)
|
||||
+32
@@ -0,0 +1,32 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright (c) 2021 Brian Scholer (@briantist)
|
||||
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
from __future__ import (absolute_import, division, print_function)
|
||||
__metaclass__ = type
|
||||
|
||||
import pytest
|
||||
|
||||
from ansible.plugins import AnsiblePlugin
|
||||
|
||||
from ansible_collections.community.hashi_vault.plugins.plugin_utils._hashi_vault_plugin import HashiVaultPlugin
|
||||
from ansible_collections.community.hashi_vault.plugins.module_utils._hashi_vault_common import HashiVaultOptionAdapter
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def hashi_vault_plugin():
|
||||
return HashiVaultPlugin()
|
||||
|
||||
|
||||
class TestHashiVaultPlugin(object):
|
||||
|
||||
def test_is_ansible_plugin(self, hashi_vault_plugin):
|
||||
assert issubclass(type(hashi_vault_plugin), AnsiblePlugin)
|
||||
|
||||
def test_has_option_adapter(self, hashi_vault_plugin):
|
||||
assert hasattr(hashi_vault_plugin, '_options_adapter') and issubclass(type(hashi_vault_plugin._options_adapter), HashiVaultOptionAdapter)
|
||||
|
||||
# TODO: remove when deprecate() is no longer needed
|
||||
def test_has_process_deprecations(self, hashi_vault_plugin):
|
||||
assert hasattr(hashi_vault_plugin, 'process_deprecations') and callable(hashi_vault_plugin.process_deprecations)
|
||||
+67
@@ -0,0 +1,67 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright (c) 2021 Brian Scholer (@briantist)
|
||||
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
from __future__ import (absolute_import, division, print_function)
|
||||
__metaclass__ = type
|
||||
|
||||
# this file must define the "adapter" fixture at a minimum,
|
||||
# and anything else that it needs or depends on that isn't already defined in in the test files themselves.
|
||||
|
||||
# Keep in mind that this one is for plugin_utils and so it can depend on
|
||||
# or import controller-side code, however it will only be run against python versions
|
||||
# that are supported on the controller.
|
||||
|
||||
import pytest
|
||||
|
||||
from packaging import version
|
||||
from ansible.release import __version__ as ansible_version
|
||||
from ansible.plugins import AnsiblePlugin
|
||||
|
||||
from ......plugins.module_utils._hashi_vault_common import HashiVaultOptionAdapter
|
||||
|
||||
|
||||
ver = version.parse(ansible_version)
|
||||
cutoff = version.parse('2.17')
|
||||
option_adapter_from_plugin_marks = pytest.mark.option_adapter_raise_on_missing if ver.release >= cutoff.release else []
|
||||
# https://github.com/ansible-collections/community.hashi_vault/issues/417
|
||||
|
||||
|
||||
def _generate_options(opts: dict) -> dict:
|
||||
return {k: {"type": type(v).__name__} if v is not None else {} for k, v in opts.items()}
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def ansible_plugin(sample_dict):
|
||||
optdef = _generate_options(opts=sample_dict)
|
||||
|
||||
class LookupModule(AnsiblePlugin):
|
||||
_load_name = 'community.hashi_vault.fake'
|
||||
|
||||
import ansible.constants as C
|
||||
C.config.initialize_plugin_configuration_definitions("lookup", LookupModule._load_name, optdef)
|
||||
plugin = LookupModule()
|
||||
plugin._options = sample_dict
|
||||
return plugin
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def adapter_from_ansible_plugin(ansible_plugin):
|
||||
def _create_adapter_from_ansible_plugin():
|
||||
return HashiVaultOptionAdapter.from_ansible_plugin(ansible_plugin)
|
||||
|
||||
return _create_adapter_from_ansible_plugin
|
||||
|
||||
|
||||
@pytest.fixture(params=[
|
||||
'dict',
|
||||
'dict_defaults',
|
||||
pytest.param('ansible_plugin', marks=option_adapter_from_plugin_marks),
|
||||
])
|
||||
def adapter(request, adapter_from_dict, adapter_from_dict_defaults, adapter_from_ansible_plugin):
|
||||
return {
|
||||
'dict': adapter_from_dict,
|
||||
'dict_defaults': adapter_from_dict_defaults,
|
||||
'ansible_plugin': adapter_from_ansible_plugin,
|
||||
}[request.param]()
|
||||
+15
@@ -0,0 +1,15 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright (c) 2021 Brian Scholer (@briantist)
|
||||
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
from __future__ import (absolute_import, division, print_function)
|
||||
__metaclass__ = type
|
||||
|
||||
# this file is here just to run the exact same tests as written in the imported file, with the main difference
|
||||
# being the fixtures defined in conftest.py (this version can run tests that rely on controller-side code)
|
||||
# and the supported python versions being different.
|
||||
# So we really do want to import * and so we disable lint failure on wildcard imports.
|
||||
#
|
||||
# pylint: disable=wildcard-import,unused-wildcard-import
|
||||
from ...module_utils.option_adapter.test_hashi_vault_option_adapter import *
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
hvac
|
||||
urllib3
|
||||
azure-identity
|
||||
Reference in New Issue
Block a user