diff -r 058bb3dc685f -r 0b59724cb3f2 sobjects/ldapparser.py --- a/sobjects/ldapparser.py Mon Jan 04 18:40:30 2016 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,255 +0,0 @@ -# copyright 2011-2015 LOGILAB S.A. (Paris, FRANCE), all rights reserved. -# contact http://www.logilab.fr/ -- mailto:contact@logilab.fr -# -# This file is part of CubicWeb. -# -# CubicWeb is free software: you can redistribute it and/or modify it under the -# terms of the GNU Lesser General Public License as published by the Free -# Software Foundation, either version 2.1 of the License, or (at your option) -# any later version. -# -# CubicWeb 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 Lesser General Public License for more -# details. -# -# You should have received a copy of the GNU Lesser General Public License along -# with CubicWeb. If not, see . -"""cubicweb ldap feed source - -unlike ldapuser source, this source is copy based and will import ldap content -(beside passwords for authentication) into the system source. -""" -from six.moves import map, filter - -from logilab.common.decorators import cached, cachedproperty -from logilab.common.shellutils import generate_password - -from cubicweb import Binary, ConfigurationError -from cubicweb.server.utils import crypt_password -from cubicweb.server.sources import datafeed -from cubicweb.dataimport import stores, importer - - -class UserMetaGenerator(stores.MetaGenerator): - """Specific metadata generator, used to see newly created user into their initial state. - """ - @cached - def base_etype_dicts(self, entity): - entity, rels = super(UserMetaGenerator, self).base_etype_dicts(entity) - if entity.cw_etype == 'CWUser': - wf_state = self._cnx.execute('Any S WHERE ET default_workflow WF, ET name %(etype)s, ' - 'WF initial_state S', {'etype': entity.cw_etype}).one() - rels['in_state'] = wf_state.eid - return entity, rels - - -class DataFeedLDAPAdapter(datafeed.DataFeedParser): - __regid__ = 'ldapfeed' - # attributes that may appears in source user_attrs dict which are not - # attributes of the cw user - non_attribute_keys = set(('email', 'eid', 'member', 'modification_date')) - - @cachedproperty - def searchfilterstr(self): - """ ldap search string, including user-filter """ - return '(&%s)' % ''.join(self.source.base_filters) - - @cachedproperty - def searchgroupfilterstr(self): - """ ldap search string, including user-filter """ - return '(&%s)' % ''.join(self.source.group_base_filters) - - @cachedproperty - def user_source_entities_by_extid(self): - source = self.source - if source.user_base_dn.strip(): - attrs = list(map(str, source.user_attrs.keys())) - return dict((userdict['dn'].encode('ascii'), userdict) - for userdict in source._search(self._cw, - source.user_base_dn, - source.user_base_scope, - self.searchfilterstr, - attrs)) - return {} - - @cachedproperty - def group_source_entities_by_extid(self): - source = self.source - if source.group_base_dn.strip(): - attrs = list(map(str, ['modifyTimestamp'] + list(source.group_attrs.keys()))) - return dict((groupdict['dn'].encode('ascii'), groupdict) - for groupdict in source._search(self._cw, - source.group_base_dn, - source.group_base_scope, - self.searchgroupfilterstr, - attrs)) - return {} - - def process(self, url, raise_on_error=False): - """IDataFeedParser main entry point""" - self.debug('processing ldapfeed source %s %s', self.source, self.searchfilterstr) - self._group_members = {} - eeimporter = self.build_importer(raise_on_error) - for name in self.source.user_default_groups: - geid = self._get_group(name) - eeimporter.extid2eid[geid] = geid - entities = self.extentities_generator() - set_cwuri = importer.use_extid_as_cwuri(eeimporter.extid2eid) - eeimporter.import_entities(set_cwuri(entities)) - self.stats['created'] = eeimporter.created - self.stats['updated'] = eeimporter.updated - # handle in_group relation - for group, members in self._group_members.items(): - self._cw.execute('DELETE U in_group G WHERE G name %(g)s', {'g': group}) - if members: - members = ["'%s'" % e for e in members] - rql = 'SET U in_group G WHERE G name %%(g)s, U login IN (%s)' % ','.join(members) - self._cw.execute(rql, {'g': group}) - # ensure updated users are activated - for eid in eeimporter.updated: - entity = self._cw.entity_from_eid(eid) - if entity.cw_etype == 'CWUser': - self.ensure_activated(entity) - # manually set primary email if necessary, it's not handled automatically since hooks are - # deactivated - self._cw.execute('SET X primary_email E WHERE NOT X primary_email E, X use_email E, ' - 'X cw_source S, S eid %(s)s, X in_state ST, TS name "activated"', - {'s': self.source.eid}) - - def build_importer(self, raise_on_error): - """Instantiate and configure an importer""" - etypes = ('CWUser', 'EmailAddress', 'CWGroup') - extid2eid = dict((self.source.decode_extid(x), y) for x, y in - self._cw.system_sql('select extid, eid from entities where asource = %(s)s', {'s': self.source.uri})) - existing_relations = {} - for rtype in ('in_group', 'use_email', 'owned_by'): - rql = 'Any S,O WHERE S {} O, S cw_source SO, SO eid %(s)s'.format(rtype) - rset = self._cw.execute(rql, {'s': self.source.eid}) - existing_relations[rtype] = set(tuple(x) for x in rset) - return importer.ExtEntitiesImporter(self._cw.vreg.schema, self.build_store(), - extid2eid=extid2eid, - existing_relations=existing_relations, - etypes_order_hint=etypes, - import_log=self.import_log, - raise_on_error=raise_on_error) - - def build_store(self): - """Instantiate and configure a store""" - metagenerator = UserMetaGenerator(self._cw, source=self.source) - return stores.NoHookRQLObjectStore(self._cw, metagenerator) - - def extentities_generator(self): - self.debug('processing ldapfeed source %s %s', self.source, self.searchgroupfilterstr) - # generate users and email addresses - for userdict in self.user_source_entities_by_extid.values(): - attrs = self.ldap2cwattrs(userdict, 'CWUser') - pwd = attrs.get('upassword') - if not pwd: - # generate a dumb password if not fetched from ldap (see - # userPassword) - pwd = crypt_password(generate_password()) - attrs['upassword'] = set([Binary(pwd)]) - extuser = importer.ExtEntity('CWUser', userdict['dn'].encode('ascii'), attrs) - extuser.values['owned_by'] = set([extuser.extid]) - for extemail in self._process_email(extuser, userdict): - yield extemail - groups = list(filter(None, [self._get_group(name) - for name in self.source.user_default_groups])) - if groups: - extuser.values['in_group'] = groups - yield extuser - # generate groups - for groupdict in self.group_source_entities_by_extid.values(): - attrs = self.ldap2cwattrs(groupdict, 'CWGroup') - extgroup = importer.ExtEntity('CWGroup', groupdict['dn'].encode('ascii'), attrs) - yield extgroup - # record group membership for later insertion - members = groupdict.get(self.source.group_rev_attrs['member'], ()) - self._group_members[attrs['name']] = members - - def _process_email(self, extuser, userdict): - try: - emailaddrs = userdict.pop(self.source.user_rev_attrs['email']) - except KeyError: - return # no email for that user, nothing to do - if not isinstance(emailaddrs, list): - emailaddrs = [emailaddrs] - for emailaddr in emailaddrs: - # search for existing email first, may be coming from another source - rset = self._cw.execute('EmailAddress X WHERE X address %(addr)s', - {'addr': emailaddr}) - emailextid = (userdict['dn'] + '@@' + emailaddr).encode('ascii') - if not rset: - # not found, create it. first forge an external id - extuser.values.setdefault('use_email', []).append(emailextid) - yield importer.ExtEntity('EmailAddress', emailextid, dict(address=[emailaddr])) - elif self.sourceuris: - # pop from sourceuris anyway, else email may be removed by the - # source once import is finished - self.sourceuris.pop(emailextid, None) - # XXX else check use_email relation? - - def handle_deletion(self, config, cnx, myuris): - if config['delete-entities']: - super(DataFeedLDAPAdapter, self).handle_deletion(config, cnx, myuris) - return - if myuris: - for extid, (eid, etype) in myuris.items(): - if etype != 'CWUser' or not self.is_deleted(extid, etype, eid): - continue - self.info('deactivate user %s', eid) - wf = cnx.entity_from_eid(eid).cw_adapt_to('IWorkflowable') - wf.fire_transition_if_possible('deactivate') - cnx.commit() - - def ensure_activated(self, entity): - if entity.cw_etype == 'CWUser': - wf = entity.cw_adapt_to('IWorkflowable') - if wf.state == 'deactivated': - wf.fire_transition('activate') - self.info('user %s reactivated', entity.login) - - def ldap2cwattrs(self, sdict, etype): - """Transform dictionary of LDAP attributes to CW. - - etype must be CWUser or CWGroup - """ - assert etype in ('CWUser', 'CWGroup'), etype - tdict = {} - if etype == 'CWUser': - items = self.source.user_attrs.items() - elif etype == 'CWGroup': - items = self.source.group_attrs.items() - for sattr, tattr in items: - if tattr not in self.non_attribute_keys: - try: - value = sdict[sattr] - except KeyError: - raise ConfigurationError( - 'source attribute %s has not been found in the source, ' - 'please check the %s-attrs-map field and the permissions of ' - 'the LDAP binding user' % (sattr, etype[2:].lower())) - if not isinstance(value, list): - value = [value] - tdict[tattr] = value - return tdict - - def is_deleted(self, extidplus, etype, eid): - try: - extid = extidplus.rsplit(b'@@', 1)[0] - except ValueError: - # for some reason extids here tend to come in both forms, e.g: - # dn, dn@@Babar - extid = extidplus - return extid not in self.user_source_entities_by_extid - - @cached - def _get_group(self, name): - try: - return self._cw.execute('Any X WHERE X is CWGroup, X name %(name)s', - {'name': name})[0][0] - except IndexError: - self.error('group %r referenced by source configuration %r does not exist', - name, self.source.uri) - return None