[Freeipa-devel] [PATCH] Password vault

Endi Sukma Dewata edewata at redhat.com
Fri Apr 3 03:37:05 UTC 2015


Hi,

Attached are new patches replacing all old ones. Please take a look at 
them. They should applied in this order: 365, 353-8, 355-6, 357-3, 
359-2, 360-1, 364-1, 361-1.

I'm planning to merge the vault and vault container object and use the 
vault type attribute to distinguish between the two. See more discussion 
about that below.

On 3/23/2015 5:07 AM, Jan Cholasta wrote:
>>> Patch 353:
>>>
>>> 1) Please follow PEP8 in new code.
>>>
> Well, I did not use pylint, but pep8:
> <http://pep8.readthedocs.org/en/latest/>
>
>> Is there an existing ticket for fixing PEP8 errors? Let's use that for
>> fixing the errors in the existing code.
>
> There is no ticket, but we still follow PEP8 in new code, so please do
> that. It shouldn't be too hard.

Fixed.

>>> 3) The container_vault config option should be renamed to
>>> container_vaultcontainer, as it is used in the vaultcontainer plugin,
>>> not the vault plugin.
>>
>> It was named container_vault because it defines the DN for of the
>> subtree that contains all vault-related entries. I moved the base_dn
>> variable from vaultcontainer object to the vault object for clarity.
>
> That does not make much sense to me. Vault objects are contained in
> their respective vaultcontainer objects, not directly in cn=vaults.

The cn=vaults itself is actually a vault container (i.e. 
ipaVaultContainer). Theoretically you could store a vault in any 
container including cn=vaults, but we just don't want people to use it 
that way.

I think this is consistent with other plugins. For example, the 
container_user points to cn=users, which is an nsContainer. There is no 
concept of 'user container' other than the cn=users itself. But even if 
there is one, the container_user will still be stored in the user class.

When the vault & vaultcontainer is merged, this will no longer be an issue.

>>> 4) The vault object should be child of the vaultcontainer object.
>>>
>>> Not only is this correct from the object model perspective, but it would
>>> also make all the container_id hacks go away.
>>
>> It's a bit difficult because it will affect how the container & vault
>> ID's are represented on the CLI.
>
> Yes, but the API should be done right (without hacks) first. You can
> tune the CLI after that if you want.

I think the current framework is rather limiting. It's kind of hard to 
build an interface that looks exactly what you want then add the 
implementation later to match the interface because many things are 
interrelated. In this particular case the object hierarchy on the server 
side would affect how the vault ID will be represented on the client side.

>> In the design the container ID would be a single value like this:
>>
>>    $ ipa vault-add /services/server.example.com/HTTP
>>
>> And if the vault ID is relative (without initial slash), it will be
>> appended to the user's private container (i.e. /users/<username>/):
>>
>>    $ ipa vault-add PrivateVault
>>
>> The implementation is not complete yet. Currently it accepts this format:
>>
>>    $ ipa vault-add <vault name> [--container <container ID>]
>>
>> and I'm still planning to add this:
>>
>>    $ ipa vault-add <vault ID>

This is actually now done in the latest patch. Internally the ID is 
still split into name & parent ID.

>> If the vault must be a child of vaultcontainer, and the vaultcontainer
>> must be a child of a vaultcontainer, does it mean the vault ID would
>> have to be split into separate arguments like this?
>>
>>    $ ipa vaultcontainer-add services server.example.com HTTP
>>
>> If that's the case we'd lose the ability to specify a relative vault ID.
>
> Yes, that's the case.
>
> But I don't think relative IDs should be a problem, we can do this:
>
>      $ ipa vaultcontainer-add a b c  # absolute
>      $ ipa vaultcontainer-add . c    # relative

I think a "." will be confusing because there's no concept of "current 
vaultcontainer" like "current directory".

> or this:
>
>      $ ipa vaultcontainer-add '' a b c  # absolute
>      $ ipa vaultcontainer-add c         # relative

An empty string is also confusing and can be problematic to distinguish 
with missing argument.

> or this:
>
>      $ ipa vaultcontainer-add a b c         # absolute
>      $ ipa vaultcontainer-add c --relative  # relative
>
> or this:
>
>      $ ipa vaultcontainer-add a b c --absolute  # absolute
>      $ ipa vaultcontainer-add c                 # relative

Per discussion in the IPA-CS meeting, we'd rather keep the "/" for vault 
ID delimiters because the spaces will be confusing to users, but we'll 
not use absolute ID anymore.

It's not implemented yet, but here is the plan. By default the vault 
will be created in the user's private container:

   $ ipa vault-add PrivateVault

For shared vaults, instead of specifying an absolute ID we can specify a 
--shared option:

   $ ipa vault-add --shared projects/IPA

Same thing with service vaults:

   $ ipa vault-add --service server.example.com/LDAP

To access a vault in another user's private container:

   $ ipa vault-show --user testuser PrivateVault

>>> 11) No clever optimizations like this please:
>>>
>>> +        # vault DN cannot be the container base DN
>>> +        if len(dn) == len(api.Object.vaultcontainer.base_dn):
>>> +            raise ValueError('Invalid vault DN: %s' % dn)
>>>
>>> Compare the DNs by value instead.
>>
>> Actually the DN values have already been compared in the code right
>> above it:
>>
>>      # make sure the DN is a vault DN
>>      if not dn.endswith(self.api.Object.vaultcontainer.base_dn):
>>          raise ValueError('Invalid vault DN: %s' % dn)
>>
>> This code confirms that the incoming vault DN is within the vault
>> subtree. After that, the DN length comparison above is just to make sure
>> the incoming vault DN is not the root of the vault subtree itself. It
>> doesn't need to compare the values again.
>
> I see. You can combine both of the checks into one:
>
>      if not dn.endswith(self.api.Object.vaultcontainer.base_dn, 1):
>          raise ValueError(...)

Changed, but this might not be necessary once the vault & vaultcontainer 
are merged.

>> 6) The `container` param of vault should actually be an option in
>> vault_* commands.
>>
>> Also it should be renamed to `container_id`, for consistency with
>> vaultcontainer.
>
> Fixed. It was actually made to be consistent with the 'parent'
> attribute in the vaultcontainer class. Now the 'parent' has been
> renamed to 'parent_id' as well.

Since we're going to merge vault & vaultcontainer, I've renamed vault's 
container_id to parent_id.

>>> 14) Use File instead of Str for input files:
>>>
>>> +        Str('in?',
>>> +            cli_name='in',
>>> +            doc=_('File containing data to archive'),
>>> +        ),
>>
>> The File type doesn't work with binary files because it tries to decode
>> the content.
>
> OK. I know File is broken and plan to fix it in the future. Just add a
> comment saying that it should be a File, but it's broken, OK?

Added the notes.

>>> 16) You do way too much stuff in vault_add.forward(). Only code that
>>> must be done on the client needs to be there, i.e. handling of the
>>> "data", "text" and "in" options.
>>>
>>> The vault_archive call must be in vault_add.execute(), otherwise a) we
>>> will be making 2 RPC calls from the client and b) it won't be called at
>>> all when api.env.in_server is True.
>>
>> This is done by design. The vault_add.forward() generates the salt and
>> the keys. The vault_archive.forward() will encrypt the data. These
>> operations have to be done on the client side to secure the transport of
>> the data from the client through the server and finally to KRA. This
>> mechanism prevents the server from looking at the unencrypted data.
>
> OK, but that does not justify that it's broken in server-side API. It
> can and should be done so that it works the same way on both client and
> server.
>
> I think the best solution would be to split the command into two
> commands, server-side vault_archive_raw to archive already encrypted
> data, and client-side vault_archive to encrypt data and archive them
> with vault_archive_raw in its .execute(). Same thing for vault_retrieve.

Actually I think it's better to just merge the add and archive, reducing 
the number of RPC calls. The vault_archive now will support two types of 
operations:

(a) Archive data into a new vault (it will create the vault just before 
archiving the data):

   $ ipa vault-archive testvault --create --in data ...

(b) Archive data into an existing vault:

   $ ipa vault-archive testvault --in data ...

The vault_add is now just a wrapper for the vault_archive(a).

> BTW, I also think it would be better if there were 2 separate sets of
> commands for binary and textual data
> (vault_{archive,retrieve}_{data,text}) rather than trying to handle
> everything in vault_{archive,retrieve}.

I don't think we want to provide a separate command of every possible 
data type & operation combination. Users would get confused. The archive 
& retrieve commands should be able to handle all current & future data 
types with options.

>> The add & archive combination was added for convenience, not for
>> optimization. This way you would be able to archive data into a new
>> vault using a single command. Without this, you'd have to execute two
>> separate commands: add & archive, which will result in 2 RPC calls
>> anyway.
>
> I think I would prefer if it was separate, as that would be consistent
> with other plugins (e.g. for objects with members, we don't allow adding
> members directly in -add, you have to use -add-member after -add).

The vault data is similar to group description, not group members. When 
creating a group we can supply the description. If not specified it will 
be blank. Archiving vault data is similar to updating the group description.

Vault secrets on the other hand is similar to group members. You will 
see that in the other patch.

>>> 17) Why are vaultcontainer objects automatically created in vault_add?
>>>
>>> If you have to automatically create them, you also have to automatically
>>> delete them when the command fails. But that's a hassle, so I would just
>>> not create them automatically.
>>
>> The vaultcontainer is created automatically to provide a private
>> container (i.e. /users/<username>/) for the each user if they need it.
>> Without this, the admin will have to create the container manually first
>> before a user can create a vault, which would be an unreasonable
>> requirement. If the vault_add fails, it's ok to leave the private
>> container intact because it can be used again if the user tries to
>> create a vault again later and it will not affect other users. If the
>> user is deleted, the private container will be deleted too.
>>
>> The code was fixed to create the container only if they are adding a
>> vault/vault container into the user's private container. If they are
>> adding into other container, the container must already exist.
>
> This sounds like a job fit for the managed entries plugin. Have you
> tried using it for this?

I'm not that familiar with that yet. I think we can make that 
improvement later once we know if it fits the needs. Right now we'll 
keep it simple.

>>> 18) Why are vaultcontainer objects automatically created in vault_find?
>>>
>>> This is just plain wrong and has to be removed, now.
>>
>> The code was supposed to create the user's private container like in
>> #17, but the behavior has been changed. If the container being searched
>> is the user's private container, it will ignore the container not found
>> error and return zero results as if the private container already
>> exists. For other containers the container must already exist. For this
>> to work I had to add a handle_not_found() into LDAPSearch so the plugins
>> can customize the proper search response for the missing private
>> container.
>
> No ad-hoc refactoring please. If you want to refactor anything, it
> should be first designed properly and put in a separate patch.
>
> Anyway, what should actually happen here is that if parent object is not
> found, its object plugin's handle_not_found is called, i.e. something
> like this:
>
>      parent = self.obj.parent_object
>      if parent:
>          self.api.Object[parent].handle_not_found(*args[:-1])
>      else:
>          raise errors.NotFound(
>              reason=self.obj.container_not_found_msg % {
>                  'container': self.obj.container_dn,
>              }
>          )

It will not work because vault doesn't have a parent object. I'm adding 
handle_not_found() into LDAPCreate and LDAPSearch in the first patch.

>>> 21) vault_archive is not a retrieve operation, it should be based on
>>> LDAPUpdate instead of LDAPRetrieve. Or Command actually, since it does
>>> not do anything with LDAP. The same applies to vault_retrieve.
>>
>> The vault_archive does not actually modify the LDAP entry because it
>> stores the data in KRA. It is actually an LDAPRetrieve operation because
>> it needs to get the vault info before it can perform the archival
>> operation. Same thing with vault_retrieve.
>
> It is not a LDAPRetrieve operation, because it has different semantics.
> Please use Command as base class and either use ldap2 for direct LDAP or
> call vault_show instead of hacking around LDAPRetrieve.

It's been changed to inherit from LDAPQuery instead.

>>> 22) vault_archive will break with binary data that is not UTF-8 encoded
>>> text.
>>>
>>> This is where it occurs:
>>>
>>> +        vault_data[u'data'] = unicode(data)
>>>
>>> Generally, don't use unicode() on str values and str() on unicode values
>>> directly, always use .decode() and .encode().

The unicode(s, encoding) is actually equivalent to s.decode(encoding), 
so the following code will not solve the problem:

   vault_data[u'data'] = data.decode()

As you said, decode() will only work if the data being decoded actually 
follows the encoding rules (i.e. already UTF-8 encoded).

>> It needs to be a Unicode because json.dumps() doesn't work with binary
>> data. Fixed by adding base-64 encoding.

The base-64 encoding is necessary to convert random binaries into ASCII 
so it can be decoded into Unicode. Here is the current code:

   vault_data[u'data'] = unicode(base64.b64encode(data))

which is equivalent to:

   vault_data[u'data'] = base64.b64encode(data).decode()

> If something str needs to be unicode, you should use .decode() to
> explicitly specify the encoding, instead of relying on unicode() to pick
> the correct one.

Since we know this is ASCII data we can now specify UTF-8 encoding.

   vault_data[u'data'] = base64.b64encode(data).decode('utf-8')

But for anything that comes from user input (e.g. filenames, passwords), 
it's better to use the default encoding because that can be configured 
by the user.

> Anyway, I think a better solution than base64 would be to use the
> "raw_unicode_escape" encoding:

As explained above, base-64 encoding is necessary because random 
binaries don't follow any encoding rules. I'd rather not use 
raw_unicode_escape because it's not really a text data. Here's how it's 
now implemented:

>      if data:
>          data = data.decode('raw_unicode_escape')

Input data is already in binaries, no conversion needed.

>      elif text:
>          data = text

Input text will be converted to binaries with default encoding:

   data = text.encode()

>      elif input_file:
>          with open(input_file, 'rb') as f:
>              data = f.read()
>          data = data.decode('raw_unicode_escape')

Input contains binary data, no conversion needed.

>      else:
>          data = u''

If not specified, the data will be empty string:

   data = ''

The data needs to be converted into binaries so it can be encrypted 
before transport (depending on the vault type):

   data = self.obj.encrypt(data, ...)

>      vault_data[u'data'] = data

Then for transport the data is base-64 encoded first, then converted 
into Unicode:

   vault_data[u'data'] = base64.b64encode(data).decode('utf-8')

>>> 26) Instead of the delete_entry refactoring in baseldap and
>>> vaultcontainer_add, you can put this in vaultcontainer_add's
>>> pre_callback:
>>>
>>>      try:
>>>          ldap.get_entries(dn, scope=ldap.SCOPE_ONELEVEL, attrs_list=[])
>>>      except errors.NotFound:
>>>          pass
>>>      else:
>>>          if not options.get('force', False):
>>>              raise errors.NotAllowedOnNonLeaf()
>>
>> I suppose you meant vaultcontainer_del. Fixed, but this will generate an
>> additional search for each delete.
>>
>> I'm leaving the changes baseldap because it may be useful later and it
>> doesn't change the behavior of the current code.
>
> Again, no ad-hoc refactoring please.

The refactoring has also been moved into a separate patch. Just a note, 
I still don't think a plugin should do a search and maybe generate a 
NotAllowedOnLeaf exception on each delete operation. The exception 
should have been generated automatically by the DS. But we can discuss 
that separately.

>>> 28) The vault and vaultcontainer plugins seem to be pretty similar, I
>>> think it would make sense to put common stuff in a base class and
>>> inherit vault and vaultcontainer from that.
>>
>> I plan to refactor the common code later. Right now the focus is to get
>> the functionality working correctly first.
>
> Please do it now, "later" usually means "never". It shouldn't be too
> hard and I can give you a hand with it if you want.

As mentioned above, I'm considering merging the vault & vault container 
classes, so no need to refactor the common code out of these classes. 
This will be delivered as a separate patch later.

Thanks.

-- 
Endi S. Dewata
-------------- next part --------------
>From 7003767c159e8c75fa02ff5c3ffde971ff665ef1 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 initial vault implementation.

The initial implementation of vaults and vault containers
has been added. Some test scripts have been added as well.
The remaining functionalities will be added in subsequent
patches.

https://fedorahosted.org/freeipa/ticket/3872
---
 API.txt                                            | 177 +++-
 install/share/60basev3.ldif                        |   2 +
 install/updates/40-vault.update                    |  23 +
 install/updates/Makefile.am                        |   1 +
 ipa-client/man/default.conf.5                      |   1 +
 ipalib/constants.py                                |   1 +
 ipalib/plugins/user.py                             |  43 +-
 ipalib/plugins/vault.py                            | 956 +++++++++++++++++++++
 ipalib/plugins/vaultcontainer.py                   | 503 +++++++++++
 ipatests/test_xmlrpc/test_vault_plugin.py          | 394 +++++++++
 ipatests/test_xmlrpc/test_vaultcontainer_plugin.py | 436 ++++++++++
 11 files changed, 2533 insertions(+), 4 deletions(-)
 create mode 100644 install/updates/40-vault.update
 create mode 100644 ipalib/plugins/vault.py
 create mode 100644 ipalib/plugins/vaultcontainer.py
 create mode 100644 ipatests/test_xmlrpc/test_vault_plugin.py
 create mode 100644 ipatests/test_xmlrpc/test_vaultcontainer_plugin.py

