From d7ee87cfa1e288fe18dc2dbeb2d691753048f4db Mon Sep 17 00:00:00 2001 From: Petr Viktorin Date: Wed, 13 Nov 2013 16:31:58 +0100 Subject: Rewrite the Permission plugin Ticket: https://fedorahosted.org/freeipa/ticket/3566 Design: http://www.freeipa.org/page/V3/Permissions_V2 --- ipalib/capabilities.py | 6 +- ipalib/plugins/dns.py | 2 +- ipalib/plugins/permission.py | 1038 ++++++++++++++++++++++++++++-------------- 3 files changed, 709 insertions(+), 337 deletions(-) (limited to 'ipalib') diff --git a/ipalib/capabilities.py b/ipalib/capabilities.py index 6fcc436d..b60a570c 100644 --- a/ipalib/capabilities.py +++ b/ipalib/capabilities.py @@ -40,7 +40,11 @@ capabilities = dict( # a user with UID=999. With the capability, these parameters are optional # and 999 really means 999. # https://fedorahosted.org/freeipa/ticket/2886 - optional_uid_params=u'2.54' + optional_uid_params=u'2.54', + + # permissions2: Reworked permission system + # http://www.freeipa.org/page/V3/Permissions_V2 + permissions2=u'2.69', ) diff --git a/ipalib/plugins/dns.py b/ipalib/plugins/dns.py index 07523dc7..19811d7f 100644 --- a/ipalib/plugins/dns.py +++ b/ipalib/plugins/dns.py @@ -2030,7 +2030,7 @@ class dnszone_add_permission(LDAPQuery): permission_name = self.obj.permission_name(keys[-1]) permission = api.Command['permission_add_noaci'](permission_name, - permissiontype=u'SYSTEM' + ipapermissiontype=u'SYSTEM' )['result'] update = {} diff --git a/ipalib/plugins/permission.py b/ipalib/plugins/permission.py index 0f1fe667..ff5335d1 100644 --- a/ipalib/plugins/permission.py +++ b/ipalib/plugins/permission.py @@ -1,7 +1,7 @@ # Authors: -# Rob Crittenden +# Petr Viktorin # -# Copyright (C) 2010 Red Hat +# 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 @@ -17,106 +17,130 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . -from ipalib.plugins.baseldap import * +import re + +from ipalib.plugins import baseldap +from ipalib import errors +from ipalib.parameters import Str, StrEnum, DNParam, Flag from ipalib import api, _, ngettext -from ipalib import Flag, Str, StrEnum 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 -from ipalib import errors -from ipapython.dn import DN, EditableDN __doc__ = _(""" Permissions - +""" + """ A permission enables fine-grained delegation of rights. A permission is -a human-readable form of a 389-ds Access Control Rule, or instruction (ACI). +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 or delete. +""" + """ +* 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. add - add a new entry to the tree -4. delete - delete an existing entry -5. all - all permissions are granted - -Read permission is granted for most attributes by default so the read -permission is not expected to be used very often. - +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. type: a type of object (user, group, etc). -2. memberof: a member of a group or hostgroup -3. filter: an LDAP filter -4. subtree: an LDAP filter specifying part of the LDAP DIT. This is a - super-set of the "type" target. -5. targetgroup: grant access to modify a specific group (such as granting - the rights to manage group membership) - +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. +""" + """ 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" """) -ACI_PREFIX=u"permission" - register = Registry() +VALID_OBJECT_TYPES = (u'user', u'group', u'host', u'service', u'hostgroup', + u'netgroup', u'dnsrecord',) + +_DEPRECATED_OPTION_ALIASES = { + 'permissions': 'ipapermright', + 'attrs': 'ipapermallowedattr', + 'filter': 'ipapermtargetfilter', + 'subtree': 'ipapermlocation', +} + +KNOWN_FLAGS = {'SYSTEM', 'V2'} + output_params = ( - Str('ipapermissiontype', - label=_('Permission Type'), - ), Str('aci', label=_('ACI'), ), ) -def filter_options(options, keys): - """Return a dict that includes entries from `options` that are in `keys` - example: - >>> filtered = filter_options({'a': 1, 'b': 2, 'c': 3}, ['a', 'c']) - >>> filtered == {'a': 1, 'c': 3} - True +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):] + + +class DNOrURL(DNParam): + """DN parameter that allows, and strips, a "ldap:///" prefix on input + + Used for ``subtree`` to maintain backward compatibility. """ - return dict((k, options[k]) for k in keys if k in options) + + def _convert_scalar(self, value, index=None): + if isinstance(value, basestring) and value.startswith('ldap:///'): + value = strip_ldap_prefix(value) + return super(DNOrURL, self)._convert_scalar(value, index=index) @register() -class permission(LDAPObject): +class permission(baseldap.LDAPObject): """ Permission object. """ container_dn = api.env.container_permission object_name = _('permission') object_name_plural = _('permissions') - object_class = ['groupofnames', 'ipapermission'] + object_class = ['groupofnames', 'ipapermission', 'ipapermissionv2'] default_attributes = ['cn', 'member', 'memberof', - 'memberindirect', 'ipapermissiontype', - ] - aci_attributes = ['aci', 'group', 'permissions', 'attrs', 'type', - 'filter', 'subtree', 'targetgroup', 'memberof', + 'memberindirect', 'ipapermissiontype', 'objectclass', + 'ipapermdefaultattr', 'ipapermallowedattr', 'ipapermexcludedattr', + 'ipapermbindruletype', 'ipapermlocation', 'ipapermright', + 'ipapermtargetfilter', 'ipapermtarget' ] attribute_members = { 'member': ['privilege'], @@ -132,396 +156,740 @@ class permission(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", ), - Str('permissions+', + StrEnum( + 'ipapermright*', cli_name='permissions', label=_('Permissions'), - doc=_('Permissions to grant ' \ - '(read, write, add, delete, all)'), - csv=True, + 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'), ), - Str('attrs*', + Str('ipapermallowedattr*', cli_name='attrs', label=_('Attributes'), doc=_('Attributes to which the permission applies'), - csv=True, - normalizer=lambda value: value.lower(), - flags=('ask_create'), ), - StrEnum('type?', - cli_name='type', - label=_('Type'), - doc=_('Type of IPA object (user, group, host, hostgroup, service, netgroup, dns)'), - values=(u'user', u'group', u'host', u'service', u'hostgroup', u'netgroup', u'dnsrecord',), - flags=('ask_create'), - ), - Str('memberof?', - cli_name='memberof', - label=_('Member of group'), # FIXME: Does this label make sense? - doc=_('Target members of a group'), - flags=('ask_create'), + StrEnum( + 'ipapermbindruletype', + cli_name='bindtype', + label=_('Bind rule type'), + doc=_('Bind rule type'), + autofill=True, + values=(u'permission',), + default=u'permission', ), - Str('filter?', - cli_name='filter', - label=_('Filter'), - doc=_('Legal LDAP filter (e.g. ou=Engineering)'), - flags=('ask_create'), - ), - Str('subtree?', + DNOrURL( + 'ipapermlocation?', cli_name='subtree', label=_('Subtree'), doc=_('Subtree to apply permissions to'), - flags=('ask_create'), + default=api.env.basedn, + flags={'ask_create'}, + ), + Str( + 'ipapermtargetfilter?', + cli_name='filter', + label=_('ACI target filter'), + doc=_('ACI target filter'), + ), + + DNParam( + 'ipapermtarget?', + cli_name='target', + label=_('ACI target DN'), + flags={'no_option'} + ), + + Str('memberof?', + label=_('Member of group'), # FIXME: Does this label make sense? + doc=_('Target members of a group (sets targetfilter)'), + flags={'ask_create', 'virtual_attribute'}, ), Str('targetgroup?', - cli_name='targetgroup', label=_('Target group'), - doc=_('User group to apply permissions to'), - flags=('ask_create'), + doc=_('User group to apply permissions to (sets target)'), + flags={'ask_create', 'virtual_attribute'}, + ), + StrEnum( + 'type?', + label=_('Type'), + doc=_('Type of IPA object (sets subtree and filter)'), + values=VALID_OBJECT_TYPES, + 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() ) - # Don't allow SYSTEM permissions to be modified or removed - def check_system(self, ldap, dn, *keys): + 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 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``. + """ + if not options.get('raw') and not options.get('pkey_only'): + ipapermtargetfilter = entry.single_value.get('ipapermtargetfilter', + '') + ipapermtarget = entry.single_value.get('ipapermtarget') + ipapermlocation = entry.single_value.get('ipapermlocation') + + # memberof + match = re.match('^\(memberof=(.*)\)$', ipapermtargetfilter, re.I) + if match: + dn = DN(match.group(1)) + if dn[1:] == DN(self.api.Object.group.container_dn, + self.api.env.basedn)[:] and dn[0].attr == 'cn': + entry.single_value['memberof'] = dn[0].value + + # 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 + + # type + if ipapermtarget and ipapermlocation: + for objname in VALID_OBJECT_TYPES: + obj = self.api.Object[objname] + wantdn = DN(obj.container_dn, self.api.env.basedn) + if DN(ipapermlocation) == wantdn: + targetdn = DN( + (obj.rdn_attribute or obj.primary_key.name, '*'), + obj.container_dn, + self.api.env.basedn) + if ipapermtarget == targetdn: + entry.single_value['type'] = objname + break + + # old output names + if not client_has_capability(options['version'], 'permissions2'): + 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: + rights['memberof'] = rights['ipapermtargetfilter'] + rights['targetgroup'] = rights['ipapermtarget'] + + type_rights = set(rights['ipapermtarget']) + type_rights.intersection_update(rights['ipapermlocation']) + rights['type'] = ''.join(sorted(type_rights, + key=rights['ipapermtarget'].index)) + + if not client_has_capability(options['version'], 'permissions2'): + 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 + acientry, acistring = self._get_aci_entry_and_string(entry) + entry.single_value['aci'] = acistring + + if not client_has_capability(options['version'], 'permissions2'): + # Legacy clients expect some attributes as a single value + for attr in 'type', 'targetgroup', 'memberof', 'aci': + if attr in entry: + entry[attr] = entry.single_value[attr] + 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 + + def make_aci(self, entry): + """Make an ACI string from the given permission entry""" + + aci = ACI() + name = entry.single_value['cn'] + aci.name = 'permission:%s' % name + ipapermtarget = entry.single_value.get('ipapermtarget') + if ipapermtarget: + aci.set_target('ldap:///%s' % ipapermtarget) + ipapermtargetfilter = entry.single_value.get('ipapermtargetfilter') + if ipapermtargetfilter: + aci.set_target_filter(ipapermtargetfilter) + + ipapermbindruletype = entry.single_value.get('ipapermbindruletype', + 'permission') + if ipapermbindruletype == 'permission': + dn = DN(('cn', name), self.container_dn, self.api.env.basedn) + aci.set_bindrule('groupdn = "ldap:///%s"' % dn) + elif ipapermbindruletype == 'all': + aci.set_bindrule('userdn = "ldap:///all"') + elif ipapermbindruletype == 'anonymous': + aci.set_bindrule('userdn = "ldap:///anyone"') + else: + raise ValueError(ipapermbindruletype) + aci.permissions = entry['ipapermright'] + aci.set_target_attr(entry.get('ipapermallowedattr', [])) + + return aci.export_to_string() + + 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)) + entry = ldap.get_entry(location, ['aci']) + entry.setdefault('aci', []).append(acistring) + ldap.update_entry(entry) + + def remove_aci(self, permission_entry): + """Remove the ACI corresponding to the given permission entry""" + self._replace_aci(permission_entry) + + def update_aci(self, permission_entry, old_name=None): + """Update the ACI corresponding to the given permission entry""" + new_acistring = self.make_aci(permission_entry) + 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 + """ + 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['aci'].append(new_acistring) try: - (dn, entry_attrs) = ldap.get_entry(dn, ['ipapermissiontype']) + ldap.update_entry(acientry) + except errors.EmptyModlist: + self.log.info('No changes to ACI') + + def _get_aci_entry_and_string(self, permission_entry, name=None, + notfound_ok=False): + """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 + """ + 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 + + try: + acientry = ldap.get_entry(location, ['aci']) except errors.NotFound: - self.handle_not_found(*keys) - if 'ipapermissiontype' in entry_attrs: - if 'SYSTEM' in entry_attrs['ipapermissiontype']: - return False - return True + acientry = {} + acis = acientry.get('aci', ()) + for acistring in acis: + aci = ACI(acistring) + 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): + """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 are not updated to V2. + Used for the -find and -show commands. + """ + 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) + + 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 'targetattr' in aci.target: + target_entry['ipapermallowedattr'] = ( + aci.target['targetattr']['expression']) + 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['ipapermallowedattr'] = [ + unicode(a) for a in aci.target['targetattr']['expression']] - def filter_aci_attributes(self, options): - """Return option dictionary that only includes ACI attributes""" - return dict((k, v) for k, v in options.items() if - k in self.aci_attributes) + if not output_only: + target_entry['ipapermissiontype'] = ['SYSTEM', 'V2'] + if 'ipapermissionv2' not in entry['objectclass']: + target_entry['objectclass'] = list(entry['objectclass']) + [ + u'ipapermissionv2'] -@register() -class permission_add(LDAPCreate): - __doc__ = _('Add a new permission.') + target_entry['ipapermlocation'] = [self.api.env.basedn] - msg_summary = _('Added permission "%(value)s"') - has_output_params = LDAPCreate.has_output_params + output_params - - def pre_callback(self, ldap, dn, entry_attrs, attrs_list, *keys, **options): - assert isinstance(dn, DN) - # Test the ACI before going any further - opts = self.obj.filter_aci_attributes(options) - opts['test'] = True - opts['permission'] = keys[-1] - opts['aciprefix'] = ACI_PREFIX - self.api.Command.aci_add(keys[-1], **opts) - - # Clear the aci attributes out of the permission entry - for o in options: - try: - if o not in ['objectclass']: - del entry_attrs[o] - except: - pass - return dn + # 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 post_callback(self, ldap, dn, entry_attrs, *keys, **options): - assert isinstance(dn, DN) - # Now actually add the aci. - opts = self.obj.filter_aci_attributes(options) - opts['test'] = False - opts['permission'] = keys[-1] - opts['aciprefix'] = ACI_PREFIX - try: - result = self.api.Command.aci_add(keys[-1], **opts)['result'] - for attr in self.obj.aci_attributes: - if attr in result: - entry_attrs[attr] = result[attr] - except errors.InvalidSyntax, e: - # A syntax error slipped past our attempt at validation, clean up - self.api.Command.permission_del(keys[-1]) - raise e - except Exception, e: - # Something bad happened, clean up as much as we can and return - # that error + def preprocess_options(self, options): + """Preprocess options (in-place)""" + + if options.get('subtree'): + if isinstance(options['subtree'], (list, tuple)): + [options['subtree']] = options['subtree'] try: - self.api.Command.permission_del(keys[-1]) - except Exception, ignore: - pass + 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: + memberof = options.pop('memberof') + if memberof: + if 'ipapermtargetfilter' in options: + raise errors.ValidationError( + name='ipapermtargetfilter', + error=_('filter and memberof are mutually exclusive')) + try: + groupdn = self.api.Object.group.get_dn_if_exists(memberof) + except errors.NotFound: + raise errors.NotFound( + reason=_('%s: group not found') % memberof) + options['ipapermtargetfilter'] = u'(memberOf=%s)' % groupdn + else: + if 'ipapermtargetfilter' not in options: + options['ipapermtargetfilter'] = None + + # 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') + if objtype: + if 'ipapermlocation' in options: + raise errors.ValidationError( + name='ipapermlocation', + error=_('subtree and type are mutually exclusive')) + if 'ipapermtarget' in options: + raise errors.ValidationError( + name='ipapermtarget', + error=_('target and type are mutually exclusive')) + obj = self.api.Object[objtype.lower()] + container_dn = DN(obj.container_dn, self.api.env.basedn) + options['ipapermtarget'] = DN( + (obj.rdn_attribute or obj.primary_key.name, '*'), + container_dn) + options['ipapermlocation'] = container_dn + else: + if 'ipapermtarget' not in options: + options['ipapermtarget'] = None + if 'ipapermlocation' not in options: + options['ipapermlocation'] = None + + def validate_permission(self, entry): + ldap = self.Backend.ldap2 + + # Rough filter validation by a search + if 'ipapermtargetfilter' in entry: try: - self.api.Command.aci_del(keys[-1], aciprefix=ACI_PREFIX) - except Exception, ignore: + ldap.find_entries( + filter=entry.single_value['ipapermtargetfilter'], + base_dn=self.env.basedn, + scope=ldap.SCOPE_BASE, + size_limit=1) + except errors.NotFound: pass - raise e - return dn + except errors.BadSearchFilter: + raise errors.ValidationError( + name='ipapermtargetfilter', + error=_('Bad search filter')) + + # Ensure there's something in the ACI's filter + needed_attrs = ( + 'ipapermtarget', 'ipapermtargetfilter', 'ipapermallowedattr') + if not any(entry.single_value.get(a) for a in needed_attrs): + 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(LDAPCreate): - __doc__ = _('Add a system permission without an ACI') +class permission_add_noaci(baseldap.LDAPCreate): + __doc__ = _('Add a system permission without an ACI (internal command)') msg_summary = _('Added permission "%(value)s"') - has_output_params = LDAPCreate.has_output_params + output_params NO_CLI = True + has_output_params = baseldap.LDAPCreate.has_output_params + output_params takes_options = ( - StrEnum('permissiontype?', - label=_('Permission type'), - values=(u'SYSTEM',), + Str('ipapermissiontype+', + label=_('Permission flags'), ), ) - def get_args(self): - # do not validate system permission names - yield self.obj.primary_key.clone(pattern=None, pattern_errmsg=None) - 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(): - # filter out ACI options - if option.name in self.obj.aci_attributes: - continue - yield option - - def pre_callback(self, ldap, dn, entry_attrs, attrs_list, *keys, **options): - assert isinstance(dn, DN) - permission_type = options.get('permissiontype') - if permission_type: - entry_attrs['ipapermissiontype'] = [ permission_type ] + # 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_del(LDAPDelete): +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 args_options_2_params so that processed options apply to + # the whole command, not just the callbacks + def args_options_2_params(self, *args, **options): + if self.env.in_server: + self.obj.preprocess_options(options) + + return super(permission_add, self).args_options_2_params( + *args, **options) + + 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]) + + self.obj.validate_permission(entry) + return dn + + def post_callback(self, ldap, dn, entry, *keys, **options): + self.obj.add_aci(entry) + 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 = LDAPDelete.takes_options + ( + takes_options = baseldap.LDAPDelete.takes_options + ( Flag('force', label=_('Force'), - flags=['no_option', 'no_output'], + flags={'no_option', 'no_output'}, doc=_('force delete of SYSTEM permissions'), ), ) def pre_callback(self, ldap, dn, *keys, **options): - assert isinstance(dn, DN) - if not options.get('force') and not self.obj.check_system(ldap, dn, *keys): - raise errors.ACIError( - info=_('A SYSTEM permission may not be removed')) - # remove permission even when the underlying ACI is missing try: - self.api.Command.aci_del(keys[-1], aciprefix=ACI_PREFIX) + entry = ldap.get_entry(dn, attrs_list=self.obj.default_attributes) except errors.NotFound: - pass + self.obj.handle_not_found(*keys) + + if not options.get('force'): + self.obj.reject_system(entry) + + try: + self.obj.remove_aci(entry) + except errors.NotFound: + errors.NotFound('ACI of permission %s was not found' % keys[0]) + return dn @register() -class permission_mod(LDAPUpdate): +class permission_mod(baseldap.LDAPUpdate): __doc__ = _('Modify a permission.') msg_summary = _('Modified permission "%(value)s"') - has_output_params = LDAPUpdate.has_output_params + output_params + has_output_params = baseldap.LDAPUpdate.has_output_params + output_params - def pre_callback(self, ldap, dn, entry_attrs, attrs_list, *keys, **options): - assert isinstance(dn, DN) - if not self.obj.check_system(ldap, dn, *keys): - raise errors.ACIError( - info=_('A SYSTEM permission may not be modified')) + def args_options_2_params(self, *args, **options): + if self.env.in_server: + self.obj.preprocess_options(options) + + return super(permission_mod, self).args_options_2_params( + *args, **options) + + 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') - # check if permission is in LDAP try: - (dn, attrs) = ldap.get_entry(dn, attrs_list) + 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) - # when renaming permission, check if the target permission does not - # exists already. Then, make changes to underlying ACI - if 'rename' in options: - if options['rename']: - try: - try: - new_dn = EditableDN(dn) - new_dn[0]['cn'] # assure the first RDN has cn as it's type - except (IndexError, KeyError), e: - raise ValueError("expected dn starting with 'cn=' but got '%s'" % dn) - new_dn[0].value = options['rename'] - entry = ldap.get_entry(new_dn, attrs_list) - raise errors.DuplicateEntry() - except errors.NotFound: - pass # permission may be renamed, continue - else: - raise errors.ValidationError( - name='rename', error=_('New name can not be empty')) - - opts = self.obj.filter_aci_attributes(options) - setattr(context, 'aciupdate', False) - # If there are no options left we don't need to do anything to the - # underlying ACI. - if len(opts) > 0: - opts['permission'] = keys[-1] - opts['aciprefix'] = ACI_PREFIX - self.api.Command.aci_mod(keys[-1], **opts) - setattr(context, 'aciupdate', True) - - # Clear the aci attributes out of the permission entry - for o in self.obj.aci_attributes: - try: - del entry_attrs[o] - except: - pass + self.obj.reject_system(old_entry) + self.obj.upgrade_permission(old_entry) - return dn + # 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.iteritems(): + if key not in options and key != 'cn': + entry.setdefault(key, value) - def exc_callback(self, keys, options, exc, call_func, *call_args, **call_kwargs): - if call_func.func_name == 'update_entry': - if isinstance(exc, errors.EmptyModlist): - aciupdate = getattr(context, 'aciupdate') - if aciupdate: - return - raise exc + if not entry.get('ipapermlocation'): + entry['ipapermlocation'] = [self.api.env.basedn] - def post_callback(self, ldap, dn, entry_attrs, *keys, **options): - assert isinstance(dn, DN) - # rename the underlying ACI after the change to permission - cn = keys[-1] + self.obj.validate_permission(entry) - if 'rename' in options: - self.api.Command.aci_mod(cn,aciprefix=ACI_PREFIX, - permission=options['rename']) + 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: + self.obj.remove_aci(old_entry) + except errors.NotFound, e: + self.log.error('permission ACI not found: %s' % e) - self.api.Command.aci_rename(cn, aciprefix=ACI_PREFIX, - newname=options['rename']) + # To pass data to postcallback, we currently need to use the context + context.old_entry = old_entry - cn = options['rename'] # rename finished + return dn - # all common options to permission-mod and show need to be listed here - common_options = filter_options(options, ['all', 'raw', 'rights']) - result = self.api.Command.permission_show(cn, **common_options)['result'] + def post_callback(self, ldap, dn, entry, *keys, **options): + old_entry = context.old_entry - for r in result: - if not r.startswith('member_'): - entry_attrs[r] = result[r] + if context.permision_moving_aci: + self.obj.add_aci(entry) + else: + self.obj.update_aci(entry, old_entry.single_value['cn']) + self.obj.postprocess_result(entry, options) + entry['dn'] = entry.dn return dn @register() -class permission_find(LDAPSearch): +class permission_find(baseldap.LDAPSearch): __doc__ = _('Search for permissions.') msg_summary = ngettext( - '%(count)d permission matched', '%(count)d permissions matched', 0 - ) - has_output_params = LDAPSearch.has_output_params + output_params + '%(count)d permission matched', '%(count)d permissions matched', 0) + has_output_params = baseldap.LDAPSearch.has_output_params + output_params - def post_callback(self, ldap, entries, truncated, *args, **options): + def args_options_2_params(self, *args, **options): + if self.env.in_server: + self.obj.preprocess_options(options) - # There is an option/param overlap: "cn" must be passed as "aciname" - # to aci-find. Besides that we don't need cn anymore so pop it - aciname = options.pop('cn', None) + return super(permission_find, self).args_options_2_params( + *args, **options) + + def post_callback(self, ldap, entries, truncated, *args, **options): + attribute_options = [o for o in options + if (o in self.options and + self.options[o].attribute)] - pkey_only = options.pop('pkey_only', False) - if not pkey_only: + if not options.get('pkey_only'): for entry in entries: - (dn, attrs) = entry - try: - common_options = filter_options(options, ['all', 'raw']) - aci = self.api.Command.aci_show(attrs['cn'][0], - aciprefix=ACI_PREFIX, **common_options)['result'] - - # copy information from respective ACI to permission entry - for attr in self.obj.aci_attributes: - if attr in aci: - attrs[attr] = aci[attr] - except errors.NotFound: - self.debug('ACI not found for %s' % attrs['cn'][0]) - if truncated: - # size/time limit met, no need to search acis - return truncated + # Old-style permissions might have matched (e.g. by name) + self.obj.upgrade_permission(entry, output_only=True) - if 'sizelimit' in options: - max_entries = options['sizelimit'] - else: - config = ldap.get_ipa_config()[1] - max_entries = config['ipasearchrecordslimit'] - - # Now find all the ACIs that match. Once we find them, add any that - # aren't already in the list along with their permission info. - - opts = self.obj.filter_aci_attributes(options) - if aciname: - opts['aciname'] = aciname - opts['aciprefix'] = ACI_PREFIX - # permission ACI attribute is needed - aciresults = self.api.Command.aci_find(*args, **opts) - truncated = truncated or aciresults['truncated'] - results = aciresults['result'] - - for aci in results: - found = False - if 'permission' in aci: - for entry in entries: - (dn, attrs) = entry - if aci['permission'] == attrs['cn'][0]: - found = True - break - if not found: - common_options = filter_options(options, ['all', 'raw']) - permission = self.api.Command.permission_show( - aci['permission'], **common_options)['result'] - dn = permission['dn'] - del permission['dn'] - if pkey_only: - pk = self.obj.primary_key.name - new_entry = ldap.make_entry(dn, {pk: permission[pk]}) + if not truncated: + if 'sizelimit' in options: + max_entries = options['sizelimit'] + else: + config = ldap.get_ipa_config()[1] + max_entries = int(config.single_value['ipasearchrecordslimit']) + + filters = ['(objectclass=ipaPermission)', + '(!(ipaPermissionType=V2))'] + if args: + filters.append(ldap.make_filter_from_attr('cn', args[0], + 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) + except errors.NotFound: + legacy_entries = () + 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 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) + entries.pop() + 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) + 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 else: - new_entry = ldap.make_entry(dn, permission) - - if (dn, permission) not in entries: - if len(entries) < max_entries: - entries.append(new_entry) - else: - truncated = True - break + entries.append(entry) + + for entry in entries: + if options.get('pkey_only'): + for opt_name in entry.keys(): + 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(LDAPRetrieve): +class permission_show(baseldap.LDAPRetrieve): __doc__ = _('Display information about a permission.') + has_output_params = baseldap.LDAPRetrieve.has_output_params + output_params - has_output_params = LDAPRetrieve.has_output_params + output_params - def post_callback(self, ldap, dn, entry_attrs, *keys, **options): - assert isinstance(dn, DN) - try: - common_options = filter_options(options, ['all', 'raw']) - aci = self.api.Command.aci_show(keys[-1], aciprefix=ACI_PREFIX, - **common_options)['result'] - for attr in self.obj.aci_attributes: - if attr in aci: - entry_attrs[attr] = aci[attr] - except errors.NotFound: - self.debug('ACI not found for %s' % entry_attrs['cn'][0]) - if options.get('rights', False) and options.get('all', False): - # The ACI attributes are just broken-out components of aci so - # the rights should all match it. - for attr in self.obj.aci_attributes: - entry_attrs['attributelevelrights'][attr] = entry_attrs['attributelevelrights']['aci'] + 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(LDAPAddMember): - """ - Add members to a permission. - """ +class permission_add_member(baseldap.LDAPAddMember): + """Add members to a permission.""" NO_CLI = True @register() -class permission_remove_member(LDAPRemoveMember): - """ - Remove members from a permission. - """ +class permission_remove_member(baseldap.LDAPRemoveMember): + """Remove members from a permission.""" NO_CLI = True -- cgit