Please take a look at the attached patch (#353-9). It obsoletes all previous patches. See comments below.

On 4/20/2015 1:12 AM, Jan Cholasta wrote:
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.


OK.

The vault container plugin has been removed instead of merged (see explanation below). Internally the vaults are still stored in built-in containers in the DS, but there won't be an interface to manage them. The following containers are available for use: private, shared, and services, but they are flat, not hierarchical.

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.

In fact it is not consistent with other plugins. All entries managed by
the user plugin are stored *directly* under cn=users. Entries managed by
the vault plugin are not stored directly under cn=vaults, but rather
anywhere in the cn=vaults subtree and their DN is derived from the DN of
the parent vault container. For such objects, we don't set
<plugin>.container_dn and don't have container_<plugin> constant, but
rather define them as child objects of their container objects.

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

OK.

The vaults are still stored in the cn=vaults subtree, but now the containers will use nsContainer instead of ipaVaultContainer. The container_vault variable still defines the root container DN, i.e. cn=vaults.

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.

It indeed is limiting and that's a good thing. We don't want people to
be able to create any crazy interfaces they can imagine, inconsistent
with everything else in IPA.

So the vault container plugin was removed because the current framework cannot support the hierarchical structure described in the vault design without overriding the default parameter handling (which was referred to as 'hacks', although it was actually suggested by the previous reviewer). Adding the missing functionality will require modifications to the base framework classes. Such changes should only be done after thoroughly evaluating the impact on the existing plugins, probably in a future release.

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.

I didn't mean empty string specifically, it could have been any special
value.

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.

I'm sorry if I gave you the impression that this is up for discussion,
but it is not. You either follow the convention without doing ugly hacks
or your patch will not be accepted.

It won't be confusing to users, because they are used to the convention.

Since the vault container plugin is removed, the hierarchical vault ID no longer needed, so this point is irrelevant now.

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

Fine by me, as long as you follow the convention.

The vault is now accessed using the name and the container:
* private vault: ipa vault-show <name>
* shared vault:  ipa vault-show <name> --shared
* service vault: ipa vault-show <name> --host <hostname>

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).

If that's just an implementation detail, OK.

If it's possible to modify existing vault objects using vault-add or
create new objects using vault-archive, then NACK.

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.

A command with two sets of mutually exclusive options is really two
commands in disguise, which is a good sign it should be divided into two
actual commands.

Who are you to say users would get confused? I say they would be more
confused by a command with a plethora of mutually exclusive "options".

What other possible data types are there?

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.

It's similar to group members because there are separate commands to
manipulate them.

You have to choose one of:

   a) vault data is settable using vault-add and vault-mod and gettable
using vault-mod, vault-show and vault-find

   b) vault data is settable using vault-archive and gettable using
vault-retrieve

Anything in between is not permitted.

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

All archival/retrieval stuff will be addressed in a separate patch after the current patch (i.e. the basic structure & interface) is finalized.

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.

NACK, this change exists for the sole reason of supporting your hacks.
Follow IPA convetions and this change won't be necessary.

This is no longer relevant.

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.

NACK, it's not a LDAPQuery operation, because it has different
semantics. There is more to a command than executing code, so you should
use a correct base class.

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 you read a little bit further, you would get to the point, which is
certainly not calling .decode() without arguments, but *always
explicitly specify the encoding*.

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.

You are confusing user's configured encoding with Python's default
encoding. Default encoding in Python isn't derived from user's
localization settings.

Anyway, anything that comes from user input is already decoded using
user's configured encoding when it enters the framework so I don't know
why are you even bringing it up here.

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.

The result of decoding binary data using raw_unicode_escape is perfectly
valid unicode data which doesn't eat 33% more space like base64 encoded
binary does, hence my suggestion.

Anyway, it turns out when encoded in JSON, raw_unicode_escape string
generally takes more space than base64 encoded string because of JSON
escaping, so base64 is indeed better.

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()

See what the default encoding actually is and why you shouldn't rely on
it above.

     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')

Same thing, all archival/retrieval stuff will be dealt with separately later.

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.

