Hello,
Here are a few fixes/improvements, and the first part of a managed permission updater.

The patches should go in this order but don't need to be ACKed/pushed all at once.


Design: http://www.freeipa.org/page/V3/Managed_Read_permissions#Default_Permission_Updater
Part of the work for: https://fedorahosted.org/freeipa/ticket/3566


This part is a "preview" of sorts, to get the basic mechanism and the metadata format reviewed before I add all of the default read permissions. It implements the first section of "Default Permission Updater" in the design; "Replacing legacy default permissions" and "Removing the global anonymous read ACI" is left for later. Metadata is added for the netgroup plugin* for starters, so installing this will give you two shiny new read permissions:

$ ipa permission-find ipa: --all
---------------------
2 permissions matched
---------------------
  dn: cn=ipa:Read Netgroup Membership,cn=permissions,cn=pbac,$SUFFIX
  Permission name: ipa:Read Netgroup Membership
  Permissions: read, compare, search
  Effective attributes: externalhost, member, memberof, memberuser
  Default attributes: member, memberof, memberuser, externalhost
  Bind rule type: all
  Subtree: cn=ng,cn=alt,$SUFFIX
  Target filter: (objectclass=ipanisnetgroup)
  Type: netgroup
  ipapermissiontype: V2, MANAGED, SYSTEM
  objectclass: ipapermission, groupofnames, top, ipapermissionv2

  dn: cn=ipa:Read Netgroups,cn=permissions,cn=pbac,$SUFFIX
  Permission name: ipa:Read Netgroups
  Permissions: read, compare, search
Effective attributes: cn, description, hostcategory, ipaenabledflag, ipauniqueid, nisdomainname, usercategory Default attributes: cn, usercategory, hostcategory, ipauniqueid, ipaenabledflag, nisdomainname, description
  Bind rule type: all
  Subtree: cn=ng,cn=alt,$SUFFIX
  Target filter: (objectclass=ipanisnetgroup)
  Type: netgroup
  ipapermissiontype: V2, MANAGED, SYSTEM
  objectclass: ipapermission, groupofnames, top, ipapermissionv2
----------------------------
Number of entries returned 2
----------------------------

with corresponding ACIs at cn=ng,cn=alt,$SUFFIX:

(targetattr = "externalhost || member || memberof || memberuser")(targetfilter = "(objectclass=ipanisnetgroup)")(version 3.0;acl "permission:ipa:Read Netgroup Membership";allow (read,compare,search) userdn = "ldap:///all";;) (targetattr = "cn || description || hostcategory || ipaenabledflag || ipauniqueid || nisdomainname || usercategory")(targetfilter = "(objectclass=ipanisnetgroup)")(version 3.0;acl "permission:ipa:Read Netgroups";allow (read,compare,search) userdn = "ldap:///all";;)



Patches:

0473: Enables refactoring that will make it more clear (to humans and machines) what plugins code depends on.
https://fedorahosted.org/freeipa/ticket/4185

0474: Fix handling of the search term for legacy permissions
My code that's in master now handles the search term incorrectly. This does a better job.

0475: Fix tests that relied on some assumptions I'll be breaking

0476: Allow modifying (but not creating) permissions with ":" in the name

0477: Permission updater & sample metadata


--
PetrĀ³

(* picked by fair dice roll)

From acbca2183b52c3c3f2d3d733aa5a5086c2f24830 Mon Sep 17 00:00:00 2001
From: Petr Viktorin <pvikt...@redhat.com>
Date: Wed, 12 Feb 2014 16:17:39 +0100
Subject: [PATCH] Allow indexing API object types by class

This allows code like:
    from ipalib.plugins.dns import dnszone_mod

    api.Command[dnszone_mod]

This form should be preferred when getting specific objects
because it ensures that the appropriate plugin is imported.

https://fedorahosted.org/freeipa/ticket/4185
---
 ipalib/base.py                    | 15 +++++++++++++--
 ipatests/test_ipalib/test_base.py | 12 ++++++++++--
 2 files changed, 23 insertions(+), 4 deletions(-)

diff --git a/ipalib/base.py b/ipalib/base.py
index 83d778fe751e66414fe34d0d3243cdae34941ad6..91259c7f3b480ffc0e70061e1efa0135ff500478 100644
--- a/ipalib/base.py
+++ b/ipalib/base.py
@@ -287,7 +287,7 @@ class NameSpace(ReadOnly):
     >>> class Member(object):
     ...     def __init__(self, i):
     ...         self.i = i
-    ...         self.name = 'member%d' % i
+    ...         self.name = self.__name__ = 'member%d' % i
     ...     def __repr__(self):
     ...         return 'Member(%d)' % self.i
     ...
@@ -378,6 +378,14 @@ class NameSpace(ReadOnly):
     >>> unsorted_ns[0]
     Member(7)
 
