server/sources/ldapuser.py
changeset 8188 1867e252e487
parent 7884 35d2e2f4e10a
child 8239 c6cdd060212e
equal deleted inserted replaced
8187:981f6e487788 8188:1867e252e487
     1 # copyright 2003-2011 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
     1 # copyright 2003-2012 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
     2 # contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
     2 # contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
     3 #
     3 #
     4 # This file is part of CubicWeb.
     4 # This file is part of CubicWeb.
     5 #
     5 #
     6 # CubicWeb is free software: you can redistribute it and/or modify it under the
     6 # CubicWeb is free software: you can redistribute it and/or modify it under the
    16 # You should have received a copy of the GNU Lesser General Public License along
    16 # You should have received a copy of the GNU Lesser General Public License along
    17 # with CubicWeb.  If not, see <http://www.gnu.org/licenses/>.
    17 # with CubicWeb.  If not, see <http://www.gnu.org/licenses/>.
    18 """cubicweb ldap user source
    18 """cubicweb ldap user source
    19 
    19 
    20 this source is for now limited to a read-only CWUser source
    20 this source is for now limited to a read-only CWUser source
    21 
       
    22 Part of the code is coming form Zope's LDAPUserFolder
       
    23 
       
    24 Copyright (c) 2004 Jens Vagelpohl.
       
    25 All Rights Reserved.
       
    26 
       
    27 This software is subject to the provisions of the Zope Public License,
       
    28 Version 2.1 (ZPL).  A copy of the ZPL should accompany this distribution.
       
    29 THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
       
    30 WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
       
    31 WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
       
    32 FOR A PARTICULAR PURPOSE.
       
    33 """
    21 """
    34 from __future__ import division
    22 from __future__ import division
    35 from base64 import b64decode
    23 from base64 import b64decode
    36 
    24 
    37 import ldap
    25 import ldap
    38 from ldap.ldapobject import ReconnectLDAPObject
    26 from ldap.filter import escape_filter_chars
    39 from ldap.filter import filter_format, escape_filter_chars
       
    40 from ldapurl import LDAPUrl
       
    41 
    27 
    42 from rql.nodes import Relation, VariableRef, Constant, Function
    28 from rql.nodes import Relation, VariableRef, Constant, Function
    43 
    29 
    44 from cubicweb import AuthenticationError, UnknownEid, RepositoryError
    30 from cubicweb import UnknownEid, RepositoryError
       
    31 from cubicweb.server import ldaputils
    45 from cubicweb.server.utils import cartesian_product
    32 from cubicweb.server.utils import cartesian_product
    46 from cubicweb.server.sources import (AbstractSource, TrFunc, GlobTrFunc,
    33 from cubicweb.server.sources import (AbstractSource, TrFunc, GlobTrFunc,
    47                                      ConnectionWrapper, TimedCache)
    34                                      TimedCache)
    48 
    35 
    49 # search scopes
    36 # search scopes
    50 BASE = ldap.SCOPE_BASE
    37 BASE = ldap.SCOPE_BASE
    51 ONELEVEL = ldap.SCOPE_ONELEVEL
    38 ONELEVEL = ldap.SCOPE_ONELEVEL
    52 SUBTREE = ldap.SCOPE_SUBTREE
    39 SUBTREE = ldap.SCOPE_SUBTREE
    56               'ldaps': 636,
    43               'ldaps': 636,
    57               'ldapi': None,
    44               'ldapi': None,
    58               }
    45               }
    59 
    46 
    60 
    47 
    61 class LDAPUserSource(AbstractSource):
    48 class LDAPUserSource(ldaputils.LDAPSourceMixIn, AbstractSource):
    62     """LDAP read-only CWUser source"""
    49     """LDAP read-only CWUser source"""
    63     support_entities = {'CWUser': False}
    50     support_entities = {'CWUser': False}
    64 
    51 
    65     options = (
    52     options = ldaputils.LDAPSourceMixIn.options + (
    66         ('host',
       
    67          {'type' : 'string',
       
    68           'default': 'ldap',
       
    69           'help': 'ldap host. It may contains port information using \
       
    70 <host>:<port> notation.',
       
    71           'group': 'ldap-source', 'level': 1,
       
    72           }),
       
    73         ('protocol',
       
    74          {'type' : 'choice',
       
    75           'default': 'ldap',
       
    76           'choices': ('ldap', 'ldaps', 'ldapi'),
       
    77           'help': 'ldap protocol (allowed values: ldap, ldaps, ldapi)',
       
    78           'group': 'ldap-source', 'level': 1,
       
    79           }),
       
    80         ('auth-mode',
       
    81          {'type' : 'choice',
       
    82           'default': 'simple',
       
    83           'choices': ('simple', 'cram_md5', 'digest_md5', 'gssapi'),
       
    84           'help': 'authentication mode used to authenticate user to the ldap.',
       
    85           'group': 'ldap-source', 'level': 3,
       
    86           }),
       
    87         ('auth-realm',
       
    88          {'type' : 'string',
       
    89           'default': None,
       
    90           'help': 'realm to use when using gssapi/kerberos authentication.',
       
    91           'group': 'ldap-source', 'level': 3,
       
    92           }),
       
    93 
       
    94         ('data-cnx-dn',
       
    95          {'type' : 'string',
       
    96           'default': '',
       
    97           'help': 'user dn to use to open data connection to the ldap (eg used \
       
    98 to respond to rql queries). Leave empty for anonymous bind',
       
    99           'group': 'ldap-source', 'level': 1,
       
   100           }),
       
   101         ('data-cnx-password',
       
   102          {'type' : 'string',
       
   103           'default': '',
       
   104           'help': 'password to use to open data connection to the ldap (eg used to respond to rql queries). Leave empty for anonymous bind.',
       
   105           'group': 'ldap-source', 'level': 1,
       
   106           }),
       
   107 
       
   108         ('user-base-dn',
       
   109          {'type' : 'string',
       
   110           'default': 'ou=People,dc=logilab,dc=fr',
       
   111           'help': 'base DN to lookup for users',
       
   112           'group': 'ldap-source', 'level': 1,
       
   113           }),
       
   114         ('user-scope',
       
   115          {'type' : 'choice',
       
   116           'default': 'ONELEVEL',
       
   117           'choices': ('BASE', 'ONELEVEL', 'SUBTREE'),
       
   118           'help': 'user search scope (valid values: "BASE", "ONELEVEL", "SUBTREE")',
       
   119           'group': 'ldap-source', 'level': 1,
       
   120           }),
       
   121         ('user-classes',
       
   122          {'type' : 'csv',
       
   123           'default': ('top', 'posixAccount'),
       
   124           'help': 'classes of user (with Active Directory, you want to say "user" here)',
       
   125           'group': 'ldap-source', 'level': 1,
       
   126           }),
       
   127         ('user-filter',
       
   128          {'type': 'string',
       
   129           'default': '',
       
   130           'help': 'additional filters to be set in the ldap query to find valid users',
       
   131           'group': 'ldap-source', 'level': 2,
       
   132           }),
       
   133         ('user-login-attr',
       
   134          {'type' : 'string',
       
   135           'default': 'uid',
       
   136           'help': 'attribute used as login on authentication (with Active Directory, you want to use "sAMAccountName" here)',
       
   137           'group': 'ldap-source', 'level': 1,
       
   138           }),
       
   139         ('user-default-group',
       
   140          {'type' : 'csv',
       
   141           'default': ('users',),
       
   142           'help': 'name of a group in which ldap users will be by default. \
       
   143 You can set multiple groups by separating them by a comma.',
       
   144           'group': 'ldap-source', 'level': 1,
       
   145           }),
       
   146         ('user-attrs-map',
       
   147          {'type' : 'named',
       
   148           'default': {'uid': 'login', 'gecos': 'email'},
       
   149           'help': 'map from ldap user attributes to cubicweb attributes (with Active Directory, you want to use sAMAccountName:login,mail:email,givenName:firstname,sn:surname)',
       
   150           'group': 'ldap-source', 'level': 1,
       
   151           }),
       
   152 
    53 
   153         ('synchronization-interval',
    54         ('synchronization-interval',
   154          {'type' : 'time',
    55          {'type' : 'time',
   155           'default': '1d',
    56           'default': '1d',
   156           'help': 'interval between synchronization with the ldap \
    57           'help': 'interval between synchronization with the ldap \
   166 
    67 
   167     )
    68     )
   168 
    69 
   169     def __init__(self, repo, source_config, eid=None):
    70     def __init__(self, repo, source_config, eid=None):
   170         AbstractSource.__init__(self, repo, source_config, eid)
    71         AbstractSource.__init__(self, repo, source_config, eid)
   171         self.update_config(None, self.check_conf_dict(eid, source_config))
    72         self.update_config(None, self.check_conf_dict(eid, source_config,
   172         self._conn = None
    73                                                       fail_if_unknown=False))
       
    74 
       
    75     def _entity_update(self, source_entity):
       
    76         # XXX copy from datafeed source
       
    77         if source_entity.url:
       
    78             self.urls = [url.strip() for url in source_entity.url.splitlines()
       
    79                          if url.strip()]
       
    80         else:
       
    81             self.urls = []
       
    82         # /end XXX
       
    83         ldaputils.LDAPSourceMixIn._entity_update(self, source_entity)
   173 
    84 
   174     def update_config(self, source_entity, typedconfig):
    85     def update_config(self, source_entity, typedconfig):
   175         """update configuration from source entity. `typedconfig` is config
    86         """update configuration from source entity. `typedconfig` is config
   176         properly typed with defaults set
    87         properly typed with defaults set
   177         """
    88         """
   178         self.host = typedconfig['host']
    89         ldaputils.LDAPSourceMixIn.update_config(self, source_entity, typedconfig)
   179         self.protocol = typedconfig['protocol']
       
   180         self.authmode = typedconfig['auth-mode']
       
   181         self._authenticate = getattr(self, '_auth_%s' % self.authmode)
       
   182         self.cnx_dn = typedconfig['data-cnx-dn']
       
   183         self.cnx_pwd = typedconfig['data-cnx-password']
       
   184         self.user_base_dn = str(typedconfig['user-base-dn'])
       
   185         self.user_base_scope = globals()[typedconfig['user-scope']]
       
   186         self.user_login_attr = typedconfig['user-login-attr']
       
   187         self.user_default_groups = typedconfig['user-default-group']
       
   188         self.user_attrs = typedconfig['user-attrs-map']
       
   189         self.user_rev_attrs = {'eid': 'dn'}
       
   190         for ldapattr, cwattr in self.user_attrs.items():
       
   191             self.user_rev_attrs[cwattr] = ldapattr
       
   192         self.base_filters = [filter_format('(%s=%s)', ('objectClass', o))
       
   193                              for o in typedconfig['user-classes']]
       
   194         if typedconfig['user-filter']:
       
   195             self.base_filters.append(typedconfig['user-filter'])
       
   196         self._interval = typedconfig['synchronization-interval']
    90         self._interval = typedconfig['synchronization-interval']
   197         self._cache_ttl = max(71, typedconfig['cache-life-time'])
    91         self._cache_ttl = max(71, typedconfig['cache-life-time'])
   198         self.reset_caches()
    92         self.reset_caches()
   199         self._conn = None
    93         # XXX copy from datafeed source
       
    94         if source_entity is not None:
       
    95             self._entity_update(source_entity)
       
    96         self.config = typedconfig
       
    97         # /end XXX
   200 
    98 
   201     def reset_caches(self):
    99     def reset_caches(self):
   202         """method called during test to reset potential source caches"""
   100         """method called during test to reset potential source caches"""
   203         self._cache = {}
   101         self._cache = {}
   204         self._query_cache = TimedCache(self._cache_ttl)
   102         self._query_cache = TimedCache(self._cache_ttl)
   205 
   103 
   206     def init(self, activated, source_entity):
   104     def init(self, activated, source_entity):
   207         """method called by the repository once ready to handle request"""
   105         """method called by the repository once ready to handle request"""
   208         if activated:
   106         if activated:
   209             self.info('ldap init')
   107             self.info('ldap init')
       
   108             self._entity_update(source_entity)
   210             # set minimum period of 5min 1s (the additional second is to
   109             # set minimum period of 5min 1s (the additional second is to
   211             # minimize resonnance effet)
   110             # minimize resonnance effet)
   212             self.repo.looping_task(max(301, self._interval), self.synchronize)
   111             if self.user_rev_attrs['email']:
       
   112                 self.repo.looping_task(max(301, self._interval), self.synchronize)
   213             self.repo.looping_task(self._cache_ttl // 10,
   113             self.repo.looping_task(self._cache_ttl // 10,
   214                                    self._query_cache.clear_expired)
   114                                    self._query_cache.clear_expired)
   215 
   115 
   216     def synchronize(self):
   116     def synchronize(self):
       
   117         self.pull_data(self.repo.internal_session())
       
   118 
       
   119     def pull_data(self, session, force=False, raise_on_error=False):
   217         """synchronize content known by this repository with content in the
   120         """synchronize content known by this repository with content in the
   218         external repository
   121         external repository
   219         """
   122         """
   220         self.info('synchronizing ldap source %s', self.uri)
   123         self.info('synchronizing ldap source %s', self.uri)
   221         try:
   124         ldap_emailattr = self.user_rev_attrs['email']
   222             ldap_emailattr = self.user_rev_attrs['email']
   125         assert ldap_emailattr
   223         except KeyError:
       
   224             return # no email in ldap, we're done
       
   225         session = self.repo.internal_session()
   126         session = self.repo.internal_session()
   226         execute = session.execute
   127         execute = session.execute
   227         try:
   128         try:
   228             cursor = session.system_sql("SELECT eid, extid FROM entities WHERE "
   129             cursor = session.system_sql("SELECT eid, extid FROM entities WHERE "
   229                                         "source='%s'" % self.uri)
   130                                         "source='%s'" % self.uri)
   265                                 # no email found, create it
   166                                 # no email found, create it
   266                                 _insert_email(session, ldapemailaddr, eid)
   167                                 _insert_email(session, ldapemailaddr, eid)
   267         finally:
   168         finally:
   268             session.commit()
   169             session.commit()
   269             session.close()
   170             session.close()
   270 
       
   271     def get_connection(self):
       
   272         """open and return a connection to the source"""
       
   273         if self._conn is None:
       
   274             try:
       
   275                 self._connect()
       
   276             except Exception:
       
   277                 self.exception('unable to connect to ldap:')
       
   278         return ConnectionWrapper(self._conn)
       
   279 
       
   280     def authenticate(self, session, login, password=None, **kwargs):
       
   281         """return CWUser eid for the given login/password if this account is
       
   282         defined in this source, else raise `AuthenticationError`
       
   283 
       
   284         two queries are needed since passwords are stored crypted, so we have
       
   285         to fetch the salt first
       
   286         """
       
   287         self.info('ldap authenticate %s', login)
       
   288         if not password:
       
   289             # On Windows + ADAM this would have succeeded (!!!)
       
   290             # You get Authenticated as: 'NT AUTHORITY\ANONYMOUS LOGON'.
       
   291             # we really really don't want that
       
   292             raise AuthenticationError()
       
   293         searchfilter = [filter_format('(%s=%s)', (self.user_login_attr, login))]
       
   294         searchfilter.extend(self.base_filters)
       
   295         searchstr = '(&%s)' % ''.join(searchfilter)
       
   296         # first search the user
       
   297         try:
       
   298             user = self._search(session, self.user_base_dn,
       
   299                                 self.user_base_scope, searchstr)[0]
       
   300         except IndexError:
       
   301             # no such user
       
   302             raise AuthenticationError()
       
   303         # check password by establishing a (unused) connection
       
   304         try:
       
   305             self._connect(user, password)
       
   306         except ldap.LDAPError, ex:
       
   307             # Something went wrong, most likely bad credentials
       
   308             self.info('while trying to authenticate %s: %s', user, ex)
       
   309             raise AuthenticationError()
       
   310         except Exception:
       
   311             self.error('while trying to authenticate %s', user, exc_info=True)
       
   312             raise AuthenticationError()
       
   313         eid = self.repo.extid2eid(self, user['dn'], 'CWUser', session)
       
   314         if eid < 0:
       
   315             # user has been moved away from this source
       
   316             raise AuthenticationError()
       
   317         return eid
       
   318 
   171 
   319     def ldap_name(self, var):
   172     def ldap_name(self, var):
   320         if var.stinfo['relations']:
   173         if var.stinfo['relations']:
   321             relname = iter(var.stinfo['relations']).next().r_type
   174             relname = iter(var.stinfo['relations']).next().r_type
   322             return self.user_rev_attrs.get(relname)
   175             return self.user_rev_attrs.get(relname)
   457         for trfunc in globtransforms:
   310         for trfunc in globtransforms:
   458             result = trfunc.apply(result)
   311             result = trfunc.apply(result)
   459         #print '--> ldap result', result
   312         #print '--> ldap result', result
   460         return result
   313         return result
   461 
   314 
   462 
   315     def _process_ldap_item(self, dn, iterator):
   463     def _connect(self, user=None, userpwd=None):
   316         itemdict = super(LDAPUserSource, self)._process_ldap_item(dn, iterator)
   464         if self.protocol == 'ldapi':
   317         self._cache[dn] = itemdict
   465             hostport = self.host
   318         return itemdict
   466         elif not ':' in self.host:
   319 
   467             hostport = '%s:%s' % (self.host, PROTO_PORT[self.protocol])
   320     def _process_no_such_object(self, session, dn):
   468         else:
   321         eid = self.repo.extid2eid(self, dn, 'CWUser', session, insert=False)
   469             hostport = self.host
   322         if eid:
   470         self.info('connecting %s://%s as %s', self.protocol, hostport,
   323             self.warning('deleting ldap user with eid %s and dn %s', eid, dn)
   471                   user and user['dn'] or 'anonymous')
   324             entity = session.entity_from_eid(eid, 'CWUser')
   472         # don't require server certificate when using ldaps (will
   325             self.repo.delete_info(session, entity, self.uri)
   473         # enable self signed certs)
   326             self.reset_caches()
   474         ldap.set_option(ldap.OPT_X_TLS_REQUIRE_CERT, ldap.OPT_X_TLS_NEVER)
       
   475         url = LDAPUrl(urlscheme=self.protocol, hostport=hostport)
       
   476         conn = ReconnectLDAPObject(url.initializeUrl())
       
   477         # Set the protocol version - version 3 is preferred
       
   478         try:
       
   479             conn.set_option(ldap.OPT_PROTOCOL_VERSION, ldap.VERSION3)
       
   480         except ldap.LDAPError: # Invalid protocol version, fall back safely
       
   481             conn.set_option(ldap.OPT_PROTOCOL_VERSION, ldap.VERSION2)
       
   482         # Deny auto-chasing of referrals to be safe, we handle them instead
       
   483         #try:
       
   484         #    connection.set_option(ldap.OPT_REFERRALS, 0)
       
   485         #except ldap.LDAPError: # Cannot set referrals, so do nothing
       
   486         #    pass
       
   487         #conn.set_option(ldap.OPT_NETWORK_TIMEOUT, conn_timeout)
       
   488         #conn.timeout = op_timeout
       
   489         # Now bind with the credentials given. Let exceptions propagate out.
       
   490         if user is None:
       
   491             # no user specified, we want to initialize the 'data' connection,
       
   492             assert self._conn is None
       
   493             self._conn = conn
       
   494             # XXX always use simple bind for data connection
       
   495             if not self.cnx_dn:
       
   496                 conn.simple_bind_s(self.cnx_dn, self.cnx_pwd)
       
   497             else:
       
   498                 self._authenticate(conn, {'dn': self.cnx_dn}, self.cnx_pwd)
       
   499         else:
       
   500             # user specified, we want to check user/password, no need to return
       
   501             # the connection which will be thrown out
       
   502             self._authenticate(conn, user, userpwd)
       
   503         return conn
       
   504 
       
   505     def _auth_simple(self, conn, user, userpwd):
       
   506         conn.simple_bind_s(user['dn'], userpwd)
       
   507 
       
   508     def _auth_cram_md5(self, conn, user, userpwd):
       
   509         from ldap import sasl
       
   510         auth_token = sasl.cram_md5(user['dn'], userpwd)
       
   511         conn.sasl_interactive_bind_s('', auth_token)
       
   512 
       
   513     def _auth_digest_md5(self, conn, user, userpwd):
       
   514         from ldap import sasl
       
   515         auth_token = sasl.digest_md5(user['dn'], userpwd)
       
   516         conn.sasl_interactive_bind_s('', auth_token)
       
   517 
       
   518     def _auth_gssapi(self, conn, user, userpwd):
       
   519         # print XXX not proper sasl/gssapi
       
   520         import kerberos
       
   521         if not kerberos.checkPassword(user[self.user_login_attr], userpwd):
       
   522             raise Exception('BAD login / mdp')
       
   523         #from ldap import sasl
       
   524         #conn.sasl_interactive_bind_s('', sasl.gssapi())
       
   525 
       
   526     def _search(self, session, base, scope,
       
   527                 searchstr='(objectClass=*)', attrs=()):
       
   528         """make an ldap query"""
       
   529         self.debug('ldap search %s %s %s %s %s', self.uri, base, scope,
       
   530                    searchstr, list(attrs))
       
   531         # XXX for now, we do not have connections set support for LDAP, so
       
   532         # this is always self._conn
       
   533         cnx = session.cnxset.connection(self.uri).cnx
       
   534         try:
       
   535             res = cnx.search_s(base, scope, searchstr, attrs)
       
   536         except ldap.PARTIAL_RESULTS:
       
   537             res = cnx.result(all=0)[1]
       
   538         except ldap.NO_SUCH_OBJECT:
       
   539             self.info('ldap NO SUCH OBJECT')
       
   540             eid = self.repo.extid2eid(self, base, 'CWUser', session, insert=False)
       
   541             if eid:
       
   542                 self.warning('deleting ldap user with eid %s and dn %s',
       
   543                              eid, base)
       
   544                 entity = session.entity_from_eid(eid, 'CWUser')
       
   545                 self.repo.delete_info(session, entity, self.uri)
       
   546                 self.reset_caches()
       
   547             return []
       
   548         # except ldap.REFERRAL, e:
       
   549         #     cnx = self.handle_referral(e)
       
   550         #     try:
       
   551         #         res = cnx.search_s(base, scope, searchstr, attrs)
       
   552         #     except ldap.PARTIAL_RESULTS:
       
   553         #         res_type, res = cnx.result(all=0)
       
   554         result = []
       
   555         for rec_dn, rec_dict in res:
       
   556             # When used against Active Directory, "rec_dict" may not be
       
   557             # be a dictionary in some cases (instead, it can be a list)
       
   558             # An example of a useless "res" entry that can be ignored
       
   559             # from AD is
       
   560             # (None, ['ldap://ForestDnsZones.PORTAL.LOCAL/DC=ForestDnsZones,DC=PORTAL,DC=LOCAL'])
       
   561             # This appears to be some sort of internal referral, but
       
   562             # we can't handle it, so we need to skip over it.
       
   563             try:
       
   564                 items =  rec_dict.items()
       
   565             except AttributeError:
       
   566                 # 'items' not found on rec_dict, skip
       
   567                 continue
       
   568             for key, value in items: # XXX syt: huuum ?
       
   569                 if not isinstance(value, str):
       
   570                     try:
       
   571                         for i in range(len(value)):
       
   572                             value[i] = unicode(value[i], 'utf8')
       
   573                     except Exception:
       
   574                         pass
       
   575                 if isinstance(value, list) and len(value) == 1:
       
   576                     rec_dict[key] = value = value[0]
       
   577             rec_dict['dn'] = rec_dn
       
   578             self._cache[rec_dn] = rec_dict
       
   579             result.append(rec_dict)
       
   580         #print '--->', result
       
   581         self.debug('ldap built results %s', len(result))
       
   582         return result
       
   583 
   327 
   584     def before_entity_insertion(self, session, lid, etype, eid, sourceparams):
   328     def before_entity_insertion(self, session, lid, etype, eid, sourceparams):
   585         """called by the repository when an eid has been attributed for an
   329         """called by the repository when an eid has been attributed for an
   586         entity stored here but the entity has not been inserted in the system
   330         entity stored here but the entity has not been inserted in the system
   587         table yet.
   331         table yet.
   602         inserted in the system table.
   346         inserted in the system table.
   603         """
   347         """
   604         self.debug('ldap after entity insertion')
   348         self.debug('ldap after entity insertion')
   605         super(LDAPUserSource, self).after_entity_insertion(
   349         super(LDAPUserSource, self).after_entity_insertion(
   606             session, lid, entity, sourceparams)
   350             session, lid, entity, sourceparams)
   607         dn = lid
       
   608         for group in self.user_default_groups:
   351         for group in self.user_default_groups:
   609             session.execute('SET X in_group G WHERE X eid %(x)s, G name %(group)s',
   352             session.execute('SET X in_group G WHERE X eid %(x)s, G name %(group)s',
   610                             {'x': entity.eid, 'group': group})
   353                             {'x': entity.eid, 'group': group})
   611         # search for existant email first
   354         # search for existant email first
   612         try:
   355         try:
   613             emailaddr = self._cache[dn][self.user_rev_attrs['email']]
   356             # lid = dn
       
   357             emailaddr = self._cache[lid][self.user_rev_attrs['email']]
   614         except KeyError:
   358         except KeyError:
   615             return
   359             return
   616         if isinstance(emailaddr, list):
   360         if isinstance(emailaddr, list):
   617             emailaddr = emailaddr[0] # XXX consider only the first email in the list
   361             emailaddr = emailaddr[0] # XXX consider only the first email in the list
   618         rset = session.execute('EmailAddress X WHERE X address %(addr)s',
   362         rset = session.execute('EmailAddress X WHERE X address %(addr)s',
   629         raise RepositoryError('this source is read only')
   373         raise RepositoryError('this source is read only')
   630 
   374 
   631     def delete_entity(self, session, entity):
   375     def delete_entity(self, session, entity):
   632         """delete an entity from the source"""
   376         """delete an entity from the source"""
   633         raise RepositoryError('this source is read only')
   377         raise RepositoryError('this source is read only')
       
   378 
   634 
   379 
   635 def _insert_email(session, emailaddr, ueid):
   380 def _insert_email(session, emailaddr, ueid):
   636     session.execute('INSERT EmailAddress X: X address %(addr)s, U primary_email X '
   381     session.execute('INSERT EmailAddress X: X address %(addr)s, U primary_email X '
   637                     'WHERE U eid %(x)s', {'addr': emailaddr, 'x': ueid})
   382                     'WHERE U eid %(x)s', {'addr': emailaddr, 'x': ueid})
   638 
   383