Sorry for the long delay. Attached is an updated patch addressing most of the concerns. I think the rest can be addressed in subsequent patches.

On 11/5/2014 4:06 AM, Petr Viktorin wrote:
ipapython/dn.py: This change is not needed. If you have a sequence of
RNDs you can do `DN(*seq)`.


This is actually needed to construct a parent DN from an existing DN:

   parent_dn = DN(dn[1:])

Right, for that you can do
     parent_dn = DN(*dn[1:])
(note the asterisk)

Fixed.

All existing plugins are actually using LDAPObject directly, not
baseldap.LDAPObject. Is there a concern doing that? I fixed the code to
do the same thing.

Your choice.
If there are many objects imported from a module I prefer importing the
module itself, but individual names work too.

For now I'll keep it consistent with the other plugins. There aren't that many objects imported, and it's easier to copy codes between plugins.

The pattern and pattern_errmsg for the 'cn' param don't match. Which one
is right? Shouldn't a similar check be there for parent?

This is actually copied verbatim from user's uid. Could you give some
sample values that show the problem?

These messages are misleading:

$ ipa vaultcontainer-add '$$'
ipa: ERROR: invalid 'container_name': may only include letters, numbers,
_, -, . and $
$ ipa vaultcontainer-add -- '-myvault-'
ipa: ERROR: invalid 'container_name': may only include letters, numbers,
_, -, . and $

(Note that in webUI you wouldn't need the awkward quoting and `--`)

I think ideally there should be a similar check for parent, but I
haven't figured out the proper pattern yet. I think we can do this as a
later enhancement. Or maybe we should remove the validation for cn for
now.

Well, what are the PKI limitations on ids?
Are there any best practices for the id names?

I don't think KRA itself puts any restrictions on the ID, but see the problem with get_active_key_info() below.

I think we want to limit the names to avoid nasty surprises later;
'[a-zA-Z0-9_.-]+' seems like a good set.
For parent you also need the slash, otherwise use the same pattern.

I think this is quite an important decision; weird names could lead to
security issues.

OK, I changed the container name to use that pattern and added slash in the parent's pattern.

vaultcontainer.get_dn: Why put '/' at the end of container_id, if the
empty string is ignored later anyway?

I'm still considering some options for container ID:
a) use a slash at the end for all containers (e.g. /users/)
b) don't use a trailing slash, except for root container (/)

Maybe a better way is to use an array of names internally and the slash
is only used during input/output. The problem is when calling another
command the array has to be converted into string and parsed back into
array, so it may even introduce more redundancies. I don't see a way to
pass an object parameter to a command. Any suggestion?

That would indeed be better. Hopefully you can avoid re-parsing and
re-validating by not calling nested Commands, see below.

In vaultcontainer.get_id, what's the purpose of the len() check? Did you
want dn.endswith() instead?

Yes, but my assumption is the DN will always be within the namespace
because it's only used internally, and using len() will be faster than
endswith(). The error case will never happen, but I was just making sure
there won't be an infinite loop. Maybe instead of calling the method
recursively I should just use a loop? That will avoid repetitive
validations.

Well, if you end up with representing the id as a list the problem will
largely go away, but: use an internal error (e.g. ValueError) for
internal sanity checking. The public errors are for user errors, they
are presented "nicely" which makes them much harder to debug. And
"Invalid container DN" with no indication of what the DN is isn't
helpful to the user either.

I revised the code to use endswith() and a loop instead of recursion, but I'm keeping the string ID for now. I changed the exception to ValueError and included the DN value. We can improve this later.

I sugggest writing doctests for the id manipulation methods (get_id,
get_private_id, ...) – it's not always obvious what exactly they do.

Do you mean sample code in docs? Is it still necessary if we have some
test code?

Not strictly necessary, but it better explains what the method does.

I'm not sure how to write doctests for plugin objects/commands. I don't see any example in other plugins. Do I need to write the API initialization code? What if the output depends on the authenticated user? I suppose we can add this later. I already added some unit tests.

vaultcontainer_add: You should use ldap backend directly. Calling
Commands is costly, most of the call is spent doing validation of what
here is already validated. You should add a function to recursively
create vault container using just the ldap client, and call that here
and in vault_add.

Hmm.. I'm not sure if we should write a code that will duplicate
everything done by the LDAPCreate class. Shouldn't server-side
command-to-command invocation be relatively faster?

But you don't everything done by LDAPCreate. Most of what it does is
parsing options, validating options, formatting output. You don't even
use the added entry, after LDAPCreate spends an additional LDAP call to
re-retreive it after adding.

Commands are not really designed to be nested, and tend to have subtle
assumptions of "owning" the current thread.

Not to mention that debugging nested Command calls is a nightmare.
Imagine calling your own vault_retrieve from an unrelated command, and
getting a "Data not found." error, formatted for user consumption (e.g.
no traceback).

(Yes, some parts of IPA do currently call nested commands. This is
unfortunate and leads to hard-to-debug issues.)

I've removed the server-side nested command invocations (see below), but on the client-side I don't think it's avoidable. Some of the commands (e.g. vault-show, vault-transport-cert) provide basic functionalities that are used by other commands.

The recursion is mainly used to create the user's private container.
Note that the private container is not created by default. We only
provide /users top-level container. When a user creates a private vault
the command will automatically create the /users/<username> private
container on the user's behalf. This will only happen once for each
user. In most other cases the parent would already exist.

Another option is to require the user or the admin to create the private
container manually, but that would be less user-friendly.

Another option is to check the parent container using direct LDAP
search, but still do the add using vaultcontainer_add. What do you think?

Why? Is adding the entry via ldap that hard to do?
What functionality from LDAPCreate do you need? Maybe we can pull it out
into a function.

OK, I added the LDAP code. It's not about the complexity, but it's kind of breaking the encapsulation. Fortunately for this particular case the entry is simple enough that it can be constructed manually.

You can delete a container with children; is that expected?

This seems to be LDAPDelete's built-in functionality. Is there an option
to disallow non-empty subtree removal?

For vault purposes, when a user is removed from IPA the server will
remove the user's entire private container, so the feature would
actually be useful here. But probably in general we should warn the user
if they are deleting a non-empty container. If there's an option in
LDAPDelete we can let the user specify that option if necessary.

