--- 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 <http://www.gnu.org/licenses/>.
-"""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