diff --git a/API.txt b/API.txt
index 0c7eda9f5b9176aa6e97ef03f26b0bf0a885fe4e..ebae899fe93fd742b052de9ca5e63ad567c437ef 100644
--- a/API.txt
+++ b/API.txt
@@ -4351,9 +4351,10 @@ output: Entry('result', <type 'dict'>, Gettext('A dictionary representing an LDA
 output: Output('summary', (<type 'unicode'>, <type 'NoneType'>), None)
 output: PrimaryKey('value', None, None)
 command: user_del
-args: 1,2,3
+args: 1,3,3
 arg: Str('uid', attribute=True, cli_name='login', maxlength=255, multivalue=True, pattern='^[a-zA-Z0-9_.][a-zA-Z0-9_.-]{0,252}[a-zA-Z0-9_.$-]?$', primary_key=True, query=True, required=True)
 option: Flag('continue', autofill=True, cli_name='continue', default=False)
+option: Flag('force?', autofill=True, default=False)
 option: Str('version?', exclude='webui')
 output: Output('result', <type 'dict'>, None)
 output: Output('summary', (<type 'unicode'>, <type 'NoneType'>), None)
@@ -4513,6 +4514,180 @@ 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='vault_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: Bytes('data?', cli_name='data')
+option: Str('description?', cli_name='desc')
+option: Str('in?', cli_name='in')
+option: Str('ipavaulttype?', autofill=True, cli_name='type', default=u'standard')
+option: Str('parent_id?', cli_name='parent_id')
+option: Flag('raw', autofill=True, cli_name='raw', default=False, exclude='webui')
+option: Str('text?', cli_name='text')
+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_archive
+args: 1,13,3
+arg: Str('cn', attribute=True, cli_name='vault_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('create?', autofill=True, default=False)
+option: Bytes('data?', cli_name='data')
+option: Str('description?', cli_name='desc')
+option: Str('in?', cli_name='in')
+option: Str('ipavaulttype?', autofill=True, cli_name='type', default=u'standard')
+option: Str('nonce?', cli_name='nonce')
+option: Str('parent_id?', cli_name='parent_id')
+option: Flag('raw', autofill=True, cli_name='raw', default=False, exclude='webui')
+option: Str('session_key?', cli_name='session_key')
+option: Str('text?', cli_name='text')
+option: Str('vault_data?', cli_name='vault_data')
+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,3,3
+arg: Str('cn', attribute=True, cli_name='vault_name', maxlength=255, multivalue=False, pattern='^[a-zA-Z0-9_.-/]+$', primary_key=True, query=True, required=True)
+option: Flag('continue', autofill=True, cli_name='continue', default=False)
+option: Str('parent_id?', cli_name='parent_id')
+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,10,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='vault_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: Str('parent_id?', cli_name='parent_id')
+option: Flag('pkey_only?', autofill=True, default=False)
+option: Flag('raw', autofill=True, cli_name='raw', default=False, exclude='webui')
+option: Int('sizelimit?', autofill=False, minvalue=0)
+option: Int('timelimit?', autofill=False, minvalue=0)
+option: Str('vault_id', attribute=False, autofill=False, cli_name='vault_id', multivalue=False, query=True, required=False)
+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,10,3
+arg: Str('cn', attribute=True, cli_name='vault_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: Str('parent_id?', cli_name='parent_id')
+option: Flag('raw', autofill=True, cli_name='raw', default=False, exclude='webui')
+option: Flag('rights', autofill=True, default=False)
+option: Str('setattr*', cli_name='setattr', exclude='webui')
+option: Str('vault_id', attribute=False, autofill=False, cli_name='vault_id', multivalue=False, required=False)
+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_retrieve
+args: 1,8,3
+arg: Str('cn', attribute=True, cli_name='vault_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: Str('out?', cli_name='out')
+option: Str('parent_id?', cli_name='parent_id')
+option: Flag('raw', autofill=True, cli_name='raw', default=False, exclude='webui')
+option: Str('session_key?', cli_name='session_key')
+option: Flag('show_text?', autofill=True, default=False)
+option: Flag('stdout?', autofill=True, default=False)
+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,5,3
+arg: Str('cn', attribute=True, cli_name='vault_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: Str('parent_id?', cli_name='parent_id')
+option: Flag('raw', autofill=True, cli_name='raw', default=False, exclude='webui')
+option: Flag('rights', autofill=True, default=False)
+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_transport_cert
+args: 0,2,1
+option: Str('out?', cli_name='out')
+option: Str('version?', exclude='webui')
+output: Output('result', None, None)
+command: vaultcontainer_add
+args: 1,8,3
+arg: Str('cn', attribute=True, cli_name='container_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('container_id', attribute=False, cli_name='container_id', multivalue=False, required=False)
+option: Str('description', attribute=True, cli_name='desc', multivalue=False, required=False)
+option: Str('parent_id?', cli_name='parent_id')
+option: Flag('raw', autofill=True, cli_name='raw', default=False, exclude='webui')
+option: Str('setattr*', cli_name='setattr', exclude='webui')
+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: vaultcontainer_del
+args: 1,4,3
+arg: Str('cn', attribute=True, cli_name='container_name', maxlength=255, multivalue=False, pattern='^[a-zA-Z0-9_.-/]+$', primary_key=True, query=True, required=True)
+option: Flag('continue', autofill=True, cli_name='continue', default=False)
+option: Flag('force?', autofill=True, default=False)
+option: Str('parent_id?', cli_name='parent_id')
+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: vaultcontainer_find
+args: 1,10,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='container_name', maxlength=255, multivalue=False, pattern='^[a-zA-Z0-9_.-/]+$', primary_key=True, query=True, required=False)
+option: Str('container_id', attribute=False, autofill=False, cli_name='container_id', multivalue=False, query=True, required=False)
+option: Str('description', attribute=True, autofill=False, cli_name='desc', multivalue=False, query=True, required=False)
+option: Str('parent_id?', cli_name='parent_id')
+option: Flag('pkey_only?', autofill=True, default=False)
+option: Flag('raw', autofill=True, cli_name='raw', default=False, exclude='webui')
+option: Int('sizelimit?', autofill=False, minvalue=0)
+option: Int('timelimit?', autofill=False, minvalue=0)
+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: vaultcontainer_mod
+args: 1,10,3
+arg: Str('cn', attribute=True, cli_name='container_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('container_id', attribute=False, autofill=False, cli_name='container_id', multivalue=False, required=False)
+option: Str('delattr*', cli_name='delattr', exclude='webui')
+option: Str('description', attribute=True, autofill=False, cli_name='desc', multivalue=False, required=False)
+option: Str('parent_id?', cli_name='parent_id')
+option: Flag('raw', autofill=True, cli_name='raw', default=False, exclude='webui')
+option: Flag('rights', autofill=True, default=False)
+option: Str('setattr*', cli_name='setattr', exclude='webui')
+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: vaultcontainer_show
+args: 1,5,3
+arg: Str('cn', attribute=True, cli_name='container_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: Str('parent_id?', cli_name='parent_id')
+option: Flag('raw', autofill=True, cli_name='raw', default=False, exclude='webui')
+option: Flag('rights', autofill=True, default=False)
+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/install/share/60basev3.ldif b/install/share/60basev3.ldif
index 4efb1fe8ba8a91d3a8b920d39d217124066728c0..c04f8d5d096bc3d91aec3e2f1703f658d76d3779 100644
--- a/install/share/60basev3.ldif
+++ b/install/share/60basev3.ldif
@@ -77,3 +77,5 @@ 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' )
+objectClasses: (2.16.840.1.113730.3.8.18.1.2 NAME 'ipaVaultContainer' DESC 'IPA vault container' 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..dac2f67112dc33f012c6d559285464fb7c944d1a
--- /dev/null
+++ b/install/updates/40-vault.update
@@ -0,0 +1,23 @@
+dn: cn=vaults,$SUFFIX
+default: objectClass: top
+default: objectClass: ipaVaultContainer
+default: cn: vaults
+default: description: Root vault container
+
+dn: cn=services,cn=vaults,$SUFFIX
+default: objectClass: top
+default: objectClass: ipaVaultContainer
+default: cn: services
+default: description: Services vault container
+
+dn: cn=shared,cn=vaults,$SUFFIX
+default: objectClass: top
+default: objectClass: ipaVaultContainer
+default: cn: shared
+default: description: Shared vault container
+
+dn: cn=users,cn=vaults,$SUFFIX
+default: objectClass: top
+default: objectClass: ipaVaultContainer
+default: cn: users
+default: description: Users vault container
diff --git a/install/updates/Makefile.am b/install/updates/Makefile.am
index 40de5635621071d34b6475d51ca598ed41a8ba09..34bb0981c44a3fcc3242401873769f332b95988b 100644
--- a/install/updates/Makefile.am
+++ b/install/updates/Makefile.am
@@ -32,6 +32,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 50a2b1f7aa7f0d447bacfd005b102c7451e670ce..baaf9be8d0329e89cb92a03de302095fe7acb847 100644
--- a/ipalib/constants.py
+++ b/ipalib/constants.py
@@ -97,6 +97,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/user.py b/ipalib/plugins/user.py
index abe5ee26b8e48681eeb0cbb3bcff8617e212225c..6caca14767b4fd6fbfc4a6ae2d3ea243eaa424cc 100644
--- a/ipalib/plugins/user.py
+++ b/ipalib/plugins/user.py
@@ -883,17 +883,48 @@ class user_add(LDAPCreate):
 class user_del(LDAPDelete):
     __doc__ = _('Delete a user.')
 
+    takes_options = LDAPDelete.takes_options + (
+        Flag(
+            'force?',
+            doc=_('Force deletion'),
+            autofill=False,
+        ),
+    )
+
     msg_summary = _('Deleted user "%(value)s"')
 
     def pre_callback(self, ldap, dn, *keys, **options):
         assert isinstance(dn, DN)
         check_protected_member(keys[-1])
 
+        force = options.get('force', False)
+
+        # Find all tokens owned and managed by this user.
+        owner = self.api.Object.user.get_primary_key_from_dn(dn)
+        otptokens = self.api.Command.otptoken_find(ipatokenowner=owner)['result']
+
+        if len(otptokens) and not force:
+            raise errors.NotAllowedOnNonLeaf(
+                message=_('User owns OTP tokens. '
+                          'Specify --force to force deletion.'))
+
+        # Find user's private vault container.
+        vaultcontainer_id = self.api.Object.vaultcontainer.get_private_id(owner)
+        vaultcontainer = None
+        try:
+            vaultcontainer = self.api.Command.vaultcontainer_show(
+                vaultcontainer_id)['result']
+
+            if vaultcontainer and not force:
+                raise errors.NotAllowedOnNonLeaf(
+                    message=_('User owns private vaults. '
+                              'Specify --force to force deletion.'))
+        except errors.NotFound:
+            pass
+
         # Delete all tokens owned and managed by this user.
         # Orphan all tokens owned but not managed by this user.
-        owner = self.api.Object.user.get_primary_key_from_dn(dn)
-        results = self.api.Command.otptoken_find(ipatokenowner=owner)['result']
-        for token in results:
+        for token in otptokens:
             orphan = not [x for x in token.get('managedby_user', []) if x == owner]
             token = self.api.Object.otptoken.get_primary_key_from_dn(token['dn'])
             if orphan:
@@ -901,6 +932,12 @@ class user_del(LDAPDelete):
             else:
                 self.api.Command.otptoken_del(token)
 
+        # Delete user's private vault container.
+        if vaultcontainer:
+            self.api.Command.vaultcontainer_del(
+                vaultcontainer_id,
+                force=force)
+
         return dn
 
 
diff --git a/ipalib/plugins/vault.py b/ipalib/plugins/vault.py
new file mode 100644
index 0000000000000000000000000000000000000000..9945581bed2844e753e8b0ec9ac12af2374ab7bd
--- /dev/null
+++ b/ipalib/plugins/vault.py
@@ -0,0 +1,956 @@
+# 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/>.
+
+import base64
+import json
+import os
+import sys
+import tempfile
+
+import nss.nss as nss
+
+import pki.account
+import pki.crypto
+import pki.key
+
+from ipalib.frontend import Command
+from ipalib import api, errors
+from ipalib import Str, Bytes, Flag
+from ipalib import output
+from ipalib.plugable import Registry
+from ipalib.plugins.baseldap import LDAPObject, LDAPCreate, LDAPDelete,\
+    LDAPSearch, LDAPUpdate, LDAPRetrieve, LDAPQuery, LDAPMultiQuery
+from ipalib.request import context
+from ipalib.plugins.user import split_principal
+from ipalib import _, ngettext
+from ipaplatform.paths import paths
+from ipapython.dn import DN
+import ipapython.nsslib
+
+__doc__ = _("""
+Vaults
+""") + _("""
+Manage vaults.
+""") + _("""
+EXAMPLES:
+""") + _("""
+ List private vaults:
+   ipa vault-find
+""") + _("""
+ List shared vaults:
+   ipa vault-find /shared
+""") + _("""
+ Add a standard vault:
+   ipa vault-add MyVault
+""") + _("""
+ Show a vault:
+   ipa vault-show MyVault
+""") + _("""
+ Modify a vault:
+   ipa vault-mod MyVault --desc "My vault"
+""") + _("""
+ Archive data into standard vault:
+   ipa vault-archive MyVault --in data.bin
+""") + _("""
+ Retrieve data from standard vault:
+   ipa vault-retrieve MyVault --out data.bin
+""") + _("""
+ Delete a vault:
+   ipa vault-del MyVault
+""")
+
+register = Registry()
+
+
+ at register()
+class vault(LDAPObject):
+    __doc__ = _("""
+    Vault object.
+    """)
+
+    base_dn = DN(api.env.container_vault, api.env.basedn)
+
+    object_name = _('vault')
+    object_name_plural = _('vaults')
+
+    object_class = ['ipaVault']
+    default_attributes = [
+        'cn',
+        'vault_id',
+        'description',
+    ]
+    search_display_attributes = [
+        'cn',
+        'vault_id',
+        'description',
+    ]
+
+    label = _('Vaults')
+    label_singular = _('Vault')
+
+    takes_params = (
+        Str(
+            'cn',
+            cli_name='vault_name',
+            label=_('Vault name'),
+            primary_key=True,
+            pattern='^[a-zA-Z0-9_.-/]+$',
+            pattern_errmsg='may only include letters, numbers, _, ., -, and /',
+            maxlength=255,
+        ),
+        Str(
+            'vault_id?',
+            cli_name='vault_id',
+            label=_('Vault ID'),
+            doc=_('Vault ID'),
+            flags={'no_option', 'virtual_attribute'},
+        ),
+        Str(
+            'description?',
+            cli_name='desc',
+            label=_('Description'),
+            doc=_('Vault description'),
+        ),
+    )
+
+    def get_dn(self, *args, **options):
+        """
+        Generates vault DN from vault ID.
+        """
+
+        # get vault ID from parameters
+        vault_name = args[0]
+        parent_id = self.api.Object.vaultcontainer.normalize_id(
+            options.get('parent_id'))
+
+        vault_id = self.merge_id(vault_name, parent_id)
+        vault_id = self.absolute_id(vault_id)
+
+        dn = self.base_dn
+
+        # for each name in the ID, prepend the base DN
+        for name in vault_id.split(u'/'):
+            if name:
+                dn = DN(('cn', name), dn)
+
+        return dn
+
+    def get_id(self, dn):
+        """
+        Generates vault ID from vault DN.
+        """
+
+        # make sure the DN is a vault DN
+        if not dn.endswith(self.base_dn, 1):
+            raise ValueError('Invalid vault DN: %s' % dn)
+
+        # construct the vault ID from the bottom up
+        id = u''
+        for rdn in dn[:-len(self.base_dn)]:
+            name = rdn['cn']
+            id = u'/' + name + id
+
+        return id
+
+    def split_id(self, id):
+        """
+        Splits a vault ID into (vault name, parent ID) tuple.
+        """
+
+        if not id:
+            return (None, None)
+
+        # split ID into parent ID and vault name
+        parts = id.rsplit(u'/', 1)
+
+        if len(parts) == 2:
+            vault_name = parts[1]
+            parent_id = u'%s/' % parts[0]
+
+        else:
+            vault_name = parts[0]
+            parent_id = None
+
+        if not vault_name:
+            vault_name = None
+
+        return (vault_name, parent_id)
+
+    def merge_id(self, vault_name, parent_id):
+        """
+        Merges a vault name and a parent ID into a vault ID.
+        """
+
+        if not vault_name:
+            id = parent_id
+
+        elif vault_name.startswith('/') or not parent_id:
+            id = vault_name
+
+        else:
+            id = parent_id + vault_name
+
+        return id
+
+    def absolute_id(self, id):
+        """
+        Generate absolute vault ID.
+        """
+
+        if not id:
+            return self.api.Object.vaultcontainer.get_private_id()
+
+        # if it's an absolute ID, do nothing
+        if id.startswith(u'/'):
+            return id
+
+        # otherwise, prepend with user's private container ID
+        return self.api.Object.vaultcontainer.get_private_id() + id
+
+    def normalize_params(self, *args, **options):
+        """
+        Normalizes the vault ID in the parameters.
+        """
+
+        vault_id = self.parse_params(*args, **options)
+        (vault_name, parent_id) = self.split_id(vault_id)
+        return self.update_params(vault_name, parent_id, *args, **options)
+
+    def parse_params(self, *args, **options):
+        """
+        Extracts the vault name and parent ID in the parameters.
+        """
+
+        # get vault name and parent ID from parameters
+        vault_name = args[0]
+        parent_id = self.api.Object.vaultcontainer.normalize_id(
+            options.get('parent_id'))
+
+        return self.merge_id(vault_name, parent_id)
+
+    def update_params(
+            self, new_vault_name, new_parent_id, *args, **options):
+        """
+        Stores vault name and parent ID back into the parameters.
+        """
+
+        args_list = list(args)
+        args_list[0] = new_vault_name
+        args = tuple(args_list)
+
+        options['parent_id'] = new_parent_id
+
+        return (args, options)
+
+    def create_entry(self, dn, description=None):
+        """
+        Creates vault entry and its parents.
+        """
+
+        rdn = dn[0]
+        entry = self.backend.make_entry(
+            dn,
+            {
+                'objectclass': self.object_class,
+                'cn': rdn['cn'],
+                'description': description,
+            })
+
+        # 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.api.Object.vaultcontainer.create_entry(parent_dn)
+
+        # then create the entry itself
+        self.backend.add_entry(entry)
+
+    def get_kra_id(self, id):
+        """
+        Generates a client key ID to store/retrieve data in KRA.
+        """
+        return 'ipa:' + id
+
+
+ at register()
+class vault_add(LDAPQuery):
+    __doc__ = _('Create a new vault.')
+
+    takes_options = LDAPQuery.takes_options + (
+        Str(
+            'parent_id?',
+            cli_name='parent_id',
+            doc=_('Parent ID'),
+        ),
+        Str(
+            'description?',
+            cli_name='desc',
+            doc=_('Vault description'),
+        ),
+        Str(
+            'ipavaulttype?',
+            cli_name='type',
+            doc=_('Vault type'),
+            default=u'standard',
+            autofill=True,
+        ),
+        Bytes(
+            'data?',
+            cli_name='data',
+            doc=_('Binary data to archive'),
+        ),
+        Str(
+            'text?',
+            cli_name='text',
+            doc=_('Text data to archive'),
+        ),
+        Str(  # TODO: use File parameter
+            'in?',
+            cli_name='in',
+            doc=_('File containing data to archive'),
+        ),
+    )
+
+    has_output = output.standard_entry
+
+    msg_summary = _('Added vault "%(value)s"')
+
+    def forward(self, *args, **options):
+
+        options['create'] = True
+
+        try:
+            response = self.api.Command.vault_archive(*args, **options)
+
+        except errors.DuplicateEntry:
+            self.obj.handle_duplicate_entry(*args)
+
+        response['summary'] = self.msg_summary % response
+        return response
+
+
+ at register()
+class vault_del(LDAPDelete):
+    __doc__ = _('Delete a vault.')
+
+    takes_options = LDAPDelete.takes_options + (
+        Str(
+            'parent_id?',
+            cli_name='parent_id',
+            doc=_('Parent ID'),
+        ),
+    )
+
+    msg_summary = _('Deleted vault "%(value)s"')
+
+    def get_args(self):
+        # maintain single-valued primary key
+        return super(LDAPMultiQuery, self).get_args()
+
+    def params_2_args_options(self, **params):
+        (args, options) = super(vault_del, self)\
+            .params_2_args_options(**params)
+        return self.obj.normalize_params(*args, **options)
+
+    def post_callback(self, ldap, dn, *args, **options):
+        assert isinstance(dn, DN)
+
+        vault_id = self.obj.get_id(dn)
+
+        kra_client = self.api.Backend.kra.get_client()
+
+        kra_account = pki.account.AccountClient(kra_client.connection)
+        kra_account.login()
+
+        client_key_id = self.obj.get_kra_id(vault_id)
+
+        # deactivate vault record in KRA
+        response = kra_client.keys.list_keys(
+            client_key_id, pki.key.KeyClient.KEY_STATUS_ACTIVE)
+
+        for key_info in response.key_infos:
+            kra_client.keys.modify_key_status(
+                key_info.get_key_id(),
+                pki.key.KeyClient.KEY_STATUS_INACTIVE)
+
+        kra_account.logout()
+
+        return True
+
+
+ at register()
+class vault_find(LDAPSearch):
+    __doc__ = _('Search for vaults.')
+
+    takes_options = LDAPSearch.takes_options + (
+        Str(
+            'parent_id?',
+            cli_name='parent_id',
+            doc=_('Parent ID'),
+        ),
+    )
+
+    msg_summary = ngettext(
+        '%(count)d vault matched',
+        '%(count)d vaults matched',
+        0,
+    )
+
+    def params_2_args_options(self, **params):
+        (args, options) = super(vault_find, self)\
+            .params_2_args_options(**params)
+        parent_id = self.obj.parse_params(*args, **options)
+        parent_id = self.api.Object.vaultcontainer.normalize_id(
+            parent_id)
+        return self.obj.update_params(None, parent_id, *args, **options)
+
+    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 post_callback(self, ldap, entries, truncated, *args, **options):
+
+        for entry in entries:
+            entry['vault_id'] = self.obj.get_id(entry.dn)
+
+        return truncated
+
+    def handle_not_found(self, *args, **options):
+
+        dn = self.obj.get_dn(*args, **options)
+        parent_id = self.obj.get_id(dn)
+        parent_id = self.api.Object.vaultcontainer.normalize_id(
+            parent_id)
+
+        # vault container is user's private container, ignore
+        if parent_id == self.api.Object.vaultcontainer.get_private_id():
+            return
+
+        # otherwise, raise an error
+        raise errors.NotFound(
+            reason=self.obj.parent_not_found_msg % {
+                'parent': parent_id,
+                'oname': self.api.Object.vaultcontainer.object_name,
+            }
+        )
+
+
+ at register()
+class vault_mod(LDAPUpdate):
+    __doc__ = _('Modify a vault.')
+
+    takes_options = LDAPUpdate.takes_options + (
+        Str(
+            'parent_id?',
+            cli_name='parent_id',
+            doc=_('Parent ID'),
+        ),
+    )
+
+    msg_summary = _('Modified vault "%(value)s"')
+
+    def params_2_args_options(self, **params):
+        (args, options) = super(vault_mod, self)\
+            .params_2_args_options(**params)
+        return self.obj.normalize_params(*args, **options)
+
+    def post_callback(self, ldap, dn, entry_attrs, *args, **options):
+        assert isinstance(dn, DN)
+
+        entry_attrs['vault_id'] = self.obj.get_id(dn)
+
+        return dn
+
+
+ at register()
+class vault_show(LDAPRetrieve):
+    __doc__ = _('Display information about a vault.')
+
+    takes_options = LDAPRetrieve.takes_options + (
+        Str(
+            'parent_id?',
+            cli_name='parent_id',
+            doc=_('Parent ID'),
+        ),
+    )
+
+    def params_2_args_options(self, **params):
+        (args, options) = super(vault_show, self)\
+            .params_2_args_options(**params)
+        return self.obj.normalize_params(*args, **options)
+
+    def post_callback(self, ldap, dn, entry_attrs, *args, **options):
+        assert isinstance(dn, DN)
+
+        entry_attrs['vault_id'] = self.obj.get_id(dn)
+
+        return dn
+
+
+ at register()
+class vault_transport_cert(Command):
+    __doc__ = _('Retrieve vault transport certificate.')
+
+    takes_options = (
+        Str(
+            'out?',
+            cli_name='out',
+            doc=_('Output file to store the transport certificate'),
+        ),
+    )
+
+    has_output_params = (
+        Str(
+            'certificate',
+            label=_('Certificate'),
+        ),
+    )
+
+    def forward(self, *args, **options):
+
+        file = options.get('out')
+
+        # don't send these parameters to server
+        if 'out' in options:
+            del options['out']
+
+        response = super(vault_transport_cert, self).forward(*args, **options)
+
+        if file:
+            with open(file, 'w') as f:
+                f.write(response['result']['certificate'])
+
+        return response
+
+    def execute(self, *args, **options):
+
+        kra_client = self.api.Backend.kra.get_client()
+        transport_cert = kra_client.system_certs.get_transport_cert()
+        return {
+            'result': {
+                'certificate': transport_cert.encoded
+            }
+        }
+
+
+ at register()
+class vault_archive(LDAPQuery):
+    __doc__ = _('Archive data into a vault.')
+
+    takes_options = LDAPQuery.takes_options + (
+        Str(
+            'parent_id?',
+            cli_name='parent_id',
+            doc=_('Parent ID'),
+        ),
+        Flag(
+            'create?',
+            doc=_('Create new vault'),
+        ),
+        Str(
+            'description?',
+            cli_name='desc',
+            doc=_('Vault description'),
+        ),
+        Str(
+            'ipavaulttype?',
+            cli_name='type',
+            doc=_('Vault type'),
+            default=u'standard',
+            autofill=True,
+        ),
+        Bytes(
+            'data?',
+            cli_name='data',
+            doc=_('Binary data to archive'),
+        ),
+        Str(
+            'text?',
+            cli_name='text',
+            doc=_('Text data to archive'),
+        ),
+        Str(  # TODO: use File parameter
+            'in?',
+            cli_name='in',
+            doc=_('File containing data to archive'),
+        ),
+        Str(
+            'session_key?',
+            cli_name='session_key',
+            doc=_(
+                'Session key wrapped with transport certificate'
+                ' and encoded in base-64'),
+        ),
+        Str(
+            'vault_data?',
+            cli_name='vault_data',
+            doc=_(
+                'Vault data encrypted with session key'
+                ' and encoded in base-64'),
+        ),
+        Str(
+            'nonce?',
+            cli_name='nonce',
+            doc=_('Nonce encrypted encoded in base-64'),
+        ),
+    )
+
+    has_output = output.standard_entry
+
+    msg_summary = _('Archived data into vault "%(value)s"')
+
+    def params_2_args_options(self, **params):
+        (args, options) = super(vault_archive, self)\
+            .params_2_args_options(**params)
+        return self.obj.normalize_params(*args, **options)
+
+    def forward(self, *args, **options):
+
+        data = options.get('data')
+        text = options.get('text')
+        input_file = options.get('in')
+
+        # don't send these parameters to server
+        if 'data' in options:
+            del options['data']
+        if 'text' in options:
+            del options['text']
+        if 'in' in options:
+            del options['in']
+
+        # get data
+        if data:
+            if text or input_file:
+                raise errors.MutuallyExclusiveError(
+                    reason=_('Input data specified multiple times'))
+
+        elif text:
+            if input_file:
+                raise errors.MutuallyExclusiveError(
+                    reason=_('Input data specified multiple times'))
+
+            data = text.encode()
+
+        elif input_file:
+            with open(input_file, 'rb') as f:
+                data = f.read()
+
+        else:
+            data = ''
+
+        # initialize NSS database
+        crypto = pki.crypto.NSSCryptoProvider(paths.IPA_NSSDB_DIR)
+        crypto.initialize()
+        ipapython.nsslib.current_dbdir = paths.IPA_NSSDB_DIR
+
+        # retrieve transport certificate
+        (file, filename) = tempfile.mkstemp()
+        os.close(file)
+        try:
+            self.api.Command.vault_transport_cert(out=unicode(filename))
+            transport_cert_der = nss.read_der_from_file(filename, True)
+            nss_transport_cert = nss.Certificate(transport_cert_der)
+
+        finally:
+            os.remove(filename)
+
+        # generate session key
+        session_key = crypto.generate_session_key()
+
+        # wrap session key with transport certificate
+        wrapped_session_key = crypto.asymmetric_wrap(
+            session_key,
+            nss_transport_cert
+        )
+
+        options['session_key'] = base64.b64encode(wrapped_session_key)\
+            .decode('utf-8')
+
+        nonce = crypto.generate_nonce_iv()
+        options['nonce'] = base64.b64encode(nonce).decode('utf-8')
+
+        vault_data = {}
+        vault_data[u'data'] = base64.b64encode(data).decode('utf-8')
+
+        json_vault_data = json.dumps(vault_data)
+
+        # wrap vault_data with session key
+        wrapped_vault_data = crypto.symmetric_wrap(
+            json_vault_data,
+            session_key,
+            nonce_iv=nonce
+        )
+
+        options['vault_data'] = base64.b64encode(wrapped_vault_data)\
+            .decode('utf-8')
+
+        return super(vault_archive, self).forward(*args, **options)
+
+    def execute(self, *args, **options):
+
+        dn = self.obj.get_dn(*args, **options)
+        vault_id = self.obj.get_id(dn)
+        (vault_name, parent_id) = self.obj.split_id(vault_id)
+
+        create = options.get('create')
+
+        if create:
+            description = options.get('description')
+
+            # creating new vault
+            self.obj.create_entry(
+                dn,
+                description=description,
+            )
+
+        # retrieve vault info
+        vault = self.api.Command.vault_show(vault_id)['result']
+
+        # connect to KRA
+        kra_client = self.api.Backend.kra.get_client()
+
+        kra_account = pki.account.AccountClient(kra_client.connection)
+        kra_account.login()
+
+        client_key_id = self.obj.get_kra_id(vault_id)
+
+        # deactivate existing vault record in KRA
+        response = kra_client.keys.list_keys(
+            client_key_id,
+            pki.key.KeyClient.KEY_STATUS_ACTIVE)
+
+        for key_info in response.key_infos:
+            kra_client.keys.modify_key_status(
+                key_info.get_key_id(),
+                pki.key.KeyClient.KEY_STATUS_INACTIVE)
+
+        wrapped_session_key = base64.b64decode(options['session_key'])
+        nonce = base64.b64decode(options['nonce'])
+
+        # forward wrapped data to KRA
+        wrapped_vault_data = base64.b64decode(options['vault_data'])
+
+        kra_client.keys.archive_encrypted_data(
+            client_key_id,
+            pki.key.KeyClient.PASS_PHRASE_TYPE,
+            wrapped_vault_data,
+            wrapped_session_key,
+            None,
+            nonce,
+        )
+
+        kra_account.logout()
+
+        response = {}
+        response['result'] = vault
+        response['value'] = vault_name
+        response['summary'] = self.msg_summary % response
+
+        return response
+
+
+ at register()
+class vault_retrieve(LDAPQuery):
+    __doc__ = _('Retrieve a data from a vault.')
+
+    takes_options = LDAPQuery.takes_options + (
+        Str(
+            'parent_id?',
+            cli_name='parent_id',
+            doc=_('Parent ID'),
+        ),
+        Flag(
+            'show_text?',
+            doc=_('Show text data'),
+            autofill=False,
+        ),
+        Flag(
+            'stdout?',
+            doc=_('Show data on standard output'),
+            autofill=False,
+        ),
+        Str(
+            'out?',
+            cli_name='out',
+            doc=_('File to store retrieved data'),
+        ),
+        Str(
+            'session_key?',
+            cli_name='session_key',
+            doc=_(
+                'Session key wrapped with transport certificate'
+                ' and encoded in base-64'),
+        ),
+    )
+
+    has_output = output.standard_entry
+
+    has_output_params = LDAPQuery.has_output_params + (
+        Bytes(
+            'data',
+            label=_('Data'),
+        ),
+        Bytes(
+            'text',
+            label=_('Text'),
+        ),
+    )
+
+    msg_summary = _('Retrieved data from vault "%(value)s"')
+
+    def params_2_args_options(self, **params):
+        (args, options) = super(vault_retrieve, self)\
+            .params_2_args_options(**params)
+        return self.obj.normalize_params(*args, **options)
+
+    def forward(self, *args, **options):
+
+        show_text = options.get('show_text')
+        stdout = options.get('stdout')
+        output_file = options.get('out')
+
+        # don't send these parameters to server
+        if 'show_text' in options:
+            del options['show_text']
+        if 'stdout' in options:
+            del options['stdout']
+        if 'out' in options:
+            del options['out']
+
+        # initialize NSS database
+        crypto = pki.crypto.NSSCryptoProvider(paths.IPA_NSSDB_DIR)
+        crypto.initialize()
+        ipapython.nsslib.current_dbdir = paths.IPA_NSSDB_DIR
+
+        # generate session key
+        session_key = crypto.generate_session_key()
+
+        # retrieve transport certificate
+        (file, filename) = tempfile.mkstemp()
+        os.close(file)
+        try:
+            self.api.Command.vault_transport_cert(out=unicode(filename))
+            transport_cert_der = nss.read_der_from_file(filename, True)
+            nss_transport_cert = nss.Certificate(transport_cert_der)
+
+        finally:
+            os.remove(filename)
+
+        # wrap session key with transport certificate
+        wrapped_session_key = crypto.asymmetric_wrap(
+            session_key,
+            nss_transport_cert
+        )
+
+        # send retrieval request to server
+        options['session_key'] = base64.b64encode(wrapped_session_key)\
+            .decode('utf-8')
+
+        response = super(vault_retrieve, self).forward(*args, **options)
+
+        result = response['result']
+        nonce = base64.b64decode(result['nonce'])
+
+        # unwrap data with session key
+        wrapped_vault_data = base64.b64decode(result['vault_data'])
+
+        json_vault_data = crypto.symmetric_unwrap(
+            wrapped_vault_data,
+            session_key,
+            nonce_iv=nonce)
+
+        vault_data = json.loads(json_vault_data)
+        data = base64.b64decode(vault_data[u'data'].encode('utf-8'))
+
+        if stdout:
+            sys.stdout.write(data)
+            response['result'] = {}
+            response['summary'] = None
+
+        elif output_file:
+            with open(output_file, 'w') as f:
+                f.write(data)
+
+        elif show_text:
+            response['result']['text'] = unicode(data)
+
+        else:
+            response['result']['data'] = data
+
+        return response
+
+    def execute(self, *args, **options):
+        ldap = self.obj.backend
+
+        dn = self.obj.get_dn(*args, **options)
+        vault_id = self.obj.get_id(dn)
+        (vault_name, parent_id) = self.obj.split_id(vault_id)
+
+        # retrieve vault info
+        vault = self.api.Command.vault_show(vault_id)['result']
+
+        wrapped_session_key = base64.b64decode(options['session_key'])
+
+        # connect to KRA
+        kra_client = self.api.Backend.kra.get_client()
+
+        kra_account = pki.account.AccountClient(kra_client.connection)
+        kra_account.login()
+
+        client_key_id = self.obj.get_kra_id(vault_id)
+
+        # find vault record in KRA
+        response = kra_client.keys.list_keys(
+            client_key_id,
+            pki.key.KeyClient.KEY_STATUS_ACTIVE)
+
+        if not len(response.key_infos):
+            raise errors.NotFound(reason=_('Missing archived data.'))
+
+        key_info = response.key_infos[0]
+
+        # retrieve encrypted data from KRA
+        key = kra_client.keys.retrieve_key(
+            key_info.get_key_id(),
+            wrapped_session_key)
+
+        vault['vault_data'] = base64.b64encode(
+            key.encrypted_data).decode('utf-8')
+        vault['nonce'] = base64.b64encode(key.nonce_data).decode('utf-8')
+
+        kra_account.logout()
+
+        response = {}
+        response['result'] = vault
+        response['value'] = vault_name
+        response['summary'] = self.msg_summary % response
+
+        return response
diff --git a/ipalib/plugins/vaultcontainer.py b/ipalib/plugins/vaultcontainer.py
new file mode 100644
index 0000000000000000000000000000000000000000..577d9d8a3cde5e3e55401f51d343a07bb2a1687d
--- /dev/null
+++ b/ipalib/plugins/vaultcontainer.py
@@ -0,0 +1,503 @@
+# 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/>.
+
+import base64
+
+from ipalib import api, errors
+from ipalib import Str, Flag
+from ipalib.plugable import Registry
+from ipalib.plugins.baseldap import LDAPObject, LDAPCreate, LDAPDelete,\
+    LDAPSearch, LDAPUpdate, LDAPRetrieve, LDAPMultiQuery
+from ipalib.request import context
+from ipalib.plugins.user import split_principal
+from ipalib import _, ngettext
+from ipapython.dn import DN
+
+__doc__ = _("""
+Vault containers
+""") + _("""
+Manage vault containers.
+""") + _("""
+EXAMPLES:
+""") + _("""
+ List private vault containers:
+   ipa vaultcontainer-find
+""") + _("""
+ List top-level vault containers:
+   ipa vaultcontainer-find /
+""") + _("""
+ List shared vault containers:
+   ipa vaultcontainer-find /shared
+""") + _("""
+ Add a vault container:
+   ipa vaultcontainer-add MyContainer
+""") + _("""
+ Show a vault container:
+   ipa vaultcontainer-show MyContainer
+""") + _("""
+ Modify a vault container:
+   ipa vaultcontainer-mod MyContainer --desc "My container"
+""") + _("""
+ Delete a vault container:
+   ipa vaultcontainer-del MyContainer
+""")
+
+register = Registry()
+
+
+ at register()
+class vaultcontainer(LDAPObject):
+    __doc__ = _("""
+    Vault container object.
+    """)
+
+    object_name = _('vault container')
+    object_name_plural = _('vault containers')
+
+    object_class = ['ipaVaultContainer']
+    default_attributes = [
+        'cn',
+        'container_id',
+        'description',
+    ]
+    search_display_attributes = [
+        'cn',
+        'container_id',
+        'description',
+    ]
+
+    label = _('Vault Containers')
+    label_singular = _('Vault Container')
+
+    takes_params = (
+        Str(
+            'cn',
+            cli_name='container_name',
+            label=_('Container name'),
+            primary_key=True,
+            pattern='^[a-zA-Z0-9_.-/]+$',
+            pattern_errmsg='may only include letters, numbers, _, ., -, and /',
+            maxlength=255,
+        ),
+        Str(
+            'container_id?',
+            cli_name='container_id',
+            label=_('Container ID'),
+            doc=_('Container ID'),
+            flags={'no_option', 'virtual_attribute'},
+        ),
+        Str(
+            'description?',
+            cli_name='desc',
+            label=_('Description'),
+            doc=_('Container description'),
+        ),
+    )
+
+    def get_dn(self, *args, **options):
+        """
+        Generates vault container DN from container ID.
+        """
+
+        # get container ID from parameters
+        container_name = args[0]
+        parent_id = self.normalize_id(options.get('parent_id'))
+
+        container_id = self.merge_id(container_name, parent_id)
+        container_id = self.absolute_id(container_id)
+
+        dn = self.api.Object.vault.base_dn
+
+        # for each name in the ID, prepend the base DN
+        for name in container_id.split(u'/'):
+            if name:
+                dn = DN(('cn', name), dn)
+
+        return dn
+
+    def get_id(self, dn):
+        """
+        Generates container ID from container DN.
+        """
+
+        # make sure the DN is a container DN
+        if not dn.endswith(self.api.Object.vault.base_dn, 1):
+            raise ValueError('Invalid container DN: %s' % dn)
+
+        # construct container ID from the bottom up
+        id = u'/'
+        for rdn in dn[:-len(self.api.Object.vault.base_dn)]:
+            name = rdn['cn']
+            id = u'/' + name + id
+
+        return id
+
+    def get_private_id(self, username=None):
+        """
+        Returns user's private container ID (i.e. /users/<username>/).
+        """
+
+        if not username:
+            principal = getattr(context, 'principal')
+            (username, realm) = split_principal(principal)
+
+        return u'/users/' + username + u'/'
+
+    def normalize_id(self, id):
+        """
+        Normalizes container ID.
+        """
+
+        # make sure ID ends with slash
+        if id and not id.endswith(u'/'):
+            return id + u'/'
+
+        return id
+
+    def absolute_id(self, id):
+        """
+        Generate absolute container ID.
+        """
+
+        # if ID is empty, return user's private container ID
+        if not id:
+            return self.get_private_id()
+
+        # if it's an absolute ID, do nothing
+        if id.startswith(u'/'):
+            return id
+
+        # otherwise, prepend with user's private container ID
+        return self.get_private_id() + id
+
+    def split_id(self, id):
+        """
+        Splits a normalized container ID into (container name, parent ID)
+        tuple.
+        """
+
+        # handle root ID
+        if id == u'/':
+            return (None, u'/')
+
+        # split ID into parent ID, container name, and empty string
+        parts = id.rsplit(u'/', 2)
+
+        if len(parts) == 3:
+            container_name = parts[1]
+            parent_id = u'%s/' % parts[0]
+
+        elif len(parts) == 2:
+            container_name = parts[0]
+            parent_id = None
+
+        if not container_name:
+            container_name = None
+
+        return (container_name, parent_id)
+
+    def merge_id(self, container_name, parent_id):
+        """
+        Merges a container name and a parent ID into a container ID.
+        """
+
+        if not container_name:
+            id = parent_id
+
+        elif container_name.startswith('/') or not parent_id:
+            id = container_name
+
+        else:
+            id = parent_id + container_name
+
+        return self.normalize_id(id)
+
+    def normalize_params(self, *args, **options):
+        """
+        Normalizes the container ID in the parameters.
+        """
+
+        container_id = self.parse_params(*args, **options)
+        (container_name, parent_id) = self.split_id(container_id)
+        return self.update_params(container_name, parent_id, *args, **options)
+
+    def parse_params(self, *args, **options):
+        """
+        Extracts the container name and parent ID in the parameters.
+        """
+
+        container_name = args[0]
+        parent_id = self.normalize_id(options.get('parent_id'))
+
+        return self.merge_id(container_name, parent_id)
+
+    def update_params(
+            self, new_container_name, new_parent_id, *args, **options):
+        """
+        Stores container name and parent ID back into the parameters.
+        """
+
+        args_list = list(args)
+        args_list[0] = new_container_name
+        args = tuple(args_list)
+
+        options['parent_id'] = new_parent_id
+
+        return (args, options)
+
+    def create_entry(self, dn):
+        """
+        Creates a container entry and its parents.
+        """
+
+        rdn = dn[0]
+        entry = self.backend.make_entry(
+            dn,
+            {
+                'objectclass': self.object_class,
+                '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_entry(parent_dn)
+
+        # then create the entry itself
+        self.backend.add_entry(entry)
+
+
+ at register()
+class vaultcontainer_add(LDAPCreate):
+    __doc__ = _('Create a new vault container.')
+
+    takes_options = LDAPCreate.takes_options + (
+        Str(
+            'parent_id?',
+            cli_name='parent_id',
+            doc=_('Parent ID'),
+        ),
+    )
+
+    msg_summary = _('Added vault container "%(value)s"')
+
+    def params_2_args_options(self, **params):
+        (args, options) = super(vaultcontainer_add, self)\
+            .params_2_args_options(**params)
+        return self.obj.normalize_params(*args, **options)
+
+    def pre_callback(
+            self, ldap, dn, entry_attrs, attrs_list,
+            *args, **options):
+        assert isinstance(dn, DN)
+
+        container_id = self.obj.get_id(dn)
+        (container_name, parent_id) = self.obj.split_id(container_id)
+
+        # parent is user's private container, create parent
+        if parent_id == self.obj.get_private_id():
+            try:
+                self.obj.create_entry(DN(*dn[1:]))
+            except errors.DuplicateEntry:
+                pass
+
+        return dn
+
+    def post_callback(self, ldap, dn, entry_attrs, *args, **options):
+        assert isinstance(dn, DN)
+
+        entry_attrs['container_id'] = self.obj.get_id(dn)
+
+        return dn
+
+    def handle_not_found(self, *args, **options):
+
+        dn = self.obj.get_dn(*args, **options)
+        container_id = self.obj.get_id(dn)
+        (container_name, parent_id) = self.obj.split_id(container_id)
+
+        raise errors.NotFound(
+            reason=self.obj.parent_not_found_msg % {
+                'parent': parent_id,
+                'oname': self.obj.object_name,
+            }
+        )
+
+
+ at register()
+class vaultcontainer_del(LDAPDelete):
+    __doc__ = _('Delete a vault container.')
+
+    takes_options = LDAPDelete.takes_options + (
+        Str(
+            'parent_id?',
+            cli_name='parent_id',
+            doc=_('Parent ID'),
+        ),
+        Flag(
+            'force?',
+            doc=_('Force deletion'),
+            autofill=False,
+        ),
+    )
+
+    msg_summary = _('Deleted vault container "%(value)s"')
+
+    def get_args(self):
+        # maintain single-valued primary key
+        return super(LDAPMultiQuery, self).get_args()
+
+    def params_2_args_options(self, **params):
+        (args, options) = super(vaultcontainer_del, self)\
+            .params_2_args_options(**params)
+        return self.obj.normalize_params(*args, **options)
+
+    def pre_callback(self, ldap, dn, *args, **options):
+        assert isinstance(dn, DN)
+
+        try:
+            entries = ldap.get_entries(
+                dn, scope=ldap.SCOPE_ONELEVEL, attrs_list=[])
+        except errors.NotFound:
+            pass
+        else:
+            if not options.get('force', False):
+                raise errors.NotAllowedOnNonLeaf(
+                    message=_('Container is not empty. '
+                              'Specify --force to force deletion.'))
+            print 'Deleting %d entries' % len(entries)
+
+        return dn
+
+
+ at register()
+class vaultcontainer_find(LDAPSearch):
+    __doc__ = _('Search for vault containers.')
+
+    takes_options = LDAPSearch.takes_options + (
+        Str(
+            'parent_id?',
+            cli_name='parent_id',
+            doc=_('Parent ID'),
+        ),
+    )
+
+    msg_summary = ngettext(
+        '%(count)d vault container matched',
+        '%(count)d vault containers matched',
+        0,
+    )
+
+    def params_2_args_options(self, **params):
+        (args, options) = super(vaultcontainer_find, self)\
+            .params_2_args_options(**params)
+        parent_id = self.obj.parse_params(*args, **options)
+        return self.obj.update_params(None, parent_id, *args, **options)
+
+    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 post_callback(self, ldap, entries, truncated, *args, **options):
+
+        for entry in entries:
+            entry['container_id'] = self.obj.get_id(entry.dn)
+
+        return truncated
+
+    def handle_not_found(self, *args, **options):
+
+        dn = self.obj.get_dn(*args, **options)
+        parent_id = self.obj.get_id(dn)
+
+        # parent is user's private container, ignore
+        if parent_id == self.obj.get_private_id():
+            return
+
+        # otherwise, raise an error
+        raise errors.NotFound(
+            reason=self.obj.parent_not_found_msg % {
+                'parent': parent_id,
+                'oname': self.obj.object_name,
+            }
+        )
+
+
+ at register()
+class vaultcontainer_mod(LDAPUpdate):
+    __doc__ = _('Modify a vault container.')
+
+    takes_options = LDAPUpdate.takes_options + (
+        Str(
+            'parent_id?',
+            cli_name='parent_id',
+            doc=_('Parent ID'),
+        ),
+    )
+
+    msg_summary = _('Modified vault container "%(value)s"')
+
+    def params_2_args_options(self, **params):
+        (args, options) = super(vaultcontainer_mod, self)\
+            .params_2_args_options(**params)
+        return self.obj.normalize_params(*args, **options)
+
+    def post_callback(self, ldap, dn, entry_attrs, *args, **options):
+        assert isinstance(dn, DN)
+
+        entry_attrs['container_id'] = self.obj.get_id(dn)
+
+        return dn
+
+
+ at register()
+class vaultcontainer_show(LDAPRetrieve):
+    __doc__ = _('Display information about a vault container.')
+
+    takes_options = LDAPRetrieve.takes_options + (
+        Str(
+            'parent_id?',
+            cli_name='parent_id',
+            doc=_('Parent ID'),
+        ),
+    )
+
+    def params_2_args_options(self, **params):
+        (args, options) = super(vaultcontainer_show, self)\
+            .params_2_args_options(**params)
+        return self.obj.normalize_params(*args, **options)
+
+    def post_callback(self, ldap, dn, entry_attrs, *args, **options):
+        assert isinstance(dn, DN)
+
+        entry_attrs['container_id'] = self.obj.get_id(dn)
+
+        return dn
diff --git a/ipatests/test_xmlrpc/test_vault_plugin.py b/ipatests/test_xmlrpc/test_vault_plugin.py
new file mode 100644
index 0000000000000000000000000000000000000000..1b3d864bfdb2c911cdb9021d79f9ce07eb72b06e
--- /dev/null
+++ b/ipatests/test_xmlrpc/test_vault_plugin.py
@@ -0,0 +1,394 @@
+# 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
+
+test_vault = u'test_vault'
+shared_test_vault = u'/shared/%s' % test_vault
+
+binary_data = '\x01\x02\x03\x04'
+text_data = u'secret'
+
+
+class test_vault_plugin(Declarative):
+
+    cleanup_commands = [
+        ('vault_del', [test_vault], {'continue': True}),
+        ('vault_del', [shared_test_vault], {'continue': True}),
+    ]
+
+    tests = [
+
+        {
+            'desc': 'Create test vault',
+            'command': (
+                'vault_add',
+                [test_vault],
+                {},
+            ),
+            'expected': {
+                'value': test_vault,
+                'summary': 'Added vault "%s"' % test_vault,
+                'result': {
+                    'dn': u'cn=%s,cn=admin,cn=users,cn=vaults,%s'
+                          % (test_vault, api.env.basedn),
+                    'cn': [test_vault],
+                    'vault_id': u'/users/admin/%s' % test_vault,
+                },
+            },
+        },
+
+        {
+            'desc': 'Create duplicate vault',
+            'command': (
+                'vault_add',
+                [test_vault],
+                {},
+            ),
+            'expected': errors.DuplicateEntry(
+                message=u'vault with name "%s" already exists' % test_vault),
+        },
+
+        {
+            'desc': 'Find test 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'
+                              % (test_vault, api.env.basedn),
+                        'cn': [test_vault],
+                        'vault_id': u'/users/admin/%s' % test_vault,
+                    },
+                ],
+            },
+        },
+
+        {
+            'desc': 'Show test vault',
+            'command': (
+                'vault_show',
+                [test_vault],
+                {},
+            ),
+            'expected': {
+                'value': test_vault,
+                'summary': None,
+                'result': {
+                    'dn': u'cn=%s,cn=admin,cn=users,cn=vaults,%s'
+                          % (test_vault, api.env.basedn),
+                    'cn': [test_vault],
+                    'vault_id': u'/users/admin/%s' % test_vault,
+                },
+            },
+        },
+
+        {
+            'desc': 'Modify test vault',
+            'command': (
+                'vault_mod',
+                [test_vault],
+                {
+                    'description': u'Test vault',
+                },
+            ),
+            'expected': {
+                'value': test_vault,
+                'summary': u'Modified vault "%s"' % test_vault,
+                'result': {
+                    'cn': [test_vault],
+                    'vault_id': u'/users/admin/%s' % test_vault,
+                    'description': [u'Test vault'],
+                },
+            },
+        },
+
+        {
+            'desc': 'Archive binary data',
+            'command': (
+                'vault_archive',
+                [test_vault],
+                {
+                    'data': binary_data,
+                },
+            ),
+            'expected': {
+                'value': test_vault,
+                'summary': u'Archived data into vault "%s"' % test_vault,
+                'result': {
+                    'dn': u'cn=%s,cn=admin,cn=users,cn=vaults,%s'
+                          % (test_vault, api.env.basedn),
+                    'cn': [test_vault],
+                    'vault_id': u'/users/admin/%s' % test_vault,
+                    'description': [u'Test vault'],
+                },
+            },
+        },
+
+        {
+            'desc': 'Retrieve binary data',
+            'command': (
+                'vault_retrieve',
+                [test_vault],
+                {},
+            ),
+            'expected': {
+                'value': test_vault,
+                'summary': u'Retrieved data from vault "%s"' % test_vault,
+                'result': {
+                    'dn': u'cn=%s,cn=admin,cn=users,cn=vaults,%s'
+                          % (test_vault, api.env.basedn),
+                    'cn': [test_vault],
+                    'vault_id': u'/users/admin/%s' % test_vault,
+                    'description': [u'Test vault'],
+                    'nonce': fuzzy_string,
+                    'vault_data': fuzzy_string,
+                    'data': binary_data,
+                },
+            },
+        },
+
+        {
+            'desc': 'Archive text data',
+            'command': (
+                'vault_archive',
+                [test_vault],
+                {
+                    'text': text_data,
+                },
+            ),
+            'expected': {
+                'value': test_vault,
+                'summary': u'Archived data into vault "%s"' % test_vault,
+                'result': {
+                    'dn': u'cn=%s,cn=admin,cn=users,cn=vaults,%s'
+                          % (test_vault, api.env.basedn),
+                    'cn': [test_vault],
+                    'vault_id': u'/users/admin/%s' % test_vault,
+                    'description': [u'Test vault'],
+                },
+            },
+        },
+
+        {
+            'desc': 'Retrieve text data',
+            'command': (
+                'vault_retrieve',
+                [test_vault],
+                {
+                    'show_text': True,
+                },
+            ),
+            'expected': {
+                'value': test_vault,
+                'summary': u'Retrieved data from vault "%s"' % test_vault,
+                'result': {
+                    'dn': u'cn=%s,cn=admin,cn=users,cn=vaults,%s'
+                          % (test_vault, api.env.basedn),
+                    'cn': [test_vault],
+                    'vault_id': u'/users/admin/%s' % test_vault,
+                    'description': [u'Test vault'],
+                    'nonce': fuzzy_string,
+                    'vault_data': fuzzy_string,
+                    'text': text_data,
+                },
+            },
+        },
+
+        {
+            'desc': 'Delete test vault',
+            'command': (
+                'vault_del',
+                [test_vault],
+                {},
+            ),
+            'expected': {
+                'value': [test_vault],
+                'summary': u'Deleted vault "%s"' % test_vault,
+                'result': {
+                    'failed': (),
+                },
+            },
+        },
+
+        {
+            'desc': 'Delete non-existent vault',
+            'command': (
+                'vault_del',
+                [test_vault],
+                {},
+            ),
+            'expected': errors.NotFound(
+                reason=u'%s: vault not found' % test_vault),
+        },
+
+        {
+            'desc': 'Create shared vault',
+            'command': (
+                'vault_add',
+                [shared_test_vault],
+                {},
+            ),
+            'expected': {
+                'value': test_vault,
+                'summary': u'Added vault "%s"' % test_vault,
+                'result': {
+                    'dn': u'cn=%s,cn=shared,cn=vaults,%s'
+                          % (test_vault, api.env.basedn),
+                    'cn': [test_vault],
+                    'vault_id': shared_test_vault,
+                },
+            },
+        },
+
+        {
+            'desc': 'Find shared vaults',
+            'command': (
+                'vault_find',
+                [u'/shared/'],
+                {},
+            ),
+            'expected': {
+                'count': 1,
+                'truncated': False,
+                'summary': u'1 vault matched',
+                'result': [
+                    {
+                        'dn': u'cn=%s,cn=shared,cn=vaults,%s'
+                              % (test_vault, api.env.basedn),
+                        'cn': [test_vault],
+                        'vault_id': shared_test_vault,
+                    },
+                ],
+            },
+        },
+
+        {
+            'desc': 'Show shared vault',
+            'command': (
+                'vault_show',
+                [shared_test_vault],
+                {},
+            ),
+            'expected': {
+                'value': test_vault,
+                'summary': None,
+                'result': {
+                    'dn': u'cn=%s,cn=shared,cn=vaults,%s'
+                          % (test_vault, api.env.basedn),
+                    'cn': [test_vault],
+                    'vault_id': shared_test_vault,
+                },
+            },
+        },
+
+        {
+            'desc': 'Modify shared vault',
+            'command': (
+                'vault_mod',
+                [shared_test_vault],
+                {
+                    'description': u'Test vault',
+                },
+            ),
+            'expected': {
+                'value': test_vault,
+                'summary': u'Modified vault "%s"' % test_vault,
+                'result': {
+                    'cn': [test_vault],
+                    'vault_id': shared_test_vault,
+                    'description': [u'Test vault'],
+                },
+            },
+        },
+
+        {
+            'desc': 'Archive binary data in shared vault',
+            'command': (
+                'vault_archive',
+                [shared_test_vault],
+                {
+                    'data': binary_data,
+                },
+            ),
+            'expected': {
+                'value': test_vault,
+                'summary': u'Archived data into vault "%s"' % test_vault,
+                'result': {
+                    'dn': u'cn=%s,cn=shared,cn=vaults,%s'
+                          % (test_vault, api.env.basedn),
+                    'cn': [test_vault],
+                    'vault_id': shared_test_vault,
+                    'description': [u'Test vault'],
+                },
+            },
+        },
+
+        {
+            'desc': 'Retrieve binary data in shared vault',
+            'command': (
+                'vault_retrieve',
+                [shared_test_vault],
+                {},
+            ),
+            'expected': {
+                'value': test_vault,
+                'summary': u'Retrieved data from vault "%s"' % test_vault,
+                'result': {
+                    'dn': u'cn=%s,cn=shared,cn=vaults,%s'
+                          % (test_vault, api.env.basedn),
+                    'cn': [test_vault],
+                    'vault_id': shared_test_vault,
+                    'description': [u'Test vault'],
+                    'nonce': fuzzy_string,
+                    'vault_data': fuzzy_string,
+                    'data': binary_data,
+                },
+            },
+        },
+
+        {
+            'desc': 'Delete shared vault',
+            'command': (
+                'vault_del',
+                [shared_test_vault],
+                {},
+            ),
+            'expected': {
+                'value': [test_vault],
+                'summary': u'Deleted vault "%s"' % test_vault,
+                'result': {
+                    'failed': (),
+                },
+            },
+        },
+
+    ]
diff --git a/ipatests/test_xmlrpc/test_vaultcontainer_plugin.py b/ipatests/test_xmlrpc/test_vaultcontainer_plugin.py
new file mode 100644
index 0000000000000000000000000000000000000000..8cd21458fe1dae64773593dab1a0833e1a233d67
--- /dev/null
+++ b/ipatests/test_xmlrpc/test_vaultcontainer_plugin.py
@@ -0,0 +1,436 @@
+# 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/vaultcontainer.py` module.
+"""
+
+from ipalib import api, errors
+from xmlrpc_test import Declarative
+
+test_container = u'test_container'
+private_container = test_container
+shared_test_container = u'/shared/%s' % test_container
+service_test_container = u'/services/%s' % test_container
+
+base_container = u'base_container'
+child_container = u'child_container'
+grandchild_container = u'grandchild_container'
+
+
+class test_vaultcontainer_plugin(Declarative):
+
+    cleanup_commands = [
+        ('vaultcontainer_del', [private_container], {'continue': True}),
+        ('vaultcontainer_del', [shared_test_container], {'continue': True}),
+        ('vaultcontainer_del', [service_test_container], {'continue': True}),
+        ('vaultcontainer_del', [base_container], {
+            'force': True, 'continue': True}),
+    ]
+
+    tests = [
+
+        {
+            'desc': 'Find top-level containers',
+            'command': (
+                'vaultcontainer_find',
+                [],
+                {
+                    'parent_id': u'/',
+                },
+            ),
+            'expected': {
+                'count': 3,
+                'truncated': False,
+                'summary': u'3 vault containers matched',
+                'result': [
+                    {
+                        'dn': u'cn=services,cn=vaults,%s' % api.env.basedn,
+                        'cn': [u'services'],
+                        'container_id': u'/services/',
+                        'description': [u'Services vault container'],
+                    },
+                    {
+                        'dn': u'cn=shared,cn=vaults,%s' % api.env.basedn,
+                        'cn': [u'shared'],
+                        'container_id': u'/shared/',
+                        'description': [u'Shared vault container'],
+                    },
+                    {
+                        'dn': u'cn=users,cn=vaults,%s' % api.env.basedn,
+                        'cn': [u'users'],
+                        'container_id': u'/users/',
+                        'description': [u'Users vault container'],
+                    },
+                ],
+            },
+        },
+
+        {
+            'desc': 'Create private container',
+            'command': (
+                'vaultcontainer_add',
+                [private_container],
+                {},
+            ),
+            'expected': {
+                'value': private_container,
+                'summary': 'Added vault container "%s"' % private_container,
+                'result': {
+                    'dn': u'cn=%s,cn=admin,cn=users,cn=vaults,%s'
+                          % (private_container, api.env.basedn),
+                    'objectclass': (u'ipaVaultContainer', u'top'),
+                    'cn': [private_container],
+                    'container_id': u'/users/admin/%s/' % private_container,
+                },
+            },
+        },
+
+        {
+            'desc': 'Create duplicate container',
+            'command': (
+                'vaultcontainer_add',
+                [private_container],
+                {},
+            ),
+            'expected': errors.DuplicateEntry(
+                message=u'vault container with name "%s" already exists'
+                        % private_container),
+        },
+
+        {
+            'desc': 'Find private containers',
+            'command': (
+                'vaultcontainer_find',
+                [],
+                {},
+            ),
+            'expected': {
+                'count': 1,
+                'truncated': False,
+                'summary': u'1 vault container matched',
+                'result': [
+                    {
+                        'dn': u'cn=%s,cn=admin,cn=users,cn=vaults,%s'
+                              % (private_container, api.env.basedn),
+                        'cn': [private_container],
+                        'container_id': u'/users/admin/%s/'
+                                        % private_container,
+                    },
+                ],
+            },
+        },
+
+        {
+            'desc': 'Show private container',
+            'command': (
+                'vaultcontainer_show',
+                [private_container],
+                {},
+            ),
+            'expected': {
+                'value': private_container,
+                'summary': None,
+                'result': {
+                    'dn': u'cn=%s,cn=admin,cn=users,cn=vaults,%s'
+                          % (private_container, api.env.basedn),
+                    'cn': [private_container],
+                    'container_id': u'/users/admin/%s/' % private_container,
+                },
+            },
+        },
+
+        {
+            'desc': 'Modify private container',
+            'command': (
+                'vaultcontainer_mod',
+                [private_container],
+                {
+                    'description': u'Private container',
+                },
+            ),
+            'expected': {
+                'value': private_container,
+                'summary': 'Modified vault container "%s"' % private_container,
+                'result': {
+                    'cn': [private_container],
+                    'container_id': u'/users/admin/%s/' % private_container,
+                    'description': [u'Private container'],
+                },
+            },
+        },
+
+        {
+            'desc': 'Delete private container',
+            'command': (
+                'vaultcontainer_del',
+                [private_container],
+                {},
+            ),
+            'expected': {
+                'value': [private_container],
+                'summary': u'Deleted vault container "%s"' % private_container,
+                'result': {
+                    'failed': (),
+                },
+            },
+        },
+
+        {
+            'desc': 'Delete non-existent container',
+            'command': (
+                'vaultcontainer_del',
+                [private_container],
+                {},
+            ),
+            'expected': errors.NotFound(
+                reason=u'%s: vault container not found' % private_container),
+        },
+
+        {
+            'desc': 'Create shared container',
+            'command': (
+                'vaultcontainer_add',
+                [shared_test_container],
+                {},
+            ),
+            'expected': {
+                'value': test_container,
+                'summary': 'Added vault container "%s"' % test_container,
+                'result': {
+                    'dn': u'cn=%s,cn=shared,cn=vaults,%s'
+                          % (test_container, api.env.basedn),
+                    'objectclass': (u'ipaVaultContainer', u'top'),
+                    'cn': [test_container],
+                    'container_id': u'/shared/%s/' % test_container,
+                },
+            },
+        },
+
+        {
+            'desc': 'Find shared containers',
+            'command': (
+                'vaultcontainer_find',
+                [u'/shared/'],
+                {},
+            ),
+            'expected': {
+                'count': 1,
+                'truncated': False,
+                'summary': u'1 vault container matched',
+                'result': [
+                    {
+                        'dn': u'cn=%s,cn=shared,cn=vaults,%s'
+                              % (test_container, api.env.basedn),
+                        'cn': [test_container],
+                        'container_id': u'/shared/%s/' % test_container,
+                    },
+                ],
+            },
+        },
+
+        {
+            'desc': 'Show shared container',
+            'command': (
+                'vaultcontainer_show',
+                [shared_test_container],
+                {},
+            ),
+            'expected': {
+                'value': test_container,
+                'summary': None,
+                'result': {
+                    'dn': u'cn=%s,cn=shared,cn=vaults,%s'
+                          % (test_container, api.env.basedn),
+                    'cn': [test_container],
+                    'container_id': u'/shared/%s/' % test_container,
+                },
+            },
+        },
+
+        {
+            'desc': 'Modify shared container',
+            'command': (
+                'vaultcontainer_mod',
+                [shared_test_container],
+                {
+                    'description': u'shared container',
+                },
+            ),
+            'expected': {
+                'value': test_container,
+                'summary': 'Modified vault container "%s"' % test_container,
+                'result': {
+                    'cn': [test_container],
+                    'container_id': u'/shared/%s/' % test_container,
+                    'description': [u'shared container'],
+                },
+            },
+        },
+
+        {
+            'desc': 'Delete shared container',
+            'command': (
+                'vaultcontainer_del',
+                [shared_test_container],
+                {},
+            ),
+            'expected': {
+                'value': [test_container],
+                'summary': u'Deleted vault container "%s"' % test_container,
+                'result': {
+                    'failed': (),
+                },
+            },
+        },
+
+        {
+            'desc': 'Create service container',
+            'command': (
+                'vaultcontainer_add',
+                [service_test_container],
+                {},
+            ),
+            'expected': {
+                'value': test_container,
+                'summary': 'Added vault container "%s"' % test_container,
+                'result': {
+                    'dn': u'cn=%s,cn=services,cn=vaults,%s'
+                          % (test_container, api.env.basedn),
+                    'objectclass': (u'ipaVaultContainer', u'top'),
+                    'cn': [test_container],
+                    'container_id': u'/services/%s/' % test_container,
+                },
+            },
+        },
+        {
+            'desc': 'Create base container',
+            'command': (
+                'vaultcontainer_add',
+                [base_container],
+                {},
+            ),
+            'expected': {
+                'value': base_container,
+                'summary': 'Added vault container "%s"' % base_container,
+                'result': {
+                    'dn': u'cn=%s,cn=admin,cn=users,cn=vaults,%s'
+                          % (base_container, api.env.basedn),
+                    'objectclass': (u'ipaVaultContainer', u'top'),
+                    'cn': [base_container],
+                    'container_id': u'/users/admin/%s/' % base_container,
+                },
+            },
+        },
+
+        {
+            'desc': 'Create child container',
+            'command': (
+                'vaultcontainer_add',
+                [child_container],
+                {
+                    'parent_id': base_container,
+                },
+            ),
+            'expected': {
+                'value': child_container,
+                'summary': 'Added vault container "%s"' % child_container,
+                'result': {
+                    'dn': u'cn=%s,cn=%s,cn=admin,cn=users,cn=vaults,%s'
+                          % (child_container, base_container, api.env.basedn),
+                    'objectclass': (u'ipaVaultContainer', u'top'),
+                    'cn': [child_container],
+                    'container_id': u'/users/admin/%s/%s/'
+                                    % (base_container, child_container),
+                },
+            },
+        },
+
+        {
+            'desc': 'Create grandchild container',
+            'command': (
+                'vaultcontainer_add',
+                [grandchild_container],
+                {
+                    'parent_id': base_container + u'/' + child_container,
+                },
+            ),
+            'expected': {
+                'value': grandchild_container,
+                'summary': 'Added vault container "%s"' % grandchild_container,
+                'result': {
+                    'dn': u'cn=%s,cn=%s,cn=%s,cn=admin,cn=users,cn=vaults,%s'
+                          % (
+                              grandchild_container,
+                              child_container,
+                              base_container,
+                              api.env.basedn),
+                    'objectclass': (u'ipaVaultContainer', u'top'),
+                    'cn': [grandchild_container],
+                    'container_id': u'/users/admin/%s/%s/%s/'
+                                    % (
+                                        base_container,
+                                        child_container,
+                                        grandchild_container),
+                },
+            },
+        },
+
+        {
+            'desc': 'Delete base container',
+            'command': (
+                'vaultcontainer_del',
+                [base_container],
+                {},
+            ),
+            'expected': errors.NotAllowedOnNonLeaf(
+                message=u'Container is not empty. '
+                        u'Specify --force to force deletion.'),
+        },
+
+        {
+            'desc': 'Delete base container with force',
+            'command': (
+                'vaultcontainer_del',
+                [base_container],
+                {
+                    'force': True,
+                },
+            ),
+            'expected': {
+                'value': [base_container],
+                'summary': u'Deleted vault container "%s"' % base_container,
+                'result': {
+                    'failed': (),
+                },
+            },
+        },
+
+        {
+            'desc': 'Delete non-existent container',
+            'command': (
+                'vaultcontainer_del',
+                [base_container],
+                {},
+            ),
+            'expected': errors.NotFound(
+                reason=u'%s: vault container not found' % base_container),
+        },
+
+    ]
-- 
2.3.1

-------------- next part --------------
>From b9563e971228339c632f8b318f8fc89f96d81af8 Mon Sep 17 00:00:00 2001
From: "Endi S. Dewata" <edewata at redhat.com>
Date: Fri, 17 Oct 2014 12:05:34 -0400
Subject: [PATCH] Added vault access control.

New LDAP ACIs have been added to allow users to create their own
private vault container, to allow owners to manage vaults and
containers, and to allow members to use the vaults. New commands
have been added to manage the owner and member list. The LDAP
schema has been updated as well.

https://fedorahosted.org/freeipa/ticket/3872
---
 API.txt                                            | 132 +++++++++++++--
 install/share/60basev3.ldif                        |   4 +-
 install/updates/40-vault.update                    |  17 ++
 ipalib/plugins/vault.py                            | 178 +++++++++++++++++++--
 ipalib/plugins/vaultcontainer.py                   | 167 ++++++++++++++++++-
 ipatests/test_xmlrpc/test_vault_plugin.py          |  12 ++
 ipatests/test_xmlrpc/test_vaultcontainer_plugin.py |  10 ++
 7 files changed, 487 insertions(+), 33 deletions(-)

diff --git a/API.txt b/API.txt
index ebae899fe93fd742b052de9ca5e63ad567c437ef..e7e39ec7e64b0e9878ba8a00e89f4124991adc92 100644
--- a/API.txt
+++ b/API.txt
@@ -4521,7 +4521,7 @@ option: Flag('all', autofill=True, cli_name='all', default=False, exclude='webui
 option: Bytes('data?', cli_name='data')
 option: Str('description?', cli_name='desc')
 option: Str('in?', cli_name='in')
-option: Str('ipavaulttype?', autofill=True, cli_name='type', default=u'standard')
+option: Flag('no_members', autofill=True, default=False, exclude='webui')
 option: Str('parent_id?', cli_name='parent_id')
 option: Flag('raw', autofill=True, cli_name='raw', default=False, exclude='webui')
 option: Str('text?', cli_name='text')
@@ -4529,6 +4529,32 @@ 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_add_member
+args: 1,7,3
+arg: Str('cn', attribute=True, cli_name='vault_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: Str('group*', alwaysask=True, cli_name='groups', csv=True)
+option: Flag('no_members', autofill=True, default=False, exclude='webui')
+option: Str('parent_id?', cli_name='parent_id')
+option: Flag('raw', autofill=True, cli_name='raw', default=False, exclude='webui')
+option: Str('user*', alwaysask=True, cli_name='users', csv=True)
+option: Str('version?', exclude='webui')
+output: Output('completed', <type 'int'>, None)
+output: Output('failed', <type 'dict'>, None)
+output: Entry('result', <type 'dict'>, Gettext('A dictionary representing an LDAP entry', domain='ipa', localedir=None))
+command: vault_add_owner
+args: 1,7,3
+arg: Str('cn', attribute=True, cli_name='vault_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: Str('group*', alwaysask=True, cli_name='groups', csv=True)
+option: Flag('no_members', autofill=True, default=False, exclude='webui')
+option: Str('parent_id?', cli_name='parent_id')
+option: Flag('raw', autofill=True, cli_name='raw', default=False, exclude='webui')
+option: Str('user*', alwaysask=True, cli_name='users', csv=True)
+option: Str('version?', exclude='webui')
+output: Output('completed', <type 'int'>, None)
+output: Output('failed', <type 'dict'>, None)
+output: Entry('result', <type 'dict'>, Gettext('A dictionary representing an LDAP entry', domain='ipa', localedir=None))
 command: vault_archive
 args: 1,13,3
 arg: Str('cn', attribute=True, cli_name='vault_name', maxlength=255, multivalue=False, pattern='^[a-zA-Z0-9_.-/]+$', primary_key=True, query=True, required=True)
@@ -4537,7 +4563,7 @@ option: Flag('create?', autofill=True, default=False)
 option: Bytes('data?', cli_name='data')
 option: Str('description?', cli_name='desc')
 option: Str('in?', cli_name='in')
-option: Str('ipavaulttype?', autofill=True, cli_name='type', default=u'standard')
+option: Flag('no_members', autofill=True, default=False, exclude='webui')
 option: Str('nonce?', cli_name='nonce')
 option: Str('parent_id?', cli_name='parent_id')
 option: Flag('raw', autofill=True, cli_name='raw', default=False, exclude='webui')
@@ -4558,11 +4584,12 @@ output: Output('result', <type 'dict'>, None)
 output: Output('summary', (<type 'unicode'>, <type 'NoneType'>), None)
 output: ListOfPrimaryKeys('value', None, None)
 command: vault_find
-args: 1,10,4
+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='vault_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('no_members', autofill=True, default=False, exclude='webui')
 option: Str('parent_id?', cli_name='parent_id')
 option: Flag('pkey_only?', autofill=True, default=False)
 option: Flag('raw', autofill=True, cli_name='raw', default=False, exclude='webui')
@@ -4575,12 +4602,13 @@ output: ListOfEntries('result', (<type 'list'>, <type 'tuple'>), Gettext('A list
 output: Output('summary', (<type 'unicode'>, <type 'NoneType'>), None)
 output: Output('truncated', <type 'bool'>, None)
 command: vault_mod
-args: 1,10,3
+args: 1,11,3
 arg: Str('cn', attribute=True, cli_name='vault_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('no_members', autofill=True, default=False, exclude='webui')
 option: Str('parent_id?', cli_name='parent_id')
 option: Flag('raw', autofill=True, cli_name='raw', default=False, exclude='webui')
 option: Flag('rights', autofill=True, default=False)
@@ -4590,10 +4618,37 @@ 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_remove_member
+args: 1,7,3
+arg: Str('cn', attribute=True, cli_name='vault_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: Str('group*', alwaysask=True, cli_name='groups', csv=True)
+option: Flag('no_members', autofill=True, default=False, exclude='webui')
+option: Str('parent_id?', cli_name='parent_id')
+option: Flag('raw', autofill=True, cli_name='raw', default=False, exclude='webui')
+option: Str('user*', alwaysask=True, cli_name='users', csv=True)
+option: Str('version?', exclude='webui')
+output: Output('completed', <type 'int'>, None)
+output: Output('failed', <type 'dict'>, None)
+output: Entry('result', <type 'dict'>, Gettext('A dictionary representing an LDAP entry', domain='ipa', localedir=None))
+command: vault_remove_owner
+args: 1,7,3
+arg: Str('cn', attribute=True, cli_name='vault_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: Str('group*', alwaysask=True, cli_name='groups', csv=True)
+option: Flag('no_members', autofill=True, default=False, exclude='webui')
+option: Str('parent_id?', cli_name='parent_id')
+option: Flag('raw', autofill=True, cli_name='raw', default=False, exclude='webui')
+option: Str('user*', alwaysask=True, cli_name='users', csv=True)
+option: Str('version?', exclude='webui')
+output: Output('completed', <type 'int'>, None)
+output: Output('failed', <type 'dict'>, None)
+output: Entry('result', <type 'dict'>, Gettext('A dictionary representing an LDAP entry', domain='ipa', localedir=None))
 command: vault_retrieve
-args: 1,8,3
+args: 1,9,3
 arg: Str('cn', attribute=True, cli_name='vault_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('no_members', autofill=True, default=False, exclude='webui')
 option: Str('out?', cli_name='out')
 option: Str('parent_id?', cli_name='parent_id')
 option: Flag('raw', autofill=True, cli_name='raw', default=False, exclude='webui')
@@ -4605,9 +4660,10 @@ output: Entry('result', <type 'dict'>, Gettext('A dictionary representing an LDA
 output: Output('summary', (<type 'unicode'>, <type 'NoneType'>), None)
 output: PrimaryKey('value', None, None)
 command: vault_show
-args: 1,5,3
+args: 1,6,3
 arg: Str('cn', attribute=True, cli_name='vault_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('no_members', autofill=True, default=False, exclude='webui')
 option: Str('parent_id?', cli_name='parent_id')
 option: Flag('raw', autofill=True, cli_name='raw', default=False, exclude='webui')
 option: Flag('rights', autofill=True, default=False)
@@ -4621,12 +4677,13 @@ option: Str('out?', cli_name='out')
 option: Str('version?', exclude='webui')
 output: Output('result', None, None)
 command: vaultcontainer_add
-args: 1,8,3
+args: 1,9,3
 arg: Str('cn', attribute=True, cli_name='container_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('container_id', attribute=False, cli_name='container_id', multivalue=False, required=False)
 option: Str('description', attribute=True, cli_name='desc', multivalue=False, required=False)
+option: Flag('no_members', autofill=True, default=False, exclude='webui')
 option: Str('parent_id?', cli_name='parent_id')
 option: Flag('raw', autofill=True, cli_name='raw', default=False, exclude='webui')
 option: Str('setattr*', cli_name='setattr', exclude='webui')
@@ -4634,6 +4691,32 @@ 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: vaultcontainer_add_member
+args: 1,7,3
+arg: Str('cn', attribute=True, cli_name='container_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: Str('group*', alwaysask=True, cli_name='groups', csv=True)
+option: Flag('no_members', autofill=True, default=False, exclude='webui')
+option: Str('parent_id?', cli_name='parent_id')
+option: Flag('raw', autofill=True, cli_name='raw', default=False, exclude='webui')
+option: Str('user*', alwaysask=True, cli_name='users', csv=True)
+option: Str('version?', exclude='webui')
+output: Output('completed', <type 'int'>, None)
+output: Output('failed', <type 'dict'>, None)
+output: Entry('result', <type 'dict'>, Gettext('A dictionary representing an LDAP entry', domain='ipa', localedir=None))
+command: vaultcontainer_add_owner
+args: 1,7,3
+arg: Str('cn', attribute=True, cli_name='container_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: Str('group*', alwaysask=True, cli_name='groups', csv=True)
+option: Flag('no_members', autofill=True, default=False, exclude='webui')
+option: Str('parent_id?', cli_name='parent_id')
+option: Flag('raw', autofill=True, cli_name='raw', default=False, exclude='webui')
+option: Str('user*', alwaysask=True, cli_name='users', csv=True)
+option: Str('version?', exclude='webui')
+output: Output('completed', <type 'int'>, None)
+output: Output('failed', <type 'dict'>, None)
+output: Entry('result', <type 'dict'>, Gettext('A dictionary representing an LDAP entry', domain='ipa', localedir=None))
 command: vaultcontainer_del
 args: 1,4,3
 arg: Str('cn', attribute=True, cli_name='container_name', maxlength=255, multivalue=False, pattern='^[a-zA-Z0-9_.-/]+$', primary_key=True, query=True, required=True)
@@ -4645,12 +4728,13 @@ output: Output('result', <type 'dict'>, None)
 output: Output('summary', (<type 'unicode'>, <type 'NoneType'>), None)
 output: ListOfPrimaryKeys('value', None, None)
 command: vaultcontainer_find
-args: 1,10,4
+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='container_name', maxlength=255, multivalue=False, pattern='^[a-zA-Z0-9_.-/]+$', primary_key=True, query=True, required=False)
 option: Str('container_id', attribute=False, autofill=False, cli_name='container_id', multivalue=False, query=True, required=False)
 option: Str('description', attribute=True, autofill=False, cli_name='desc', multivalue=False, query=True, required=False)
+option: Flag('no_members', autofill=True, default=False, exclude='webui')
 option: Str('parent_id?', cli_name='parent_id')
 option: Flag('pkey_only?', autofill=True, default=False)
 option: Flag('raw', autofill=True, cli_name='raw', default=False, exclude='webui')
@@ -4662,13 +4746,14 @@ output: ListOfEntries('result', (<type 'list'>, <type 'tuple'>), Gettext('A list
 output: Output('summary', (<type 'unicode'>, <type 'NoneType'>), None)
 output: Output('truncated', <type 'bool'>, None)
 command: vaultcontainer_mod
-args: 1,10,3
+args: 1,11,3
 arg: Str('cn', attribute=True, cli_name='container_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('container_id', attribute=False, autofill=False, cli_name='container_id', multivalue=False, required=False)
 option: Str('delattr*', cli_name='delattr', exclude='webui')
 option: Str('description', attribute=True, autofill=False, cli_name='desc', multivalue=False, required=False)
+option: Flag('no_members', autofill=True, default=False, exclude='webui')
 option: Str('parent_id?', cli_name='parent_id')
 option: Flag('raw', autofill=True, cli_name='raw', default=False, exclude='webui')
 option: Flag('rights', autofill=True, default=False)
@@ -4677,10 +4762,37 @@ 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: vaultcontainer_remove_member
+args: 1,7,3
+arg: Str('cn', attribute=True, cli_name='container_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: Str('group*', alwaysask=True, cli_name='groups', csv=True)
+option: Flag('no_members', autofill=True, default=False, exclude='webui')
+option: Str('parent_id?', cli_name='parent_id')
+option: Flag('raw', autofill=True, cli_name='raw', default=False, exclude='webui')
+option: Str('user*', alwaysask=True, cli_name='users', csv=True)
+option: Str('version?', exclude='webui')
+output: Output('completed', <type 'int'>, None)
+output: Output('failed', <type 'dict'>, None)
+output: Entry('result', <type 'dict'>, Gettext('A dictionary representing an LDAP entry', domain='ipa', localedir=None))
+command: vaultcontainer_remove_owner
+args: 1,7,3
+arg: Str('cn', attribute=True, cli_name='container_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: Str('group*', alwaysask=True, cli_name='groups', csv=True)
+option: Flag('no_members', autofill=True, default=False, exclude='webui')
+option: Str('parent_id?', cli_name='parent_id')
+option: Flag('raw', autofill=True, cli_name='raw', default=False, exclude='webui')
+option: Str('user*', alwaysask=True, cli_name='users', csv=True)
+option: Str('version?', exclude='webui')
+output: Output('completed', <type 'int'>, None)
+output: Output('failed', <type 'dict'>, None)
+output: Entry('result', <type 'dict'>, Gettext('A dictionary representing an LDAP entry', domain='ipa', localedir=None))
 command: vaultcontainer_show
-args: 1,5,3
+args: 1,6,3
 arg: Str('cn', attribute=True, cli_name='container_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('no_members', autofill=True, default=False, exclude='webui')
 option: Str('parent_id?', cli_name='parent_id')
 option: Flag('raw', autofill=True, cli_name='raw', default=False, exclude='webui')
 option: Flag('rights', autofill=True, default=False)
diff --git a/install/share/60basev3.ldif b/install/share/60basev3.ldif
index c04f8d5d096bc3d91aec3e2f1703f658d76d3779..cdabfac35488bda184af564a174306a681183084 100644
--- a/install/share/60basev3.ldif
+++ b/install/share/60basev3.ldif
@@ -77,5 +77,5 @@ 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' )
-objectClasses: (2.16.840.1.113730.3.8.18.1.2 NAME 'ipaVaultContainer' DESC 'IPA vault container' SUP top STRUCTURAL MUST ( cn ) MAY ( description ) X-ORIGIN 'IPA v4.2' )
+objectClasses: (2.16.840.1.113730.3.8.18.1.1 NAME 'ipaVault' DESC 'IPA vault' SUP top STRUCTURAL MUST ( cn ) MAY ( description $ owner $ member ) X-ORIGIN 'IPA v4.2' )
+objectClasses: (2.16.840.1.113730.3.8.18.1.2 NAME 'ipaVaultContainer' DESC 'IPA vault container' SUP top STRUCTURAL MUST ( cn ) MAY ( description $ owner $ member ) X-ORIGIN 'IPA v4.2' )
diff --git a/install/updates/40-vault.update b/install/updates/40-vault.update
index dac2f67112dc33f012c6d559285464fb7c944d1a..95915f77e5366b75de23409e5554598e7495b5df 100644
--- a/install/updates/40-vault.update
+++ b/install/updates/40-vault.update
@@ -3,6 +3,23 @@ default: objectClass: top
 default: objectClass: ipaVaultContainer
 default: cn: vaults
 default: description: Root vault container
+default: aci: (target="ldap:///cn=*,cn=users,cn=vaults,$SUFFIX")(targetattr="*")(version 3.0; acl "Allow users to create private container"; allow (add) userdn = "ldap:///uid=($$attr.cn),cn=users,cn=accounts,$SUFFIX";)
+default: aci: (targetfilter="(objectClass=ipaVaultContainer)")(targetattr="*")(version 3.0; acl "Container members can access the container"; allow(read, search, compare) userattr="member#USERDN";)
+default: aci: (targetfilter="(objectClass=ipaVaultContainer)")(targetattr="*")(version 3.0; acl "Indirect container members can access the container"; allow(read, search, compare) userattr="member#GROUPDN";)
+default: aci: (targetfilter="(objectClass=ipaVaultContainer)")(targetattr="*")(version 3.0; acl "Container members can access sub-containers"; allow(read, search, compare) userattr="parent[1].member#USERDN";)
+default: aci: (targetfilter="(objectClass=ipaVaultContainer)")(targetattr="*")(version 3.0; acl "Indirect container members can access sub-containers"; allow(read, search, compare) userattr="parent[1].member#GROUPDN";)
+default: aci: (targetfilter="(objectClass=ipaVaultContainer)")(targetattr="*")(version 3.0; acl "Container owners can manage the container"; allow(read, search, compare, write) userattr="owner#USERDN";)
+default: aci: (targetfilter="(objectClass=ipaVaultContainer)")(targetattr="*")(version 3.0; acl "Indirect container owners can manage the container"; allow(read, search, compare, write) userattr="owner#GROUPDN";)
+default: aci: (targetfilter="(objectClass=ipaVaultContainer)")(targetattr="*")(version 3.0; acl "Container owners can manage sub-containers"; allow(read, search, compare, add, delete) userattr="parent[1].owner#USERDN";)
+default: aci: (targetfilter="(objectClass=ipaVaultContainer)")(targetattr="*")(version 3.0; acl "Indirect container owners can manage sub-containers"; allow(read, search, compare, add, delete) userattr="parent[1].owner#GROUPDN";)
+default: aci: (targetfilter="(objectClass=ipaVault)")(targetattr="*")(version 3.0; acl "Container members can access vaults in the container"; allow(read, search, compare) userattr="parent[1].member#USERDN";)
+default: aci: (targetfilter="(objectClass=ipaVault)")(targetattr="*")(version 3.0; acl "Indirect container members can access vaults in the container"; allow(read, search, compare) userattr="parent[1].member#GROUPDN";)
+default: aci: (targetfilter="(objectClass=ipaVault)")(targetattr="*")(version 3.0; acl "Container owners can manage vaults in the container"; allow(read, search, compare, add, delete) userattr="parent[1].owner#USERDN";)
+default: aci: (targetfilter="(objectClass=ipaVault)")(targetattr="*")(version 3.0; acl "Indirect container owners can manage vaults in the container"; allow(read, search, compare, add, delete) userattr="parent[1].owner#GROUPDN";)
+default: aci: (targetfilter="(objectClass=ipaVault)")(targetattr="*")(version 3.0; acl "Vault members can access the vault"; allow(read, search, compare) userattr="member#USERDN";)
+default: aci: (targetfilter="(objectClass=ipaVault)")(targetattr="*")(version 3.0; acl "Indirect vault members can access the vault"; allow(read, search, compare) userattr="member#GROUPDN";)
+default: aci: (targetfilter="(objectClass=ipaVault)")(targetattr="*")(version 3.0; acl "Vault owners can manage the vault"; allow(read, search, compare, write) userattr="owner#USERDN";)
+default: aci: (targetfilter="(objectClass=ipaVault)")(targetattr="*")(version 3.0; acl "Indirect vault owners can manage the vault"; allow(read, search, compare, write) userattr="owner#GROUPDN";)
 
 dn: cn=services,cn=vaults,$SUFFIX
 default: objectClass: top
diff --git a/ipalib/plugins/vault.py b/ipalib/plugins/vault.py
index 9945581bed2844e753e8b0ec9ac12af2374ab7bd..ae4c6fe5cae4b7d6c41464cebcb3ec820912efe3 100644
--- a/ipalib/plugins/vault.py
+++ b/ipalib/plugins/vault.py
@@ -35,7 +35,8 @@ from ipalib import Str, Bytes, Flag
 from ipalib import output
 from ipalib.plugable import Registry
 from ipalib.plugins.baseldap import LDAPObject, LDAPCreate, LDAPDelete,\
-    LDAPSearch, LDAPUpdate, LDAPRetrieve, LDAPQuery, LDAPMultiQuery
+    LDAPSearch, LDAPUpdate, LDAPRetrieve, LDAPQuery, LDAPMultiQuery,\
+    LDAPAddMember, LDAPRemoveMember
 from ipalib.request import context
 from ipalib.plugins.user import split_principal
 from ipalib import _, ngettext
@@ -73,6 +74,18 @@ EXAMPLES:
 """) + _("""
  Delete a vault:
    ipa vault-del MyVault
+""") + _("""
+ Add a vault owner:
+   ipa vault-add-owner MyVault --users testuser
+""") + _("""
+ Delete a vault owner:
+   ipa vault-remove-owner MyVault --users testuser
+""") + _("""
+ Add a vault member:
+   ipa vault-add-member MyVault --users testuser
+""") + _("""
+ Delete a vault member:
+   ipa vault-remove-member MyVault --users testuser
 """)
 
 register = Registry()
@@ -94,12 +107,18 @@ class vault(LDAPObject):
         'cn',
         'vault_id',
         'description',
+        'owner',
+        'member',
     ]
     search_display_attributes = [
         'cn',
         'vault_id',
         'description',
     ]
+    attribute_members = {
+        'owner': ['user', 'group'],
+        'member': ['user', 'group'],
+    }
 
     label = _('Vaults')
     label_singular = _('Vault')
@@ -258,7 +277,7 @@ class vault(LDAPObject):
 
         return (args, options)
 
-    def create_entry(self, dn, description=None):
+    def create_entry(self, dn, description=None, owner=None):
         """
         Creates vault entry and its parents.
         """
@@ -270,6 +289,7 @@ class vault(LDAPObject):
                 'objectclass': self.object_class,
                 'cn': rdn['cn'],
                 'description': description,
+                'owner': owner,
             })
 
         # if entry can be added return
@@ -282,7 +302,7 @@ class vault(LDAPObject):
 
         # otherwise, create parent entry first
         parent_dn = DN(*dn[1:])
-        self.api.Object.vaultcontainer.create_entry(parent_dn)
+        self.api.Object.vaultcontainer.create_entry(parent_dn, owner=owner)
 
         # then create the entry itself
         self.backend.add_entry(entry)
@@ -309,13 +329,6 @@ class vault_add(LDAPQuery):
             cli_name='desc',
             doc=_('Vault description'),
         ),
-        Str(
-            'ipavaulttype?',
-            cli_name='type',
-            doc=_('Vault type'),
-            default=u'standard',
-            autofill=True,
-        ),
         Bytes(
             'data?',
             cli_name='data',
@@ -412,6 +425,17 @@ class vault_find(LDAPSearch):
         ),
     )
 
+    has_output_params = LDAPSearch.has_output_params + (
+        Str(
+            'owner_user',
+            label=_('Owner users'),
+        ),
+        Str(
+            'owner_group',
+            label=_('Owner groups'),
+        ),
+    )
+
     msg_summary = ngettext(
         '%(count)d vault matched',
         '%(count)d vaults matched',
@@ -502,6 +526,17 @@ class vault_show(LDAPRetrieve):
         ),
     )
 
+    has_output_params = LDAPRetrieve.has_output_params + (
+        Str(
+            'owner_user',
+            label=_('Owner users'),
+        ),
+        Str(
+            'owner_group',
+            label=_('Owner groups'),
+        ),
+    )
+
     def params_2_args_options(self, **params):
         (args, options) = super(vault_show, self)\
             .params_2_args_options(**params)
@@ -580,13 +615,6 @@ class vault_archive(LDAPQuery):
             cli_name='desc',
             doc=_('Vault description'),
         ),