A warning doesn't seem very useful. ("Yes sir, deleted your data! Oh, by
the way, did you really want to do that?")
I think requiring a --force or --recursive option for non-empty trees
could better prevent surprises.

Right, that's what I meant, but the problem is the current LDAPDelete will always remove the whole subtree without any option to prevent it. If I change the LDAPDelete's behavior it may affect other plugins. So for now I just refactored the LDAPDelete to allow vaultcontainer_del to override delete_entry() and control the behavior using --force option. Later on we can move this option to the base class if necessary.

vault_add should complain if it does not get exactly one of
data/text/in.

Fixed.

Actually, no. The primary way to archive data is via vault_archive. The
vault_add is for creating a vault, but optionally you can specify the
initial data to be archived. If nothing is specified it will archive an
empty string. Note that for symmetric vault the encrypted empty string
can be used to enforce that the same password will be used on on
subsequent archival operations.

Then the command should complain if is gets more than one type of input.

Fixed the vault_archive too.

It still seems redundant to me.
If I'm getting it correctly, to change a vault's attributes like
description, you need vault_mod, but to change the stored secret you do
vault_archive. Is that right? Why not do all modifications in vault_mod?

It's because during archival the data is actually stored into KRA, not in the vault LDAP entry, so it's not an LDAP modify operation. Also, for symmetric & asymmetric vaults there will be additional encryption parameters required (e.g. password, private key) to archive/retrieve data. A regular vault attribute modification would not need those parameters.

And if you do end up needing separate commands, you can pull the
communication with KRA into a function and call that, rather than
calling the whole Command machinery.

I'm trying to change vault_archive to raise an error if it's missing the
input data and change vault_add to archive empty string if nothing is
specified, but it doesn't work since the framework seems to be
converting the empty string into None, so vault_archive can't tell if
it's archiving an empty string or missing the input data. Any suggestion?

Due to this limitation, for now if there's no input data specified it will archive an empty string, no errors raised.

What's the difference between vault_add and vault_archive? I don't see
vault_archive in the design.

Yes, that's due to a recent 'standard vault' addition. I have not added
vault_archive yet to the design. The vault_archive is used to archive a
generic blob of data. The vaultsecret_archive will later be used to add
secrets into a JSON structure which will then be archived using the
vault_archive.

Okay. I'm not sure I get the details right, a design would help.

I added the following sections:
http://www.freeipa.org/page/V4/Password_Vault_Implementation#Archiving_data
http://www.freeipa.org/page/V4/Password_Vault_Implementation#Retrieving_data

http://www.freeipa.org/page/V4/Password_Vault_Implementation#vaultsecret_archive.28.29
http://www.freeipa.org/page/V4/Password_Vault_Implementation#vaultsecret_retrieve.28.29

It seems '/' is equivalent to '-' as far as KRA is concerned; should we
disallow '-' in container/vault names?

I think so, but as I mentioned earlier I haven't look very far on
attribute validations.

The slashes in vault ID is converted into dashes in KRA's client key ID.
KRA actually accepts slashes, but the slashes will not look pretty in
REST URL because they will be URL-encoded (in case it's needed for
troubleshooting KRA via browser) so I changed them to dashes.

It turns out there's a also problem with get_active_key_info() processing the slashes. This is a Dogtag issue. I've changed the code to use list_keys() which doesn't suffer from the same problem.

So it seems that these IDs would be equivalent for the KRA:

/users/admin/private/secret
/users/admin-private/secret

If this is just for troubleshooting purposes, please don't change to
dashes.

After changing to list_keys() I can use the slashes without problems.

You can specify an absolute id by starting it with a slash, but only in
--parent and not in the name itself. I think this should be possible in
the name too.

Right, but I'm not sure how it will work with the IPA framework. The
first command parameter is the cn (vault name). If we specify a vault ID
(with slashes) there it will be entered into cn. Should we parse the ID
and insert the vault name and parent ID back into keys and options
variables? Which value will eventually be stored into cn in LDAP?

You're right, managing that would need some delicate coding.

But I you could override args_options_2_params to adjust the options
before they're even validated, so the framework gets cn and parent as it
does currently.

You can't include slashes in the name, so you always need to specify the
prefix with --parent. I don't think there's a technical reason for this
limitation.

I think it can be confusing. If we define vault name to allow slashes, people might think slashes are valid characters of vault names when actually they are only delimiters. Maybe in a separate patch later we can try changing the argument from vault name to vault ID.

There are no tests.

Right, I'll take a look at how to write tests. Is there a way to run
tests without running the full build?

Yes, see http://www.freeipa.org/page/Testing
(Though I usually just copy changed files to /usr/lib/ and restart Apache)

OK. Added some tests.

--
Endi S. Dewata
>From 3290436d6bce38476519c354c0017b5cd1b81d3a Mon Sep 17 00:00:00 2001
From: "Endi S. Dewata" <edew...@redhat.com>
Date: Tue, 21 Oct 2014 10:57:08 -0400
Subject: [PATCH] Added initial vault implementation.

This patch provides the initial vault implementation which allows
the admin to create a vault, archive data, and retrieve data using
a standard vault. It also includes the initial LDAP schema.

It currently has limitations including:
 - The vault only supports the standard vault type.
 - The vault can only be used by the admin user.
 - The transport certificate has to be installed manually.

These limitations, other vault features, schema and ACL changes
will be addressed in subsequent patches.

The LDAPDelete has been refactored to allow overriding the subtree
deletion behavior.

A test script have been added as well.

https://fedorahosted.org/freeipa/ticket/3872
---
 API.txt                                   |  169 +++++
 VERSION                                   |    4 +-
 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/baseldap.py                |   78 +--
 ipalib/plugins/user.py                    |    9 +
 ipalib/plugins/vault.py                   | 1003 +++++++++++++++++++++++++++++
 ipatests/test_xmlrpc/test_vault_plugin.py |  614 ++++++++++++++++++
 11 files changed, 1865 insertions(+), 40 deletions(-)
 create mode 100644 install/updates/40-vault.update
 create mode 100644 ipalib/plugins/vault.py
 create mode 100644 ipatests/test_xmlrpc/test_vault_plugin.py

