summaryrefslogtreecommitdiffstats
path: root/ipaserver/plugins/permission.py
diff options
context:
space:
mode:
authorJan Cholasta <jcholast@redhat.com>2016-04-28 10:30:05 +0200
committerJan Cholasta <jcholast@redhat.com>2016-06-03 09:00:34 +0200
commit6e44557b601f769d23ee74555a72e8b5cc62c0c9 (patch)
treeeedd3e054b0709341b9f58c190ea54f999f7d13a /ipaserver/plugins/permission.py
parentec841e5d7ab29d08de294b3fa863a631cd50e30a (diff)
downloadfreeipa-6e44557b601f769d23ee74555a72e8b5cc62c0c9.tar.gz
freeipa-6e44557b601f769d23ee74555a72e8b5cc62c0c9.tar.xz
freeipa-6e44557b601f769d23ee74555a72e8b5cc62c0c9.zip
ipalib: move server-side plugins to ipaserver
Move the remaining plugin code from ipalib.plugins to ipaserver.plugins. Remove the now unused ipalib.plugins package. https://fedorahosted.org/freeipa/ticket/4739 Reviewed-By: David Kupka <dkupka@redhat.com>
Diffstat (limited to 'ipaserver/plugins/permission.py')
-rw-r--r--ipaserver/plugins/permission.py1395
1 files changed, 1395 insertions, 0 deletions
diff --git a/ipaserver/plugins/permission.py b/ipaserver/plugins/permission.py
new file mode 100644
index 000000000..9f19358da
--- /dev/null
+++ b/ipaserver/plugins/permission.py
@@ -0,0 +1,1395 @@
+# Authors:
+# Petr Viktorin <pviktori@redhat.com>
+#
+# Copyright (C) 2013 Red Hat
+# see file 'COPYING' for use and warranty information
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+import re
+import traceback
+
+import six
+
+from . import baseldap
+from .privilege import validate_permission_to_privilege
+from ipalib import errors
+from ipalib.parameters import Str, StrEnum, DNParam, Flag
+from ipalib import api, _, ngettext
+from ipalib.plugable import Registry
+from ipalib.capabilities import client_has_capability
+from ipalib.aci import ACI
+from ipapython.dn import DN
+from ipalib.request import context
+
+if six.PY3:
+ unicode = str
+
+__doc__ = _("""
+Permissions
+""") + _("""
+A permission enables fine-grained delegation of rights. A permission is
+a human-readable wrapper around a 389-ds Access Control Rule,
+or instruction (ACI).
+A permission grants the right to perform a specific task such as adding a
+user, modifying a group, etc.
+""") + _("""
+A permission may not contain other permissions.
+""") + _("""
+* A permission grants access to read, write, add, delete, read, search,
+ or compare.
+* A privilege combines similar permissions (for example all the permissions
+ needed to add a user).
+* A role grants a set of privileges to users, groups, hosts or hostgroups.
+""") + _("""
+A permission is made up of a number of different parts:
+
+1. The name of the permission.
+2. The target of the permission.
+3. The rights granted by the permission.
+""") + _("""
+Rights define what operations are allowed, and may be one or more
+of the following:
+1. write - write one or more attributes
+2. read - read one or more attributes
+3. search - search on one or more attributes
+4. compare - compare one or more attributes
+5. add - add a new entry to the tree
+6. delete - delete an existing entry
+7. all - all permissions are granted
+""") + _("""
+Note the distinction between attributes and entries. The permissions are
+independent, so being able to add a user does not mean that the user will
+be editable.
+""") + _("""
+There are a number of allowed targets:
+1. subtree: a DN; the permission applies to the subtree under this DN
+2. target filter: an LDAP filter
+3. target: DN with possible wildcards, specifies entries permission applies to
+""") + _("""
+Additionally, there are the following convenience options.
+Setting one of these options will set the corresponding attribute(s).
+1. type: a type of object (user, group, etc); sets subtree and target filter.
+2. memberof: apply to members of a group; sets target filter
+3. targetgroup: grant access to modify a specific group (such as granting
+ the rights to manage group membership); sets target.
+""") + _("""
+Managed permissions
+""") + _("""
+Permissions that come with IPA by default can be so-called "managed"
+permissions. These have a default set of attributes they apply to,
+but the administrator can add/remove individual attributes to/from the set.
+""") + _("""
+Deleting or renaming a managed permission, as well as changing its target,
+is not allowed.
+""") + _("""
+EXAMPLES:
+""") + _("""
+ Add a permission that grants the creation of users:
+ ipa permission-add --type=user --permissions=add "Add Users"
+""") + _("""
+ Add a permission that grants the ability to manage group membership:
+ ipa permission-add --attrs=member --permissions=write --type=group "Manage Group Members"
+""")
+
+register = Registry()
+
+_DEPRECATED_OPTION_ALIASES = {
+ 'permissions': 'ipapermright',
+ 'filter': 'extratargetfilter',
+ 'subtree': 'ipapermlocation',
+}
+
+KNOWN_FLAGS = {'SYSTEM', 'V2', 'MANAGED'}
+
+output_params = (
+ Str('aci',
+ label=_('ACI'),
+ ),
+)
+
+
+def strip_ldap_prefix(uri):
+ prefix = 'ldap:///'
+ if not uri.startswith(prefix):
+ raise ValueError('%r does not start with %r' % (uri, prefix))
+ return uri[len(prefix):]
+
+
+def prevalidate_filter(ugettext, value):
+ if not value.startswith('(') or not value.endswith(')'):
+ return _('must be enclosed in parentheses')
+
+
+class DNOrURL(DNParam):
+ """DN parameter that allows, and strips, a "ldap:///" prefix on input
+
+ Used for ``subtree`` to maintain backward compatibility.
+ """
+
+ def _convert_scalar(self, value, index=None):
+ if isinstance(value, six.string_types) and value.startswith('ldap:///'):
+ value = strip_ldap_prefix(value)
+ return super(DNOrURL, self)._convert_scalar(value)
+
+
+def validate_type(ugettext, typestr):
+ try:
+ obj = api.Object[typestr]
+ except KeyError:
+ return _('"%s" is not an object type') % typestr
+ if not getattr(obj, 'permission_filter_objectclasses', None):
+ 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):
+ """
+ Permission object.
+ """
+ container_dn = api.env.container_permission
+ object_name = _('permission')
+ object_name_plural = _('permissions')
+ # For use the complete object_class list, including 'top', so
+ # the updater doesn't try to delete 'top' every time.
+ object_class = ['top', 'groupofnames', 'ipapermission', 'ipapermissionv2']
+ permission_filter_objectclasses = ['ipapermission']
+ default_attributes = ['cn', 'member', 'memberof',
+ 'memberindirect', 'ipapermissiontype', 'objectclass',
+ 'ipapermdefaultattr', 'ipapermincludedattr', 'ipapermexcludedattr',
+ 'ipapermbindruletype', 'ipapermlocation', 'ipapermright',
+ 'ipapermtargetfilter', 'ipapermtarget'
+ ]
+ attribute_members = {
+ 'member': ['privilege'],
+ 'memberindirect': ['role'],
+ }
+ rdn_is_primary_key = True
+ managed_permissions = {
+ 'System: Read Permissions': {
+ 'replaces_global_anonymous_aci': True,
+ 'ipapermright': {'read', 'search', 'compare'},
+ 'ipapermdefaultattr': {
+ 'businesscategory', 'cn', 'description', 'ipapermissiontype',
+ 'o', 'objectclass', 'ou', 'owner', 'seealso',
+ 'ipapermdefaultattr', 'ipapermincludedattr',
+ 'ipapermexcludedattr', 'ipapermbindruletype', 'ipapermtarget',
+ 'ipapermlocation', 'ipapermright', 'ipapermtargetfilter',
+ 'member', 'memberof', 'memberuser', 'memberhost',
+ },
+ 'default_privileges': {'RBAC Readers'},
+ },
+ 'System: Read ACIs': {
+ # Readable ACIs are needed for reading legacy permissions.
+ 'non_object': True,
+ 'ipapermlocation': api.env.basedn,
+ 'replaces_global_anonymous_aci': True,
+ 'ipapermright': {'read', 'search', 'compare'},
+ 'ipapermdefaultattr': {'aci'},
+ 'default_privileges': {'RBAC Readers'},
+ },
+ 'System: Modify Privilege Membership': {
+ 'ipapermright': {'write'},
+ 'ipapermdefaultattr': {'member'},
+ 'replaces': [
+ '(targetattr = "member")(target = "ldap:///cn=*,cn=permissions,cn=pbac,$SUFFIX")(version 3.0;acl "permission:Modify privilege membership";allow (write) groupdn = "ldap:///cn=Modify privilege membership,cn=permissions,cn=pbac,$SUFFIX";)',
+ ],
+ 'default_privileges': {'Delegation Administrator'},
+ },
+ }
+
+ label = _('Permissions')
+ label_singular = _('Permission')
+
+ takes_params = (
+ Str('cn',
+ cli_name='name',
+ label=_('Permission name'),
+ primary_key=True,
+ pattern='^[-_ a-zA-Z0-9.:/]+$',
+ pattern_errmsg="May only contain letters, numbers, "
+ "-, _, ., :, /, and space",
+ ),
+ StrEnum(
+ 'ipapermright*',
+ cli_name='right',
+ deprecated_cli_aliases={'permissions'},
+ label=_('Granted rights'),
+ doc=_('Rights to grant '
+ '(read, search, compare, write, add, delete, all)'),
+ values=(u'read', u'search', u'compare',
+ u'write', u'add', u'delete', u'all'),
+ flags={'ask_create'},
+ ),
+ Str('attrs*',
+ label=_('Effective attributes'),
+ doc=_('All attributes to which the permission applies'),
+ flags={'virtual_attribute', 'allow_mod_for_managed_permission'},
+ ),
+ Str('ipapermincludedattr*',
+ cli_name='includedattrs',
+ label=_('Included attributes'),
+ doc=_('User-specified attributes to which the permission applies'),
+ flags={'no_create', 'allow_mod_for_managed_permission'},
+ ),
+ Str('ipapermexcludedattr*',
+ cli_name='excludedattrs',
+ label=_('Excluded attributes'),
+ doc=_('User-specified attributes to which the permission '
+ 'explicitly does not apply'),
+ flags={'no_create', 'allow_mod_for_managed_permission'},
+ ),
+ Str('ipapermdefaultattr*',
+ cli_name='defaultattrs',
+ label=_('Default attributes'),
+ doc=_('Attributes to which the permission applies by default'),
+ flags={'no_create', 'no_update'},
+ ),
+ StrEnum(
+ 'ipapermbindruletype',
+ cli_name='bindtype',
+ label=_('Bind rule type'),
+ doc=_('Bind rule type'),
+ autofill=True,
+ values=(u'permission', u'all', u'anonymous'),
+ default=u'permission',
+ flags={'allow_mod_for_managed_permission'},
+ ),
+ DNOrURL(
+ 'ipapermlocation?',
+ cli_name='subtree',
+ label=_('Subtree'),
+ doc=_('Subtree to apply permissions to'),
+ flags={'ask_create'},
+ ),
+ Str(
+ 'extratargetfilter*', prevalidate_filter,
+ cli_name='filter',
+ label=_('Extra target filter'),
+ doc=_('Extra target filter'),
+ flags={'virtual_attribute'},
+ ),
+ Str(
+ 'ipapermtargetfilter*', prevalidate_filter,
+ cli_name='rawfilter',
+ label=_('Raw target filter'),
+ doc=_('All target filters, including those implied by '
+ 'type and memberof'),
+ ),
+
+ DNParam(
+ 'ipapermtarget?',
+ cli_name='target',
+ label=_('Target DN'),
+ doc=_('Optional DN to apply the permission to '
+ '(must be in the subtree, but may not yet exist)'),
+ ),
+
+ DNParam(
+ 'ipapermtargetto?',
+ cli_name='targetto',
+ label=_('Target DN subtree'),
+ doc=_('Optional DN subtree where an entry can be moved to '
+ '(must be in the subtree, but may not yet exist)'),
+ ),
+
+ DNParam(
+ 'ipapermtargetfrom?',
+ cli_name='targetfrom',
+ label=_('Origin DN subtree'),
+ doc=_('Optional DN subtree from where an entry can be moved '
+ '(must be in the subtree, but may not yet exist)'),
+ ),
+
+ Str('memberof*',
+ label=_('Member of group'), # FIXME: Does this label make sense?
+ doc=_('Target members of a group (sets memberOf targetfilter)'),
+ flags={'ask_create', 'virtual_attribute'},
+ ),
+ Str('targetgroup?',
+ label=_('Target group'),
+ doc=_('User group to apply permissions to (sets target)'),
+ flags={'ask_create', 'virtual_attribute'},
+ ),
+ Str(
+ 'type?', validate_type,
+ label=_('Type'),
+ doc=_('Type of IPA object '
+ '(sets subtree and objectClass targetfilter)'),
+ flags={'ask_create', 'virtual_attribute'},
+ ),
+ ) + tuple(
+ Str(old_name + '*',
+ doc=_('Deprecated; use %s' % new_name),
+ flags={'no_option', 'virtual_attribute'})
+ for old_name, new_name in _DEPRECATED_OPTION_ALIASES.items()
+ )
+
+ def reject_system(self, entry):
+ """Raise if permission entry has unknown flags, or is a SYSTEM perm"""
+ flags = entry.get('ipapermissiontype', [])
+ for flag in flags:
+ if flag not in KNOWN_FLAGS:
+ raise errors.ACIError(
+ info=_('Permission with unknown flag %s may not be '
+ 'modified or removed') % flag)
+ if list(flags) == [u'SYSTEM']:
+ raise errors.ACIError(
+ info=_('A SYSTEM permission may not be modified or removed'))
+
+ def _get_filter_attr_info(self, entry):
+ """Get information on filter-related virtual attributes
+
+ Returns a dict with this information:
+ 'implicit_targetfilters': targetfilters implied by memberof and type
+ 'memberof': list of names of groups from memberof
+ 'type': the type
+ """
+ ipapermtargetfilter = entry.get('ipapermtargetfilter', [])
+ ipapermlocation = entry.single_value.get('ipapermlocation')
+
+ implicit_targetfilters = set()
+ result = {'implicit_targetfilters': implicit_targetfilters}
+
+ # memberof
+ memberof = []
+ for targetfilter in ipapermtargetfilter:
+ match = re.match('^\(memberof=(.*)\)$', targetfilter, re.I)
+ if match:
+ try:
+ dn = DN(match.group(1))
+ except ValueError:
+ # Malformed DN; e.g. (memberof=*)
+ continue
+ groups_dn = DN(self.api.Object.group.container_dn,
+ self.api.env.basedn)
+ if dn[1:] == groups_dn[:] and dn[0].attr == 'cn':
+ memberof.append(dn[0].value)
+ implicit_targetfilters.add(match.group(0))
+ if memberof:
+ result['memberof'] = memberof
+
+ # type
+ if ipapermtargetfilter and ipapermlocation:
+ for obj in self.api.Object():
+ filt = self.make_type_filter(obj)
+ if not filt:
+ continue
+
+ wantdn = DN(obj.container_dn, self.api.env.basedn)
+ if DN(ipapermlocation) != wantdn:
+ continue
+
+ if filt in ipapermtargetfilter:
+ result['type'] = [unicode(obj.name)]
+ implicit_targetfilters.add(filt)
+ break
+
+ return result
+
+ def postprocess_result(self, entry, options):
+ """Update a permission entry for output (in place)
+
+ :param entry: The entry to update
+ :param options:
+ Command options. Contains keys such as ``raw``, ``all``,
+ ``pkey_only``, ``version``.
+ """
+ old_client = not client_has_capability(
+ options['version'], 'permissions2')
+
+ if not options.get('raw') and not options.get('pkey_only'):
+ ipapermtargetfilter = entry.get('ipapermtargetfilter', [])
+ ipapermtarget = entry.single_value.get('ipapermtarget')
+
+ # targetgroup
+ if ipapermtarget:
+ dn = DN(ipapermtarget)
+ if (dn[1:] == DN(self.api.Object.group.container_dn,
+ self.api.env.basedn)[:] and
+ dn[0].attr == 'cn' and dn[0].value != '*'):
+ entry.single_value['targetgroup'] = dn[0].value
+
+ filter_attr_info = self._get_filter_attr_info(entry)
+ if 'type' in filter_attr_info:
+ entry['type'] = filter_attr_info['type']
+ if 'memberof' in filter_attr_info:
+ entry['memberof'] = filter_attr_info['memberof']
+ if 'implicit_targetfilters' in filter_attr_info:
+ extratargetfilter = sorted(
+ set(ipapermtargetfilter) -
+ filter_attr_info['implicit_targetfilters'])
+ if extratargetfilter:
+ entry['extratargetfilter'] = extratargetfilter
+
+ # old output names
+ if old_client:
+ for old_name, new_name in _DEPRECATED_OPTION_ALIASES.items():
+ if new_name in entry:
+ entry[old_name] = entry[new_name]
+ del entry[new_name]
+
+ rights = entry.get('attributelevelrights')
+ if rights:
+ if 'ipapermtarget' in rights:
+ rights['targetgroup'] = rights['ipapermtarget']
+ if 'ipapermtargetfilter' in rights:
+ rights['memberof'] = rights['ipapermtargetfilter']
+
+ type_rights = set(rights['ipapermtargetfilter'])
+ location_rights = set(rights.get('ipapermlocation', ''))
+ type_rights.intersection_update(location_rights)
+ rights['type'] = ''.join(sorted(
+ type_rights, key=rights['ipapermtargetfilter'].index))
+
+ if 'ipapermincludedattr' in rights:
+ rights['attrs'] = ''.join(sorted(
+ set(rights['ipapermincludedattr']) &
+ set(rights.get('ipapermexcludedattr', '')),
+ key=rights['ipapermincludedattr'].index))
+
+ if old_client:
+ for old_name, new_name in _DEPRECATED_OPTION_ALIASES.items():
+ if new_name in entry:
+ rights[old_name] = rights[new_name]
+ del rights[new_name]
+
+ if options.get('raw'):
+ # Retreive the ACI from LDAP to ensure we get the real thing
+ try:
+ acientry, acistring = self._get_aci_entry_and_string(entry)
+ except errors.NotFound:
+ if list(entry.get('ipapermissiontype')) == ['SYSTEM']:
+ # SYSTEM permissions don't have normal ACIs
+ pass
+ else:
+ raise
+ else:
+ entry.single_value['aci'] = acistring
+ else:
+ effective_attrs = self.get_effective_attrs(entry)
+ if effective_attrs:
+ entry['attrs'] = effective_attrs
+ if (not options.get('all') and
+ not entry.get('ipapermexcludedattr') and
+ not entry.get('ipapermdefaultattr')):
+ entry.pop('ipapermincludedattr', None)
+
+ if old_client:
+ # Legacy clients expect some attributes as a single value
+ for attr in 'type', 'targetgroup', 'aci':
+ if attr in entry:
+ entry[attr] = entry.single_value[attr]
+ # memberof was also single-valued, but not any more
+ if entry.get('memberof'):
+ joined_value = u', '.join(str(m) for m in entry['memberof'])
+ entry['memberof'] = joined_value
+ if 'subtree' in entry:
+ # Legacy clients expect subtree as a URL
+ dn = entry.single_value['subtree']
+ entry['subtree'] = u'ldap:///%s' % dn
+ if 'filter' in entry:
+ # Legacy clients expect filter without parentheses
+ new_filter = []
+ for flt in entry['filter']:
+ assert flt[0] == '(' and flt[-1] == ')'
+ new_filter.append(flt[1:-1])
+ entry['filter'] = new_filter
+
+ if not options['raw'] and not options['all']:
+ # Don't return the raw target filter by default
+ entry.pop('ipapermtargetfilter', None)
+
+ def get_effective_attrs(self, entry):
+ attrs = set(entry.get('ipapermdefaultattr', ()))
+ attrs.update(entry.get('ipapermincludedattr', ()))
+ if ('read' in entry.get('ipapermright', ()) and
+ 'objectclass' in (x.lower() for x in attrs)):
+ # Add special-cased operational attributes
+ # We want to allow reading these whenever reading the objectclass
+ # is allowed.
+ # (But they can still be excluded explicitly, at least in managed
+ # permissions).
+ attrs.update((u'entryusn', u'createtimestamp', u'modifytimestamp'))
+ attrs.difference_update(entry.get('ipapermexcludedattr', ()))
+ return sorted(attrs)
+
+ def make_aci(self, entry):
+ """Make an ACI string from the given permission entry"""
+
+ aci_parts = []
+ name = entry.single_value['cn']
+
+ # targetattr
+ attrs = self.get_effective_attrs(entry)
+ if attrs:
+ aci_parts.append("(targetattr = \"%s\")" % ' || '.join(attrs))
+
+ # target
+ ipapermtarget = entry.single_value.get('ipapermtarget')
+ if ipapermtarget:
+ aci_parts.append("(target = \"%s\")" %
+ 'ldap:///%s' % ipapermtarget)
+
+ # target_to
+ ipapermtargetto = entry.single_value.get('ipapermtargetto')
+ if ipapermtargetto:
+ aci_parts.append("(target_to = \"%s\")" %
+ 'ldap:///%s' % ipapermtargetto)
+
+ # target_from
+ ipapermtargetfrom = entry.single_value.get('ipapermtargetfrom')
+ if ipapermtargetfrom:
+ aci_parts.append("(target_from = \"%s\")" %
+ 'ldap:///%s' % ipapermtargetfrom)
+
+ # targetfilter
+ ipapermtargetfilter = entry.get('ipapermtargetfilter')
+ if ipapermtargetfilter:
+ assert all(f.startswith('(') and f.endswith(')')
+ for f in ipapermtargetfilter)
+ if len(ipapermtargetfilter) == 1:
+ filter = ipapermtargetfilter[0]
+ else:
+ filter = '(&%s)' % ''.join(sorted(ipapermtargetfilter))
+ aci_parts.append("(targetfilter = \"%s\")" % filter)
+
+ # version, name, rights, bind rule
+ ipapermbindruletype = entry.single_value.get('ipapermbindruletype',
+ 'permission')
+ if ipapermbindruletype == 'permission':
+ dn = DN(('cn', name), self.container_dn, self.api.env.basedn)
+ bindrule = 'groupdn = "ldap:///%s"' % dn
+ elif ipapermbindruletype == 'all':
+ bindrule = 'userdn = "ldap:///all"'
+ elif ipapermbindruletype == 'anonymous':
+ bindrule = 'userdn = "ldap:///anyone"'
+ else:
+ raise ValueError(ipapermbindruletype)
+
+ aci_parts.append('(version 3.0;acl "permission:%s";allow (%s) %s;)' % (
+ name, ','.join(sorted(entry['ipapermright'])), bindrule))
+
+ return ''.join(aci_parts)
+
+ def add_aci(self, permission_entry):
+ """Add the ACI coresponding to the given permission entry"""
+ ldap = self.api.Backend.ldap2
+ acistring = self.make_aci(permission_entry)
+ location = permission_entry.single_value.get('ipapermlocation',
+ self.api.env.basedn)
+
+ self.log.debug('Adding ACI %r to %s' % (acistring, location))
+ try:
+ entry = ldap.get_entry(location, ['aci'])
+ except errors.NotFound:
+ raise errors.NotFound(reason=_('Entry %s not found') % location)
+ entry.setdefault('aci', []).append(acistring)
+ ldap.update_entry(entry)
+
+ def remove_aci(self, permission_entry):
+ """Remove the ACI corresponding to the given permission entry
+
+ :return: tuple:
+ - entry
+ - removed ACI string, or None if none existed previously
+ """
+ return self._replace_aci(permission_entry)
+
+ def update_aci(self, permission_entry, old_name=None):
+ """Update the ACI corresponding to the given permission entry
+
+ :return: tuple:
+ - entry
+ - removed ACI string, or None if none existed previously
+ """
+ new_acistring = self.make_aci(permission_entry)
+ return self._replace_aci(permission_entry, old_name, new_acistring)
+
+ def _replace_aci(self, permission_entry, old_name=None, new_acistring=None):
+ """Replace ACI corresponding to permission_entry
+
+ :param old_name: the old name of the permission, if different from new
+ :param new_acistring: new ACI string; if None the ACI is just deleted
+ :return: tuple:
+ - entry
+ - removed ACI string, or None if none existed previously
+ """
+ ldap = self.api.Backend.ldap2
+ acientry, acistring = self._get_aci_entry_and_string(
+ permission_entry, old_name, notfound_ok=True)
+
+ # (pylint thinks `acientry` is just a dict, but it's an LDAPEntry)
+ acidn = acientry.dn # pylint: disable=E1103
+
+ if acistring is not None:
+ self.log.debug('Removing ACI %r from %s' % (acistring, acidn))
+ acientry['aci'].remove(acistring)
+ if new_acistring:
+ self.log.debug('Adding ACI %r to %s' % (new_acistring, acidn))
+ acientry.setdefault('aci', []).append(new_acistring)
+ try:
+ ldap.update_entry(acientry)
+ except errors.EmptyModlist:
+ self.log.debug('No changes to ACI')
+ return acientry, acistring
+
+ def _get_aci_entry_and_string(self, permission_entry, name=None,
+ notfound_ok=False, cached_acientry=None):
+ """Get the entry and ACI corresponding to the permission entry
+
+ :param name: The name of the permission, or None for the cn
+ :param notfound_ok:
+ If true, (acientry, None) will be returned on missing ACI, rather
+ than raising exception
+ :param cached_acientry: See upgrade_permission()
+ """
+ ldap = self.api.Backend.ldap2
+ if name is None:
+ name = permission_entry.single_value['cn']
+ location = permission_entry.single_value.get('ipapermlocation',
+ self.api.env.basedn)
+ wanted_aciname = 'permission:%s' % name
+
+ if (cached_acientry and
+ cached_acientry.dn == location and
+ 'aci' in cached_acientry):
+ acientry = cached_acientry
+ else:
+ try:
+ acientry = ldap.get_entry(location, ['aci'])
+ except errors.NotFound:
+ acientry = ldap.make_entry(location)
+ acis = acientry.get('aci', ())
+ for acistring in acis:
+ try:
+ aci = ACI(acistring)
+ except SyntaxError as e:
+ self.log.warning('Unparseable ACI %s: %s (at %s)',
+ acistring, e, location)
+ continue
+ if aci.name == wanted_aciname:
+ return acientry, acistring
+ else:
+ if notfound_ok:
+ return acientry, None
+ raise errors.NotFound(
+ reason=_('The ACI for permission %(name)s was not found '
+ 'in %(dn)s ') % {'name': name, 'dn': location})
+
+ def upgrade_permission(self, entry, target_entry=None,
+ output_only=False, cached_acientry=None):
+ """Upgrade the given permission entry to V2, in-place
+
+ The entry is only upgraded if it is a plain old-style permission,
+ that is, it has no flags set.
+
+ :param target_entry:
+ If given, ``target_entry`` is filled from information taken
+ from the ACI corresponding to ``entry``.
+ If None, ``entry`` itself is filled
+ :param output_only:
+ If true, the flags & objectclass are not updated to V2.
+ Used for the -find and -show commands.
+ :param cached_acientry:
+ Optional pre-retreived entry that contains the existing ACI.
+ If it is None or its DN does not match the location DN,
+ cached_acientry is ignored and the entry is retreived from LDAP.
+ """
+ if entry.get('ipapermissiontype'):
+ # Only convert old-style, non-SYSTEM permissions -- i.e. no flags
+ return
+ base, acistring = self._get_aci_entry_and_string(
+ entry, cached_acientry=cached_acientry)
+
+ if not target_entry:
+ target_entry = entry
+
+ # The DN of old permissions is always basedn
+ # (pylint thinks `base` is just a dict, but it's an LDAPEntry)
+ assert base.dn == self.api.env.basedn, base # pylint: disable=E1103
+
+ aci = ACI(acistring)
+
+ if 'target' in aci.target:
+ target_entry.single_value['ipapermtarget'] = DN(strip_ldap_prefix(
+ aci.target['target']['expression']))
+ if 'targetfilter' in aci.target:
+ target_entry.single_value['ipapermtargetfilter'] = unicode(
+ aci.target['targetfilter']['expression'])
+ if aci.bindrule['expression'] == 'ldap:///all':
+ target_entry.single_value['ipapermbindruletype'] = u'all'
+ elif aci.bindrule['expression'] == 'ldap:///anyone':
+ target_entry.single_value['ipapermbindruletype'] = u'anonymous'
+ else:
+ target_entry.single_value['ipapermbindruletype'] = u'permission'
+ target_entry['ipapermright'] = aci.permissions
+ if 'targetattr' in aci.target:
+ target_entry['ipapermincludedattr'] = [
+ unicode(a) for a in aci.target['targetattr']['expression']]
+
+ if not output_only:
+ target_entry['ipapermissiontype'] = ['SYSTEM', 'V2']
+ if 'ipapermissionv2' not in entry['objectclass']:
+ target_entry['objectclass'] = list(entry['objectclass']) + [
+ u'ipapermissionv2']
+
+ target_entry['ipapermlocation'] = [self.api.env.basedn]
+
+ # Make sure we're not losing *any info* by the upgrade
+ new_acistring = self.make_aci(target_entry)
+ if not ACI(new_acistring).isequal(aci):
+ raise ValueError('Cannot convert ACI, %r != %r' % (new_acistring,
+ acistring))
+
+ def make_type_filter(self, obj):
+ """Make a filter for a --type based permission from an Object"""
+ objectclasses = getattr(obj, 'permission_filter_objectclasses', None)
+ if not objectclasses:
+ return None
+ filters = [u'(objectclass=%s)' % o for o in objectclasses]
+ if len(filters) == 1:
+ return filters[0]
+ else:
+ return '(|%s)' % ''.join(sorted(filters))
+
+ def preprocess_options(self, options,
+ return_filter_ops=False,
+ merge_targetfilter=False):
+ """Preprocess options (in-place)
+
+ :param options: A dictionary of options
+ :param return_filter_ops:
+ If false, assumes there is no pre-existing entry;
+ additional values of ipapermtargetfilter are added to options.
+ If true, a dictionary of operations on ipapermtargetfilter is
+ returned.
+ These operations must be performed after the existing entry
+ is retrieved.
+ The dict has the following keys:
+ - remove: list of regular expression objects;
+ implicit values that match any of them should be removed
+ - add: list of values to be added, after any removals
+ :merge_targetfilter:
+ If true, the extratargetfilter is copied into ipapermtargetfilter.
+ """
+
+ if 'extratargetfilter' in options:
+ if 'ipapermtargetfilter' in options:
+ raise errors.ValidationError(
+ name='ipapermtargetfilter',
+ error=_('cannot specify full target filter '
+ 'and extra target filter simultaneously'))
+ if merge_targetfilter:
+ options['ipapermtargetfilter'] = options['extratargetfilter']
+
+ filter_ops = {'add': [], 'remove': []}
+
+ if options.get('subtree'):
+ if isinstance(options['subtree'], (list, tuple)):
+ [options['subtree']] = options['subtree']
+ try:
+ options['subtree'] = strip_ldap_prefix(options['subtree'])
+ except ValueError:
+ raise errors.ValidationError(
+ name='subtree',
+ error='does not start with "ldap:///"')
+
+ # Handle old options
+ for old_name, new_name in _DEPRECATED_OPTION_ALIASES.items():
+ if old_name in options:
+ if client_has_capability(options['version'], 'permissions2'):
+ raise errors.ValidationError(
+ name=old_name,
+ error=_('option was renamed; use %s') % new_name)
+ if new_name in options:
+ raise errors.ValidationError(
+ name=old_name,
+ error=(_('Cannot use %(old_name)s with %(new_name)s') %
+ {'old_name': old_name, 'new_name': new_name}))
+ options[new_name] = options[old_name]
+ del options[old_name]
+
+ # memberof
+ if 'memberof' in options:
+ filter_ops['remove'].append(re.compile(r'\(memberOf=.*\)', re.I))
+ memberof = options.pop('memberof')
+ for group in (memberof or ()):
+ try:
+ groupdn = self.api.Object.group.get_dn_if_exists(group)
+ except errors.NotFound:
+ raise errors.NotFound(
+ reason=_('%s: group not found') % group)
+ filter_ops['add'].append(u'(memberOf=%s)' % groupdn)
+
+ # targetgroup
+ if 'targetgroup' in options:
+ targetgroup = options.pop('targetgroup')
+ if targetgroup:
+ if 'ipapermtarget' in options:
+ raise errors.ValidationError(
+ name='ipapermtarget',
+ error=_('target and targetgroup are mutually exclusive'))
+ try:
+ groupdn = self.api.Object.group.get_dn_if_exists(targetgroup)
+ except errors.NotFound:
+ raise errors.NotFound(
+ reason=_('%s: group not found') % targetgroup)
+ options['ipapermtarget'] = groupdn
+ else:
+ if 'ipapermtarget' not in options:
+ options['ipapermtarget'] = None
+
+ # type
+ if 'type' in options:
+ objtype = options.pop('type')
+ filter_ops['remove'].append(re.compile(r'\(objectclass=.*\)', re.I))
+ filter_ops['remove'].append(re.compile(
+ r'\(\|(\(objectclass=[^(]*\))+\)', re.I))
+ if objtype:
+ if 'ipapermlocation' in options:
+ raise errors.ValidationError(
+ name='ipapermlocation',
+ error=_('subtree and type are mutually exclusive'))
+ obj = self.api.Object[objtype.lower()]
+ filt = self.make_type_filter(obj)
+ if not filt:
+ raise errors.ValidationError(
+ _('"%s" is not a valid permission type') % objtype)
+ filter_ops['add'].append(filt)
+ container_dn = DN(obj.container_dn, self.api.env.basedn)
+ options['ipapermlocation'] = container_dn
+ else:
+ if 'ipapermlocation' not in options:
+ options['ipapermlocation'] = None
+
+ if return_filter_ops:
+ return filter_ops
+ elif filter_ops['add']:
+ options['ipapermtargetfilter'] = list(options.get(
+ 'ipapermtargetfilter') or []) + filter_ops['add']
+
+ def validate_permission(self, entry):
+ ldap = self.Backend.ldap2
+
+ # Rough filter validation by a search
+ if entry.get('ipapermtargetfilter'):
+ try:
+ ldap.find_entries(
+ filter=ldap.combine_filters(entry['ipapermtargetfilter'],
+ rules='&'),
+ base_dn=self.env.basedn,
+ scope=ldap.SCOPE_BASE,
+ size_limit=1)
+ except errors.NotFound:
+ pass
+ except errors.BadSearchFilter:
+ raise errors.ValidationError(
+ name='ipapermtargetfilter',
+ error=_('Bad search filter'))
+
+ # Ensure location exists
+ if entry.get('ipapermlocation'):
+ location = DN(entry.single_value['ipapermlocation'])
+ try:
+ ldap.get_entry(location, attrs_list=[])
+ except errors.NotFound:
+ raise errors.ValidationError(
+ name='ipapermlocation',
+ error=_('Entry %s does not exist') % location)
+
+ # Ensure there's something in the ACI's filter
+ needed_attrs = (
+ 'ipapermtarget', 'ipapermtargetfilter',
+ 'ipapermincludedattr', 'ipapermexcludedattr', 'ipapermdefaultattr')
+ if not any(v for a in needed_attrs for v in (entry.get(a) or ())):
+ raise errors.ValidationError(
+ name='target',
+ error=_('there must be at least one target entry specifier '
+ '(e.g. target, targetfilter, attrs)'))
+
+ # Ensure there's a right
+ if not entry.get('ipapermright'):
+ raise errors.RequirementError(name='ipapermright')
+
+
+@register()
+class permission_add_noaci(baseldap.LDAPCreate):
+ __doc__ = _('Add a system permission without an ACI (internal command)')
+
+ msg_summary = _('Added permission "%(value)s"')
+ NO_CLI = True
+ has_output_params = baseldap.LDAPCreate.has_output_params + output_params
+
+ takes_options = (
+ Str('ipapermissiontype+',
+ label=_('Permission flags'),
+ ),
+ )
+
+ def get_options(self):
+ perm_options = set(o.name for o in self.obj.takes_params)
+ for option in super(permission_add_noaci, self).get_options():
+ # From new options, only cn & ipapermissiontype are supported
+ if option.name in ['ipapermissiontype']:
+ yield option.clone()
+ # Other options such as raw, version are supported
+ elif option.name not in perm_options:
+ yield option.clone()
+
+ def pre_callback(self, ldap, dn, entry, attrs_list, *keys, **options):
+ entry['ipapermissiontype'] = list(options['ipapermissiontype'])
+ entry['objectclass'] = [oc for oc in entry['objectclass']
+ if oc.lower() != 'ipapermissionv2']
+ return dn
+
+
+@register()
+class permission_add(baseldap.LDAPCreate):
+ __doc__ = _('Add a new permission.')
+
+ msg_summary = _('Added permission "%(value)s"')
+ has_output_params = baseldap.LDAPCreate.has_output_params + output_params
+
+ # Need to override execute so that processed options apply to
+ # the whole command, not just the callbacks
+ def execute(self, *keys, **options):
+ self.obj.preprocess_options(options, merge_targetfilter=True)
+ 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)
+ if not entry.get('ipapermlocation'):
+ entry.setdefault('ipapermlocation', [api.env.basedn])
+
+ if 'attrs' in options:
+ if 'ipapermincludedattr' in options:
+ raise errors.ValidationError(
+ name='attrs',
+ error=_('attrs and included attributes are '
+ 'mutually exclusive'))
+ entry['ipapermincludedattr'] = list(options.pop('attrs') or ())
+
+ self.obj.validate_permission(entry)
+ return dn
+
+ def post_callback(self, ldap, dn, entry, *keys, **options):
+ try:
+ self.obj.add_aci(entry)
+ except Exception as e:
+ # Adding the ACI failed.
+ # We want to be 100% sure the ACI is not there, so try to
+ # remove it. (This is a no-op if the ACI was not added.)
+ self.obj.remove_aci(entry)
+ # Remove the entry.
+ # The permission entry serves as a "lock" tho prevent
+ # permission-add commands started at the same time from
+ # interfering. As long as the entry is there, the other
+ # permission-add will fail with DuplicateEntry.
+ # So deleting entry ("releasing the lock") must be the last
+ # thing we do here.
+ try:
+ self.api.Backend['ldap2'].delete_entry(entry)
+ except errors.NotFound:
+ pass
+ if isinstance(e, errors.NotFound):
+ # add_aci may raise NotFound if the subtree is only virtual
+ # like cn=compat,SUFFIX and thus passes the LDAP get entry test
+ location = DN(entry.single_value['ipapermlocation'])
+ raise errors.ValidationError(
+ name='ipapermlocation',
+ error=_('Cannot store permission ACI to %s') % location)
+ # Re-raise original exception
+ raise
+ self.obj.postprocess_result(entry, options)
+ return dn
+
+
+@register()
+class permission_del(baseldap.LDAPDelete):
+ __doc__ = _('Delete a permission.')
+
+ msg_summary = _('Deleted permission "%(value)s"')
+
+ takes_options = baseldap.LDAPDelete.takes_options + (
+ Flag('force',
+ label=_('Force'),
+ flags={'no_option', 'no_output'},
+ doc=_('force delete of SYSTEM permissions'),
+ ),
+ )
+
+ def pre_callback(self, ldap, dn, *keys, **options):
+ try:
+ entry = ldap.get_entry(dn, attrs_list=self.obj.default_attributes)
+ except errors.NotFound:
+ self.obj.handle_not_found(*keys)
+
+ if not options.get('force'):
+ self.obj.reject_system(entry)
+ if entry.get('ipapermdefaultattr'):
+ raise errors.ACIError(
+ info=_('cannot delete managed permissions'))
+
+ try:
+ self.obj.remove_aci(entry)
+ except errors.NotFound:
+ errors.NotFound(
+ reason=_('ACI of permission %s was not found') % keys[0])
+
+ return dn
+
+
+@register()
+class permission_mod(baseldap.LDAPUpdate):
+ __doc__ = _('Modify a permission.')
+
+ msg_summary = _('Modified permission "%(value)s"')
+ has_output_params = baseldap.LDAPUpdate.has_output_params + output_params
+
+ def execute(self, *keys, **options):
+ context.filter_ops = self.obj.preprocess_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',
+ error='New name can not be empty')
+
+ try:
+ attrs_list = self.obj.default_attributes
+ old_entry = ldap.get_entry(dn, attrs_list=attrs_list)
+ except errors.NotFound:
+ self.obj.handle_not_found(*keys)
+
+ self.obj.reject_system(old_entry)
+ self.obj.upgrade_permission(old_entry)
+
+ if 'MANAGED' in old_entry.get('ipapermissiontype', ()):
+ for option_name in sorted(options):
+ if option_name == 'rename':
+ raise errors.ValidationError(
+ name=option_name,
+ error=_('cannot rename managed permissions'))
+ option = self.options[option_name]
+ allow_mod = 'allow_mod_for_managed_permission' in option.flags
+ if (option.attribute and not allow_mod or
+ option_name == 'extratargetfilter'):
+ raise errors.ValidationError(
+ name=option_name,
+ error=_('not modifiable on managed permissions'))
+ if context.filter_ops.get('add'):
+ raise errors.ValidationError(
+ name='ipapermtargetfilter',
+ error=_('not modifiable on managed permissions'))
+ else:
+ if options.get('ipapermexcludedattr'):
+ # prevent setting excluded attributes on normal permissions
+ # (but do allow deleting them all)
+ raise errors.ValidationError(
+ name='ipapermexcludedattr',
+ error=_('only available on managed permissions'))
+
+ if 'attrs' in options:
+ if any(a in options for a in ('ipapermincludedattr',
+ 'ipapermexcludedattr')):
+ raise errors.ValidationError(
+ name='attrs',
+ error=_('attrs and included/excluded attributes are '
+ 'mutually exclusive'))
+ attrs = set(options.pop('attrs') or ())
+ defaults = set(old_entry.get('ipapermdefaultattr', ()))
+ entry['ipapermincludedattr'] = list(attrs - defaults)
+ entry['ipapermexcludedattr'] = list(defaults - attrs)
+
+ # Check setting bindtype for an assigned permission
+ if options.get('ipapermbindruletype') and old_entry.get('member'):
+ raise errors.ValidationError(
+ name='ipapermbindruletype',
+ error=_('cannot set bindtype for a permission that is '
+ 'assigned to a privilege'))
+
+ # Since `entry` only contains the attributes we are currently changing,
+ # it cannot be used directly to generate an ACI.
+ # First we need to copy the original data into it.
+ for key, value in old_entry.items():
+ if (key not in options and
+ key != 'cn' and
+ key not in self.obj.attribute_members):
+ entry.setdefault(key, value)
+
+ # For extratargetfilter, add it to the implicit filters
+ # to get the full target filter
+ if 'extratargetfilter' in options:
+ filter_attr_info = self.obj._get_filter_attr_info(entry)
+ entry['ipapermtargetfilter'] = (
+ list(options['extratargetfilter'] or []) +
+ list(filter_attr_info['implicit_targetfilters']))
+
+ filter_ops = context.filter_ops
+ old_filter_attr_info = self.obj._get_filter_attr_info(old_entry)
+ old_implicit_filters = old_filter_attr_info['implicit_targetfilters']
+ removes = filter_ops.get('remove', [])
+ new_filters = set(
+ filt for filt in (entry.get('ipapermtargetfilter') or [])
+ if filt not in old_implicit_filters or
+ not any(rem.match(filt) for rem in removes))
+ new_filters.update(filter_ops.get('add', []))
+ new_filters.update(options.get('ipapermtargetfilter') or [])
+ entry['ipapermtargetfilter'] = list(new_filters)
+
+ if not entry.get('ipapermlocation'):
+ entry['ipapermlocation'] = [self.api.env.basedn]
+
+ self.obj.validate_permission(entry)
+
+ old_location = old_entry.single_value.get('ipapermlocation',
+ self.api.env.basedn)
+ if old_location == options.get('ipapermlocation', old_location):
+ context.permision_moving_aci = False
+ else:
+ context.permision_moving_aci = True
+ try:
+ context.old_aci_info = self.obj.remove_aci(old_entry)
+ except errors.NotFound as e:
+ self.log.error('permission ACI not found: %s' % e)
+
+ # To pass data to postcallback, we currently need to use the context
+ context.old_entry = old_entry
+
+ return dn
+
+ def exc_callback(self, keys, options, exc, call_func, *call_args, **call_kwargs):
+ if call_func.__name__ == 'update_entry':
+ self._revert_aci()
+ raise exc
+
+ def _revert_aci(self):
+ old_aci_info = getattr(context, 'old_aci_info', None)
+ if old_aci_info:
+ # Try to roll back the old ACI
+ entry, old_aci_string = old_aci_info
+ if old_aci_string:
+ self.log.warning('Reverting ACI on %s to %s' % (entry.dn,
+ old_aci_string))
+ entry['aci'].append(old_aci_string)
+ self.Backend.ldap2.update_entry(entry)
+
+ def post_callback(self, ldap, dn, entry, *keys, **options):
+ old_entry = context.old_entry
+
+ try:
+ if context.permision_moving_aci:
+ self.obj.add_aci(entry)
+ else:
+ self.obj.update_aci(entry, old_entry.single_value['cn'])
+ except Exception:
+ # Don't revert attribute which doesn't exist in LDAP
+ entry.pop('attributelevelrights', None)
+
+ self.log.error('Error updating ACI: %s' % traceback.format_exc())
+ self.log.warning('Reverting entry')
+ old_entry.reset_modlist(entry)
+ ldap.update_entry(old_entry)
+ self._revert_aci()
+ raise
+ self.obj.postprocess_result(entry, options)
+ entry['dn'] = entry.dn
+ return dn
+
+
+@register()
+class permission_find(baseldap.LDAPSearch):
+ __doc__ = _('Search for permissions.')
+
+ msg_summary = ngettext(
+ '%(count)d permission matched', '%(count)d permissions matched', 0)
+ has_output_params = baseldap.LDAPSearch.has_output_params + output_params
+
+ def execute(self, *keys, **options):
+ self.obj.preprocess_options(options, merge_targetfilter=True)
+ return super(permission_find, self).execute(*keys, **options)
+
+ def pre_callback(self, ldap, filters, attrs_list, base_dn, scope,
+ *args, **options):
+ if 'attrs' in options and 'ipapermincludedattr' in options:
+ raise errors.ValidationError(
+ name='attrs',
+ error=_('attrs and included/excluded attributes are '
+ 'mutually exclusive'))
+
+ if options.get('attrs'):
+ # Effective attributes:
+ # each attr must be in either default or included,
+ # but not in excluded
+ filters = ldap.combine_filters(
+ [filters] + [
+ '(&'
+ '(|'
+ '(ipapermdefaultattr=%(attr)s)'
+ '(ipapermincludedattr=%(attr)s))'
+ '(!(ipapermexcludedattr=%(attr)s)))' % {'attr': attr}
+ for attr in options['attrs']
+ ],
+ ldap.MATCH_ALL,
+ )
+
+ return filters, base_dn, scope
+
+ def post_callback(self, ldap, entries, truncated, *args, **options):
+ if 'attrs' in options:
+ options['ipapermincludedattr'] = options['attrs']
+
+ attribute_options = [o for o in options
+ if (o in self.options and
+ self.options[o].attribute)]
+
+ if not options.get('pkey_only'):
+ for entry in entries:
+ # Old-style permissions might have matched (e.g. by name)
+ self.obj.upgrade_permission(entry, output_only=True)
+
+ if not truncated:
+ if 'sizelimit' in options:
+ max_entries = options['sizelimit']
+ else:
+ max_entries = self.api.Backend.ldap2.size_limit
+
+ filters = ['(objectclass=ipaPermission)',
+ '(!(ipaPermissionType=V2))']
+ 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)
+ if options.get('all'):
+ attrs_list.append('*')
+ try:
+ legacy_entries = ldap.get_entries(
+ base_dn=DN(self.obj.container_dn, self.api.env.basedn),
+ filter=ldap.combine_filters(filters, rules=ldap.MATCH_ALL),
+ attrs_list=attrs_list)
+ # Retrieve the root entry (with all legacy ACIs) at once
+ root_entry = ldap.get_entry(DN(api.env.basedn), ['aci'])
+ except errors.NotFound:
+ legacy_entries = ()
+ cached_root_entry = None
+ self.log.debug('potential legacy entries: %s', len(legacy_entries))
+ nonlegacy_names = {e.single_value['cn'] for e in entries}
+ for entry in legacy_entries:
+ if entry.single_value['cn'] in nonlegacy_names:
+ continue
+ if max_entries > 0 and len(entries) > max_entries:
+ # We've over the limit, pop the last entry and set
+ # truncated flag
+ # (this is easier to do than checking before adding
+ # the entry to results)
+ # (max_entries <= 0 means unlimited)
+ entries.pop()
+ truncated = True
+ break
+ self.obj.upgrade_permission(entry, output_only=True,
+ cached_acientry=root_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:
+ # 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)
+
+ for entry in entries:
+ if options.get('pkey_only'):
+ for opt_name in list(entry):
+ if opt_name != self.obj.primary_key.name:
+ del entry[opt_name]
+ else:
+ self.obj.postprocess_result(entry, options)
+
+ return truncated
+
+
+@register()
+class permission_show(baseldap.LDAPRetrieve):
+ __doc__ = _('Display information about a permission.')
+ has_output_params = baseldap.LDAPRetrieve.has_output_params + output_params
+
+ def post_callback(self, ldap, dn, entry, *keys, **options):
+ self.obj.upgrade_permission(entry, output_only=True)
+ self.obj.postprocess_result(entry, options)
+ return dn
+
+
+@register()
+class permission_add_member(baseldap.LDAPAddMember):
+ """Add members to a permission."""
+ NO_CLI = True
+
+ def pre_callback(self, ldap, dn, member_dns, failed, *keys, **options):
+ # We can only add permissions with bind rule type set to
+ # "permission" (or old-style permissions)
+ validate_permission_to_privilege(self.api, keys[-1])
+ return dn
+
+
+@register()
+class permission_remove_member(baseldap.LDAPRemoveMember):
+ """Remove members from a permission."""
+ NO_CLI = True