-        Str(
-            'ipavaulttype?',
-            cli_name='type',
-            doc=_('Vault type'),
-            default=u'standard',
-            autofill=True,
-        ),
         Bytes(
             'data?',
             cli_name='data',
@@ -725,10 +753,16 @@ class vault_archive(LDAPQuery):
         if create:
             description = options.get('description')
 
+            # get user
+            principal = getattr(context, 'principal')
+            (username, realm) = split_principal(principal)
+            owner_dn = self.api.Object.user.get_dn(username)
+
             # creating new vault
             self.obj.create_entry(
                 dn,
                 description=description,
+                owner=owner_dn,
             )
 
         # retrieve vault info
@@ -954,3 +988,113 @@ class vault_retrieve(LDAPQuery):
         response['summary'] = self.msg_summary % response
 
         return response
+
+
+ at register()
+class vault_add_owner(LDAPAddMember):
+    __doc__ = _('Add owners to a vault.')
+
+    takes_options = LDAPAddMember.takes_options + (
+        Str(
+            'parent_id?',
+            cli_name='parent_id',
+            doc=_('Parent ID'),
+        ),
+    )
+
+    member_attributes = ['owner']
+    member_count_out = ('%i owner added.', '%i owners added.')
+
+    def params_2_args_options(self, **params):
+        (args, options) = super(vault_add_owner, self)\
+            .params_2_args_options(**params)
+        return self.obj.normalize_params(*args, **options)
+
+    def post_callback(
+            self, ldap, completed, failed, dn, entry_attrs, *args, **options):
+        assert isinstance(dn, DN)
+
+        entry_attrs['vault_id'] = self.obj.get_id(dn)
+
+        return (completed, dn)
+
+
+ at register()
+class vault_remove_owner(LDAPRemoveMember):
+    __doc__ = _('Remove owners from a vault.')
+
+    takes_options = LDAPRemoveMember.takes_options + (
+        Str(
+            'parent_id?',
+            cli_name='parent_id',
+            doc=_('Parent ID'),
+        ),
+    )
+
+    member_attributes = ['owner']
+    member_count_out = ('%i owner removed.', '%i owners removed.')
+
+    def params_2_args_options(self, **params):
+        (args, options) = super(vault_remove_owner, self)\
+            .params_2_args_options(**params)
+        return self.obj.normalize_params(*args, **options)
+
+    def post_callback(
+            self, ldap, completed, failed, dn, entry_attrs, *args, **options):
+        assert isinstance(dn, DN)
+
+        entry_attrs['vault_id'] = self.obj.get_id(dn)
+
+        return (completed, dn)
+
+
+ at register()
+class vault_add_member(LDAPAddMember):
+    __doc__ = _('Add members to a vault.')
+
+    takes_options = LDAPAddMember.takes_options + (
+        Str(
+            'parent_id?',
+            cli_name='parent_id',
+            doc=_('Parent ID'),
+        ),
+    )
+
+    def params_2_args_options(self, **params):
+        (args, options) = super(vault_add_member, self)\
+            .params_2_args_options(**params)
+        return self.obj.normalize_params(*args, **options)
+
+    def post_callback(
+            self, ldap, completed, failed, dn, entry_attrs, *args, **options):
+        assert isinstance(dn, DN)
+
+        entry_attrs['vault_id'] = self.obj.get_id(dn)
+
+        return (completed, dn)
+
+
+ at register()
+class vault_remove_member(LDAPRemoveMember):
+    __doc__ = _('Remove members from a vault.')
+
+    takes_options = LDAPRemoveMember.takes_options + (
+        Str(
+            'parent_id?',
+            cli_name='parent_id',
+            doc=_('Parent ID'),
+        ),
+    )
+
+    def params_2_args_options(self, **params):
+        (args, options) = super(vault_remove_member, self)\
+            .params_2_args_options(**params)
+        return self.obj.normalize_params(*args, **options)
+
+    def post_callback(
+            self, ldap, completed, failed, dn, entry_attrs, *args, **options):
+        assert isinstance(dn, DN)
+
+        entry_attrs['vault_id'] = self.obj.get_id(dn)
+
+        return (completed, dn)
diff --git a/ipalib/plugins/vaultcontainer.py b/ipalib/plugins/vaultcontainer.py
index 577d9d8a3cde5e3e55401f51d343a07bb2a1687d..4aad9b7b25f2f633fa7c2ac21085839241664a5e 100644
--- a/ipalib/plugins/vaultcontainer.py
+++ b/ipalib/plugins/vaultcontainer.py
@@ -23,7 +23,8 @@ from ipalib import api, errors
 from ipalib import Str, Flag
 from ipalib.plugable import Registry
 from ipalib.plugins.baseldap import LDAPObject, LDAPCreate, LDAPDelete,\
-    LDAPSearch, LDAPUpdate, LDAPRetrieve, LDAPMultiQuery
+    LDAPSearch, LDAPUpdate, LDAPRetrieve, LDAPMultiQuery, LDAPAddMember,\
+    LDAPRemoveMember
 from ipalib.request import context
 from ipalib.plugins.user import split_principal
 from ipalib import _, ngettext
@@ -56,6 +57,18 @@ EXAMPLES:
 """) + _("""
  Delete a vault container:
    ipa vaultcontainer-del MyContainer
+""") + _("""
+ Add a vault container owner:
+   ipa vaultcontainer-add-owner MyContainer --users testuser
+""") + _("""
+ Delete a vault container owner:
+   ipa vaultcontainer-remove-owner MyContainer --users testuser
+""") + _("""
+ Add a vault container member:
+   ipa vaultcontainer-add-member MyContainer --users testuser
+""") + _("""
+ Delete a vault container member:
+   ipa vaultcontainer-remove-member MyContainer --users testuser
 """)
 
 register = Registry()
@@ -75,12 +88,18 @@ class vaultcontainer(LDAPObject):
         'cn',
         'container_id',
         'description',
+        'owner',
+        'member',
     ]
     search_display_attributes = [
         'cn',
         'container_id',
         'description',
     ]
+    attribute_members = {
+        'owner': ['user', 'group'],
+        'member': ['user', 'group'],
+    }
 
     label = _('Vault Containers')
     label_singular = _('Vault Container')
@@ -261,7 +280,7 @@ class vaultcontainer(LDAPObject):
 
         return (args, options)
 
-    def create_entry(self, dn):
+    def create_entry(self, dn, owner=None):
         """
         Creates a container entry and its parents.
         """
@@ -272,6 +291,7 @@ class vaultcontainer(LDAPObject):
             {
                 'objectclass': self.object_class,
                 'cn': rdn['cn'],
+                'owner': owner,
             })
 
         # if entry can be added return
@@ -284,7 +304,7 @@ class vaultcontainer(LDAPObject):
 
         # otherwise, create parent entry first
         parent_dn = DN(*dn[1:])
-        self.create_entry(parent_dn)
+        self.create_entry(parent_dn, owner=owner)
 
         # then create the entry itself
         self.backend.add_entry(entry)
@@ -317,10 +337,17 @@ class vaultcontainer_add(LDAPCreate):
         container_id = self.obj.get_id(dn)
         (container_name, parent_id) = self.obj.split_id(container_id)
 
+        # get user
+        principal = getattr(context, 'principal')
+        (username, realm) = split_principal(principal)
+        owner_dn = self.api.Object.user.get_dn(username)
+        entry_attrs['owner'] = owner_dn
+
         # parent is user's private container, create parent
         if parent_id == self.obj.get_private_id():
             try:
-                self.obj.create_entry(DN(*dn[1:]))
+                self.obj.create_entry(
+                    DN(*dn[1:]), owner=owner_dn)
             except errors.DuplicateEntry:
                 pass
 
@@ -405,6 +432,17 @@ class vaultcontainer_find(LDAPSearch):
         ),
     )
 
+    has_output_params = LDAPSearch.has_output_params + (
+        Str(
+            'owner_user',
+            label=_('Owner users'),
+        ),
+        Str(
+            'owner_group',
+            label=_('Owner groups'),
+        ),
+    )
+
     msg_summary = ngettext(
         '%(count)d vault container matched',
         '%(count)d vault containers matched',
@@ -490,6 +528,17 @@ class vaultcontainer_show(LDAPRetrieve):
         ),
     )
 
+    has_output_params = LDAPRetrieve.has_output_params + (
+        Str(
+            'owner_user',
+            label=_('Owner users'),
+        ),
+        Str(
+            'owner_group',
+            label=_('Owner groups'),
+        ),
+    )
+
     def params_2_args_options(self, **params):
         (args, options) = super(vaultcontainer_show, self)\
             .params_2_args_options(**params)
@@ -501,3 +550,113 @@ class vaultcontainer_show(LDAPRetrieve):
         entry_attrs['container_id'] = self.obj.get_id(dn)
 
         return dn
+
+
+ at register()
+class vaultcontainer_add_owner(LDAPAddMember):
+    __doc__ = _('Add owners to a vault container.')
+
+    takes_options = LDAPAddMember.takes_options + (
+        Str(
+            'parent_id?',
+            cli_name='parent_id',
+            doc=_('Parent ID'),
+        ),
+    )
+
+    member_attributes = ['owner']
+    member_count_out = ('%i owner added.', '%i owners added.')
+
+    def params_2_args_options(self, **params):
+        (args, options) = super(vaultcontainer_add_owner, self)\
+            .params_2_args_options(**params)
+        return self.obj.normalize_params(*args, **options)
+
+    def post_callback(
+            self, ldap, completed, failed, dn, entry_attrs, *args, **options):
+        assert isinstance(dn, DN)
+
+        entry_attrs['container_id'] = self.obj.get_id(dn)
+
+        return (completed, dn)
+
+
+ at register()
+class vaultcontainer_remove_owner(LDAPRemoveMember):
+    __doc__ = _('Remove owners from a vault container.')
+
+    takes_options = LDAPRemoveMember.takes_options + (
+        Str(
+            'parent_id?',
+            cli_name='parent_id',
+            doc=_('Parent ID'),
+        ),
+    )
+
+    member_attributes = ['owner']
+    member_count_out = ('%i owner removed.', '%i owners removed.')
+
+    def params_2_args_options(self, **params):
+        (args, options) = super(vaultcontainer_remove_owner, self)\
+            .params_2_args_options(**params)
+        return self.obj.normalize_params(*args, **options)
+
+    def post_callback(
+            self, ldap, completed, failed, dn, entry_attrs, *args, **options):
+        assert isinstance(dn, DN)
+
+        entry_attrs['container_id'] = self.obj.get_id(dn)
+
+        return (completed, dn)
+
+
+ at register()
+class vaultcontainer_add_member(LDAPAddMember):
+    __doc__ = _('Add members to a vault container.')
+
+    takes_options = LDAPAddMember.takes_options + (
+        Str(
+            'parent_id?',
+            cli_name='parent_id',
+            doc=_('Parent ID'),
+        ),
+    )
+
+    def params_2_args_options(self, **params):
+        (args, options) = super(vaultcontainer_add_member, self)\
+            .params_2_args_options(**params)
+        return self.obj.normalize_params(*args, **options)
+
+    def post_callback(
+            self, ldap, completed, failed, dn, entry_attrs, *args, **options):
+        assert isinstance(dn, DN)
+
+        entry_attrs['container_id'] = self.obj.get_id(dn)
+
+        return (completed, dn)
+
+
+ at register()
+class vaultcontainer_remove_member(LDAPRemoveMember):
+    __doc__ = _('Remove members from a vault container.')
+
+    takes_options = LDAPRemoveMember.takes_options + (
+        Str(
+            'parent_id?',
+            cli_name='parent_id',
+            doc=_('Parent ID'),
+        ),
+    )
+
+    def params_2_args_options(self, **params):
+        (args, options) = super(vaultcontainer_remove_member, self)\
+            .params_2_args_options(**params)
+        return self.obj.normalize_params(*args, **options)
+
+    def post_callback(
+            self, ldap, completed, failed, dn, entry_attrs, *args, **options):
+        assert isinstance(dn, DN)
+
+        entry_attrs['container_id'] = self.obj.get_id(dn)
+
+        return (completed, dn)
diff --git a/ipatests/test_xmlrpc/test_vault_plugin.py b/ipatests/test_xmlrpc/test_vault_plugin.py
index 1b3d864bfdb2c911cdb9021d79f9ce07eb72b06e..4c46cf7783071873b6accf9b63a0f819b720103e 100644
--- a/ipatests/test_xmlrpc/test_vault_plugin.py
+++ b/ipatests/test_xmlrpc/test_vault_plugin.py
@@ -55,6 +55,7 @@ class test_vault_plugin(Declarative):
                           % (test_vault, api.env.basedn),
                     'cn': [test_vault],
                     'vault_id': u'/users/admin/%s' % test_vault,
+                    'owner_user': [u'admin'],
                 },
             },
         },
@@ -107,6 +108,7 @@ class test_vault_plugin(Declarative):
                           % (test_vault, api.env.basedn),
                     'cn': [test_vault],
                     'vault_id': u'/users/admin/%s' % test_vault,
+                    'owner_user': [u'admin'],
                 },
             },
         },
@@ -127,6 +129,7 @@ class test_vault_plugin(Declarative):
                     'cn': [test_vault],
                     'vault_id': u'/users/admin/%s' % test_vault,
                     'description': [u'Test vault'],
+                    'owner_user': [u'admin'],
                 },
             },
         },
@@ -149,6 +152,7 @@ class test_vault_plugin(Declarative):
                     'cn': [test_vault],
                     'vault_id': u'/users/admin/%s' % test_vault,
                     'description': [u'Test vault'],
+                    'owner_user': [u'admin'],
                 },
             },
         },
@@ -169,6 +173,7 @@ class test_vault_plugin(Declarative):
                     'cn': [test_vault],
                     'vault_id': u'/users/admin/%s' % test_vault,
                     'description': [u'Test vault'],
+                    'owner_user': [u'admin'],
                     'nonce': fuzzy_string,
                     'vault_data': fuzzy_string,
                     'data': binary_data,
@@ -194,6 +199,7 @@ class test_vault_plugin(Declarative):
                     'cn': [test_vault],
                     'vault_id': u'/users/admin/%s' % test_vault,
                     'description': [u'Test vault'],
+                    'owner_user': [u'admin'],
                 },
             },
         },
@@ -216,6 +222,7 @@ class test_vault_plugin(Declarative):
                     'cn': [test_vault],
                     'vault_id': u'/users/admin/%s' % test_vault,
                     'description': [u'Test vault'],
+                    'owner_user': [u'admin'],
                     'nonce': fuzzy_string,
                     'vault_data': fuzzy_string,
                     'text': text_data,
@@ -265,6 +272,7 @@ class test_vault_plugin(Declarative):
                           % (test_vault, api.env.basedn),
                     'cn': [test_vault],
                     'vault_id': shared_test_vault,
+                    'owner_user': [u'admin'],
                 },
             },
         },
@@ -306,6 +314,7 @@ class test_vault_plugin(Declarative):
                           % (test_vault, api.env.basedn),
                     'cn': [test_vault],
                     'vault_id': shared_test_vault,
+                    'owner_user': [u'admin'],
                 },
             },
         },
@@ -326,6 +335,7 @@ class test_vault_plugin(Declarative):
                     'cn': [test_vault],
                     'vault_id': shared_test_vault,
                     'description': [u'Test vault'],
+                    'owner_user': [u'admin'],
                 },
             },
         },
@@ -348,6 +358,7 @@ class test_vault_plugin(Declarative):
                     'cn': [test_vault],
                     'vault_id': shared_test_vault,
                     'description': [u'Test vault'],
+                    'owner_user': [u'admin'],
                 },
             },
         },
@@ -368,6 +379,7 @@ class test_vault_plugin(Declarative):
                     'cn': [test_vault],
                     'vault_id': shared_test_vault,
                     'description': [u'Test vault'],
+                    'owner_user': [u'admin'],
                     'nonce': fuzzy_string,
                     'vault_data': fuzzy_string,
                     'data': binary_data,
diff --git a/ipatests/test_xmlrpc/test_vaultcontainer_plugin.py b/ipatests/test_xmlrpc/test_vaultcontainer_plugin.py
index 8cd21458fe1dae64773593dab1a0833e1a233d67..b7c79618a955dc68b89628e402849a865a9e4388 100644
--- a/ipatests/test_xmlrpc/test_vaultcontainer_plugin.py
+++ b/ipatests/test_xmlrpc/test_vaultcontainer_plugin.py
@@ -98,6 +98,7 @@ class test_vaultcontainer_plugin(Declarative):
                     'objectclass': (u'ipaVaultContainer', u'top'),
                     'cn': [private_container],
                     'container_id': u'/users/admin/%s/' % private_container,
+                    'owner_user': [u'admin'],
                 },
             },
         },
@@ -152,6 +153,7 @@ class test_vaultcontainer_plugin(Declarative):
                           % (private_container, api.env.basedn),
                     'cn': [private_container],
                     'container_id': u'/users/admin/%s/' % private_container,
+                    'owner_user': [u'admin'],
                 },
             },
         },
@@ -172,6 +174,7 @@ class test_vaultcontainer_plugin(Declarative):
                     'cn': [private_container],
                     'container_id': u'/users/admin/%s/' % private_container,
                     'description': [u'Private container'],
+                    'owner_user': [u'admin'],
                 },
             },
         },
@@ -219,6 +222,7 @@ class test_vaultcontainer_plugin(Declarative):
                     'objectclass': (u'ipaVaultContainer', u'top'),
                     'cn': [test_container],
                     'container_id': u'/shared/%s/' % test_container,
+                    'owner_user': [u'admin'],
                 },
             },
         },
@@ -260,6 +264,7 @@ class test_vaultcontainer_plugin(Declarative):
                           % (test_container, api.env.basedn),
                     'cn': [test_container],
                     'container_id': u'/shared/%s/' % test_container,
+                    'owner_user': [u'admin'],
                 },
             },
         },
@@ -280,6 +285,7 @@ class test_vaultcontainer_plugin(Declarative):
                     'cn': [test_container],
                     'container_id': u'/shared/%s/' % test_container,
                     'description': [u'shared container'],
+                    'owner_user': [u'admin'],
                 },
             },
         },
@@ -316,6 +322,7 @@ class test_vaultcontainer_plugin(Declarative):
                     'objectclass': (u'ipaVaultContainer', u'top'),
                     'cn': [test_container],
                     'container_id': u'/services/%s/' % test_container,
+                    'owner_user': [u'admin'],
                 },
             },
         },
@@ -335,6 +342,7 @@ class test_vaultcontainer_plugin(Declarative):
                     'objectclass': (u'ipaVaultContainer', u'top'),
                     'cn': [base_container],
                     'container_id': u'/users/admin/%s/' % base_container,
+                    'owner_user': [u'admin'],
                 },
             },
         },
@@ -358,6 +366,7 @@ class test_vaultcontainer_plugin(Declarative):
                     'cn': [child_container],
                     'container_id': u'/users/admin/%s/%s/'
                                     % (base_container, child_container),
+                    'owner_user': [u'admin'],
                 },
             },
         },
@@ -388,6 +397,7 @@ class test_vaultcontainer_plugin(Declarative):
                                         base_container,
                                         child_container,
                                         grandchild_container),
+                    'owner_user': [u'admin'],
                 },
             },
         },
-- 
2.3.1

-------------- next part --------------
>From 4859732dbb43da17cfeaf6a431ee40afb6bbcce1 Mon Sep 17 00:00:00 2001
From: "Endi S. Dewata" <edewata at redhat.com>
Date: Fri, 24 Oct 2014 19:53:16 -0400
Subject: [PATCH] Added symmetric and asymmetric vaults.

The vault plugin has been modified to support symmetric and
asymmetric vaults for additional layer of security. It will
use python-cryptography for the crypto functionality. New
LDAP attribute types have been added to store vault type,
salt and public key. New test cases have been added as well.

https://fedorahosted.org/freeipa/ticket/3872
---
 API.txt                                   |  31 ++-
 freeipa.spec.in                           |   2 +
 install/share/60basev3.ldif               |   4 +-
 ipalib/plugins/vault.py                   | 441 +++++++++++++++++++++++++++++-
 ipatests/test_xmlrpc/test_vault_plugin.py | 240 ++++++++++++++++
 5 files changed, 710 insertions(+), 8 deletions(-)

diff --git a/API.txt b/API.txt
index e7e39ec7e64b0e9878ba8a00e89f4124991adc92..e8912070b96cbb7b53a7811465cfc6cd6bc9059d 100644
--- a/API.txt
+++ b/API.txt
@@ -4515,14 +4515,19 @@ 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
+args: 1,14,3
 arg: Str('cn', attribute=True, cli_name='vault_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: Bytes('data?', cli_name='data')
 option: Str('description?', cli_name='desc')
 option: Str('in?', cli_name='in')
+option: Bytes('ipapublickey?', cli_name='public_key')
+option: Str('ipavaulttype?', autofill=True, cli_name='type', default=u'standard')
 option: Flag('no_members', autofill=True, default=False, exclude='webui')
 option: Str('parent_id?', cli_name='parent_id')
+option: Str('password?', cli_name='password')
+option: Str('password_file?', cli_name='password_file')
+option: Str('public_key_file?', cli_name='public_key_file')
 option: Flag('raw', autofill=True, cli_name='raw', default=False, exclude='webui')
 option: Str('text?', cli_name='text')
 option: Str('version?', exclude='webui')
@@ -4556,16 +4561,22 @@ output: Output('completed', <type 'int'>, None)
 output: Output('failed', <type 'dict'>, None)
 output: Entry('result', <type 'dict'>, Gettext('A dictionary representing an LDAP entry', domain='ipa', localedir=None))
 command: vault_archive
-args: 1,13,3
+args: 1,19,3
 arg: Str('cn', attribute=True, cli_name='vault_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('create?', autofill=True, default=False)
 option: Bytes('data?', cli_name='data')
 option: Str('description?', cli_name='desc')
 option: Str('in?', cli_name='in')
+option: Bytes('ipapublickey?', cli_name='public_key')
+option: Str('ipavaultsalt?', cli_name='salt')
+option: Str('ipavaulttype?', autofill=True, cli_name='type', default=u'standard')
 option: Flag('no_members', autofill=True, default=False, exclude='webui')
 option: Str('nonce?', cli_name='nonce')
 option: Str('parent_id?', cli_name='parent_id')
+option: Str('password?', cli_name='password')
+option: Str('password_file?', cli_name='password_file')
+option: Str('public_key_file?', cli_name='public_key_file')
 option: Flag('raw', autofill=True, cli_name='raw', default=False, exclude='webui')
 option: Str('session_key?', cli_name='session_key')
 option: Str('text?', cli_name='text')
@@ -4584,11 +4595,14 @@ 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
+args: 1,14,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='vault_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: Bytes('ipapublickey', attribute=True, autofill=False, cli_name='public_key', multivalue=False, query=True, required=False)
+option: Str('ipavaultsalt', attribute=True, autofill=False, cli_name='salt', multivalue=False, query=True, required=False)
+option: Str('ipavaulttype', attribute=True, autofill=False, cli_name='type', default=u'standard', multivalue=False, query=True, required=False)
 option: Flag('no_members', autofill=True, default=False, exclude='webui')
 option: Str('parent_id?', cli_name='parent_id')
 option: Flag('pkey_only?', autofill=True, default=False)
@@ -4602,12 +4616,15 @@ output: ListOfEntries('result', (<type 'list'>, <type 'tuple'>), Gettext('A list
 output: Output('summary', (<type 'unicode'>, <type 'NoneType'>), None)
 output: Output('truncated', <type 'bool'>, None)
 command: vault_mod
-args: 1,11,3
+args: 1,14,3
 arg: Str('cn', attribute=True, cli_name='vault_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: Bytes('ipapublickey', attribute=True, autofill=False, cli_name='public_key', multivalue=False, required=False)
+option: Str('ipavaultsalt', attribute=True, autofill=False, cli_name='salt', multivalue=False, required=False)
+option: Str('ipavaulttype', attribute=True, autofill=False, cli_name='type', default=u'standard', multivalue=False, required=False)
 option: Flag('no_members', autofill=True, default=False, exclude='webui')
 option: Str('parent_id?', cli_name='parent_id')
 option: Flag('raw', autofill=True, cli_name='raw', default=False, exclude='webui')
@@ -4645,12 +4662,16 @@ output: Output('completed', <type 'int'>, None)
 output: Output('failed', <type 'dict'>, None)
 output: Entry('result', <type 'dict'>, Gettext('A dictionary representing an LDAP entry', domain='ipa', localedir=None))
 command: vault_retrieve
-args: 1,9,3
+args: 1,13,3
 arg: Str('cn', attribute=True, cli_name='vault_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('no_members', autofill=True, default=False, exclude='webui')
 option: Str('out?', cli_name='out')
 option: Str('parent_id?', cli_name='parent_id')
+option: Str('password?', cli_name='password')
+option: Str('password_file?', cli_name='password_file')
+option: Bytes('private_key?', cli_name='private_key')
+option: Str('private_key_file?', cli_name='private_key_file')
 option: Flag('raw', autofill=True, cli_name='raw', default=False, exclude='webui')
 option: Str('session_key?', cli_name='session_key')
 option: Flag('show_text?', autofill=True, default=False)
diff --git a/freeipa.spec.in b/freeipa.spec.in
index 8d58f2568e1de418c25cb1bd34fc7d4736a15e54..228b7f7ed86eaac58643fd1fce19dccdceb85565 100644
--- a/freeipa.spec.in
+++ b/freeipa.spec.in
@@ -94,6 +94,7 @@ BuildRequires:  p11-kit-devel
 BuildRequires:  pki-base >= 10.2.1-0.1
 BuildRequires:  python-pytest-multihost >= 0.5
 BuildRequires:  python-pytest-sourceorder
+BuildRequires:  python-cryptography
 
 %description
 IPA is an integrated solution to provide centrally managed Identity (machine,
@@ -150,6 +151,7 @@ Requires: openssl
 Requires: softhsm >= 2.0.0b1-3
 Requires: p11-kit
 Requires: systemd-python
+Requires: python-cryptography
 
 Conflicts: %{alt_name}-server
 Obsoletes: %{alt_name}-server < %{version}
diff --git a/install/share/60basev3.ldif b/install/share/60basev3.ldif
index cdabfac35488bda184af564a174306a681183084..9e0f70a41ef50e78d4e464bab428325dfb6568fa 100644
--- a/install/share/60basev3.ldif
+++ b/install/share/60basev3.ldif
@@ -54,6 +54,8 @@ attributeTypes: (2.16.840.1.113730.3.8.11.55 NAME 'ipaSecretKey' DESC 'Encrypted
 attributeTypes: (2.16.840.1.113730.3.8.11.61 NAME 'ipaWrappingKey' DESC 'PKCS#11 URI of the wrapping key' EQUALITY caseExactMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 SINGLE-VALUE X-ORIGIN 'IPA v4.1' )
 attributeTypes: (2.16.840.1.113730.3.8.11.64 NAME 'ipaSecretKeyRef' DESC 'DN of the ipa key object' EQUALITY distinguishedNameMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.12 X-ORIGIN 'IPA v4.1' )
 attributeTypes: (2.16.840.1.113730.3.8.11.65 NAME 'ipaWrappingMech' DESC 'PKCS#11 wrapping mechanism equivalent to CK_MECHANISM_TYPE' EQUALITY caseIgnoreMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 SINGLE-VALUE X-ORIGIN 'IPA v4.1')
+attributeTypes: (2.16.840.1.113730.3.8.18.2.1 NAME 'ipaVaultType' DESC 'IPA vault type' EQUALITY caseExactMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 X-ORIGIN 'IPA v4.2')
+attributeTypes: (2.16.840.1.113730.3.8.18.2.2 NAME 'ipaVaultSalt' DESC 'IPA vault salt' EQUALITY caseExactMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 X-ORIGIN 'IPA v4.2')
 objectClasses: (2.16.840.1.113730.3.8.12.1 NAME 'ipaExternalGroup' SUP top STRUCTURAL MUST ( cn ) MAY ( ipaExternalMember $ memberOf $ description $ owner) X-ORIGIN 'IPA v3' )
 objectClasses: (2.16.840.1.113730.3.8.12.2 NAME 'ipaNTUserAttrs' SUP top AUXILIARY MUST ( ipaNTSecurityIdentifier ) MAY ( ipaNTHash $ ipaNTLogonScript $ ipaNTProfilePath $ ipaNTHomeDirectory $ ipaNTHomeDirectoryDrive ) X-ORIGIN 'IPA v3' )
 objectClasses: (2.16.840.1.113730.3.8.12.3 NAME 'ipaNTGroupAttrs' SUP top AUXILIARY MUST ( ipaNTSecurityIdentifier ) X-ORIGIN 'IPA v3' )
@@ -77,5 +79,5 @@ 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 $ owner $ member ) X-ORIGIN 'IPA v4.2' )
+objectClasses: (2.16.840.1.113730.3.8.18.1.1 NAME 'ipaVault' DESC 'IPA vault' SUP top STRUCTURAL MUST ( cn ) MAY ( description $ owner $ member $ ipaVaultType $ ipaVaultSalt $ ipaPublicKey ) X-ORIGIN 'IPA v4.2' )
 objectClasses: (2.16.840.1.113730.3.8.18.1.2 NAME 'ipaVaultContainer' DESC 'IPA vault container' SUP top STRUCTURAL MUST ( cn ) MAY ( description $ owner $ member ) X-ORIGIN 'IPA v4.2' )
diff --git a/ipalib/plugins/vault.py b/ipalib/plugins/vault.py
index ae4c6fe5cae4b7d6c41464cebcb3ec820912efe3..848488d646f87e52ff44dfa95d024372f2c9bd2c 100644
--- a/ipalib/plugins/vault.py
+++ b/ipalib/plugins/vault.py
@@ -18,11 +18,20 @@
 # along with this program.  If not, see <http://www.gnu.org/licenses/>.
 
 import base64
+import getpass
 import json
 import os
 import sys
 import tempfile
 
+from cryptography.fernet import Fernet, InvalidToken
+from cryptography.hazmat.backends import default_backend
+from cryptography.hazmat.primitives import hashes
+from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2HMAC
+from cryptography.hazmat.primitives.asymmetric import padding
+from cryptography.hazmat.primitives.serialization import load_pem_public_key,\
+    load_pem_private_key
+
 import nss.nss as nss
 
 import pki.account
@@ -60,6 +69,12 @@ EXAMPLES:
  Add a standard vault:
    ipa vault-add MyVault
 """) + _("""
+ Add a symmetric vault:
+   ipa vault-add MyVault --type symmetric --password-file password.txt
+""") + _("""
+ Add an asymmetric vault:
+   ipa vault-add MyVault --type asymmetric --public-key-file public.pem
+""") + _("""
  Show a vault:
    ipa vault-show MyVault
 """) + _("""
@@ -72,6 +87,21 @@ EXAMPLES:
  Retrieve data from standard vault:
    ipa vault-retrieve MyVault --out data.bin
 """) + _("""
+ Archive data into symmetric vault:
+   ipa vault-archive MyVault --in data.bin --password-file password.txt
+""") + _("""
+ Archive data into asymmetric vault:
+   ipa vault-archive MyVault --in data.bin
+""") + _("""
+ Retrieve data from standard vault:
+   ipa vault-retrieve MyVault --out data.bin
+""") + _("""
+ Retrieve data from symmetric vault:
+   ipa vault-retrieve MyVault --out data.bin --password-file password.txt
+""") + _("""
+ Retrieve data from asymmetric vault:
+   ipa vault-retrieve MyVault --out data.bin --private-key-file private.pem
+""") + _("""
  Delete a vault:
    ipa vault-del MyVault
 """) + _("""
@@ -109,11 +139,15 @@ class vault(LDAPObject):
         'description',
         'owner',
         'member',
+        'ipavaulttype',
+        'ipavaultsalt',
+        'ipapublickey',
     ]
     search_display_attributes = [
         'cn',
         'vault_id',
         'description',
+        'ipavaulttype',
     ]
     attribute_members = {
         'owner': ['user', 'group'],
@@ -146,6 +180,26 @@ class vault(LDAPObject):
             label=_('Description'),
             doc=_('Vault description'),
         ),
+        Str(
+            'ipavaulttype?',
+            cli_name='type',
+            label=_('Type'),
+            doc=_('Vault type'),
+            default=u'standard',
+            autofill=True,
+        ),
+        Str(
+            'ipavaultsalt?',
+            cli_name='salt',
+            label=_('Salt'),
+            doc=_('Vault salt in base-64'),
+        ),
+        Bytes(
+            'ipapublickey?',
+            cli_name='public_key',
+            label=_('Public key'),
+            doc=_('Vault public key'),
+        ),
     )
 
     def get_dn(self, *args, **options):
@@ -277,7 +331,8 @@ class vault(LDAPObject):
 
         return (args, options)
 
-    def create_entry(self, dn, description=None, owner=None):
+    def create_entry(self, dn, description=None, vault_type=u'standard',
+                     salt=None, public_key=None, owner=None):
         """
         Creates vault entry and its parents.
         """
@@ -289,6 +344,9 @@ class vault(LDAPObject):
                 'objectclass': self.object_class,
                 'cn': rdn['cn'],
                 'description': description,
+                'ipavaulttype': vault_type,
+                'ipavaultsalt': salt,
+                'ipapublickey': public_key,
                 'owner': owner,
             })
 
@@ -313,6 +371,90 @@ class vault(LDAPObject):
         """
         return 'ipa:' + id
 
+    def get_password(self, new=False):
+        """
+        Gets password from user.
+        """
+
+        if new:
+            while True:
+                password = unicode(getpass.getpass('New password: '))
+                password2 = unicode(getpass.getpass('Verify password: '))
+
+                if password == password2:
+                    return password
+
+                print '  ** Passwords do not match! **'
+
+        return unicode(getpass.getpass('Password: '))
+
+    def generate_symmetric_key(self, password, salt):
+        """
+        Generates symmetric key from password and salt.
+        """
+        kdf = PBKDF2HMAC(
+            algorithm=hashes.SHA256(),
+            length=32,
+            salt=salt,
+            iterations=100000,
+            backend=default_backend()
+        )
+
+        return base64.b64encode(kdf.derive(password.encode('utf-8')))
+
+    def encrypt(self, data, symmetric_key=None, public_key=None):
+        """
+        Encrypts data with symmetric key or public key.
+        """
+        if symmetric_key:
+            fernet = Fernet(symmetric_key)
+            return fernet.encrypt(data)
+
+        elif public_key:
+            rsa_public_key = load_pem_public_key(
+                data=public_key,
+                backend=default_backend()
+            )
+            return rsa_public_key.encrypt(
+                data,
+                padding.OAEP(
+                    mgf=padding.MGF1(algorithm=hashes.SHA1()),
+                    algorithm=hashes.SHA1(),
+                    label=None
+                )
+            )
+
+    def decrypt(self, data, symmetric_key=None, private_key=None):
+        """
+        Decrypts data with symmetric key or public key.
+        """
+        if symmetric_key:
+            try:
+                fernet = Fernet(symmetric_key)
+                return fernet.decrypt(data)
+            except InvalidToken:
+                raise errors.AuthenticationError(
+                    message=_('Invalid credentials'))
+
+        elif private_key:
+            try:
+                rsa_private_key = load_pem_private_key(
+                    data=private_key,
+                    password=None,
+                    backend=default_backend()
+                )
+                return rsa_private_key.decrypt(
+                    data,
+                    padding.OAEP(
+                        mgf=padding.MGF1(algorithm=hashes.SHA1()),
+                        algorithm=hashes.SHA1(),
+                        label=None
+                    )
+                )
+            except AssertionError:
+                raise errors.AuthenticationError(
+                    message=_('Invalid credentials'))
+
 
 @register()
 class vault_add(LDAPQuery):
@@ -329,6 +471,13 @@ class vault_add(LDAPQuery):
             cli_name='desc',
             doc=_('Vault description'),
         ),
