--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/cubicweb/sobjects/ldapparser.py Sat Jan 16 13:48:51 2016 +0100
@@ -0,0 +1,255 @@
+# 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