318 lines
11 KiB
Python
318 lines
11 KiB
Python
# (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
|