summaryrefslogtreecommitdiffstats
path: root/ipaserver/plugins/baseldap.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/baseldap.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/baseldap.py')
-rw-r--r--ipaserver/plugins/baseldap.py2397
1 files changed, 2397 insertions, 0 deletions
diff --git a/ipaserver/plugins/baseldap.py b/ipaserver/plugins/baseldap.py
new file mode 100644
index 000000000..bbd8ba146
--- /dev/null
+++ b/ipaserver/plugins/baseldap.py
@@ -0,0 +1,2397 @@
+# Authors:
+# Pavel Zuna <pzuna@redhat.com>
+#
+# Copyright (C) 2009 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/>.
+"""
+Base classes for LDAP plugins.
+"""
+
+import re
+import time
+from copy import deepcopy
+import base64
+
+import six
+
+from ipalib import api, crud, errors
+from ipalib import Method, Object
+from ipalib import Flag, Int, Str
+from ipalib.cli import to_cli
+from ipalib import output
+from ipalib.text import _
+from ipalib.util import json_serialize, validate_hostname
+from ipalib.capabilities import client_has_capability
+from ipalib.messages import add_message, SearchResultTruncated
+from ipapython.dn import DN
+from ipapython.version import API_VERSION
+
+if six.PY3:
+ unicode = str
+
+DNA_MAGIC = -1
+
+global_output_params = (
+ Flag('has_password',
+ label=_('Password'),
+ ),
+ Str('member',
+ label=_('Failed members'),
+ ),
+ Str('member_user?',
+ label=_('Member users'),
+ ),
+ Str('member_group?',
+ label=_('Member groups'),
+ ),
+ Str('memberof_group?',
+ label=_('Member of groups'),
+ ),
+ Str('member_host?',
+ label=_('Member hosts'),
+ ),
+ Str('member_hostgroup?',
+ label=_('Member host-groups'),
+ ),
+ Str('memberof_hostgroup?',
+ label=_('Member of host-groups'),
+ ),
+ Str('memberof_permission?',
+ label=_('Permissions'),
+ ),
+ Str('memberof_privilege?',
+ label='Privileges',
+ ),
+ Str('memberof_role?',
+ label=_('Roles'),
+ ),
+ Str('memberof_sudocmdgroup?',
+ label=_('Sudo Command Groups'),
+ ),
+ Str('member_privilege?',
+ label='Granted to Privilege',
+ ),
+ Str('member_role?',
+ label=_('Granting privilege to roles'),
+ ),
+ Str('member_netgroup?',
+ label=_('Member netgroups'),
+ ),
+ Str('memberof_netgroup?',
+ label=_('Member of netgroups'),
+ ),
+ Str('member_service?',
+ label=_('Member services'),
+ ),
+ Str('member_servicegroup?',
+ label=_('Member service groups'),
+ ),
+ Str('memberof_servicegroup?',
+ label='Member of service groups',
+ ),
+ Str('member_hbacsvc?',
+ label=_('Member HBAC service'),
+ ),
+ Str('member_hbacsvcgroup?',
+ label=_('Member HBAC service groups'),
+ ),
+ Str('memberof_hbacsvcgroup?',
+ label='Member of HBAC service groups',
+ ),
+ Str('member_sudocmd?',
+ label='Member Sudo commands',
+ ),
+ Str('memberof_sudorule?',
+ label='Member of Sudo rule',
+ ),
+ Str('memberof_hbacrule?',
+ label='Member of HBAC rule',
+ ),
+ Str('memberindirect_user?',
+ label=_('Indirect Member users'),
+ ),
+ Str('memberindirect_group?',
+ label=_('Indirect Member groups'),
+ ),
+ Str('memberindirect_host?',
+ label=_('Indirect Member hosts'),
+ ),
+ Str('memberindirect_hostgroup?',
+ label=_('Indirect Member host-groups'),
+ ),
+ Str('memberindirect_role?',
+ label=_('Indirect Member of roles'),
+ ),
+ Str('memberindirect_permission?',
+ label=_('Indirect Member permissions'),
+ ),
+ Str('memberindirect_hbacsvc?',
+ label=_('Indirect Member HBAC service'),
+ ),
+ Str('memberindirect_hbacsvcgrp?',
+ label=_('Indirect Member HBAC service group'),
+ ),
+ Str('memberindirect_netgroup?',
+ label=_('Indirect Member netgroups'),
+ ),
+ Str('memberofindirect_group?',
+ label='Indirect Member of group',
+ ),
+ Str('memberofindirect_netgroup?',
+ label='Indirect Member of netgroup',
+ ),
+ Str('memberofindirect_hostgroup?',
+ label='Indirect Member of host-group',
+ ),
+ Str('memberofindirect_role?',
+ label='Indirect Member of role',
+ ),
+ Str('memberofindirect_sudorule?',
+ label='Indirect Member of Sudo rule',
+ ),
+ Str('memberofindirect_hbacrule?',
+ label='Indirect Member of HBAC rule',
+ ),
+ Str('sourcehost',
+ label=_('Failed source hosts/hostgroups'),
+ ),
+ Str('memberhost',
+ label=_('Failed hosts/hostgroups'),
+ ),
+ Str('memberuser',
+ label=_('Failed users/groups'),
+ ),
+ Str('memberservice',
+ label=_('Failed service/service groups'),
+ ),
+ Str('failed',
+ label=_('Failed to remove'),
+ flags=['suppress_empty'],
+ ),
+ Str('ipasudorunas',
+ label=_('Failed RunAs'),
+ ),
+ Str('ipasudorunasgroup',
+ label=_('Failed RunAsGroup'),
+ ),
+)
+
+
+def validate_add_attribute(ugettext, attr):
+ validate_attribute(ugettext, 'addattr', attr)
+
+def validate_set_attribute(ugettext, attr):
+ validate_attribute(ugettext, 'setattr', attr)
+
+def validate_del_attribute(ugettext, attr):
+ validate_attribute(ugettext, 'delattr', attr)
+
+def validate_attribute(ugettext, name, attr):
+ m = re.match("\s*(.*?)\s*=\s*(.*?)\s*$", attr)
+ if not m or len(m.groups()) != 2:
+ raise errors.ValidationError(
+ name=name, error=_('Invalid format. Should be name=value'))
+
+def get_effective_rights(ldap, dn, attrs=None):
+ assert isinstance(dn, DN)
+ if attrs is None:
+ attrs = ['*', 'nsaccountlock', 'cospriority']
+ rights = ldap.get_effective_rights(dn, attrs)
+ rdict = {}
+ if 'attributelevelrights' in rights:
+ rights = rights['attributelevelrights']
+ rights = rights[0].split(', ')
+ for r in rights:
+ (k,v) = r.split(':')
+ if v == 'none':
+ # the string "none" means "no rights found"
+ # see https://fedorahosted.org/freeipa/ticket/4359
+ v = u''
+ rdict[k.strip().lower()] = v
+
+ return rdict
+
+def entry_from_entry(entry, newentry):
+ """
+ Python is more or less pass-by-value except for immutable objects. So if
+ you pass in a dict to a function you are free to change members of that
+ dict but you can't create a new dict in the function and expect to replace
+ what was passed in.
+
+ In some post-op plugins that is exactly what we want to do, so here is a
+ clumsy way around the problem.
+ """
+
+ # Wipe out the current data
+ for e in list(entry):
+ del entry[e]
+
+ # Re-populate it with new wentry
+ for e in newentry.keys():
+ entry[e] = newentry[e]
+
+def entry_to_dict(entry, **options):
+ if options.get('raw', False):
+ result = {}
+ for attr in entry:
+ if attr.lower() == 'attributelevelrights':
+ value = entry[attr]
+ elif entry.conn.get_attribute_type(attr) is bytes:
+ value = entry.raw[attr]
+ else:
+ value = list(entry.raw[attr])
+ for (i, v) in enumerate(value):
+ try:
+ value[i] = v.decode('utf-8')
+ except UnicodeDecodeError:
+ pass
+ result[attr] = value
+ else:
+ result = dict((k.lower(), v) for (k, v) in entry.items())
+ if options.get('all', False):
+ result['dn'] = entry.dn
+ return result
+
+def pkey_to_unicode(key):
+ if key is None:
+ key = []
+ elif not isinstance(key, (tuple, list)):
+ key = [key]
+ key = u','.join(unicode(k) for k in key)
+ return key
+
+def pkey_to_value(key, options):
+ version = options.get('version', API_VERSION)
+ if client_has_capability(version, 'primary_key_types'):
+ return key
+ return pkey_to_unicode(key)
+
+def wait_for_value(ldap, dn, attr, value):
+ """
+ 389-ds postoperation plugins are executed after the data has been
+ returned to a client. This means that plugins that add data in a
+ postop are not included in data returned to the user.
+
+ The downside of waiting is that this increases the time of the
+ command.
+
+ The updated entry is returned.
+ """
+ # Loop a few times to give the postop-plugin a chance to complete
+ # Don't sleep for more than 6 seconds.
+ x = 0
+ while x < 20:
+ # sleep first because the first search, even on a quiet system,
+ # almost always fails.
+ time.sleep(.3)
+ x = x + 1
+
+ # FIXME: put a try/except around here? I think it is probably better
+ # to just let the exception filter up to the caller.
+ entry_attrs = ldap.get_entry(dn, ['*'])
+ if attr in entry_attrs:
+ if isinstance(entry_attrs[attr], (list, tuple)):
+ values = [y.lower() for y in entry_attrs[attr]]
+ if value.lower() in values:
+ break
+ else:
+ if value.lower() == entry_attrs[attr].lower():
+ break
+
+ return entry_attrs
+
+
+def validate_externalhost(ugettext, hostname):
+ try:
+ validate_hostname(hostname, check_fqdn=False, allow_underscore=True)
+ except ValueError as e:
+ return unicode(e)
+
+
+external_host_param = Str('externalhost*', validate_externalhost,
+ label=_('External host'),
+ flags=['no_option'],
+)
+
+
+def add_external_pre_callback(membertype, ldap, dn, keys, options):
+ """
+ Pre callback to validate external members.
+
+ This should be called by a command pre callback directly.
+
+ membertype is the type of member
+ """
+ assert isinstance(dn, DN)
+
+ # validate hostname with allowed underscore characters, non-fqdn
+ # hostnames are allowed
+ def validate_host(hostname):
+ validate_hostname(hostname, check_fqdn=False, allow_underscore=True)
+
+ if options.get(membertype):
+ if membertype == 'host':
+ validator = validate_host
+ else:
+ param = api.Object[membertype].primary_key
+
+ def validator(value):
+ value = param(value)
+ param.validate(value)
+
+ for value in options[membertype]:
+ try:
+ validator(value)
+ except errors.ValidationError as e:
+ raise errors.ValidationError(name=membertype, error=e.error)
+ except ValueError as e:
+ raise errors.ValidationError(name=membertype, error=e)
+ return dn
+
+
+def add_external_post_callback(ldap, dn, entry_attrs, failed, completed,
+ memberattr, membertype, externalattr,
+ normalize=True):
+ """
+ Takes the following arguments:
+ failed - the list of failed entries, these are candidates for possible
+ external entries to add
+ completed - the number of successfully added entries so far
+ memberattr - the attribute name that IPA uses for membership natively
+ (e.g. memberhost)
+ membertype - the object type of the member (e.g. host)
+ externalattr - the attribute name that IPA uses to store the membership
+ of the entries that are not managed by IPA
+ (e.g externalhost)
+
+ Returns the number of completed entries so far (the number of entries
+ handled by IPA incremented by the number of handled external entries) and
+ dn.
+ """
+ assert isinstance(dn, DN)
+
+ completed_external = 0
+
+ # Sift through the failures. We assume that these are all
+ # entries that aren't stored in IPA, aka external entries.
+ if memberattr in failed and membertype in failed[memberattr]:
+ entry_attrs_ = ldap.get_entry(dn, [externalattr])
+ dn = entry_attrs_.dn
+ members = entry_attrs.get(memberattr, [])
+ external_entries = entry_attrs_.get(externalattr, [])
+ lc_external_entries = set(e.lower() for e in external_entries)
+
+ failed_entries = []
+ for entry in failed[memberattr][membertype]:
+ membername = entry[0].lower()
+ member_dn = api.Object[membertype].get_dn(membername)
+ assert isinstance(member_dn, DN)
+
+ if (membername not in lc_external_entries and
+ member_dn not in members):
+ # Not an IPA entry, assume external
+ if normalize:
+ external_entries.append(membername)
+ else:
+ external_entries.append(entry[0])
+ lc_external_entries.add(membername)
+ completed_external += 1
+ elif (membername in lc_external_entries and
+ member_dn not in members):
+ # Already an external member, reset the error message
+ msg = unicode(errors.AlreadyGroupMember().message)
+ newerror = (entry[0], msg)
+ ind = failed[memberattr][membertype].index(entry)
+ failed[memberattr][membertype][ind] = newerror
+ failed_entries.append(membername)
+ else:
+ # Really a failure
+ failed_entries.append(membername)
+
+ if completed_external:
+ entry_attrs_[externalattr] = external_entries
+ try:
+ ldap.update_entry(entry_attrs_)
+ except errors.EmptyModlist:
+ pass
+ failed[memberattr][membertype] = failed_entries
+ entry_attrs[externalattr] = external_entries
+
+ return (completed + completed_external, dn)
+
+
+def remove_external_post_callback(ldap, dn, entry_attrs, failed, completed,
+ memberattr, membertype, externalattr):
+ """
+ Takes the following arguments:
+ failed - the list of failed entries, these are candidates for possible
+ external entries to remove
+ completed - the number of successfully removed entries so far
+ memberattr - the attribute name that IPA uses for membership natively
+ (e.g. memberhost)
+ membertype - the object type of the member (e.g. host)
+ externalattr - the attribute name that IPA uses to store the membership
+ of the entries that are not managed by IPA
+ (e.g externalhost)
+
+ Returns the number of completed entries so far (the number of entries
+ handled by IPA incremented by the number of handled external entries) and
+ dn.
+ """
+
+ assert isinstance(dn, DN)
+
+ # Run through the failures and gracefully remove any member defined
+ # as an external member.
+ completed_external = 0
+ if memberattr in failed and membertype in failed[memberattr]:
+ entry_attrs_ = ldap.get_entry(dn, [externalattr])
+ dn = entry_attrs_.dn
+ external_entries = entry_attrs_.get(externalattr, [])
+ failed_entries = []
+
+ for entry in failed[memberattr][membertype]:
+ membername = entry[0].lower()
+ if membername in external_entries or entry[0] in external_entries:
+ try:
+ external_entries.remove(membername)
+ except ValueError:
+ external_entries.remove(entry[0])
+ completed_external += 1
+ else:
+ msg = unicode(errors.NotGroupMember().message)
+ newerror = (entry[0], msg)
+ ind = failed[memberattr][membertype].index(entry)
+ failed[memberattr][membertype][ind] = newerror
+ failed_entries.append(membername)
+
+ if completed_external:
+ entry_attrs_[externalattr] = external_entries
+ try:
+ ldap.update_entry(entry_attrs_)
+ except errors.EmptyModlist:
+ pass
+ failed[memberattr][membertype] = failed_entries
+ entry_attrs[externalattr] = external_entries
+
+ return (completed + completed_external, dn)
+
+
+def host_is_master(ldap, fqdn):
+ """
+ Check to see if this host is a master.
+
+ Raises an exception if a master, otherwise returns nothing.
+ """
+ master_dn = DN(('cn', fqdn), ('cn', 'masters'), ('cn', 'ipa'), ('cn', 'etc'), api.env.basedn)
+ try:
+ ldap.get_entry(master_dn, ['objectclass'])
+ raise errors.ValidationError(name='hostname', error=_('An IPA master host cannot be deleted or disabled'))
+ except errors.NotFound:
+ # Good, not a master
+ return
+
+
+def add_missing_object_class(ldap, objectclass, dn, entry_attrs=None, update=True):
+ """
+ Add object class if missing into entry. Fetches entry if not passed. Updates
+ the entry by default.
+
+ Returns the entry
+ """
+
+ if not entry_attrs:
+ entry_attrs = ldap.get_entry(dn, ['objectclass'])
+ if (objectclass.lower() not in (o.lower() for o in entry_attrs['objectclass'])):
+ entry_attrs['objectclass'].append(objectclass)
+ if update:
+ ldap.update_entry(entry_attrs)
+ return entry_attrs
+
+
+class LDAPObject(Object):
+ """
+ Object representing a LDAP entry.
+ """
+ backend_name = 'ldap2'
+
+ parent_object = ''
+ container_dn = ''
+ object_name = _('entry')
+ object_name_plural = _('entries')
+ object_class = []
+ object_class_config = None
+ # If an objectclass is possible but not default in an entry. Needed for
+ # collecting attributes for ACI UI.
+ possible_objectclasses = []
+ limit_object_classes = [] # Only attributes in these are allowed
+ disallow_object_classes = [] # Disallow attributes in these
+ permission_filter_objectclasses = None
+ search_attributes = []
+ search_attributes_config = None
+ default_attributes = []
+ search_display_attributes = [] # attributes displayed in LDAPSearch
+ hidden_attributes = ['objectclass', 'aci']
+ # set rdn_attribute only if RDN attribute differs from primary key!
+ rdn_attribute = ''
+ uuid_attribute = ''
+ attribute_members = {}
+ rdn_is_primary_key = False # Do we need RDN change to do a rename?
+ password_attributes = []
+ # Can bind as this entry (has userPassword or krbPrincipalKey)
+ bindable = False
+ relationships = {
+ # attribute: (label, inclusive param prefix, exclusive param prefix)
+ 'member': ('Member', '', 'no_'),
+ 'memberof': ('Member Of', 'in_', 'not_in_'),
+ 'memberindirect': (
+ 'Indirect Member', None, 'no_indirect_'
+ ),
+ 'memberofindirect': (
+ 'Indirect Member Of', None, 'not_in_indirect_'
+ ),
+ }
+ label = _('Entry')
+ label_singular = _('Entry')
+ managed_permissions = {}
+
+ container_not_found_msg = _('container entry (%(container)s) not found')
+ parent_not_found_msg = _('%(parent)s: %(oname)s not found')
+ object_not_found_msg = _('%(pkey)s: %(oname)s not found')
+ already_exists_msg = _('%(oname)s with name "%(pkey)s" already exists')
+
+ def get_dn(self, *keys, **kwargs):
+ if self.parent_object:
+ parent_dn = self.api.Object[self.parent_object].get_dn(*keys[:-1])
+ else:
+ parent_dn = DN(self.container_dn, api.env.basedn)
+ if self.rdn_attribute:
+ try:
+ entry_attrs = self.backend.find_entry_by_attr(
+ self.primary_key.name, keys[-1], self.object_class, [''],
+ DN(self.container_dn, api.env.basedn)
+ )
+ except errors.NotFound:
+ pass
+ else:
+ return entry_attrs.dn
+ if self.primary_key and keys[-1] is not None:
+ return self.backend.make_dn_from_attr(
+ self.primary_key.name, keys[-1], parent_dn
+ )
+ assert isinstance(parent_dn, DN)
+ return parent_dn
+
+ def get_dn_if_exists(self, *keys, **kwargs):
+ dn = self.get_dn(*keys, **kwargs)
+ entry = self.backend.get_entry(dn, [''])
+ return entry.dn
+
+ def get_primary_key_from_dn(self, dn):
+ assert isinstance(dn, DN)
+ try:
+ if self.rdn_attribute:
+ entry_attrs = self.backend.get_entry(
+ dn, [self.primary_key.name]
+ )
+ try:
+ return entry_attrs[self.primary_key.name][0]
+ except (KeyError, IndexError):
+ return ''
+ except errors.NotFound:
+ pass
+ try:
+ return dn[self.primary_key.name]
+ except KeyError:
+ # The primary key is not in the DN.
+ # This shouldn't happen, but we don't want a "show" command to
+ # crash.
+ # Just return the entire DN, it's all we have if the entry
+ # doesn't exist
+ return unicode(dn)
+
+ def get_ancestor_primary_keys(self):
+ if self.parent_object:
+ parent_obj = self.api.Object[self.parent_object]
+ for key in parent_obj.get_ancestor_primary_keys():
+ yield key
+ if parent_obj.primary_key:
+ pkey = parent_obj.primary_key
+ yield pkey.clone_rename(
+ parent_obj.name + pkey.name, required=True, query=True,
+ cli_name=parent_obj.name, label=pkey.label
+ )
+
+ def has_objectclass(self, classes, objectclass):
+ oc = [x.lower() for x in classes]
+ return objectclass.lower() in oc
+
+ def convert_attribute_members(self, entry_attrs, *keys, **options):
+ if options.get('raw', False):
+ return
+
+ container_dns = {}
+ new_attrs = {}
+
+ for attr in self.attribute_members:
+ try:
+ value = entry_attrs.raw[attr]
+ except KeyError:
+ continue
+ del entry_attrs[attr]
+
+ for member in value:
+ memberdn = DN(member)
+ for ldap_obj_name in self.attribute_members[attr]:
+ ldap_obj = self.api.Object[ldap_obj_name]
+ try:
+ container_dn = container_dns[ldap_obj_name]
+ except KeyError:
+ container_dn = DN(ldap_obj.container_dn, api.env.basedn)
+ container_dns[ldap_obj_name] = container_dn
+
+ if memberdn.endswith(container_dn):
+ new_value = ldap_obj.get_primary_key_from_dn(memberdn)
+ new_attr_name = '%s_%s' % (attr, ldap_obj.name)
+ try:
+ new_attr = new_attrs[new_attr_name]
+ except KeyError:
+ new_attr = entry_attrs.setdefault(new_attr_name, [])
+ new_attrs[new_attr_name] = new_attr
+ new_attr.append(new_value)
+ break
+
+ def get_indirect_members(self, entry_attrs, attrs_list):
+ if 'memberindirect' in attrs_list:
+ self.get_memberindirect(entry_attrs)
+ if 'memberofindirect' in attrs_list:
+ self.get_memberofindirect(entry_attrs)
+
+ def get_memberindirect(self, group_entry):
+ """
+ Get indirect members
+ """
+
+ mo_filter = self.backend.make_filter({'memberof': group_entry.dn})
+ filter = self.backend.combine_filters(
+ ('(member=*)', mo_filter), self.backend.MATCH_ALL)
+ try:
+ result = self.backend.get_entries(
+ self.api.env.basedn,
+ filter=filter,
+ attrs_list=['member'],
+ size_limit=-1, # paged search will get everything anyway
+ paged_search=True)
+ except errors.NotFound:
+ result = []
+
+ indirect = set()
+ for entry in result:
+ indirect.update(entry.raw.get('member', []))
+ indirect.difference_update(group_entry.raw.get('member', []))
+
+ if indirect:
+ group_entry.raw['memberindirect'] = list(indirect)
+
+ def get_memberofindirect(self, entry):
+
+ dn = entry.dn
+ filter = self.backend.make_filter(
+ {'member': dn, 'memberuser': dn, 'memberhost': dn})
+ try:
+ result = self.backend.get_entries(
+ self.api.env.basedn,
+ filter=filter,
+ attrs_list=[''])
+ except errors.NotFound:
+ result = []
+
+ direct = set()
+ indirect = set(entry.raw.get('memberof', []))
+ for group_entry in result:
+ dn = str(group_entry.dn)
+ if dn in indirect:
+ indirect.remove(dn)
+ direct.add(dn)
+
+ entry.raw['memberof'] = list(direct)
+ if indirect:
+ entry.raw['memberofindirect'] = list(indirect)
+
+ def get_password_attributes(self, ldap, dn, entry_attrs):
+ """
+ Search on the entry to determine if it has a password or
+ keytab set.
+
+ A tuple is used to determine which attribute is set
+ in entry_attrs. The value is set to True/False whether a
+ given password type is set.
+ """
+ for (pwattr, attr) in self.password_attributes:
+ search_filter = '(%s=*)' % pwattr
+ try:
+ (entries, truncated) = ldap.find_entries(
+ search_filter, [pwattr], dn, ldap.SCOPE_BASE
+ )
+ entry_attrs[attr] = True
+ except errors.NotFound:
+ entry_attrs[attr] = False
+
+ def handle_not_found(self, *keys):
+ pkey = ''
+ if self.primary_key:
+ pkey = keys[-1]
+ raise errors.NotFound(
+ reason=self.object_not_found_msg % {
+ 'pkey': pkey, 'oname': self.object_name,
+ }
+ )
+
+ def handle_duplicate_entry(self, *keys):
+ try:
+ pkey = keys[-1]
+ except IndexError:
+ pkey = ''
+ raise errors.DuplicateEntry(
+ message=self.already_exists_msg % {
+ 'pkey': pkey, 'oname': self.object_name,
+ }
+ )
+
+ # list of attributes we want exported to JSON
+ json_friendly_attributes = (
+ 'parent_object', 'container_dn', 'object_name', 'object_name_plural',
+ 'object_class', 'object_class_config', 'default_attributes', 'label', 'label_singular',
+ 'hidden_attributes', 'uuid_attribute', 'attribute_members', 'name',
+ 'takes_params', 'rdn_attribute', 'bindable', 'relationships',
+ )
+
+ def __json__(self):
+ ldap = self.backend
+ json_dict = dict(
+ (a, json_serialize(getattr(self, a))) for a in self.json_friendly_attributes
+ )
+ if self.primary_key:
+ json_dict['primary_key'] = self.primary_key.name
+ objectclasses = self.object_class
+ if self.object_class_config:
+ config = ldap.get_ipa_config()
+ objectclasses = config.get(
+ self.object_class_config, objectclasses
+ )
+ objectclasses = objectclasses + self.possible_objectclasses
+ # Get list of available attributes for this object for use
+ # in the ACI UI.
+ attrs = self.api.Backend.ldap2.schema.attribute_types(objectclasses)
+ attrlist = []
+ # Go through the MUST first
+ for (oid, attr) in attrs[0].items():
+ attrlist.append(attr.names[0].lower())
+ # And now the MAY
+ for (oid, attr) in attrs[1].items():
+ attrlist.append(attr.names[0].lower())
+ json_dict['aciattrs'] = attrlist
+ attrlist.sort()
+ json_dict['methods'] = [m for m in self.methods]
+ json_dict['can_have_permissions'] = bool(
+ self.permission_filter_objectclasses)
+ return json_dict
+
+
+# addattr can cause parameters to have more than one value even if not defined
+# as multivalue, make sure this isn't the case
+def _check_single_value_attrs(params, entry_attrs):
+ for (a, v) in entry_attrs.items():
+ if isinstance(v, (list, tuple)) and len(v) > 1:
+ if a in params and not params[a].multivalue:
+ raise errors.OnlyOneValueAllowed(attr=a)
+
+# setattr or --option='' can cause parameters to be empty that are otherwise
+# required, make sure we enforce that.
+def _check_empty_attrs(params, entry_attrs):
+ for (a, v) in entry_attrs.items():
+ if v is None or (isinstance(v, six.string_types) and len(v) == 0):
+ if a in params and params[a].required:
+ raise errors.RequirementError(name=a)
+
+
+def _check_limit_object_class(attributes, attrs, allow_only):
+ """
+ If the set of objectclasses is limited enforce that only those
+ are updated in entry_attrs (plus dn)
+
+ allow_only tells us what mode to check in:
+
+ If True then we enforce that the attributes must be in the list of
+ allowed.
+
+ If False then those attributes are not allowed.
+ """
+ if len(attributes[0]) == 0 and len(attributes[1]) == 0:
+ return
+ limitattrs = deepcopy(attrs)
+ # Go through the MUST first
+ for (oid, attr) in attributes[0].items():
+ if attr.names[0].lower() in limitattrs:
+ if not allow_only:
+ raise errors.ObjectclassViolation(
+ info=_('attribute "%(attribute)s" not allowed') % dict(
+ attribute=attr.names[0].lower()))
+ limitattrs.remove(attr.names[0].lower())
+ # And now the MAY
+ for (oid, attr) in attributes[1].items():
+ if attr.names[0].lower() in limitattrs:
+ if not allow_only:
+ raise errors.ObjectclassViolation(
+ info=_('attribute "%(attribute)s" not allowed') % dict(
+ attribute=attr.names[0].lower()))
+ limitattrs.remove(attr.names[0].lower())
+ if len(limitattrs) > 0 and allow_only:
+ raise errors.ObjectclassViolation(
+ info=_('attribute "%(attribute)s" not allowed') % dict(
+ attribute=limitattrs[0]))
+
+
+class BaseLDAPCommand(Method):
+ """
+ Base class for Base LDAP Commands.
+ """
+ setattr_option = Str('setattr*', validate_set_attribute,
+ cli_name='setattr',
+ doc=_("""Set an attribute to a name/value pair. Format is attr=value.
+For multi-valued attributes, the command replaces the values already present."""),
+ exclude='webui',
+ )
+ addattr_option = Str('addattr*', validate_add_attribute,
+ cli_name='addattr',
+ doc=_("""Add an attribute/value pair. Format is attr=value. The attribute
+must be part of the schema."""),
+ exclude='webui',
+ )
+ delattr_option = Str('delattr*', validate_del_attribute,
+ cli_name='delattr',
+ doc=_("""Delete an attribute/value pair. The option will be evaluated
+last, after all sets and adds."""),
+ exclude='webui',
+ )
+
+ callback_types = Method.callback_types + ('pre',
+ 'post',
+ 'exc')
+
+ def get_summary_default(self, output):
+ if 'value' in output:
+ output = dict(output)
+ output['value'] = pkey_to_unicode(output['value'])
+ return super(BaseLDAPCommand, self).get_summary_default(output)
+
+ def _convert_2_dict(self, ldap, attrs):
+ """
+ Convert a string in the form of name/value pairs into a dictionary.
+
+ :param attrs: A list of name/value pair strings, in the "name=value"
+ format. May also be a single string, or None.
+ """
+
+ newdict = {}
+ if attrs is None:
+ attrs = []
+ elif not type(attrs) in (list, tuple):
+ attrs = [attrs]
+ for a in attrs:
+ m = re.match("\s*(.*?)\s*=\s*(.*?)\s*$", a)
+ attr = str(m.group(1)).lower()
+ value = m.group(2)
+ if attr in self.obj.params and attr not in self.params:
+ # The attribute is managed by IPA, but it didn't get cloned
+ # to the command. This happens with no_update/no_create attrs.
+ raise errors.ValidationError(
+ name=attr, error=_('attribute is not configurable'))
+ if len(value) == 0:
+ # None means "delete this attribute"
+ value = None
+
+ if attr in newdict:
+ if type(value) in (tuple,):
+ newdict[attr] += list(value)
+ else:
+ newdict[attr].append(value)
+ else:
+ if type(value) in (tuple,):
+ newdict[attr] = list(value)
+ else:
+ newdict[attr] = [value]
+ return newdict
+
+ def process_attr_options(self, entry_attrs, dn, keys, options):
+ """
+ Process all --setattr, --addattr, and --delattr options and add the
+ resulting value to the list of attributes. --setattr is processed first,
+ then --addattr and finally --delattr.
+
+ When --setattr is not used then the original LDAP object is looked up
+ (of course, not when dn is None) and the changes are applied to old
+ object values.
+
+ Attribute values deleted by --delattr may be deleted from attribute
+ values set or added by --setattr, --addattr. For example, the following
+ attributes will result in a NOOP:
+
+ --addattr=attribute=foo --delattr=attribute=foo
+
+ AttrValueNotFound exception may be raised when an attribute value was
+ not found either by --setattr and --addattr nor in existing LDAP object.
+
+ :param entry_attrs: A list of attributes that will be updated
+ :param dn: dn of updated LDAP object or None if a new object is created
+ :param keys: List of command arguments
+ :param options: List of options
+ """
+
+ if all(k not in options for k in ("setattr", "addattr", "delattr")):
+ return
+
+ ldap = self.obj.backend
+
+ adddict = self._convert_2_dict(ldap, options.get('addattr', []))
+ setdict = self._convert_2_dict(ldap, options.get('setattr', []))
+ deldict = self._convert_2_dict(ldap, options.get('delattr', []))
+
+ setattrs = set(setdict)
+ addattrs = set(adddict)
+ delattrs = set(deldict)
+
+ if dn is None:
+ direct_add = addattrs
+ direct_del = delattrs
+ needldapattrs = []
+ else:
+ assert isinstance(dn, DN)
+ direct_add = setattrs & addattrs
+ direct_del = setattrs & delattrs
+ needldapattrs = list((addattrs | delattrs) - setattrs)
+
+ for attr, val in setdict.items():
+ entry_attrs[attr] = val
+
+ for attr in direct_add:
+ try:
+ val = entry_attrs[attr]
+ except KeyError:
+ val = []
+ else:
+ if not isinstance(val, (list, tuple)):
+ val = [val]
+ elif isinstance(val, tuple):
+ val = list(val)
+ val.extend(adddict[attr])
+ entry_attrs[attr] = val
+
+ for attr in direct_del:
+ for delval in deldict[attr]:
+ try:
+ entry_attrs[attr].remove(delval)
+ except ValueError:
+ raise errors.AttrValueNotFound(attr=attr, value=delval)
+
+ if needldapattrs:
+ try:
+ old_entry = self._exc_wrapper(keys, options, ldap.get_entry)(
+ dn, needldapattrs
+ )
+ except errors.NotFound:
+ self.obj.handle_not_found(*keys)
+
+ # Provide a nice error message when user tries to delete an
+ # attribute that does not exist on the entry (and user is not
+ # adding it)
+ names = set(n.lower() for n in old_entry)
+ del_nonexisting = delattrs - (names | setattrs | addattrs)
+ if del_nonexisting:
+ raise errors.ValidationError(name=del_nonexisting.pop(),
+ error=_('No such attribute on this entry'))
+
+ for attr in needldapattrs:
+ entry_attrs[attr] = old_entry.get(attr, [])
+
+ if attr in addattrs:
+ entry_attrs[attr].extend(adddict.get(attr, []))
+
+ for delval in deldict.get(attr, []):
+ try:
+ entry_attrs[attr].remove(delval)
+ except ValueError:
+ if isinstance(delval, bytes):
+ # This is a Binary value, base64 encode it
+ delval = unicode(base64.b64encode(delval))
+ raise errors.AttrValueNotFound(attr=attr, value=delval)
+
+ # normalize all values
+ changedattrs = setattrs | addattrs | delattrs
+ for attr in changedattrs:
+ if attr in self.params and self.params[attr].attribute:
+ # convert single-value params to scalars
+ param = self.params[attr]
+ value = entry_attrs[attr]
+ if not param.multivalue:
+ if len(value) == 1:
+ value = value[0]
+ elif not value:
+ value = None
+ else:
+ raise errors.OnlyOneValueAllowed(attr=attr)
+ # validate, convert and encode params
+ try:
+ value = param(value)
+ param.validate(value)
+ except errors.ValidationError as err:
+ raise errors.ValidationError(name=attr, error=err.error)
+ except errors.ConversionError as err:
+ raise errors.ConversionError(name=attr, error=err.error)
+ if isinstance(value, tuple):
+ value = list(value)
+ entry_attrs[attr] = value
+ else:
+ # unknown attribute: remove duplicite and invalid values
+ entry_attrs[attr] = list(set([val for val in entry_attrs[attr] if val]))
+ if not entry_attrs[attr]:
+ entry_attrs[attr] = None
+ elif isinstance(entry_attrs[attr], (tuple, list)) and len(entry_attrs[attr]) == 1:
+ entry_attrs[attr] = entry_attrs[attr][0]
+
+ @classmethod
+ def register_pre_callback(cls, callback, first=False):
+ """Shortcut for register_callback('pre', ...)"""
+ cls.register_callback('pre', callback, first)
+
+ @classmethod
+ def register_post_callback(cls, callback, first=False):
+ """Shortcut for register_callback('post', ...)"""
+ cls.register_callback('post', callback, first)
+
+ @classmethod
+ def register_exc_callback(cls, callback, first=False):
+ """Shortcut for register_callback('exc', ...)"""
+ cls.register_callback('exc', callback, first)
+
+ def _exc_wrapper(self, keys, options, call_func):
+ """Function wrapper that automatically calls exception callbacks"""
+ def wrapped(*call_args, **call_kwargs):
+ # call call_func first
+ func = call_func
+ callbacks = list(self.get_callbacks('exc'))
+ while True:
+ try:
+ return func(*call_args, **call_kwargs)
+ except errors.ExecutionError as exc:
+ e = exc
+ if not callbacks:
+ raise
+ # call exc_callback in the next loop
+ callback = callbacks.pop(0)
+ def exc_func(*args, **kwargs):
+ return callback(
+ self, keys, options, e, call_func, *args, **kwargs)
+ func = exc_func
+ return wrapped
+
+ def get_options(self):
+ for param in super(BaseLDAPCommand, self).get_options():
+ yield param
+ if self.obj.attribute_members:
+ for o in self.has_output:
+ if isinstance(o, (output.Entry, output.ListOfEntries)):
+ yield Flag('no_members',
+ doc=_('Suppress processing of membership attributes.'),
+ exclude='webui',
+ flags={'no_output'},
+ )
+ break
+
+class LDAPCreate(BaseLDAPCommand, crud.Create):
+ """
+ Create a new entry in LDAP.
+ """
+ takes_options = (BaseLDAPCommand.setattr_option, BaseLDAPCommand.addattr_option)
+
+ def get_args(self):
+ for key in self.obj.get_ancestor_primary_keys():
+ yield key
+ for arg in super(LDAPCreate, self).get_args():
+ yield arg
+
+ has_output_params = global_output_params
+
+ def execute(self, *keys, **options):
+ ldap = self.obj.backend
+
+ dn = self.obj.get_dn(*keys, **options)
+ entry_attrs = ldap.make_entry(
+ dn, self.args_options_2_entry(*keys, **options))
+
+ self.process_attr_options(entry_attrs, None, keys, options)
+
+ entry_attrs['objectclass'] = deepcopy(self.obj.object_class)
+
+ if self.obj.object_class_config:
+ config = ldap.get_ipa_config()
+ entry_attrs['objectclass'] = config.get(
+ self.obj.object_class_config, entry_attrs['objectclass']
+ )
+
+ if self.obj.uuid_attribute:
+ entry_attrs[self.obj.uuid_attribute] = 'autogenerate'
+
+ if self.obj.rdn_attribute:
+ try:
+ dn_attr = dn[0].attr
+ except (IndexError, KeyError):
+ dn_attr = None
+ if dn_attr != self.obj.primary_key.name:
+ self.obj.handle_duplicate_entry(*keys)
+ entry_attrs.dn = ldap.make_dn(
+ entry_attrs, self.obj.rdn_attribute,
+ DN(self.obj.container_dn, api.env.basedn))
+
+ if options.get('all', False):
+ attrs_list = ['*'] + self.obj.default_attributes
+ else:
+ attrs_list = set(self.obj.default_attributes)
+ attrs_list.update(entry_attrs.keys())
+ if options.get('no_members', False):
+ attrs_list.difference_update(self.obj.attribute_members)
+ attrs_list = list(attrs_list)
+
+ for callback in self.get_callbacks('pre'):
+ entry_attrs.dn = callback(
+ self, ldap, entry_attrs.dn, entry_attrs, attrs_list,
+ *keys, **options)
+
+ _check_single_value_attrs(self.params, entry_attrs)
+ _check_limit_object_class(self.api.Backend.ldap2.schema.attribute_types(self.obj.limit_object_classes), list(entry_attrs), allow_only=True)
+ _check_limit_object_class(self.api.Backend.ldap2.schema.attribute_types(self.obj.disallow_object_classes), list(entry_attrs), allow_only=False)
+
+ try:
+ self._exc_wrapper(keys, options, ldap.add_entry)(entry_attrs)
+ except errors.NotFound:
+ parent = self.obj.parent_object
+ if parent:
+ raise errors.NotFound(
+ reason=self.obj.parent_not_found_msg % {
+ 'parent': keys[-2],
+ 'oname': self.api.Object[parent].object_name,
+ }
+ )
+ raise errors.NotFound(
+ reason=self.obj.container_not_found_msg % {
+ 'container': self.obj.container_dn,
+ }
+ )
+ except errors.DuplicateEntry:
+ self.obj.handle_duplicate_entry(*keys)
+
+ try:
+ if self.obj.rdn_attribute:
+ # make sure objectclass is either set or None
+ if self.obj.object_class:
+ object_class = self.obj.object_class
+ else:
+ object_class = None
+ entry_attrs = self._exc_wrapper(keys, options, ldap.find_entry_by_attr)(
+ self.obj.primary_key.name, keys[-1], object_class, attrs_list,
+ DN(self.obj.container_dn, api.env.basedn)
+ )
+ else:
+ entry_attrs = self._exc_wrapper(keys, options, ldap.get_entry)(
+ entry_attrs.dn, attrs_list)
+ except errors.NotFound:
+ self.obj.handle_not_found(*keys)
+
+ self.obj.get_indirect_members(entry_attrs, attrs_list)
+
+ for callback in self.get_callbacks('post'):
+ entry_attrs.dn = callback(
+ self, ldap, entry_attrs.dn, entry_attrs, *keys, **options)
+
+ self.obj.convert_attribute_members(entry_attrs, *keys, **options)
+
+ dn = entry_attrs.dn
+ entry_attrs = entry_to_dict(entry_attrs, **options)
+ entry_attrs['dn'] = dn
+
+ if self.obj.primary_key:
+ pkey = keys[-1]
+ else:
+ pkey = None
+
+ return dict(result=entry_attrs, value=pkey_to_value(pkey, options))
+
+ def pre_callback(self, ldap, dn, entry_attrs, attrs_list, *keys, **options):
+ assert isinstance(dn, DN)
+ return dn
+
+ def post_callback(self, ldap, dn, entry_attrs, *keys, **options):
+ assert isinstance(dn, DN)
+ return dn
+
+ def exc_callback(self, keys, options, exc, call_func, *call_args, **call_kwargs):
+ raise exc
+
+
+class LDAPQuery(BaseLDAPCommand, crud.PKQuery):
+ """
+ Base class for commands that need to retrieve an existing entry.
+ """
+ def get_args(self):
+ for key in self.obj.get_ancestor_primary_keys():
+ yield key
+ for arg in super(LDAPQuery, self).get_args():
+ yield arg
+
+
+class LDAPMultiQuery(LDAPQuery):
+ """
+ Base class for commands that need to retrieve one or more existing entries.
+ """
+ takes_options = (
+ Flag('continue',
+ cli_name='continue',
+ doc=_('Continuous mode: Don\'t stop on errors.'),
+ ),
+ )
+
+ def get_args(self):
+ for arg in super(LDAPMultiQuery, self).get_args():
+ if self.obj.primary_key and arg.name == self.obj.primary_key.name:
+ yield arg.clone(multivalue=True)
+ else:
+ yield arg
+
+
+class LDAPRetrieve(LDAPQuery):
+ """
+ Retrieve an LDAP entry.
+ """
+ has_output = output.standard_entry
+ has_output_params = global_output_params
+
+ takes_options = (
+ Flag('rights',
+ label=_('Rights'),
+ doc=_('Display the access rights of this entry (requires --all). See ipa man page for details.'),
+ ),
+ )
+
+ def execute(self, *keys, **options):
+ ldap = self.obj.backend
+
+ dn = self.obj.get_dn(*keys, **options)
+ assert isinstance(dn, DN)
+
+ if options.get('all', False):
+ attrs_list = ['*'] + self.obj.default_attributes
+ else:
+ attrs_list = set(self.obj.default_attributes)
+ if options.get('no_members', False):
+ attrs_list.difference_update(self.obj.attribute_members)
+ attrs_list = list(attrs_list)
+
+ for callback in self.get_callbacks('pre'):
+ dn = callback(self, ldap, dn, attrs_list, *keys, **options)
+ assert isinstance(dn, DN)
+
+ try:
+ entry_attrs = self._exc_wrapper(keys, options, ldap.get_entry)(
+ dn, attrs_list
+ )
+ except errors.NotFound:
+ self.obj.handle_not_found(*keys)
+
+ self.obj.get_indirect_members(entry_attrs, attrs_list)
+
+ if options.get('rights', False) and options.get('all', False):
+ entry_attrs['attributelevelrights'] = get_effective_rights(
+ ldap, entry_attrs.dn)
+
+ for callback in self.get_callbacks('post'):
+ entry_attrs.dn = callback(
+ self, ldap, entry_attrs.dn, entry_attrs, *keys, **options)
+
+ self.obj.convert_attribute_members(entry_attrs, *keys, **options)
+
+ dn = entry_attrs.dn
+ entry_attrs = entry_to_dict(entry_attrs, **options)
+ entry_attrs['dn'] = dn
+
+ if self.obj.primary_key:
+ pkey = keys[-1]
+ else:
+ pkey = None
+
+ return dict(result=entry_attrs, value=pkey_to_value(pkey, options))
+
+ def pre_callback(self, ldap, dn, attrs_list, *keys, **options):
+ assert isinstance(dn, DN)
+ return dn
+
+ def post_callback(self, ldap, dn, entry_attrs, *keys, **options):
+ assert isinstance(dn, DN)
+ return dn
+
+ def exc_callback(self, keys, options, exc, call_func, *call_args, **call_kwargs):
+ raise exc
+
+
+class LDAPUpdate(LDAPQuery, crud.Update):
+ """
+ Update an LDAP entry.
+ """
+
+ takes_options = (
+ BaseLDAPCommand.setattr_option,
+ BaseLDAPCommand.addattr_option,
+ BaseLDAPCommand.delattr_option,
+ Flag('rights',
+ label=_('Rights'),
+ doc=_('Display the access rights of this entry (requires --all). See ipa man page for details.'),
+ ),
+ )
+
+ has_output_params = global_output_params
+
+ def _get_rename_option(self):
+ rdnparam = getattr(self.obj.params, self.obj.primary_key.name)
+ return rdnparam.clone_rename('rename',
+ cli_name='rename', required=False, label=_('Rename'),
+ doc=_('Rename the %(ldap_obj_name)s object') % dict(
+ ldap_obj_name=self.obj.object_name
+ )
+ )
+
+ def get_options(self):
+ for option in super(LDAPUpdate, self).get_options():
+ yield option
+ if self.obj.rdn_is_primary_key:
+ yield self._get_rename_option()
+
+ def execute(self, *keys, **options):
+ ldap = self.obj.backend
+
+ if len(options) == 2: # 'all' and 'raw' are always sent
+ raise errors.EmptyModlist()
+
+ dn = self.obj.get_dn(*keys, **options)
+ entry_attrs = ldap.make_entry(dn, self.args_options_2_entry(**options))
+
+ self.process_attr_options(entry_attrs, dn, keys, options)
+
+ if options.get('all', False):
+ attrs_list = ['*'] + self.obj.default_attributes
+ else:
+ attrs_list = set(self.obj.default_attributes)
+ attrs_list.update(entry_attrs.keys())
+ if options.get('no_members', False):
+ attrs_list.difference_update(self.obj.attribute_members)
+ attrs_list = list(attrs_list)
+
+ _check_single_value_attrs(self.params, entry_attrs)
+ _check_empty_attrs(self.obj.params, entry_attrs)
+
+ for callback in self.get_callbacks('pre'):
+ entry_attrs.dn = callback(
+ self, ldap, entry_attrs.dn, entry_attrs, attrs_list,
+ *keys, **options)
+
+ _check_limit_object_class(self.api.Backend.ldap2.schema.attribute_types(self.obj.limit_object_classes), list(entry_attrs), allow_only=True)
+ _check_limit_object_class(self.api.Backend.ldap2.schema.attribute_types(self.obj.disallow_object_classes), list(entry_attrs), allow_only=False)
+
+ rdnupdate = False
+ try:
+ if self.obj.rdn_is_primary_key and 'rename' in options:
+ if not options['rename']:
+ raise errors.ValidationError(name='rename', error=u'can\'t be empty')
+ entry_attrs[self.obj.primary_key.name] = options['rename']
+
+ if self.obj.rdn_is_primary_key and self.obj.primary_key.name in entry_attrs:
+ try:
+ # RDN change
+ new_dn = DN((self.obj.primary_key.name,
+ entry_attrs[self.obj.primary_key.name]),
+ *entry_attrs.dn[1:])
+ self._exc_wrapper(keys, options, ldap.move_entry)(
+ entry_attrs.dn,
+ new_dn)
+
+ rdnkeys = keys[:-1] + (entry_attrs[self.obj.primary_key.name], )
+ entry_attrs.dn = self.obj.get_dn(*rdnkeys)
+ options['rdnupdate'] = True
+ rdnupdate = True
+ except errors.EmptyModlist:
+ # Attempt to rename to the current name, ignore
+ pass
+ finally:
+ # Delete the primary_key from entry_attrs either way
+ del entry_attrs[self.obj.primary_key.name]
+
+ # Exception callbacks will need to test for options['rdnupdate']
+ # to decide what to do. An EmptyModlist in this context doesn't
+ # mean an error occurred, just that there were no other updates to
+ # perform.
+ update = self._exc_wrapper(keys, options, ldap.get_entry)(
+ entry_attrs.dn, list(entry_attrs))
+ update.update(entry_attrs)
+
+ self._exc_wrapper(keys, options, ldap.update_entry)(update)
+ except errors.EmptyModlist as e:
+ if not rdnupdate:
+ raise e
+ except errors.NotFound:
+ self.obj.handle_not_found(*keys)
+
+ try:
+ entry_attrs = self._exc_wrapper(keys, options, ldap.get_entry)(
+ entry_attrs.dn, attrs_list)
+ except errors.NotFound:
+ raise errors.MidairCollision(
+ format=_('the entry was deleted while being modified')
+ )
+
+ self.obj.get_indirect_members(entry_attrs, attrs_list)
+
+ if options.get('rights', False) and options.get('all', False):
+ entry_attrs['attributelevelrights'] = get_effective_rights(
+ ldap, entry_attrs.dn)
+
+ for callback in self.get_callbacks('post'):
+ entry_attrs.dn = callback(
+ self, ldap, entry_attrs.dn, entry_attrs, *keys, **options)
+
+ self.obj.convert_attribute_members(entry_attrs, *keys, **options)
+
+ entry_attrs = entry_to_dict(entry_attrs, **options)
+
+ if self.obj.primary_key:
+ pkey = keys[-1]
+ else:
+ pkey = None
+
+ return dict(result=entry_attrs, value=pkey_to_value(pkey, options))
+
+ def pre_callback(self, ldap, dn, entry_attrs, attrs_list, *keys, **options):
+ assert isinstance(dn, DN)
+ return dn
+
+ def post_callback(self, ldap, dn, entry_attrs, *keys, **options):
+ assert isinstance(dn, DN)
+ return dn
+
+ def exc_callback(self, keys, options, exc, call_func, *call_args, **call_kwargs):
+ raise exc
+
+
+class LDAPDelete(LDAPMultiQuery):
+ """
+ Delete an LDAP entry and all of its direct subentries.
+ """
+ has_output = output.standard_multi_delete
+
+ has_output_params = global_output_params
+
+ subtree_delete = True
+
+ def execute(self, *keys, **options):
+ ldap = self.obj.backend
+
+ def delete_entry(pkey):
+ nkeys = keys[:-1] + (pkey, )
+ dn = self.obj.get_dn(*nkeys, **options)
+ assert isinstance(dn, DN)
+
+ for callback in self.get_callbacks('pre'):
+ dn = callback(self, ldap, dn, *nkeys, **options)
+ assert isinstance(dn, DN)
+
+ def delete_subtree(base_dn):
+ assert isinstance(base_dn, DN)
+ truncated = True
+ while truncated:
+ try:
+ (subentries, truncated) = ldap.find_entries(
+ None, [''], base_dn, ldap.SCOPE_ONELEVEL
+ )
+ except errors.NotFound:
+ break
+ else:
+ for entry_attrs in subentries:
+ delete_subtree(entry_attrs.dn)
+ try:
+ self._exc_wrapper(nkeys, options, ldap.delete_entry)(base_dn)
+ except errors.NotFound:
+ self.obj.handle_not_found(*nkeys)
+
+ try:
+ self._exc_wrapper(nkeys, options, ldap.delete_entry)(dn)
+ except errors.NotFound:
+ self.obj.handle_not_found(*nkeys)
+ except errors.NotAllowedOnNonLeaf:
+ if not self.subtree_delete:
+ raise
+ # this entry is not a leaf entry, delete all child nodes
+ delete_subtree(dn)
+
+ for callback in self.get_callbacks('post'):
+ result = callback(self, ldap, dn, *nkeys, **options)
+
+ return result
+
+ if self.obj.primary_key and isinstance(keys[-1], (list, tuple)):
+ pkeyiter = keys[-1]
+ elif keys[-1] is not None:
+ pkeyiter = [keys[-1]]
+ else:
+ pkeyiter = []
+
+ deleted = []
+ failed = []
+ for pkey in pkeyiter:
+ try:
+ delete_entry(pkey)
+ except errors.ExecutionError:
+ if not options.get('continue', False):
+ raise
+ failed.append(pkey)
+ else:
+ deleted.append(pkey)
+ deleted = pkey_to_value(deleted, options)
+ failed = pkey_to_value(failed, options)
+
+ return dict(result=dict(failed=failed), value=deleted)
+
+ def pre_callback(self, ldap, dn, *keys, **options):
+ assert isinstance(dn, DN)
+ return dn
+
+ def post_callback(self, ldap, dn, *keys, **options):
+ assert isinstance(dn, DN)
+ return True
+
+ def exc_callback(self, keys, options, exc, call_func, *call_args, **call_kwargs):
+ raise exc
+
+
+class LDAPModMember(LDAPQuery):
+ """
+ Base class for member manipulation.
+ """
+ member_attributes = ['member']
+ member_param_doc = _('%s')
+ member_param_label = _('member %s')
+ member_count_out = ('%i member processed.', '%i members processed.')
+
+ def get_options(self):
+ for option in super(LDAPModMember, self).get_options():
+ yield option
+ for attr in self.member_attributes:
+ for ldap_obj_name in self.obj.attribute_members[attr]:
+ ldap_obj = self.api.Object[ldap_obj_name]
+ name = to_cli(ldap_obj_name)
+ doc = self.member_param_doc % ldap_obj.object_name_plural
+ label = self.member_param_label % ldap_obj.object_name
+ yield Str('%s*' % name, cli_name='%ss' % name, doc=doc,
+ label=label, alwaysask=True)
+
+ def get_member_dns(self, **options):
+ dns = {}
+ failed = {}
+ for attr in self.member_attributes:
+ dns[attr] = {}
+ failed[attr] = {}
+ for ldap_obj_name in self.obj.attribute_members[attr]:
+ dns[attr][ldap_obj_name] = []
+ failed[attr][ldap_obj_name] = []
+ names = options.get(to_cli(ldap_obj_name), [])
+ if not names:
+ continue
+ for name in names:
+ if not name:
+ continue
+ ldap_obj = self.api.Object[ldap_obj_name]
+ try:
+ dns[attr][ldap_obj_name].append(ldap_obj.get_dn(name))
+ except errors.PublicError as e:
+ failed[attr][ldap_obj_name].append((name, unicode(e)))
+ return (dns, failed)
+
+
+class LDAPAddMember(LDAPModMember):
+ """
+ Add other LDAP entries to members.
+ """
+ member_param_doc = _('%s to add')
+ member_count_out = ('%i member added.', '%i members added.')
+ allow_same = False
+
+ has_output = (
+ output.Entry('result'),
+ output.Output('failed',
+ type=dict,
+ doc=_('Members that could not be added'),
+ ),
+ output.Output('completed',
+ type=int,
+ doc=_('Number of members added'),
+ ),
+ )
+
+ has_output_params = global_output_params
+
+ def execute(self, *keys, **options):
+ ldap = self.obj.backend
+
+ (member_dns, failed) = self.get_member_dns(**options)
+
+ dn = self.obj.get_dn(*keys, **options)
+ assert isinstance(dn, DN)
+
+ for callback in self.get_callbacks('pre'):
+ dn = callback(self, ldap, dn, member_dns, failed, *keys, **options)
+ assert isinstance(dn, DN)
+
+ completed = 0
+ for (attr, objs) in member_dns.items():
+ for ldap_obj_name in objs:
+ for m_dn in member_dns[attr][ldap_obj_name]:
+ assert isinstance(m_dn, DN)
+ if not m_dn:
+ continue
+ try:
+ ldap.add_entry_to_group(m_dn, dn, attr, allow_same=self.allow_same)
+ except errors.PublicError as e:
+ ldap_obj = self.api.Object[ldap_obj_name]
+ failed[attr][ldap_obj_name].append((
+ ldap_obj.get_primary_key_from_dn(m_dn),
+ unicode(e),)
+ )
+ else:
+ completed += 1
+
+ if options.get('all', False):
+ attrs_list = ['*'] + self.obj.default_attributes
+ else:
+ attrs_list = set(self.obj.default_attributes)
+ attrs_list.update(member_dns.keys())
+ if options.get('no_members', False):
+ attrs_list.difference_update(self.obj.attribute_members)
+ attrs_list = list(attrs_list)
+
+ try:
+ entry_attrs = self._exc_wrapper(keys, options, ldap.get_entry)(
+ dn, attrs_list
+ )
+ except errors.NotFound:
+ self.obj.handle_not_found(*keys)
+
+ self.obj.get_indirect_members(entry_attrs, attrs_list)
+
+ for callback in self.get_callbacks('post'):
+ (completed, entry_attrs.dn) = callback(
+ self, ldap, completed, failed, entry_attrs.dn, entry_attrs,
+ *keys, **options)
+
+ self.obj.convert_attribute_members(entry_attrs, *keys, **options)
+
+ dn = entry_attrs.dn
+ entry_attrs = entry_to_dict(entry_attrs, **options)
+ entry_attrs['dn'] = dn
+
+ return dict(
+ completed=completed,
+ failed=failed,
+ result=entry_attrs,
+ )
+
+ def pre_callback(self, ldap, dn, found, not_found, *keys, **options):
+ assert isinstance(dn, DN)
+ return dn
+
+ def post_callback(self, ldap, completed, failed, dn, entry_attrs, *keys, **options):
+ assert isinstance(dn, DN)
+ return (completed, dn)
+
+ def exc_callback(self, keys, options, exc, call_func, *call_args, **call_kwargs):
+ raise exc
+
+
+class LDAPRemoveMember(LDAPModMember):
+ """
+ Remove LDAP entries from members.
+ """
+ member_param_doc = _('%s to remove')
+ member_count_out = ('%i member removed.', '%i members removed.')
+
+ has_output = (
+ output.Entry('result'),
+ output.Output('failed',
+ type=dict,
+ doc=_('Members that could not be removed'),
+ ),
+ output.Output('completed',
+ type=int,
+ doc=_('Number of members removed'),
+ ),
+ )
+
+ has_output_params = global_output_params
+
+ def execute(self, *keys, **options):
+ ldap = self.obj.backend
+
+ (member_dns, failed) = self.get_member_dns(**options)
+
+ dn = self.obj.get_dn(*keys, **options)
+ assert isinstance(dn, DN)
+
+ for callback in self.get_callbacks('pre'):
+ dn = callback(self, ldap, dn, member_dns, failed, *keys, **options)
+ assert isinstance(dn, DN)
+
+ completed = 0
+ for (attr, objs) in member_dns.items():
+ for ldap_obj_name, m_dns in objs.items():
+ for m_dn in m_dns:
+ assert isinstance(m_dn, DN)
+ if not m_dn:
+ continue
+ try:
+ ldap.remove_entry_from_group(m_dn, dn, attr)
+ except errors.PublicError as e:
+ ldap_obj = self.api.Object[ldap_obj_name]
+ failed[attr][ldap_obj_name].append((
+ ldap_obj.get_primary_key_from_dn(m_dn),
+ unicode(e),)
+ )
+ else:
+ completed += 1
+
+ if options.get('all', False):
+ attrs_list = ['*'] + self.obj.default_attributes
+ else:
+ attrs_list = set(self.obj.default_attributes)
+ attrs_list.update(member_dns.keys())
+ if options.get('no_members', False):
+ attrs_list.difference_update(self.obj.attribute_members)
+ attrs_list = list(attrs_list)
+
+ # Give memberOf a chance to update entries
+ time.sleep(.3)
+
+ try:
+ entry_attrs = self._exc_wrapper(keys, options, ldap.get_entry)(
+ dn, attrs_list
+ )
+ except errors.NotFound:
+ self.obj.handle_not_found(*keys)
+
+ self.obj.get_indirect_members(entry_attrs, attrs_list)
+
+ for callback in self.get_callbacks('post'):
+ (completed, entry_attrs.dn) = callback(
+ self, ldap, completed, failed, entry_attrs.dn, entry_attrs,
+ *keys, **options)
+
+ self.obj.convert_attribute_members(entry_attrs, *keys, **options)
+
+ dn = entry_attrs.dn
+ entry_attrs = entry_to_dict(entry_attrs, **options)
+ entry_attrs['dn'] = dn
+
+ return dict(
+ completed=completed,
+ failed=failed,
+ result=entry_attrs,
+ )
+
+ def pre_callback(self, ldap, dn, found, not_found, *keys, **options):
+ assert isinstance(dn, DN)
+ return dn
+
+ def post_callback(self, ldap, completed, failed, dn, entry_attrs, *keys, **options):
+ assert isinstance(dn, DN)
+ return (completed, dn)
+
+ def exc_callback(self, keys, options, exc, call_func, *call_args, **call_kwargs):
+ raise exc
+
+
+def gen_pkey_only_option(cli_name):
+ return Flag('pkey_only?',
+ label=_('Primary key only'),
+ doc=_('Results should contain primary key attribute only ("%s")') \
+ % to_cli(cli_name),)
+
+class LDAPSearch(BaseLDAPCommand, crud.Search):
+ """
+ Retrieve all LDAP entries matching the given criteria.
+ """
+ member_attributes = []
+ member_param_incl_doc = _('Search for %(searched_object)s with these %(relationship)s %(ldap_object)s.')
+ member_param_excl_doc = _('Search for %(searched_object)s without these %(relationship)s %(ldap_object)s.')
+
+ # LDAPSearch sorts all matched records in the end using their primary key
+ # as a key attribute
+ # Set the following attribute to False to turn sorting off
+ sort_result_entries = True
+
+ takes_options = (
+ Int('timelimit?',
+ label=_('Time Limit'),
+ doc=_('Time limit of search in seconds (0 is unlimited)'),
+ flags=['no_display'],
+ minvalue=0,
+ autofill=False,
+ ),
+ Int('sizelimit?',
+ label=_('Size Limit'),
+ doc=_('Maximum number of entries returned (0 is unlimited)'),
+ flags=['no_display'],
+ minvalue=0,
+ autofill=False,
+ ),
+ )
+
+ def get_args(self):
+ for key in self.obj.get_ancestor_primary_keys():
+ yield key
+ for arg in super(LDAPSearch, self).get_args():
+ yield arg
+
+ def get_member_options(self, attr):
+ for ldap_obj_name in self.obj.attribute_members[attr]:
+ ldap_obj = self.api.Object[ldap_obj_name]
+ relationship = self.obj.relationships.get(
+ attr, ['member', '', 'no_']
+ )
+ doc = self.member_param_incl_doc % dict(
+ searched_object=self.obj.object_name_plural,
+ relationship=relationship[0].lower(),
+ ldap_object=ldap_obj.object_name_plural
+ )
+ name = '%s%s' % (relationship[1], to_cli(ldap_obj_name))
+ yield Str(
+ '%s*' % name, cli_name='%ss' % name, doc=doc,
+ label=ldap_obj.object_name
+ )
+ doc = self.member_param_excl_doc % dict(
+ searched_object=self.obj.object_name_plural,
+ relationship=relationship[0].lower(),
+ ldap_object=ldap_obj.object_name_plural
+ )
+ name = '%s%s' % (relationship[2], to_cli(ldap_obj_name))
+ yield Str(
+ '%s*' % name, cli_name='%ss' % name, doc=doc,
+ label=ldap_obj.object_name
+ )
+
+ def get_options(self):
+ for option in super(LDAPSearch, self).get_options():
+ if option.name == 'no_members':
+ # no_members are always true for find commands, do not
+ # show option in CLI but keep API compatibility
+ option = option.clone(
+ default=True, flags=option.flags | {"no_option"})
+ yield option
+ if self.obj.primary_key and \
+ 'no_output' not in self.obj.primary_key.flags:
+ yield gen_pkey_only_option(self.obj.primary_key.cli_name)
+ for attr in self.member_attributes:
+ for option in self.get_member_options(attr):
+ yield option
+
+ def get_member_filter(self, ldap, **options):
+ filter = ''
+ for attr in self.member_attributes:
+ for ldap_obj_name in self.obj.attribute_members[attr]:
+ ldap_obj = self.api.Object[ldap_obj_name]
+ relationship = self.obj.relationships.get(
+ attr, ['member', '', 'no_']
+ )
+ # Handle positive (MATCH_ALL) and negative (MATCH_NONE)
+ # searches similarly
+ param_prefixes = relationship[1:] # e.g. ('in_', 'not_in_')
+ rules = ldap.MATCH_ALL, ldap.MATCH_NONE
+ for param_prefix, rule in zip(param_prefixes, rules):
+ param_name = '%s%s' % (param_prefix, to_cli(ldap_obj_name))
+ if options.get(param_name):
+ dns = []
+ for pkey in options[param_name]:
+ dns.append(ldap_obj.get_dn(pkey))
+ flt = ldap.make_filter_from_attr(attr, dns, rule)
+ filter = ldap.combine_filters(
+ (filter, flt), ldap.MATCH_ALL
+ )
+ return filter
+
+ has_output_params = global_output_params
+
+ def execute(self, *args, **options):
+ ldap = self.obj.backend
+
+ index = tuple(self.args).index('criteria')
+ keys = args[:index]
+ try:
+ term = args[index]
+ except IndexError:
+ term = None
+ if self.obj.parent_object:
+ base_dn = self.api.Object[self.obj.parent_object].get_dn(*keys)
+ else:
+ base_dn = DN(self.obj.container_dn, api.env.basedn)
+ assert isinstance(base_dn, DN)
+
+ search_kw = self.args_options_2_entry(**options)
+
+ if self.obj.search_display_attributes:
+ defattrs = self.obj.search_display_attributes
+ else:
+ defattrs = self.obj.default_attributes
+
+ if options.get('pkey_only', False):
+ attrs_list = [self.obj.primary_key.name]
+ elif options.get('all', False):
+ attrs_list = ['*'] + defattrs
+ else:
+ attrs_list = set(defattrs)
+ attrs_list.update(search_kw.keys())
+ if options.get('no_members', False):
+ attrs_list.difference_update(self.obj.attribute_members)
+ attrs_list = list(attrs_list)
+
+ if self.obj.search_attributes:
+ search_attrs = self.obj.search_attributes
+ else:
+ search_attrs = self.obj.default_attributes
+ if self.obj.search_attributes_config:
+ config = ldap.get_ipa_config()
+ config_attrs = config.get(
+ self.obj.search_attributes_config, [])
+ if len(config_attrs) == 1 and (
+ isinstance(config_attrs[0], six.string_types)):
+ search_attrs = config_attrs[0].split(',')
+
+ search_kw['objectclass'] = self.obj.object_class
+ attr_filter = ldap.make_filter(search_kw, rules=ldap.MATCH_ALL)
+
+ search_kw = {}
+ for a in search_attrs:
+ search_kw[a] = term
+ term_filter = ldap.make_filter(search_kw, exact=False)
+
+ member_filter = self.get_member_filter(ldap, **options)
+
+ filter = ldap.combine_filters(
+ (term_filter, attr_filter, member_filter), rules=ldap.MATCH_ALL
+ )
+
+ scope = ldap.SCOPE_ONELEVEL
+ for callback in self.get_callbacks('pre'):
+ (filter, base_dn, scope) = callback(
+ self, ldap, filter, attrs_list, base_dn, scope, *args, **options)
+ assert isinstance(base_dn, DN)
+
+ try:
+ (entries, truncated) = self._exc_wrapper(args, options, ldap.find_entries)(
+ filter, attrs_list, base_dn, scope,
+ time_limit=options.get('timelimit', None),
+ size_limit=options.get('sizelimit', None)
+ )
+ except errors.EmptyResult:
+ (entries, truncated) = ([], False)
+ except errors.NotFound:
+ self.api.Object[self.obj.parent_object].handle_not_found(*keys)
+
+ for callback in self.get_callbacks('post'):
+ truncated = callback(self, ldap, entries, truncated, *args, **options)
+
+ if self.sort_result_entries:
+ if self.obj.primary_key:
+ def sort_key(x):
+ return self.obj.primary_key.sort_key(
+ x[self.obj.primary_key.name][0])
+ entries.sort(key=sort_key)
+
+ if not options.get('raw', False):
+ for e in entries:
+ self.obj.get_indirect_members(e, attrs_list)
+ self.obj.convert_attribute_members(e, *args, **options)
+
+ for (i, e) in enumerate(entries):
+ entries[i] = entry_to_dict(e, **options)
+ entries[i]['dn'] = e.dn
+
+ result = dict(
+ result=entries,
+ count=len(entries),
+ truncated=bool(truncated),
+ )
+
+ try:
+ ldap.handle_truncated_result(truncated)
+ except errors.LimitsExceeded as e:
+ add_message(options['version'], result, SearchResultTruncated(
+ reason=e))
+
+ return result
+
+ def pre_callback(self, ldap, filters, attrs_list, base_dn, scope, *args, **options):
+ assert isinstance(base_dn, DN)
+ return (filters, base_dn, scope)
+
+ def post_callback(self, ldap, entries, truncated, *args, **options):
+ return truncated
+
+ def exc_callback(self, args, options, exc, call_func, *call_args, **call_kwargs):
+ raise exc
+
+
+class LDAPModReverseMember(LDAPQuery):
+ """
+ Base class for reverse member manipulation.
+ """
+ reverse_attributes = ['member']
+ reverse_param_doc = _('%s')
+ reverse_count_out = ('%i member processed.', '%i members processed.')
+
+ has_output_params = global_output_params
+
+ def get_options(self):
+ for option in super(LDAPModReverseMember, self).get_options():
+ yield option
+ for attr in self.reverse_attributes:
+ for ldap_obj_name in self.obj.reverse_members[attr]:
+ ldap_obj = self.api.Object[ldap_obj_name]
+ name = to_cli(ldap_obj_name)
+ doc = self.reverse_param_doc % ldap_obj.object_name_plural
+ yield Str('%s*' % name, cli_name='%ss' % name, doc=doc,
+ label=ldap_obj.object_name, alwaysask=True)
+
+
+class LDAPAddReverseMember(LDAPModReverseMember):
+ """
+ Add other LDAP entries to members in reverse.
+
+ The call looks like "add A to B" but in fact executes
+ add B to A to handle reverse membership.
+ """
+ member_param_doc = _('%s to add')
+ member_count_out = ('%i member added.', '%i members added.')
+
+ show_command = None
+ member_command = None
+ reverse_attr = None
+ member_attr = None
+
+ has_output = (
+ output.Entry('result'),
+ output.Output('failed',
+ type=dict,
+ doc=_('Members that could not be added'),
+ ),
+ output.Output('completed',
+ type=int,
+ doc=_('Number of members added'),
+ ),
+ )
+
+ has_output_params = global_output_params
+
+ def execute(self, *keys, **options):
+ ldap = self.obj.backend
+
+ # Ensure our target exists
+ result = self.api.Command[self.show_command](keys[-1])['result']
+ dn = result['dn']
+ assert isinstance(dn, DN)
+
+ for callback in self.get_callbacks('pre'):
+ dn = callback(self, ldap, dn, *keys, **options)
+ assert isinstance(dn, DN)
+
+ if options.get('all', False):
+ attrs_list = ['*'] + self.obj.default_attributes
+ else:
+ attrs_list = set(self.obj.default_attributes)
+ if options.get('no_members', False):
+ attrs_list.difference_update(self.obj.attribute_members)
+ attrs_list = list(attrs_list)
+
+ completed = 0
+ failed = {'member': {self.reverse_attr: []}}
+ for attr in options.get(self.reverse_attr) or []:
+ try:
+ options = {'%s' % self.member_attr: keys[-1]}
+ try:
+ result = self._exc_wrapper(keys, options, self.api.Command[self.member_command])(attr, **options)
+ if result['completed'] == 1:
+ completed = completed + 1
+ else:
+ failed['member'][self.reverse_attr].append((attr, result['failed']['member'][self.member_attr][0][1]))
+ except errors.NotFound as e:
+ msg = str(e)
+ (attr, msg) = msg.split(':', 1)
+ failed['member'][self.reverse_attr].append((attr, unicode(msg.strip())))
+
+ except errors.PublicError as e:
+ failed['member'][self.reverse_attr].append((attr, unicode(e)))
+
+ # Update the member data.
+ entry_attrs = ldap.get_entry(dn, ['*'])
+ self.obj.convert_attribute_members(entry_attrs, *keys, **options)
+
+ for callback in self.get_callbacks('post'):
+ (completed, entry_attrs.dn) = callback(
+ self, ldap, completed, failed, entry_attrs.dn, entry_attrs,
+ *keys, **options)
+
+ dn = entry_attrs.dn
+ entry_attrs = entry_to_dict(entry_attrs, **options)
+ entry_attrs['dn'] = dn
+
+ return dict(
+ completed=completed,
+ failed=failed,
+ result=entry_attrs,
+ )
+
+ def pre_callback(self, ldap, dn, *keys, **options):
+ assert isinstance(dn, DN)
+ return dn
+
+ def post_callback(self, ldap, completed, failed, dn, entry_attrs, *keys, **options):
+ assert isinstance(dn, DN)
+ return (completed, dn)
+
+ def exc_callback(self, keys, options, exc, call_func, *call_args, **call_kwargs):
+ raise exc
+
+
+class LDAPRemoveReverseMember(LDAPModReverseMember):
+ """
+ Remove other LDAP entries from members in reverse.
+
+ The call looks like "remove A from B" but in fact executes
+ remove B from A to handle reverse membership.
+ """
+ member_param_doc = _('%s to remove')
+ member_count_out = ('%i member removed.', '%i members removed.')
+
+ show_command = None
+ member_command = None
+ reverse_attr = None
+ member_attr = None
+
+ has_output = (
+ output.Entry('result'),
+ output.Output('failed',
+ type=dict,
+ doc=_('Members that could not be removed'),
+ ),
+ output.Output('completed',
+ type=int,
+ doc=_('Number of members removed'),
+ ),
+ )
+
+ has_output_params = global_output_params
+
+ def execute(self, *keys, **options):
+ ldap = self.obj.backend
+
+ # Ensure our target exists
+ result = self.api.Command[self.show_command](keys[-1])['result']
+ dn = result['dn']
+ assert isinstance(dn, DN)
+
+ for callback in self.get_callbacks('pre'):
+ dn = callback(self, ldap, dn, *keys, **options)
+ assert isinstance(dn, DN)
+
+ if options.get('all', False):
+ attrs_list = ['*'] + self.obj.default_attributes
+ else:
+ attrs_list = set(self.obj.default_attributes)
+ if options.get('no_members', False):
+ attrs_list.difference_update(self.obj.attribute_members)
+ attrs_list = list(attrs_list)
+
+ completed = 0
+ failed = {'member': {self.reverse_attr: []}}
+ for attr in options.get(self.reverse_attr) or []:
+ try:
+ options = {'%s' % self.member_attr: keys[-1]}
+ try:
+ result = self._exc_wrapper(keys, options, self.api.Command[self.member_command])(attr, **options)
+ if result['completed'] == 1:
+ completed = completed + 1
+ else:
+ failed['member'][self.reverse_attr].append((attr, result['failed']['member'][self.member_attr][0][1]))
+ except errors.NotFound as e:
+ msg = str(e)
+ (attr, msg) = msg.split(':', 1)
+ failed['member'][self.reverse_attr].append((attr, unicode(msg.strip())))
+
+ except errors.PublicError as e:
+ failed['member'][self.reverse_attr].append((attr, unicode(e)))
+
+ # Update the member data.
+ entry_attrs = ldap.get_entry(dn, ['*'])
+ self.obj.convert_attribute_members(entry_attrs, *keys, **options)
+
+ for callback in self.get_callbacks('post'):
+ (completed, entry_attrs.dn) = callback(
+ self, ldap, completed, failed, entry_attrs.dn, entry_attrs,
+ *keys, **options)
+
+ dn = entry_attrs.dn
+ entry_attrs = entry_to_dict(entry_attrs, **options)
+ entry_attrs['dn'] = dn
+
+ return dict(
+ completed=completed,
+ failed=failed,
+ result=entry_attrs,
+ )
+
+ def pre_callback(self, ldap, dn, *keys, **options):
+ assert isinstance(dn, DN)
+ return dn
+
+ def post_callback(self, ldap, completed, failed, dn, entry_attrs, *keys, **options):
+ assert isinstance(dn, DN)
+ return (completed, dn)
+
+ def exc_callback(self, keys, options, exc, call_func, *call_args, **call_kwargs):
+ raise exc
+
+
+class LDAPModAttribute(LDAPQuery):
+
+ attribute = None
+
+ has_output = output.standard_entry
+
+ def get_options(self):
+ for option in super(LDAPModAttribute, self).get_options():
+ yield option
+
+ option = self.obj.params[self.attribute]
+ attribute = 'virtual_attribute' not in option.flags
+ yield option.clone(attribute=attribute, alwaysask=True)
+
+ def _update_attrs(self, update, entry_attrs):
+ raise NotImplementedError("%s.update_attrs()", self.__class__.__name__)
+
+ def execute(self, *keys, **options):
+ ldap = self.obj.backend
+
+ dn = self.obj.get_dn(*keys, **options)
+ entry_attrs = ldap.make_entry(dn, self.args_options_2_entry(**options))
+
+ if options.get('all', False):
+ attrs_list = ['*', self.obj.primary_key.name]
+ else:
+ attrs_list = {self.obj.primary_key.name}
+ attrs_list.update(entry_attrs.keys())
+ attrs_list = list(attrs_list)
+
+ for callback in self.get_callbacks('pre'):
+ entry_attrs.dn = callback(
+ self, ldap, entry_attrs.dn, entry_attrs, attrs_list,
+ *keys, **options)
+
+ try:
+ update = self._exc_wrapper(keys, options, ldap.get_entry)(
+ entry_attrs.dn, list(entry_attrs))
+ self._update_attrs(update, entry_attrs)
+
+ self._exc_wrapper(keys, options, ldap.update_entry)(update)
+ except errors.NotFound:
+ self.obj.handle_not_found(*keys)
+
+ try:
+ entry_attrs = self._exc_wrapper(keys, options, ldap.get_entry)(
+ entry_attrs.dn, attrs_list)
+ except errors.NotFound:
+ raise errors.MidairCollision(
+ format=_('the entry was deleted while being modified')
+ )
+
+ for callback in self.get_callbacks('post'):
+ entry_attrs.dn = callback(
+ self, ldap, entry_attrs.dn, entry_attrs, *keys, **options)
+
+ entry_attrs = entry_to_dict(entry_attrs, **options)
+
+ if self.obj.primary_key:
+ pkey = keys[-1]
+ else:
+ pkey = None
+
+ return dict(result=entry_attrs, value=pkey_to_value(pkey, options))
+
+ def pre_callback(self, ldap, dn, entry_attrs, attrs_list, *keys,
+ **options):
+ assert isinstance(dn, DN)
+ return dn
+
+ def post_callback(self, ldap, dn, entry_attrs, *keys, **options):
+ assert isinstance(dn, DN)
+ return dn
+
+ def exc_callback(self, keys, options, exc, call_func, *call_args,
+ **call_kwargs):
+ raise exc
+
+
+class LDAPAddAttribute(LDAPModAttribute):
+ msg_summary = _('added attribute value to entry %(value)')
+
+ def _update_attrs(self, update, entry_attrs):
+ for name, value in entry_attrs.items():
+ old_value = set(update.get(name, []))
+ value_to_add = set(value)
+
+ if not old_value.isdisjoint(value_to_add):
+ raise errors.ExecutionError(
+ message=_('\'%s\' already contains one or more values'
+ % name)
+ )
+
+ update[name] = list(old_value | value_to_add)
+
+
+class LDAPRemoveAttribute(LDAPModAttribute):
+ msg_summary = _('removed attribute values from entry %(value)')
+
+ def _update_attrs(self, update, entry_attrs):
+ for name, value in entry_attrs.items():
+ old_value = set(update.get(name, []))
+ value_to_remove = set(value)
+
+ if not value_to_remove.issubset(old_value):
+ raise errors.AttrValueNotFound(
+ attr=name, value=_("one or more values to remove"))
+
+ update[name] = list(old_value - value_to_remove)