[Freeipa-devel] [PATCH] Password vault

Endi Sukma Dewata edewata at redhat.com
Mon May 18 19:17:35 UTC 2015


Please take a look at the attached new patch which includes some of your 
changes you proposed.

On 5/14/2015 7:17 PM, Endi Sukma Dewata wrote:
> On 5/14/2015 1:42 PM, Jan Cholasta wrote:
>>>> Question: Services in IPA are identified by Kerberos principal. Why are
>>>> service vaults identified by hostname alone?
>>>
>>> The service vaults are actually identified by the hostname and service
>>> name assuming the principal is in this format: <name>/<host>@<realm>.
>>> The vault is stored in cn=<name>, cn=<host>, cn=services, cn=vaults,
>>> <suffix>. When accessing a vault service you are supposed to specify the
>>> name and the host (except for vault-find which will return all services
>>> in the same host):
>>>
>>> $ ipa vault-find --host <host>
>>> $ ipa vault-add <name> --host <host>
>>>
>>> Are there other service principal formats that we need to support? Do we
>>> need to support services of different realms?
>>
>> Well, users are identified by username (uid attribute), hosts by
>> hostname (fqdn attribute) and services by principal name
>> (krbprincipalname attribute). Each of them has separate container and is
>> also uniquely identified by principal name. I guess it would make sense
>> to follow that for vaults as well, in which case service vaults should
>> be called host vaults (because they are created in a host-specific
>> container, not a service-specific one) and maybe "real" service vaults
>> should be added.
>
> There's no plan for host vaults in the design doc, but it's not
> impossible to add it in the future. What is the use case?
>
> I suppose we can change the service vault into a container, and possibly
> add generic user vaults too (in addition to private user vaults), the
> interface will look like this:
>
> $ ipa vault-add PrivateVault
> $ ipa vault-add ServiceVault --service <name>/<hostname>
> $ ipa vault-add SharedVault --shared
> $ ipa vault-add UsersVault --user <username>
> $ ipa vault-add HostVault --host <hostname>
>
> Basically we'll need a new parameter for each new container. For the
> initial implementation we only need the first 2 or 3.

I changed the 'host vaults' to become 'service vaults'. The interface 
will look like this:

$ ipa vault-find --service HTTP/server.example.com
$ ipa vault-add test --service HTTP/server.example.com

I also added user vaults:

$ ipa vault-find --user testuser
$ ipa vault-add test --user testuser

Private vaults is a special case of user vaults where username=<you>.

Host vaults can be added later once we define the use case.

>>> 1. The following code in get_dn() is causing problems for vault-find.
>>>
>>>    dn = super(vault, self).get_dn(*keys, **options)
>>>    rdn = dn[0]
>>>    container_dn = DN(*dn[1:])
>>>
>>> The super.get_dn() will return cn=vaults, <suffix>, so the rdn will
>>> become cn=vaults and container_dn will become <suffix>. When combined
>>> with the subcontainer (private/shared/service) it will produce an
>>> invalid DN.
>>
>> Right, fixed.
>>
>> I should have tested the patch before posting it, my bad, sorry.
>
> OK.

This change has been included in this patch.

>>> 2. Not sure if it'related to #1, the vault-find is failing.
>>>
>>> $ ipa vault-find
>>> ipa: ERROR: an internal error has occurred
>>>
>>> The error_log shows TypeError: 'NoneType' object is not iterable
>>
>> Fixed. It was caused by missing return value in vault_find.exc_callback.
>
> We can actually ignore all NotFound errors since now all containers are
> either created automatically or already created by default.
>
>    if call_func.__name__ == 'find_entries':
>        if isinstance(exc, errors.NotFound):
>            shared = options.get('shared')
>
>            # if private or service container has not been created, ignore
>            if not shared:
>                raise errors.EmptyResult(reason=str(exc))
>
> The original code was only ignoring NotFound errors on user vaults
> because previously only the user containers was supposed to be created
> automatically, and there wasn't a plan to provide service container.

This change has been included and it will ignore all NotFound errors.

>>> 3. The --shared option in vault-find is now requiring an argument even
>>> though it's a Flag.
>>>
>>> $ ipa vault-find --shared
>>> Usage: ipa [global-options] vault-find [CRITERIA] [options]
>>>
>>> ipa: error: --shared option requires an argument
>>
>> Fixed.
>
> OK.

Not sure which code you changed, but the new patch doesn't exhibit this 
problem.

