mirror of
https://github.com/ansible-collections/community.general.git
synced 2024-09-14 20:13:21 +02:00
conjur_variable: redirecting to correct collection (#570)
* - Redirecting to correct collection - Removing the plugin and adding changelog and deprecation * Making suggested changes * Earlier version on leftovers * Update changelogs/fragments/cyberarkconjur-removal.yml Co-authored-by: Felix Fontein <felix@fontein.de>
This commit is contained in:
parent
d2ee51253d
commit
cca84abeb5
4 changed files with 8 additions and 269 deletions
2
changelogs/fragments/cyberarkconjur-removal.yml
Normal file
2
changelogs/fragments/cyberarkconjur-removal.yml
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
removed_features:
|
||||||
|
- "conjur_variable lookup - has been moved to the ``cyberark.conjur`` collection. A redirection is active, which will be removed in version 2.0.0 (https://github.com/ansible-collections/community.general/pull/570)."
|
|
@ -57,6 +57,12 @@ action_groups:
|
||||||
- ovirt_vm_facts
|
- ovirt_vm_facts
|
||||||
- ovirt_vmpool_facts
|
- ovirt_vmpool_facts
|
||||||
plugin_routing:
|
plugin_routing:
|
||||||
|
lookup:
|
||||||
|
conjur_variable:
|
||||||
|
redirect: cyberark.conjur.conjur_variable
|
||||||
|
deprecation:
|
||||||
|
removal_version: 2.0.0
|
||||||
|
warning_text: The conjur_variable lookup has been moved to the cyberark.conjur collection.
|
||||||
modules:
|
modules:
|
||||||
ali_instance_facts:
|
ali_instance_facts:
|
||||||
deprecation:
|
deprecation:
|
||||||
|
|
|
@ -1,159 +0,0 @@
|
||||||
# (c) 2018, Jason Vanderhoof <jason.vanderhoof@cyberark.com>, Oren Ben Meir <oren.benmeir@cyberark.com>
|
|
||||||
# (c) 2018 Ansible Project
|
|
||||||
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
|
|
||||||
from __future__ import (absolute_import, division, print_function)
|
|
||||||
__metaclass__ = type
|
|
||||||
|
|
||||||
DOCUMENTATION = '''
|
|
||||||
lookup: conjur_variable
|
|
||||||
short_description: Fetch credentials from CyberArk Conjur.
|
|
||||||
description:
|
|
||||||
- "Retrieves credentials from Conjur using the controlling host's Conjur identity. Conjur info: U(https://www.conjur.org/)."
|
|
||||||
requirements:
|
|
||||||
- 'The controlling host running Ansible has a Conjur identity.
|
|
||||||
(More: U(https://docs.conjur.org/Latest/en/Content/Get%20Started/key_concepts/machine_identity.html))'
|
|
||||||
options:
|
|
||||||
_term:
|
|
||||||
description: Variable path
|
|
||||||
required: True
|
|
||||||
identity_file:
|
|
||||||
description: Path to the Conjur identity file. The identity file follows the netrc file format convention.
|
|
||||||
type: path
|
|
||||||
default: /etc/conjur.identity
|
|
||||||
required: False
|
|
||||||
ini:
|
|
||||||
- section: conjur,
|
|
||||||
key: identity_file_path
|
|
||||||
env:
|
|
||||||
- name: CONJUR_IDENTITY_FILE
|
|
||||||
config_file:
|
|
||||||
description: Path to the Conjur configuration file. The configuration file is a YAML file.
|
|
||||||
type: path
|
|
||||||
default: /etc/conjur.conf
|
|
||||||
required: False
|
|
||||||
ini:
|
|
||||||
- section: conjur,
|
|
||||||
key: config_file_path
|
|
||||||
env:
|
|
||||||
- name: CONJUR_CONFIG_FILE
|
|
||||||
'''
|
|
||||||
|
|
||||||
EXAMPLES = """
|
|
||||||
- debug:
|
|
||||||
msg: "{{ lookup('conjur_variable', '/path/to/secret') }}"
|
|
||||||
"""
|
|
||||||
|
|
||||||
RETURN = """
|
|
||||||
_raw:
|
|
||||||
description:
|
|
||||||
- Value stored in Conjur.
|
|
||||||
"""
|
|
||||||
|
|
||||||
import os.path
|
|
||||||
from ansible.errors import AnsibleError
|
|
||||||
from ansible.plugins.lookup import LookupBase
|
|
||||||
from base64 import b64encode
|
|
||||||
from netrc import netrc
|
|
||||||
from os import environ
|
|
||||||
from time import time
|
|
||||||
from ansible.module_utils.six.moves.urllib.parse import quote_plus
|
|
||||||
import yaml
|
|
||||||
|
|
||||||
from ansible.module_utils.urls import open_url
|
|
||||||
from ansible.utils.display import Display
|
|
||||||
|
|
||||||
display = Display()
|
|
||||||
|
|
||||||
|
|
||||||
# Load configuration and return as dictionary if file is present on file system
|
|
||||||
def _load_conf_from_file(conf_path):
|
|
||||||
display.vvv('conf file: {0}'.format(conf_path))
|
|
||||||
|
|
||||||
if not os.path.exists(conf_path):
|
|
||||||
raise AnsibleError('Conjur configuration file `{0}` was not found on the controlling host'
|
|
||||||
.format(conf_path))
|
|
||||||
|
|
||||||
display.vvvv('Loading configuration from: {0}'.format(conf_path))
|
|
||||||
with open(conf_path) as f:
|
|
||||||
config = yaml.safe_load(f.read())
|
|
||||||
if 'account' not in config or 'appliance_url' not in config:
|
|
||||||
raise AnsibleError('{0} on the controlling host must contain an `account` and `appliance_url` entry'
|
|
||||||
.format(conf_path))
|
|
||||||
return config
|
|
||||||
|
|
||||||
|
|
||||||
# Load identity and return as dictionary if file is present on file system
|
|
||||||
def _load_identity_from_file(identity_path, appliance_url):
|
|
||||||
display.vvvv('identity file: {0}'.format(identity_path))
|
|
||||||
|
|
||||||
if not os.path.exists(identity_path):
|
|
||||||
raise AnsibleError('Conjur identity file `{0}` was not found on the controlling host'
|
|
||||||
.format(identity_path))
|
|
||||||
|
|
||||||
display.vvvv('Loading identity from: {0} for {1}'.format(identity_path, appliance_url))
|
|
||||||
|
|
||||||
conjur_authn_url = '{0}/authn'.format(appliance_url)
|
|
||||||
identity = netrc(identity_path)
|
|
||||||
|
|
||||||
if identity.authenticators(conjur_authn_url) is None:
|
|
||||||
raise AnsibleError('The netrc file on the controlling host does not contain an entry for: {0}'
|
|
||||||
.format(conjur_authn_url))
|
|
||||||
|
|
||||||
id, account, api_key = identity.authenticators(conjur_authn_url)
|
|
||||||
if not id or not api_key:
|
|
||||||
raise AnsibleError('{0} on the controlling host must contain a `login` and `password` entry for {1}'
|
|
||||||
.format(identity_path, appliance_url))
|
|
||||||
|
|
||||||
return {'id': id, 'api_key': api_key}
|
|
||||||
|
|
||||||
|
|
||||||
# Use credentials to retrieve temporary authorization token
|
|
||||||
def _fetch_conjur_token(conjur_url, account, username, api_key):
|
|
||||||
conjur_url = '{0}/authn/{1}/{2}/authenticate'.format(conjur_url, account, username)
|
|
||||||
display.vvvv('Authentication request to Conjur at: {0}, with user: {1}'.format(conjur_url, username))
|
|
||||||
|
|
||||||
response = open_url(conjur_url, data=api_key, method='POST')
|
|
||||||
code = response.getcode()
|
|
||||||
if code != 200:
|
|
||||||
raise AnsibleError('Failed to authenticate as \'{0}\' (got {1} response)'
|
|
||||||
.format(username, code))
|
|
||||||
|
|
||||||
return response.read()
|
|
||||||
|
|
||||||
|
|
||||||
# Retrieve Conjur variable using the temporary token
|
|
||||||
def _fetch_conjur_variable(conjur_variable, token, conjur_url, account):
|
|
||||||
token = b64encode(token)
|
|
||||||
headers = {'Authorization': 'Token token="{0}"'.format(token)}
|
|
||||||
display.vvvv('Header: {0}'.format(headers))
|
|
||||||
|
|
||||||
url = '{0}/secrets/{1}/variable/{2}'.format(conjur_url, account, quote_plus(conjur_variable))
|
|
||||||
display.vvvv('Conjur Variable URL: {0}'.format(url))
|
|
||||||
|
|
||||||
response = open_url(url, headers=headers, method='GET')
|
|
||||||
|
|
||||||
if response.getcode() == 200:
|
|
||||||
display.vvvv('Conjur variable {0} was successfully retrieved'.format(conjur_variable))
|
|
||||||
return [response.read()]
|
|
||||||
if response.getcode() == 401:
|
|
||||||
raise AnsibleError('Conjur request has invalid authorization credentials')
|
|
||||||
if response.getcode() == 403:
|
|
||||||
raise AnsibleError('The controlling host\'s Conjur identity does not have authorization to retrieve {0}'
|
|
||||||
.format(conjur_variable))
|
|
||||||
if response.getcode() == 404:
|
|
||||||
raise AnsibleError('The variable {0} does not exist'.format(conjur_variable))
|
|
||||||
|
|
||||||
return {}
|
|
||||||
|
|
||||||
|
|
||||||
class LookupModule(LookupBase):
|
|
||||||
|
|
||||||
def run(self, terms, variables=None, **kwargs):
|
|
||||||
conf_file = self.get_option('config_file')
|
|
||||||
conf = _load_conf_from_file(conf_file)
|
|
||||||
|
|
||||||
identity_file = self.get_option('identity_file')
|
|
||||||
identity = _load_identity_from_file(identity_file, conf['appliance_url'])
|
|
||||||
|
|
||||||
token = _fetch_conjur_token(conf['appliance_url'], conf['account'], identity['id'], identity['api_key'])
|
|
||||||
return _fetch_conjur_variable(terms[0], token, conf['appliance_url'], conf['account'])
|
|
|
@ -1,110 +0,0 @@
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
# (c) 2018, Jason Vanderhoof <jason.vanderhoof@cyberark.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
|
|
||||||
|
|
||||||
|
|
||||||
import pytest
|
|
||||||
from ansible_collections.community.general.tests.unit.compat.mock import MagicMock
|
|
||||||
from ansible.errors import AnsibleError
|
|
||||||
from ansible.module_utils.six.moves import http_client
|
|
||||||
from ansible_collections.community.general.plugins.lookup import conjur_variable
|
|
||||||
import tempfile
|
|
||||||
|
|
||||||
|
|
||||||
class TestLookupModule:
|
|
||||||
def test_valid_netrc_file(self):
|
|
||||||
with tempfile.NamedTemporaryFile() as temp_netrc:
|
|
||||||
temp_netrc.write(b"machine http://localhost/authn\n")
|
|
||||||
temp_netrc.write(b" login admin\n")
|
|
||||||
temp_netrc.write(b" password my-pass\n")
|
|
||||||
temp_netrc.seek(0)
|
|
||||||
|
|
||||||
results = conjur_variable._load_identity_from_file(temp_netrc.name, 'http://localhost')
|
|
||||||
|
|
||||||
assert results['id'] == 'admin'
|
|
||||||
assert results['api_key'] == 'my-pass'
|
|
||||||
|
|
||||||
def test_netrc_without_host_file(self):
|
|
||||||
with tempfile.NamedTemporaryFile() as temp_netrc:
|
|
||||||
temp_netrc.write(b"machine http://localhost/authn\n")
|
|
||||||
temp_netrc.write(b" login admin\n")
|
|
||||||
temp_netrc.write(b" password my-pass\n")
|
|
||||||
temp_netrc.seek(0)
|
|
||||||
|
|
||||||
with pytest.raises(AnsibleError):
|
|
||||||
conjur_variable._load_identity_from_file(temp_netrc.name, 'http://foo')
|
|
||||||
|
|
||||||
def test_valid_configuration(self):
|
|
||||||
with tempfile.NamedTemporaryFile() as configuration_file:
|
|
||||||
configuration_file.write(b"---\n")
|
|
||||||
configuration_file.write(b"account: demo-policy\n")
|
|
||||||
configuration_file.write(b"plugins: []\n")
|
|
||||||
configuration_file.write(b"appliance_url: http://localhost:8080\n")
|
|
||||||
configuration_file.seek(0)
|
|
||||||
|
|
||||||
results = conjur_variable._load_conf_from_file(configuration_file.name)
|
|
||||||
assert results['account'] == 'demo-policy'
|
|
||||||
assert results['appliance_url'] == 'http://localhost:8080'
|
|
||||||
|
|
||||||
def test_valid_token_retrieval(self, mocker):
|
|
||||||
mock_response = MagicMock(spec_set=http_client.HTTPResponse)
|
|
||||||
try:
|
|
||||||
mock_response.getcode.return_value = 200
|
|
||||||
except Exception:
|
|
||||||
# HTTPResponse is a Python 3 only feature. This uses a generic mock for python 2.6
|
|
||||||
mock_response = MagicMock()
|
|
||||||
mock_response.getcode.return_value = 200
|
|
||||||
|
|
||||||
mock_response.read.return_value = 'foo-bar-token'
|
|
||||||
mocker.patch.object(conjur_variable, 'open_url', return_value=mock_response)
|
|
||||||
|
|
||||||
response = conjur_variable._fetch_conjur_token('http://conjur', 'account', 'username', 'api_key')
|
|
||||||
assert response == 'foo-bar-token'
|
|
||||||
|
|
||||||
def test_valid_fetch_conjur_variable(self, mocker):
|
|
||||||
mock_response = MagicMock(spec_set=http_client.HTTPResponse)
|
|
||||||
try:
|
|
||||||
mock_response.getcode.return_value = 200
|
|
||||||
except Exception:
|
|
||||||
# HTTPResponse is a Python 3 only feature. This uses a generic mock for python 2.6
|
|
||||||
mock_response = MagicMock()
|
|
||||||
mock_response.getcode.return_value = 200
|
|
||||||
|
|
||||||
mock_response.read.return_value = 'foo-bar'
|
|
||||||
mocker.patch.object(conjur_variable, 'open_url', return_value=mock_response)
|
|
||||||
|
|
||||||
response = conjur_variable._fetch_conjur_token('super-secret', 'token', 'http://conjur', 'account')
|
|
||||||
assert response == 'foo-bar'
|
|
||||||
|
|
||||||
def test_invalid_fetch_conjur_variable(self, mocker):
|
|
||||||
for code in [401, 403, 404]:
|
|
||||||
mock_response = MagicMock(spec_set=http_client.HTTPResponse)
|
|
||||||
try:
|
|
||||||
mock_response.getcode.return_value = code
|
|
||||||
except Exception:
|
|
||||||
# HTTPResponse is a Python 3 only feature. This uses a generic mock for python 2.6
|
|
||||||
mock_response = MagicMock()
|
|
||||||
mock_response.getcode.return_value = code
|
|
||||||
|
|
||||||
mocker.patch.object(conjur_variable, 'open_url', return_value=mock_response)
|
|
||||||
|
|
||||||
with pytest.raises(AnsibleError):
|
|
||||||
response = conjur_variable._fetch_conjur_token('super-secret', 'token', 'http://conjur', 'account')
|
|
Loading…
Reference in a new issue