diff --git a/API.txt b/API.txt
index 
08615c80476171695dac262575c4e20e72615c36..559f4b97fad334f037cb61bbb787f7dfcbd6e23c
 100644
--- a/API.txt
+++ b/API.txt
@@ -4517,6 +4517,175 @@ 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,10,3
+arg: Str('cn', attribute=True, cli_name='vault_name', maxlength=255, 
multivalue=False, pattern='^[a-zA-Z0-9_.-]+$', primary_key=True, required=True)
+option: Flag('all', autofill=True, cli_name='all', default=False, 
exclude='webui')
+option: Bytes('data?', cli_name='data')
+option: Str('description', attribute=True, cli_name='desc', multivalue=False, 
required=False)
+option: Str('in?', cli_name='in')
+option: Str('parent', attribute=False, cli_name='parent', multivalue=False, 
pattern='^[a-zA-Z0-9_.-/]+$', required=False)
+option: Flag('raw', autofill=True, cli_name='raw', default=False, 
exclude='webui')
+option: Flag('rights', autofill=True, default=False)
+option: Str('text?', cli_name='text')
+option: Str('vault_id', attribute=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_archive
+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: Flag('all', autofill=True, cli_name='all', default=False, 
exclude='webui')
+option: Bytes('data?', cli_name='data')
+option: Bytes('encrypted_data?', cli_name='encrypted_data')
+option: Str('in?', cli_name='in')
+option: Bytes('nonce?', cli_name='nonce')
+option: Str('parent?', cli_name='parent')
+option: Flag('raw', autofill=True, cli_name='raw', default=False, 
exclude='webui')
+option: Flag('rights', autofill=True, default=False)
+option: Str('text?', cli_name='text')
+option: Str('version?', exclude='webui')
+option: Bytes('wrapped_session_key?', cli_name='wrapped_session_key')
+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=True, pattern='^[a-zA-Z0-9_.-]+$', primary_key=True, query=True, 
required=True)
+option: Flag('continue', autofill=True, cli_name='continue', default=False)
+option: Str('parent?', cli_name='parent')
+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', attribute=False, autofill=False, cli_name='parent', 
multivalue=False, pattern='^[a-zA-Z0-9_.-/]+$', query=True, required=False)
+option: Flag('pkey_only?', autofill=True, default=False)
+option: Flag('raw', autofill=True, cli_name='raw', default=False, 
exclude='webui')
+option: 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', attribute=False, autofill=False, cli_name='parent', 
multivalue=False, pattern='^[a-zA-Z0-9_.-/]+$', required=False)
+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,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: Str('out?', cli_name='out')
+option: Str('parent?', cli_name='parent')
+option: Flag('raw', autofill=True, cli_name='raw', default=False, 
exclude='webui')
+option: Flag('rights', autofill=True, default=False)
+option: Flag('show_text?', autofill=True, default=False)
+option: Flag('stdout?', autofill=True, default=False)
+option: Str('version?', exclude='webui')
+option: Bytes('wrapped_session_key?', cli_name='wrapped_session_key')
+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?', cli_name='parent')
+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: 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', attribute=False, cli_name='parent', multivalue=False, 
pattern='^[a-zA-Z0-9_.-/]+$', required=False)
+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=True, 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?', cli_name='parent')
+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', attribute=False, autofill=False, cli_name='parent', 
multivalue=False, pattern='^[a-zA-Z0-9_.-/]+$', query=True, required=False)
+option: Flag('pkey_only?', autofill=True, default=False)
+option: Flag('raw', autofill=True, cli_name='raw', default=False, 
exclude='webui')
+option: 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', attribute=False, autofill=False, cli_name='parent', 
multivalue=False, pattern='^[a-zA-Z0-9_.-/]+$', required=False)
+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: Flag('continue', autofill=True, cli_name='continue', default=False)
+option: Str('parent?', cli_name='parent')
+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)
 capability: messages 2.52
 capability: optional_uid_params 2.54
 capability: permissions2 2.69
diff --git a/VERSION b/VERSION
index 
f587c65c8868a1693d2b19fb4dae97b7cdd4c65a..5acbd7bafee959b97ce2a6584646b5bfb58199d3
 100644
--- a/VERSION
+++ b/VERSION
@@ -90,5 +90,5 @@ IPA_DATA_VERSION=20100614120000
 #                                                      #
 ########################################################
 IPA_API_VERSION_MAJOR=2
-IPA_API_VERSION_MINOR=112
-# Last change: tbabej - change ipaassignedidview to Str
+IPA_API_VERSION_MINOR=113
+# Last change: edewata - initial vault implementation
diff --git a/install/share/60basev3.ldif b/install/share/60basev3.ldif
index 
4efb1fe8ba8a91d3a8b920d39d217124066728c0..e0bc4151d88fc394cea0d81a20d8da9537dd5cd3
 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/baseldap.py b/ipalib/plugins/baseldap.py
index 
4b1c701924d57919538e0c428ea181c2e898505e..d693709ac1ba7ddb3c559199c199039b6f8bd9ac
 100644
--- a/ipalib/plugins/baseldap.py
+++ b/ipalib/plugins/baseldap.py
@@ -1498,48 +1498,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 +1554,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
diff --git a/ipalib/plugins/user.py b/ipalib/plugins/user.py
index 
56585b9f86593c0c5879139103bc71707b88e15f..7322e2b69efbcc0a655caeb9e0863ad93dbf1ce9
 100644
--- a/ipalib/plugins/user.py
+++ b/ipalib/plugins/user.py
@@ -901,6 +901,15 @@ class user_del(LDAPDelete):
             else:
                 self.api.Command.otptoken_del(token)
 
+        # Delete user's private vault container.
+        vaultcontainer_id = 
self.api.Object.vaultcontainer.get_private_id(owner)
+        (vaultcontainer_name, vaultcontainer_parent_id) = 
self.api.Object.vaultcontainer.split_id(vaultcontainer_id)
+
+        try:
+            self.api.Command.vaultcontainer_del(vaultcontainer_name, 
parent=vaultcontainer_parent_id)
+        except errors.NotFound:
+            pass
+
         return dn
 
 