+    As a special extension, NameSpace objects can be indexed by objects that
+    have a "__name__" attribute (e.g. classes). These lookups are converted
+    to lookups on the name:
+
+    >>> class_ns = NameSpace([Member(7), Member(3), Member(5)], sort=False)
+    >>> unsorted_ns[Member(3)]
+    Member(3)
+
     The `NameSpace` class is used in many places throughout freeIPA.  For a few
     examples, see the `plugable.API` and the `frontend.Command` classes.
     """
@@ -447,6 +455,7 @@ def __contains__(self, name):
         """
         Return ``True`` if namespace has a member named ``name``.
         """
+        name = getattr(name, '__name__', name)
         return name in self.__map
 
     def __getitem__(self, key):
@@ -455,12 +464,14 @@ def __getitem__(self, key):
 
         :param key: The name or index of a member, or a slice object.
         """
+        key = getattr(key, '__name__',  key)
         if isinstance(key, basestring):
             return self.__map[key]
         if type(key) in (int, slice):
             return self.__members[key]
         raise TypeError(
-            TYPE_ERROR % ('key', (str, int, slice), key, type(key))
+            TYPE_ERROR % ('key', (str, int, slice, 'object with __name__'),
+                          key, type(key))
         )
 
     def __repr__(self):
diff --git a/ipatests/test_ipalib/test_base.py b/ipatests/test_ipalib/test_base.py
index ef6c180c798632cb0cebd9b6ad89caaf744d7931..0117946bc2de8cb2aa014e42e6c6569b33f0900b 100644
--- a/ipatests/test_ipalib/test_base.py
+++ b/ipatests/test_ipalib/test_base.py
@@ -195,7 +195,7 @@ def membername(i):
 class DummyMember(object):
     def __init__(self, i):
         self.i = i
-        self.name = membername(i)
+        self.name = self.__name__ = membername(i)
 
 
 def gen_members(*indexes):
@@ -283,8 +283,10 @@ def test_contains(self):
             for i in yes:
                 assert membername(i) in o
                 assert membername(i).upper() not in o
+                assert DummyMember(i) in o
             for i in no:
                 assert membername(i) not in o
+                assert DummyMember(i) not in o
 
     def test_getitem(self):
         """
@@ -320,9 +322,15 @@ def test_getitem(self):
             assert o[:-4] == members[:-4]
             assert o[-9:-4] == members[-9:-4]
 
+            # Test retrieval by value
+            for member in members:
+                assert o[DummyMember(member.i)] is member
+
             # Test that TypeError is raised with wrong type
             e = raises(TypeError, o.__getitem__, 3.0)
-            assert str(e) == TYPE_ERROR % ('key', (str, int, slice), 3.0, float)
+            assert str(e) == TYPE_ERROR % (
+                'key', (str, int, slice, 'object with __name__'),
+                3.0, float)
 
     def test_repr(self):
         """
-- 
1.8.5.3

From d5f030eda3716219b27fb915a109c7252ad3c363 Mon Sep 17 00:00:00 2001
From: Petr Viktorin <pvikt...@redhat.com>
Date: Mon, 24 Feb 2014 10:36:47 +0100
Subject: [PATCH] permission-find: Fix handling of the search term for legacy
 permissions

Previously the search term was only applied to the name.
Fix it so that it filters results based on any attribute.
---
 ipalib/plugins/permission.py | 41 ++++++++++++++++++++++++-----------------
 1 file changed, 24 insertions(+), 17 deletions(-)

diff --git a/ipalib/plugins/permission.py b/ipalib/plugins/permission.py
index 670e3f1c65452fef26838558ad115ebc2aeda87a..a8c36782d710491d3ce1882c957f63ef645cb33d 100644
--- a/ipalib/plugins/permission.py
+++ b/ipalib/plugins/permission.py
@@ -1059,8 +1059,9 @@ def post_callback(self, ldap, entries, truncated, *args, **options):
 
             filters = ['(objectclass=ipaPermission)',
                        '(!(ipaPermissionType=V2))']
