# 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 sourceunlike ldapuser source, this source is copy based and will import ldap content(beside passwords for authentication) into the system source."""fromsix.movesimportmap,filterfromlogilab.common.decoratorsimportcached,cachedpropertyfromlogilab.common.shellutilsimportgenerate_passwordfromcubicwebimportBinary,ConfigurationErrorfromcubicweb.server.utilsimportcrypt_passwordfromcubicweb.server.sourcesimportdatafeedfromcubicweb.dataimportimportstores,importerclassUserMetaGenerator(stores.MetaGenerator):"""Specific metadata generator, used to see newly created user into their initial state. """@cacheddefbase_etype_dicts(self,entity):entity,rels=super(UserMetaGenerator,self).base_etype_dicts(entity)ifentity.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.eidreturnentity,relsclassDataFeedLDAPAdapter(datafeed.DataFeedParser):__regid__='ldapfeed'# attributes that may appears in source user_attrs dict which are not# attributes of the cw usernon_attribute_keys=set(('email','eid','member','modification_date'))@cachedpropertydefsearchfilterstr(self):""" ldap search string, including user-filter """return'(&%s)'%''.join(self.source.base_filters)@cachedpropertydefsearchgroupfilterstr(self):""" ldap search string, including user-filter """return'(&%s)'%''.join(self.source.group_base_filters)@cachedpropertydefuser_source_entities_by_extid(self):source=self.sourceifsource.user_base_dn.strip():attrs=list(map(str,source.user_attrs.keys()))returndict((userdict['dn'].encode('ascii'),userdict)foruserdictinsource._search(self._cw,source.user_base_dn,source.user_base_scope,self.searchfilterstr,attrs))return{}@cachedpropertydefgroup_source_entities_by_extid(self):source=self.sourceifsource.group_base_dn.strip():attrs=list(map(str,['modifyTimestamp']+list(source.group_attrs.keys())))returndict((groupdict['dn'].encode('ascii'),groupdict)forgroupdictinsource._search(self._cw,source.group_base_dn,source.group_base_scope,self.searchgroupfilterstr,attrs))return{}defprocess(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)fornameinself.source.user_default_groups:geid=self._get_group(name)eeimporter.extid2eid[geid]=geidentities=self.extentities_generator()set_cwuri=importer.use_extid_as_cwuri(eeimporter.extid2eid)eeimporter.import_entities(set_cwuri(entities))self.stats['created']=eeimporter.createdself.stats['updated']=eeimporter.updated# handle in_group relationforgroup,membersinself._group_members.items():self._cw.execute('DELETE U in_group G WHERE G name %(g)s',{'g':group})ifmembers:members=["'%s'"%eforeinmembers]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 activatedforeidineeimporter.updated:entity=self._cw.entity_from_eid(eid)ifentity.cw_etype=='CWUser':self.ensure_activated(entity)# manually set primary email if necessary, it's not handled automatically since hooks are# deactivatedself._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})defbuild_importer(self,raise_on_error):"""Instantiate and configure an importer"""etypes=('CWUser','EmailAddress','CWGroup')extid2eid=dict((self.source.decode_extid(x),y)forx,yinself._cw.system_sql('select extid, eid from entities where asource = %(s)s',{'s':self.source.uri}))existing_relations={}forrtypein('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)forxinrset)returnimporter.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)defbuild_store(self):"""Instantiate and configure a store"""metagenerator=UserMetaGenerator(self._cw,source=self.source)returnstores.NoHookRQLObjectStore(self._cw,metagenerator)defextentities_generator(self):self.debug('processing ldapfeed source %s%s',self.source,self.searchgroupfilterstr)# generate users and email addressesforuserdictinself.user_source_entities_by_extid.values():attrs=self.ldap2cwattrs(userdict,'CWUser')pwd=attrs.get('upassword')ifnotpwd:# 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])forextemailinself._process_email(extuser,userdict):yieldextemailgroups=list(filter(None,[self._get_group(name)fornameinself.source.user_default_groups]))ifgroups:extuser.values['in_group']=groupsyieldextuser# generate groupsforgroupdictinself.group_source_entities_by_extid.values():attrs=self.ldap2cwattrs(groupdict,'CWGroup')extgroup=importer.ExtEntity('CWGroup',groupdict['dn'].encode('ascii'),attrs)yieldextgroup# record group membership for later insertionmembers=groupdict.get(self.source.group_rev_attrs['member'],())self._group_members[attrs['name']]=membersdef_process_email(self,extuser,userdict):try:emailaddrs=userdict.pop(self.source.user_rev_attrs['email'])exceptKeyError:return# no email for that user, nothing to doifnotisinstance(emailaddrs,list):emailaddrs=[emailaddrs]foremailaddrinemailaddrs:# search for existing email first, may be coming from another sourcerset=self._cw.execute('EmailAddress X WHERE X address %(addr)s',{'addr':emailaddr})emailextid=(userdict['dn']+'@@'+emailaddr).encode('ascii')ifnotrset:# not found, create it. first forge an external idextuser.values.setdefault('use_email',[]).append(emailextid)yieldimporter.ExtEntity('EmailAddress',emailextid,dict(address=[emailaddr]))elifself.sourceuris:# pop from sourceuris anyway, else email may be removed by the# source once import is finishedself.sourceuris.pop(emailextid,None)# XXX else check use_email relation?defhandle_deletion(self,config,cnx,myuris):ifconfig['delete-entities']:super(DataFeedLDAPAdapter,self).handle_deletion(config,cnx,myuris)returnifmyuris:forextid,(eid,etype)inmyuris.items():ifetype!='CWUser'ornotself.is_deleted(extid,etype,eid):continueself.info('deactivate user %s',eid)wf=cnx.entity_from_eid(eid).cw_adapt_to('IWorkflowable')wf.fire_transition_if_possible('deactivate')cnx.commit()defensure_activated(self,entity):ifentity.cw_etype=='CWUser':wf=entity.cw_adapt_to('IWorkflowable')ifwf.state=='deactivated':wf.fire_transition('activate')self.info('user %s reactivated',entity.login)defldap2cwattrs(self,sdict,etype):"""Transform dictionary of LDAP attributes to CW. etype must be CWUser or CWGroup """assertetypein('CWUser','CWGroup'),etypetdict={}ifetype=='CWUser':items=self.source.user_attrs.items()elifetype=='CWGroup':items=self.source.group_attrs.items()forsattr,tattrinitems:iftattrnotinself.non_attribute_keys:try:value=sdict[sattr]exceptKeyError:raiseConfigurationError('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()))ifnotisinstance(value,list):value=[value]tdict[tattr]=valuereturntdictdefis_deleted(self,extidplus,etype,eid):try:extid=extidplus.rsplit(b'@@',1)[0]exceptValueError:# for some reason extids here tend to come in both forms, e.g:# dn, dn@@Babarextid=extidplusreturnextidnotinself.user_source_entities_by_extid@cacheddef_get_group(self,name):try:returnself._cw.execute('Any X WHERE X is CWGroup, X name %(name)s',{'name':name})[0][0]exceptIndexError:self.error('group %r referenced by source configuration %r does not exist',name,self.source.uri)returnNone