>>> 4. The following code in get_dn() is incorrect:
>>>
>>>    principal = getattr(context, 'principal')
>>>    (name, realm) = split_principal(principal)
>>>    name = name.split('/')
>>>    if len(name) == 1:
>>>        container_dn = DN(('cn', 'users'), container_dn)
>>>    else:
>>>        container_dn = DN(('cn', 'services'), container_dn)
>>>    container_dn = DN(('cn', name[-1]), container_dn)
>>>
>>> A service does not have a private container like users (cn=<username>,
>>> cn=users, cn=vaults). The entry cn=<name>, cn=<host>, cn=services,
>>> cn=vaults is a service vault, not a container. The service vault is used
>>> by the admin to provide a secret for a service.
>>>
>>> I'm not sure what the behavior should be if a service is executing a
>>> vault command that uses a private container such as:
>>>
>>>    $ ipa vault-add test
>>>
>>> Maybe it should just generate an error.
>>
>> Users, hosts and services are all user-like objects, is there a reason
>> not to support private vaults for all of them?
>
> As mentioned above, it's not required in the design doc, but we can add
> it if there's a clear use case. I agree that at least for now we can
> change the service vault into a service container to store multiple
> service's private vaults.

The code has been changed to use the service name as the container:

   cn=vaults
   + cn=services
     + cn=<service name>
       + cn=<vault 1>
       + cn=<vault 2>

>>> 5. In create_container() why do you need to reconstruct the container_dn
>>> on each invocation even though the value is fixed?
>>>
>>>    container_dn = DN(self.container_dn, self.api.env.basedn)
>>
>> Because self.api may not necessarily be the same as ipalib.api.
>
> Under what scenario would that be a problem? The original code seems to
> be working fine with ipalib.api.
>
> If it is a problem, why do we still use ipalib.api to initialize
> container_dn vault class attribute?
>
>    container_dn = api.env.container_vault
>
> Then in get_dn() we basically construct the container_dn variable with
> values from both self.api and ipalib.api:
>
>    container_dn = DN(self.container_dn, self.api.env.basedn)
>
> When is the self.api actually initialized? Can we initialize the
> container_dn (or base_dn as in the original code) attribute immediately
> after that?

This change is not included. The code will now obtain the values from 
apilib.api.env at init time and store it in class attributes so it can 
be reused.

     container_dn = api.env.container_vault
     base_dn = DN(container_dn, api.env.basedn)

>>> 6. The loop in create_container() is incorrect. Suppose we're creating a
>>> container cn=A, cn=B, <suffix> and the parent container cn=B, <suffix>
>>> doesn't exist yet. The first add_entry() invocation will fail as
>>> expected, but instead of adding the parent entry the whole method will
>>> fail.
>>
>> Right, fixed.
>
> It's still not working. The new code is trying to add cn=vaults first,
> and it stops because it already exists, but the intermediary entries are
> still not added. Also the DN displayed in the message misleading:
>
> $ ipa vault-add test
> ipa: ERROR: container entry (cn=vaults) not found
>
> $ ipa vault-add test --host server.example.com
> ipa: ERROR: container entry (cn=vaults) not found
>
> The unit tests are failing because of this.

This change is not included. The original code and the tests work just fine.

> 8. There's a new PEP8 error:
>
> .../ipalib/plugins/vault.py:98:1: E302 expected 2 blank lines, found 1

The new patch doesn't have this issue.

> 9. The API.txt needs to be regenerated.

The API.txt and VERSION file have been updated.

-- 
Endi S. Dewata
-------------- next part --------------
>From 00a5b0444e8d3c33c5236ae1d23fedde29135427 Mon Sep 17 00:00:00 2001
From: "Endi S. Dewata" <edewata at redhat.com>
Date: Tue, 21 Oct 2014 10:57:08 -0400
Subject: [PATCH] Added vault plugin.

A new plugin has been added to manage vaults. Test scripts have
also been added to verify the functionality.

https://fedorahosted.org/freeipa/ticket/3872
---
 API.txt                                   |  74 +++++
 VERSION                                   |   4 +-
 install/share/60basev3.ldif               |   1 +
 install/updates/40-vault.update           |  19 ++
 install/updates/Makefile.am               |   1 +
 ipa-client/man/default.conf.5             |   1 +
 ipalib/constants.py                       |   1 +
 ipalib/plugins/vault.py                   | 307 +++++++++++++++++++++
 ipatests/test_xmlrpc/test_vault_plugin.py | 445 ++++++++++++++++++++++++++++++
 9 files changed, 851 insertions(+), 2 deletions(-)
 create mode 100644 install/updates/40-vault.update
 create mode 100644 ipalib/plugins/vault.py
 create mode 100644 ipatests/test_xmlrpc/test_vault_plugin.py

diff --git a/API.txt b/API.txt
index 0808f3c64595495c8a9e60da5cbd689d5cdc6224..38deafefa57942bf242f989d79b1e93ee2c2013e 100644
--- a/API.txt
+++ b/API.txt
@@ -4717,6 +4717,80 @@ option: Str('version?', exclude='webui')
 output: Output('result', <type 'bool'>, None)
 output: Output('summary', (<type 'unicode'>, <type 'NoneType'>), None)
 output: PrimaryKey('value', None, None)