+        Str(
+            'ipavaulttype?',
+            cli_name='type',
+            doc=_('Vault type'),
+            default=u'standard',
+            autofill=True,
+        ),
         Bytes(
             'data?',
             cli_name='data',
@@ -344,6 +493,26 @@ class vault_add(LDAPQuery):
             cli_name='in',
             doc=_('File containing data to archive'),
         ),
+        Str(
+            'password?',
+            cli_name='password',
+            doc=_('Vault password'),
+        ),
+        Str(  # TODO: use File parameter
+            'password_file?',
+            cli_name='password_file',
+            doc=_('File containing the vault password'),
+        ),
+        Bytes(
+            'ipapublickey?',
+            cli_name='public_key',
+            doc=_('Vault public key'),
+        ),
+        Str(  # TODO: use File parameter
+            'public_key_file?',
+            cli_name='public_key_file',
+            doc=_('File containing the vault public key'),
+        ),
     )
 
     has_output = output.standard_entry
@@ -615,6 +784,13 @@ class vault_archive(LDAPQuery):
             cli_name='desc',
             doc=_('Vault description'),
         ),
+        Str(
+            'ipavaulttype?',
+            cli_name='type',
+            doc=_('Vault type'),
+            default=u'standard',
+            autofill=True,
+        ),
         Bytes(
             'data?',
             cli_name='data',
@@ -649,6 +825,31 @@ class vault_archive(LDAPQuery):
             cli_name='nonce',
             doc=_('Nonce encrypted encoded in base-64'),
         ),
+        Str(
+            'ipavaultsalt?',
+            cli_name='salt',
+            doc=_('Vault salt in base-64'),
+        ),
+        Str(
+            'password?',
+            cli_name='password',
+            doc=_('Vault password'),
+        ),
+        Str(  # TODO: use File parameter
+            'password_file?',
+            cli_name='password_file',
+            doc=_('File containing the vault password'),
+        ),
+        Bytes(
+            'ipapublickey?',
+            cli_name='public_key',
+            doc=_('Vault public key'),
+        ),
+        Str(  # TODO: use File parameter
+            'public_key_file?',
+            cli_name='public_key_file',
+            doc=_('File containing the vault public key'),
+        ),
     )
 
     has_output = output.standard_entry
@@ -662,9 +863,38 @@ class vault_archive(LDAPQuery):
 
     def forward(self, *args, **options):
 
+        dn = self.obj.get_dn(*args, **options)
+        vault_id = self.obj.get_id(dn)
+
+        create = options.get('create')
         data = options.get('data')
         text = options.get('text')
         input_file = options.get('in')
+        password = options.get('password')
+        password_file = options.get('password_file')
+
+        if create:
+            vault_type = options.get('ipavaulttype', u'standard')
+            public_key = options.get('ipapublickey')
+            public_key_file = options.get('public_key_file')
+
+        else:
+            vault_type = u'standard'
+            salt = None
+            public_key = None
+            public_key_file = None
+
+            # retrieve vault info
+            vault = self.api.Command.vault_show(vault_id)['result']
+
+            if 'ipavaulttype' in vault:
+                vault_type = vault['ipavaulttype'][0]
+
+            if 'ipavaultsalt' in vault:
+                salt = base64.b64decode(vault['ipavaultsalt'][0])
+
+            if 'ipapublickey' in vault:
+                public_key = vault['ipapublickey'][0].encode('utf-8')
 
         # don't send these parameters to server
         if 'data' in options:
@@ -673,6 +903,12 @@ class vault_archive(LDAPQuery):
             del options['text']
         if 'in' in options:
             del options['in']
+        if 'password' in options:
+            del options['password']
+        if 'password_file' in options:
+            del options['password_file']
+        if 'public_key_file' in options:
+            del options['public_key_file']
 
         # get data
         if data:
@@ -694,6 +930,94 @@ class vault_archive(LDAPQuery):
         else:
             data = ''
 
+        encrypted_key = None
+
+        if vault_type == u'standard':
+
+            pass
+
+        elif vault_type == u'symmetric':
+
+            if create:
+                # generate vault salt
+                salt = os.urandom(16)
+                options['ipavaultsalt'] = base64.b64encode(salt)\
+                    .decode('utf-8')
+
+                # get new password
+                if password:
+                    pass
+
+                elif password_file:
+                    with open(password_file) as f:
+                        password = unicode(f.read().rstrip('\n'))
+
+                else:
+                    password = self.obj.get_password(new=True)
+
+            else:
+                # get existing password
+                if password:
+                    pass
+
+                elif password_file:
+                    with open(password_file) as f:
+                        password = unicode(f.read().rstrip('\n'))
+
+                else:
+                    password = self.obj.get_password()
+
+                # verify existing password
+                self.api.Command.vault_retrieve(
+                    vault_id,
+                    password=password)
+
+            # generate encryption key from vault password
+            encryption_key = self.obj.generate_symmetric_key(
+                password, salt)
+
+            # encrypt data with encryption key
+            data = self.obj.encrypt(data, symmetric_key=encryption_key)
+
+        elif vault_type == u'asymmetric':
+
+            # generate encryption key
+            encryption_key = base64.b64encode(os.urandom(32))
+
+            if create:
+                # generate vault salt
+                salt = os.urandom(16)
+                options['ipavaultsalt'] = base64.b64encode(salt)\
+                    .decode('utf-8')
+
+                # get new vault public key
+                if public_key:
+                    pass
+
+                elif public_key_file:
+                    with open(public_key_file, 'rb') as f:
+                        public_key = f.read()
+
+                else:
+                    raise errors.ValidationError(
+                        name='ipapublickey',
+                        error=_('Missing vault public key'))
+
+                # store vault public key
+                options['ipapublickey'] = public_key
+
+            # encrypt data with encryption key
+            data = self.obj.encrypt(data, symmetric_key=encryption_key)
+
+            # encrypt encryption key with public key
+            encrypted_key = self.obj.encrypt(
+                encryption_key, public_key=public_key)
+
+        else:
+            raise errors.ValidationError(
+                name='vault_type',
+                error=_('Invalid vault type'))
+
         # initialize NSS database
         crypto = pki.crypto.NSSCryptoProvider(paths.IPA_NSSDB_DIR)
         crypto.initialize()
@@ -728,6 +1052,10 @@ class vault_archive(LDAPQuery):
         vault_data = {}
         vault_data[u'data'] = base64.b64encode(data).decode('utf-8')
 
+        if encrypted_key:
+            vault_data[u'encrypted_key'] = base64.b64encode(encrypted_key)\
+                .decode('utf-8')
+
         json_vault_data = json.dumps(vault_data)
 
         # wrap vault_data with session key
@@ -749,9 +1077,12 @@ class vault_archive(LDAPQuery):
         (vault_name, parent_id) = self.obj.split_id(vault_id)
 
         create = options.get('create')
+        salt = options.get('ipavaultsalt')
 
         if create:
             description = options.get('description')
+            vault_type = options.get('ipavaulttype')
+            public_key = options.get('ipapublickey')
 
             # get user
             principal = getattr(context, 'principal')
@@ -762,6 +1093,9 @@ class vault_archive(LDAPQuery):
             self.obj.create_entry(
                 dn,
                 description=description,
+                vault_type=vault_type,
+                salt=salt,
+                public_key=public_key,
                 owner=owner_dn,
             )
 
@@ -843,6 +1177,26 @@ class vault_retrieve(LDAPQuery):
                 'Session key wrapped with transport certificate'
                 ' and encoded in base-64'),
         ),
+        Str(
+            'password?',
+            cli_name='password',
+            doc=_('Vault password'),
+        ),
+        Str(  # TODO: use File parameter
+            'password_file?',
+            cli_name='password_file',
+            doc=_('File containing the vault password'),
+        ),
+        Bytes(
+            'private_key?',
+            cli_name='private_key',
+            doc=_('Vault private key'),
+        ),
+        Str(  # TODO: use File parameter
+            'private_key_file?',
+            cli_name='private_key_file',
+            doc=_('File containing the vault private key'),
+        ),
     )
 
     has_output = output.standard_entry
@@ -852,7 +1206,7 @@ class vault_retrieve(LDAPQuery):
             'data',
             label=_('Data'),
         ),
