Adding in Ansible (still a WIP).

This commit is contained in:
2025-01-16 16:12:35 -05:00
parent 5495f51326
commit 7298674536
4270 changed files with 606627 additions and 0 deletions
@@ -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__'
@@ -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')
@@ -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
@@ -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'
@@ -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
}
@@ -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
}
@@ -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
}
}
@@ -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
}
}
@@ -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
}
@@ -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
}
@@ -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
}
@@ -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
}
@@ -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
}
@@ -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
}
}
@@ -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
}
@@ -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
}
@@ -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
}
@@ -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
}
@@ -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
}
@@ -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
}
@@ -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
}
}
@@ -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
}
}
@@ -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
}
}
@@ -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
}
@@ -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
}
@@ -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
}
@@ -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
}
@@ -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
}
@@ -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)
@@ -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',
}
@@ -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))
@@ -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')
@@ -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
@@ -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
@@ -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)
@@ -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.")
@@ -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)
@@ -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)
@@ -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)
@@ -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
@@ -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
@@ -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()
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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()
@@ -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
@@ -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)
@@ -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))
@@ -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]()
@@ -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
@@ -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()
@@ -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
@@ -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)
@@ -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
@@ -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
@@ -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)
@@ -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)
@@ -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)
@@ -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)
@@ -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)
@@ -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)
@@ -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)
@@ -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)
@@ -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)
@@ -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)
@@ -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)
@@ -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)
@@ -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)
@@ -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)
@@ -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
@@ -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])
@@ -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
@@ -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,)
@@ -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)
@@ -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')
@@ -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
@@ -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)
@@ -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'
@@ -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)
@@ -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)
@@ -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)
@@ -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]()
@@ -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 *
@@ -0,0 +1,3 @@
hvac
urllib3
azure-identity