NACK, turns out there is a better (and preferable) solution I didn't
remember before, you can use exception callback in vaultcontainer_del:

     def exc_callback(self, keys, options, exc, call_func, *call_args,
**call_kwargs):
         if call_func.func_name == 'delete_entry':
             if isinstance(exc, errors.NotAllowedOnLeaf):
                 if not options.get('force', False):
                     raise errors.DatabaseError(...)
         raise exc

This is irrelevant too.

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.

OK.

Now irrelevant too.

--
Endi S. Dewata
>From dd39abebe083cc265b2eb6674cd602253993dd8f 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 vault plugin.

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

https://fedorahosted.org/freeipa/ticket/3872
---
 API.txt                                   |  69 ++++++
 install/share/60basev3.ldif               |   1 +
 install/updates/40-vault.update           |  19 ++
 install/updates/Makefile.am               |   1 +
 ipa-client/man/default.conf.5             |   1 +
 ipalib/constants.py                       |   1 +
 ipalib/plugins/vault.py                   | 320 ++++++++++++++++++++++++++++
 ipatests/test_xmlrpc/test_vault_plugin.py | 338 ++++++++++++++++++++++++++++++
 8 files changed, 750 insertions(+)
 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 
f747765d7f9c87761fed0277cd59d1bc3fbd57e9..adb1116569a22b04a9ee6ad567166dfb3aca99ef
 100644