-            if args:
-                filters.append(ldap.make_filter_from_attr('cn', args[0],
+            if 'name' in options:
+                filters.append(ldap.make_filter_from_attr('cn',
+                                                          options['name'],
                                                           exact=False))
             attrs_list = list(self.obj.default_attributes)
             attrs_list += list(self.obj.attribute_members)
@@ -1088,22 +1089,28 @@ def post_callback(self, ldap, entries, truncated, *args, **options):
                     truncated = True
                     break
                 self.obj.upgrade_permission(entry, output_only=True)
-                cn = entry.single_value['cn']
-                if any(a.lower() in cn.lower() for a in args if a):
-                    entries.append(entry)
+                # If all given options match, include the entry
+                # Do a case-insensitive match, on any value if multi-valued
+                for opt in attribute_options:
+                    optval = options[opt]
+                    if not isinstance(optval, (tuple, list)):
+                        optval = [optval]
+                    value = entry.get(opt)
+                    if not value:
+                        break
+                    if not all(any(str(ov).lower() in str(v).lower()
+                                for v in value) for ov in optval):
+                        break
                 else:
-                    # If all given options match, include the entry
-                    # Do a case-insensitive match, on any value if multi-valued
-                    for opt in attribute_options:
-                        optval = options[opt]
-                        if not isinstance(optval, (tuple, list)):
-                            optval = [optval]
-                        value = entry.get(opt)
-                        if not value:
-                            break
-                        if not all(any(str(ov).lower() in str(v).lower()
-                                   for v in value) for ov in optval):
-                            break
+                    # Each search term must be present in some
+                    # attribute value
+                    for arg in args:
+                        if arg:
+                            arg = arg.lower()
+                            if not any(arg in str(value).lower()
+                                       for values in entry.values()
+                                       for value in values):
+                                break
                     else:
                         entries.append(entry)
 
-- 
1.8.5.3

From c7b1efa1c8dbdda0cf6d9969b95ab04aba78dad5 Mon Sep 17 00:00:00 2001
From: Petr Viktorin <pvikt...@redhat.com>
Date: Fri, 21 Feb 2014 12:29:39 +0100
Subject: [PATCH] test_permission_plugin: Fix tests that make too broad
 assumptions

The test that searches with a limit of 1 assumes a specific order
LDAP returns entries in. Future patches will change this order.
Do not check the specific entry returned.

The test that searched for --bindtype assumed that no anonymous
permissions exist in a clean install. Again, this will be changed
in future patches.
Add a name to the bindtype test, and add a negatitive test to
verify the filtering works.
---
 ipatests/test_xmlrpc/test_old_permission_plugin.py | 26 ++++--------
 ipatests/test_xmlrpc/test_permission_plugin.py     | 46 ++++++++++------------
 2 files changed, 28 insertions(+), 44 deletions(-)

diff --git a/ipatests/test_xmlrpc/test_old_permission_plugin.py b/ipatests/test_xmlrpc/test_old_permission_plugin.py
index 72c218208a0d512c66a995de9d2a1d72ea5e1d8c..816890462fb2b764becb6a0cec48c7219dffc0f7 100644
--- a/ipatests/test_xmlrpc/test_old_permission_plugin.py
+++ b/ipatests/test_xmlrpc/test_old_permission_plugin.py
@@ -565,31 +565,19 @@ class test_old_permission(Declarative):
 
         # This tests setting truncated to True in the post_callback of
         # permission_find(). The return order in LDAP is not guaranteed
-        # but in practice this is the first entry it finds. This is subject
-        # to change.
+        # so do not check the actual entry.
         dict(
             desc='Search for permissions by attr with a limit of 1 (truncated)',
-            command=('permission_find', [], dict(attrs=u'ipaenabledflag',
-                                                 sizelimit=1)),
+            command=('permission_find', [u'Modify'],
+                     dict(attrs=u'ipaenabledflag', sizelimit=1)),
             expected=dict(
                 count=1,
                 truncated=True,
                 summary=u'1 permission matched',
-                result=[
-                    {
-                        'dn': DN(('cn', 'Modify HBAC rule'),
-                                 api.env.container_permission, api.env.basedn),
-                        'cn': [u'Modify HBAC rule'],
-                        'objectclass': objectclasses.permission,
-                        'member_privilege': [u'HBAC Administrator'],
-                        'memberindirect_role': [u'IT Security Specialist'],
-                        'permissions' : [u'write'],
-                        'attrs': [u'servicecategory', u'sourcehostcategory', u'cn', u'description', u'ipaenabledflag', u'accesstime', u'usercategory', u'hostcategory', u'accessruletype', u'sourcehost'],
-                        'ipapermbindruletype': [u'permission'],
-                        'ipapermtarget': [DN('ipauniqueid=*', hbac_dn)],
-                        'subtree': u'ldap:///%s' % api.env.basedn,
-                    },
-                ],
+                result=[lambda res:
+                    DN(res['dn']).endswith(DN(api.env.container_permission,
+                                              api.env.basedn)) and
+                    'ipapermission' in res['objectclass']],
             ),
         ),
 
diff --git a/ipatests/test_xmlrpc/test_permission_plugin.py b/ipatests/test_xmlrpc/test_permission_plugin.py
index 4903bfae340dd8955a170bbb2c8121468bc47a18..62ffe14c097004a65604a1cd9b11c44022c456af 100644
--- a/ipatests/test_xmlrpc/test_permission_plugin.py
+++ b/ipatests/test_xmlrpc/test_permission_plugin.py
@@ -787,36 +787,19 @@ class test_permission(Declarative):
 
         # This tests setting truncated to True in the post_callback of
         # permission_find(). The return order in LDAP is not guaranteed
-        # but in practice this is the first entry it finds. This is subject
-        # to change.
+        # so do not check the actual entry.
         dict(
             desc='Search for permissions by attr with a limit of 1 (truncated)',
-            command=('permission_find', [], dict(attrs=u'ipaenabledflag',
-                                                 sizelimit=1)),
+            command=('permission_find', [u'Modify'],
+                     dict(attrs=u'ipaenabledflag', sizelimit=1)),
             expected=dict(
                 count=1,
                 truncated=True,
                 summary=u'1 permission matched',
-                result=[
-                    {
-                        'dn': DN(('cn', 'Modify HBAC rule'),
-                                 api.env.container_permission, api.env.basedn),
-                        'cn': [u'Modify HBAC rule'],
-                        'objectclass': objectclasses.permission,
-                        'member_privilege': [u'HBAC Administrator'],
-                        'memberindirect_role': [u'IT Security Specialist'],
-                        'ipapermright' : [u'write'],
-                        'attrs': [u'servicecategory', u'sourcehostcategory',
-                                  u'cn', u'description', u'ipaenabledflag',
-                                  u'accesstime', u'usercategory',
-                                  u'hostcategory', u'accessruletype',
-                                  u'sourcehost'],
-                        'ipapermtarget': [DN(('ipauniqueid', '*'),
-                                             ('cn', 'hbac'), api.env.basedn)],
-                        'ipapermbindruletype': [u'permission'],
-                        'ipapermlocation': [api.env.basedn],
-                    },
-                ],
+                result=[lambda res:
+                    DN(res['dn']).endswith(DN(api.env.container_permission,
+                                              api.env.basedn)) and
+                    'ipapermission' in res['objectclass']],
             ),
         ),
 
@@ -2307,7 +2290,8 @@ class test_permission_bindtype(Declarative):
 
         dict(
             desc='Search for %r using --bindtype' % permission1,
-            command=('permission_find', [], {'ipapermbindruletype': u'all'}),
+            command=('permission_find', [permission1],
+                     {'ipapermbindruletype': u'all'}),
             expected=dict(
                 count=1,
                 truncated=False,
@@ -2329,6 +2313,18 @@ class test_permission_bindtype(Declarative):
         ),
 
         dict(
+            desc='Search for %r using bad --bindtype' % permission1,
+            command=('permission_find', [permission1],
+                     {'ipapermbindruletype': u'anonymous'}),
+            expected=dict(
+                count=0,
+                truncated=False,
+                summary=u'0 permissions matched',
+                result=[],
+            ),
+        ),
+
+        dict(
             desc='Add zero permissions to %r' % (privilege1),
             command=('privilege_add_permission', [privilege1], {}),
             expected=dict(
-- 
1.8.5.3

From ec194f2ef17d70cd4a2e09a7d5c33b9d064eebed Mon Sep 17 00:00:00 2001
From: Petr Viktorin <pvikt...@redhat.com>
Date: Tue, 25 Feb 2014 17:24:02 +0100
Subject: [PATCH] Allow modifying permissions with ":" in the name

The ":" character will be reserved for default permissions, so that
users cannot create a permission with a name that will later be
added as a default.

Allow the ":" character modifying/deleting permissions*, but not
when creating them. Also do not allow the new name to contain ":"
when renaming.

(* modify/delete have their own restrictions on managed permissions)
---
 API.txt                                        | 14 ++++++------
 VERSION                                        |  4 ++--
 ipalib/plugins/permission.py                   | 31 ++++++++++++++++++++++++--
 ipatests/test_xmlrpc/test_permission_plugin.py | 27 +++++++++++++++++++++-
 4 files changed, 64 insertions(+), 12 deletions(-)

diff --git a/API.txt b/API.txt
index 070134959dd2cfdd7a281b3e50d8bc92fe21cdeb..72476ddd9277a2f9fe74100aebbca09f95422ad2 100644
--- a/API.txt
+++ b/API.txt
@@ -2349,7 +2349,7 @@ command: permission_add
 output: Output('value', <type 'unicode'>, None)
 command: permission_add_member
 args: 1,5,3
-arg: Str('cn', attribute=True, cli_name='name', multivalue=False, pattern='^[-_ a-zA-Z0-9.]+$', primary_key=True, query=True, required=True)
+arg: Str('cn', attribute=True, cli_name='name', multivalue=False, pattern='^[-_ a-zA-Z0-9.:]+$', primary_key=True, query=True, required=True)
 option: Flag('all', autofill=True, cli_name='all', default=False, exclude='webui')
 option: Flag('no_members', autofill=True, default=False, exclude='webui')
 option: Str('privilege*', alwaysask=True, cli_name='privileges', csv=True)
@@ -2360,7 +2360,7 @@ command: permission_add_member
 output: Entry('result', <type 'dict'>, Gettext('A dictionary representing an LDAP entry', domain='ipa', localedir=None))
 command: permission_add_noaci
 args: 1,5,3
-arg: Str('cn', attribute=True, cli_name='name', multivalue=False, pattern='^[-_ a-zA-Z0-9.]+$', primary_key=True, required=True)
+arg: Str('cn', attribute=True, cli_name='name', multivalue=False, pattern='^[-_ a-zA-Z0-9.:]+$', primary_key=True, required=True)
 option: Flag('all', autofill=True, cli_name='all', default=False, exclude='webui', multivalue=False, required=True)
 option: Str('ipapermissiontype', cli_name='ipapermissiontype', multivalue=True, required=True)
 option: Flag('no_members', autofill=True, cli_name='no_members', default=False, exclude='webui', multivalue=False, required=True)
@@ -2371,7 +2371,7 @@ command: permission_add_noaci
 output: Output('value', <type 'unicode'>, None)
 command: permission_del
 args: 1,3,3
-arg: Str('cn', attribute=True, cli_name='name', multivalue=True, pattern='^[-_ a-zA-Z0-9.]+$', primary_key=True, query=True, required=True)
+arg: Str('cn', attribute=True, cli_name='name', multivalue=True, pattern='^[-_ a-zA-Z0-9.:]+$', primary_key=True, query=True, required=True)
 option: Flag('continue', autofill=True, cli_name='continue', default=False)
 option: Flag('force', autofill=True, default=False)
 option: Str('version?', exclude='webui')
@@ -2383,7 +2383,7 @@ command: permission_find
 arg: Str('criteria?', noextrawhitespace=False)
 option: Flag('all', autofill=True, cli_name='all', default=False, exclude='webui')
 option: Str('attrs', attribute=False, autofill=False, cli_name='attrs', multivalue=True, query=True, required=False)
-option: Str('cn', attribute=True, autofill=False, cli_name='name', multivalue=False, pattern='^[-_ a-zA-Z0-9.]+$', primary_key=True, query=True, required=False)
+option: Str('cn', attribute=True, autofill=False, cli_name='name', multivalue=False, pattern='^[-_ a-zA-Z0-9.:]+$', primary_key=True, query=True, required=False)
 option: Str('filter', attribute=False, autofill=False, cli_name='filter', multivalue=True, query=True, required=False)
 option: StrEnum('ipapermbindruletype', attribute=True, autofill=False, cli_name='bindtype', default=u'permission', multivalue=False, query=True, required=False, values=(u'permission', u'all', u'anonymous'))
 option: Str('ipapermdefaultattr', attribute=True, autofill=False, cli_name='defaultattrs', multivalue=True, query=True, required=False)
@@ -2410,7 +2410,7 @@ command: permission_find
 output: Output('truncated', <type 'bool'>, None)
 command: permission_mod
 args: 1,23,3
-arg: Str('cn', attribute=True, cli_name='name', multivalue=False, pattern='^[-_ a-zA-Z0-9.]+$', primary_key=True, query=True, required=True)
+arg: Str('cn', attribute=True, cli_name='name', 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('attrs', attribute=False, autofill=False, cli_name='attrs', multivalue=True, required=False)
@@ -2439,7 +2439,7 @@ command: permission_mod
 output: Output('value', <type 'unicode'>, None)
 command: permission_remove_member
 args: 1,5,3
-arg: Str('cn', attribute=True, cli_name='name', multivalue=False, pattern='^[-_ a-zA-Z0-9.]+$', primary_key=True, query=True, required=True)
+arg: Str('cn', attribute=True, cli_name='name', multivalue=False, pattern='^[-_ a-zA-Z0-9.:]+$', primary_key=True, query=True, required=True)
 option: Flag('all', autofill=True, cli_name='all', default=False, exclude='webui')
 option: Flag('no_members', autofill=True, default=False, exclude='webui')
 option: Str('privilege*', alwaysask=True, cli_name='privileges', csv=True)
@@ -2450,7 +2450,7 @@ command: permission_remove_member
 output: Entry('result', <type 'dict'>, Gettext('A dictionary representing an LDAP entry', domain='ipa', localedir=None))
 command: permission_show
 args: 1,5,3
-arg: Str('cn', attribute=True, cli_name='name', multivalue=False, pattern='^[-_ a-zA-Z0-9.]+$', primary_key=True, query=True, required=True)
+arg: Str('cn', attribute=True, cli_name='name', multivalue=False, pattern='^[-_ a-zA-Z0-9.:]+$', primary_key=True, query=True, required=True)
 option: Flag('all', autofill=True, cli_name='all', default=False, exclude='webui')
 option: Flag('no_members', autofill=True, default=False, exclude='webui')
 option: Flag('raw', autofill=True, cli_name='raw', default=False, exclude='webui')
diff --git a/VERSION b/VERSION
index b1dcb9c05416fb611f523c9c10599b5fc0e18045..cea091bcbde5ee9867029e90ae336160dd7b5d42 100644
--- a/VERSION
+++ b/VERSION
@@ -89,5 +89,5 @@ IPA_DATA_VERSION=20100614120000
 #                                                      #
 ########################################################
 IPA_API_VERSION_MAJOR=2
-IPA_API_VERSION_MINOR=75
-# Last change: npmccallum - HOTP support
+IPA_API_VERSION_MINOR=76
+# Last change: pviktori - ":" in permission names
diff --git a/ipalib/plugins/permission.py b/ipalib/plugins/permission.py
index a8c36782d710491d3ce1882c957f63ef645cb33d..4477a5e4e379198e2fccc4c2d503ccbe7c97f1fd 100644
--- a/ipalib/plugins/permission.py
+++ b/ipalib/plugins/permission.py
@@ -147,6 +147,18 @@ def validate_type(ugettext, typestr):
         return _('"%s" is not a valid permission type') % typestr
 
 
+def _disallow_colon(option):
+    """Given a "cn" option, return a new "cn" option with ':' disallowed
+
+    Used in permission-add and for --rename in permission-mod to prevent user
+    from creating new permissions with ":" in the name.
+    """
+    return option.clone(
+        pattern='^[-_ a-zA-Z0-9.]+$',
+        pattern_errmsg="May only contain letters, numbers, -, _, ., and space",
+    )
+
+
 @register()
 class permission(baseldap.LDAPObject):
     """
@@ -176,8 +188,9 @@ class permission(baseldap.LDAPObject):
             cli_name='name',
             label=_('Permission name'),
             primary_key=True,
-            pattern='^[-_ a-zA-Z0-9.]+$',
-            pattern_errmsg="May only contain letters, numbers, -, _, ., and space",
+            pattern='^[-_ a-zA-Z0-9.:]+$',
+            pattern_errmsg="May only contain letters, numbers, "
+                           "-, _, ., :, and space",
         ),
         StrEnum(
             'ipapermright*',
@@ -797,6 +810,13 @@ def execute(self, *keys, **options):
         self.obj.preprocess_options(options)
         return super(permission_add, self).execute(*keys, **options)
 
+    def get_args(self):
+        for arg in super(permission_add, self).get_args():
+            if arg.name == 'cn':
+                yield _disallow_colon(arg)
+            else:
+                yield arg
+
     def pre_callback(self, ldap, dn, entry, attrs_list, *keys, **options):
         entry['ipapermissiontype'] = ['SYSTEM', 'V2']
         entry['cn'] = list(keys)
@@ -867,6 +887,13 @@ def execute(self, *keys, **options):
             options, return_filter_ops=True)
         return super(permission_mod, self).execute(*keys, **options)
 
+    def get_options(self):
+        for opt in super(permission_mod, self).get_options():
+            if opt.name == 'rename':
+                yield _disallow_colon(opt)
+            else:
+                yield opt
+
     def pre_callback(self, ldap, dn, entry, attrs_list, *keys, **options):
         if 'rename' in options and not options['rename']:
             raise errors.ValidationError(name='rename',
diff --git a/ipatests/test_xmlrpc/test_permission_plugin.py b/ipatests/test_xmlrpc/test_permission_plugin.py
index 62ffe14c097004a65604a1cd9b11c44022c456af..58b26fcee9542ddcfe5e87eb5cc54bf2ea2f8bdc 100644
--- a/ipatests/test_xmlrpc/test_permission_plugin.py
+++ b/ipatests/test_xmlrpc/test_permission_plugin.py
@@ -220,6 +220,18 @@ class test_permission_negative(Declarative):
         verify_permission_aci_missing(permission1, users_dn),
 
         dict(
+            desc='Try to create permission with : in the name',
+            command=('permission_add', ['bad:' + permission1], dict(
+                    type=u'user',
+                    ipapermright=u'write',
+                )),
+            expected=errors.ValidationError(name='name',
+                error='May only contain letters, numbers, -, _, ., and space'),
+        ),
+
+        verify_permission_aci_missing(permission1, users_dn),
+
+        dict(
             desc='Create %r so we can try breaking it' % permission1,
             command=(
                 'permission_add', [permission1], dict(
@@ -285,7 +297,7 @@ class test_permission_negative(Declarative):
         ),
 
         dict(
-            desc='Try to rename %r to invalid invalid %r' % (
+            desc='Try to rename %r to invalid %r' % (
                 permission1, invalid_permission1),
             command=('permission_mod', [permission1], dict(
                     rename=invalid_permission1,
@@ -1501,6 +1513,19 @@ class test_permission(Declarative):
                 name='ipapermlocation',
                 error='Entry %s does not exist' % nonexistent_dn)
         ),
+
+        dict(
+            desc='Search fo rnonexisting permission with ":" in the name',
+            command=(
+                'permission_find', ['doesnotexist:' + permission1], {}
+            ),
+            expected=dict(
+                count=0,
+                truncated=False,
+                summary=u'0 permissions matched',
+                result=[],
+            ),
+        ),
     ]
 
 
-- 
1.8.5.3

From 3decdcb2c4c921dc224976386ee949411614be14 Mon Sep 17 00:00:00 2001
From: Petr Viktorin <pvikt...@redhat.com>
Date: Thu, 19 Sep 2013 17:41:04 +0200
Subject: [PATCH] Add Object metadata and update plugin for managed permissions

The default read permission is added for Netgroup as an example.

Part of the work for: https://fedorahosted.org/freeipa/ticket/3566
Design: http://www.freeipa.org/page/V3/Managed_Read_permissions
---
 ipalib/plugins/baseldap.py                         |   1 +
 ipalib/plugins/netgroup.py                         |  19 +++
 .../install/plugins/update_managed_permissions.py  | 159 +++++++++++++++++++++
 3 files changed, 179 insertions(+)
 create mode 100644 ipaserver/install/plugins/update_managed_permissions.py

diff --git a/ipalib/plugins/baseldap.py b/ipalib/plugins/baseldap.py
index c2aad784df2b2bcd03f4ec0462e74cd6abce594c..2fc81ac48772e465c2823dff22df819ec38093d0 100644
--- a/ipalib/plugins/baseldap.py
+++ b/ipalib/plugins/baseldap.py
@@ -469,6 +469,7 @@ class LDAPObject(Object):
     }
     label = _('Entry')
     label_singular = _('Entry')
+    managed_permissions = {}
 
     container_not_found_msg = _('container entry (%(container)s) not found')
     parent_not_found_msg = _('%(parent)s: %(oname)s not found')
diff --git a/ipalib/plugins/netgroup.py b/ipalib/plugins/netgroup.py
index fe27e6cb6e623496f03a78e27b8c5e44b3ea6585..e41ca27b53ddd1a6043ca1f943e73e9be3ec7fc3 100644
--- a/ipalib/plugins/netgroup.py
+++ b/ipalib/plugins/netgroup.py
@@ -105,6 +105,25 @@ class netgroup(LDAPObject):
         'memberuser': ('Member', '', 'no_'),
         'memberhost': ('Member', '', 'no_'),
     }
+    managed_permissions = {
+        'ipa:Read Netgroups': {
+            'replaces_global_anonymous_aci': True,
+            'ipapermbindruletype': 'all',
+            'ipapermright': {'read', 'search', 'compare'},
+            'ipapermdefaultattr': {
+                'cn', 'description', 'hostcategory', 'ipaenabledflag',
+                'ipauniqueid', 'nisdomainname', 'usercategory'
+            },
+        },
+        'ipa:Read Netgroup Membership': {
+            'replaces_global_anonymous_aci': True,
+            'ipapermbindruletype': 'all',
+            'ipapermright': {'read', 'search', 'compare'},
+            'ipapermdefaultattr': {
+                'externalhost', 'member', 'memberof', 'memberuser'
+            },
+        },
+    }
 
     label = _('Netgroups')
     label_singular = _('Netgroup')
diff --git a/ipaserver/install/plugins/update_managed_permissions.py b/ipaserver/install/plugins/update_managed_permissions.py
new file mode 100644
index 0000000000000000000000000000000000000000..226b4fa67e7f84e71c34154b4ada2ee82c54493d
--- /dev/null
+++ b/ipaserver/install/plugins/update_managed_permissions.py
@@ -0,0 +1,159 @@
+# Authors:
+#   Petr Viktorin <pvikt...@redhat.com>
+#
+# Copyright (C) 2014  Red Hat
+# see file 'COPYING' for use and warranty information
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program.  If not, see <http://www.gnu.org/licenses/>.
+
+from ipalib import errors
+from ipapython.dn import DN
+from ipalib.plugable import Registry
+from ipalib.plugins import aci
+from ipalib.plugins.permission import permission
+from ipaserver.plugins.ldap2 import ldap2
+from ipaserver.install.plugins import LAST
+from ipaserver.install.plugins.baseupdate import PostUpdate
+
+
+register = Registry()
+
+
+@register()
+class update_managed_permissions(PostUpdate):
+    """Update managed permissions after an update.
+
+    Update managed permissions according to templates specified in plugins.
+    For read permissions, puts any attributes specified in the legacy
+    Anonymous access ACI in the exclude list when creating the permission.
+    """
+    order = LAST
+
+    def get_anonymous_read_blacklist(self, ldap):
+        """Get the list of attributes from the legacy anonymous access ACI"""
+        aciname = u'Enable Anonymous access'
+        aciprefix = u'none'
+
+        base_entry = ldap.get_entry(self.api.env.basedn, ['aci'])
+
+        acistrs = base_entry.get('aci', [])
+        acilist = aci._convert_strings_to_acis(acistrs)
+        try:
+            rawaci = aci._find_aci_by_name(acilist, aciprefix, aciname)
+        except errors.NotFound:
+            self.log.info('Anonymous ACI not found, using no blacklist')
+            return []
+
+        return rawaci.target['targetattr']['expression']
+
+    def execute(self, **options):
+        ldap = self.api.Backend[ldap2]
+
+        anonymous_read_blacklist = self.get_anonymous_read_blacklist(ldap)
+
+        self.log.info('Anonymous read blacklist: %s', anonymous_read_blacklist)
+
+        for obj in self.api.Object():
+            managed_permissions = getattr(obj, 'managed_permissions', {})
+            if managed_permissions:
+                self.log.info('Updating managed permissions for %s', obj.name)
+            for name, template in managed_permissions.items():
+                assert name.startswith('ipa:')
+                self.update_permission(ldap,
+                                       obj,
+                                       unicode(name),
+                                       template,
+                                       anonymous_read_blacklist)
+
+        return False, False, ()
+
+    def update_permission(self, ldap, obj, name, template,
+                          anonymous_read_blacklist):
+        """Update the given permission and the corresponding ACI"""
+        dn = self.api.Object[permission].get_dn(name)
+
+        try:
+            attrs_list = self.api.Object[permission].default_attributes
+            entry = ldap.get_entry(dn, attrs_list)
+            is_new = False
+        except errors.NotFound:
+            entry = ldap.make_entry(dn)
+            is_new = True
+
+        self.log.info('Updating managed permission %s', name)
+        self.update_entry(obj, entry, template,
+                          anonymous_read_blacklist, is_new=is_new)
+
+        if is_new:
+            ldap.add_entry(entry)
+        else:
+            try:
+                ldap.update_entry(entry)
+            except errors.EmptyModlist:
+                return
+
+        self.log.info('Updating ACI for managed permission %s', name)
+
+        self.api.Object[permission].update_aci(entry)
+
+    def update_entry(self, obj, entry, template,
+                     anonymous_read_blacklist, is_new):
+        """Update the given permission Entry (without contacting LDAP)"""
+
+        [name_ava] = entry.dn[0]
+        assert name_ava.attr == 'cn'
+        name = name_ava.value
+        entry.single_value['cn'] = name
+
+        template = dict(template)
+
+        # Common attributes
+        entry['objectclass'] = self.api.Object[permission].object_class
+
+        entry['ipapermissiontype'] = [u'SYSTEM', u'V2', u'MANAGED']
+
+        # Object-specific attributes
+        ldap_filter = ['(objectclass=%s)' % oc
+                       for oc in obj.permission_filter_objectclasses]
+        entry['ipapermtargetfilter'] = ldap_filter
+
+        ipapermlocation = DN(obj.container_dn, self.api.env.basedn)
+        entry.single_value['ipapermlocation'] = ipapermlocation
+
+        # Attributes from template
+        bindruletype = template.pop('ipapermbindruletype')
+        entry.single_value['ipapermbindruletype'] = bindruletype
+
+        entry['ipapermright'] = list(template.pop('ipapermright'))
+
+        # Add to the set of default attributes
+        attributes = set(template.pop('ipapermdefaultattr', ()))
+        attributes.update(entry.get('ipapermdefaultattr', ()))
+        attributes = set(a.lower() for a in attributes)
+        entry['ipapermdefaultattr'] = list(attributes)
+
+        # Exclude attributes filtered from the global read ACI
+        if template.pop('replaces_global_anonymous_aci', False) and is_new:
+            read_blacklist = set(a.lower() for a in anonymous_read_blacklist)
+            read_blacklist &= attributes
+            if read_blacklist:
+                self.log.info('Excluded attributes for %s: %s',
+                              name, ', '.join(read_blacklist))
+                entry['ipapermexcludedattr'] = list(read_blacklist)
+
+        # Sanity check
+        if template:
+            raise ValueError(
+                'Unknown key(s) in managed permission template %s: %s' % (
+                    name, ', '.join(template.keys())))
-- 
1.8.5.3

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

Reply via email to