-        Bytes(
+        Str(
             'text',
             label=_('Text'),
         ),
@@ -867,9 +1221,28 @@ class vault_retrieve(LDAPQuery):
 
     def forward(self, *args, **options):
 
+        dn = self.obj.get_dn(*args, **options)
+        vault_id = self.obj.get_id(dn)
+
+        vault_type = u'standard'
+        salt = None
+
+        # retrieve vault info
+        vault = self.api.Command.vault_show(vault_id)['result']
+
+        if 'ipavaulttype' in vault:
+            vault_type = vault['ipavaulttype'][0]
+
+        if 'ipavaultsalt' in vault:
+            salt = base64.b64decode(vault['ipavaultsalt'][0])
+
         show_text = options.get('show_text')
         stdout = options.get('stdout')
         output_file = options.get('out')
+        password = options.get('password')
+        password_file = options.get('password_file')
+        private_key = options.get('private_key')
+        private_key_file = options.get('private_key_file')
 
         # don't send these parameters to server
         if 'show_text' in options:
@@ -878,6 +1251,14 @@ class vault_retrieve(LDAPQuery):
             del options['stdout']
         if 'out' in options:
             del options['out']
+        if 'password' in options:
+            del options['password']
+        if 'password_file' in options:
+            del options['password_file']
+        if 'private_key' in options:
+            del options['private_key']
+        if 'private_key_file' in options:
+            del options['private_key_file']
 
         # initialize NSS database
         crypto = pki.crypto.NSSCryptoProvider(paths.IPA_NSSDB_DIR)
@@ -924,6 +1305,62 @@ class vault_retrieve(LDAPQuery):
         vault_data = json.loads(json_vault_data)
         data = base64.b64decode(vault_data[u'data'].encode('utf-8'))
 
+        encrypted_key = None
+
+        if 'encrypted_key' in vault_data:
+            encrypted_key = base64.b64decode(vault_data[u'encrypted_key']
+                                             .encode('utf-8'))
+
+        if vault_type == u'standard':
+
+            pass
+
+        elif vault_type == u'symmetric':
+
+            # get encryption key from vault password
+            if password:
+                pass
+
+            elif password_file:
+                with open(password_file) as f:
+                    password = unicode(f.read().rstrip('\n'))
+
+            else:
+                password = unicode(getpass.getpass('Password: '))
+
+            # generate encryption key from password
+            encryption_key = self.obj.generate_symmetric_key(password, salt)
+
+            # decrypt data with encryption key
+            data = self.obj.decrypt(data, symmetric_key=encryption_key)
+
+        elif vault_type == u'asymmetric':
+
+            # get encryption key with vault private key
+            if private_key:
+                pass
+
+            elif private_key_file:
+                with open(private_key_file, 'rb') as f:
+                    private_key = f.read()
+
+            else:
+                raise errors.ValidationError(
+                    name='private_key',
+                    error=_('Missing vault private key'))
+
+            # decrypt encryption key with private key
+            encryption_key = self.obj.decrypt(
+                encrypted_key, private_key=private_key)
+
+            # decrypt data with encryption key
+            data = self.obj.decrypt(data, symmetric_key=encryption_key)
+
+        else:
+            raise errors.ValidationError(
+                name='vault_type',
+                error=_('Invalid vault type'))
+
         if stdout:
             sys.stdout.write(data)
             response['result'] = {}
diff --git a/ipatests/test_xmlrpc/test_vault_plugin.py b/ipatests/test_xmlrpc/test_vault_plugin.py
index 4c46cf7783071873b6accf9b63a0f819b720103e..218aa49dfd08de5b36a734ea84e7aa58a25a4d1b 100644
--- a/ipatests/test_xmlrpc/test_vault_plugin.py
+++ b/ipatests/test_xmlrpc/test_vault_plugin.py
@@ -26,16 +26,107 @@ from xmlrpc_test import Declarative, fuzzy_string
 
 test_vault = u'test_vault'
 shared_test_vault = u'/shared/%s' % test_vault
+symmetric_vault = u'symmetric_vault'
+asymmetric_vault = u'asymmetric_vault'
 
 binary_data = '\x01\x02\x03\x04'
 text_data = u'secret'
 
+password = u'password'
+other_password = u'other_password'
+
+public_key = """
+-----BEGIN PUBLIC KEY-----
+MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAnT61EFxUOQgCJdM0tmw/
+pRRPDPGchTClnU1eBtiQD3ItKYf1+weMGwGOSJXPtkto7NlE7Qs8WHAr0UjyeBDe
+k/zeB6nSVdk47OdaW1AHrJL+44r238Jbm/+7VO5lTu6Z4N5p0VqoWNLi0Uh/CkqB
+tsxXaaAgjMp0AGq2U/aO/akeEYWQOYIdqUKVgAEKX5MmIA8tmbmoYIQ+B4Q3vX7N
+otG4eR6c2o9Fyjd+M4Gai5Ce0fSrigRvxAYi8xpRkQ5yQn5gf4WVrn+UKTfOIjLO
+pVThop+Xivcre3SpI0kt6oZPhBw9i8gbMnqifVmGFpVdhq+QVBqp+MVJvTbhRPG6
+3wIDAQAB
+-----END PUBLIC KEY-----
+"""
+
+private_key = """
+-----BEGIN RSA PRIVATE KEY-----
+MIIEpAIBAAKCAQEAnT61EFxUOQgCJdM0tmw/pRRPDPGchTClnU1eBtiQD3ItKYf1
++weMGwGOSJXPtkto7NlE7Qs8WHAr0UjyeBDek/zeB6nSVdk47OdaW1AHrJL+44r2
+38Jbm/+7VO5lTu6Z4N5p0VqoWNLi0Uh/CkqBtsxXaaAgjMp0AGq2U/aO/akeEYWQ
+OYIdqUKVgAEKX5MmIA8tmbmoYIQ+B4Q3vX7NotG4eR6c2o9Fyjd+M4Gai5Ce0fSr
+igRvxAYi8xpRkQ5yQn5gf4WVrn+UKTfOIjLOpVThop+Xivcre3SpI0kt6oZPhBw9
+i8gbMnqifVmGFpVdhq+QVBqp+MVJvTbhRPG63wIDAQABAoIBAQCD2bXnfxPcMnvi
+jaPwpvoDCPF0EBBHmk/0g5ApO2Qon3uBDJFUqbJwXrCY6o2d9MOJfnGONlKmcYA8
+X+d4h+SqwGjIkjxdYeSauS+Jy6Rzr1ptH/P8EjPQrfG9uJxYQDflV3nxYwwwVrx7
+8kccMPdteRB+8Bb7FzOHufMimmayCNFETnVT5CKH2PrYoPB+fr0itCipWOenDp33
+e73OV+K9U3rclmtHaoRxGohqByKfQRUkipjw4m+T3qfZZc5eN77RGW8J+oL1GVom
+fwtiH7N1HVte0Dmd13nhiASg355kjqRPcIMPsRHvXkOpgg5HRUTKG5elqAyvvm27
+Fzj1YdeRAoGBAMnE61+FYh8qCyEGe8r6RGjO8iuoyk1t+0gBWbmILLBiRnj4K8Tc
+k7HBG/pg3XCNbCuRwiLg8tk3VAAXzn6o+IJr3QnKbNCGa1lKfYU4mt11sBEyuL5V
+NpZcZ8IiPhMlGyDA9cFbTMKOE08RqbOIdxOmTizFt0R5sYZAwOjEvBIZAoGBAMeC
+N/P0bdrScFZGeS51wEdiWme/CO0IyGoqU6saI8L0dbmMJquiaAeIEjIKLqxH1RON
+axhsyk97e0PCcc5QK62Utf50UUAbL/v7CpIG+qdSRYDO4bVHSCkwF32N3pYh/iVU
+EsEBEkZiJi0dWa/0asDbsACutxcHda3RI5pi7oO3AoGAcbGNs/CUHt1xEfX2UaT+
+YVSjb2iYPlNH8gYYygvqqqVl8opdF3v3mYUoP8jPXrnCBzcF/uNk1HNx2O+RQxvx
+lIQ1NGwlLsdfvBvWaPhBg6LqSHadVVrs/IMrUGA9PEp/Y9B3arIIqeSnCrn4Nxsh
+higDCwWKRIKSPwVD7qXVGBkCgYEAu5/CASIRIeYgEXMLSd8hKcDcJo8o1MoauIT/
+1Hyrvw9pm0qrn2QHk3WrLvYWeJzBTTcEzZ6aEG+fN9UodA8/VGnzUc6QDsrCsKWh
+hj0cArlDdeSZrYLQ4TNCFCiUePqU6QQM8weP6TMqlejxTKF+t8qi1bF5rCWuzP1P
+D0UU7DcCgYAUvmEGckugS+FTatop8S/rmkcQ4Bf5M/YCZfsySavucDiHcBt0QtXt
+Swh0XdDsYS3W1yj2XqqsQ7R58KNaffCHjjulWFzb5IiuSvvdxzWtiXHisOpO36MJ
+kUlCMj24a8XsShzYTWBIyW2ngvGe3pQ9PfjkUdm0LGZjYITCBvgOKw==
+-----END RSA PRIVATE KEY-----
+"""
+
+other_public_key = """
+-----BEGIN PUBLIC KEY-----
+MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAv7E/QLVyKjrgDctZ50U7
+rmtL7Ks1QLoccp9WvZJ6WI1rYd0fX5FySS4dI6QTNZc6qww8NeNuZtkoxT9m1wkk
+Rl/3wK7fWNLenH/+VHOaTQc20exg7ztfsO7JIsmKmigtticdR5C4jLfjcOp+WjLH
+w3zrmrO5SIZ8njxMoDcQJa2vu/t281U/I7ti8ue09FSitIECU05vgmPS+MnXR8HK
+PxXqrNkjl29mXNbPiByWwlse3Prwved9I7fwgpiHJqUBFudD/0tZ4DWyLG7t9wM1
+O8gRaRg1r+ENVpmMSvXo4+8+bR3rEYddD5zU7nKXafeuthXlXplae/8uZmCiSI63
+TwIDAQAB
+-----END PUBLIC KEY-----
+"""
+
+other_private_key = """
+-----BEGIN RSA PRIVATE KEY-----
+MIIEpgIBAAKCAQEAv7E/QLVyKjrgDctZ50U7rmtL7Ks1QLoccp9WvZJ6WI1rYd0f
+X5FySS4dI6QTNZc6qww8NeNuZtkoxT9m1wkkRl/3wK7fWNLenH/+VHOaTQc20exg
+7ztfsO7JIsmKmigtticdR5C4jLfjcOp+WjLHw3zrmrO5SIZ8njxMoDcQJa2vu/t2
+81U/I7ti8ue09FSitIECU05vgmPS+MnXR8HKPxXqrNkjl29mXNbPiByWwlse3Prw
+ved9I7fwgpiHJqUBFudD/0tZ4DWyLG7t9wM1O8gRaRg1r+ENVpmMSvXo4+8+bR3r
+EYddD5zU7nKXafeuthXlXplae/8uZmCiSI63TwIDAQABAoIBAQCA+0GFR9F+isjx
+Xy+qBpKmxLl8kKKvX8r+cSpLOkEqTlW/rqqKgnI0vVuL/L2UJKKsLvpghBxoBZyC
+RCvtatBGrhIlS0UrHg/9m73Ek1hylfUUAQokTn4PrkwWJSgmm/xOATmZSs5ymNTn
+yFCmXl69sdNR77YvD5bQXeBtOT+bKXy7yQ1TmYPwwSjL+WSlMV6ZfE3HNVmxPTpk
+CTFS638cJblWk9MUIy8HIlhu6If2P4RnHr7ZGGivhREayvs0zXcAfqhIyFHruxSE
+yYnmqH9paWjv5mP3YyLoKr+NUvvxnBr/9wCTt0TKgG8G6rpkHuPDLQni9wUGnew8
+QdMgFEohAoGBAPH4vaVB5gDVfvIqwJBsBLHpPq72GvxjrM/exD0jIIpXZxz9gCql
+CmC5b1RS1uy8PMoc/RO4CE7UTLaTesciP6LjTD1RhH3rLLJO8/iVC1RXgMrCLHLm
+ZQnDhIQGGNQxpvBjQy5ZOWat2dFxYhHN630IFPOtrWsOmJ5HsL1JrjzxAoGBAMrO
+R1zNwQ42VbJS6AFshZVjmUV2h3REGh4zG/9IqL0Hz493hyCTGoDPLLXIbtkqNqzQ
+XibSZ9RMVPKKTiNQTx91DTgh4Anz8xUr84tA2iAf3ayNWKi3Y3GhmP2EWp1qYeom
+kV8Uq0lt4dHZuEo3LuqvbtbzlF9qUXqKS5qy6Tg/AoGBAKCp02o2HjzxhS/QeTmr
+r1ZeE7PiTzrECAuh01TwzPtuW1XhcEdgfEqK9cPcmT5pIkflBZkhOcr1pdYYiI5O
+TEigeY/BX6KoE251hALLG9GtpCN82DyWhAH+oy9ySOwj5793eTT+I2HtD1LE4SQH
+QVQsmJTP/fS2pVl7KnwUvy9RAoGBAKzo2qchNewsHzx+uxgbsnkABfnXaP2T4sDE
+yqYJCPTB6BFl02vOf9Y6zN/gF8JH333P2bY3xhaXTgXMLXqmSg+D+NVW7HEP8Lyo
+UGj1zgN9p74qdODEGqETKiFb6vYzcW/1mhP6x18/tDz658k+611kXZge7O288+MK
+bhNjXrx5AoGBAMox25PcxVgOjCd9+LdUcIOG6LQ971eCH1NKL9YAekICnwMrStbK
+veCYju6ok4ZWnMiH8MR1jgC39RWtjJZwynCuPXUP2/vZkoVf1tCZyz7dSm8TdS/2
+5NdOHVy7+NQcEPSm7/FmXdpcR9ZSGAuxMBfnEUibdyz5LdJGnFUN/+HS
+-----END RSA PRIVATE KEY-----
+"""
+
 
 class test_vault_plugin(Declarative):
 
     cleanup_commands = [
         ('vault_del', [test_vault], {'continue': True}),
         ('vault_del', [shared_test_vault], {'continue': True}),
+        ('vault_del', [symmetric_vault], {'continue': True}),
+        ('vault_del', [asymmetric_vault], {'continue': True}),
     ]
 
     tests = [
@@ -56,6 +147,7 @@ class test_vault_plugin(Declarative):
                     'cn': [test_vault],
                     'vault_id': u'/users/admin/%s' % test_vault,
                     'owner_user': [u'admin'],
+                    'ipavaulttype': [u'standard'],
                 },
             },
         },
@@ -88,6 +180,7 @@ class test_vault_plugin(Declarative):
                               % (test_vault, api.env.basedn),
                         'cn': [test_vault],
                         'vault_id': u'/users/admin/%s' % test_vault,
+                        'ipavaulttype': [u'standard'],
                     },
                 ],
             },
@@ -109,6 +202,7 @@ class test_vault_plugin(Declarative):
                     'cn': [test_vault],
                     'vault_id': u'/users/admin/%s' % test_vault,
                     'owner_user': [u'admin'],
+                    'ipavaulttype': [u'standard'],
                 },
             },
         },
@@ -130,6 +224,7 @@ class test_vault_plugin(Declarative):
                     'vault_id': u'/users/admin/%s' % test_vault,
                     'description': [u'Test vault'],
                     'owner_user': [u'admin'],
+                    'ipavaulttype': [u'standard'],
                 },
             },
         },
@@ -153,6 +248,7 @@ class test_vault_plugin(Declarative):
                     'vault_id': u'/users/admin/%s' % test_vault,
                     'description': [u'Test vault'],
                     'owner_user': [u'admin'],
+                    'ipavaulttype': [u'standard'],
                 },
             },
         },
@@ -174,6 +270,7 @@ class test_vault_plugin(Declarative):
                     'vault_id': u'/users/admin/%s' % test_vault,
                     'description': [u'Test vault'],
                     'owner_user': [u'admin'],
+                    'ipavaulttype': [u'standard'],
                     'nonce': fuzzy_string,
                     'vault_data': fuzzy_string,
                     'data': binary_data,
@@ -200,6 +297,7 @@ class test_vault_plugin(Declarative):
                     'vault_id': u'/users/admin/%s' % test_vault,
                     'description': [u'Test vault'],
                     'owner_user': [u'admin'],
+                    'ipavaulttype': [u'standard'],
                 },
             },
         },
@@ -223,6 +321,7 @@ class test_vault_plugin(Declarative):
                     'vault_id': u'/users/admin/%s' % test_vault,
                     'description': [u'Test vault'],
                     'owner_user': [u'admin'],
+                    'ipavaulttype': [u'standard'],
                     'nonce': fuzzy_string,
                     'vault_data': fuzzy_string,
                     'text': text_data,
@@ -273,6 +372,7 @@ class test_vault_plugin(Declarative):
                     'cn': [test_vault],
                     'vault_id': shared_test_vault,
                     'owner_user': [u'admin'],
+                    'ipavaulttype': [u'standard'],
                 },
             },
         },
@@ -294,6 +394,7 @@ class test_vault_plugin(Declarative):
                               % (test_vault, api.env.basedn),
                         'cn': [test_vault],
                         'vault_id': shared_test_vault,
+                        'ipavaulttype': [u'standard'],
                     },
                 ],
             },
@@ -315,6 +416,7 @@ class test_vault_plugin(Declarative):
                     'cn': [test_vault],
                     'vault_id': shared_test_vault,
                     'owner_user': [u'admin'],
+                    'ipavaulttype': [u'standard'],
                 },
             },
         },
@@ -336,6 +438,7 @@ class test_vault_plugin(Declarative):
                     'vault_id': shared_test_vault,
                     'description': [u'Test vault'],
                     'owner_user': [u'admin'],
+                    'ipavaulttype': [u'standard'],
                 },
             },
         },
@@ -359,6 +462,7 @@ class test_vault_plugin(Declarative):
                     'vault_id': shared_test_vault,
                     'description': [u'Test vault'],
                     'owner_user': [u'admin'],
+                    'ipavaulttype': [u'standard'],
                 },
             },
         },
@@ -380,6 +484,7 @@ class test_vault_plugin(Declarative):
                     'vault_id': shared_test_vault,
                     'description': [u'Test vault'],
                     'owner_user': [u'admin'],
+                    'ipavaulttype': [u'standard'],
                     'nonce': fuzzy_string,
                     'vault_data': fuzzy_string,
                     'data': binary_data,
@@ -403,4 +508,139 @@ class test_vault_plugin(Declarative):
             },
         },
 
+        {
+            'desc': 'Create symmetric vault',
+            'command': (
+                'vault_add',
+                [symmetric_vault],
+                {
+                    'ipavaulttype': u'symmetric',
+                    'password': password,
+                    'data': binary_data,
+                },
+            ),
+            'expected': {
+                'value': symmetric_vault,
+                'summary': 'Added vault "%s"' % symmetric_vault,
+                'result': {
+                    'dn': u'cn=%s,cn=admin,cn=users,cn=vaults,%s'
+                          % (symmetric_vault, api.env.basedn),
+                    'cn': [symmetric_vault],
+                    'vault_id': u'/users/admin/%s' % symmetric_vault,
+                    'owner_user': [u'admin'],
+                    'ipavaulttype': [u'symmetric'],
+                    'ipavaultsalt': [fuzzy_string],
+                },
+            },
+        },
+
+        {
+            'desc': 'Retrieve symmetric vault',
+            'command': (
+                'vault_retrieve',
+                [symmetric_vault],
+                {
+                    'password': password,
+                },
+            ),
+            'expected': {
+                'value': symmetric_vault,
+                'summary': u'Retrieved data from vault "%s"' % symmetric_vault,
+                'result': {
+                    'dn': u'cn=%s,cn=admin,cn=users,cn=vaults,%s'
+                          % (symmetric_vault, api.env.basedn),
+                    'cn': [symmetric_vault],
+                    'vault_id': u'/users/admin/%s' % symmetric_vault,
+                    'owner_user': [u'admin'],
+                    'ipavaulttype': [u'symmetric'],
+                    'ipavaultsalt': [fuzzy_string],
+                    'nonce': fuzzy_string,
+                    'vault_data': fuzzy_string,
+                    'data': binary_data,
+                },
+            },
+        },
+
+        {
+            'desc': 'Retrieve symmetric vault with wrong password',
+            'command': (
+                'vault_retrieve',
+                [symmetric_vault],
+                {
+                    'password': other_password,
+                },
+            ),
+            'expected': errors.AuthenticationError(
+                message=u'Invalid credentials'),
+        },
+
+        {
+            'desc': 'Create asymmetric vault',
+            'command': (
+                'vault_add',
+                [asymmetric_vault],
+                {
+                    'ipavaulttype': u'asymmetric',
+                    'ipapublickey': public_key,
+                    'data': binary_data,
+                },
+            ),
+            'expected': {
+                'value': asymmetric_vault,
+                'summary': 'Added vault "%s"' % asymmetric_vault,
+                'result': {
+                    'dn': u'cn=%s,cn=admin,cn=users,cn=vaults,%s'
+                          % (asymmetric_vault, api.env.basedn),
+                    'cn': [asymmetric_vault],
+                    'vault_id': u'/users/admin/%s' % asymmetric_vault,
+                    'owner_user': [u'admin'],
+                    'ipavaulttype': [u'asymmetric'],
+                    'ipavaultsalt': [fuzzy_string],
+                    'ipapublickey': [public_key],
+                },
+            },
+        },
+
+        {
+            'desc': 'Retrieve asymmetric vault',
+            'command': (
+                'vault_retrieve',
+                [asymmetric_vault],
+                {
+                    'private_key': private_key,
+                },
+            ),
+            'expected': {
+                'value': asymmetric_vault,
+                'summary': u'Retrieved data from vault "%s"'
+                           % asymmetric_vault,
+                'result': {
+                    'dn': u'cn=%s,cn=admin,cn=users,cn=vaults,%s'
+                          % (asymmetric_vault, api.env.basedn),
+                    'cn': [asymmetric_vault],
+                    'vault_id': u'/users/admin/%s' % asymmetric_vault,
+                    'owner_user': [u'admin'],
+                    'ipavaulttype': [u'asymmetric'],
+                    'ipavaultsalt': [fuzzy_string],
+                    'ipapublickey': [public_key],
+                    'nonce': fuzzy_string,
+                    'vault_data': fuzzy_string,
+                    'data': binary_data,
+                },
+            },
+        },
+
+        {
+            'desc': 'Retrieve asymmetric vault with wrong private key',
+            'command': (
+                'vault_retrieve',
+                [asymmetric_vault],
+                {
+                    'private_key': other_private_key,
+                },
+            ),
+            'expected': errors.AuthenticationError(
+                message=u'Invalid credentials'),
+        },
+
     ]
-- 
2.3.1

-------------- next part --------------
>From 3ee085a500b46fbc7951eef871863c700a63cad5 Mon Sep 17 00:00:00 2001
From: "Endi S. Dewata" <edewata at redhat.com>
Date: Sat, 8 Nov 2014 02:04:03 -0500
Subject: [PATCH] Added vault secrets.

A new plugin has been added to provide the interface to manage
secrets stored in a vault. New test scripts have been added as
well.

https://fedorahosted.org/freeipa/ticket/3872
---
 API.txt                                         |   91 +++
 ipalib/plugins/vaultsecret.py                   | 1000 +++++++++++++++++++++++
 ipatests/test_xmlrpc/test_vaultsecret_plugin.py |  470 +++++++++++
 3 files changed, 1561 insertions(+)
 create mode 100644 ipalib/plugins/vaultsecret.py
 create mode 100644 ipatests/test_xmlrpc/test_vaultsecret_plugin.py

