Blame SOURCES/0004-Vendor-custodia.ipa.patch

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