Repository URL to install this package:
Version:
6.0.0 ▾
|
# (c) 2020 CyberArk Software Ltd. All rights reserved.
# (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
ANSIBLE_METADATA = {'metadata_version': '1.1',
'status': ['preview'],
'supported_by': 'community'}
DOCUMENTATION = """
lookup: conjur_variable
version_added: "2.5"
short_description: Fetch credentials from CyberArk Conjur.
author:
- CyberArk BizDev (@cyberark-bizdev)
- CyberArk Community and Integrations Team (@cyberark/community-and-integrations-team)
description:
Retrieves credentials from Conjur using the controlling host's Conjur identity
or environment variables.
Environment variables could be CONJUR_ACCOUNT, CONJUR_APPLIANCE_URL, CONJUR_CERT_FILE, CONJUR_AUTHN_LOGIN, CONJUR_AUTHN_API_KEY, CONJUR_AUTHN_TOKEN_FILE
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:
_terms:
description: Variable path
required: True
validate_certs:
description: Flag to control SSL certificate validation
type: boolean
default: True
as_file:
description: >
Store lookup result in a temporary file and returns the file path. Thus allowing it to be consumed as an ansible file parameter
(eg ansible_ssh_private_key_file).
type: boolean
default: False
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
authn_token_file:
description: Path to the access token file.
type: path
default: /var/run/conjur/access-token
required: False
ini:
- section: conjur,
key: authn_token_file
env:
- name: CONJUR_AUTHN_TOKEN_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 = """
---
- hosts: localhost
collections:
- cyberark.conjur
tasks:
- name: Lookup variable in Conjur
debug:
msg: "{{ lookup('cyberark.conjur.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
from stat import S_IRUSR, S_IWUSR
from tempfile import gettempdir, NamedTemporaryFile
import yaml
from ansible.module_utils.urls import open_url
from ansible.utils.display import Display
import ssl
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):
return {}
# 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())
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):
return {}
# 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:
return {}
return {'id': id, 'api_key': api_key}
# Merge multiple dictionaries by using dict.update mechanism
def _merge_dictionaries(*arg):
ret = {}
for item in arg:
ret.update(item)
return ret
# The `quote` method's default value for `safe` is '/' so it doesn't encode slashes
# into "%2F" which is what the Conjur server expects. Thus, we need to use this
# method with no safe characters. We can't use the method `quote_plus` (which encodes
# slashes correctly) because it encodes spaces into the character '+' instead of "%20"
# as expected by the Conjur server
def _encode_str(input_str):
return quote(input_str, safe='')
# Use credentials to retrieve temporary authorization token
def _fetch_conjur_token(conjur_url, account, username, api_key, validate_certs, cert_file):
conjur_url = '{0}/authn/{1}/{2}/authenticate'.format(conjur_url, account, _encode_str(username))
display.vvvv('Authentication request to Conjur at: {0}, with user: {1}'.format(
conjur_url,
_encode_str(username)))
response = open_url(conjur_url,
data=api_key,
method='POST',
validate_certs=validate_certs,
ca_path=cert_file)
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, validate_certs, cert_file):
token = b64encode(token)
headers = {'Authorization': 'Token token="{0}"'.format(token.decode("utf-8"))}
url = '{0}/secrets/{1}/variable/{2}'.format(conjur_url, account, _encode_str(conjur_variable))
display.vvvv('Conjur Variable URL: {0}'.format(url))
response = open_url(url,
headers=headers,
method='GET',
validate_certs=validate_certs,
ca_path=cert_file)
if response.getcode() == 200:
display.vvvv('Conjur variable {0} was successfully retrieved'.format(conjur_variable))
value = response.read().decode("utf-8")
return [value]
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 {}
def _default_tmp_path():
if os.access("/dev/shm", os.W_OK):
return "/dev/shm"
return gettempdir()
def _store_secret_in_file(value):
secrets_file = NamedTemporaryFile(mode='w', dir=_default_tmp_path(), delete=False)
os.chmod(secrets_file.name, S_IRUSR | S_IWUSR)
secrets_file.write(value[0])
return [secrets_file.name]
class LookupModule(LookupBase):
def run(self, terms, variables=None, **kwargs):
self.set_options(direct=kwargs)
validate_certs = self.get_option('validate_certs')
conf_file = self.get_option('config_file')
as_file = self.get_option('as_file')
conf = _merge_dictionaries(
_load_conf_from_file(conf_file),
{
"account": environ.get('CONJUR_ACCOUNT'),
"appliance_url": environ.get("CONJUR_APPLIANCE_URL")
} if (
environ.get('CONJUR_ACCOUNT') is not None
and environ.get('CONJUR_APPLIANCE_URL') is not None
)
else {},
{
"cert_file": environ.get('CONJUR_CERT_FILE')
} if (environ.get('CONJUR_CERT_FILE') is not None)
else {},
{
"authn_token_file": environ.get('CONJUR_AUTHN_TOKEN_FILE')
} if (environ.get('CONJUR_AUTHN_TOKEN_FILE') is not None)
else {}
)
if 'authn_token_file' not in conf:
identity_file = self.get_option('identity_file')
identity = _merge_dictionaries(
_load_identity_from_file(identity_file, conf['appliance_url']),
{
"id": environ.get('CONJUR_AUTHN_LOGIN'),
"api_key": environ.get('CONJUR_AUTHN_API_KEY')
} if (environ.get('CONJUR_AUTHN_LOGIN') is not None
and environ.get('CONJUR_AUTHN_API_KEY') is not None)
else {}
)
if 'account' not in conf or 'appliance_url' not in conf:
raise AnsibleError(
("Configuration file on the controlling host must "
"define `account` and `appliance_url`"
"entries or they should be environment variables")
)
if 'id' not in identity or 'api_key' not in identity:
raise AnsibleError(
("Identity file on the controlling host must contain "
"`login` and `password` entries for Conjur appliance"
" URL or they should be environment variables")
)
cert_file = None
if 'cert_file' in conf:
display.vvv("Using cert file path {0}".format(conf['cert_file']))
cert_file = conf['cert_file']
token = None
if 'authn_token_file' not in conf:
token = _fetch_conjur_token(
conf['appliance_url'],
conf['account'],
identity['id'],
identity['api_key'],
validate_certs,
cert_file
)
else:
if not os.path.exists(conf['authn_token_file']):
raise AnsibleError('Conjur authn token file `{0}` was not found on the host'
.format(conf['authn_token_file']))
with open(conf['authn_token_file'], 'rb') as f:
token = f.read()
conjur_variable = _fetch_conjur_variable(
terms[0],
token,
conf['appliance_url'],
conf['account'],
validate_certs,
cert_file
)
if as_file:
return _store_secret_in_file(conjur_variable)
return conjur_variable