# Authors: # Petr Viktorin # # 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 . import re import traceback from ipalib.plugins import baseldap 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 __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': 'ipapermtargetfilter', '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, basestring) and value.startswith('ldap:///'): value = strip_ldap_prefix(value) return super(DNOrURL, self)._convert_scalar(value, index=index) 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 @register() class permission(baseldap.LDAPObject): """ Permission object. """ container_dn = api.env.container_permission object_name = _('permission') object_name_plural = _('permissions') object_class = ['groupofnames', 'ipapermission', 'ipapermissionv2'] 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 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='permissions', label=_('Permissions'), 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*', 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( 'ipapermtargetfilter*', prevalidate_filter, cli_name='filter', label=_('Target filter'), doc=_('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 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 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.get('ipapermtargetfilter', []) ipapermtarget = entry.single_value.get('ipapermtarget') ipapermlocation = entry.single_value.get('ipapermlocation') # memberof memberof = [] for targetfilter in ipapermtargetfilter: match = re.match('^\(memberof=(.*)\)$', targetfilter, re.I) if match: dn = DN(match.group(1)) 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) if memberof: entry['memberof'] = memberof # 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 ipapermtargetfilter and ipapermlocation: for obj in self.api.Object(): filter_objectclasses = getattr( obj, 'permission_filter_objectclasses', None) if not filter_objectclasses: continue wantdn = DN(obj.container_dn, self.api.env.basedn) if DN(ipapermlocation) != wantdn: continue for objclass in filter_objectclasses: filter_re = '\(objectclass=%s\)' % re.escape(objclass) if not any(re.match(filter_re, tf, re.I) for tf in ipapermtargetfilter): break else: entry.single_value['type'] = unicode(obj.name) 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['ipapermtargetfilter']) type_rights.intersection_update(rights['ipapermlocation']) 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 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 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 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 get_effective_attrs(self, entry): attrs = set(entry.get('ipapermdefaultattr', ())) attrs.update(entry.get('ipapermincludedattr', ())) 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) # 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(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.info('No changes to ACI') return acientry, acistring 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: acientry = ldap.make_entry(location) 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 '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 preprocess_options(self, options, return_filter_ops=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 retreived. The dict has the following keys: - remove: list of regular expression objects; values that match any of them sould be removed - add: list of values to be added, after any removals """ 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: memberof = options.pop('memberof') filter_ops['remove'].append(re.compile(r'\(memberOf=.*\)', re.I)) if memberof: try: groupdn = self.api.Object.group.get_dn_if_exists(memberof) except errors.NotFound: raise errors.NotFound( reason=_('%s: group not found') % memberof) 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)) if objtype: if 'ipapermlocation' in options: raise errors.ValidationError( name='ipapermlocation', error=_('subtree and type are mutually exclusive')) obj = self.api.Object[objtype.lower()] new_values = [u'(objectclass=%s)' % o for o in obj.permission_filter_objectclasses] filter_ops['add'].extend(new_values) 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', [])) + 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) return super(permission_add, self).execute(*keys, **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]) 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): 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 = 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 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: 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.iteritems(): if (key not in options and key != 'cn' and key not in self.obj.attribute_members): entry.setdefault(key, value) filter_ops = context.filter_ops removes = filter_ops.get('remove', []) new_filters = set( filt for filt in (entry.get('ipapermtargetfilter') or []) if 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, 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.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.warn('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: self.log.error('Error updating ACI: %s' % traceback.format_exc()) self.log.warn('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) 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: config = ldap.get_ipa_config() 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 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) 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: 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(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 @register() class permission_remove_member(baseldap.LDAPRemoveMember): """Remove members from a permission.""" NO_CLI = True