--- a/API.txt
+++ b/API.txt
@@ -4562,6 +4562,75 @@ 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,8,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: Str('addattr*', cli_name='addattr', exclude='webui')
+option: Flag('all', autofill=True, cli_name='all', default=False, 
exclude='webui')
+option: Str('description', attribute=True, cli_name='desc', multivalue=False, 
required=False)
+option: Str('host?')
+option: Flag('raw', autofill=True, cli_name='raw', default=False, 
exclude='webui')
+option: Str('setattr*', cli_name='setattr', exclude='webui')
+option: Flag('shared?', 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_del
+args: 1,4,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('host?')
+option: Flag('shared?', autofill=True, default=False)
+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('host?')
+option: Flag('pkey_only?', autofill=True, default=False)
+option: Flag('raw', autofill=True, cli_name='raw', default=False, 
exclude='webui')
+option: Flag('shared?', autofill=True, default=False)
+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: 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('host?')
+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: Flag('shared?', 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,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: Str('host?')
+option: Flag('raw', autofill=True, cli_name='raw', default=False, 
exclude='webui')
+option: Flag('rights', autofill=True, default=False)
+option: Flag('shared?', 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..929b25d5ff739e0275f7b0cdb2e2cac6d026f083
 100644
--- a/install/share/60basev3.ldif
+++ b/install/share/60basev3.ldif
@@ -77,3 +77,4 @@ objectClasses: (2.16.840.1.113730.3.8.12.24 NAME 
'ipaPublicKeyObject' DESC 'Wrap
 objectClasses: (2.16.840.1.113730.3.8.12.25 NAME 'ipaPrivateKeyObject' DESC 
'Wrapped private keys' SUP top AUXILIARY MUST ( ipaPrivateKey $ ipaWrappingKey 
$ ipaWrappingMech ) X-ORIGIN 'IPA v4.1' )
 objectClasses: (2.16.840.1.113730.3.8.12.26 NAME 'ipaSecretKeyObject' DESC 
'Wrapped secret keys' SUP top AUXILIARY MUST ( ipaSecretKey $ ipaWrappingKey $ 
ipaWrappingMech ) X-ORIGIN 'IPA v4.1' )
 objectClasses: (2.16.840.1.113730.3.8.12.34 NAME 'ipaSecretKeyRefObject' DESC 
'Indirect storage for encoded key material' SUP top AUXILIARY MUST ( 
ipaSecretKeyRef ) X-ORIGIN 'IPA v4.1' )
+objectClasses: (2.16.840.1.113730.3.8.18.1.1 NAME 'ipaVault' DESC 'IPA vault' 
SUP top STRUCTURAL MUST ( cn ) MAY ( description ) X-ORIGIN 'IPA v4.2' )
diff --git a/install/updates/40-vault.update b/install/updates/40-vault.update
new file mode 100644
index 
0000000000000000000000000000000000000000..5a6b8c6a022fa56e5a5bc05369ce143d39644092
--- /dev/null
+++ b/install/updates/40-vault.update
@@ -0,0 +1,19 @@
+dn: cn=vaults,$SUFFIX
+default: objectClass: top
+default: objectClass: nsContainer
+default: cn: vaults
+
+dn: cn=services,cn=vaults,$SUFFIX
+default: objectClass: top
+default: objectClass: nsContainer
+default: cn: services
+
+dn: cn=shared,cn=vaults,$SUFFIX
+default: objectClass: top
+default: objectClass: nsContainer
+default: cn: shared
+
+dn: cn=users,cn=vaults,$SUFFIX
+default: objectClass: top
+default: objectClass: nsContainer
+default: cn: users
diff --git a/install/updates/Makefile.am b/install/updates/Makefile.am
index 
0d63d9ea8d85f1add5f036e7a39f89543586d33b..66f6b9d37971f8b8501d73fc6ddca21b6686ff4b
 100644
--- a/install/updates/Makefile.am
+++ b/install/updates/Makefile.am
@@ -33,6 +33,7 @@ app_DATA =                            \
        40-dns.update                   \
        40-automember.update            \
        40-otp.update                   \
+       40-vault.update                 \
        45-roles.update                 \
        50-7_bit_check.update           \
        50-dogtag10-migration.update    \
diff --git a/ipa-client/man/default.conf.5 b/ipa-client/man/default.conf.5
index 
dbc8a5b4647439de4de7c01152d098eb0561e236..0973f1a07179ad64daa326a02803cdc9ba1870aa
 100644
--- a/ipa-client/man/default.conf.5
+++ b/ipa-client/man/default.conf.5
@@ -221,6 +221,7 @@ The following define the containers for the IPA server. 
Containers define where
   container_sudocmdgroup: cn=sudocmdgroups,cn=sudo
   container_sudorule: cn=sudorules,cn=sudo
   container_user: cn=users,cn=accounts
+  container_vault: cn=vaults
   container_virtual: cn=virtual operations,cn=etc
 
 .SH "FILES"
diff --git a/ipalib/constants.py b/ipalib/constants.py
index 
f1e14702ffdf5a3bd23a62b1fdd2ee3cd95d84f8..195938a355d1b24c02aa0a5833c1725c76e85c76
 100644
--- a/ipalib/constants.py
+++ b/ipalib/constants.py
@@ -99,6 +99,7 @@ DEFAULT_CONFIG = (
     ('container_hbacservice', DN(('cn', 'hbacservices'), ('cn', 'hbac'))),
     ('container_hbacservicegroup', DN(('cn', 'hbacservicegroups'), ('cn', 
'hbac'))),
     ('container_dns', DN(('cn', 'dns'))),
+    ('container_vault', DN(('cn', 'vaults'))),
     ('container_virtual', DN(('cn', 'virtual operations'), ('cn', 'etc'))),
     ('container_sudorule', DN(('cn', 'sudorules'), ('cn', 'sudo'))),
     ('container_sudocmd', DN(('cn', 'sudocmds'), ('cn', 'sudo'))),
diff --git a/ipalib/plugins/vault.py b/ipalib/plugins/vault.py
new file mode 100644
index 
0000000000000000000000000000000000000000..020e3045fce76210ecbacc8c967d3bdff29d9209
--- /dev/null
+++ b/ipalib/plugins/vault.py
@@ -0,0 +1,320 @@
+# Authors:
+#   Endi S. Dewata <edew...@redhat.com>
+#
+# Copyright (C) 2015  Red Hat
+# see file 'COPYING' for use and warranty information
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program.  If not, see <http://www.gnu.org/licenses/>.
+
+from ipalib import api, errors
+from ipalib import Str, Flag
+from ipalib import output
+from ipalib.plugable import Registry
+from ipalib.plugins.baseldap import LDAPObject, LDAPCreate, LDAPDelete,\
+    LDAPSearch, LDAPUpdate, LDAPRetrieve
+from ipalib.request import context
+from ipalib.plugins.user import split_principal
+from ipalib import _, ngettext
+from ipapython.dn import DN
+
+__doc__ = _("""
+Vaults
+""") + _("""
+Manage vaults.
+""") + _("""
+EXAMPLES:
+""") + _("""
+ List private vaults:
+   ipa vault-find
+""") + _("""
+ List shared vaults:
+   ipa vault-find --shared
+""") + _("""
+ List service vaults:
+   ipa vault-find --host <hostname>
+""") + _("""
+ Add a private vault:
+   ipa vault-add <vault name>
+""") + _("""
+ Add a shared vault:
+   ipa vault-add <vault name> --shared
+""") + _("""
+ Add a service vault:
+   ipa vault-add <vault name> --host <hostname>
+""") + _("""
+ Show a private vault:
+   ipa vault-show <vault name>
+""") + _("""
+ Show a shared vault:
+   ipa vault-show <vault name> --shared
+""") + _("""
+ Show a service vault:
+   ipa vault-show <vault name> --host <hostname>
+""") + _("""
+ Modify a private vault:
+   ipa vault-mod <vault name> --desc <description>
+""") + _("""
+ Modify a shared vault:
+   ipa vault-mod <vault name> --shared --desc <description>
+""") + _("""
+ Modify a service vault:
+   ipa vault-mod <vault name> --host <hostname> --desc <description>
+""") + _("""
+ Delete a private vault:
+   ipa vault-del <vault name>
+""") + _("""
+ Delete a shared vault:
+   ipa vault-del <vault name> --shared
+""") + _("""
+ Delete a service vault:
+   ipa vault-del <vault name> --host <hostname>
+""")
+
+register = Registry()
+
+
+@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',
+        'description',
+    ]
+    search_display_attributes = [
+        'cn',
+        '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(
+            'description?',
+            cli_name='desc',
+            label=_('Description'),
+            doc=_('Vault description'),
+        ),
+    )
+
+    def get_dn(self, *args, **options):
+        """
+        Generates vault DN from parameters.
+        """
+
+        vault_name = args[0]
+
+        host = options.get('host')
+        shared = options.get('shared')
+
+        dn = self.base_dn
+
+        if host and shared:
+            raise errors.MutuallyExclusiveError(
+                reason=_('Host and shared options ' +
+                         'cannot be specified simultaneously'))
+
+        elif host:
+            dn = DN(('cn', host), ('cn', 'services'), dn)
+
+        elif shared:
+            dn = DN(('cn', 'shared'), dn)
+
+        else:
+            principal = getattr(context, 'principal')
+            (username, realm) = split_principal(principal)
+            dn = DN(('cn', username), ('cn', 'users'), dn)
+
+        if vault_name:
+            dn = DN(('cn', vault_name), dn)
+
+        return dn
+
+    def create_container(self, dn):
+        """
+        Creates vault container and its parents.
+        """
+
+        rdn = dn[0]
+        entry = self.backend.make_entry(
+            dn,
+            {
+                'objectclass': ['nsContainer'],
+                'cn': rdn['cn'],
+            })
+
+        # if entry can be added, return
+        try:
+            self.backend.add_entry(entry)
+            return
+
+        except errors.NotFound:
+            pass
+
+        # otherwise, create parent entry first
+        parent_dn = DN(*dn[1:])
+        self.create_container(parent_dn)
+
+        # then create the entry itself again
+        self.backend.add_entry(entry)
+
+
+@register()
+class vault_add(LDAPCreate):
+    __doc__ = _('Create a new vault.')
+
+    takes_options = LDAPCreate.takes_options + (
+        Str(
+            'host?',
+            doc=_('Service hostname'),
+        ),
+        Flag(
+            'shared?',
+            doc=_('Shared vault'),
+        ),
+    )
+
+    has_output = output.standard_entry
+
+    msg_summary = _('Added vault "%(value)s"')
+
+    def pre_callback(
+            self, ldap, dn, entry_attrs, attrs_list,
+            *keys, **options):
+        assert isinstance(dn, DN)
+
+        try:
+            parent_dn = DN(*dn[1:])
+            self.obj.create_container(parent_dn)
+
+        except errors.DuplicateEntry, e:
+            pass
+
+        return dn
+
+
+@register()
+class vault_del(LDAPDelete):
+    __doc__ = _('Delete a vault.')
+
+    takes_options = LDAPDelete.takes_options + (
+        Str(
+            'host?',
+            doc=_('Service hostname'),
+        ),
+        Flag(
+            'shared?',
+            doc=_('Shared vault'),
+        ),
+    )
+
+    msg_summary = _('Deleted vault "%(value)s"')
+
+
+@register()
+class vault_find(LDAPSearch):
+    __doc__ = _('Search for vaults.')
+
+    takes_options = LDAPSearch.takes_options + (
+        Str(
+            'host?',
+            doc=_('Service hostname'),
+        ),
+        Flag(
+            'shared?',
+            doc=_('Shared vault'),
+        ),
+    )
+
+    msg_summary = ngettext(
+        '%(count)d vault matched',
+        '%(count)d vaults matched',
+        0,
+    )
+
+    def pre_callback(
+            self, ldap, filter, attrs_list, base_dn, scope,
+            *args, **options):
+
+        assert isinstance(base_dn, DN)
+
+        base_dn = self.obj.get_dn(*args, **options)
+
+        return (filter, base_dn, scope)
+
+    def handle_not_found(self, *args, **options):
+
+        host = options.get('host')
+        shared = options.get('shared')
+
+        # if private container has not been created, ignore
+        if not host and not shared:
+            return
+
+        # otherwise, raise an error
+        raise errors.NotFound(
+            reason=_('Container does not exist')
+        )
+
+
+@register()
+class vault_mod(LDAPUpdate):
+    __doc__ = _('Modify a vault.')
+
+    takes_options = LDAPUpdate.takes_options + (
+        Str(
+            'host?',
+            doc=_('Service hostname'),
+        ),
+        Flag(
+            'shared?',
+            doc=_('Shared vault'),
+        ),
+    )
+
+    msg_summary = _('Modified vault "%(value)s"')
+
+
+@register()
+class vault_show(LDAPRetrieve):
+    __doc__ = _('Display information about a vault.')
+
+    takes_options = LDAPRetrieve.takes_options + (
+        Str(
+            'host?',
+            doc=_('Service hostname'),
+        ),
+        Flag(
+            'shared?',
+            doc=_('Shared vault'),
+        ),
+    )
diff --git a/ipatests/test_xmlrpc/test_vault_plugin.py 
b/ipatests/test_xmlrpc/test_vault_plugin.py
new file mode 100644
index 
0000000000000000000000000000000000000000..4bc1a930377957a7a822b8809b475e14b7facd74
--- /dev/null
+++ b/ipatests/test_xmlrpc/test_vault_plugin.py
@@ -0,0 +1,338 @@
+# Authors:
+#   Endi S. Dewata <edew...@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'
+hostname = u'server.example.com'
+
+
+class test_vault_plugin(Declarative):
+
+    cleanup_commands = [
+        ('vault_del', [test_vault], {'continue': True}),
+        ('vault_del', [test_vault], {'shared': True, 'continue': True}),
+        ('vault_del', [test_vault], {'host': hostname, 'continue': True}),
+    ]
+
+    tests = [
+
+        {
+            'desc': 'Create private 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),
+                    'objectclass': [u'top', u'ipaVault'],
+                    'cn': [test_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'
+                              % (test_vault, api.env.basedn),
+                        'cn': [test_vault],
+                    },
+                ],
+            },
+        },
+
+        {
+            'desc': 'Show private 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],
+                },
+            },
+        },
+
+        {
+            'desc': 'Modify private 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],
+                    'description': [u'Test vault'],
+                },
+            },
+        },
+
+        {
+            'desc': 'Delete private vault',
+            'command': (
+                'vault_del',
+                [test_vault],
+                {},
+            ),
+            'expected': {
+                'value': [test_vault],
+                'summary': u'Deleted vault "%s"' % test_vault,
+                'result': {
+                    'failed': (),
+                },
+            },
+        },
+
+        {
+            'desc': 'Create shared vault',
+            'command': (
+                'vault_add',
+                [test_vault],
+                {
+                    'shared': True
+                },
+            ),
+            '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),
+                    'objectclass': [u'top', u'ipaVault'],
+                    'cn': [test_vault],
+                },
+            },
+        },
+
+        {
+            'desc': 'Find shared vaults',
+            'command': (
+                'vault_find',
+                [],
+                {
+                    'shared': True
+                },
+            ),
+            'expected': {
+                'count': 1,
+                'truncated': False,
+                'summary': u'1 vault matched',
+                'result': [
+                    {
+                        'dn': u'cn=%s,cn=shared,cn=vaults,%s'
+                              % (test_vault, api.env.basedn),
+                        'cn': [test_vault],
+                    },
+                ],
+            },
+        },
+
+        {
+            'desc': 'Show shared vault',
+            'command': (
+                'vault_show',
+                [test_vault],
+                {
+                    'shared': True
+                },
+            ),
+            'expected': {
+                'value': test_vault,
+                'summary': None,
+                'result': {
+                    'dn': u'cn=%s,cn=shared,cn=vaults,%s'
+                          % (test_vault, api.env.basedn),
+                    'cn': [test_vault],
+                },
+            },
+        },
+
+        {
+            'desc': 'Modify shared vault',
+            'command': (
+                'vault_mod',
+                [test_vault],
+                {
+                    'shared': True,
+                    'description': u'Test vault',
+                },
+            ),
+            'expected': {
+                'value': test_vault,
+                'summary': u'Modified vault "%s"' % test_vault,
+                'result': {
+                    'cn': [test_vault],
+                    'description': [u'Test vault'],
+                },
+            },
+        },
+
+        {
+            'desc': 'Delete shared vault',
+            'command': (
+                'vault_del',
+                [test_vault],
+                {
+                    'shared': True
+                },
+            ),
+            'expected': {
+                'value': [test_vault],
+                'summary': u'Deleted vault "%s"' % test_vault,
+                'result': {
+                    'failed': (),
+                },
+            },
+        },
+
+        {
+            'desc': 'Create service vault',
+            'command': (
+                'vault_add',
+                [test_vault],
+                {
+                    'host': hostname,
+                },
+            ),
+            'expected': {
+                'value': test_vault,
+                'summary': u'Added vault "%s"' % test_vault,
+                'result': {
+                    'dn': u'cn=%s,cn=%s,cn=services,cn=vaults,%s'
+                          % (test_vault, hostname, api.env.basedn),
+                    'objectclass': [u'top', u'ipaVault'],
+                    'cn': [test_vault],
+                },
+            },
+        },
+
+        {
+            'desc': 'Find service vaults',
+            'command': (
+                'vault_find',
+                [],
+                {
+                    'host': hostname,
+                },
+            ),
+            'expected': {
+                'count': 1,
+                'truncated': False,
+                'summary': u'1 vault matched',
+                'result': [
+                    {
+                        'dn': u'cn=%s,cn=%s,cn=services,cn=vaults,%s'
+                              % (test_vault, hostname, api.env.basedn),
+                        'cn': [test_vault],
+                    },
+                ],
+            },
+        },
+
+        {
+            'desc': 'Show service vault',
+            'command': (
+                'vault_show',
+                [test_vault],
+                {
+                    'host': hostname,
+                },
+            ),
+            'expected': {
+                'value': test_vault,
+                'summary': None,
+                'result': {
+                    'dn': u'cn=%s,cn=%s,cn=services,cn=vaults,%s'
+                          % (test_vault, hostname, api.env.basedn),
+                    'cn': [test_vault],
+                },
+            },
+        },
+
+        {
+            'desc': 'Modify service vault',
+            'command': (
+                'vault_mod',
+                [test_vault],
+                {
+                    'host': hostname,
+                    'description': u'Test vault',
+                },
+            ),
+            'expected': {
+                'value': test_vault,
+                'summary': u'Modified vault "%s"' % test_vault,
+                'result': {
+                    'cn': [test_vault],
+                    'description': [u'Test vault'],
+                },
+            },
+        },
+
+        {
+            'desc': 'Delete service vault',
+            'command': (
+                'vault_del',
+                [test_vault],
+                {
+                    'host': hostname,
+                },
+            ),
+            'expected': {
+                'value': [test_vault],
+                'summary': u'Deleted vault "%s"' % test_vault,
+                'result': {
+                    'failed': (),
+                },
+            },
+        },
+
+    ]
-- 
1.9.3

-- 
Manage your subscription for the Freeipa-devel mailing list:
https://www.redhat.com/mailman/listinfo/freeipa-devel
Contribute to FreeIPA: http://www.freeipa.org/page/Contribute/Code

Reply via email to