diff --git a/API.txt b/API.txt
index e8912070b96cbb7b53a7811465cfc6cd6bc9059d..efea0cca83df0ba8d24b4218a809263186da9e0d 100644
--- a/API.txt
+++ b/API.txt
@@ -4821,6 +4821,97 @@ 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: vaultsecret_add
+args: 2,12,3
+arg: Str('vaultcn', cli_name='vault', maxlength=255, multivalue=False, pattern='^[a-zA-Z0-9_.-/]+$', primary_key=True, query=True, required=True)
+arg: Str('secret_name', attribute=True, cli_name='secret', 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: Bytes('data?', cli_name='data')
+option: Str('description?', cli_name='desc')
+option: Str('in?', cli_name='in')
+option: Str('parent_id?', cli_name='parent_id')
+option: Str('password?', cli_name='password')
+option: Str('password_file?', cli_name='password_file')
+option: Bytes('private_key?', cli_name='private_key')
+option: Str('private_key_file?', cli_name='private_key_file')
+option: Flag('raw', autofill=True, cli_name='raw', default=False, exclude='webui')
+option: Str('text?', cli_name='text')
+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: vaultsecret_del
+args: 2,8,3
+arg: Str('vaultcn', cli_name='vault', maxlength=255, multivalue=False, pattern='^[a-zA-Z0-9_.-/]+$', primary_key=True, query=True, required=True)
+arg: Str('secret_name', attribute=True, cli_name='secret', 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: Str('parent_id?', cli_name='parent_id')
+option: Str('password?', cli_name='password')
+option: Str('password_file?', cli_name='password_file')
+option: Bytes('private_key?', cli_name='private_key')
+option: Str('private_key_file?', cli_name='private_key_file')
+option: Flag('raw', autofill=True, cli_name='raw', default=False, exclude='webui')
+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: vaultsecret_find
+args: 2,12,4
+arg: Str('vaultcn', cli_name='vault', maxlength=255, multivalue=False, pattern='^[a-zA-Z0-9_.-/]+$', primary_key=True, query=True, required=True)
+arg: Str('criteria?', noextrawhitespace=False)
+option: Flag('all', autofill=True, cli_name='all', default=False, exclude='webui')
+option: Bytes('data', attribute=True, autofill=False, cli_name='data', multivalue=False, query=True, required=False)
+option: Str('description', attribute=True, autofill=False, cli_name='desc', multivalue=False, query=True, required=False)
+option: Str('parent_id?', cli_name='parent_id')
+option: Str('password?', cli_name='password')
+option: Str('password_file?', cli_name='password_file')
+option: Flag('pkey_only?', autofill=True, default=False)
+option: Bytes('private_key?', cli_name='private_key')
+option: Str('private_key_file?', cli_name='private_key_file')
+option: Flag('raw', autofill=True, cli_name='raw', default=False, exclude='webui')
+option: Str('secret_name', attribute=True, autofill=False, cli_name='secret', maxlength=255, multivalue=False, pattern='^[a-zA-Z0-9_.-]+$', primary_key=True, query=True, required=False)
+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: vaultsecret_mod
+args: 2,12,3
+arg: Str('vaultcn', cli_name='vault', maxlength=255, multivalue=False, pattern='^[a-zA-Z0-9_.-/]+$', primary_key=True, query=True, required=True)
+arg: Str('secret_name', attribute=True, cli_name='secret', 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: Bytes('data?', cli_name='data')
+option: Str('description?', cli_name='desc')
+option: Str('in?', cli_name='in')
+option: Str('parent_id?', cli_name='parent_id')
+option: Str('password?', cli_name='password')
+option: Str('password_file?', cli_name='password_file')
+option: Bytes('private_key?', cli_name='private_key')
+option: Str('private_key_file?', cli_name='private_key_file')
+option: Flag('raw', autofill=True, cli_name='raw', default=False, exclude='webui')
+option: Str('text?', cli_name='text')
+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: vaultsecret_show
+args: 2,11,3
+arg: Str('vaultcn', cli_name='vault', maxlength=255, multivalue=False, pattern='^[a-zA-Z0-9_.-/]+$', primary_key=True, query=True, required=True)
+arg: Str('secret_name', attribute=True, cli_name='secret', 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: Str('out?', cli_name='out')
+option: Str('parent_id?', cli_name='parent_id')
+option: Str('password?', cli_name='password')
+option: Str('password_file?', cli_name='password_file')
+option: Bytes('private_key?', cli_name='private_key')
+option: Str('private_key_file?', cli_name='private_key_file')
+option: Flag('raw', autofill=True, cli_name='raw', default=False, exclude='webui')
+option: Flag('show_text?', autofill=True, default=False)
+option: Flag('stdout?', autofill=True, default=False)
+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/ipalib/plugins/vaultsecret.py b/ipalib/plugins/vaultsecret.py
new file mode 100644
index 0000000000000000000000000000000000000000..688b64e03b63061cae13c385825f06b04d97896a
--- /dev/null
+++ b/ipalib/plugins/vaultsecret.py
@@ -0,0 +1,1000 @@
+# 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/>.
+
+import base64
+import getpass
+import json
+import sys
+
+from ipalib import api, errors
+from ipalib import Str, Bytes, Flag
+from ipalib.plugable import Registry
+from ipalib.plugins.baseldap import LDAPObject, LDAPSearch, LDAPRetrieve
+from ipalib import _, ngettext
+
+__doc__ = _("""
+Vault secrets
+""") + _("""
+Manage vault secrets.
+""") + _("""
+EXAMPLES:
+""") + _("""
+ List vault secrets:
+   ipa vaultsecret-find MyVault
+""") + _("""
+ Add a vault secret:
+   ipa vaultsecret-add MyVault MySecret --in data.bin --desc "My vault secret"
+""") + _("""
+ Retrieve a vault secret:
+   ipa vaultsecret-show MyVault MySecret --out data.bin
+""") + _("""
+ Modify a vault secret:
+   ipa vaultsecret-mod MyVault MySecret --desc "My vault secret"
+""") + _("""
+ Delete a vault secret:
+   ipa vaultsecret-del MyVault MySecret
+""")
+
+register = Registry()
+
+
+ at register()
+class vaultsecret(LDAPObject):
+    __doc__ = _("""
+    Vault secret object.
+    """)
+
+    parent_object = 'vault'
+    object_name = _('vault secret')
+    object_name_plural = _('vault secrets')
+
+    default_attributes = [
+        'cn',
+        'description',
+        'data',
+    ]
+    search_display_attributes = [
+        'cn',
+        'description',
+    ]
+
+    label = _('Vault secrets')
+    label_singular = _('Vault secret')
+
+    takes_params = (
+        Str(
+            'secret_name',
+            cli_name='secret',
+            label=_('Secret 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=_('Secret description'),
+        ),
+        Bytes(
+            'data?',
+            cli_name='data',
+            label=_('Data'),
+            doc=_('Binary secret data'),
+        ),
+    )
+
+    def find(self, secrets, secret_name):
+        """
+        Finds a secret with the given name in a list of secrets.
+        Raises an exception if the secret is not found.
+        """
+
+        for secret in secrets:
+            if secret['secret_name'] == secret_name:
+                return secret
+
+        raise errors.NotFound(
+            reason=_('%s: vault secret not found' % secret_name))
+
+    def parse_result(self, result):
+        """
+        Returns JSON data from vault retrieval result.
+        """
+
+        vault_data = result['data']
+
+        if vault_data:
+            return json.loads(vault_data)
+
+        return {
+            'secrets': []
+        }
+
+
+ at register()
+class vaultsecret_add(LDAPRetrieve):
+    __doc__ = _('Add a new vault secret.')
+
+    takes_options = (
+        Str(
+            'parent_id?',
+            cli_name='parent_id',
+            doc=_('Parent ID'),
+        ),
+        Str(
+            'description?',
+            cli_name='desc',
+            doc=_('Secret description'),
+        ),
+        Bytes(
+            'data?',
+            cli_name='data',
+            doc=_('Binary secret data'),
+        ),
+        Str(
+            'text?',
+            cli_name='text',
+            doc=_('Text secret data'),
+        ),
+        Str(
+            'in?',
+            cli_name='in',
+            doc=_('File containing secret data'),
+        ),
+        Str(
+            'password?',
+            cli_name='password',
+            doc=_('Vault password'),
+        ),
+        Str(  # TODO: use File parameter
+            'password_file?',
+            cli_name='password_file',
+            doc=_('File containing the vault password'),
+        ),
+        Bytes(
+            'private_key?',
+            cli_name='private_key',
+            doc=_('Vault private key'),
+        ),
+        Str(  # TODO: use File parameter
+            'private_key_file?',
+            cli_name='private_key_file',
+            doc=_('File containing the vault private key'),
+        ),
+    )
+
+    msg_summary = _('Added vault secret "%(value)s"')
+
+    def forward(self, *args, **options):
+
+        dn = self.api.Object.vault.get_dn(*args, **options)
+        vault_id = self.api.Object.vault.get_id(dn)
+        secret_name = args[1]
+
+        vault_type = u'standard'
+        salt = None
+
+        # retrieve vault info
+        vault = self.api.Command.vault_show(vault_id)['result']
+
+        if 'ipavaulttype' in vault:
+            vault_type = vault['ipavaulttype'][0]
+
+        if 'ipavaultsalt' in vault:
+            salt = vault['ipavaultsalt'][0].encode('utf-8')
+
+        description = options.get('description')
+        data = options.get('data')
+        text = options.get('text')
+        input_file = options.get('in')
+        password = options.get('password')
+        password_file = options.get('password_file')
+        private_key = options.get('private_key')
+        private_key_file = options.get('private_key_file')
+
+        # don't send these parameters to server
+        if 'data' in options:
+            del options['data']
+        if 'text' in options:
+            del options['text']
+        if 'in' in options:
+            del options['in']
+        if 'password' in options:
+            del options['password']
+        if 'password_file' in options:
+            del options['password_file']
+        if 'private_key' in options:
+            del options['private_key']
+        if 'private_key_file' in options:
+            del options['private_key_file']
+
+        # type-specific initialization
+        if vault_type == u'standard':
+
+            pass
+
+        elif vault_type == u'symmetric':
+
+            # get vault password
+            if password:
+                pass
+
+            elif password_file:
+                with open(password_file) as f:
+                    password = unicode(f.read().rstrip('\n'))
+
+            else:
+                password = unicode(getpass.getpass('Password: '))
+
+        elif vault_type == u'asymmetric':
+
+            # get vault private key
+            if private_key:
+                pass
+
+            elif private_key_file:
+                with open(private_key_file, 'rb') as f:
+                    private_key = f.read()
+
+            else:
+                raise errors.ValidationError(
+                    name='private_key',
+                    error=_('Missing vault private key'))
+
+        else:
+            raise errors.ValidationError(
+                name='vault_type',
+                error=_('Invalid vault type'))
+
+        # retrieve secrets
+        result = self.api.Command.vault_retrieve(
+            vault_id,
+            password=password,
+            private_key=private_key)['result']
+
+        json_data = self.obj.parse_result(result)
+
+        secrets = json_data['secrets']
+
+        # get data
+        if data:
+            if text or input_file:
+                raise errors.MutuallyExclusiveError(
+                    reason=_('Input data specified multiple times'))
+
+        elif text:
+            if input_file:
+                raise errors.MutuallyExclusiveError(
+                    reason=_('Input data specified multiple times'))
+
+            data = text.encode('utf-8')
+
+        elif input_file:
+            with open(input_file, 'rb') as f:
+                data = f.read()
+
+        else:
+            data = ''
+
+        # add new secret
+        for secret in secrets:
+            if secret['secret_name'] == secret_name:
+                raise errors.DuplicateEntry(
+                    message=_(
+                        'vault secret with name "%s" already exists'
+                        % secret_name))
+
+        # store encoded data for storage
+        secret = {
+            'secret_name': secret_name,
+            'data': base64.b64encode(data).decode('utf-8'),
+        }
+        if description:
+            secret['description'] = description
+
+        secrets.append(secret)
+
+        # rearchive secrets
+        vault_data = json.dumps(json_data)
+        response = self.api.Command.vault_archive(
+            vault_id,
+            data=vault_data,
+            password=password)
+
+        # restore binary data for response
+        secret['data'] = data
+
+        response = {
+            'value': secret_name,
+            'summary': u'Added vault secret "%s"' % secret_name,
+            'result': secret,
+        }
+
+        return response
+
+
+ at register()
+class vaultsecret_del(LDAPRetrieve):
+    __doc__ = _('Delete a vault secret.')
+
+    takes_options = (
+        Str(
+            'parent_id?',
+            cli_name='parent_id',
+            doc=_('Parent ID'),
+        ),
+        Str(
+            'password?',
+            cli_name='password',
+            doc=_('Vault password'),
+        ),
+        Str(  # TODO: use File parameter
+            'password_file?',
+            cli_name='password_file',
+            doc=_('File containing the vault password'),
+        ),
+        Bytes(
+            'private_key?',
+            cli_name='private_key',
+            doc=_('Vault private key'),
+        ),
+        Str(  # TODO: use File parameter
+            'private_key_file?',
+            cli_name='private_key_file',
+            doc=_('File containing the vault private key'),
+        ),
+    )
+
+    msg_summary = _('Deleted vault secret "%(value)s"')
+
+    def forward(self, *args, **options):
+
+        dn = self.api.Object.vault.get_dn(*args, **options)
+        vault_id = self.api.Object.vault.get_id(dn)
+        secret_name = args[1]
+
+        vault_type = u'standard'
+        salt = None
+
+        # retrieve vault info
+        vault = self.api.Command.vault_show(vault_id)['result']
+
+        if 'ipavaulttype' in vault:
+            vault_type = vault['ipavaulttype'][0]
+
+        if 'ipavaultsalt' in vault:
+            salt = vault['ipavaultsalt'][0].encode('utf-8')
+
+        password = options.get('password')
+        password_file = options.get('password_file')
+        private_key = options.get('private_key')
+        private_key_file = options.get('private_key_file')
+
+        # don't send these parameters to server
+        if 'password' in options:
+            del options['password']
+        if 'password_file' in options:
+            del options['password_file']
+        if 'private_key' in options:
+            del options['private_key']
+        if 'private_key_file' in options:
+            del options['private_key_file']
+
+        # type-specific initialization
+        if vault_type == u'standard':
+
+            pass
+
+        elif vault_type == u'symmetric':
+
+            # get vault password
+            if password:
+                pass
+
+            elif password_file:
+                with open(password_file) as f:
+                    password = unicode(f.read().rstrip('\n'))
+
+            else:
+                password = unicode(getpass.getpass('Password: '))
+
+        elif vault_type == u'asymmetric':
+
+            # get vault private key
+            if private_key:
+                pass
+
+            elif private_key_file:
+                with open(private_key_file, 'rb') as f:
+                    private_key = f.read()
+
+            else:
+                raise errors.ValidationError(
+                    name='private_key',
+                    error=_('Missing vault private key'))
+
+        else:
+            raise errors.ValidationError(
+                name='vault_type',
+                error=_('Invalid vault type'))
+
+        # retrieve secrets
+        result = self.api.Command.vault_retrieve(
+            vault_id,
+            password=password,
+            private_key=private_key)['result']
+
+        json_data = self.obj.parse_result(result)
+
+        secrets = json_data['secrets']
+
+        # find the secret
+        secret = None
+        for s in secrets:
+            if s['secret_name'] == secret_name:
+                secret = s
+                break
+
+        if not secret:
+            raise errors.NotFound(
+                reason=_('%s: vault secret not found' % secret_name))
+
+        # delete secret
+        secrets.remove(secret)
+
+        # rearchive secrets
+        vault_data = json.dumps(json_data)
+        response = self.api.Command.vault_archive(
+            vault_id,
+            data=vault_data,
+            password=password)
+
+        response = {
+            'value': secret_name,
+            'summary': u'Deleted vault secret "%s"' % secret_name,
+            'result': {
+                'failed': (),
+            },
+        }
+
+        return response
+
+
+ at register()
+class vaultsecret_find(LDAPSearch):
+    __doc__ = _('Search for vault secrets.')
+
+    takes_options = (
+        Str(
+            'parent_id?',
+            cli_name='parent_id',
+            doc=_('Parent ID'),
+        ),
+        Str(
+            'password?',
+            cli_name='password',
+            doc=_('Vault password'),
+        ),
+        Str(  # TODO: use File parameter
+            'password_file?',
+            cli_name='password_file',
+            doc=_('File containing the vault password'),
+        ),
+        Bytes(
+            'private_key?',
+            cli_name='private_key',
+            doc=_('Vault private key'),
+        ),
+        Str(  # TODO: use File parameter
+            'private_key_file?',
+            cli_name='private_key_file',
+            doc=_('File containing the vault private key'),
+        ),
+    )
+
+    def forward(self, *args, **options):
+
+        dn = self.api.Object.vault.get_dn(*args, **options)
+        vault_id = self.api.Object.vault.get_id(dn)
+
+        vault_type = u'standard'
+        salt = None
+
+        # retrieve vault info
+        vault = self.api.Command.vault_show(vault_id)['result']
+
+        if 'ipavaulttype' in vault:
+            vault_type = vault['ipavaulttype'][0]
+
+        if 'ipavaultsalt' in vault:
+            salt = vault['ipavaultsalt'][0].encode('utf-8')
+
+        password = options.get('password')
+        password_file = options.get('password_file')
+        private_key = options.get('private_key')
+        private_key_file = options.get('private_key_file')
+
+        # don't send these parameters to server
+        if 'password' in options:
+            del options['password']
+        if 'password_file' in options:
+            del options['password_file']
+        if 'private_key' in options:
+            del options['private_key']
+        if 'private_key_file' in options:
+            del options['private_key_file']
+
+        # type-specific initialization
+        if vault_type == u'standard':
+
+            pass
+
+        elif vault_type == u'symmetric':
+
+            # get vault password
+            if password:
+                pass
+
+            elif password_file:
+                with open(password_file) as f:
+                    password = unicode(f.read().rstrip('\n'))
+
+            else:
+                password = unicode(getpass.getpass('Password: '))
+
+        elif vault_type == u'asymmetric':
+
+            # get vault private key
+            if private_key:
+                pass
+
+            elif private_key_file:
+                with open(private_key_file, 'rb') as f:
+                    private_key = f.read()
+
+            else:
+                raise errors.ValidationError(
+                    name='private_key',
+                    error=_('Missing vault private key'))
+
+        else:
+            raise errors.ValidationError(
+                name='vault_type',
+                error=_('Invalid vault type'))
+
+        result = self.api.Command.vault_retrieve(
+            vault_id,
+            password=password,
+            private_key=private_key)['result']
+
+        json_data = self.obj.parse_result(result)
+
+        secrets = json_data['secrets']
+
+        # decode data for response
+        for secret in secrets:
+            secret['data'] = base64.b64decode(secret['data'])
+
+        response = {
+            'count': len(secrets),
+            'truncated': False,
+            'summary': u'%d vault secret matched' % len(secrets),
+            'result': secrets,
+        }
+
+        return response
+
+
+ at register()
+class vaultsecret_mod(LDAPRetrieve):
+    __doc__ = _('Modify a vault secret.')
+
+    takes_options = (
+        Str(
+            'parent_id?',
+            cli_name='parent_id',
+            doc=_('Parent ID'),
+        ),
+        Str(
+            'description?',
+            cli_name='desc',
+            doc=_('Secret description'),
+        ),
+        Bytes(
+            'data?',
+            cli_name='data',
+            doc=_('Binary secret data'),
+        ),
+        Str(
+            'text?',
+            cli_name='text',
+            doc=_('Text secret data'),
+        ),
+        Str(  # TODO: use File parameter
+            'in?',
+            cli_name='in',
+            doc=_('File containing secret data'),
+        ),
+        Str(
+            'password?',
+            cli_name='password',
+            doc=_('Vault password'),
+        ),
+        Str(  # TODO: use File parameter
+            'password_file?',
+            cli_name='password_file',
+            doc=_('File containing the vault password'),
+        ),
+        Bytes(
+            'private_key?',
+            cli_name='private_key',
+            doc=_('Vault private key'),
+        ),
+        Str(  # TODO: use File parameter
+            'private_key_file?',
+            cli_name='private_key_file',
+            doc=_('File containing the vault private key'),
+        ),
+    )
+
+    msg_summary = _('Modified vault secret "%(value)s"')
+
+    def forward(self, *args, **options):
+
+        dn = self.api.Object.vault.get_dn(*args, **options)
+        vault_id = self.api.Object.vault.get_id(dn)
+        secret_name = args[1]
+
+        vault_type = u'standard'
+        salt = None
+
+        # retrieve vault info
+        vault = self.api.Command.vault_show(vault_id)['result']
+
+        if 'ipavaulttype' in vault:
+            vault_type = vault['ipavaulttype'][0]
+
+        if 'ipavaultsalt' in vault:
+            salt = vault['ipavaultsalt'][0].encode('utf-8')
+
+        description = options.get('description')
+        data = options.get('data')
+        text = options.get('text')
+        input_file = options.get('in')
+        password = options.get('password')
+        password_file = options.get('password_file')
+        private_key = options.get('private_key')
+        private_key_file = options.get('private_key_file')
+
+        # don't send these parameters to server
+        if 'data' in options:
+            del options['data']
+        if 'text' in options:
+            del options['text']
+        if 'in' in options:
+            del options['in']
+        if 'password' in options:
+            del options['password']
+        if 'password_file' in options:
+            del options['password_file']
+        if 'private_key' in options:
+            del options['private_key']
+        if 'private_key_file' in options:
+            del options['private_key_file']
+
+        # type-specific initialization
+        if vault_type == u'standard':
+
+            pass
+
+        elif vault_type == u'symmetric':
+
+            # get vault password
+            if password:
+                pass
+
+            elif password_file:
+                with open(password_file) as f:
+                    password = unicode(f.read().rstrip('\n'))
+
+            else:
+                password = unicode(getpass.getpass('Password: '))
+
+        elif vault_type == u'asymmetric':
+
+            # get vault private key
+            if private_key:
+                pass
+
+            elif private_key_file:
+                with open(private_key_file, 'rb') as f:
+                    private_key = f.read()
+
+            else:
+                raise errors.ValidationError(
+                    name='private_key',
+                    error=_('Missing vault private key'))
+
+        else:
+            raise errors.ValidationError(
+                name='vault_type',
+                error=_('Invalid vault type'))
+
+        # retrieve secrets
+        result = self.api.Command.vault_retrieve(
+            vault_id,
+            password=password,
+            private_key=private_key)['result']
+
+        json_data = self.obj.parse_result(result)
+
+        secrets = json_data['secrets']
+
+        # find the secret
+        secret = self.obj.find(secrets, secret_name)
+
+        # get data
+        if data:
+            if text or input_file:
+                raise errors.MutuallyExclusiveError(
+                    reason=_('Input data specified multiple times'))
+
+        elif text:
+            if input_file:
+                raise errors.MutuallyExclusiveError(
+                    reason=_('Input data specified multiple times'))
+
+            data = text.encode()
+
+        elif input_file:
+            with open(input_file, 'rb') as f:
+                data = f.read()
+
+        else:
+            pass
+
+        # retrieve secrets
+        result = self.api.Command.vault_retrieve(
+            vault_id,
+            password=password,
+            private_key=private_key)['result']
+
+        json_data = self.obj.parse_result(result)
+
+        secrets = json_data['secrets']
+
+        # find the secret
+        secret = None
+        for s in secrets:
+            if s['secret_name'] == secret_name:
+                secret = s
+                break
+
+        if not secret:
+            raise errors.NotFound(
+                reason=_('%s: vault secret not found' % secret_name))
+
+        # modify the secret
+        if description:
+            secret['description'] = description
+        if data:
+            secret['data'] = base64.b64encode(data).decode('utf-8')
+
+        # rearchive secrets
+        vault_data = json.dumps(json_data)
+        response = self.api.Command.vault_archive(
+            vault_id,
+            data=vault_data,
+            password=password)
+
+        # decode data for response
+        secret['data'] = base64.b64decode(secret['data'])
+
+        response = {
+            'value': secret_name,
+            'summary': u'Modified vault secret "%s"' % secret_name,
+            'result': secret,
+        }
+
+        return response
+
+
+ at register()
+class vaultsecret_show(LDAPRetrieve):
+    __doc__ = _('Display information about a vault secret.')
+
+    takes_options = (
+        Str(
+            'parent_id?',
+            cli_name='parent_id',
+            doc=_('Parent ID'),
+        ),
+        Flag(
+            'show_text?',
+            doc=_('Show text data'),
+            autofill=False,
+        ),
+        Flag(
+            'stdout?',
+            doc=_('Show data on standard output'),
+            autofill=False,
+        ),
+        Str(
+            'out?',
+            cli_name='out',
+            doc=_('File to store retrieved data'),
+        ),
+        Str(
+            'password?',
+            cli_name='password',
+            doc=_('Vault password'),
+        ),
+        Str(  # TODO: use File parameter
+            'password_file?',
+            cli_name='password_file',
+            doc=_('File containing the vault password'),
+        ),
+        Bytes(
+            'private_key?',
+            cli_name='private_key',
+            doc=_('Vault private key'),
+        ),
+        Str(  # TODO: use File parameter
+            'private_key_file?',
+            cli_name='private_key_file',
+            doc=_('File containing the vault private key'),
+        ),
+    )
+
+    has_output_params = (
+        Str(
+            'text',
+            label=_('Text'),
+        ),
+    )
+
+    def forward(self, *args, **options):
+
+        dn = self.api.Object.vault.get_dn(*args, **options)
+        vault_id = self.api.Object.vault.get_id(dn)
+        secret_name = args[1]
+
+        vault_type = u'standard'
+        salt = None
+
+        # retrieve vault info
+        vault = self.api.Command.vault_show(vault_id)['result']
+
+        if 'ipavaulttype' in vault:
+            vault_type = vault['ipavaulttype'][0]
+
+        if 'ipavaultsalt' in vault:
+            salt = vault['ipavaultsalt'][0].encode('utf-8')
+
+        show_text = options.get('show_text')
+        stdout = options.get('stdout')
+        output_file = options.get('out')
+        password = options.get('password')
+        password_file = options.get('password_file')
+        private_key = options.get('private_key')
+        private_key_file = options.get('private_key_file')
+
+        # don't send these parameters to server
+        if 'show_text' in options:
+            del options['show_text']
+        if 'stdout' in options:
+            del options['stdout']
+        if 'out' in options:
+            del options['out']
+        if 'password' in options:
+            del options['password']
+        if 'password_file' in options:
+            del options['password_file']
+        if 'private_key' in options:
+            del options['private_key']
+        if 'private_key_file' in options:
+            del options['private_key_file']
+
+        # type-specific initialization
+        if vault_type == u'standard':
+
+            pass
+
+        elif vault_type == u'symmetric':
+
+            # get vault password
+            if password:
+                pass
+
+            elif password_file:
+                with open(password_file) as f:
+                    password = unicode(f.read().rstrip('\n'))
+
+            else:
+                password = unicode(getpass.getpass('Password: '))
+
+        elif vault_type == u'asymmetric':
+
+            # get vault private key
+            if private_key:
+                pass
+
+            elif private_key_file:
+                with open(private_key_file, 'rb') as f:
+                    private_key = f.read()
+
+            else:
+                raise errors.ValidationError(
+                    name='private_key',
+                    error=_('Missing vault private key'))
+
+        else:
+            raise errors.ValidationError(
+                name='vault_type',
+                error=_('Invalid vault type'))
+
+        # retrieve secrets
+        result = self.api.Command.vault_retrieve(
+            vault_id,
+            password=password,
+            private_key=private_key)['result']
+
+        json_data = self.obj.parse_result(result)
+
+        secrets = json_data['secrets']
+
+        secret = None
+
+        # find the secret
+        for s in secrets:
+            if s['secret_name'] == secret_name:
+                secret = s
+                break
+
+        if not secret:
+            raise errors.NotFound(
+                reason=_('%s: vault secret not found' % secret_name))
+
+        # decode data for response
+        secret['data'] = base64.b64decode(secret['data'])
+
+        response = {
+            'value': secret_name,
+            'result': secret,
+        }
+
+        if stdout:
+            sys.stdout.write(secret['data'])
+            response['result'] = {}
+
+        elif output_file:
+            with open(output_file, 'w') as f:
+                f.write(secret['data'])
+            response['result'] = {}
+
+        elif show_text:
+            response['result']['text'] = unicode(secret['data'])
+            del response['result']['data']
+
+        else:
+            pass
+
+        return response
diff --git a/ipatests/test_xmlrpc/test_vaultsecret_plugin.py b/ipatests/test_xmlrpc/test_vaultsecret_plugin.py
new file mode 100644
index 0000000000000000000000000000000000000000..b8257299510df9efecbbba320c9d22177aad0276
--- /dev/null
+++ b/ipatests/test_xmlrpc/test_vaultsecret_plugin.py
@@ -0,0 +1,470 @@
+# 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/vaultsecret.py` module.
+"""
+
+from ipalib import api, errors
+from xmlrpc_test import Declarative, fuzzy_string
+
+test_vault = u'test_vault'
+shared_test_vault = u'/shared/%s' % test_vault
+symmetric_vault = u'symmetric_vault'
+asymmetric_vault = u'asymmetric_vault'
+
+test_secret = u'test_secret'
+binary_data = '\x01\x02\x03\x04'
+text_data = u'secret'
+
+password = u'password'
+
+public_key = """
+-----BEGIN PUBLIC KEY-----
+MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAnT61EFxUOQgCJdM0tmw/
+pRRPDPGchTClnU1eBtiQD3ItKYf1+weMGwGOSJXPtkto7NlE7Qs8WHAr0UjyeBDe
+k/zeB6nSVdk47OdaW1AHrJL+44r238Jbm/+7VO5lTu6Z4N5p0VqoWNLi0Uh/CkqB
+tsxXaaAgjMp0AGq2U/aO/akeEYWQOYIdqUKVgAEKX5MmIA8tmbmoYIQ+B4Q3vX7N
+otG4eR6c2o9Fyjd+M4Gai5Ce0fSrigRvxAYi8xpRkQ5yQn5gf4WVrn+UKTfOIjLO
+pVThop+Xivcre3SpI0kt6oZPhBw9i8gbMnqifVmGFpVdhq+QVBqp+MVJvTbhRPG6
+3wIDAQAB
+-----END PUBLIC KEY-----
+"""
+
+private_key = """
+-----BEGIN RSA PRIVATE KEY-----
+MIIEpAIBAAKCAQEAnT61EFxUOQgCJdM0tmw/pRRPDPGchTClnU1eBtiQD3ItKYf1
++weMGwGOSJXPtkto7NlE7Qs8WHAr0UjyeBDek/zeB6nSVdk47OdaW1AHrJL+44r2
+38Jbm/+7VO5lTu6Z4N5p0VqoWNLi0Uh/CkqBtsxXaaAgjMp0AGq2U/aO/akeEYWQ
+OYIdqUKVgAEKX5MmIA8tmbmoYIQ+B4Q3vX7NotG4eR6c2o9Fyjd+M4Gai5Ce0fSr
+igRvxAYi8xpRkQ5yQn5gf4WVrn+UKTfOIjLOpVThop+Xivcre3SpI0kt6oZPhBw9
+i8gbMnqifVmGFpVdhq+QVBqp+MVJvTbhRPG63wIDAQABAoIBAQCD2bXnfxPcMnvi
+jaPwpvoDCPF0EBBHmk/0g5ApO2Qon3uBDJFUqbJwXrCY6o2d9MOJfnGONlKmcYA8
+X+d4h+SqwGjIkjxdYeSauS+Jy6Rzr1ptH/P8EjPQrfG9uJxYQDflV3nxYwwwVrx7
+8kccMPdteRB+8Bb7FzOHufMimmayCNFETnVT5CKH2PrYoPB+fr0itCipWOenDp33
+e73OV+K9U3rclmtHaoRxGohqByKfQRUkipjw4m+T3qfZZc5eN77RGW8J+oL1GVom
+fwtiH7N1HVte0Dmd13nhiASg355kjqRPcIMPsRHvXkOpgg5HRUTKG5elqAyvvm27
+Fzj1YdeRAoGBAMnE61+FYh8qCyEGe8r6RGjO8iuoyk1t+0gBWbmILLBiRnj4K8Tc
+k7HBG/pg3XCNbCuRwiLg8tk3VAAXzn6o+IJr3QnKbNCGa1lKfYU4mt11sBEyuL5V
+NpZcZ8IiPhMlGyDA9cFbTMKOE08RqbOIdxOmTizFt0R5sYZAwOjEvBIZAoGBAMeC
+N/P0bdrScFZGeS51wEdiWme/CO0IyGoqU6saI8L0dbmMJquiaAeIEjIKLqxH1RON
+axhsyk97e0PCcc5QK62Utf50UUAbL/v7CpIG+qdSRYDO4bVHSCkwF32N3pYh/iVU
+EsEBEkZiJi0dWa/0asDbsACutxcHda3RI5pi7oO3AoGAcbGNs/CUHt1xEfX2UaT+
+YVSjb2iYPlNH8gYYygvqqqVl8opdF3v3mYUoP8jPXrnCBzcF/uNk1HNx2O+RQxvx
+lIQ1NGwlLsdfvBvWaPhBg6LqSHadVVrs/IMrUGA9PEp/Y9B3arIIqeSnCrn4Nxsh
+higDCwWKRIKSPwVD7qXVGBkCgYEAu5/CASIRIeYgEXMLSd8hKcDcJo8o1MoauIT/
+1Hyrvw9pm0qrn2QHk3WrLvYWeJzBTTcEzZ6aEG+fN9UodA8/VGnzUc6QDsrCsKWh
+hj0cArlDdeSZrYLQ4TNCFCiUePqU6QQM8weP6TMqlejxTKF+t8qi1bF5rCWuzP1P
+D0UU7DcCgYAUvmEGckugS+FTatop8S/rmkcQ4Bf5M/YCZfsySavucDiHcBt0QtXt
+Swh0XdDsYS3W1yj2XqqsQ7R58KNaffCHjjulWFzb5IiuSvvdxzWtiXHisOpO36MJ
+kUlCMj24a8XsShzYTWBIyW2ngvGe3pQ9PfjkUdm0LGZjYITCBvgOKw==
+-----END RSA PRIVATE KEY-----
+"""
+
+
+class test_vaultsecret_plugin(Declarative):
+
+    cleanup_commands = [
+        ('vault_del', [test_vault], {'continue': True}),
+        ('vault_del', [shared_test_vault], {'continue': True}),
+        ('vault_del', [symmetric_vault], {'continue': True}),
+        ('vault_del', [asymmetric_vault], {'continue': True}),
+    ]
+
+    tests = [
+
+        {
+            'desc': 'Create test vault',
+            'command': (
+                'vault_add',
+                [test_vault],
+                {},
+            ),
+            'expected': {
+                'value': test_vault,
+                'summary': 'Added vault "%s"' % test_vault,
+                'result': {
+                    'dn': u'cn=%s,cn=admin,cn=users,cn=vaults,%s'
+                          % (test_vault, api.env.basedn),
+                    'cn': [test_vault],
+                    'vault_id': u'/users/admin/%s' % test_vault,
+                    'owner_user': [u'admin'],
+                    'ipavaulttype': [u'standard'],
+                },
+            },
+        },
+
+        {
+            'desc': 'Create secret with binary data',
+            'command': (
+                'vaultsecret_add',
+                [test_vault, test_secret],
+                {
+                    'data': binary_data,
+                },
+            ),
+            'expected': {
+                'value': test_secret,
+                'summary': 'Added vault secret "%s"' % test_secret,
+                'result': {
+                    'secret_name': test_secret,
+                    'data': binary_data,
+                },
+            },
+        },
+
+        {
+            'desc': 'Create duplicate vault secret',
+            'command': (
+                'vaultsecret_add',
+                [test_vault, test_secret],
+                {},
+            ),
+            'expected': errors.DuplicateEntry(
+                message=u'vault secret with name "%s" already exists'
+                        % test_secret),
+        },
+
+        {
+            'desc': 'Find vault secrets',
+            'command': (
+                'vaultsecret_find',
+                [test_vault],
+                {},
+            ),
+            'expected': {
+                'count': 1,
+                'truncated': False,
+                'summary': u'1 vault secret matched',
+                'result': [
+                    {
+                        'secret_name': test_secret,
+                        'data': binary_data,
+                    },
+                ],
+            },
+        },
+
+        {
+            'desc': 'Retrieve secret',
+            'command': (
+                'vaultsecret_show',
+                [test_vault, test_secret],
+                {},
+            ),
+            'expected': {
+                'value': test_secret,
+                'summary': None,
+                'result': {
+                    'secret_name': test_secret,
+                    'data': binary_data,
+                },
+            },
+        },
+
+        {
+            'desc': 'Modify secret',
+            'command': (
+                'vaultsecret_mod',
+                [test_vault, test_secret],
+                {
+                    'description': u'Secret',
+                },
+            ),
+            'expected': {
+                'value': test_secret,
+                'summary': u'Modified vault secret "%s"' % test_secret,
+                'result': {
+                    'secret_name': test_secret,
+                    'description': u'Secret',
+                    'data': binary_data,
+                },
+            },
+        },
+
+        {
+            'desc': 'Create symmetric vault',
+            'command': (
+                'vault_add',
+                [symmetric_vault],
+                {
+                    'ipavaulttype': u'symmetric',
+                    'password': password,
+                },
+            ),
+            'expected': {
+                'value': symmetric_vault,
+                'summary': 'Added vault "%s"' % symmetric_vault,
+                'result': {
+                    'dn': u'cn=%s,cn=admin,cn=users,cn=vaults,%s'
+                          % (symmetric_vault, api.env.basedn),
+                    'cn': [symmetric_vault],
+                    'vault_id': u'/users/admin/%s' % symmetric_vault,
+                    'owner_user': [u'admin'],
+                    'ipavaulttype': [u'symmetric'],
+                    'ipavaultsalt': [fuzzy_string],
+                },
+            },
+        },
+
+        {
+            'desc': 'Create secret in symmetric vault',
+            'command': (
+                'vaultsecret_add',
+                [symmetric_vault, test_secret],
+                {
+                    'password': password,
+                    'data': binary_data,
+                },
+            ),
+            'expected': {
+                'value': test_secret,
+                'summary': 'Added vault secret "%s"' % test_secret,
+                'result': {
+                    'secret_name': test_secret,
+                    'data': binary_data,
+                },
+            },
+        },
+
+        {
+            'desc': 'Create asymmetric vault',
+            'command': (
+                'vault_add',
+                [asymmetric_vault],
+                {
+                    'ipavaulttype': u'asymmetric',
+                    'ipapublickey': public_key,
+                },
+            ),
+            'expected': {
+                'value': asymmetric_vault,
+                'summary': 'Added vault "%s"' % asymmetric_vault,
+                'result': {
+                    'dn': u'cn=%s,cn=admin,cn=users,cn=vaults,%s'
+                          % (asymmetric_vault, api.env.basedn),
+                    'cn': [asymmetric_vault],
+                    'vault_id': u'/users/admin/%s' % asymmetric_vault,
+                    'owner_user': [u'admin'],
+                    'ipavaulttype': [u'asymmetric'],
+                    'ipavaultsalt': [fuzzy_string],
+                    'ipapublickey': [public_key],
+                },
+            },
+        },
+
+        {
+            'desc': 'Create secret in asymmetric vault',
+            'command': (
+                'vaultsecret_add',
+                [asymmetric_vault, test_secret],
+                {
+                    'private_key': private_key,
+                    'data': binary_data,
+                },
+            ),
+            'expected': {
+                'value': test_secret,
+                'summary': 'Added vault secret "%s"' % test_secret,
+                'result': {
+                    'secret_name': test_secret,
+                    'data': binary_data,
+                },
+            },
+        },
+
+        {
+            'desc': 'Delete secret',
+            'command': (
+                'vaultsecret_del',
+                [test_vault, test_secret],
+                {},
+            ),
+            'expected': {
+                'value': test_secret,
+                'summary': u'Deleted vault secret "%s"' % test_secret,
+                'result': {
+                    'failed': (),
+                },
+            },
+        },
+
+        {
+            'desc': 'Delete non-existent vault secret',
+            'command': (
+                'vaultsecret_del',
+                [test_vault, test_secret],
+                {},
+            ),
+            'expected': errors.NotFound(
+                reason=u'%s: vault secret not found' % test_secret),
+        },
+
+        {
+            'desc': 'Create secret with text data',
+            'command': (
+                'vaultsecret_add',
+                [test_vault, test_secret],
+                {
+                    'text': text_data,
+                },
+            ),
+            'expected': {
+                'value': test_secret,
+                'summary': 'Added vault secret "%s"' % test_secret,
+                'result': {
+                    'secret_name': test_secret,
+                    'data': text_data.encode('utf-8'),
+                },
+            },
+        },
+
+        {
+            'desc': 'Retrieve secret as text',
+            'command': (
+                'vaultsecret_show',
+                [test_vault, test_secret],
+                {
+                    'show_text': True,
+                },
+            ),
+            'expected': {
+                'value': test_secret,
+                'summary': None,
+                'result': {
+                    'secret_name': test_secret,
+                    'text': text_data,
+                },
+            },
+        },
+
+        {
+            'desc': 'Create shared test vault',
+            'command': (
+                'vault_add',
+                [shared_test_vault],
+                {},
+            ),
+            'expected': {
+                'value': test_vault,
+                'summary': 'Added vault "%s"' % test_vault,
+                'result': {
+                    'dn': u'cn=%s,cn=shared,cn=vaults,%s'
+                          % (test_vault, api.env.basedn),
+                    'cn': [test_vault],
+                    'vault_id': u'/shared/%s' % test_vault,
+                    'owner_user': [u'admin'],
+                    'ipavaulttype': [u'standard'],
+                },
+            },
+        },
+
+        {
+            'desc': 'Create shared secret with binary data',
+            'command': (
+                'vaultsecret_add',
+                [shared_test_vault, test_secret],
+                {
+                    'data': binary_data,
+                },
+            ),
+            'expected': {
+                'value': test_secret,
+                'summary': 'Added vault secret "%s"' % test_secret,
+                'result': {
+                    'secret_name': test_secret,
+                    'data': binary_data,
+                },
+            },
+        },
+
+        {
+            'desc': 'Find shared vault secrets',
+            'command': (
+                'vaultsecret_find',
+                [shared_test_vault],
+                {},
+            ),
+            'expected': {
+                'count': 1,
+                'truncated': False,
+                'summary': u'1 vault secret matched',
+                'result': [
+                    {
+                        'secret_name': test_secret,
+                        'data': binary_data,
+                    },
+                ],
+            },
+        },
+
+        {
+            'desc': 'Retrieve shared secret',
+            'command': (
+                'vaultsecret_show',
+                [shared_test_vault, test_secret],
+                {},
+            ),
+            'expected': {
+                'value': test_secret,
+                'summary': None,
+                'result': {
+                    'secret_name': test_secret,
+                    'data': binary_data,
+                },
+            },
+        },
+
+        {
+            'desc': 'Modify shared secret',
+            'command': (
+                'vaultsecret_mod',
+                [shared_test_vault, test_secret],
+                {
+                    'description': u'Secret',
+                },
+            ),
+            'expected': {
+                'value': test_secret,
+                'summary': u'Modified vault secret "%s"' % test_secret,
+                'result': {
+                    'secret_name': test_secret,
+                    'description': u'Secret',
+                    'data': binary_data,
+                },
+            },
+        },
+
+        {
+            'desc': 'Delete shared secret',
+            'command': (
+                'vaultsecret_del',
+                [shared_test_vault, test_secret],
+                {},
+            ),
+            'expected': {
+                'value': test_secret,
+                'summary': u'Deleted vault secret "%s"' % test_secret,
+                'result': {
+                    'failed': (),
+                },
+            },
+        },
+
+    ]
-- 
2.3.1

-------------- next part --------------
>From 89a0867b7b6f033cace7676abbc11ed111980326 Mon Sep 17 00:00:00 2001
From: "Endi S. Dewata" <edewata at redhat.com>
Date: Sat, 21 Feb 2015 17:17:03 -0500
Subject: [PATCH] Added vault escrow.

The symmetric and asymmetric vaults have been modified to support
escrow for recovery. A new LDAP attribute type has been added to
store the escrow public key. New test scripts have been added to
test the functionality.

https://fedorahosted.org/freeipa/ticket/3872
---
 API.txt                                   |  18 ++--
 install/share/60basev3.ldif               |   3 +-
 ipalib/plugins/vault.py                   | 144 +++++++++++++++++++++++++++---
 ipatests/test_xmlrpc/test_vault_plugin.py | 120 +++++++++++++++++++++++++
 4 files changed, 267 insertions(+), 18 deletions(-)

diff --git a/API.txt b/API.txt
index efea0cca83df0ba8d24b4218a809263186da9e0d..5b7114803ddb48d2f9d846107cc99f7b3dd996d2 100644
--- a/API.txt
+++ b/API.txt
@@ -4515,12 +4515,14 @@ output: Output('result', <type 'bool'>, None)
 output: Output('summary', (<type 'unicode'>, <type 'NoneType'>), None)
 output: PrimaryKey('value', None, None)
 command: vault_add
-args: 1,14,3
+args: 1,16,3
 arg: Str('cn', attribute=True, cli_name='vault_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: Bytes('data?', cli_name='data')
 option: Str('description?', cli_name='desc')
+option: Str('escrow_public_key_file?', cli_name='escrow_public_key_file')
 option: Str('in?', cli_name='in')
+option: Bytes('ipaescrowpublickey?', cli_name='escrow_public_key')
 option: Bytes('ipapublickey?', cli_name='public_key')
 option: Str('ipavaulttype?', autofill=True, cli_name='type', default=u'standard')
 option: Flag('no_members', autofill=True, default=False, exclude='webui')
@@ -4561,13 +4563,15 @@ output: Output('completed', <type 'int'>, None)
 output: Output('failed', <type 'dict'>, None)
 output: Entry('result', <type 'dict'>, Gettext('A dictionary representing an LDAP entry', domain='ipa', localedir=None))
 command: vault_archive
-args: 1,19,3
+args: 1,21,3
 arg: Str('cn', attribute=True, cli_name='vault_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('create?', autofill=True, default=False)
 option: Bytes('data?', cli_name='data')
 option: Str('description?', cli_name='desc')
+option: Str('escrow_public_key_file?', cli_name='escrow_public_key_file')
 option: Str('in?', cli_name='in')
+option: Bytes('ipaescrowpublickey?', cli_name='escrow_public_key')
 option: Bytes('ipapublickey?', cli_name='public_key')
 option: Str('ipavaultsalt?', cli_name='salt')
 option: Str('ipavaulttype?', autofill=True, cli_name='type', default=u'standard')
@@ -4595,11 +4599,12 @@ output: Output('result', <type 'dict'>, None)
 output: Output('summary', (<type 'unicode'>, <type 'NoneType'>), None)
 output: ListOfPrimaryKeys('value', None, None)
 command: vault_find
-args: 1,14,4
+args: 1,15,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='vault_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: Bytes('ipaescrowpublickey', attribute=True, autofill=False, cli_name='escrow_public_key', multivalue=False, query=True, required=False)
 option: Bytes('ipapublickey', attribute=True, autofill=False, cli_name='public_key', multivalue=False, query=True, required=False)
 option: Str('ipavaultsalt', attribute=True, autofill=False, cli_name='salt', multivalue=False, query=True, required=False)
 option: Str('ipavaulttype', attribute=True, autofill=False, cli_name='type', default=u'standard', multivalue=False, query=True, required=False)
@@ -4616,12 +4621,13 @@ output: ListOfEntries('result', (<type 'list'>, <type 'tuple'>), Gettext('A list
 output: Output('summary', (<type 'unicode'>, <type 'NoneType'>), None)
 output: Output('truncated', <type 'bool'>, None)
 command: vault_mod
-args: 1,14,3
+args: 1,15,3
 arg: Str('cn', attribute=True, cli_name='vault_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: Bytes('ipaescrowpublickey', attribute=True, autofill=False, cli_name='escrow_public_key', multivalue=False, required=False)
 option: Bytes('ipapublickey', attribute=True, autofill=False, cli_name='public_key', multivalue=False, required=False)
 option: Str('ipavaultsalt', attribute=True, autofill=False, cli_name='salt', multivalue=False, required=False)
 option: Str('ipavaulttype', attribute=True, autofill=False, cli_name='type', default=u'standard', multivalue=False, required=False)
@@ -4662,9 +4668,11 @@ output: Output('completed', <type 'int'>, None)
 output: Output('failed', <type 'dict'>, None)
 output: Entry('result', <type 'dict'>, Gettext('A dictionary representing an LDAP entry', domain='ipa', localedir=None))
 command: vault_retrieve
-args: 1,13,3
+args: 1,15,3
 arg: Str('cn', attribute=True, cli_name='vault_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: Bytes('escrow_private_key?', cli_name='escrow_private_key')
+option: Str('escrow_private_key_file?', cli_name='escrow_private_key_file')
 option: Flag('no_members', autofill=True, default=False, exclude='webui')
 option: Str('out?', cli_name='out')
 option: Str('parent_id?', cli_name='parent_id')
diff --git a/install/share/60basev3.ldif b/install/share/60basev3.ldif
index 9e0f70a41ef50e78d4e464bab428325dfb6568fa..d0ea7135b724381dc21281870c78b7b5b0f67be1 100644
--- a/install/share/60basev3.ldif
+++ b/install/share/60basev3.ldif
@@ -56,6 +56,7 @@ attributeTypes: (2.16.840.1.113730.3.8.11.64 NAME 'ipaSecretKeyRef' DESC 'DN of
 attributeTypes: (2.16.840.1.113730.3.8.11.65 NAME 'ipaWrappingMech' DESC 'PKCS#11 wrapping mechanism equivalent to CK_MECHANISM_TYPE' EQUALITY caseIgnoreMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 SINGLE-VALUE X-ORIGIN 'IPA v4.1')
 attributeTypes: (2.16.840.1.113730.3.8.18.2.1 NAME 'ipaVaultType' DESC 'IPA vault type' EQUALITY caseExactMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 X-ORIGIN 'IPA v4.2')
 attributeTypes: (2.16.840.1.113730.3.8.18.2.2 NAME 'ipaVaultSalt' DESC 'IPA vault salt' EQUALITY caseExactMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 X-ORIGIN 'IPA v4.2')
+attributeTypes: (2.16.840.1.113730.3.8.18.2.3 NAME 'ipaEscrowPublicKey' DESC 'IPA escrow public key as DER-encoded SubjectPublicKeyInfo (RFC 5280)' EQUALITY octetStringMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.40 X-ORIGIN 'IPA v4.2' )
 objectClasses: (2.16.840.1.113730.3.8.12.1 NAME 'ipaExternalGroup' SUP top STRUCTURAL MUST ( cn ) MAY ( ipaExternalMember $ memberOf $ description $ owner) X-ORIGIN 'IPA v3' )
 objectClasses: (2.16.840.1.113730.3.8.12.2 NAME 'ipaNTUserAttrs' SUP top AUXILIARY MUST ( ipaNTSecurityIdentifier ) MAY ( ipaNTHash $ ipaNTLogonScript $ ipaNTProfilePath $ ipaNTHomeDirectory $ ipaNTHomeDirectoryDrive ) X-ORIGIN 'IPA v3' )
 objectClasses: (2.16.840.1.113730.3.8.12.3 NAME 'ipaNTGroupAttrs' SUP top AUXILIARY MUST ( ipaNTSecurityIdentifier ) X-ORIGIN 'IPA v3' )
@@ -79,5 +80,5 @@ 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 $ owner $ member $ ipaVaultType $ ipaVaultSalt $ ipaPublicKey ) X-ORIGIN 'IPA v4.2' )
+objectClasses: (2.16.840.1.113730.3.8.18.1.1 NAME 'ipaVault' DESC 'IPA vault' SUP top STRUCTURAL MUST ( cn ) MAY ( description $ owner $ member $ ipaVaultType $ ipaVaultSalt $ ipaPublicKey $ ipaEscrowPublicKey ) X-ORIGIN 'IPA v4.2' )
 objectClasses: (2.16.840.1.113730.3.8.18.1.2 NAME 'ipaVaultContainer' DESC 'IPA vault container' SUP top STRUCTURAL MUST ( cn ) MAY ( description $ owner $ member ) X-ORIGIN 'IPA v4.2' )
diff --git a/ipalib/plugins/vault.py b/ipalib/plugins/vault.py
index 848488d646f87e52ff44dfa95d024372f2c9bd2c..7978bf29a9be09b8db1726280ca7a5b913876cd7 100644
--- a/ipalib/plugins/vault.py
+++ b/ipalib/plugins/vault.py
@@ -75,6 +75,14 @@ EXAMPLES:
  Add an asymmetric vault:
    ipa vault-add MyVault --type asymmetric --public-key-file public.pem
 """) + _("""
+ Add a escrowed symmetric vault:
+   ipa vault-add MyVault --type symmetric --password-file password.txt\
+       --escrow-public-key-file escrow-public.pem
+""") + _("""
+ Add an escrowed asymmetric vault:
+   ipa vault-add MyVault --type asymmetric --public-key-file public.pem\
+       --escrow-public-key-file escrow-public.pem
+""") + _("""
  Show a vault:
    ipa vault-show MyVault
 """) + _("""
@@ -102,6 +110,14 @@ EXAMPLES:
  Retrieve data from asymmetric vault:
    ipa vault-retrieve MyVault --out data.bin --private-key-file private.pem
 """) + _("""
+ Recover data from escrowed symmetric vault:
+   ipa vault-retrieve MyVault --out data.bin\
+       --escrow-private-key-file escrow-private.pem
+""") + _("""
+ Recover data from escrowed asymmetric vault:
+   ipa vault-retrieve MyVault --out data.bin\
+       --escrow-private-key-file escrow-private.pem
+""") + _("""
  Delete a vault:
    ipa vault-del MyVault
 """) + _("""
@@ -142,6 +158,7 @@ class vault(LDAPObject):
         'ipavaulttype',
         'ipavaultsalt',
         'ipapublickey',
+        'ipaescrowpublickey',
     ]
     search_display_attributes = [
         'cn',
@@ -200,6 +217,12 @@ class vault(LDAPObject):
             label=_('Public key'),
             doc=_('Vault public key'),
         ),
+        Bytes(
+            'ipaescrowpublickey?',
+            cli_name='escrow_public_key',
+            label=_('Escrow public key'),
+            doc=_('Escrow public key'),
+        ),
     )
 
     def get_dn(self, *args, **options):
@@ -332,7 +355,8 @@ class vault(LDAPObject):
         return (args, options)
 
     def create_entry(self, dn, description=None, vault_type=u'standard',
-                     salt=None, public_key=None, owner=None):
+                     salt=None, public_key=None, escrow_public_key=None,
+                     owner=None):
         """
         Creates vault entry and its parents.
         """
@@ -347,6 +371,7 @@ class vault(LDAPObject):
                 'ipavaulttype': vault_type,
                 'ipavaultsalt': salt,
                 'ipapublickey': public_key,
+                'ipaescrowpublickey': escrow_public_key,
                 'owner': owner,
             })
 
@@ -513,6 +538,16 @@ class vault_add(LDAPQuery):
             cli_name='public_key_file',
             doc=_('File containing the vault public key'),
         ),
+        Bytes(
+            'ipaescrowpublickey?',
+            cli_name='escrow_public_key',
+            doc=_('Escrow public key'),
+        ),
+        Str(  # TODO: use File parameter
+            'escrow_public_key_file?',
+            cli_name='escrow_public_key_file',
+            doc=_('File containing the escrow public key'),
+        ),
     )
 
     has_output = output.standard_entry
@@ -850,6 +885,16 @@ class vault_archive(LDAPQuery):
             cli_name='public_key_file',
             doc=_('File containing the vault public key'),
         ),
+        Bytes(
+            'ipaescrowpublickey?',
+            cli_name='escrow_public_key',
+            doc=_('Escrow public key'),
+        ),
+        Str(  # TODO: use File parameter
+            'escrow_public_key_file?',
+            cli_name='escrow_public_key_file',
+            doc=_('File containing the escrow public key'),
+        ),
     )
 
     has_output = output.standard_entry
@@ -877,12 +922,16 @@ class vault_archive(LDAPQuery):
             vault_type = options.get('ipavaulttype', u'standard')
             public_key = options.get('ipapublickey')
             public_key_file = options.get('public_key_file')
+            escrow_public_key = options.get('ipaescrowpublickey')
+            escrow_public_key_file = options.get('escrow_public_key_file')
 
         else:
             vault_type = u'standard'
             salt = None
             public_key = None
             public_key_file = None
+            escrow_public_key = None
+            escrow_public_key_file = None
 
             # retrieve vault info
             vault = self.api.Command.vault_show(vault_id)['result']
@@ -896,6 +945,10 @@ class vault_archive(LDAPQuery):
             if 'ipapublickey' in vault:
                 public_key = vault['ipapublickey'][0].encode('utf-8')
 
+            if 'ipaescrowpublickey' in vault:
+                escrow_public_key = vault['ipaescrowpublickey'][0]\
+                    .encode('utf-8')
+
         # don't send these parameters to server
         if 'data' in options:
             del options['data']
@@ -909,6 +962,8 @@ class vault_archive(LDAPQuery):
             del options['password_file']
         if 'public_key_file' in options:
             del options['public_key_file']
+        if 'escrow_public_key_file' in options:
+            del options['escrow_public_key_file']
 
         # get data
         if data:
@@ -931,6 +986,7 @@ class vault_archive(LDAPQuery):
             data = ''
 
         encrypted_key = None
+        escrowed_key = None
 
         if vault_type == u'standard':
 
@@ -979,6 +1035,11 @@ class vault_archive(LDAPQuery):
             # encrypt data with encryption key
             data = self.obj.encrypt(data, symmetric_key=encryption_key)
 
+            # encrypt encryption key with escrow public key
+            if escrow_public_key:
+                escrowed_key = self.obj.encrypt(
+                    encryption_key, public_key=escrow_public_key)
+
         elif vault_type == u'asymmetric':
 
             # generate encryption key
@@ -1013,6 +1074,11 @@ class vault_archive(LDAPQuery):
             encrypted_key = self.obj.encrypt(
                 encryption_key, public_key=public_key)
 
+            # encrypt encryption key with escrow public key
+            if escrow_public_key:
+                escrowed_key = self.obj.encrypt(
+                    encryption_key, public_key=escrow_public_key)
+
         else:
             raise errors.ValidationError(
                 name='vault_type',
@@ -1056,6 +1122,10 @@ class vault_archive(LDAPQuery):
             vault_data[u'encrypted_key'] = base64.b64encode(encrypted_key)\
                 .decode('utf-8')
 
+        if escrowed_key:
+            vault_data[u'escrowed_key'] = base64.b64encode(escrowed_key)\
+                .decode('utf-8')
+
         json_vault_data = json.dumps(vault_data)
 
         # wrap vault_data with session key
@@ -1083,6 +1153,7 @@ class vault_archive(LDAPQuery):
             description = options.get('description')
             vault_type = options.get('ipavaulttype')
             public_key = options.get('ipapublickey')
+            escrow_public_key = options.get('ipaescrowpublickey')
 
             # get user
             principal = getattr(context, 'principal')
@@ -1096,6 +1167,7 @@ class vault_archive(LDAPQuery):
                 vault_type=vault_type,
                 salt=salt,
                 public_key=public_key,
+                escrow_public_key=escrow_public_key,
                 owner=owner_dn,
             )
 
@@ -1197,6 +1269,16 @@ class vault_retrieve(LDAPQuery):
             cli_name='private_key_file',
             doc=_('File containing the vault private key'),
         ),
+        Bytes(
+            'escrow_private_key?',
+            cli_name='escrow_private_key',
+            doc=_('Escrow vault private key'),
+        ),
+        Str(  # TODO: use File parameter
+            'escrow_private_key_file?',
+            cli_name='escrow_private_key_file',
+            doc=_('File containing the escrow vault private key'),
+        ),
     )
 
     has_output = output.standard_entry
@@ -1243,6 +1325,8 @@ class vault_retrieve(LDAPQuery):
         password_file = options.get('password_file')
         private_key = options.get('private_key')
         private_key_file = options.get('private_key_file')
+        escrow_private_key = options.get('escrow_private_key')
+        escrow_private_key_file = options.get('escrow_private_key_file')
 
         # don't send these parameters to server
         if 'show_text' in options:
@@ -1259,6 +1343,10 @@ class vault_retrieve(LDAPQuery):
             del options['private_key']
         if 'private_key_file' in options:
             del options['private_key_file']
+        if 'escrow_private_key' in options:
+            del options['escrow_private_key']
+        if 'escrow_private_key_file' in options:
+            del options['escrow_private_key_file']
 
         # initialize NSS database
         crypto = pki.crypto.NSSCryptoProvider(paths.IPA_NSSDB_DIR)
@@ -1311,48 +1399,80 @@ class vault_retrieve(LDAPQuery):
             encrypted_key = base64.b64decode(vault_data[u'encrypted_key']
                                              .encode('utf-8'))
 
+        escrowed_key = None
+
+        if 'escrowed_key' in vault_data:
+            escrowed_key = base64.b64decode(vault_data['escrowed_key']
+                                            .encode('utf-8'))
+
         if vault_type == u'standard':
 
             pass
 
         elif vault_type == u'symmetric':
 
-            # get encryption key from vault password
+            # get encryption key from vault password or escrowed private key
             if password:
-                pass
+                encryption_key = self.obj.generate_symmetric_key(
+                    password, salt)
 
             elif password_file:
                 with open(password_file) as f:
                     password = unicode(f.read().rstrip('\n'))
 
+                encryption_key = self.obj.generate_symmetric_key(
+                    password, salt)
+
+            elif escrow_private_key:
+
+                encryption_key = self.obj.decrypt(
+                    escrowed_key, private_key=escrow_private_key)
+
+            elif escrow_private_key_file:
+                with open(escrow_private_key_file, 'rb') as f:
+                    escrow_private_key = f.read()
+
+                encryption_key = self.obj.decrypt(
+                    escrowed_key, private_key=escrow_private_key)
+
             else:
                 password = unicode(getpass.getpass('Password: '))
-
-            # generate encryption key from password
-            encryption_key = self.obj.generate_symmetric_key(password, salt)
+                encryption_key = self.obj.generate_symmetric_key(
+                    password, salt)
 
             # decrypt data with encryption key
             data = self.obj.decrypt(data, symmetric_key=encryption_key)
 
         elif vault_type == u'asymmetric':
 
-            # get encryption key with vault private key
+            # get encryption key with vault private key or escrowed private key
             if private_key:
-                pass
+                encryption_key = self.obj.decrypt(
+                    encrypted_key, private_key=private_key)
 
             elif private_key_file:
                 with open(private_key_file, 'rb') as f:
                     private_key = f.read()
 
+                encryption_key = self.obj.decrypt(
+                    encrypted_key, private_key=private_key)
+
+            elif escrow_private_key:
+                encryption_key = self.obj.decrypt(
+                    escrowed_key, private_key=escrow_private_key)
+
+            elif escrow_private_key_file:
+                with open(escrow_private_key_file, 'rb') as f:
+                    escrow_private_key = f.read()
+
+                encryption_key = self.obj.decrypt(
+                    escrowed_key, private_key=escrow_private_key)
+
             else:
                 raise errors.ValidationError(
                     name='private_key',
                     error=_('Missing vault private key'))
 
-            # decrypt encryption key with private key
-            encryption_key = self.obj.decrypt(
-                encrypted_key, private_key=private_key)
-
             # decrypt data with encryption key
             data = self.obj.decrypt(data, symmetric_key=encryption_key)
 
diff --git a/ipatests/test_xmlrpc/test_vault_plugin.py b/ipatests/test_xmlrpc/test_vault_plugin.py
index 218aa49dfd08de5b36a734ea84e7aa58a25a4d1b..3d0dd4473d3bdeb5087a04ed4351ea2a16f8e3a0 100644
--- a/ipatests/test_xmlrpc/test_vault_plugin.py
+++ b/ipatests/test_xmlrpc/test_vault_plugin.py
@@ -28,6 +28,8 @@ test_vault = u'test_vault'
 shared_test_vault = u'/shared/%s' % test_vault
 symmetric_vault = u'symmetric_vault'
 asymmetric_vault = u'asymmetric_vault'
+escrowed_symmetric_vault = u'escrowed_symmetric_vault'
+escrowed_asymmetric_vault = u'escrowed_asymmetric_vault'
 
 binary_data = '\x01\x02\x03\x04'
 text_data = u'secret'
@@ -127,6 +129,8 @@ class test_vault_plugin(Declarative):
         ('vault_del', [shared_test_vault], {'continue': True}),
         ('vault_del', [symmetric_vault], {'continue': True}),
         ('vault_del', [asymmetric_vault], {'continue': True}),
+        ('vault_del', [escrowed_symmetric_vault], {'continue': True}),
+        ('vault_del', [escrowed_asymmetric_vault], {'continue': True}),
     ]
 
     tests = [
@@ -643,4 +647,120 @@ class test_vault_plugin(Declarative):
                 message=u'Invalid credentials'),
         },
 
+        {
+            'desc': 'Create escrowed symmetric vault',
+            'command': (
+                'vault_add',
+                [escrowed_symmetric_vault],
+                {
+                    'ipavaulttype': u'symmetric',
+                    'password': password,
+                    'ipaescrowpublickey': other_public_key,
+                    'data': binary_data,
+                },
+            ),
+            'expected': {
+                'value': escrowed_symmetric_vault,
+                'summary': 'Added vault "%s"' % escrowed_symmetric_vault,
+                'result': {
+                    'dn': u'cn=%s,cn=admin,cn=users,cn=vaults,%s'
+                          % (escrowed_symmetric_vault, api.env.basedn),
+                    'cn': [escrowed_symmetric_vault],
+                    'vault_id': u'/users/admin/%s' % escrowed_symmetric_vault,
+                    'owner_user': [u'admin'],
+                    'ipavaulttype': [u'symmetric'],
+                    'ipavaultsalt': [fuzzy_string],
+                    'ipaescrowpublickey': [fuzzy_string],
+                },
+            },
+        },
+
+        {
+            'desc': 'Recover escrowed symmetric vault',
+            'command': (
+                'vault_retrieve',
+                [escrowed_symmetric_vault],
+                {
+                    'escrow_private_key': other_private_key,
+                },
+            ),
+            'expected': {
+                'value': escrowed_symmetric_vault,
+                'summary': u'Retrieved data from vault "%s"'
+                           % escrowed_symmetric_vault,
+                'result': {
+                    'dn': u'cn=%s,cn=admin,cn=users,cn=vaults,%s'
+                          % (escrowed_symmetric_vault, api.env.basedn),
+                    'cn': [escrowed_symmetric_vault],
+                    'vault_id': u'/users/admin/%s' % escrowed_symmetric_vault,
+                    'owner_user': [u'admin'],
+                    'ipavaulttype': [u'symmetric'],
+                    'ipavaultsalt': [fuzzy_string],
+                    'ipaescrowpublickey': [fuzzy_string],
+                    'nonce': fuzzy_string,
+                    'vault_data': fuzzy_string,
+                    'data': binary_data,
+                },
+            },
+        },
+
+        {
+            'desc': 'Create escrowed asymmetric vault',
+            'command': (
+                'vault_add',
+                [escrowed_asymmetric_vault],
+                {
+                    'ipavaulttype': u'asymmetric',
+                    'ipapublickey': public_key,
+                    'ipaescrowpublickey': other_public_key,
+                    'data': binary_data,
+                },
+            ),
+            'expected': {
+                'value': escrowed_asymmetric_vault,
+                'summary': 'Added vault "%s"' % escrowed_asymmetric_vault,
+                'result': {
+                    'dn': u'cn=%s,cn=admin,cn=users,cn=vaults,%s'
+                          % (escrowed_asymmetric_vault, api.env.basedn),
+                    'cn': [escrowed_asymmetric_vault],
+                    'vault_id': u'/users/admin/%s' % escrowed_asymmetric_vault,
+                    'owner_user': [u'admin'],
+                    'ipavaulttype': [u'asymmetric'],
+                    'ipavaultsalt': [fuzzy_string],
+                    'ipapublickey': [public_key],
+                    'ipaescrowpublickey': [fuzzy_string],
+                },
+            },
+        },
+
+        {
+            'desc': 'Recover escrowed asymmetric vault',
+            'command': (
+                'vault_retrieve',
+                [escrowed_asymmetric_vault],
+                {
+                    'escrow_private_key': other_private_key,
+                },
+            ),
+            'expected': {
+                'value': escrowed_asymmetric_vault,
+                'summary': u'Retrieved data from vault "%s"'
+                           % escrowed_asymmetric_vault,
+                'result': {
+                    'dn': u'cn=%s,cn=admin,cn=users,cn=vaults,%s'
+                          % (escrowed_asymmetric_vault, api.env.basedn),
+                    'cn': [escrowed_asymmetric_vault],
+                    'vault_id': u'/users/admin/%s' % escrowed_asymmetric_vault,
+                    'owner_user': [u'admin'],
+                    'ipavaulttype': [u'asymmetric'],
+                    'ipavaultsalt': [fuzzy_string],
+                    'ipapublickey': [public_key],
+                    'ipaescrowpublickey': [fuzzy_string],
+                    'nonce': fuzzy_string,
+                    'vault_data': fuzzy_string,
+                    'data': binary_data,
+                },
+            },
+        },
+
     ]
-- 
2.3.1

-------------- next part --------------
>From 2e71e450a24bc466e136d5dfaeab5ae2c4f3416d Mon Sep 17 00:00:00 2001
From: "Endi S. Dewata" <edewata at redhat.com>
Date: Mon, 23 Feb 2015 10:37:25 -0500
Subject: [PATCH] Updated VERSION file.

The API version number has been updated for the new password vault.

https://fedorahosted.org/freeipa/ticket/3872
---
 VERSION | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/VERSION b/VERSION
index 13c9760c82d587e8fbf9434dc15b58a902d94ba7..9de803316823666e234a1f4ca45f3c3488651e1f 100644
--- a/VERSION
+++ b/VERSION
@@ -90,5 +90,5 @@ IPA_DATA_VERSION=20100614120000
 #                                                      #
 ########################################################
 IPA_API_VERSION_MAJOR=2
-IPA_API_VERSION_MINOR=115
-# Last change: mbasti - Remove NSEC3PARAM record from dnsrecord-* commands
+IPA_API_VERSION_MINOR=116
+# Last change: edewata - Password Vault
-- 
2.3.1

-------------- next part --------------
>From a442016df1fd785dc1690ddf0dd09d04d78a181f Mon Sep 17 00:00:00 2001
From: "Endi S. Dewata" <edewata at redhat.com>
Date: Mon, 16 Mar 2015 05:08:56 -0400
Subject: [PATCH] Added vault copy functionality.

The vault plugins have been modified to provide a way to copy
data from one vault or vault secret to another. New test scripts
have been added as well.

https://fedorahosted.org/freeipa/ticket/3872
---
 API.txt                                            |  32 +-
 ipalib/plugins/vault.py                            | 148 +++-
 ipalib/plugins/vaultsecret.py                      | 294 ++++++--
 ipatests/test_xmlrpc/test_vault_plugin.py          | 827 +++++++++++++++++++++
 ipatests/test_xmlrpc/test_vaultcontainer_plugin.py |   1 +
 ipatests/test_xmlrpc/test_vaultsecret_plugin.py    | 149 ++++
 6 files changed, 1394 insertions(+), 57 deletions(-)

diff --git a/API.txt b/API.txt
index 5b7114803ddb48d2f9d846107cc99f7b3dd996d2..b745c623e3be68f9b7a15905022568b3dcb7e868 100644
--- a/API.txt
+++ b/API.txt
@@ -4515,7 +4515,7 @@ output: Output('result', <type 'bool'>, None)
 output: Output('summary', (<type 'unicode'>, <type 'NoneType'>), None)
 output: PrimaryKey('value', None, None)
 command: vault_add
-args: 1,16,3
+args: 1,22,3
 arg: Str('cn', attribute=True, cli_name='vault_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: Bytes('data?', cli_name='data')
@@ -4531,6 +4531,12 @@ option: Str('password?', cli_name='password')
 option: Str('password_file?', cli_name='password_file')
 option: Str('public_key_file?', cli_name='public_key_file')
 option: Flag('raw', autofill=True, cli_name='raw', default=False, exclude='webui')
+option: Str('source_password?', cli_name='source_password')
+option: Str('source_password_file?', cli_name='source_password_file')
+option: Bytes('source_private_key?', cli_name='source_private_key')
+option: Str('source_private_key_file?', cli_name='source_private_key_file')
+option: Str('source_secret_id?', cli_name='source_secret_id')
+option: Str('source_vault_id?', cli_name='source_vault_id')
 option: Str('text?', cli_name='text')
 option: Str('version?', exclude='webui')
 output: Entry('result', <type 'dict'>, Gettext('A dictionary representing an LDAP entry', domain='ipa', localedir=None))
@@ -4563,7 +4569,7 @@ output: Output('completed', <type 'int'>, None)
 output: Output('failed', <type 'dict'>, None)
 output: Entry('result', <type 'dict'>, Gettext('A dictionary representing an LDAP entry', domain='ipa', localedir=None))
 command: vault_archive
-args: 1,21,3
+args: 1,27,3
 arg: Str('cn', attribute=True, cli_name='vault_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('create?', autofill=True, default=False)
@@ -4583,6 +4589,12 @@ option: Str('password_file?', cli_name='password_file')
 option: Str('public_key_file?', cli_name='public_key_file')
 option: Flag('raw', autofill=True, cli_name='raw', default=False, exclude='webui')
 option: Str('session_key?', cli_name='session_key')
+option: Str('source_password?', cli_name='source_password')
+option: Str('source_password_file?', cli_name='source_password_file')
+option: Bytes('source_private_key?', cli_name='source_private_key')
+option: Str('source_private_key_file?', cli_name='source_private_key_file')
+option: Str('source_secret_id?', cli_name='source_secret_id')
+option: Str('source_vault_id?', cli_name='source_vault_id')
 option: Str('text?', cli_name='text')
 option: Str('vault_data?', cli_name='vault_data')
 option: Str('version?', exclude='webui')
@@ -4830,7 +4842,7 @@ output: Entry('result', <type 'dict'>, Gettext('A dictionary representing an LDA
 output: Output('summary', (<type 'unicode'>, <type 'NoneType'>), None)
 output: PrimaryKey('value', None, None)
 command: vaultsecret_add
-args: 2,12,3
+args: 2,18,3
 arg: Str('vaultcn', cli_name='vault', maxlength=255, multivalue=False, pattern='^[a-zA-Z0-9_.-/]+$', primary_key=True, query=True, required=True)
 arg: Str('secret_name', attribute=True, cli_name='secret', 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')
@@ -4843,6 +4855,12 @@ option: Str('password_file?', cli_name='password_file')
 option: Bytes('private_key?', cli_name='private_key')
 option: Str('private_key_file?', cli_name='private_key_file')
 option: Flag('raw', autofill=True, cli_name='raw', default=False, exclude='webui')
+option: Str('source_password?', cli_name='source_password')
+option: Str('source_password_file?', cli_name='source_password_file')
+option: Bytes('source_private_key?', cli_name='source_private_key')
+option: Str('source_private_key_file?', cli_name='source_private_key_file')
+option: Str('source_secret_id?', cli_name='source_secret_id')
+option: Str('source_vault_id?', cli_name='source_vault_id')
 option: Str('text?', cli_name='text')
 option: Str('version?', exclude='webui')
 output: Entry('result', <type 'dict'>, Gettext('A dictionary representing an LDAP entry', domain='ipa', localedir=None))
@@ -4884,7 +4902,7 @@ output: ListOfEntries('result', (<type 'list'>, <type 'tuple'>), Gettext('A list
 output: Output('summary', (<type 'unicode'>, <type 'NoneType'>), None)
 output: Output('truncated', <type 'bool'>, None)
 command: vaultsecret_mod
-args: 2,12,3
+args: 2,18,3
 arg: Str('vaultcn', cli_name='vault', maxlength=255, multivalue=False, pattern='^[a-zA-Z0-9_.-/]+$', primary_key=True, query=True, required=True)
 arg: Str('secret_name', attribute=True, cli_name='secret', 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')
@@ -4897,6 +4915,12 @@ option: Str('password_file?', cli_name='password_file')
 option: Bytes('private_key?', cli_name='private_key')
 option: Str('private_key_file?', cli_name='private_key_file')
 option: Flag('raw', autofill=True, cli_name='raw', default=False, exclude='webui')
+option: Str('source_password?', cli_name='source_password')
+option: Str('source_password_file?', cli_name='source_password_file')
+option: Bytes('source_private_key?', cli_name='source_private_key')
+option: Str('source_private_key_file?', cli_name='source_private_key_file')
+option: Str('source_secret_id?', cli_name='source_secret_id')
+option: Str('source_vault_id?', cli_name='source_vault_id')
 option: Str('text?', cli_name='text')
 option: Str('version?', exclude='webui')
 output: Entry('result', <type 'dict'>, Gettext('A dictionary representing an LDAP entry', domain='ipa', localedir=None))
diff --git a/ipalib/plugins/vault.py b/ipalib/plugins/vault.py
index 7978bf29a9be09b8db1726280ca7a5b913876cd7..9e569467245ff152dd9f497d09cbc56a937632fd 100644
--- a/ipalib/plugins/vault.py
+++ b/ipalib/plugins/vault.py
@@ -548,6 +548,36 @@ class vault_add(LDAPQuery):
             cli_name='escrow_public_key_file',
             doc=_('File containing the escrow public key'),
         ),
+        Str(
+            'source_vault_id?',
+            cli_name='source_vault_id',
+            doc=_('Source vault ID'),
+        ),
+        Str(
+            'source_secret_id?',
+            cli_name='source_secret_id',
+            doc=_('Source secret ID'),
+        ),
+        Str(
+            'source_password?',
+            cli_name='source_password',
+            doc=_('Source vault password'),
+        ),
+        Str(  # TODO: use File parameter
+            'source_password_file?',
+            cli_name='source_password_file',
+            doc=_('File containing the source vault password'),
+        ),
+        Bytes(
+            'source_private_key?',
+            cli_name='source_private_key',
+            doc=_('Source vault private key'),
+        ),
+        Str(  # TODO: use File parameter
+            'source_private_key_file?',
+            cli_name='source_private_key_file',
+            doc=_('File containing the source vault private key'),
+        ),
     )
 
     has_output = output.standard_entry
@@ -895,6 +925,36 @@ class vault_archive(LDAPQuery):
             cli_name='escrow_public_key_file',
             doc=_('File containing the escrow public key'),
         ),
+        Str(
+            'source_vault_id?',
+            cli_name='source_vault_id',
+            doc=_('Source vault ID'),
+        ),
+        Str(
+            'source_secret_id?',
+            cli_name='source_secret_id',
+            doc=_('Source secret ID'),
+        ),
+        Str(
+            'source_password?',
+            cli_name='source_password',
+            doc=_('Source vault password'),
+        ),
+        Str(  # TODO: use File parameter
+            'source_password_file?',
+            cli_name='source_password_file',
+            doc=_('File containing the source vault password'),
+        ),
+        Bytes(
+            'source_private_key?',
+            cli_name='source_private_key',
+            doc=_('Source vault private key'),
+        ),
+        Str(  # TODO: use File parameter
+            'source_private_key_file?',
+            cli_name='source_private_key_file',
+            doc=_('File containing the source vault private key'),
+        ),
     )
 
     has_output = output.standard_entry
@@ -917,6 +977,12 @@ class vault_archive(LDAPQuery):
         input_file = options.get('in')
         password = options.get('password')
         password_file = options.get('password_file')
+        source_vault_id = options.get('source_vault_id')
+        source_secret_id = options.get('source_secret_id')
+        source_password = options.get('source_password')
+        source_password_file = options.get('source_password_file')
+        source_private_key = options.get('source_private_key')
+        source_private_key_file = options.get('source_private_key_file')
 
         if create:
             vault_type = options.get('ipavaulttype', u'standard')
@@ -964,24 +1030,102 @@ class vault_archive(LDAPQuery):
             del options['public_key_file']
         if 'escrow_public_key_file' in options:
             del options['escrow_public_key_file']
+        if 'source_vault_id' in options:
+            del options['source_vault_id']
+        if 'source_secret_id' in options:
+            del options['source_secret_id']
+        if 'source_password' in options:
+            del options['source_password']
+        if 'source_password_file' in options:
+            del options['source_password_file']
+        if 'source_private_key' in options:
+            del options['source_private_key']
+        if 'source_private_key_file' in options:
+            del options['source_private_key_file']
 
         # get data
         if data:
-            if text or input_file:
+            if text or input_file or source_vault_id:
                 raise errors.MutuallyExclusiveError(
                     reason=_('Input data specified multiple times'))
 
         elif text:
-            if input_file:
+            if input_file or source_vault_id:
                 raise errors.MutuallyExclusiveError(
                     reason=_('Input data specified multiple times'))
 
             data = text.encode()
 
         elif input_file:
+            if source_vault_id:
+                raise errors.MutuallyExclusiveError(
+                    reason=_('Input data specified multiple times'))
+
             with open(input_file, 'rb') as f:
                 data = f.read()
 
+        elif source_vault_id:
+
+            source_vault = self.api.Command.vault_show(
+                source_vault_id)['result']
+
+            if 'ipavaulttype' in source_vault:
+                source_vault_type = source_vault['ipavaulttype'][0]
+
+            if source_vault_type == u'standard':
+
+                pass
+
+            elif source_vault_type == u'symmetric':
+
+                # get source vault password
+                if source_password:
+                    pass
+
+                elif source_password_file:
+                    with open(source_password_file) as f:
+                        source_password = unicode(f.read().rstrip('\n'))
+
+                else:
+                    source_password = unicode(
+                        getpass.getpass('Source password: '))
+
+            elif source_vault_type == u'asymmetric':
+
+                # get source vault private key
+                if source_private_key:
+                    pass
+
+                elif source_private_key_file:
+                    with open(source_private_key_file, 'rb') as f:
+                        source_private_key = f.read()
+
+                else:
+                    raise errors.ValidationError(
+                        name='source_private_key',
+                        error=_('Missing source vault private key'))
+
+            else:
+                raise errors.ValidationError(
+                    name='source_vault_type',
+                    error=_('Invalid source vault type'))
+
+            source_result = self.api.Command.vault_retrieve(
+                source_vault_id,
+                password=source_password,
+                private_key=source_private_key)['result']
+
+            if source_secret_id:
+                source_json_data = self.api.Object.vaultsecret\
+                    .parse_result(source_result)
+                source_secrets = source_json_data['secrets']
+                source_secret = self.obj.Object.vaultsecret\
+                    .find(source_secrets, source_secret_id)
+                data = base64.b64decode(source_secret['data'])
+
+            else:
+                data = source_result['data']
+
         else:
             data = ''
 
diff --git a/ipalib/plugins/vaultsecret.py b/ipalib/plugins/vaultsecret.py
index 688b64e03b63061cae13c385825f06b04d97896a..3d984d9c471847d50a11eaca64b732b00bdb460e 100644
--- a/ipalib/plugins/vaultsecret.py
+++ b/ipalib/plugins/vaultsecret.py
@@ -179,6 +179,36 @@ class vaultsecret_add(LDAPRetrieve):
             cli_name='private_key_file',
             doc=_('File containing the vault private key'),
         ),
+        Str(
+            'source_secret_id?',
+            cli_name='source_secret_id',
+            doc=_('Source secret ID'),
+        ),
+        Str(
+            'source_vault_id?',
+            cli_name='source_vault_id',
+            doc=_('Source vault ID'),
+        ),
+        Str(
+            'source_password?',
+            cli_name='source_password',
+            doc=_('Source vault password'),
+        ),
+        Str(  # TODO: use File parameter
+            'source_password_file?',
+            cli_name='source_password_file',
+            doc=_('File containing the source vault password'),
+        ),
+        Bytes(
+            'source_private_key?',
+            cli_name='source_private_key',
+            doc=_('Source vault private key'),
+        ),
+        Str(  # TODO: use File parameter
+            'source_private_key_file?',
+            cli_name='source_private_key_file',
+            doc=_('File containing the source vault private key'),
+        ),
     )
 
     msg_summary = _('Added vault secret "%(value)s"')
@@ -209,6 +239,12 @@ class vaultsecret_add(LDAPRetrieve):
         password_file = options.get('password_file')
         private_key = options.get('private_key')
         private_key_file = options.get('private_key_file')
+        source_secret_id = options.get('source_secret_id')
+        source_vault_id = options.get('source_vault_id')
+        source_password = options.get('source_password')
+        source_password_file = options.get('source_password_file')
+        source_private_key = options.get('source_private_key')
+        source_private_key_file = options.get('source_private_key_file')
 
         # don't send these parameters to server
         if 'data' in options:
@@ -225,6 +261,18 @@ class vaultsecret_add(LDAPRetrieve):
             del options['private_key']
         if 'private_key_file' in options:
             del options['private_key_file']
+        if 'source_secret_id' in options:
+            del options['source_secret_id']
+        if 'source_vault_id' in options:
+            del options['source_vault_id']
+        if 'source_password' in options:
+            del options['source_password']
+        if 'source_password_file' in options:
+            del options['source_password_file']
+        if 'source_private_key' in options:
+            del options['source_private_key']
+        if 'source_private_key_file' in options:
+            del options['source_private_key_file']
 
         # type-specific initialization
         if vault_type == u'standard':
@@ -276,31 +324,99 @@ class vaultsecret_add(LDAPRetrieve):
 
         # get data
         if data:
-            if text or input_file:
+            if text or input_file or source_secret_id:
                 raise errors.MutuallyExclusiveError(
                     reason=_('Input data specified multiple times'))
 
         elif text:
-            if input_file:
+            if input_file or source_secret_id:
                 raise errors.MutuallyExclusiveError(
                     reason=_('Input data specified multiple times'))
 
             data = text.encode('utf-8')
 
         elif input_file:
+            if source_secret_id:
+                raise errors.MutuallyExclusiveError(
+                    reason=_('Input data specified multiple times'))
+
             with open(input_file, 'rb') as f:
                 data = f.read()
 
+        elif source_secret_id:
+
+            if source_vault_id:
+
+                source_vault = self.api.Command.vault_show(
+                    source_vault_id)['result']
+
+                if 'ipavaulttype' in source_vault:
+                    source_vault_type = source_vault['ipavaulttype'][0]
+
+                if source_vault_type == u'standard':
+
+                    pass
+
+                elif source_vault_type == u'symmetric':
+
+                    # get source vault password
+                    if source_password:
+                        pass
+
+                    elif source_password_file:
+                        with open(source_password_file) as f:
+                            source_password = unicode(f.read().rstrip('\n'))
+
+                    else:
+                        source_password = unicode(
+                            getpass.getpass('Source password: '))
+
+                elif source_vault_type == u'asymmetric':
+
+                    # get source vault private key
+                    if source_private_key:
+                        pass
+
+                    elif source_private_key_file:
+                        with open(source_private_key_file, 'rb') as f:
+                            source_private_key = f.read()
+
+                    else:
+                        raise errors.ValidationError(
+                            name='source_private_key',
+                            error=_('Missing source vault private key'))
+
+                else:
+                    raise errors.ValidationError(
+                        name='source_vault_type',
+                        error=_('Invalid source vault type'))
+
+                source_result = self.api.Command.vault_retrieve(
+                    source_vault_id,
+                    password=source_password,
+                    private_key=source_private_key)['result']
+
+                source_json_data = self.obj.parse_result(source_result)
+
+                source_secrets = source_json_data['secrets']
+
+            else:
+                source_secrets = secrets
+
+            source_secret = self.obj.find(source_secrets, source_secret_id)
+            data = base64.b64decode(source_secret['data'])
+
         else:
             data = ''
 
         # add new secret
-        for secret in secrets:
-            if secret['secret_name'] == secret_name:
-                raise errors.DuplicateEntry(
-                    message=_(
-                        'vault secret with name "%s" already exists'
-                        % secret_name))
+        try:
+            self.obj.find(secrets, secret_name)
+            raise errors.DuplicateEntry(
+                message=_('vault secret with name "%s" already exists'
+                          % secret_name))
+        except errors.NotFound:
+            pass
 
         # store encoded data for storage
         secret = {
@@ -447,15 +563,7 @@ class vaultsecret_del(LDAPRetrieve):
         secrets = json_data['secrets']
 
         # find the secret
-        secret = None
-        for s in secrets:
-            if s['secret_name'] == secret_name:
-                secret = s
-                break
-
-        if not secret:
-            raise errors.NotFound(
-                reason=_('%s: vault secret not found' % secret_name))
+        secret = self.obj.find(secrets, secret_name)
 
         # delete secret
         secrets.remove(secret)
@@ -653,6 +761,36 @@ class vaultsecret_mod(LDAPRetrieve):
             cli_name='private_key_file',
             doc=_('File containing the vault private key'),
         ),
+        Str(
+            'source_secret_id?',
+            cli_name='source_secret_id',
+            doc=_('Source secret ID'),
+        ),
+        Str(
+            'source_vault_id?',
+            cli_name='source_vault_id',
+            doc=_('Source vault ID'),
+        ),
+        Str(
+            'source_password?',
+            cli_name='source_password',
+            doc=_('Source vault password'),
+        ),
+        Str(  # TODO: use File parameter
+            'source_password_file?',
+            cli_name='source_password_file',
+            doc=_('File containing the source vault password'),
+        ),
+        Bytes(
+            'source_private_key?',
+            cli_name='source_private_key',
+            doc=_('Source vault private key'),
+        ),
+        Str(  # TODO: use File parameter
+            'source_private_key_file?',
+            cli_name='source_private_key_file',
+            doc=_('File containing the source vault private key'),
+        ),
     )
 
     msg_summary = _('Modified vault secret "%(value)s"')
@@ -683,6 +821,12 @@ class vaultsecret_mod(LDAPRetrieve):
         password_file = options.get('password_file')
         private_key = options.get('private_key')
         private_key_file = options.get('private_key_file')
+        source_secret_id = options.get('source_secret_id')
+        source_vault_id = options.get('source_vault_id')
+        source_password = options.get('source_password')
+        source_password_file = options.get('source_password_file')
+        source_private_key = options.get('source_private_key')
+        source_private_key_file = options.get('source_private_key_file')
 
         # don't send these parameters to server
         if 'data' in options:
@@ -699,6 +843,18 @@ class vaultsecret_mod(LDAPRetrieve):
             del options['private_key']
         if 'private_key_file' in options:
             del options['private_key_file']
+        if 'source_secret_id' in options:
+            del options['source_secret_id']
+        if 'source_vault_id' in options:
+            del options['source_vault_id']
+        if 'source_password' in options:
+            del options['source_password']
+        if 'source_password_file' in options:
+            del options['source_password_file']
+        if 'source_private_key' in options:
+            del options['source_private_key']
+        if 'source_private_key_file' in options:
+            del options['source_private_key_file']
 
         # type-specific initialization
         if vault_type == u'standard':
@@ -753,45 +909,91 @@ class vaultsecret_mod(LDAPRetrieve):
 
         # get data
         if data:
-            if text or input_file:
+            if text or input_file or source_secret_id:
                 raise errors.MutuallyExclusiveError(
                     reason=_('Input data specified multiple times'))
 
         elif text:
-            if input_file:
+            if input_file or source_secret_id:
                 raise errors.MutuallyExclusiveError(
                     reason=_('Input data specified multiple times'))
 
             data = text.encode()
 
         elif input_file:
+            if source_secret_id:
+                raise errors.MutuallyExclusiveError(
+                    reason=_('Input data specified multiple times'))
+
             with open(input_file, 'rb') as f:
                 data = f.read()
 
+        elif source_secret_id:
+
+            if source_vault_id:
+
+                source_vault = self.api.Command.vault_show(
+                    source_vault_id)['result']
+
+                if 'ipavaulttype' in source_vault:
+                    source_vault_type = source_vault['ipavaulttype'][0]
+
+                if source_vault_type == u'standard':
+
+                    pass
+
+                elif source_vault_type == u'symmetric':
+
+                    # get source vault password
+                    if source_password:
+                        pass
+
+                    elif source_password_file:
+                        with open(source_password_file) as f:
+                            source_password = unicode(f.read().rstrip('\n'))
+
+                    else:
+                        source_password = unicode(
+                            getpass.getpass('Source password: '))
+
+                elif source_vault_type == u'asymmetric':
+
+                    # get source vault private key
+                    if source_private_key:
+                        pass
+
+                    elif source_private_key_file:
+                        with open(source_private_key_file, 'rb') as f:
+                            source_private_key = f.read()
+
+                    else:
+                        raise errors.ValidationError(
+                            name='source_private_key',
+                            error=_('Missing source vault private key'))
+
+                else:
+                    raise errors.ValidationError(
+                        name='source_vault_type',
+                        error=_('Invalid source vault type'))
+
+                source_result = self.api.Command.vault_retrieve(
+                    source_vault_id,
+                    password=source_password,
+                    private_key=source_private_key)['result']
+
+                source_json_data = self.obj.parse_result(source_result)
+
+                source_secrets = source_json_data['secrets']
+
+            else:
+                source_secrets = secrets
+
+            source_secret = self.obj.find(source_secrets, source_secret_id)
+            data = base64.b64decode(source_secret['data'])
+
         else:
             pass
 
-        # retrieve secrets
-        result = self.api.Command.vault_retrieve(
-            vault_id,
-            password=password,
-            private_key=private_key)['result']
-
-        json_data = self.obj.parse_result(result)
-
-        secrets = json_data['secrets']
-
-        # find the secret
-        secret = None
-        for s in secrets:
-            if s['secret_name'] == secret_name:
-                secret = s
-                break
-
-        if not secret:
-            raise errors.NotFound(
-                reason=_('%s: vault secret not found' % secret_name))
-
         # modify the secret
         if description:
             secret['description'] = description
@@ -961,17 +1163,7 @@ class vaultsecret_show(LDAPRetrieve):
 
         secrets = json_data['secrets']
 
-        secret = None
-
-        # find the secret
-        for s in secrets:
-            if s['secret_name'] == secret_name:
-                secret = s
-                break
-
-        if not secret:
-            raise errors.NotFound(
-                reason=_('%s: vault secret not found' % secret_name))
+        secret = self.obj.find(secrets, secret_name)
 
         # decode data for response
         secret['data'] = base64.b64decode(secret['data'])
diff --git a/ipatests/test_xmlrpc/test_vault_plugin.py b/ipatests/test_xmlrpc/test_vault_plugin.py
index 3d0dd4473d3bdeb5087a04ed4351ea2a16f8e3a0..48f2f99db54e6854c062cd0af487a6b837b9a450 100644
--- a/ipatests/test_xmlrpc/test_vault_plugin.py
+++ b/ipatests/test_xmlrpc/test_vault_plugin.py
@@ -26,14 +26,31 @@ from xmlrpc_test import Declarative, fuzzy_string
 
 test_vault = u'test_vault'
 shared_test_vault = u'/shared/%s' % test_vault
+
+standard_secrets_vault = u'standard_secrets_vault'
+symmetric_secrets_vault = u'symmetric_secrets_vault'
+asymmetric_secrets_vault = u'asymmetric_secrets_vault'
+
+standard_vault = u'standard_vault'
+standard_vault_copy = u'standard_vault_copy'
+standard_vault_copy2 = u'standard_vault_copy2'
+
 symmetric_vault = u'symmetric_vault'
+symmetric_vault_copy = u'symmetric_vault_copy'
+symmetric_vault_copy2 = u'symmetric_vault_copy2'
+
 asymmetric_vault = u'asymmetric_vault'
+asymmetric_vault_copy = u'asymmetric_vault_copy'
+asymmetric_vault_copy2 = u'asymmetric_vault_copy2'
+
 escrowed_symmetric_vault = u'escrowed_symmetric_vault'
 escrowed_asymmetric_vault = u'escrowed_asymmetric_vault'
 
 binary_data = '\x01\x02\x03\x04'
 text_data = u'secret'
 
+test_secret = u'test_secret'
+
 password = u'password'
 other_password = u'other_password'
 
@@ -127,8 +144,18 @@ class test_vault_plugin(Declarative):
     cleanup_commands = [
         ('vault_del', [test_vault], {'continue': True}),
         ('vault_del', [shared_test_vault], {'continue': True}),
+        ('vault_del', [standard_vault], {'continue': True}),
+        ('vault_del', [standard_secrets_vault], {'continue': True}),
+        ('vault_del', [standard_vault_copy], {'continue': True}),
+        ('vault_del', [standard_vault_copy2], {'continue': True}),
         ('vault_del', [symmetric_vault], {'continue': True}),
+        ('vault_del', [symmetric_secrets_vault], {'continue': True}),
+        ('vault_del', [symmetric_vault_copy], {'continue': True}),
+        ('vault_del', [symmetric_vault_copy2], {'continue': True}),
         ('vault_del', [asymmetric_vault], {'continue': True}),
+        ('vault_del', [asymmetric_secrets_vault], {'continue': True}),
+        ('vault_del', [asymmetric_vault_copy], {'continue': True}),
+        ('vault_del', [asymmetric_vault_copy2], {'continue': True}),
         ('vault_del', [escrowed_symmetric_vault], {'continue': True}),
         ('vault_del', [escrowed_asymmetric_vault], {'continue': True}),
     ]
@@ -513,6 +540,126 @@ class test_vault_plugin(Declarative):
         },
 
         {
+            'desc': 'Create standard vault',
+            'command': (
+                'vault_add',
+                [standard_vault],
+                {
+                    'data': binary_data,
+                },
+            ),
+            'expected': {
+                'value': standard_vault,
+                'summary': 'Added vault "%s"' % standard_vault,
+                'result': {
+                    'dn': u'cn=%s,cn=admin,cn=users,cn=vaults,%s'
+                          % (standard_vault, api.env.basedn),
+                    'cn': [standard_vault],
+                    'vault_id': u'/users/admin/%s' % standard_vault,
+                    'owner_user': [u'admin'],
+                    'ipavaulttype': [u'standard'],
+                },
+            },
+        },
+
+        {
+            'desc': 'Create a copy of standard vault',
+            'command': (
+                'vault_add',
+                [standard_vault_copy],
+                {
+                    'source_vault_id': standard_vault,
+                },
+            ),
+            'expected': {
+                'value': standard_vault_copy,
+                'summary': 'Added vault "%s"' % standard_vault_copy,
+                'result': {
+                    'dn': u'cn=%s,cn=admin,cn=users,cn=vaults,%s'
+                          % (standard_vault_copy, api.env.basedn),
+                    'cn': [standard_vault_copy],
+                    'vault_id': u'/users/admin/%s' % standard_vault_copy,
+                    'owner_user': [u'admin'],
+                    'ipavaulttype': [u'standard'],
+                },
+            },
+        },
+
+        {
+            'desc': 'Verify the copy creation of standard vault',
+            'command': (
+                'vault_retrieve',
+                [standard_vault_copy],
+                {},
+            ),
+            'expected': {
+                'value': standard_vault_copy,
+                'summary': u'Retrieved data from vault "%s"'
+                           % standard_vault_copy,
+                'result': {
+                    'dn': u'cn=%s,cn=admin,cn=users,cn=vaults,%s'
+                          % (standard_vault_copy, api.env.basedn),
+                    'cn': [standard_vault_copy],
+                    'vault_id': u'/users/admin/%s' % standard_vault_copy,
+                    'owner_user': [u'admin'],
+                    'ipavaulttype': [u'standard'],
+                    'nonce': fuzzy_string,
+                    'vault_data': fuzzy_string,
+                    'data': binary_data,
+                },
+            },
+        },
+
+        {
+            'desc': 'Archive a copy of standard vault',
+            'command': (
+                'vault_archive',
+                [standard_vault_copy],
+                {
+                    'source_vault_id': standard_vault,
+                },
+            ),
+            'expected': {
+                'value': standard_vault_copy,
+                'summary': u'Archived data into vault "%s"'
+                           % standard_vault_copy,
+                'result': {
+                    'dn': u'cn=%s,cn=admin,cn=users,cn=vaults,%s'
+                          % (standard_vault_copy, api.env.basedn),
+                    'cn': [standard_vault_copy],
+                    'vault_id': u'/users/admin/%s' % standard_vault_copy,
+                    'owner_user': [u'admin'],
+                    'ipavaulttype': [u'standard'],
+                },
+            },
+        },
+
+        {
+            'desc': 'Verify the copy archival of standard vault',
+            'command': (
+                'vault_retrieve',
+                [standard_vault_copy],
+                {},
+            ),
+            'expected': {
+                'value': standard_vault_copy,
+                'summary': u'Retrieved data from vault "%s"'
+                           % standard_vault_copy,
+                'result': {
+                    'dn': u'cn=%s,cn=admin,cn=users,cn=vaults,%s'
+                          % (standard_vault_copy, api.env.basedn),
+                    'cn': [standard_vault_copy],
+                    'vault_id': u'/users/admin/%s' % standard_vault_copy,
+                    'owner_user': [u'admin'],
+                    'ipavaulttype': [u'standard'],
+                    'nonce': fuzzy_string,
+                    'vault_data': fuzzy_string,
+                    'data': binary_data,
+                },
+            },
+        },
+
+        {
             'desc': 'Create symmetric vault',
             'command': (
                 'vault_add',
@@ -579,6 +726,116 @@ class test_vault_plugin(Declarative):
         },
 
         {
+            'desc': 'Create a copy of symmetric vault',
+            'command': (
+                'vault_add',
+                [symmetric_vault_copy],
+                {
+                    'ipavaulttype': u'symmetric',
+                    'password': other_password,
+                    'source_vault_id': symmetric_vault,
+                    'source_password': password,
+                },
+            ),
+            'expected': {
+                'value': symmetric_vault_copy,
+                'summary': 'Added vault "%s"' % symmetric_vault_copy,
+                'result': {
+                    'dn': u'cn=%s,cn=admin,cn=users,cn=vaults,%s'
+                          % (symmetric_vault_copy, api.env.basedn),
+                    'cn': [symmetric_vault_copy],
+                    'vault_id': u'/users/admin/%s' % symmetric_vault_copy,
+                    'owner_user': [u'admin'],
+                    'ipavaulttype': [u'symmetric'],
+                    'ipavaultsalt': [fuzzy_string],
+                },
+            },
+        },
+
+        {
+            'desc': 'Verify the copy creation of symmetric vault',
+            'command': (
+                'vault_retrieve',
+                [symmetric_vault_copy],
+                {
+                    'password': other_password,
+                },
+            ),
+            'expected': {
+                'value': symmetric_vault_copy,
+                'summary': u'Retrieved data from vault "%s"'
+                           % symmetric_vault_copy,
+                'result': {
+                    'dn': u'cn=%s,cn=admin,cn=users,cn=vaults,%s'
+                          % (symmetric_vault_copy, api.env.basedn),
+                    'cn': [symmetric_vault_copy],
+                    'vault_id': u'/users/admin/%s' % symmetric_vault_copy,
+                    'owner_user': [u'admin'],
+                    'ipavaulttype': [u'symmetric'],
+                    'ipavaultsalt': [fuzzy_string],
+                    'nonce': fuzzy_string,
+                    'vault_data': fuzzy_string,
+                    'data': binary_data,
+                },
+            },
+        },
+
+        {
+            'desc': 'Archive a copy of symmetric vault',
+            'command': (
+                'vault_archive',
+                [symmetric_vault_copy],
+                {
+                    'password': other_password,
+                    'source_vault_id': symmetric_vault,
+                    'source_password': password,
+                },
+            ),
+            'expected': {
+                'value': symmetric_vault_copy,
+                'summary': u'Archived data into vault "%s"'
+                           % symmetric_vault_copy,
+                'result': {
+                    'dn': u'cn=%s,cn=admin,cn=users,cn=vaults,%s'
+                          % (symmetric_vault_copy, api.env.basedn),
+                    'cn': [symmetric_vault_copy],
+                    'vault_id': u'/users/admin/%s' % symmetric_vault_copy,
+                    'owner_user': [u'admin'],
+                    'ipavaulttype': [u'symmetric'],
+                    'ipavaultsalt': [fuzzy_string],
+                },
+            },
+        },
+
+        {
+            'desc': 'Verify the copy archival of symmetric vault',
+            'command': (
+                'vault_retrieve',
+                [symmetric_vault_copy],
+                {
+                    'password': other_password,
+                },
+            ),
+            'expected': {
+                'value': symmetric_vault_copy,
+                'summary': u'Retrieved data from vault "%s"'
+                           % symmetric_vault_copy,
+                'result': {
+                    'dn': u'cn=%s,cn=admin,cn=users,cn=vaults,%s'
+                          % (symmetric_vault_copy, api.env.basedn),
+                    'cn': [symmetric_vault_copy],
+                    'vault_id': u'/users/admin/%s' % symmetric_vault_copy,
+                    'owner_user': [u'admin'],
+                    'ipavaulttype': [u'symmetric'],
+                    'ipavaultsalt': [fuzzy_string],
+                    'nonce': fuzzy_string,
+                    'vault_data': fuzzy_string,
+                    'data': binary_data,
+                },
+            },
+        },
+
+        {
             'desc': 'Create asymmetric vault',
             'command': (
                 'vault_add',
@@ -648,6 +905,119 @@ class test_vault_plugin(Declarative):
         },
 
         {
+            'desc': 'Create a copy of asymmetric vault',
+            'command': (
+                'vault_add',
+                [asymmetric_vault_copy],
+                {
+                    'ipavaulttype': u'asymmetric',
+                    'ipapublickey': other_public_key,
+                    'source_vault_id': asymmetric_vault,
+                    'source_private_key': private_key,
+                },
+            ),
+            'expected': {
+                'value': asymmetric_vault_copy,
+                'summary': 'Added vault "%s"' % asymmetric_vault_copy,
+                'result': {
+                    'dn': u'cn=%s,cn=admin,cn=users,cn=vaults,%s'
+                          % (asymmetric_vault_copy, api.env.basedn),
+                    'cn': [asymmetric_vault_copy],
+                    'vault_id': u'/users/admin/%s' % asymmetric_vault_copy,
+                    'owner_user': [u'admin'],
+                    'ipavaulttype': [u'asymmetric'],
+                    'ipavaultsalt': [fuzzy_string],
+                    'ipapublickey': [other_public_key],
+                },
+            },
+        },
+
+        {
+            'desc': 'Verify the copy creation of asymmetric vault',
+            'command': (
+                'vault_retrieve',
+                [asymmetric_vault_copy],
+                {
+                    'private_key': other_private_key,
+                },
+            ),
+            'expected': {
+                'value': asymmetric_vault_copy,
+                'summary': u'Retrieved data from vault "%s"'
+                           % asymmetric_vault_copy,
+                'result': {
+                    'dn': u'cn=%s,cn=admin,cn=users,cn=vaults,%s'
+                          % (asymmetric_vault_copy, api.env.basedn),
+                    'cn': [asymmetric_vault_copy],
+                    'vault_id': u'/users/admin/%s' % asymmetric_vault_copy,
+                    'owner_user': [u'admin'],
+                    'ipavaulttype': [u'asymmetric'],
+                    'ipavaultsalt': [fuzzy_string],
+                    'ipapublickey': [other_public_key],
+                    'nonce': fuzzy_string,
+                    'vault_data': fuzzy_string,
+                    'data': binary_data,
+                },
+            },
+        },
+
+        {
+            'desc': 'Archive a copy of asymmetric vault',
+            'command': (
+                'vault_archive',
+                [asymmetric_vault_copy],
+                {
+                    'source_vault_id': asymmetric_vault,
+                    'source_private_key': private_key,
+                },
+            ),
+            'expected': {
+                'value': asymmetric_vault_copy,
+                'summary': u'Archived data into vault "%s"'
+                           % asymmetric_vault_copy,
+                'result': {
+                    'dn': u'cn=%s,cn=admin,cn=users,cn=vaults,%s'
+                          % (asymmetric_vault_copy, api.env.basedn),
+                    'cn': [asymmetric_vault_copy],
+                    'vault_id': u'/users/admin/%s' % asymmetric_vault_copy,
+                    'owner_user': [u'admin'],
+                    'ipavaulttype': [u'asymmetric'],
+                    'ipavaultsalt': [fuzzy_string],
+                    'ipapublickey': [other_public_key],
+                },
+            },
+        },
+
+        {
+            'desc': 'Verify the copy archival of asymmetric vault',
+            'command': (
+                'vault_retrieve',
+                [asymmetric_vault_copy],
+                {
+                    'private_key': other_private_key,
+                },
+            ),
+            'expected': {
+                'value': asymmetric_vault_copy,
+                'summary': u'Retrieved data from vault "%s"'
+                           % asymmetric_vault_copy,
+                'result': {
+                    'dn': u'cn=%s,cn=admin,cn=users,cn=vaults,%s'
+                          % (asymmetric_vault_copy, api.env.basedn),
+                    'cn': [asymmetric_vault_copy],
+                    'vault_id': u'/users/admin/%s' % asymmetric_vault_copy,
+                    'owner_user': [u'admin'],
+                    'ipavaulttype': [u'asymmetric'],
+                    'ipavaultsalt': [fuzzy_string],
+                    'ipapublickey': [other_public_key],
+                    'nonce': fuzzy_string,
+                    'vault_data': fuzzy_string,
+                    'data': binary_data,
+                },
+            },
+        },
+
+        {
             'desc': 'Create escrowed symmetric vault',
             'command': (
                 'vault_add',
@@ -763,4 +1133,461 @@ class test_vault_plugin(Declarative):
             },
         },
 
+        {
+            'desc': 'Create standard secrets vault',
+            'command': (
+                'vault_add',
+                [standard_secrets_vault],
+                {},
+            ),
+            'expected': {
+                'value': standard_secrets_vault,
+                'summary': 'Added vault "%s"' % standard_secrets_vault,
+                'result': {
+                    'dn': u'cn=%s,cn=admin,cn=users,cn=vaults,%s'
+                          % (standard_secrets_vault, api.env.basedn),
+                    'cn': [standard_secrets_vault],
+                    'vault_id': u'/users/admin/%s' % standard_secrets_vault,
+                    'owner_user': [u'admin'],
+                    'ipavaulttype': [u'standard'],
+                },
+            },
+        },
+
+        {
+            'desc': 'Create secret in standard vault',
+            'command': (
+                'vaultsecret_add',
+                [standard_secrets_vault, test_secret],
+                {
+                    'data': binary_data,
+                },
+            ),
+            'expected': {
+                'value': test_secret,
+                'summary': 'Added vault secret "%s"' % test_secret,
+                'result': {
+                    'secret_name': test_secret,
+                    'data': binary_data,
+                },
+            },
+        },
+
+        {
+            'desc': 'Create symmetric secrets vault',
+            'command': (
+                'vault_add',
+                [symmetric_secrets_vault],
+                {
+                    'ipavaulttype': u'symmetric',
+                    'password': password,
+                },
+            ),
+            'expected': {
+                'value': symmetric_secrets_vault,
+                'summary': 'Added vault "%s"' % symmetric_secrets_vault,
+                'result': {
+                    'dn': u'cn=%s,cn=admin,cn=users,cn=vaults,%s'
+                          % (symmetric_secrets_vault, api.env.basedn),
+                    'cn': [symmetric_secrets_vault],
+                    'vault_id': u'/users/admin/%s' % symmetric_secrets_vault,
+                    'owner_user': [u'admin'],
+                    'ipavaulttype': [u'symmetric'],
+                    'ipavaultsalt': [fuzzy_string],
+                },
+            },
+        },
+
+        {
+            'desc': 'Create secret in symmetric vault',
+            'command': (
+                'vaultsecret_add',
+                [symmetric_secrets_vault, test_secret],
+                {
+                    'data': binary_data,
+                    'password': password,
+                },
+            ),
+            'expected': {
+                'value': test_secret,
+                'summary': 'Added vault secret "%s"' % test_secret,
+                'result': {
+                    'secret_name': test_secret,
+                    'data': binary_data,
+                },
+            },
+        },
+
+        {
+            'desc': 'Create asymmetric secrets vault',
+            'command': (
+                'vault_add',
+                [asymmetric_secrets_vault],
+                {
+                    'ipavaulttype': u'asymmetric',
+                    'ipapublickey': public_key,
+                },
+            ),
+            'expected': {
+                'value': asymmetric_secrets_vault,
+                'summary': 'Added vault "%s"' % asymmetric_secrets_vault,
+                'result': {
+                    'dn': u'cn=%s,cn=admin,cn=users,cn=vaults,%s'
+                          % (asymmetric_secrets_vault, api.env.basedn),
+                    'cn': [asymmetric_secrets_vault],
+                    'vault_id': u'/users/admin/%s' % asymmetric_secrets_vault,
+                    'owner_user': [u'admin'],
+                    'ipavaulttype': [u'asymmetric'],
+                    'ipavaultsalt': [fuzzy_string],
+                    'ipapublickey': [public_key],
+                },
+            },
+        },
+
+        {
+            'desc': 'Create secret in asymmetric vault',
+            'command': (
+                'vaultsecret_add',
+                [asymmetric_secrets_vault, test_secret],
+                {
+                    'private_key': private_key,
+                    'data': binary_data,
+                },
+            ),
+            'expected': {
+                'value': test_secret,
+                'summary': 'Added vault secret "%s"' % test_secret,
+                'result': {
+                    'secret_name': test_secret,
+                    'data': binary_data,
+                },
+            },
+        },
+
+        {
+            'desc': 'Create a copy of secret from standard vault',
+            'command': (
+                'vault_add',
+                [standard_vault_copy2],
+                {
+                    'source_vault_id': standard_secrets_vault,
+                    'source_secret_id': test_secret,
+                },
+            ),
+            'expected': {
+                'value': standard_vault_copy2,
+                'summary': 'Added vault "%s"' % standard_vault_copy2,
+                'result': {
+                    'dn': u'cn=%s,cn=admin,cn=users,cn=vaults,%s'
+                          % (standard_vault_copy2, api.env.basedn),
+                    'cn': [standard_vault_copy2],
+                    'vault_id': u'/users/admin/%s' % standard_vault_copy2,
+                    'owner_user': [u'admin'],
+                    'ipavaulttype': [u'standard'],
+                },
+            },
+        },
+
+        {
+            'desc': 'Verify the copy creation of secret from standard vault',
+            'command': (
+                'vault_retrieve',
+                [standard_vault_copy2],
+                {},
+            ),
+            'expected': {
+                'value': standard_vault_copy2,
+                'summary': u'Retrieved data from vault "%s"'
+                           % standard_vault_copy2,
+                'result': {
+                    'dn': u'cn=%s,cn=admin,cn=users,cn=vaults,%s'
+                          % (standard_vault_copy2, api.env.basedn),
+                    'cn': [standard_vault_copy2],
+                    'vault_id': u'/users/admin/%s' % standard_vault_copy2,
+                    'owner_user': [u'admin'],
+                    'ipavaulttype': [u'standard'],
+                    'nonce': fuzzy_string,
+                    'vault_data': fuzzy_string,
+                    'data': binary_data,
+                },
+            },
+        },
+
+        {
+            'desc': 'Archive a copy of secret from standard vault',
+            'command': (
+                'vault_archive',
+                [standard_vault_copy2],
+                {
+                    'source_vault_id': standard_secrets_vault,
+                    'source_secret_id': test_secret,
+                },
+            ),
+            'expected': {
+                'value': standard_vault_copy2,
+                'summary': u'Archived data into vault "%s"'
+                           % standard_vault_copy2,
+                'result': {
+                    'dn': u'cn=%s,cn=admin,cn=users,cn=vaults,%s'
+                          % (standard_vault_copy2, api.env.basedn),
+                    'cn': [standard_vault_copy2],
+                    'vault_id': u'/users/admin/%s' % standard_vault_copy2,
+                    'owner_user': [u'admin'],
+                    'ipavaulttype': [u'standard'],
+                },
+            },
+        },
+
+        {
+            'desc': 'Verify the copy archival of secret from standard vault',
+            'command': (
+                'vault_retrieve',
+                [standard_vault_copy2],
+                {},
+            ),
+            'expected': {
+                'value': standard_vault_copy2,
+                'summary': u'Retrieved data from vault "%s"'
+                           % standard_vault_copy2,
+                'result': {
+                    'dn': u'cn=%s,cn=admin,cn=users,cn=vaults,%s'
+                          % (standard_vault_copy2, api.env.basedn),
+                    'cn': [standard_vault_copy2],
+                    'vault_id': u'/users/admin/%s' % standard_vault_copy2,
+                    'owner_user': [u'admin'],
+                    'ipavaulttype': [u'standard'],
+                    'nonce': fuzzy_string,
+                    'vault_data': fuzzy_string,
+                    'data': binary_data,
+                },
+            },
+        },
+
+        {
+            'desc': 'Create a copy of secret from symmetric vault',
+            'command': (
+                'vault_add',
+                [symmetric_vault_copy2],
+                {
+                    'ipavaulttype': u'symmetric',
+                    'password': password,
+                    'source_vault_id': symmetric_secrets_vault,
+                    'source_secret_id': test_secret,
+                    'source_password': password,
+                },
+            ),
+            'expected': {
+                'value': symmetric_vault_copy2,
+                'summary': 'Added vault "%s"' % symmetric_vault_copy2,
+                'result': {
+                    'dn': u'cn=%s,cn=admin,cn=users,cn=vaults,%s'
+                          % (symmetric_vault_copy2, api.env.basedn),
+                    'cn': [symmetric_vault_copy2],
+                    'vault_id': u'/users/admin/%s' % symmetric_vault_copy2,
+                    'owner_user': [u'admin'],
+                    'ipavaulttype': [u'symmetric'],
+                    'ipavaultsalt': [fuzzy_string],
+                },
+            },
+        },
+
+        {
+            'desc': 'Verify the copy creation of secret from symmetric vault',
+            'command': (
+                'vault_retrieve',
+                [symmetric_vault_copy2],
+                {
+                    'password': password,
+                },
+            ),
+            'expected': {
+                'value': symmetric_vault_copy2,
+                'summary': u'Retrieved data from vault "%s"'
+                           % symmetric_vault_copy2,
+                'result': {
+                    'dn': u'cn=%s,cn=admin,cn=users,cn=vaults,%s'
+                          % (symmetric_vault_copy2, api.env.basedn),
+                    'cn': [symmetric_vault_copy2],
+                    'vault_id': u'/users/admin/%s' % symmetric_vault_copy2,
+                    'owner_user': [u'admin'],
+                    'ipavaulttype': [u'symmetric'],
+                    'ipavaultsalt': [fuzzy_string],
+                    'nonce': fuzzy_string,
+                    'vault_data': fuzzy_string,
+                    'data': binary_data,
+                },
+            },
+        },
+
+        {
+            'desc': 'Archive a copy of secret from symmetric vault',
+            'command': (
+                'vault_archive',
+                [symmetric_vault_copy2],
+                {
+                    'password': password,
+                    'source_vault_id': symmetric_secrets_vault,
+                    'source_secret_id': test_secret,
+                    'source_password': password,
+                },
+            ),
+            'expected': {
+                'value': symmetric_vault_copy2,
+                'summary': u'Archived data into vault "%s"'
+                           % symmetric_vault_copy2,
+                'result': {
+                    'dn': u'cn=%s,cn=admin,cn=users,cn=vaults,%s'
+                          % (symmetric_vault_copy2, api.env.basedn),
+                    'cn': [symmetric_vault_copy2],
+                    'vault_id': u'/users/admin/%s' % symmetric_vault_copy2,
+                    'owner_user': [u'admin'],
+                    'ipavaulttype': [u'symmetric'],
+                    'ipavaultsalt': [fuzzy_string],
+                },
+            },
+        },
+
+        {
+            'desc': 'Verify the copy archival of secret from symmetric vault',
+            'command': (
+                'vault_retrieve',
+                [symmetric_vault_copy2],
+                {
+                    'password': password,
+                },
+            ),
+            'expected': {
+                'value': symmetric_vault_copy2,
+                'summary': u'Retrieved data from vault "%s"'
+                           % symmetric_vault_copy2,
+                'result': {
+                    'dn': u'cn=%s,cn=admin,cn=users,cn=vaults,%s'
+                          % (symmetric_vault_copy2, api.env.basedn),
+                    'cn': [symmetric_vault_copy2],
+                    'vault_id': u'/users/admin/%s' % symmetric_vault_copy2,
+                    'owner_user': [u'admin'],
+                    'ipavaulttype': [u'symmetric'],
+                    'ipavaultsalt': [fuzzy_string],
+                    'nonce': fuzzy_string,
+                    'vault_data': fuzzy_string,
+                    'data': binary_data,
+                },
+            },
+        },
+
+        {
+            'desc': 'Create a copy of secret from asymmetric vault',
+            'command': (
+                'vault_add',
+                [asymmetric_vault_copy2],
+                {
+                    'ipavaulttype': u'asymmetric',
+                    'ipapublickey': public_key,
+                    'source_vault_id': asymmetric_secrets_vault,
+                    'source_secret_id': test_secret,
+                    'source_private_key': private_key,
+                },
+            ),
+            'expected': {
+                'value': asymmetric_vault_copy2,
+                'summary': 'Added vault "%s"' % asymmetric_vault_copy2,
+                'result': {
+                    'dn': u'cn=%s,cn=admin,cn=users,cn=vaults,%s'
+                          % (asymmetric_vault_copy2, api.env.basedn),
+                    'cn': [asymmetric_vault_copy2],
+                    'vault_id': u'/users/admin/%s' % asymmetric_vault_copy2,
+                    'owner_user': [u'admin'],
+                    'ipavaulttype': [u'asymmetric'],
+                    'ipavaultsalt': [fuzzy_string],
+                    'ipapublickey': [public_key],
+                },
+            },
+        },
+
+        {
+            'desc': 'Verify the copy creation of secret from asymmetric vault',
+            'command': (
+                'vault_retrieve',
+                [asymmetric_vault_copy2],
+                {
+                    'private_key': private_key,
+                },
+            ),
+            'expected': {
+                'value': asymmetric_vault_copy2,
+                'summary': u'Retrieved data from vault "%s"'
+                           % asymmetric_vault_copy2,
+                'result': {
+                    'dn': u'cn=%s,cn=admin,cn=users,cn=vaults,%s'
+                          % (asymmetric_vault_copy2, api.env.basedn),
+                    'cn': [asymmetric_vault_copy2],
+                    'vault_id': u'/users/admin/%s' % asymmetric_vault_copy2,
+                    'owner_user': [u'admin'],
+                    'ipavaulttype': [u'asymmetric'],
+                    'ipavaultsalt': [fuzzy_string],
+                    'ipapublickey': [public_key],
+                    'nonce': fuzzy_string,
+                    'vault_data': fuzzy_string,
+                    'data': binary_data,
+                },
+            },
+        },
+
+        {
+            'desc': 'Archive a copy of secret from asymmetric vault',
+            'command': (
+                'vault_archive',
+                [asymmetric_vault_copy2],
+                {
+                    'source_vault_id': asymmetric_secrets_vault,
+                    'source_secret_id': test_secret,
+                    'source_private_key': private_key,
+                },
+            ),
+            'expected': {
+                'value': asymmetric_vault_copy2,
+                'summary': u'Archived data into vault "%s"'
+                           % asymmetric_vault_copy2,
+                'result': {
+                    'dn': u'cn=%s,cn=admin,cn=users,cn=vaults,%s'
+                          % (asymmetric_vault_copy2, api.env.basedn),
+                    'cn': [asymmetric_vault_copy2],
+                    'vault_id': u'/users/admin/%s' % asymmetric_vault_copy2,
+                    'owner_user': [u'admin'],
+                    'ipavaulttype': [u'asymmetric'],
+                    'ipavaultsalt': [fuzzy_string],
+                    'ipapublickey': [public_key],
+                },
+            },
+        },
+
+        {
+            'desc': 'Verify the copy archival of secret from asymmetric vault',
+            'command': (
+                'vault_retrieve',
+                [asymmetric_vault_copy2],
+                {
+                    'private_key': private_key,
+                },
+            ),
+            'expected': {
+                'value': asymmetric_vault_copy2,
+                'summary': u'Retrieved data from vault "%s"'
+                           % asymmetric_vault_copy2,
+                'result': {
+                    'dn': u'cn=%s,cn=admin,cn=users,cn=vaults,%s'
+                          % (asymmetric_vault_copy2, api.env.basedn),
+                    'cn': [asymmetric_vault_copy2],
+                    'vault_id': u'/users/admin/%s' % asymmetric_vault_copy2,
+                    'owner_user': [u'admin'],
+                    'ipavaulttype': [u'asymmetric'],
+                    'ipavaultsalt': [fuzzy_string],
+                    'ipapublickey': [public_key],
+                    'nonce': fuzzy_string,
+                    'vault_data': fuzzy_string,
+                    'data': binary_data,
+                },
+            },
+        },
+
     ]
diff --git a/ipatests/test_xmlrpc/test_vaultcontainer_plugin.py b/ipatests/test_xmlrpc/test_vaultcontainer_plugin.py
index b7c79618a955dc68b89628e402849a865a9e4388..bb4a0633a368e276327e230b1b31a3261b9376d5 100644
--- a/ipatests/test_xmlrpc/test_vaultcontainer_plugin.py
+++ b/ipatests/test_xmlrpc/test_vaultcontainer_plugin.py
@@ -326,6 +326,7 @@ class test_vaultcontainer_plugin(Declarative):
                 },
             },
         },
+
         {
             'desc': 'Create base container',
             'command': (
diff --git a/ipatests/test_xmlrpc/test_vaultsecret_plugin.py b/ipatests/test_xmlrpc/test_vaultsecret_plugin.py
index b8257299510df9efecbbba320c9d22177aad0276..9c4ae6ae3e8dac6caa29e0c9679cfea3b1df767b 100644
--- a/ipatests/test_xmlrpc/test_vaultsecret_plugin.py
+++ b/ipatests/test_xmlrpc/test_vaultsecret_plugin.py
@@ -30,6 +30,8 @@ symmetric_vault = u'symmetric_vault'
 asymmetric_vault = u'asymmetric_vault'
 
 test_secret = u'test_secret'
+test_secret_copy = u'test_secret_copy'
+
 binary_data = '\x01\x02\x03\x04'
 text_data = u'secret'
 
@@ -244,6 +246,48 @@ class test_vaultsecret_plugin(Declarative):
         },
 
         {
+            'desc': 'Create a copy of secret from a standard vault',
+            'command': (
+                'vaultsecret_add',
+                [symmetric_vault, test_secret_copy],
+                {
+                    'source_vault_id': test_vault,
+                    'source_secret_id': test_secret,
+                    'password': password,
+                },
+            ),
+            'expected': {
+                'value': test_secret_copy,
+                'summary': 'Added vault secret "%s"' % test_secret_copy,
+                'result': {
+                    'secret_name': test_secret_copy,
+                    'data': binary_data,
+                },
+            },
+        },
+
+        {
+            'desc': 'Update a copy of secret from a standard vault',
+            'command': (
+                'vaultsecret_mod',
+                [symmetric_vault, test_secret_copy],
+                {
+                    'source_vault_id': test_vault,
+                    'source_secret_id': test_secret,
+                    'password': password,
+                },
+            ),
+            'expected': {
+                'value': test_secret_copy,
+                'summary': 'Modified vault secret "%s"' % test_secret_copy,
+                'result': {
+                    'secret_name': test_secret_copy,
+                    'data': binary_data,
+                },
+            },
+        },
+
+        {
             'desc': 'Create asymmetric vault',
             'command': (
                 'vault_add',
@@ -290,6 +334,92 @@ class test_vaultsecret_plugin(Declarative):
         },
 
         {
+            'desc': 'Create a copy of secret from a symmetric vault',
+            'command': (
+                'vaultsecret_add',
+                [asymmetric_vault, test_secret_copy],
+                {
+                    'private_key': private_key,
+                    'source_vault_id': symmetric_vault,
+                    'source_secret_id': test_secret,
+                    'source_password': password,
+                },
+            ),
+            'expected': {
+                'value': test_secret_copy,
+                'summary': 'Added vault secret "%s"' % test_secret_copy,
+                'result': {
+                    'secret_name': test_secret_copy,
+                    'data': binary_data,
+                },
+            },
+        },
+
+        {
+            'desc': 'Update a copy of secret from a symmetric vault',
+            'command': (
+                'vaultsecret_mod',
+                [asymmetric_vault, test_secret_copy],
+                {
+                    'private_key': private_key,
+                    'source_vault_id': symmetric_vault,
+                    'source_secret_id': test_secret,
+                    'source_password': password,
+                },
+            ),
+            'expected': {
+                'value': test_secret_copy,
+                'summary': 'Modified vault secret "%s"' % test_secret_copy,
+                'result': {
+                    'secret_name': test_secret_copy,
+                    'data': binary_data,
+                },
+            },
+        },
+
+        {
+            'desc': 'Create a copy of secret from an asymmetric vault',
+            'command': (
+                'vaultsecret_add',
+                [test_vault, test_secret_copy],
+                {
+                    'source_vault_id': asymmetric_vault,
+                    'source_secret_id': test_secret,
+                    'source_private_key': private_key,
+                },
+            ),
+            'expected': {
+                'value': test_secret_copy,
+                'summary': 'Added vault secret "%s"' % test_secret_copy,
+                'result': {
+                    'secret_name': test_secret_copy,
+                    'data': binary_data,
+                },
+            },
+        },
+
+        {
+            'desc': 'Update a copy of secret from an asymmetric vault',
+            'command': (
+                'vaultsecret_mod',
+                [test_vault, test_secret_copy],
+                {
+                    'source_vault_id': asymmetric_vault,
+                    'source_secret_id': test_secret,
+                    'source_private_key': private_key,
+                },
+            ),
+            'expected': {
+                'value': test_secret_copy,
+                'summary': 'Modified vault secret "%s"' % test_secret_copy,
+                'result': {
+                    'secret_name': test_secret_copy,
+                    'data': binary_data,
+                },
+            },
+        },
+
+        {
             'desc': 'Delete secret',
             'command': (
                 'vaultsecret_del',
@@ -452,6 +582,25 @@ class test_vaultsecret_plugin(Declarative):
         },
 
         {
+            'desc': 'Create a copy of secret in the same vault',
+            'command': (
+                'vaultsecret_add',
+                [shared_test_vault, test_secret_copy],
+                {
+                    'source_secret_id': test_secret,
+                },
+            ),
+            'expected': {
+                'value': test_secret_copy,
+                'summary': 'Added vault secret "%s"' % test_secret_copy,
+                'result': {
+                    'secret_name': test_secret_copy,
+                    'data': binary_data,
+                },
+            },
+        },
+
+        {
             'desc': 'Delete shared secret',
             'command': (
                 'vaultsecret_del',
-- 
2.3.1

-------------- next part --------------
>From 196e2178b2f917e91aa72e31eae5c980c2332504 Mon Sep 17 00:00:00 2001
From: "Endi S. Dewata" <edewata at redhat.com>
Date: Fri, 27 Mar 2015 12:48:47 -0400
Subject: [PATCH] Refactored baseldap.py.

Some classes in baseldap.py have been modified to allow subclasses
such as vault plugins to override the default behavior for error
handling and subtree deletion if necessary.

https://fedorahosted.org/freeipa/ticket/3872
---
 ipalib/plugins/baseldap.py | 113 ++++++++++++++++++++++++---------------------
 1 file changed, 61 insertions(+), 52 deletions(-)

diff --git a/ipalib/plugins/baseldap.py b/ipalib/plugins/baseldap.py
index 4b1c701924d57919538e0c428ea181c2e898505e..fceaf95f42bef5fa71cbedeb291bd68d2919bc5a 100644
--- a/ipalib/plugins/baseldap.py
+++ b/ipalib/plugins/baseldap.py
@@ -1152,19 +1152,7 @@ class LDAPCreate(BaseLDAPCommand, crud.Create):
         try:
             self._exc_wrapper(keys, options, ldap.add_entry)(entry_attrs)
         except errors.NotFound:
-            parent = self.obj.parent_object
-            if parent:
-                raise errors.NotFound(
-                    reason=self.obj.parent_not_found_msg % {
-                        'parent': keys[-2],
-                        'oname': self.api.Object[parent].object_name,
-                    }
-                )
-            raise errors.NotFound(
-                reason=self.obj.container_not_found_msg % {
-                    'container': self.obj.container_dn,
-                }
-            )
+            self.handle_not_found(*keys, **options)
         except errors.DuplicateEntry:
             self.obj.handle_duplicate_entry(*keys)
 
@@ -1213,6 +1201,21 @@ class LDAPCreate(BaseLDAPCommand, crud.Create):
     def exc_callback(self, keys, options, exc, call_func, *call_args, **call_kwargs):
         raise exc
 
+    def handle_not_found(self, *args, **options):
+        parent = self.obj.parent_object
+        if parent:
+            raise errors.NotFound(
+                reason=self.obj.parent_not_found_msg % {
+                    'parent': args[-2],
+                    'oname': self.api.Object[parent].object_name,
+                }
+            )
+        raise errors.NotFound(
+            reason=self.obj.container_not_found_msg % {
+                'container': self.obj.container_dn,
+            }
+        )
+
     def interactive_prompt_callback(self, kw):
         return
 
@@ -1498,48 +1501,50 @@ class LDAPDelete(LDAPMultiQuery):
 
     has_output_params = global_output_params
 
-    def execute(self, *keys, **options):
+    def delete_subtree(self, base_dn, *nkeys, **options):
         ldap = self.obj.backend
-
-        def delete_entry(pkey):
-            nkeys = keys[:-1] + (pkey, )
-            dn = self.obj.get_dn(*nkeys, **options)
-            assert isinstance(dn, DN)
-
-            for callback in self.get_callbacks('pre'):
-                dn = callback(self, ldap, dn, *nkeys, **options)
-                assert isinstance(dn, DN)
-
-            def delete_subtree(base_dn):
-                assert isinstance(base_dn, DN)
-                truncated = True
-                while truncated:
-                    try:
-                        (subentries, truncated) = ldap.find_entries(
-                            None, [''], base_dn, ldap.SCOPE_ONELEVEL
-                        )
-                    except errors.NotFound:
-                        break
-                    else:
-                        for entry_attrs in subentries:
-                            delete_subtree(entry_attrs.dn)
-                try:
-                    self._exc_wrapper(nkeys, options, ldap.delete_entry)(base_dn)
-                except errors.NotFound:
-                    self.obj.handle_not_found(*nkeys)
-
+        assert isinstance(base_dn, DN)
+        truncated = True
+        while truncated:
             try:
-                self._exc_wrapper(nkeys, options, ldap.delete_entry)(dn)
+                (subentries, truncated) = ldap.find_entries(
+                    None, [''], base_dn, ldap.SCOPE_ONELEVEL
+                )
             except errors.NotFound:
-                self.obj.handle_not_found(*nkeys)
-            except errors.NotAllowedOnNonLeaf:
-                # this entry is not a leaf entry, delete all child nodes
-                delete_subtree(dn)
+                break
+            else:
+                for entry_attrs in subentries:
+                    self.delete_subtree(entry_attrs.dn, *nkeys, **options)
+        try:
+            self._exc_wrapper(nkeys, options, ldap.delete_entry)(base_dn)
+        except errors.NotFound:
+            self.obj.handle_not_found(*nkeys)
 
-            for callback in self.get_callbacks('post'):
-                result = callback(self, ldap, dn, *nkeys, **options)
+    def delete_entry(self, pkey, *keys, **options):
+        ldap = self.obj.backend
+        nkeys = keys[:-1] + (pkey, )
+        dn = self.obj.get_dn(*nkeys, **options)
+        assert isinstance(dn, DN)
 
-            return result
+        for callback in self.get_callbacks('pre'):
+            dn = callback(self, ldap, dn, *nkeys, **options)
+            assert isinstance(dn, DN)
+
+        try:
+            self._exc_wrapper(nkeys, options, ldap.delete_entry)(dn)
+        except errors.NotFound:
+            self.obj.handle_not_found(*nkeys)
+        except errors.NotAllowedOnNonLeaf:
+            # this entry is not a leaf entry, delete all child nodes
+            self.delete_subtree(dn, *nkeys, **options)
+
+        for callback in self.get_callbacks('post'):
+            result = callback(self, ldap, dn, *nkeys, **options)
+
+        return result
+
+    def execute(self, *keys, **options):
+        ldap = self.obj.backend
 
         if self.obj.primary_key and isinstance(keys[-1], (list, tuple)):
             pkeyiter = keys[-1]
@@ -1552,7 +1557,7 @@ class LDAPDelete(LDAPMultiQuery):
         failed = []
         for pkey in pkeyiter:
             try:
-                delete_entry(pkey)
+                self.delete_entry(pkey, *keys, **options)
             except errors.ExecutionError:
                 if not options.get('continue', False):
                     raise
@@ -1998,7 +2003,8 @@ class LDAPSearch(BaseLDAPCommand, crud.Search):
         except errors.EmptyResult:
             (entries, truncated) = ([], False)
         except errors.NotFound:
-            self.api.Object[self.obj.parent_object].handle_not_found(*args[:-1])
+            self.handle_not_found(*args, **options)
+            (entries, truncated) = ([], False)
 
         for callback in self.get_callbacks('post'):
             truncated = callback(self, ldap, entries, truncated, *args, **options)
@@ -2024,6 +2030,9 @@ class LDAPSearch(BaseLDAPCommand, crud.Search):
             truncated=truncated,
         )
 
+    def handle_not_found(self, *args, **options):
+        self.api.Object[self.obj.parent_object].handle_not_found(*args[:-1])
+
     def pre_callback(self, ldap, filters, attrs_list, base_dn, scope, *args, **options):
         assert isinstance(base_dn, DN)
         return (filters, base_dn, scope)
-- 
2.3.1



More information about the Freeipa-devel mailing list