[Freeipa-devel] [PATCH] Password vault

Endi Sukma Dewata edewata at redhat.com
Tue May 12 10:52:07 UTC 2015


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

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

https://fedorahosted.org/freeipa/ticket/3872
---
 API.txt                                   |  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 <edewata at redhat.com>
+#
+# Copyright (C) 2015  Red Hat
+# see file 'COPYING' for use and warranty information
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program.  If not, see <http://www.gnu.org/licenses/>.
+
+from ipalib import api, errors
+from ipalib import Str, Flag
+from ipalib import output
+from ipalib.plugable import Registry
+from ipalib.plugins.baseldap import LDAPObject, LDAPCreate, LDAPDelete,\
+    LDAPSearch, LDAPUpdate, LDAPRetrieve
+from ipalib.request import context
+from ipalib.plugins.user import split_principal
+from ipalib import _, ngettext
+from ipapython.dn import DN
+
+__doc__ = _("""
+Vaults
+""") + _("""
+Manage vaults.
+""") + _("""
+EXAMPLES:
+""") + _("""
+ List private vaults:
+   ipa vault-find
+""") + _("""
+ List 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()
+
+
+ at register()
+class vault(LDAPObject):
+    __doc__ = _("""
+    Vault object.
+    """)
+
+    base_dn = DN(api.env.container_vault, api.env.basedn)
+
+    object_name = _('vault')
+    object_name_plural = _('vaults')
+
+    object_class = ['ipaVault']
+    default_attributes = [
+        'cn',
+        '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)
+
+
+ at 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
+
+
+ at 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"')
+
+
+ at 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')
+        )
+
+
+ at 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"')
+
+
+ at 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 <edewata at redhat.com>
+#
+# Copyright (C) 2015  Red Hat
+# see file 'COPYING' for use and warranty information
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program.  If not, see <http://www.gnu.org/licenses/>.
+
+"""
+Test the `ipalib/plugins/vault.py` module.
+"""
+
+from ipalib import api, errors
+from xmlrpc_test import Declarative, fuzzy_string
+
+test_vault = u'test_vault'
+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



More information about the Freeipa-devel mailing list