diff --git a/ipalib/plugins/vault.py b/ipalib/plugins/vault.py
new file mode 100644
index 
0000000000000000000000000000000000000000..25372d0f02058fb5efd9f5dece96073ea1d78646
--- /dev/null
+++ b/ipalib/plugins/vault.py
@@ -0,0 +1,1003 @@
+# Authors:
+#   Endi S. Dewata <edew...@redhat.com>
+#
+# Copyright (C) 2014  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 random
+import shutil
+import string
+import tempfile
+
+import pki
+import pki.account
+import pki.crypto
+import pki.key
+
+from ipalib import api, errors
+from ipalib import Str, Bytes, Flag
+from ipalib.frontend import Command
+from ipalib.plugable import Registry
+from ipalib.plugins.baseldap import LDAPObject, LDAPCreate, LDAPDelete, 
LDAPSearch, LDAPUpdate, LDAPRetrieve
+from ipalib.request import context
+from ipalib.plugins.user import split_principal
+from ipalib import _, ngettext
+from ipaplatform.paths import paths
+from ipapython.dn import DN
+import ipapython.nsslib
+
+__doc__ = _("""
+Vaults
+""") + _("""
+Manage vaults and vault containers.
+""") + _("""
+EXAMPLES:
+""") + _("""
+ List private vaults:
+   ipa vault-find
+""") + _("""
+ List shared vaults:
+   ipa vault-find --parent /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
+""") + _("""
+ List private vault containers:
+   ipa vaultcontainer-find
+""") + _("""
+ List top-level vault containers:
+   ipa vaultcontainer-find --parent /
+""") + _("""
+ 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()
+transport_cert_nickname = 'KRA Transport Certificate'
+
+@register()
+class vaultcontainer(LDAPObject):
+    __doc__ = _("""
+    Vault container object.
+    """)
+
+    base_dn = DN(api.env.container_vault, api.env.basedn)
+    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('parent?',
+            cli_name='parent',
+            label=_('Parent'),
+            doc=_('Parent container'),
+            flags=('virtual_attribute'),
+            pattern='^[a-zA-Z0-9_.-/]+$',
+            pattern_errmsg='may only include letters, numbers, _, ., -, and /',
+        ),
+        Str('container_id?',
+            cli_name='container_id',
+            label=_('Container ID'),
+            doc=_('Container ID'),
+            flags=('virtual_attribute'),
+        ),
+        Str('description?',
+            cli_name='desc',
+            label=_('Description'),
+            doc=_('Container description'),
+        ),
+    )
+
+    def get_dn(self, *keys, **options):
+        __doc__ = _("""
+        Generates vault container DN from container ID.
+        """)
+
+        # get container ID from parameters
+        name = None
+        if keys:
+            name = keys[0]
+
+        parent_id = 
api.Object.vaultcontainer.normalize_id(options.get('parent'))
+
+        container_id = parent_id
+        if name:
+            container_id = parent_id + name + u'/'
+
+        dn = self.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):
+        __doc__ = _("""
+        Generates container ID from container DN.
+        """)
+
+        # make sure the DN is a container DN
+        if not dn.endswith(self.base_dn):
+            raise ValueError('Invalid container DN: %s' % dn)
+
+        # construct container ID from the bottom up
+        id = u'/'
+        while len(dn) > len(self.base_dn):
+
+            rdn = dn[0]
+            name = rdn['cn']
+            id =  u'/' + name + id
+
+            dn = DN(*dn[1:])
+
+        return id
+
+    def get_private_id(self, username=None):
+        __doc__ = _("""
+        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):
+        __doc__ = _("""
+        Normalizes container ID.
+        """)
+
+        # if ID is empty, return user's private container ID
+        if not id:
+            return self.get_private_id()
+
+        # make sure ID ends with slash
+        if not id.endswith(u'/'):
+            id += u'/'
+
+        # 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):
+        __doc__ = _("""
+        Splits a normalized container ID into (container name, parent ID) 
tuple.
+        """)
+
+        # handle root ID
+        if id == u'/':
+            return (None, None)
+
+        # split ID into parent ID, container name, and empty string
+        parts = id.rsplit(u'/', 2)
+
+        # return container name and parent ID
+        return (parts[1], parts[0] + u'/')
+
+
+@register()
+class vaultcontainer_add(LDAPCreate):
+    __doc__ = _('Create a new vault container.')
+
+    msg_summary = _('Added vault container "%(value)s"')
+
+    def pre_callback(self, ldap, dn, entry_attrs, attrs_list, *keys, 
**options):
+        assert isinstance(dn, DN)
+
+        # add parent containers if they don't exist
+        try:
+            ldap = self.obj.backend
+
+            while True:
+                parent_dn = DN(*dn[1:])
+                parent_rdn = parent_dn[0]
+
+                parent = ldap.make_entry(
+                    parent_dn,
+                    {
+                        'objectclass': api.Object.vaultcontainer.object_class,
+                        'cn': parent_rdn['cn'],
+                    })
+
+                ldap.add_entry(parent)
+
+        except errors.DuplicateEntry:
+            pass
+
+        return dn
+
+    def post_callback(self, ldap, dn, entry_attrs, *keys, **options):
+        assert isinstance(dn, DN)
+
+        entry_attrs['container_id'] = self.obj.get_id(dn)
+
+        return dn
+
+
+@register()
+class vaultcontainer_del(LDAPDelete):
+    __doc__ = _('Delete a vault container.')
+
+    msg_summary = _('Deleted vault container "%(value)s"')
+
+    takes_options = LDAPDelete.takes_options + (
+        Str('parent?',
+            cli_name='parent',
+            doc=_('Parent container'),
+        ),
+        Flag('force?',
+            doc=_('Force deletion'),
+            autofill=False,
+        ),
+    )
+
+    def delete_entry(self, pkey, *keys, **options):
+        __doc__ = _("""
+        Overwrites the base method to control deleting subtree with force 
option.
+        """)
+
+        ldap = self.obj.backend
+        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)
+
+        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
+            # if forced, delete all child nodes
+            if options.get('force'):
+                self.delete_subtree(dn, *nkeys, **options)
+            else:
+                raise
+
+        for callback in self.get_callbacks('post'):
+            result = callback(self, ldap, dn, *nkeys, **options)
+
+        return result
+
+
+@register()
+class vaultcontainer_find(LDAPSearch):
+    __doc__ = _('Search for vault containers.')
+
+    msg_summary = ngettext(
+        '%(count)d vault container matched', '%(count)d vault containers 
matched', 0
+    )
+
+    def pre_callback(self, ldap, filter, attrs_list, base_dn, scope, *keys, 
**options):
+        assert isinstance(base_dn, DN)
+
+        parent_id = self.obj.normalize_id(options.get('parent'))
+        base_dn = self.obj.get_dn(parent=parent_id)
+
+        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
+
+
+@register()
+class vaultcontainer_mod(LDAPUpdate):
+    __doc__ = _('Modify a vault container.')
+
+    msg_summary = _('Modified vault container "%(value)s"')
+
+    def post_callback(self, ldap, dn, entry_attrs, *keys, **options):
+        assert isinstance(dn, DN)
+
+        entry_attrs['container_id'] = self.obj.get_id(dn)
+
+        return dn
+
+
+@register()
+class vaultcontainer_show(LDAPRetrieve):
+    __doc__ = _('Display information about a vault container.')
+
+    takes_options = LDAPDelete.takes_options + (
+        Str('parent?',
+            cli_name='parent',
+            doc=_('Parent container'),
+        ),
+    )
+
+    def post_callback(self, ldap, dn, entry_attrs, *keys, **options):
+        assert isinstance(dn, DN)
+
+        entry_attrs['container_id'] = self.obj.get_id(dn)
+
+        return dn
+
+
+@register()
+class vault(LDAPObject):
+    __doc__ = _("""
+    Vault object.
+    """)
+
+    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('parent?',
+            cli_name='parent',
+            label=_('Parent'),
+            doc=_('Parent container'),
+            flags=('virtual_attribute'),
+            pattern='^[a-zA-Z0-9_.-/]+$',
+            pattern_errmsg='may only include letters, numbers, _, ., -, and /',
+        ),
+        Str('vault_id?',
+            cli_name='vault_id',
+            label=_('Vault ID'),
+            doc=_('Vault ID'),
+            flags=('virtual_attribute'),
+        ),
+        Str('description?',
+            cli_name='desc',
+            label=_('Description'),
+            doc=_('Vault description'),
+        ),
+    )
+
+    def get_dn(self, *keys, **options):
+        __doc__ = _("""
+        Generates vault DN from vault ID.
+        """)
+
+        # get vault ID from parameters
+        name = None
+        if keys:
+            name = keys[0]
+
+        parent_id = 
api.Object.vaultcontainer.normalize_id(options.get('parent'))
+
+        vault_id = parent_id
+        if name:
+            vault_id = parent_id + name
+
+        dn = api.Object.vaultcontainer.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):
+        __doc__ = _("""
+        Generates vault ID from vault DN.
+        """)
+
+        # make sure the DN is a vault DN
+        if not dn.endswith(api.Object.vaultcontainer.base_dn):
+            raise ValueError('Invalid vault DN: %s' % dn)
+
+        # vault DN cannot be the container base DN
+        if len(dn) == len(api.Object.vaultcontainer.base_dn):
+            raise ValueError('Invalid vault DN: %s' % dn)
+
+        # construct the vault ID from the bottom up
+        id = u''
+        while len(dn) > len(api.Object.vaultcontainer.base_dn):
+
+            rdn = dn[0]
+            name = rdn['cn']
+            id =  u'/' + name + id
+
+            dn = DN(*dn[1:])
+
+        return id
+
+    def split_id(self, id):
+        __doc__ = _("""
+        Splits a vault ID into (vault name, parent ID) tuple.
+        """)
+
+        # split ID into parent ID and vault name
+        parts = id.rsplit(u'/', 1)
+
+        # return vault name and parent ID
+        return (parts[1], parts[0] + u'/')
+
+    def get_kra_id(self, id):
+        __doc__ = _("""
+        Generates a client key ID to store/retrieve data in KRA.
+        """)
+        return 'ipa' + id
+
+
+@register()
+class vault_add(LDAPCreate):
+    __doc__ = _('Create a new vault.')
+
+    takes_options = LDAPRetrieve.takes_options + (
+        Bytes('data?',
+            cli_name='data',
+            doc=_('Base-64 encoded binary data to archive'),
+        ),
+        Str('text?',
+            cli_name='text',
+            doc=_('Text data to archive'),
+        ),
+        Str('in?',
+            cli_name='in',
+            doc=_('File containing data to archive'),
+        ),
+    )
+
+    msg_summary = _('Added vault "%(value)s"')
+
+    def forward(self, *args, **options):
+
+        vault_name = args[0]
+        parent_id = 
api.Object.vaultcontainer.normalize_id(options.get('parent'))
+
+        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:
+                raise errors.ValidationError(name='text',
+                    error=_('Input data already specified'))
+
+            if input_file:
+                raise errors.ValidationError(name='input_file',
+                    error=_('Input data already specified'))
+
+        elif text:
+            if input_file:
+                raise errors.ValidationError(name='input_file',
+                    error=_('Input data already specified'))
+
+            data = text.encode('utf-8')
+
+        elif input_file:
+            with open(input_file, 'rb') as f:
+                data = f.read()
+
+        else:
+            data = ''
+
+        # create the vault
+        response = super(vault_add, self).forward(*args, **options)
+
+        # archive initial data
+        api.Command.vault_archive(
+            vault_name,
+            parent=parent_id,
+            data=data)
+
+        return response
+
+    def pre_callback(self, ldap, dn, entry_attrs, attrs_list, *keys, 
**options):
+        assert isinstance(dn, DN)
+
+        # add parent containers if they don't exist
+        try:
+            ldap = self.obj.backend
+
+            while True:
+                parent_dn = DN(*dn[1:])
+                parent_rdn = parent_dn[0]
+
+                parent = ldap.make_entry(
+                    parent_dn,
+                    {
+                        'objectclass': api.Object.vaultcontainer.object_class,
+                        'cn': parent_rdn['cn'],
+                    })
+
+                ldap.add_entry(parent)
+
+        except errors.DuplicateEntry:
+            pass
+
+        return dn
+
+    def post_callback(self, ldap, dn, entry_attrs, *keys, **options):
+        assert isinstance(dn, DN)
+
+        entry_attrs['vault_id'] = self.obj.get_id(dn)
+
+        return dn
+
+
+@register()
+class vault_del(LDAPDelete):
+    __doc__ = _('Delete a vault.')
+
+    msg_summary = _('Deleted vault "%(value)s"')
+
+    takes_options = LDAPDelete.takes_options + (
+        Str('parent?',
+            cli_name='parent',
+            doc=_('Parent container'),
+        ),
+    )
+
+    def post_callback(self, ldap, dn, *keys, **options):
+        assert isinstance(dn, DN)
+
+        vault_id = self.obj.get_id(dn)
+
+        kra_client = api.Backend.kra.get_client()
+
+        kra_account = pki.account.AccountClient(kra_client.connection)
+        kra_account.login()
+
+        client_key_id = self.api.Object.vault.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
+
+
+@register()
+class vault_find(LDAPSearch):
+    __doc__ = _('Search for vaults.')
+
+    msg_summary = ngettext(
+        '%(count)d vault matched', '%(count)d vaults matched', 0
+    )
+
+    def pre_callback(self, ldap, filter, attrs_list, base_dn, scope, *keys, 
**options):
+        assert isinstance(base_dn, DN)
+
+        parent_id = 
self.Object.vaultcontainer.normalize_id(options.get('parent'))
+        base_dn = self.Object.vaultcontainer.get_dn(parent=parent_id)
+
+        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
+
+
+@register()
+class vault_mod(LDAPUpdate):
+    __doc__ = _('Modify a vault.')
+
+    msg_summary = _('Modified vault "%(value)s"')
+
+    def post_callback(self, ldap, dn, entry_attrs, *keys, **options):
+        assert isinstance(dn, DN)
+
+        entry_attrs['vault_id'] = self.obj.get_id(dn)
+
+        return dn
+
+
+@register()
+class vault_show(LDAPRetrieve):
+    __doc__ = _('Display information about a vault.')
+
+    takes_options = LDAPRetrieve.takes_options + (
+        Str('parent?',
+            cli_name='parent',
+            doc=_('Parent container'),
+        ),
+    )
+
+    def post_callback(self, ldap, dn, entry_attrs, *keys, **options):
+        assert isinstance(dn, DN)
+
+        entry_attrs['vault_id'] = self.obj.get_id(dn)
+
+        return dn
+
+
+@register()
+class vault_archive(LDAPRetrieve):
+    __doc__ = _('Archive data into a vault.')
+
+    takes_options = LDAPRetrieve.takes_options + (
+        Str('parent?',
+            cli_name='parent',
+            doc=_('Parent container'),
+        ),
+        Bytes('data?',
+            cli_name='data',
+            doc=_('Base-64 encoded binary data to archive'),
+        ),
+        Str('text?',
+            cli_name='text',
+            doc=_('Text data to archive'),
+        ),
+        Str('in?',
+            cli_name='in',
+            doc=_('File containing data to archive'),
+        ),
+        Bytes('wrapped_session_key?',
+            cli_name='wrapped_session_key',
+            doc=_('Session key wrapped with transport certificate and encoded 
in base-64'),
+        ),
+        Bytes('encrypted_data?',
+            cli_name='encrypted_data',
+            doc=_('Data encrypted with session key and encoded in base-64'),
+        ),
+        Bytes('nonce?',
+            cli_name='nonce',
+            doc=_('Nonce encrypted encoded in base-64'),
+        ),
+    )
+
+    msg_summary = _('Archived data into vault "%(value)s"')
+
+    def forward(self, *args, **options):
+
+        vault_name = args[0]
+        parent_id = 
api.Object.vaultcontainer.normalize_id(options.get('parent'))
+
+        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:
+                raise errors.ValidationError(name='text',
+                    error=_('Input data already specified'))
+
+            if input_file:
+                raise errors.ValidationError(name='input_file',
+                    error=_('Input data already specified'))
+
+        elif text:
+            if input_file:
+                raise errors.ValidationError(name='input_file',
+                    error=_('Input data already specified'))
+
+            data = text.encode('utf-8')
+
+        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
+
+        # generate session key
+        session_key = crypto.generate_session_key()
+
+        # retrieve transport certificate
+        nss_transport_cert = crypto.get_cert(transport_cert_nickname)
+
+        # wrap session key with transport certificate
+        wrapped_session_key = crypto.asymmetric_wrap(
+            session_key,
+            nss_transport_cert
+        )
+
+        # encrypt data with session key
+        nonce = crypto.generate_nonce_iv()
+        encrypted_data = crypto.symmetric_wrap(
+            data,
+            session_key,
+            nonce_iv=nonce
+        )
+
+        # send archival request to server
+        options['wrapped_session_key'] = base64.b64encode(wrapped_session_key)
+        options['encrypted_data'] = base64.b64encode(encrypted_data)
+        options['nonce'] = base64.b64encode(nonce)
+
+        return super(vault_archive, self).forward(*args, **options)
+
+    def post_callback(self, ldap, dn, entry_attrs, *keys, **options):
+        assert isinstance(dn, DN)
+
+        vault_id = self.obj.get_id(dn)
+        entry_attrs['vault_id'] = vault_id
+
+        # connect to KRA
+        kra_client = api.Backend.kra.get_client()
+
+        kra_account = pki.account.AccountClient(kra_client.connection)
+        kra_account.login()
+
+        client_key_id = self.api.Object.vault.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['wrapped_session_key'])
+        encrypted_data = base64.b64decode(options['encrypted_data'])
+        nonce = base64.b64decode(options['nonce'])
+
+        # forward encrypted data to KRA
+        kra_client.keys.archive_encrypted_data(
+            client_key_id,
+            pki.key.KeyClient.PASS_PHRASE_TYPE,
+            encrypted_data,
+            wrapped_session_key,
+            None,
+            nonce,
+        )
+
+        kra_account.logout()
+
+        return dn
+
+
+@register()
+class vault_retrieve(LDAPRetrieve):
+    __doc__ = _('Retrieve a data from a vault.')
+
+    takes_options = LDAPRetrieve.takes_options + (
+        Str('parent?',
+            cli_name='parent',
+            doc=_('Parent container'),
+        ),
+        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'),
+        ),
+        Bytes('wrapped_session_key?',
+            cli_name='wrapped_session_key',
+            doc=_('Session key wrapped with transport certificate and encoded 
in base-64'),
+        ),
+    )
+
+    has_output_params = (
+        Bytes('data',
+            label=_('Data'),
+        ),
+        Bytes('text',
+            label=_('Text'),
+        ),
+    )
+
+    msg_summary = _('Retrieved data from vault "%(value)s"')
+
+    def forward(self, *args, **options):
+
+        vault_name = args[0]
+        parent_id = 
api.Object.vaultcontainer.normalize_id(options.get('parent'))
+
+        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
+        nss_transport_cert = crypto.get_cert(transport_cert_nickname)
+
+        # wrap session key with transport certificate
+        wrapped_session_key = crypto.asymmetric_wrap(
+            session_key,
+            nss_transport_cert
+        )
+
+        # send retrieval request to server
+        options['wrapped_session_key'] = base64.b64encode(wrapped_session_key)
+
+        response = super(vault_retrieve, self).forward(*args, **options)
+
+        encrypted_data = base64.b64decode(response['result']['encrypted_data'])
+        nonce = base64.b64decode(response['result']['nonce'])
+
+        # decrypt encrypted data with session key
+        data = crypto.symmetric_unwrap(
+            encrypted_data,
+            session_key,
+            nonce_iv=nonce)
+
+        if stdout:
+            print 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 post_callback(self, ldap, dn, entry_attrs, *keys, **options):
+        assert isinstance(dn, DN)
+
+        vault_id = self.obj.get_id(dn)
+        entry_attrs['vault_id'] = vault_id
+
+        wrapped_session_key = base64.b64decode(options['wrapped_session_key'])
+
+        # connect to KRA
+        kra_client = api.Backend.kra.get_client()
+
+        kra_account = pki.account.AccountClient(kra_client.connection)
+        kra_account.login()
+
+        client_key_id = self.api.Object.vault.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)
+
+        entry_attrs['encrypted_data'] = base64.b64encode(key.encrypted_data)
+        entry_attrs['nonce'] = base64.b64encode(key.nonce_data)
+
+        kra_account.logout()
+
+        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..01896ce1811dd8b3829bc266ff288494fdb3746f
--- /dev/null
+++ b/ipatests/test_xmlrpc/test_vault_plugin.py
@@ -0,0 +1,614 @@
+# Authors:
+#   Endi S. Dewata <edew...@redhat.com>
+#
+# Copyright (C) 2014  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
+
+private_container = u'private_container'
+shared_container = u'shared_container'
+service_container = u'service_container'
+
+base_container = u'base_container'
+child_container = u'child_container'
+grandchild_container = u'grandchild_container'
+
+private_vault = u'private_vault'
+
+class test_vault(Declarative):
+
+    cleanup_commands = [
+        ('vaultcontainer_del', [private_container], {'continue': True}),
+        ('vaultcontainer_del', [shared_container], {'parent': u'/shared/', 
'continue': True}),
+        ('vaultcontainer_del', [service_container], {'parent': u'/services/', 
'continue': True}),
+        ('vaultcontainer_del', [base_container], {'force': True, 'continue': 
True}),
+        ('vault_del', [private_vault], {'continue': True}),
+    ]
+
+    tests = [
+
+        {
+            'desc': 'Find top-level containers',
+            'command': (
+                'vaultcontainer_find',
+                [],
+                {
+                    'parent': 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_container],
+                {
+                    'parent': u'/shared/',
+                },
+            ),
+            'expected': {
+                'value': shared_container,
+                'summary': 'Added vault container "%s"' % shared_container,
+                'result': {
+                    'dn': u'cn=%s,cn=shared,cn=vaults,%s' % (shared_container, 
api.env.basedn),
+                    'objectclass': (u'ipaVaultContainer', u'top'),
+                    'cn': [shared_container],
+                    'container_id': u'/shared/%s/' % shared_container,
+                },
+            },
+        },
+
+        {
+            'desc': 'Find shared containers',
+            'command': (
+                'vaultcontainer_find',
+                [],
+                {
+                    'parent': u'/shared/',
+                },
+            ),
+            'expected': {
+                'count': 1,
+                'truncated': False,
+                'summary': u'1 vault container matched',
+                'result': [
+                    {
+                        'dn': u'cn=%s,cn=shared,cn=vaults,%s' % 
(shared_container, api.env.basedn),
+                        'cn': [shared_container],
+                        'container_id': u'/shared/%s/' % shared_container,
+                    },
+                ],
+            },
+        },
+
+        {
+            'desc': 'Show shared container',
+            'command': (
+                'vaultcontainer_show',
+                [shared_container],
+                {
+                    'parent': u'/shared/',
+                },
+            ),
+            'expected': {
+                'value': shared_container,
+                'summary': None,
+                'result': {
+                    'dn': u'cn=%s,cn=shared,cn=vaults,%s' % (shared_container, 
api.env.basedn),
+                    'cn': [shared_container],
+                    'container_id': u'/shared/%s/' % shared_container,
+                },
+            },
+        },
+
+        {
+            'desc': 'Modify shared container',
+            'command': (
+                'vaultcontainer_mod',
+                [shared_container],
+                {
+                    'parent': u'/shared/',
+                    'description': u'shared container',
+                },
+            ),
+            'expected': {
+                'value': shared_container,
+                'summary': 'Modified vault container "%s"' % shared_container,
+                'result': {
+                    'cn': [shared_container],
+                    'container_id': u'/shared/%s/' % shared_container,
+                    'description': [u'shared container'],
+                },
+            },
+        },
+
+        {
+            'desc': 'Delete shared container',
+            'command': (
+                'vaultcontainer_del',
+                [shared_container],
+                {
+                    'parent': u'/shared/',
+                },
+            ),
+            'expected': {
+                'value': [shared_container],
+                'summary': u'Deleted vault container "%s"' % shared_container,
+                'result': {
+                    'failed': (),
+                },
+            },
+        },
+
+        {
+            'desc': 'Create service container',
+            'command': (
+                'vaultcontainer_add',
+                [service_container],
+                {
+                    'parent': u'/services/',
+                },
+            ),
+            'expected': {
+                'value': service_container,
+                'summary': 'Added vault container "%s"' % service_container,
+                'result': {
+                    'dn': u'cn=%s,cn=services,cn=vaults,%s' % 
(service_container, api.env.basedn),
+                    'objectclass': (u'ipaVaultContainer', u'top'),
+                    'cn': [service_container],
+                    'container_id': u'/services/%s/' % service_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': 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': 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(error=u'Not allowed on 
non-leaf entry'),
+        },
+
+        {
+            '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': 'Create private vault',
+            'command': (
+                'vault_add',
+                [private_vault],
+                {},
+            ),
+            'expected': {
+                'value': private_vault,
+                'summary': 'Added vault "%s"' % private_vault,
+                'result': {
+                    'dn': u'cn=%s,cn=admin,cn=users,cn=vaults,%s' % 
(private_vault, api.env.basedn),
+                    'objectclass': (u'ipaVault', u'top'),
+                    'cn': [private_vault],
+                    'vault_id': u'/users/admin/%s' % private_vault,
+                },
+            },
+        },
+
+        {
+            'desc': 'Create duplicate vault',
+            'command': (
+                'vault_add',
+                [private_vault],
+                {},
+            ),
+            'expected': errors.DuplicateEntry(message=u'vault with name "%s" 
already exists' % private_vault),
+        },
+
+        {
+            'desc': 'Find private vaults',
+            'command': (
+                'vault_find',
+                [],
+                {},
+            ),
+            'expected': {
+                'count': 1,
+                'truncated': False,
+                'summary': u'1 vault matched',
+                'result': [
+                    {
+                        'dn': u'cn=%s,cn=admin,cn=users,cn=vaults,%s' % 
(private_vault, api.env.basedn),
+                        'cn': [private_vault],
+                        'vault_id': u'/users/admin/%s' % private_vault,
+                    },
+                ],
+            },
+        },
+
+        {
+            'desc': 'Show private vault',
+            'command': (
+                'vault_show',
+                [private_vault],
+                {},
+            ),
+            'expected': {
+                'value': private_vault,
+                'summary': None,
+                'result': {
+                    'dn': u'cn=%s,cn=admin,cn=users,cn=vaults,%s' % 
(private_vault, api.env.basedn),
+                    'cn': [private_vault],
+                    'vault_id': u'/users/admin/%s' % private_vault,
+                },
+            },
+        },
+
+        {
+            'desc': 'Modify private vault',
+            'command': (
+                'vault_mod',
+                [private_vault],
+                {
+                    'description': u'Private vault',
+                },
+            ),
+            'expected': {
+                'value': private_vault,
+                'summary': u'Modified vault "%s"' % private_vault,
+                'result': {
+                    'cn': [private_vault],
+                    'vault_id': u'/users/admin/%s' % private_vault,
+                    'description': [u'Private vault'],
+                },
+            },
+        },
+
+        {
+            'desc': 'Archive binary data',
+            'command': (
+                'vault_archive',
+                [private_vault],
+                {
+                    'data': 'c2JVcmV0',
+                },
+            ),
+            'expected': {
+                'value': private_vault,
+                'summary': u'Archived data into vault "%s"' % private_vault,
+                'result': {
+                    'dn': u'cn=%s,cn=admin,cn=users,cn=vaults,%s' % 
(private_vault, api.env.basedn),
+                    'cn': [private_vault],
+                    'vault_id': u'/users/admin/%s' % private_vault,
+                    'description': [u'Private vault'],
+                },
+            },
+        },
+
+        {
+            'desc': 'Retrieve binary data',
+            'command': (
+                'vault_retrieve',
+                [private_vault],
+                {},
+            ),
+            'expected': {
+                'value': private_vault,
+                'summary': u'Retrieved data from vault "%s"' % private_vault,
+                'result': {
+                    'dn': u'cn=%s,cn=admin,cn=users,cn=vaults,%s' % 
(private_vault, api.env.basedn),
+                    'cn': [private_vault],
+                    'vault_id': u'/users/admin/%s' % private_vault,
+                    'description': [u'Private vault'],
+                    'nonce': fuzzy_string,
+                    'encrypted_data': fuzzy_string,
+                    'data': 'c2JVcmV0',
+                },
+            },
+        },
+
+        {
+            'desc': 'Archive text data',
+            'command': (
+                'vault_archive',
+                [private_vault],
+                {
+                    'text': u'secret',
+                },
+            ),
+            'expected': {
+                'value': private_vault,
+                'summary': u'Archived data into vault "%s"' % private_vault,
+                'result': {
+                    'dn': u'cn=%s,cn=admin,cn=users,cn=vaults,%s' % 
(private_vault, api.env.basedn),
+                    'cn': [private_vault],
+                    'vault_id': u'/users/admin/%s' % private_vault,
+                    'description': [u'Private vault'],
+                },
+            },
+        },
+
+        {
+            'desc': 'Retrieve text data',
+            'command': (
+                'vault_retrieve',
+                [private_vault],
+                {
+                    'show_text': True,
+                },
+            ),
+            'expected': {
+                'value': private_vault,
+                'summary': u'Retrieved data from vault "%s"' % private_vault,
+                'result': {
+                    'dn': u'cn=%s,cn=admin,cn=users,cn=vaults,%s' % 
(private_vault, api.env.basedn),
+                    'cn': [private_vault],
+                    'vault_id': u'/users/admin/%s' % private_vault,
+                    'description': [u'Private vault'],
+                    'nonce': fuzzy_string,
+                    'encrypted_data': fuzzy_string,
+                    'text': u'secret',
+                },
+            },
+        },
+
+        {
+            'desc': 'Delete private vault',
+            'command': (
+                'vault_del',
+                [private_vault],
+                {},
+            ),
+            'expected': {
+                'value': [private_vault],
+                'summary': u'Deleted vault "%s"' % private_vault,
+                'result': {
+                    'failed': (),
+                },
+            },
+        },
+
+        {
+            'desc': 'Delete non-existent vault',
+            'command': (
+                'vault_del',
+                [private_vault],
+                {},
+            ),
+            'expected': errors.NotFound(reason=u'%s: vault not found' % 
private_vault),
+        },
+    ]
-- 
1.9.0

_______________________________________________
Freeipa-devel mailing list
Freeipa-devel@redhat.com
https://www.redhat.com/mailman/listinfo/freeipa-devel

Reply via email to