# Authors: # Pavel Zuna # # 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; version 2 only # # 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, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """ Base classes for LDAP plugins. """ from ipalib import crud, errors from ipalib import Command, Method, Object from ipalib import Flag, List, Str from ipalib.cli import to_cli, from_cli from ipalib.base import NameSpace class LDAPObject(Object): """ Object representing a LDAP entry. """ __public__ = frozenset(( 'backend', 'methods', 'properties', 'params', 'primary_key', 'parent_key', 'params_minus_pk', 'params_minus', 'get_dn', 'container_dn', 'object_name', 'object_name_plural', 'parent_object_name', 'object_class', 'object_class_config', 'default_attributes', 'hidden_attributes', 'attribute_names', 'attribute_order', 'attribute_members', 'get_primary_key_from_dn', 'convert_attribute_members', 'print_entry', )) parent_key = None backend_name = 'ldap2' container_dn = '' object_name = 'entry' object_name_plural = 'entries' parent_object_name = '' object_class = ['top'] object_class_config = None default_attributes = [''] hidden_attributes = ['objectclass', 'aci'] attribute_names = {} attribute_order = [] attribute_members = {} def set_api(self, api): super(LDAPObject, self).set_api(api) parent_keys = filter(lambda p: p.parent_key, self.params()) if len(parent_keys) > 1: raise ValueError( '%s (LDAPObject) has multiple parent keys: %s' % ( self.name, ', '.join(p.name for p in parent_keys), ) ) if len(parent_keys) == 1: self.parent_key = parent_keys[0] self.params_minus_pk = NameSpace( filter( lambda p: not p.primary_key and not p.parent_key, self.params() ), sort=False ) elif self.params_minus_pk is None: self.params_minus_pk = self.params def get_dn(self, *keys, **kwargs): if len(keys) > 1: parent_dn = self.backend.make_dn_from_attr( self.parent_key.name, keys[0], self.container_dn ) else: parent_dn = self.container_dn return self.backend.make_dn_from_attr( self.primary_key.name, keys[-1], parent_dn ) def get_primary_key_from_dn(self, dn): return dn[len(self.primary_key.name) + 1:dn.find(',')] def convert_attribute_members(self, entry_attrs, *keys, **options): if options.get('raw', False): return for attr in self.attribute_members: for member in entry_attrs.setdefault(attr, []): for ldap_obj_name in self.attribute_members[attr]: ldap_obj = self.api.Object[ldap_obj_name] if member.find(ldap_obj.container_dn) > 0: new_attr = 'member %s' % ldap_obj.object_name_plural entry_attrs.setdefault(new_attr, []).append( ldap_obj.get_primary_key_from_dn(member) ) del entry_attrs[attr] def print_entry(self, textui, entry, *keys, **options): if options.get('raw', False): textui.print_attribute('dn', entry[0]) textui.print_entry(entry[1], attr_order=self.attribute_order) else: if self.primary_key: textui.print_attribute( self.object_name.capitalize(), keys[-1], indent=0 ) else: textui.print_plain(self.object_name.capitalize()) entry_attrs = entry[1] for a in self.hidden_attributes: if a in entry_attrs: del entry_attrs[a] textui.print_entry( entry_attrs, attr_map=self.attribute_names, attr_order=self.attribute_order ) class LDAPCreate(crud.Create): """ Create a new entry in LDAP. """ takes_options = ( Flag('raw', cli_name='raw', doc='print entries as they are stored in LDAP', ), ) def get_args(self): if self.obj.parent_key: yield self.obj.parent_key.clone(query=True) if self.obj.primary_key: yield self.obj.primary_key.clone(attribute=True) def execute(self, *keys, **options): ldap = self.obj.backend dn = self.obj.get_dn(*keys, **options) entry_attrs = self.args_options_2_entry(*keys, **options) entry_attrs['objectclass'] = self.obj.object_class if self.obj.object_class_config: config = ldap.get_ipa_config()[1] entry_attrs['objectclass'] = config.get( self.obj.object_class_config, entry_attrs['objectclass'] ) dn = self.pre_callback(ldap, dn, entry_attrs, *keys, **options) ldap.add_entry(dn, entry_attrs) (dn, entry_attrs) = ldap.get_entry(dn, entry_attrs.keys()) dn = self.post_callback(ldap, dn, entry_attrs, *keys, **options) self.obj.convert_attribute_members(entry_attrs, *keys, **options) return (dn, entry_attrs) def output_for_cli(self, textui, entry, *keys, **options): textui.print_name(self.name) self.obj.print_entry(textui, entry, *keys, **options) if len(keys) > 1: textui.print_dashed( 'Created %s "%s" in %s "%s".' % ( self.obj.object_name, keys[1], self.obj.parent_object_name, keys[0] ) ) elif len(keys) == 1: textui.print_dashed( 'Created %s "%s".' % (self.obj.object_name, keys[0]) ) else: textui.print_dashed('Created %s.' % self.obj.object_name) def pre_callback(self, ldap, dn, entry_attrs, *keys, **options): return dn def post_callback(self, ldap, dn, entry_attrs, *keys, **options): return dn class LDAPQuery(crud.PKQuery): """ Base class for commands that need to retrieve an existing entry. """ def get_args(self): if self.obj.parent_key: yield self.obj.parent_key.clone(query=True) if self.obj.primary_key: yield self.obj.primary_key.clone(attribute=True, query=True) class LDAPRetrieve(LDAPQuery): """ Retrieve an LDAP entry. """ takes_options = ( Flag('raw', cli_name='raw', doc='print entries as they are stored in LDAP', ), Flag('all', cli_name='all', doc='retrieve all attributes', ), ) def execute(self, *keys, **options): ldap = self.obj.backend dn = self.obj.get_dn(*keys, **options) if options['all']: attrs_list = ['*'] else: attrs_list = list(self.obj.default_attributes) dn = self.pre_callback(ldap, dn, attrs_list, *keys, **options) (dn, entry_attrs) = ldap.get_entry(dn, attrs_list) dn = self.post_callback(ldap, dn, entry_attrs, *keys, **options) self.obj.convert_attribute_members(entry_attrs, *keys, **options) return (dn, entry_attrs) def output_for_cli(self, textui, entry, *keys, **options): textui.print_name(self.name) self.obj.print_entry(textui, entry, *keys, **options) def pre_callback(self, ldap, dn, attrs_list, *keys, **options): return dn def post_callback(self, ldap, dn, attrs_list, *keys, **options): return dn class LDAPUpdate(LDAPQuery, crud.Update): """ Update an LDAP entry. """ takes_options = ( Flag('raw', cli_name='raw', doc='print entries as they are stored in LDAP', ), ) def execute(self, *keys, **options): ldap = self.obj.backend dn = self.obj.get_dn(*keys, **options) entry_attrs = self.args_options_2_entry(**options) dn = self.pre_callback(ldap, dn, entry_attrs, *keys, **options) try: ldap.update_entry(dn, entry_attrs) except errors.EmptyModlist: pass (dn, entry_attrs) = ldap.get_entry(dn, entry_attrs.keys()) dn = self.post_callback(ldap, dn, entry_attrs, *keys, **options) self.obj.convert_attribute_members(entry_attrs, *keys, **options) return (dn, entry_attrs) def output_for_cli(self, textui, entry, *keys, **options): textui.print_name(self.name) self.obj.print_entry(textui, entry, *keys, **options) if len(keys) > 1: textui.print_dashed( 'Modified %s "%s" in %s "%s".' % ( self.obj.object_name, keys[1], self.obj.parent_object_name, keys[0] ) ) elif len(keys) == 1: textui.print_dashed( 'Modified %s "%s".' % (self.obj.object_name, keys[0]) ) else: textui.print_dashed('Modified %s.' % self.obj.object_name) def pre_callback(self, ldap, dn, entry_attrs, *keys, **options): return dn def post_callback(self, ldap, dn, entry_attrs, *keys, **options): return dn class LDAPDelete(LDAPQuery): """ Delete an LDAP entry and all of its direct subentries. """ def execute(self, *keys, **options): ldap = self.obj.backend dn = self.obj.get_dn(*keys, **options) dn = self.pre_callback(ldap, dn, *keys, **options) truncated = True while truncated: try: (subentries, truncated) = ldap.find_entries( None, [''], dn, ldap.SCOPE_ONELEVEL ) except errors.NotFound: break else: for (dn_, entry_attrs) in subentries: ldap.delete_entry(dn_) ldap.delete_entry(dn) result = self.post_callback(ldap, dn, *keys, **options) return result def output_for_cli(self, textui, result, *keys, **options): textui.print_name(self.name) if len(keys) > 1: textui.print_dashed( 'Deleted %s "%s" in %s "%s".' % ( self.obj.object_name, keys[1], self.obj.parent_object_name, keys[0] ) ) elif len(keys) == 1: textui.print_dashed( 'Deleted %s "%s".' % (self.obj.object_name, keys[0]) ) else: textui.print_dashed('Deleted %s.' % self.obj.object_name) def pre_callback(self, ldap, dn, *keys, **options): return dn def post_callback(self, ldap, dn, *keys, **options): return True class LDAPModMember(LDAPQuery): """ Base class for member manipulation. """ member_param_doc = 'comma-separated list of %s' member_count_out = ('%i member processed.', '%i members processed.') takes_options = ( Flag('raw', cli_name='raw', doc='print entries as they are stored in LDAP', ), ) def get_options(self): for attr in self.obj.attribute_members: 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 yield List('%s?' % name, cli_name='%ss' % name, doc=doc) def get_member_dns(self, **options): dns = {} failed = {} for attr in self.obj.attribute_members: dns[attr] = {} failed[attr] = {} for ldap_obj_name in self.obj.attribute_members[attr]: dns[attr][ldap_obj_name] = [] failed[attr][ldap_obj_name] = [] for name in options.get(to_cli(ldap_obj_name), []): 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: failed[attr][ldap_obj_name].append(name) return (dns, failed) def output_for_cli(self, textui, result, *keys, **options): (completed, failed, entry) = result for (attr, objs) in failed.iteritems(): for ldap_obj_name in objs: if failed[attr][ldap_obj_name]: failed_string = ','.join(failed[attr][ldap_obj_name]) textui.print_plain('%ss failed: %s' % ( to_cli(ldap_obj_name), failed_string ) ) textui.print_name(self.name) self.obj.print_entry(textui, entry, *keys, **options) textui.print_count( completed, self.member_count_out[0], self.member_count_out[1] ) class LDAPAddMember(LDAPModMember): """ Add other LDAP entries to members. """ member_param_doc = 'comma-separated list of %s to add' member_count_out = ('%i member added.', '%i members added.') def execute(self, *keys, **options): ldap = self.obj.backend (member_dns, failed) = self.get_member_dns(**options) dn = self.obj.get_dn(*keys, **options) dn = self.pre_callback(ldap, dn, member_dns, failed, *keys, **options) completed = 0 for (attr, objs) in member_dns.iteritems(): for ldap_obj_name in objs: for m_dn in member_dns[attr][ldap_obj_name]: if not m_dn: continue try: ldap.add_entry_to_group(m_dn, dn, attr) except errors.PublicError: ldap_obj = self.api.Object[ldap_obj_name] failed[attr][ldap_obj_name].append( ldap_obj.get_primary_key_from_dn(m_dn) ) else: completed += 1 (dn, entry_attrs) = ldap.get_entry(dn, self.obj.default_attributes) (completed, dn) = self.post_callback( ldap, completed, failed, dn, entry_attrs, *keys, **options ) self.obj.convert_attribute_members(entry_attrs, *keys, **options) return (completed, failed, (dn, entry_attrs)) def pre_callback(self, ldap, dn, found, not_found, *keys, **options): return dn def post_callback(self, ldap, completed, failed, dn, entry_attrs, *keys, **options): return (completed, failed) class LDAPRemoveMember(LDAPModMember): """ Remove LDAP entries from members. """ member_param_doc = 'comma-separated list of %s to remove' member_count_out = ('%i member removed.', '%i members removed.') def execute(self, *keys, **options): ldap = self.obj.backend (member_dns, failed) = self.get_member_dns(**options) dn = self.obj.get_dn(*keys, **options) dn = self.pre_callback(ldap, dn, members_dns, failed, *keys, **options) completed = 0 for (attr, objs) in member_dns.iteritems(): for ldap_obj_name in objs: for m_dn in member_dns[attr][ldap_obj_name]: if not m_dn: continue try: ldap.remove_entry_from_group(m_dn, dn, attr) except errors.PublicError: ldap_obj = self.api.Object[ldap_obj_name] failed[attr][ldap_obj_name].append( ldap_obj.get_primary_key_from_dn(m_dn) ) else: completed += 1 (dn, entry_attrs) = ldap.get_entry(dn, self.obj.default_attributes) (completed, dn) = self.post_callback( ldap, completed, failed, dn, entry_attrs, *keys, **options ) self.obj.convert_attribute_members(entry_attrs, *keys, **options) return (completed, add_failed, (dn, entry_attrs)) def pre_callback(self, ldap, dn, found, not_found, *keys, **options): return dn def post_callback(self, ldap, completed, failed, dn, entry_attrs, *keys, **options): return (completed, dn) class LDAPSearch(crud.Search): """ Retrieve all LDAP entries matching the given criteria. """ takes_options = ( Flag('raw', cli_name='raw', doc='print entries as they are stored in LDAP', ), Flag('all', cli_name='all', doc='retrieve all attributes', ), ) def get_args(self): if self.obj.parent_key: yield self.obj.parent_key.clone(query=True) yield Str('criteria?') def execute(self, *args, **options): ldap = self.obj.backend term = args[-1] if self.obj.parent_key: base_dn = ldap.make_dn_from_attr( self.obj.parent_key.name, args[0], base_dn ) else: base_dn = self.obj.container_dn search_kw = self.args_options_2_entry(**options) if options['all']: attrs_list = ['*'] else: attrs_list = self.obj.default_attributes + search_kw.keys() search_kw['objectclass'] = self.obj.object_class attr_filter = ldap.make_filter(search_kw, rules=ldap.MATCH_ALL) search_kw = {} for a in self.obj.default_attributes: search_kw[a] = term term_filter = ldap.make_filter(search_kw, exact=False) filter = ldap.combine_filters( (term_filter, attr_filter), rules=ldap.MATCH_ALL ) filter = self.pre_callback( ldap, filter, attrs_list, base_dn, *args, **options ) try: (entries, truncated) = ldap.find_entries( filter, attrs_list, base_dn, scope=ldap.SCOPE_ONELEVEL ) except errors.NotFound: (entries, truncated) = (tuple(), False) self.post_callback(self, ldap, entries, truncated, *args, **options) if options.get('raw', False): for i in xrange(len(entries)): dn = self.obj.get_primary_key_from_dn(entries[i][0]) self.obj.convert_attribute_members( entries[i][1], *keys, **options ) entries[i] = (dn, entries[i][1]) return (entries, truncated) def output_for_cli(self, textui, result, *args, **options): (entries, truncated) = result textui.print_name(self.name) for e in entries: self.obj.print_entry(textui, e, e[0], **options) textui.print_plain('') textui.print_count( len(entries), '%i ' + self.obj.object_name + ' matched.', '%i ' + self.obj.object_name_plural + ' matched.', ) if truncated: textui.print_dashed('These results are truncated.', below=False) textui.print_dashed( 'Please refine your search and try again.', above=False ) elif len(entries) == 0: # nothing was found, return error code 1 return 1 def pre_callback(self, ldap, filter, attrs_list, base_dn, *args, **options): return filter def post_callback(self, ldap, entries, truncated, *args, **options): pass