|
|
79af3c |
From a38601968ccd8c8dfdce60c8d66b220eefb344b0 Mon Sep 17 00:00:00 2001
|
|
|
79af3c |
From: Christian Heimes <cheimes@redhat.com>
|
|
|
79af3c |
Date: Tue, 28 Mar 2017 18:41:08 +0200
|
|
|
79af3c |
Subject: [PATCH 4/4] Vendor custodia.ipa
|
|
|
79af3c |
|
|
|
79af3c |
---
|
|
|
79af3c |
README.custodia.ipa | 137 ++++++++++++++++++++++
|
|
|
79af3c |
custodia/ipa/__init__.py | 1 +
|
|
|
79af3c |
custodia/ipa/vault.py | 291 +++++++++++++++++++++++++++++++++++++++++++++++
|
|
|
79af3c |
setup.py | 8 +-
|
|
|
79af3c |
tests/test_ipa.py | 195 +++++++++++++++++++++++++++++++
|
|
|
79af3c |
6 files changed, 638 insertions(+), 2 deletions(-)
|
|
|
79af3c |
create mode 100644 README.custodia.ipa
|
|
|
79af3c |
create mode 100644 custodia/ipa/__init__.py
|
|
|
79af3c |
create mode 100644 custodia/ipa/vault.py
|
|
|
79af3c |
create mode 100644 tests/test_ipa.py
|
|
|
79af3c |
|
|
|
79af3c |
diff --git a/README.custodia.ipa b/README.custodia.ipa
|
|
|
79af3c |
new file mode 100644
|
|
|
79af3c |
index 0000000..a952ef8
|
|
|
79af3c |
--- /dev/null
|
|
|
79af3c |
+++ b/README.custodia.ipa
|
|
|
79af3c |
@@ -0,0 +1,137 @@
|
|
|
79af3c |
+.. WARNING: AUTO-GENERATED FILE. DO NOT EDIT.
|
|
|
79af3c |
+
|
|
|
79af3c |
+custodia.ipa — IPA vault plugin for Custodia
|
|
|
79af3c |
+============================================
|
|
|
79af3c |
+
|
|
|
79af3c |
+**WARNING** *custodia.ipa is a tech preview with a provisional API.*
|
|
|
79af3c |
+
|
|
|
79af3c |
+custodia.ipa is a storage plugin for
|
|
|
79af3c |
+`Custodia <https://custodia.readthedocs.io/>`__. It provides integration
|
|
|
79af3c |
+with `FreeIPA <http://www.freeipa.org>`__'s
|
|
|
79af3c |
+`vault <https://www.freeipa.org/page/V4/Password_Vault>`__ facility.
|
|
|
79af3c |
+Secrets are encrypted and stored in
|
|
|
79af3c |
+`Dogtag <http://www.dogtagpki.org>`__'s Key Recovery Agent.
|
|
|
79af3c |
+
|
|
|
79af3c |
+Requirements
|
|
|
79af3c |
+------------
|
|
|
79af3c |
+
|
|
|
79af3c |
+Installation
|
|
|
79af3c |
+~~~~~~~~~~~~
|
|
|
79af3c |
+
|
|
|
79af3c |
+- pip
|
|
|
79af3c |
+- setuptools >= 18.0
|
|
|
79af3c |
+
|
|
|
79af3c |
+Runtime
|
|
|
79af3c |
+~~~~~~~
|
|
|
79af3c |
+
|
|
|
79af3c |
+- custodia >= 0.3.1
|
|
|
79af3c |
+- ipalib >= 4.5.0
|
|
|
79af3c |
+- ipaclient >= 4.5.0
|
|
|
79af3c |
+- Python 2.7 (Python 3 support in IPA vault is unstable.)
|
|
|
79af3c |
+
|
|
|
79af3c |
+custodia.ipa requires an IPA-enrolled host and a Kerberos TGT for
|
|
|
79af3c |
+authentication. It is recommended to provide credentials with a keytab
|
|
|
79af3c |
+file or GSS-Proxy.
|
|
|
79af3c |
+
|
|
|
79af3c |
+Testing and development
|
|
|
79af3c |
+~~~~~~~~~~~~~~~~~~~~~~~
|
|
|
79af3c |
+
|
|
|
79af3c |
+- wheel
|
|
|
79af3c |
+- tox
|
|
|
79af3c |
+
|
|
|
79af3c |
+virtualenv requirements
|
|
|
79af3c |
+~~~~~~~~~~~~~~~~~~~~~~~
|
|
|
79af3c |
+
|
|
|
79af3c |
+custodia.ipa depends on several binary extensions and shared libraries
|
|
|
79af3c |
+for e.g. python-cryptography, python-gssapi, python-ldap, and
|
|
|
79af3c |
+python-nss. For installation in a virtual environment, a C compiler and
|
|
|
79af3c |
+several development packages are required.
|
|
|
79af3c |
+
|
|
|
79af3c |
+::
|
|
|
79af3c |
+
|
|
|
79af3c |
+ $ virtualenv venv
|
|
|
79af3c |
+ $ venv/bin/pip install --upgrade custodia.ipa
|
|
|
79af3c |
+
|
|
|
79af3c |
+Fedora
|
|
|
79af3c |
+^^^^^^
|
|
|
79af3c |
+
|
|
|
79af3c |
+::
|
|
|
79af3c |
+
|
|
|
79af3c |
+ $ sudo dnf install python2 python-pip python-virtualenv python-devel \
|
|
|
79af3c |
+ gcc redhat-rpm-config krb5-workstation krb5-devel libffi-devel \
|
|
|
79af3c |
+ nss-devel openldap-devel cyrus-sasl-devel openssl-devel
|
|
|
79af3c |
+
|
|
|
79af3c |
+Debian / Ubuntu
|
|
|
79af3c |
+^^^^^^^^^^^^^^^
|
|
|
79af3c |
+
|
|
|
79af3c |
+::
|
|
|
79af3c |
+
|
|
|
79af3c |
+ $ sudo apt-get update
|
|
|
79af3c |
+ $ sudo apt-get install -y python2.7 python-pip python-virtualenv python-dev \
|
|
|
79af3c |
+ gcc krb5-user libkrb5-dev libffi-dev libnss3-dev libldap2-dev \
|
|
|
79af3c |
+ libsasl2-dev libssl-dev
|
|
|
79af3c |
+
|
|
|
79af3c |
+--------------
|
|
|
79af3c |
+
|
|
|
79af3c |
+Example configuration
|
|
|
79af3c |
+---------------------
|
|
|
79af3c |
+
|
|
|
79af3c |
+Create directories
|
|
|
79af3c |
+
|
|
|
79af3c |
+::
|
|
|
79af3c |
+
|
|
|
79af3c |
+ $ sudo mkdir /etc/custodia /var/lib/custodia /var/log/custodia /var/run/custodia
|
|
|
79af3c |
+ $ sudo chown USER:GROUP /var/lib/custodia /var/log/custodia /var/run/custodia
|
|
|
79af3c |
+ $ sudo chmod 750 /var/lib/custodia /var/log/custodia
|
|
|
79af3c |
+
|
|
|
79af3c |
+Create service account and keytab
|
|
|
79af3c |
+
|
|
|
79af3c |
+::
|
|
|
79af3c |
+
|
|
|
79af3c |
+ $ kinit admin
|
|
|
79af3c |
+ $ ipa service-add custodia/client1.ipa.example
|
|
|
79af3c |
+ $ ipa service-allow-create-keytab custodia/client1.ipa.example --users=admin
|
|
|
79af3c |
+ $ mkdir -p /etc/custodia
|
|
|
79af3c |
+ $ ipa-getkeytab -p custodia/client1.ipa.example -k /etc/custodia/custodia.keytab
|
|
|
79af3c |
+
|
|
|
79af3c |
+Create ``/etc/custodia/custodia.conf``
|
|
|
79af3c |
+
|
|
|
79af3c |
+::
|
|
|
79af3c |
+
|
|
|
79af3c |
+ [DEFAULT]
|
|
|
79af3c |
+ confdir = /etc/custodia
|
|
|
79af3c |
+ libdir = /var/lib/custodia
|
|
|
79af3c |
+ logdir = /var/log/custodia
|
|
|
79af3c |
+ rundir = /var/run/custodia
|
|
|
79af3c |
+
|
|
|
79af3c |
+ [global]
|
|
|
79af3c |
+ debug = true
|
|
|
79af3c |
+ server_socket = ${rundir}/custodia.sock
|
|
|
79af3c |
+ auditlog = ${logdir}/audit.log
|
|
|
79af3c |
+
|
|
|
79af3c |
+ [store:vault]
|
|
|
79af3c |
+ handler = IPAVault
|
|
|
79af3c |
+ keytab = {confdir}/custodia.keytab
|
|
|
79af3c |
+ ccache = FILE:{rundir}/ccache
|
|
|
79af3c |
+
|
|
|
79af3c |
+ [auth:creds]
|
|
|
79af3c |
+ handler = SimpleCredsAuth
|
|
|
79af3c |
+ uid = root
|
|
|
79af3c |
+ gid = root
|
|
|
79af3c |
+
|
|
|
79af3c |
+ [authz:paths]
|
|
|
79af3c |
+ handler = SimplePathAuthz
|
|
|
79af3c |
+ paths = /. /secrets
|
|
|
79af3c |
+
|
|
|
79af3c |
+ [/]
|
|
|
79af3c |
+ handler = Root
|
|
|
79af3c |
+
|
|
|
79af3c |
+ [/secrets]
|
|
|
79af3c |
+ handler = Secrets
|
|
|
79af3c |
+ store = vault
|
|
|
79af3c |
+
|
|
|
79af3c |
+Run Custodia server
|
|
|
79af3c |
+
|
|
|
79af3c |
+::
|
|
|
79af3c |
+
|
|
|
79af3c |
+ $ custodia /etc/custodia/custodia.conf
|
|
|
79af3c |
diff --git a/custodia/ipa/__init__.py b/custodia/ipa/__init__.py
|
|
|
79af3c |
new file mode 100644
|
|
|
79af3c |
index 0000000..ef1bb9d
|
|
|
79af3c |
--- /dev/null
|
|
|
79af3c |
+++ b/custodia/ipa/__init__.py
|
|
|
79af3c |
@@ -0,0 +1 @@
|
|
|
79af3c |
+# Copyright (C) 2016 Custodia Project Contributors - see LICENSE file
|
|
|
79af3c |
diff --git a/custodia/ipa/vault.py b/custodia/ipa/vault.py
|
|
|
79af3c |
new file mode 100644
|
|
|
79af3c |
index 0000000..f681c54
|
|
|
79af3c |
--- /dev/null
|
|
|
79af3c |
+++ b/custodia/ipa/vault.py
|
|
|
79af3c |
@@ -0,0 +1,291 @@
|
|
|
79af3c |
+# Copyright (C) 2016 Custodia Project Contributors - see LICENSE file
|
|
|
79af3c |
+"""FreeIPA vault store (PoC)
|
|
|
79af3c |
+"""
|
|
|
79af3c |
+import os
|
|
|
79af3c |
+
|
|
|
79af3c |
+import ipalib
|
|
|
79af3c |
+from ipalib.errors import DuplicateEntry, NotFound
|
|
|
79af3c |
+from ipalib.krb_utils import get_principal
|
|
|
79af3c |
+
|
|
|
79af3c |
+import six
|
|
|
79af3c |
+
|
|
|
79af3c |
+from custodia.plugin import CSStore, CSStoreError, CSStoreExists, PluginOption
|
|
|
79af3c |
+
|
|
|
79af3c |
+
|
|
|
79af3c |
+def krb5_unparse_principal_name(name):
|
|
|
79af3c |
+ """Split a Kerberos principal name into parts
|
|
|
79af3c |
+
|
|
|
79af3c |
+ Returns:
|
|
|
79af3c |
+ * ('host', hostname, realm) for a host principal
|
|
|
79af3c |
+ * (servicename, hostname, realm) for a service principal
|
|
|
79af3c |
+ * (None, username, realm) for a user principal
|
|
|
79af3c |
+
|
|
|
79af3c |
+ :param text name: Kerberos principal name
|
|
|
79af3c |
+ :return: (service, host, realm) or (None, username, realm)
|
|
|
79af3c |
+ """
|
|
|
79af3c |
+ prefix, realm = name.split(u'@')
|
|
|
79af3c |
+ if u'/' in prefix:
|
|
|
79af3c |
+ service, host = prefix.rsplit(u'/', 1)
|
|
|
79af3c |
+ return service, host, realm
|
|
|
79af3c |
+ else:
|
|
|
79af3c |
+ return None, prefix, realm
|
|
|
79af3c |
+
|
|
|
79af3c |
+
|
|
|
79af3c |
+class FreeIPA(object):
|
|
|
79af3c |
+ """FreeIPA wrapper
|
|
|
79af3c |
+
|
|
|
79af3c |
+ Custodia uses a forking server model. We can bootstrap FreeIPA API in
|
|
|
79af3c |
+ the main process. Connections must be created in the client process.
|
|
|
79af3c |
+ """
|
|
|
79af3c |
+ def __init__(self, api=None, ipa_context='cli', ipa_confdir=None,
|
|
|
79af3c |
+ ipa_debug=False):
|
|
|
79af3c |
+ if api is None:
|
|
|
79af3c |
+ self._api = ipalib.api
|
|
|
79af3c |
+ else:
|
|
|
79af3c |
+ self._api = api
|
|
|
79af3c |
+ self._ipa_config = dict(
|
|
|
79af3c |
+ context=ipa_context,
|
|
|
79af3c |
+ debug=ipa_debug,
|
|
|
79af3c |
+ log=None, # disable logging to file
|
|
|
79af3c |
+ )
|
|
|
79af3c |
+ if ipa_confdir is not None:
|
|
|
79af3c |
+ self._ipa_config['confdir'] = ipa_confdir
|
|
|
79af3c |
+ self._bootstrap()
|
|
|
79af3c |
+
|
|
|
79af3c |
+ @property
|
|
|
79af3c |
+ def Command(self):
|
|
|
79af3c |
+ return self._api.Command # pylint: disable=no-member
|
|
|
79af3c |
+
|
|
|
79af3c |
+ @property
|
|
|
79af3c |
+ def env(self):
|
|
|
79af3c |
+ return self._api.env # pylint: disable=no-member
|
|
|
79af3c |
+
|
|
|
79af3c |
+ def _bootstrap(self):
|
|
|
79af3c |
+ if not self._api.isdone('bootstrap'):
|
|
|
79af3c |
+ # TODO: bandaid for "A PKCS #11 module returned CKR_DEVICE_ERROR"
|
|
|
79af3c |
+ # https://github.com/avocado-framework/avocado/issues/1112#issuecomment-206999400
|
|
|
79af3c |
+ os.environ['NSS_STRICT_NOFORK'] = 'DISABLED'
|
|
|
79af3c |
+ self._api.bootstrap(**self._ipa_config)
|
|
|
79af3c |
+ self._api.finalize()
|
|
|
79af3c |
+
|
|
|
79af3c |
+ def __enter__(self):
|
|
|
79af3c |
+ # pylint: disable=no-member
|
|
|
79af3c |
+ if not self._api.Backend.rpcclient.isconnected():
|
|
|
79af3c |
+ self._api.Backend.rpcclient.connect()
|
|
|
79af3c |
+ # pylint: enable=no-member
|
|
|
79af3c |
+ return self
|
|
|
79af3c |
+
|
|
|
79af3c |
+ def __exit__(self, exc_type, exc_val, exc_tb):
|
|
|
79af3c |
+ # pylint: disable=no-member
|
|
|
79af3c |
+ if self._api.Backend.rpcclient.isconnected():
|
|
|
79af3c |
+ self._api.Backend.rpcclient.disconnect()
|
|
|
79af3c |
+ # pylint: enable=no-member
|
|
|
79af3c |
+
|
|
|
79af3c |
+
|
|
|
79af3c |
+class IPAVault(CSStore):
|
|
|
79af3c |
+ # vault arguments
|
|
|
79af3c |
+ principal = PluginOption(
|
|
|
79af3c |
+ str, None,
|
|
|
79af3c |
+ "Service principal for service vault (auto-discovered from GSSAPI)"
|
|
|
79af3c |
+ )
|
|
|
79af3c |
+ user = PluginOption(
|
|
|
79af3c |
+ str, None,
|
|
|
79af3c |
+ "User name for user vault (auto-discovered from GSSAPI)"
|
|
|
79af3c |
+ )
|
|
|
79af3c |
+ vault_type = PluginOption(
|
|
|
79af3c |
+ str, None,
|
|
|
79af3c |
+ "vault type, one of 'user', 'service', 'shared', or "
|
|
|
79af3c |
+ "auto-discovered from GSSAPI"
|
|
|
79af3c |
+ )
|
|
|
79af3c |
+
|
|
|
79af3c |
+ # Kerberos flags
|
|
|
79af3c |
+ krb5config = PluginOption(str, None, "Kerberos krb5.conf override")
|
|
|
79af3c |
+ keytab = PluginOption(str, None, "Kerberos keytab for auth")
|
|
|
79af3c |
+ ccache = PluginOption(
|
|
|
79af3c |
+ str, None, "Kerberos ccache, e,g. FILE:/path/to/ccache")
|
|
|
79af3c |
+
|
|
|
79af3c |
+ # ipalib.api arguments
|
|
|
79af3c |
+ ipa_confdir = PluginOption(str, None, "IPA confdir override")
|
|
|
79af3c |
+ ipa_context = PluginOption(str, "cli", "IPA bootstrap context")
|
|
|
79af3c |
+ ipa_debug = PluginOption(bool, False, "debug mode for ipalib")
|
|
|
79af3c |
+
|
|
|
79af3c |
+ def __init__(self, config, section=None):
|
|
|
79af3c |
+ super(IPAVault, self).__init__(config, section)
|
|
|
79af3c |
+ # configure Kerberos / GSSAPI and acquire principal
|
|
|
79af3c |
+ gssapi_principal = self._gssapi()
|
|
|
79af3c |
+ self.logger.info(u"Vault uses Kerberos principal '%s'",
|
|
|
79af3c |
+ gssapi_principal)
|
|
|
79af3c |
+
|
|
|
79af3c |
+ # bootstrap FreeIPA API
|
|
|
79af3c |
+ self.ipa = FreeIPA(
|
|
|
79af3c |
+ ipa_confdir=self.ipa_confdir,
|
|
|
79af3c |
+ ipa_debug=self.ipa_debug,
|
|
|
79af3c |
+ ipa_context=self.ipa_context,
|
|
|
79af3c |
+ )
|
|
|
79af3c |
+ # connect
|
|
|
79af3c |
+ with self.ipa:
|
|
|
79af3c |
+ self.logger.info("IPA server '%s': %s",
|
|
|
79af3c |
+ self.ipa.env.server,
|
|
|
79af3c |
+ self.ipa.Command.ping()[u'summary'])
|
|
|
79af3c |
+ # retrieve and cache KRA transport cert
|
|
|
79af3c |
+ response = self.ipa.Command.vaultconfig_show()
|
|
|
79af3c |
+ servers = response[u'result'][u'kra_server_server']
|
|
|
79af3c |
+ self.logger.info("KRA server(s) %s", ', '.join(servers))
|
|
|
79af3c |
+
|
|
|
79af3c |
+ service, user_host, realm = krb5_unparse_principal_name(
|
|
|
79af3c |
+ gssapi_principal)
|
|
|
79af3c |
+ self._init_vault_args(service, user_host, realm)
|
|
|
79af3c |
+
|
|
|
79af3c |
+ def _gssapi(self):
|
|
|
79af3c |
+ # set client keytab env var for authentication
|
|
|
79af3c |
+ if self.keytab is not None:
|
|
|
79af3c |
+ os.environ['KRB5_CLIENT_KTNAME'] = self.keytab
|
|
|
79af3c |
+ if self.ccache is not None:
|
|
|
79af3c |
+ os.environ['KRB5CCNAME'] = self.ccache
|
|
|
79af3c |
+ if self.krb5config is not None:
|
|
|
79af3c |
+ os.environ['KRB5_CONFIG'] = self.krb5config
|
|
|
79af3c |
+ try:
|
|
|
79af3c |
+ return get_principal()
|
|
|
79af3c |
+ except Exception:
|
|
|
79af3c |
+ self.logger.error(
|
|
|
79af3c |
+ "Unable to get principal from GSSAPI. Are you missing a "
|
|
|
79af3c |
+ "TGT or valid Kerberos keytab?"
|
|
|
79af3c |
+ )
|
|
|
79af3c |
+ raise
|
|
|
79af3c |
+
|
|
|
79af3c |
+ def _init_vault_args(self, service, user_host, realm):
|
|
|
79af3c |
+ if self.vault_type is None:
|
|
|
79af3c |
+ self.vault_type = 'user' if service is None else 'service'
|
|
|
79af3c |
+ self.logger.info("Setting vault type to '%s' from Kerberos",
|
|
|
79af3c |
+ self.vault_type)
|
|
|
79af3c |
+
|
|
|
79af3c |
+ if self.vault_type == 'shared':
|
|
|
79af3c |
+ self._vault_args = {'shared': True}
|
|
|
79af3c |
+ elif self.vault_type == 'user':
|
|
|
79af3c |
+ if self.user is None:
|
|
|
79af3c |
+ if service is not None:
|
|
|
79af3c |
+ msg = "{!r}: User vault requires 'user' parameter"
|
|
|
79af3c |
+ raise ValueError(msg.format(self))
|
|
|
79af3c |
+ else:
|
|
|
79af3c |
+ self.user = user_host
|
|
|
79af3c |
+ self.logger.info(u"Setting username '%s' from Kerberos",
|
|
|
79af3c |
+ self.user)
|
|
|
79af3c |
+ if six.PY2 and isinstance(self.user, str):
|
|
|
79af3c |
+ self.user = self.user.decode('utf-8')
|
|
|
79af3c |
+ self._vault_args = {'username': self.user}
|
|
|
79af3c |
+ elif self.vault_type == 'service':
|
|
|
79af3c |
+ if self.principal is None:
|
|
|
79af3c |
+ if service is None:
|
|
|
79af3c |
+ msg = "{!r}: Service vault requires 'principal' parameter"
|
|
|
79af3c |
+ raise ValueError(msg.format(self))
|
|
|
79af3c |
+ else:
|
|
|
79af3c |
+ self.principal = u'/'.join((service, user_host))
|
|
|
79af3c |
+ self.logger.info(u"Setting principal '%s' from Kerberos",
|
|
|
79af3c |
+ self.principal)
|
|
|
79af3c |
+ if six.PY2 and isinstance(self.principal, str):
|
|
|
79af3c |
+ self.principal = self.principal.decode('utf-8')
|
|
|
79af3c |
+ self._vault_args = {'service': self.principal}
|
|
|
79af3c |
+ else:
|
|
|
79af3c |
+ msg = '{!r}: Invalid vault type {}'
|
|
|
79af3c |
+ raise ValueError(msg.format(self, self.vault_type))
|
|
|
79af3c |
+
|
|
|
79af3c |
+ def _mangle_key(self, key):
|
|
|
79af3c |
+ if '__' in key:
|
|
|
79af3c |
+ raise ValueError
|
|
|
79af3c |
+ key = key.replace('/', '__')
|
|
|
79af3c |
+ if isinstance(key, bytes):
|
|
|
79af3c |
+ key = key.decode('utf-8')
|
|
|
79af3c |
+ return key
|
|
|
79af3c |
+
|
|
|
79af3c |
+ def get(self, key):
|
|
|
79af3c |
+ key = self._mangle_key(key)
|
|
|
79af3c |
+ with self.ipa as ipa:
|
|
|
79af3c |
+ try:
|
|
|
79af3c |
+ result = ipa.Command.vault_retrieve(
|
|
|
79af3c |
+ key, **self._vault_args)
|
|
|
79af3c |
+ except NotFound as e:
|
|
|
79af3c |
+ self.logger.info(str(e))
|
|
|
79af3c |
+ return None
|
|
|
79af3c |
+ except Exception:
|
|
|
79af3c |
+ msg = "Failed to retrieve entry {}".format(key)
|
|
|
79af3c |
+ self.logger.exception(msg)
|
|
|
79af3c |
+ raise CSStoreError(msg)
|
|
|
79af3c |
+ else:
|
|
|
79af3c |
+ return result[u'result'][u'data']
|
|
|
79af3c |
+
|
|
|
79af3c |
+ def set(self, key, value, replace=False):
|
|
|
79af3c |
+ key = self._mangle_key(key)
|
|
|
79af3c |
+ if not isinstance(value, bytes):
|
|
|
79af3c |
+ value = value.encode('utf-8')
|
|
|
79af3c |
+ with self.ipa as ipa:
|
|
|
79af3c |
+ try:
|
|
|
79af3c |
+ ipa.Command.vault_add(
|
|
|
79af3c |
+ key, ipavaulttype=u"standard", **self._vault_args)
|
|
|
79af3c |
+ except DuplicateEntry:
|
|
|
79af3c |
+ if not replace:
|
|
|
79af3c |
+ raise CSStoreExists(key)
|
|
|
79af3c |
+ except Exception:
|
|
|
79af3c |
+ msg = "Failed to add entry {}".format(key)
|
|
|
79af3c |
+ self.logger.exception(msg)
|
|
|
79af3c |
+ raise CSStoreError(msg)
|
|
|
79af3c |
+ try:
|
|
|
79af3c |
+ ipa.Command.vault_archive(
|
|
|
79af3c |
+ key, data=value, **self._vault_args)
|
|
|
79af3c |
+ except Exception:
|
|
|
79af3c |
+ msg = "Failed to archive entry {}".format(key)
|
|
|
79af3c |
+ self.logger.exception(msg)
|
|
|
79af3c |
+ raise CSStoreError(msg)
|
|
|
79af3c |
+
|
|
|
79af3c |
+ def span(self, key):
|
|
|
79af3c |
+ raise CSStoreError("span is not implemented")
|
|
|
79af3c |
+
|
|
|
79af3c |
+ def list(self, keyfilter=None):
|
|
|
79af3c |
+ with self.ipa as ipa:
|
|
|
79af3c |
+ try:
|
|
|
79af3c |
+ result = ipa.Command.vault_find(
|
|
|
79af3c |
+ ipavaulttype=u"standard", **self._vault_args)
|
|
|
79af3c |
+ except Exception:
|
|
|
79af3c |
+ msg = "Failed to list entries"
|
|
|
79af3c |
+ self.logger.exception(msg)
|
|
|
79af3c |
+ raise CSStoreError(msg)
|
|
|
79af3c |
+
|
|
|
79af3c |
+ names = []
|
|
|
79af3c |
+ for entry in result[u'result']:
|
|
|
79af3c |
+ cn = entry[u'cn'][0]
|
|
|
79af3c |
+ key = cn.replace('__', '/')
|
|
|
79af3c |
+ if keyfilter is not None and not key.startswith(keyfilter):
|
|
|
79af3c |
+ continue
|
|
|
79af3c |
+ names.append(key.rsplit('/', 1)[-1])
|
|
|
79af3c |
+ return names
|
|
|
79af3c |
+
|
|
|
79af3c |
+ def cut(self, key):
|
|
|
79af3c |
+ key = self._mangle_key(key)
|
|
|
79af3c |
+ with self.ipa as ipa:
|
|
|
79af3c |
+ try:
|
|
|
79af3c |
+ ipa.Command.vault_del(key, **self._vault_args)
|
|
|
79af3c |
+ except NotFound:
|
|
|
79af3c |
+ return False
|
|
|
79af3c |
+ except Exception:
|
|
|
79af3c |
+ msg = "Failed to delete entry {}".format(key)
|
|
|
79af3c |
+ self.logger.exception(msg)
|
|
|
79af3c |
+ raise CSStoreError(msg)
|
|
|
79af3c |
+ else:
|
|
|
79af3c |
+ return True
|
|
|
79af3c |
+
|
|
|
79af3c |
+
|
|
|
79af3c |
+if __name__ == '__main__':
|
|
|
79af3c |
+ from custodia.compat import configparser
|
|
|
79af3c |
+
|
|
|
79af3c |
+ parser = configparser.ConfigParser(
|
|
|
79af3c |
+ interpolation=configparser.ExtendedInterpolation()
|
|
|
79af3c |
+ )
|
|
|
79af3c |
+ parser.read_string(u"""
|
|
|
79af3c |
+ [store:ipa_vault]
|
|
|
79af3c |
+ """)
|
|
|
79af3c |
+
|
|
|
79af3c |
+ v = IPAVault(parser, "store:ipa_vault")
|
|
|
79af3c |
+ v.set('foo', 'bar', replace=True)
|
|
|
79af3c |
+ print(v.get('foo'))
|
|
|
79af3c |
+ print(v.list())
|
|
|
79af3c |
+ v.cut('foo')
|
|
|
79af3c |
+ print(v.list())
|
|
|
79af3c |
diff --git a/setup.py b/setup.py
|
|
|
79af3c |
index c8f270d..4bf096c 100755
|
|
|
79af3c |
--- a/setup.py
|
|
|
79af3c |
+++ b/setup.py
|
|
|
79af3c |
@@ -15,10 +15,14 @@ requirements = [
|
|
|
79af3c |
'requests'
|
|
|
79af3c |
]
|
|
|
79af3c |
|
|
|
79af3c |
+# extra requirements
|
|
|
79af3c |
+ipa_requires = ['ipalib >= 4.5.0', 'ipaclient >= 4.5.0']
|
|
|
79af3c |
+
|
|
|
79af3c |
# test requirements
|
|
|
79af3c |
-test_requires = ['coverage', 'pytest']
|
|
|
79af3c |
+test_requires = ['coverage', 'pytest', 'mock'] + ipa_requires
|
|
|
79af3c |
|
|
|
79af3c |
extras_require = {
|
|
|
79af3c |
+ 'ipa': ipa_requires,
|
|
|
79af3c |
'test': test_requires,
|
|
|
79af3c |
'test_docs': ['docutils', 'markdown'],
|
|
|
79af3c |
'test_pep8': ['flake8', 'flake8-import-order', 'pep8-naming'],
|
|
|
79af3c |
@@ -66,6 +70,7 @@ custodia_consumers = [
|
|
|
79af3c |
custodia_stores = [
|
|
|
79af3c |
'EncryptedOverlay = custodia.store.encgen:EncryptedOverlay',
|
|
|
79af3c |
'EncryptedStore = custodia.store.enclite:EncryptedStore',
|
|
|
79af3c |
+ 'IPAVault = custodia.ipa.vault:IPAVault',
|
|
|
79af3c |
'SqliteStore = custodia.store.sqlite:SqliteStore',
|
|
|
79af3c |
]
|
|
|
79af3c |
|
|
|
79af3c |
@@ -84,6 +89,7 @@ setup(
|
|
|
79af3c |
'custodia',
|
|
|
79af3c |
'custodia.cli',
|
|
|
79af3c |
'custodia.httpd',
|
|
|
79af3c |
+ 'custodia.ipa',
|
|
|
79af3c |
'custodia.message',
|
|
|
79af3c |
'custodia.server',
|
|
|
79af3c |
'custodia.store',
|
|
|
79af3c |
diff --git a/tests/test_ipa.py b/tests/test_ipa.py
|
|
|
79af3c |
new file mode 100644
|
|
|
79af3c |
index 0000000..eb4d7fa
|
|
|
79af3c |
--- /dev/null
|
|
|
79af3c |
+++ b/tests/test_ipa.py
|
|
|
79af3c |
@@ -0,0 +1,195 @@
|
|
|
79af3c |
+# Copyright (C) 2017 Custodia project Contributors, for licensee see COPYING
|
|
|
79af3c |
+
|
|
|
79af3c |
+import os
|
|
|
79af3c |
+
|
|
|
79af3c |
+import ipalib
|
|
|
79af3c |
+
|
|
|
79af3c |
+import mock
|
|
|
79af3c |
+
|
|
|
79af3c |
+import pytest
|
|
|
79af3c |
+
|
|
|
79af3c |
+from custodia.compat import configparser
|
|
|
79af3c |
+from custodia.ipa.vault import FreeIPA, IPAVault, krb5_unparse_principal_name
|
|
|
79af3c |
+
|
|
|
79af3c |
+
|
|
|
79af3c |
+CONFIG = u"""
|
|
|
79af3c |
+[store:ipa_service]
|
|
|
79af3c |
+vault_type = service
|
|
|
79af3c |
+principal = custodia/ipa.example
|
|
|
79af3c |
+
|
|
|
79af3c |
+[store:ipa_user]
|
|
|
79af3c |
+vault_type = user
|
|
|
79af3c |
+user = john
|
|
|
79af3c |
+
|
|
|
79af3c |
+[store:ipa_shared]
|
|
|
79af3c |
+vault_type = shared
|
|
|
79af3c |
+
|
|
|
79af3c |
+[store:ipa_invalid]
|
|
|
79af3c |
+vault_type = invalid
|
|
|
79af3c |
+
|
|
|
79af3c |
+[store:ipa_autodiscover]
|
|
|
79af3c |
+
|
|
|
79af3c |
+[store:ipa_environ]
|
|
|
79af3c |
+krb5config = /path/to/krb5.conf
|
|
|
79af3c |
+keytab = /path/to/custodia.keytab
|
|
|
79af3c |
+ccache = FILE:/path/to/ccache
|
|
|
79af3c |
+"""
|
|
|
79af3c |
+
|
|
|
79af3c |
+vault_parametrize = pytest.mark.parametrize(
|
|
|
79af3c |
+ 'plugin,vault_type,vault_args',
|
|
|
79af3c |
+ [
|
|
|
79af3c |
+ ('store:ipa_service', 'service', {'service': 'custodia/ipa.example'}),
|
|
|
79af3c |
+ ('store:ipa_user', 'user', {'username': 'john'}),
|
|
|
79af3c |
+ ('store:ipa_shared', 'shared', {'shared': True}),
|
|
|
79af3c |
+ ]
|
|
|
79af3c |
+)
|
|
|
79af3c |
+
|
|
|
79af3c |
+
|
|
|
79af3c |
+class TestCustodiaIPA:
|
|
|
79af3c |
+ def setup_method(self, method):
|
|
|
79af3c |
+ self.parser = configparser.ConfigParser(
|
|
|
79af3c |
+ interpolation=configparser.ExtendedInterpolation(),
|
|
|
79af3c |
+ )
|
|
|
79af3c |
+ self.parser.read_string(CONFIG)
|
|
|
79af3c |
+ # mocked ipalib.api
|
|
|
79af3c |
+ self.p_api = mock.patch('ipalib.api', autospec=ipalib.api)
|
|
|
79af3c |
+ self.m_api = self.p_api.start()
|
|
|
79af3c |
+ self.m_api.isdone.return_value = False
|
|
|
79af3c |
+ self.m_api.env = mock.Mock()
|
|
|
79af3c |
+ self.m_api.env.server = 'server.ipa.example'
|
|
|
79af3c |
+ self.m_api.Backend = mock.Mock()
|
|
|
79af3c |
+ self.m_api.Command = mock.Mock()
|
|
|
79af3c |
+ self.m_api.Command.ping.return_value = {
|
|
|
79af3c |
+ u'summary': u'IPA server version 4.4.3. API version 2.215',
|
|
|
79af3c |
+ }
|
|
|
79af3c |
+ self.m_api.Command.vaultconfig_show.return_value = {
|
|
|
79af3c |
+ u'result': {
|
|
|
79af3c |
+ u'kra_server_server': [u'ipa.example'],
|
|
|
79af3c |
+ }
|
|
|
79af3c |
+ }
|
|
|
79af3c |
+ # mocked get_principal
|
|
|
79af3c |
+ self.p_get_principal = mock.patch('custodia.ipa.vault.get_principal')
|
|
|
79af3c |
+ self.m_get_principal = self.p_get_principal.start()
|
|
|
79af3c |
+ self.m_get_principal.return_value = 'custodia/ipa.example@IPA.EXAMPLE'
|
|
|
79af3c |
+ # mocked environ (empty dict)
|
|
|
79af3c |
+ self.p_env = mock.patch.dict('os.environ', clear=True)
|
|
|
79af3c |
+ self.p_env.start()
|
|
|
79af3c |
+
|
|
|
79af3c |
+ def teardown_method(self, method):
|
|
|
79af3c |
+ self.p_api.stop()
|
|
|
79af3c |
+ self.p_get_principal.stop()
|
|
|
79af3c |
+ self.p_env.stop()
|
|
|
79af3c |
+
|
|
|
79af3c |
+ def test_api_init(self):
|
|
|
79af3c |
+ m_api = self.m_api
|
|
|
79af3c |
+ freeipa = FreeIPA(api=m_api)
|
|
|
79af3c |
+ m_api.isdone.assert_called_once_with('bootstrap')
|
|
|
79af3c |
+ m_api.bootstrap.assert_called_once_with(
|
|
|
79af3c |
+ context='cli',
|
|
|
79af3c |
+ debug=False,
|
|
|
79af3c |
+ log=None,
|
|
|
79af3c |
+ )
|
|
|
79af3c |
+
|
|
|
79af3c |
+ m_api.Backend.rpcclient.isconnected.return_value = False
|
|
|
79af3c |
+ with freeipa:
|
|
|
79af3c |
+ m_api.Backend.rpcclient.connect.assert_called_once()
|
|
|
79af3c |
+ m_api.Backend.rpcclient.isconnected.return_value = True
|
|
|
79af3c |
+ m_api.Backend.rpcclient.disconnect.assert_called_once()
|
|
|
79af3c |
+
|
|
|
79af3c |
+ assert os.environ == dict(NSS_STRICT_NOFORK='DISABLED')
|
|
|
79af3c |
+
|
|
|
79af3c |
+ def test_api_environ(self):
|
|
|
79af3c |
+ assert os.environ == {}
|
|
|
79af3c |
+ IPAVault(self.parser, 'store:ipa_environ')
|
|
|
79af3c |
+ assert os.environ == dict(
|
|
|
79af3c |
+ NSS_STRICT_NOFORK='DISABLED',
|
|
|
79af3c |
+ KRB5_CONFIG='/path/to/krb5.conf',
|
|
|
79af3c |
+ KRB5_CLIENT_KTNAME='/path/to/custodia.keytab',
|
|
|
79af3c |
+ KRB5CCNAME='FILE:/path/to/ccache',
|
|
|
79af3c |
+ )
|
|
|
79af3c |
+
|
|
|
79af3c |
+ def test_invalid_vault_type(self):
|
|
|
79af3c |
+ pytest.raises(ValueError, IPAVault, self.parser, 'store:ipa_invalid')
|
|
|
79af3c |
+
|
|
|
79af3c |
+ def test_vault_autodiscover_service(self):
|
|
|
79af3c |
+ self.m_get_principal.return_value = 'custodia/ipa.example@IPA.EXAMPLE'
|
|
|
79af3c |
+ ipa = IPAVault(self.parser, 'store:ipa_autodiscover')
|
|
|
79af3c |
+ assert ipa.vault_type == 'service'
|
|
|
79af3c |
+ assert ipa.principal == 'custodia/ipa.example'
|
|
|
79af3c |
+ assert ipa.user is None
|
|
|
79af3c |
+
|
|
|
79af3c |
+ def test_vault_autodiscover_user(self):
|
|
|
79af3c |
+ self.m_get_principal.return_value = 'john@IPA.EXAMPLE'
|
|
|
79af3c |
+ ipa = IPAVault(self.parser, 'store:ipa_autodiscover')
|
|
|
79af3c |
+ assert ipa.vault_type == 'user'
|
|
|
79af3c |
+ assert ipa.principal is None
|
|
|
79af3c |
+ assert ipa.user == 'john'
|
|
|
79af3c |
+
|
|
|
79af3c |
+ @vault_parametrize
|
|
|
79af3c |
+ def test_vault_set(self, plugin, vault_type, vault_args):
|
|
|
79af3c |
+ ipa = IPAVault(self.parser, plugin)
|
|
|
79af3c |
+ assert ipa.vault_type == vault_type
|
|
|
79af3c |
+ self.m_api.Command.ping.assert_called_once()
|
|
|
79af3c |
+ ipa.set('directory/testkey', 'testvalue')
|
|
|
79af3c |
+ self.m_api.Command.vault_add.assert_called_once_with(
|
|
|
79af3c |
+ 'directory__testkey',
|
|
|
79af3c |
+ ipavaulttype=u'standard',
|
|
|
79af3c |
+ **vault_args
|
|
|
79af3c |
+ )
|
|
|
79af3c |
+ self.m_api.Command.vault_archive.assert_called_once_with(
|
|
|
79af3c |
+ 'directory__testkey',
|
|
|
79af3c |
+ data=b'testvalue',
|
|
|
79af3c |
+ **vault_args
|
|
|
79af3c |
+ )
|
|
|
79af3c |
+
|
|
|
79af3c |
+ @vault_parametrize
|
|
|
79af3c |
+ def test_vault_get(self, plugin, vault_type, vault_args):
|
|
|
79af3c |
+ ipa = IPAVault(self.parser, plugin)
|
|
|
79af3c |
+ assert ipa.vault_type == vault_type
|
|
|
79af3c |
+ self.m_api.Command.vault_retrieve.return_value = {
|
|
|
79af3c |
+ u'result': {
|
|
|
79af3c |
+ u'data': b'testvalue',
|
|
|
79af3c |
+ }
|
|
|
79af3c |
+ }
|
|
|
79af3c |
+ assert ipa.get('directory/testkey') == b'testvalue'
|
|
|
79af3c |
+ self.m_api.Command.vault_retrieve.assert_called_once_with(
|
|
|
79af3c |
+ 'directory__testkey',
|
|
|
79af3c |
+ **vault_args
|
|
|
79af3c |
+ )
|
|
|
79af3c |
+
|
|
|
79af3c |
+ @vault_parametrize
|
|
|
79af3c |
+ def test_vault_list(self, plugin, vault_type, vault_args):
|
|
|
79af3c |
+ ipa = IPAVault(self.parser, plugin)
|
|
|
79af3c |
+ assert ipa.vault_type == vault_type
|
|
|
79af3c |
+ self.m_api.Command.vault_find.return_value = {
|
|
|
79af3c |
+ u'result': [{'cn': [u'directory__testkey']}]
|
|
|
79af3c |
+ }
|
|
|
79af3c |
+ assert ipa.list('directory') == ['testkey']
|
|
|
79af3c |
+ self.m_api.Command.vault_find.assert_called_once_with(
|
|
|
79af3c |
+ ipavaulttype=u'standard',
|
|
|
79af3c |
+ **vault_args
|
|
|
79af3c |
+ )
|
|
|
79af3c |
+
|
|
|
79af3c |
+ @vault_parametrize
|
|
|
79af3c |
+ def test_vault_cut(self, plugin, vault_type, vault_args):
|
|
|
79af3c |
+ ipa = IPAVault(self.parser, plugin)
|
|
|
79af3c |
+ assert ipa.vault_type == vault_type
|
|
|
79af3c |
+ ipa.cut('directory/testkey')
|
|
|
79af3c |
+ self.m_api.Command.vault_del.assert_called_once_with(
|
|
|
79af3c |
+ 'directory__testkey',
|
|
|
79af3c |
+ **vault_args
|
|
|
79af3c |
+ )
|
|
|
79af3c |
+
|
|
|
79af3c |
+
|
|
|
79af3c |
+@pytest.mark.parametrize('principal,result', [
|
|
|
79af3c |
+ ('john@IPA.EXAMPLE',
|
|
|
79af3c |
+ (None, 'john', 'IPA.EXAMPLE')),
|
|
|
79af3c |
+ ('host/host.invalid@IPA.EXAMPLE',
|
|
|
79af3c |
+ ('host', 'host.invalid', 'IPA.EXAMPLE')),
|
|
|
79af3c |
+ ('custodia/host.invalid@IPA.EXAMPLE',
|
|
|
79af3c |
+ ('custodia', 'host.invalid', 'IPA.EXAMPLE')),
|
|
|
79af3c |
+ ('whatever/custodia/host.invalid@IPA.EXAMPLE',
|
|
|
79af3c |
+ ('whatever/custodia', 'host.invalid', 'IPA.EXAMPLE')),
|
|
|
79af3c |
+])
|
|
|
79af3c |
+def test_unparse(principal, result):
|
|
|
79af3c |
+ assert krb5_unparse_principal_name(principal) == result
|
|
|
79af3c |
--
|
|
|
79af3c |
2.9.3
|
|
|
79af3c |
|