+command: vault_add
+args: 1,9,3
+arg: Str('cn', attribute=True, cli_name='name', maxlength=255, multivalue=False, pattern='^[a-zA-Z0-9_.-]+$', primary_key=True, required=True)
+option: Str('addattr*', cli_name='addattr', exclude='webui')
+option: Flag('all', autofill=True, cli_name='all', default=False, exclude='webui')
+option: Str('description', attribute=True, cli_name='desc', multivalue=False, required=False)
+option: Flag('raw', autofill=True, cli_name='raw', default=False, exclude='webui')
+option: Str('service?')
+option: Str('setattr*', cli_name='setattr', exclude='webui')
+option: Flag('shared?', autofill=True, default=False)
+option: Str('user?')
+option: Str('version?', exclude='webui')
+output: Entry('result', <type 'dict'>, Gettext('A dictionary representing an LDAP entry', domain='ipa', localedir=None))
+output: Output('summary', (<type 'unicode'>, <type 'NoneType'>), None)
+output: PrimaryKey('value', None, None)
+command: vault_del
+args: 1,5,3
+arg: Str('cn', attribute=True, cli_name='name', maxlength=255, multivalue=True, pattern='^[a-zA-Z0-9_.-]+$', primary_key=True, query=True, required=True)
+option: Flag('continue', autofill=True, cli_name='continue', default=False)
+option: Str('service?')
+option: Flag('shared?', autofill=True, default=False)
+option: Str('user?')
+option: Str('version?', exclude='webui')
+output: Output('result', <type 'dict'>, None)
+output: Output('summary', (<type 'unicode'>, <type 'NoneType'>), None)
+output: ListOfPrimaryKeys('value', None, None)
+command: vault_find
+args: 1,11,4
+arg: Str('criteria?', noextrawhitespace=False)
+option: Flag('all', autofill=True, cli_name='all', default=False, exclude='webui')
+option: Str('cn', attribute=True, autofill=False, cli_name='name', maxlength=255, multivalue=False, pattern='^[a-zA-Z0-9_.-]+$', primary_key=True, query=True, required=False)
+option: Str('description', attribute=True, autofill=False, cli_name='desc', multivalue=False, query=True, required=False)
+option: Flag('pkey_only?', autofill=True, default=False)
+option: Flag('raw', autofill=True, cli_name='raw', default=False, exclude='webui')
+option: Str('service?')
+option: Flag('shared?', autofill=True, default=False)
+option: Int('sizelimit?', autofill=False, minvalue=0)
+option: Int('timelimit?', autofill=False, minvalue=0)
+option: Str('user?')
+option: Str('version?', exclude='webui')
+output: Output('count', <type 'int'>, None)
+output: ListOfEntries('result', (<type 'list'>, <type 'tuple'>), Gettext('A list of LDAP entries', domain='ipa', localedir=None))
+output: Output('summary', (<type 'unicode'>, <type 'NoneType'>), None)
+output: Output('truncated', <type 'bool'>, None)
+command: vault_mod
+args: 1,11,3
+arg: Str('cn', attribute=True, cli_name='name', maxlength=255, multivalue=False, pattern='^[a-zA-Z0-9_.-]+$', primary_key=True, query=True, required=True)
+option: Str('addattr*', cli_name='addattr', exclude='webui')
+option: Flag('all', autofill=True, cli_name='all', default=False, exclude='webui')
+option: Str('delattr*', cli_name='delattr', exclude='webui')
+option: Str('description', attribute=True, autofill=False, cli_name='desc', multivalue=False, required=False)
+option: Flag('raw', autofill=True, cli_name='raw', default=False, exclude='webui')
+option: Flag('rights', autofill=True, default=False)
+option: Str('service?')
+option: Str('setattr*', cli_name='setattr', exclude='webui')
+option: Flag('shared?', autofill=True, default=False)
+option: Str('user?')
+option: Str('version?', exclude='webui')
+output: Entry('result', <type 'dict'>, Gettext('A dictionary representing an LDAP entry', domain='ipa', localedir=None))
+output: Output('summary', (<type 'unicode'>, <type 'NoneType'>), None)
+output: PrimaryKey('value', None, None)
+command: vault_show
+args: 1,7,3
+arg: Str('cn', attribute=True, cli_name='name', maxlength=255, multivalue=False, pattern='^[a-zA-Z0-9_.-]+$', primary_key=True, query=True, required=True)
+option: Flag('all', autofill=True, cli_name='all', default=False, exclude='webui')
+option: Flag('raw', autofill=True, cli_name='raw', default=False, exclude='webui')
+option: Flag('rights', autofill=True, default=False)
+option: Str('service?')
+option: Flag('shared?', autofill=True, default=False)
+option: Str('user?')
+option: Str('version?', exclude='webui')
+output: Entry('result', <type 'dict'>, Gettext('A dictionary representing an LDAP entry', domain='ipa', localedir=None))
+output: Output('summary', (<type 'unicode'>, <type 'NoneType'>), None)
+output: PrimaryKey('value', None, None)
 capability: messages 2.52
 capability: optional_uid_params 2.54
 capability: permissions2 2.69
