diff --git a/CHANGELOG.md b/CHANGELOG.md index 48a9c4bfd5..964061c986 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ This is a list of high-level changes for each release of AWX. A full list of com - Removed support for HipChat notifications ([EoL announcement](https://www.atlassian.com/partnerships/slack/faq#faq-98b17ca3-247f-423b-9a78-70a91681eff0)); all previously-created HipChat notification templates will be deleted due to this removal. - Fixed a bug which broke AWX installations with oc version 4.3 (https://github.com/ansible/awx/pull/6948/files) - Fixed a performance issue that caused notable delay of stdout processing for playbooks run against large numbers of hosts (https://github.com/ansible/awx/issues/6991) +- Fixed a bug that caused CyberArk AIM credential plugin looks to hang forever in some environments (https://github.com/ansible/awx/issues/6986) - Fixed a bug that caused ANY/ALL converage settings not to properly save when editing approval nodes in the UI (https://github.com/ansible/awx/issues/6998) - Fixed a bug that broke support for the satellite6_group_prefix source variable (https://github.com/ansible/awx/issues/7031) - Fixed a bug in awx-manage run_wsbroadcast --status in kubernetes (https://github.com/ansible/awx/pull/7009) diff --git a/awx/main/credential_plugins/aim.py b/awx/main/credential_plugins/aim.py index c75d4d85aa..23036efda1 100644 --- a/awx/main/credential_plugins/aim.py +++ b/awx/main/credential_plugins/aim.py @@ -1,15 +1,10 @@ -from .plugin import CredentialPlugin +from .plugin import CredentialPlugin, CertFiles from urllib.parse import quote, urlencode, urljoin from django.utils.translation import ugettext_lazy as _ import requests -# AWX -from awx.main.utils import ( - create_temporary_fifo, -) - aim_inputs = { 'fields': [{ 'id': 'url', @@ -81,21 +76,13 @@ def aim_backend(**kwargs): request_qs = '?' + urlencode(query_params, quote_via=quote) request_url = urljoin(url, '/'.join(['AIMWebService', 'api', 'Accounts'])) - cert = None - if client_cert and client_key: - cert = ( - create_temporary_fifo(client_cert.encode()), - create_temporary_fifo(client_key.encode()) + with CertFiles(client_cert, client_key) as cert: + res = requests.get( + request_url + request_qs, + timeout=30, + cert=cert, + verify=verify, ) - elif client_cert: - cert = create_temporary_fifo(client_cert.encode()) - - res = requests.get( - request_url + request_qs, - timeout=30, - cert=cert, - verify=verify, - ) res.raise_for_status() return res.json()['Content'] diff --git a/awx/main/credential_plugins/conjur.py b/awx/main/credential_plugins/conjur.py index 55fd2e60f2..a82b91893b 100644 --- a/awx/main/credential_plugins/conjur.py +++ b/awx/main/credential_plugins/conjur.py @@ -1,4 +1,4 @@ -from .plugin import CredentialPlugin +from .plugin import CredentialPlugin, CertFiles import base64 from urllib.parse import urljoin, quote_plus @@ -6,11 +6,6 @@ from urllib.parse import urljoin, quote_plus from django.utils.translation import ugettext_lazy as _ import requests -# AWX -from awx.main.utils import ( - create_temporary_fifo, -) - conjur_inputs = { 'fields': [{ @@ -65,22 +60,20 @@ def conjur_backend(**kwargs): 'headers': {'Content-Type': 'text/plain'}, 'data': api_key } - if cacert: - auth_kwargs['verify'] = create_temporary_fifo(cacert.encode()) - # https://www.conjur.org/api.html#authentication-authenticate-post - resp = requests.post( - urljoin(url, '/'.join(['authn', account, username, 'authenticate'])), - **auth_kwargs - ) + with CertFiles(cacert) as cert: + # https://www.conjur.org/api.html#authentication-authenticate-post + auth_kwargs['verify'] = cert + resp = requests.post( + urljoin(url, '/'.join(['authn', account, username, 'authenticate'])), + **auth_kwargs + ) resp.raise_for_status() token = base64.b64encode(resp.content).decode('utf-8') lookup_kwargs = { 'headers': {'Authorization': 'Token token="{}"'.format(token)}, } - if cacert: - lookup_kwargs['verify'] = create_temporary_fifo(cacert.encode()) # https://www.conjur.org/api.html#secrets-retrieve-a-secret-get path = urljoin(url, '/'.join([ @@ -92,7 +85,9 @@ def conjur_backend(**kwargs): if version: path = '?'.join([path, version]) - resp = requests.get(path, timeout=30, **lookup_kwargs) + with CertFiles(cacert) as cert: + lookup_kwargs['verify'] = cert + resp = requests.get(path, timeout=30, **lookup_kwargs) resp.raise_for_status() return resp.text diff --git a/awx/main/credential_plugins/hashivault.py b/awx/main/credential_plugins/hashivault.py index a4bae17ac3..6e033efb43 100644 --- a/awx/main/credential_plugins/hashivault.py +++ b/awx/main/credential_plugins/hashivault.py @@ -3,16 +3,11 @@ import os import pathlib from urllib.parse import urljoin -from .plugin import CredentialPlugin +from .plugin import CredentialPlugin, CertFiles import requests from django.utils.translation import ugettext_lazy as _ -# AWX -from awx.main.utils import ( - create_temporary_fifo, -) - base_inputs = { 'fields': [{ 'id': 'url', @@ -129,14 +124,13 @@ def approle_auth(**kwargs): cacert = kwargs.get('cacert', None) request_kwargs = {'timeout': 30} - if cacert: - request_kwargs['verify'] = create_temporary_fifo(cacert.encode()) - # AppRole Login request_kwargs['json'] = {'role_id': role_id, 'secret_id': secret_id} sess = requests.Session() request_url = '/'.join([url, 'auth', auth_path, 'login']).rstrip('/') - resp = sess.post(request_url, **request_kwargs) + with CertFiles(cacert) as cert: + request_kwargs['verify'] = cert + resp = sess.post(request_url, **request_kwargs) resp.raise_for_status() token = resp.json()['auth']['client_token'] return token @@ -152,8 +146,6 @@ def kv_backend(**kwargs): api_version = kwargs['api_version'] request_kwargs = {'timeout': 30} - if cacert: - request_kwargs['verify'] = create_temporary_fifo(cacert.encode()) sess = requests.Session() sess.headers['Authorization'] = 'Bearer {}'.format(token) @@ -180,7 +172,9 @@ def kv_backend(**kwargs): path_segments = [secret_path] request_url = urljoin(url, '/'.join(['v1'] + path_segments)).rstrip('/') - response = sess.get(request_url, **request_kwargs) + with CertFiles(cacert) as cert: + request_kwargs['verify'] = cert + response = sess.get(request_url, **request_kwargs) response.raise_for_status() json = response.json() @@ -205,8 +199,6 @@ def ssh_backend(**kwargs): cacert = kwargs.get('cacert', None) request_kwargs = {'timeout': 30} - if cacert: - request_kwargs['verify'] = create_temporary_fifo(cacert.encode()) request_kwargs['json'] = {'public_key': kwargs['public_key']} if kwargs.get('valid_principals'): @@ -218,7 +210,10 @@ def ssh_backend(**kwargs): sess.headers['X-Vault-Token'] = token # https://www.vaultproject.io/api/secret/ssh/index.html#sign-ssh-key request_url = '/'.join([url, secret_path, 'sign', role]).rstrip('/') - resp = sess.post(request_url, **request_kwargs) + + with CertFiles(cacert) as cert: + request_kwargs['verify'] = cert + resp = sess.post(request_url, **request_kwargs) resp.raise_for_status() return resp.json()['data']['signed_key'] diff --git a/awx/main/credential_plugins/plugin.py b/awx/main/credential_plugins/plugin.py index c5edde7bc1..def2676a02 100644 --- a/awx/main/credential_plugins/plugin.py +++ b/awx/main/credential_plugins/plugin.py @@ -1,3 +1,45 @@ +import os +import tempfile + from collections import namedtuple CredentialPlugin = namedtuple('CredentialPlugin', ['name', 'inputs', 'backend']) + + +class CertFiles(): + """ + A context manager used for writing a certificate and (optional) key + to $TMPDIR, and cleaning up afterwards. + + This is particularly useful as a shared resource for credential plugins + that want to pull cert/key data out of the database and persist it + temporarily to the file system so that it can loaded into the openssl + certificate chain (generally, for HTTPS requests plugins make via the + Python requests library) + + with CertFiles(cert_data, key_data) as cert: + # cert is string representing a path to the cert or pemfile + # temporarily written to disk + requests.post(..., cert=cert) + """ + + certfile = None + + def __init__(self, cert, key=None): + self.cert = cert + self.key = key + + def __enter__(self): + if not self.cert: + return None + self.certfile = tempfile.NamedTemporaryFile('wb', delete=False) + self.certfile.write(self.cert.encode()) + if self.key: + self.certfile.write(b'\n') + self.certfile.write(self.key.encode()) + self.certfile.flush() + return str(self.certfile.name) + + def __exit__(self, *args): + if self.certfile and os.path.exists(self.certfile.name): + os.remove(self.certfile.name)