diff --git a/VERSION b/VERSION
index c207558504e645dec73d105189d4b862877b4e26..33e7bebe4c673279ae92395a2575dc11e9fd4956 100644
--- a/VERSION
+++ b/VERSION
@@ -90,5 +90,5 @@ IPA_DATA_VERSION=20100614120000
 #                                                      #
 ########################################################
 IPA_API_VERSION_MAJOR=2
-IPA_API_VERSION_MINOR=118
-# Last change: tbordaz - Add stageuser_find, stageuser_mod, stageuser_del, stageuser_show
+IPA_API_VERSION_MINOR=119
+# Last change: edewata - Added vault plugin
diff --git a/install/share/60basev3.ldif b/install/share/60basev3.ldif
index eb1c1298b9de3295695745ba3311bb772dec70e9..33f4804e30ff1b3814ecf295bb41f07e2a8cd12f 100644
--- a/install/share/60basev3.ldif
+++ b/install/share/60basev3.ldif
@@ -79,3 +79,4 @@ objectClasses: (2.16.840.1.113730.3.8.12.24 NAME 'ipaPublicKeyObject' DESC 'Wrap
 objectClasses: (2.16.840.1.113730.3.8.12.25 NAME 'ipaPrivateKeyObject' DESC 'Wrapped private keys' SUP top AUXILIARY MUST ( ipaPrivateKey $ ipaWrappingKey $ ipaWrappingMech ) X-ORIGIN 'IPA v4.1' )
 objectClasses: (2.16.840.1.113730.3.8.12.26 NAME 'ipaSecretKeyObject' DESC 'Wrapped secret keys' SUP top AUXILIARY MUST ( ipaSecretKey $ ipaWrappingKey $ ipaWrappingMech ) X-ORIGIN 'IPA v4.1' )
 objectClasses: (2.16.840.1.113730.3.8.12.34 NAME 'ipaSecretKeyRefObject' DESC 'Indirect storage for encoded key material' SUP top AUXILIARY MUST ( ipaSecretKeyRef ) X-ORIGIN 'IPA v4.1' )
+objectClasses: (2.16.840.1.113730.3.8.18.1.1 NAME 'ipaVault' DESC 'IPA vault' SUP top STRUCTURAL MUST ( cn ) MAY ( description ) X-ORIGIN 'IPA v4.2' )
diff --git a/install/updates/40-vault.update b/install/updates/40-vault.update
new file mode 100644
index 0000000000000000000000000000000000000000..5a6b8c6a022fa56e5a5bc05369ce143d39644092
--- /dev/null
+++ b/install/updates/40-vault.update
@@ -0,0 +1,19 @@
+dn: cn=vaults,$SUFFIX
+default: objectClass: top
+default: objectClass: nsContainer
+default: cn: vaults
+
+dn: cn=services,cn=vaults,$SUFFIX
+default: objectClass: top
+default: objectClass: nsContainer
+default: cn: services
+
+dn: cn=shared,cn=vaults,$SUFFIX
+default: objectClass: top
+default: objectClass: nsContainer
+default: cn: shared
+
+dn: cn=users,cn=vaults,$SUFFIX
+default: objectClass: top
+default: objectClass: nsContainer
+default: cn: users
diff --git a/install/updates/Makefile.am b/install/updates/Makefile.am
index 0d63d9ea8d85f1add5f036e7a39f89543586d33b..66f6b9d37971f8b8501d73fc6ddca21b6686ff4b 100644
--- a/install/updates/Makefile.am
+++ b/install/updates/Makefile.am
@@ -33,6 +33,7 @@ app_DATA =				\
 	40-dns.update			\
 	40-automember.update		\
 	40-otp.update			\
+	40-vault.update			\
 	45-roles.update			\
 	50-7_bit_check.update	        \
 	50-dogtag10-migration.update	\
diff --git a/ipa-client/man/default.conf.5 b/ipa-client/man/default.conf.5
index dbc8a5b4647439de4de7c01152d098eb0561e236..0973f1a07179ad64daa326a02803cdc9ba1870aa 100644
--- a/ipa-client/man/default.conf.5
+++ b/ipa-client/man/default.conf.5
@@ -221,6 +221,7 @@ The following define the containers for the IPA server. Containers define where
   container_sudocmdgroup: cn=sudocmdgroups,cn=sudo
   container_sudorule: cn=sudorules,cn=sudo
   container_user: cn=users,cn=accounts
+  container_vault: cn=vaults
   container_virtual: cn=virtual operations,cn=etc
 
 .SH "FILES"
diff --git a/ipalib/constants.py b/ipalib/constants.py
index f1e14702ffdf5a3bd23a62b1fdd2ee3cd95d84f8..195938a355d1b24c02aa0a5833c1725c76e85c76 100644
--- a/ipalib/constants.py
+++ b/ipalib/constants.py
@@ -99,6 +99,7 @@ DEFAULT_CONFIG = (
     ('container_hbacservice', DN(('cn', 'hbacservices'), ('cn', 'hbac'))),
     ('container_hbacservicegroup', DN(('cn', 'hbacservicegroups'), ('cn', 'hbac'))),
     ('container_dns', DN(('cn', 'dns'))),
+    ('container_vault', DN(('cn', 'vaults'))),
     ('container_virtual', DN(('cn', 'virtual operations'), ('cn', 'etc'))),
     ('container_sudorule', DN(('cn', 'sudorules'), ('cn', 'sudo'))),
     ('container_sudocmd', DN(('cn', 'sudocmds'), ('cn', 'sudo'))),
diff --git a/ipalib/plugins/vault.py b/ipalib/plugins/vault.py
new file mode 100644
index 0000000000000000000000000000000000000000..598ed645a30f12bceecf6ca9be32bbbb320863f5
--- /dev/null
+++ b/ipalib/plugins/vault.py
@@ -0,0 +1,307 @@
+# Authors:
+#   Endi S. Dewata <edewata at redhat.com>
+#
+# Copyright (C) 2015  Red Hat
+# see file 'COPYING' for use and warranty information
+#
+# This program 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.
+#
+# This program 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 this program.  If not, see <http://www.gnu.org/licenses/>.
+
+from ipalib import api, errors
+from ipalib import Str, Flag
+from ipalib import output
+from ipalib.plugable import Registry
+from ipalib.plugins.baseldap import LDAPObject, LDAPCreate, LDAPDelete,\
+    LDAPSearch, LDAPUpdate, LDAPRetrieve
+from ipalib.request import context
+from ipalib.plugins.user import split_principal
+from ipalib import _, ngettext
+from ipapython.dn import DN
+
+__doc__ = _("""
+Vaults
+""") + _("""
+Manage vaults.
+""") + _("""
+EXAMPLES:
+""") + _("""
+ List private vaults:
+   ipa vault-find
+""") + _("""
+ List service vaults:
+   ipa vault-find --service <service name>
+""") + _("""
+ List shared vaults:
+   ipa vault-find --shared
+""") + _("""
+ List user vaults:
+   ipa vault-find --user <username>
+""") + _("""
+ Add a private vault:
+   ipa vault-add <name>
+""") + _("""
+ Add a service vault:
+   ipa vault-add <name> --service <service name>
+""") + _("""
+ Add a shared vault:
+   ipa vault-add <ame> --shared
+""") + _("""
+ Add a user vault:
+   ipa vault-add <name> --user <username>
+""") + _("""
+ Show a private vault:
+   ipa vault-show <name>
+""") + _("""
+ Show a service vault:
+   ipa vault-show <name> --service <service name>
+""") + _("""
+ Show a shared vault:
+   ipa vault-show <name> --shared
+""") + _("""
+ Show a user vault:
+   ipa vault-show <name> --user <username>
+""") + _("""
+ Modify a private vault:
+   ipa vault-mod <name> --desc <description>
+""") + _("""
+ Modify a service vault:
+   ipa vault-mod <name> --service <service name> --desc <description>
+""") + _("""
+ Modify a shared vault:
+   ipa vault-mod <name> --shared --desc <description>
+""") + _("""
+ Modify a user vault:
+   ipa vault-mod <name> --user <username> --desc <description>
+""") + _("""
+ Delete a private vault:
+   ipa vault-del <name>
+""") + _("""
+ Delete a service vault:
+   ipa vault-del <name> --service <service name>
+""") + _("""
+ Delete a shared vault:
+   ipa vault-del <name> --shared
+""") + _("""
+ Delete a user vault:
+   ipa vault-del <name> --user <username>
+""")
+
+register = Registry()
+
+
+vault_options = (
+    Str(
+        'service?',
+        doc=_('Service name'),
+    ),
+    Flag(
+        'shared?',
+        doc=_('Shared vault'),
+    ),
+    Str(
+        'user?',
+        doc=_('Username'),
+    ),
+)
+
+
+ at register()
+class vault(LDAPObject):
+    __doc__ = _("""
+    Vault object.
+    """)
+
+    container_dn = api.env.container_vault
+    base_dn = DN(container_dn, api.env.basedn)
+
+    object_name = _('vault')
+    object_name_plural = _('vaults')
+
+    object_class = ['ipaVault']
+    default_attributes = [
+        'cn',
+        'description',
+    ]
+
+    label = _('Vaults')
+    label_singular = _('Vault')
+
+    takes_params = (
+        Str(
+            'cn',
+            cli_name='name',
+            label=_('Vault name'),
+            primary_key=True,
+            pattern='^[a-zA-Z0-9_.-]+$',
+            pattern_errmsg='may only include letters, numbers, _, ., and -',
+            maxlength=255,
+        ),
+        Str(
+            'description?',
+            cli_name='desc',
+            label=_('Description'),
+            doc=_('Vault description'),
+        ),
+    )
+
+    def get_dn(self, *keys, **options):
+        """
+        Generates vault DN from parameters.
+        """
+
+        service = options.get('service')
+        shared = options.get('shared')
+        user = options.get('user')
+
+        count = 0
+        if service:
+            count += 1
+
+        if shared:
+            count += 1
+
+        if user:
+            count += 1
+
+        if count > 1:
+            raise errors.MutuallyExclusiveError(
+                reason=_('Service, shared, and user options ' +
+                         'cannot be specified simultaneously'))
+
+        dn = super(vault, self).get_dn(*keys, **options)
+        assert dn.endswith(self.base_dn)
+        rdns = DN(*dn[:-len(self.base_dn)])
+
+        if not count:
+            principal = getattr(context, 'principal')
+            (name, realm) = split_principal(principal)
+            if '/' in name:
+                service = name
+            else:
+                user = name
+
+        if service:
+            parent_dn = DN(('cn', service), ('cn', 'services'), self.base_dn)
+        elif shared:
+            parent_dn = DN(('cn', 'shared'), self.base_dn)
+        else:
+            parent_dn = DN(('cn', user), ('cn', 'users'), self.base_dn)
+
+        return DN(rdns, parent_dn)
+
+    def create_container(self, dn):
+        """
+        Creates vault container and its parents.
+        """
+
+        assert dn.endswith(self.base_dn)
+
+        rdn = dn[0]
+        entry = self.backend.make_entry(
+            dn,
+            {
+                'objectclass': ['nsContainer'],
+                'cn': rdn['cn'],
+            })
+
+        # if entry can be added, return
+        try:
+            self.backend.add_entry(entry)
+            return
+
+        except errors.NotFound:
+            pass
+
+        # otherwise, create parent entry first
+        parent_dn = DN(*dn[1:])
+        self.create_container(parent_dn)
+
+        # then create the entry itself again
+        self.backend.add_entry(entry)
+
+
+ at register()
+class vault_add(LDAPCreate):
+    __doc__ = _('Create a new vault.')
+
+    takes_options = LDAPCreate.takes_options + vault_options
+
+    msg_summary = _('Added vault "%(value)s"')
+
+    def pre_callback(self, ldap, dn, entry_attrs, attrs_list, *keys,
+                     **options):
+        assert isinstance(dn, DN)
+
+        try:
+            parent_dn = DN(*dn[1:])
+            self.obj.create_container(parent_dn)
+        except errors.DuplicateEntry, e:
+            pass
+
+        return dn
+
+
+ at register()
+class vault_del(LDAPDelete):
+    __doc__ = _('Delete a vault.')
+
+    takes_options = LDAPDelete.takes_options + vault_options
+
+    msg_summary = _('Deleted vault "%(value)s"')
+
+
+ at register()
+class vault_find(LDAPSearch):
+    __doc__ = _('Search for vaults.')
+
+    takes_options = LDAPSearch.takes_options + vault_options
+
+    msg_summary = ngettext(
+        '%(count)d vault matched',
+        '%(count)d vaults matched',
+        0,
+    )
+
+    def pre_callback(self, ldap, filter, attrs_list, base_dn, scope, *args,
+                     **options):
+        assert isinstance(base_dn, DN)
+
+        base_dn = self.obj.get_dn(*args, **options)
+
+        return (filter, base_dn, scope)
+
+    def exc_callback(self, args, options, exc, call_func, *call_args,
+                     **call_kwargs):
+        if call_func.__name__ == 'find_entries':
+            if isinstance(exc, errors.NotFound):
+                # ignore missing containers since they will be created
+                # automatically on vault creation.
+                raise errors.EmptyResult(reason=str(exc))
+
+        raise exc
+
+
+ at register()
+class vault_mod(LDAPUpdate):
+    __doc__ = _('Modify a vault.')
+
+    takes_options = LDAPUpdate.takes_options + vault_options
+
+    msg_summary = _('Modified vault "%(value)s"')
+
+
+ at register()
+class vault_show(LDAPRetrieve):
+    __doc__ = _('Display information about a vault.')
+
+    takes_options = LDAPRetrieve.takes_options + vault_options
diff --git a/ipatests/test_xmlrpc/test_vault_plugin.py b/ipatests/test_xmlrpc/test_vault_plugin.py
new file mode 100644
index 0000000000000000000000000000000000000000..44d397c583928d98ec252899398ae6c3a83c207c
--- /dev/null
+++ b/ipatests/test_xmlrpc/test_vault_plugin.py
@@ -0,0 +1,445 @@
+# Authors:
+#   Endi S. Dewata <edewata at redhat.com>
+#
+# Copyright (C) 2015  Red Hat
+# see file 'COPYING' for use and warranty information
+#
+# This program 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.
+#
+# This program 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 this program.  If not, see <http://www.gnu.org/licenses/>.
+
+"""
+Test the `ipalib/plugins/vault.py` module.
+"""
+
+from ipalib import api, errors
+from xmlrpc_test import Declarative, fuzzy_string
+
+vault_name = u'test_vault'
+service_name = u'HTTP/server.example.com'
+user_name = u'testuser'
+
+
+class test_vault_plugin(Declarative):
+
+    cleanup_commands = [
+        ('vault_del', [vault_name], {'continue': True}),
+        ('vault_del', [vault_name], {
+            'service': service_name,
+            'continue': True
+        }),
+        ('vault_del', [vault_name], {'shared': True, 'continue': True}),
+        ('vault_del', [vault_name], {'user': user_name, 'continue': True}),
+    ]
+
+    tests = [
+
+        {
+            'desc': 'Create private vault',
+            'command': (
+                'vault_add',
+                [vault_name],
+                {},
+            ),
+            'expected': {
+                'value': vault_name,
+                'summary': 'Added vault "%s"' % vault_name,
+                'result': {
+                    'dn': u'cn=%s,cn=admin,cn=users,cn=vaults,%s'
+                          % (vault_name, api.env.basedn),
+                    'objectclass': [u'top', u'ipaVault'],
+                    'cn': [vault_name],
+                },
+            },
+        },
+
+        {
+            'desc': 'Find private vaults',
+            'command': (
+                'vault_find',
+                [],
+                {},
+            ),
+            'expected': {
+                'count': 1,
+                'truncated': False,
+                'summary': u'1 vault matched',
+                'result': [
+                    {
+                        'dn': u'cn=%s,cn=admin,cn=users,cn=vaults,%s'
+                              % (vault_name, api.env.basedn),
+                        'cn': [vault_name],
+                    },
+                ],
+            },
+        },
+
+        {
+            'desc': 'Show private vault',
+            'command': (
+                'vault_show',
+                [vault_name],
+                {},
+            ),
+            'expected': {
+                'value': vault_name,
+                'summary': None,
+                'result': {
+                    'dn': u'cn=%s,cn=admin,cn=users,cn=vaults,%s'
+                          % (vault_name, api.env.basedn),
+                    'cn': [vault_name],
+                },
+            },
+        },
+
+        {
+            'desc': 'Modify private vault',
+            'command': (
+                'vault_mod',
+                [vault_name],
+                {
+                    'description': u'Test vault',
+                },
+            ),
+            'expected': {
+                'value': vault_name,
+                'summary': u'Modified vault "%s"' % vault_name,
+                'result': {
+                    'cn': [vault_name],
+                    'description': [u'Test vault'],
+                },
+            },
+        },
+
+        {
+            'desc': 'Delete private vault',
+            'command': (
+                'vault_del',
+                [vault_name],
+                {},
+            ),
+            'expected': {
+                'value': [vault_name],
+                'summary': u'Deleted vault "%s"' % vault_name,
+                'result': {
+                    'failed': (),
+                },
+            },
+        },
+
+        {
+            'desc': 'Create service vault',
+            'command': (
+                'vault_add',
+                [vault_name],
+                {
+                    'service': service_name,
+                },
+            ),
+            'expected': {
+                'value': vault_name,
+                'summary': u'Added vault "%s"' % vault_name,
+                'result': {
+                    'dn': u'cn=%s,cn=%s,cn=services,cn=vaults,%s'
+                          % (vault_name, service_name, api.env.basedn),
+                    'objectclass': [u'top', u'ipaVault'],
+                    'cn': [vault_name],
+                },
+            },
+        },
+
+        {
+            'desc': 'Find service vaults',
+            'command': (
+                'vault_find',
+                [],
+                {
+                    'service': service_name,
+                },
+            ),
+            'expected': {
+                'count': 1,
+                'truncated': False,
+                'summary': u'1 vault matched',
+                'result': [
+                    {
+                        'dn': u'cn=%s,cn=%s,cn=services,cn=vaults,%s'
+                              % (vault_name, service_name, api.env.basedn),
+                        'cn': [vault_name],
+                    },
+                ],
+            },
+        },
+
+        {
+            'desc': 'Show service vault',
+            'command': (
+                'vault_show',
+                [vault_name],
+                {
+                    'service': service_name,
+                },
+            ),
+            'expected': {
+                'value': vault_name,
+                'summary': None,
+                'result': {
+                    'dn': u'cn=%s,cn=%s,cn=services,cn=vaults,%s'
+                          % (vault_name, service_name, api.env.basedn),
+                    'cn': [vault_name],
+                },
+            },
+        },
+
+        {
+            'desc': 'Modify service vault',
+            'command': (
+                'vault_mod',
+                [vault_name],
+                {
+                    'service': service_name,
+                    'description': u'Test vault',
+                },
+            ),
+            'expected': {
+                'value': vault_name,
+                'summary': u'Modified vault "%s"' % vault_name,
+                'result': {
+                    'cn': [vault_name],
+                    'description': [u'Test vault'],
+                },
+            },
+        },
+
+        {
+            'desc': 'Delete service vault',
+            'command': (
+                'vault_del',
+                [vault_name],
+                {
+                    'service': service_name,
+                },
+            ),
+            'expected': {
+                'value': [vault_name],
+                'summary': u'Deleted vault "%s"' % vault_name,
+                'result': {
+                    'failed': (),
+                },
+            },
+        },
+
+        {
+            'desc': 'Create shared vault',
+            'command': (
+                'vault_add',
+                [vault_name],
+                {
+                    'shared': True
+                },
+            ),
+            'expected': {
+                'value': vault_name,
+                'summary': u'Added vault "%s"' % vault_name,
+                'result': {
+                    'dn': u'cn=%s,cn=shared,cn=vaults,%s'
+                          % (vault_name, api.env.basedn),
+                    'objectclass': [u'top', u'ipaVault'],
+                    'cn': [vault_name],
+                },
+            },
+        },
+
+        {
+            'desc': 'Find shared vaults',
+            'command': (
+                'vault_find',
+                [],
+                {
+                    'shared': True
+                },
+            ),
+            'expected': {
+                'count': 1,
+                'truncated': False,
+                'summary': u'1 vault matched',
+                'result': [
+                    {
+                        'dn': u'cn=%s,cn=shared,cn=vaults,%s'
+                              % (vault_name, api.env.basedn),
+                        'cn': [vault_name],
+                    },
+                ],
+            },
+        },
+
+        {
+            'desc': 'Show shared vault',
+            'command': (
+                'vault_show',
+                [vault_name],
+                {
+                    'shared': True
+                },
+            ),
+            'expected': {
+                'value': vault_name,
+                'summary': None,
+                'result': {
+                    'dn': u'cn=%s,cn=shared,cn=vaults,%s'
+                          % (vault_name, api.env.basedn),
+                    'cn': [vault_name],
+                },
+            },
+        },
+
+        {
+            'desc': 'Modify shared vault',
+            'command': (
+                'vault_mod',
+                [vault_name],
+                {
+                    'shared': True,
+                    'description': u'Test vault',
+                },
+            ),
+            'expected': {
+                'value': vault_name,
+                'summary': u'Modified vault "%s"' % vault_name,
+                'result': {
+                    'cn': [vault_name],
+                    'description': [u'Test vault'],
+                },
+            },
+        },
+
+        {
+            'desc': 'Delete shared vault',
+            'command': (
+                'vault_del',
+                [vault_name],
+                {
+                    'shared': True
+                },
+            ),
+            'expected': {
+                'value': [vault_name],
+                'summary': u'Deleted vault "%s"' % vault_name,
+                'result': {
+                    'failed': (),
+                },
+            },
+        },
+
+        {
+            'desc': 'Create user vault',
+            'command': (
+                'vault_add',
+                [vault_name],
+                {
+                    'user': user_name,
+                },
+            ),
+            'expected': {
+                'value': vault_name,
+                'summary': u'Added vault "%s"' % vault_name,
+                'result': {
+                    'dn': u'cn=%s,cn=%s,cn=users,cn=vaults,%s'
+                          % (vault_name, user_name, api.env.basedn),
+                    'objectclass': [u'top', u'ipaVault'],
+                    'cn': [vault_name],
+                },
+            },
+        },
+
+        {
+            'desc': 'Find user vaults',
+            'command': (
+                'vault_find',
+                [],
+                {
+                    'user': user_name,
+                },
+            ),
+            'expected': {
+                'count': 1,
+                'truncated': False,
+                'summary': u'1 vault matched',
+                'result': [
+                    {
+                        'dn': u'cn=%s,cn=%s,cn=users,cn=vaults,%s'
+                              % (vault_name, user_name, api.env.basedn),
+                        'cn': [vault_name],
+                    },
+                ],
+            },
+        },
+
+        {
+            'desc': 'Show user vault',
+            'command': (
+                'vault_show',
+                [vault_name],
+                {
+                    'user': user_name,
+                },
+            ),
+            'expected': {
+                'value': vault_name,
+                'summary': None,
+                'result': {
+                    'dn': u'cn=%s,cn=%s,cn=users,cn=vaults,%s'
+                          % (vault_name, user_name, api.env.basedn),
+                    'cn': [vault_name],
+                },
+            },
+        },
+
+        {
+            'desc': 'Modify user vault',
+            'command': (
+                'vault_mod',
+                [vault_name],
+                {
+                    'user': user_name,
+                    'description': u'Test vault',
+                },
+            ),
+            'expected': {
+                'value': vault_name,
+                'summary': u'Modified vault "%s"' % vault_name,
+                'result': {
+                    'cn': [vault_name],
+                    'description': [u'Test vault'],
+                },
+            },
+        },
+
+        {
+            'desc': 'Delete user vault',
+            'command': (
+                'vault_del',
+                [vault_name],
+                {
+                    'user': user_name,
+                },
+            ),
+            'expected': {
+                'value': [vault_name],
+                'summary': u'Deleted vault "%s"' % vault_name,
+                'result': {
+                    'failed': (),
+                },
+            },
+        },
+
+    ]
-- 
1.9.3



More information about the Freeipa-devel mailing list