# HG changeset patch # User Aurelien Campeas # Date 1359120820 -3600 # Node ID d537786e52b899d6487b38a688e8a872308d3652 # Parent 20bd1cdf86ae691979824d572c12588416c7cab3# Parent 48731a0d3df8f85e2fd1c4810f7e07577371fcc8 [merge] backport stable fixes diff -r 20bd1cdf86ae -r d537786e52b8 doc/book/en/admin/ldap.rst --- a/doc/book/en/admin/ldap.rst Fri Jan 25 13:28:23 2013 +0100 +++ b/doc/book/en/admin/ldap.rst Fri Jan 25 14:33:40 2013 +0100 @@ -12,49 +12,55 @@ At cube creation time, one is asked if more sources are wanted. LDAP is one possible option at this time. Of course, it is always possible -to set it up later in the `source` configuration file, which we -discuss there. +to set it up later using the `CWSource` entity type, which we discuss +there. It is possible to add as many LDAP sources as wanted, which translates -in as many [ldapxxx] sections in the `source` configuration file. +in as many `CWSource` entities as needed. The general principle of the LDAP source is, given a proper configuration, to create local users matching the users available in -the directory, deriving local user attributes from directory users +the directory and deriving local user attributes from directory users attributes. Then a periodic task ensures local user information synchronization with the directory. +Users handled by such a source should not be edited directly from +within the application instance itself. Rather, updates should happen +at the LDAP server level. + Credential checks are _always_ done against the LDAP server. -The base functionality for this is in -:file:`cubicweb/server/sources/ldapuser.py`. +.. Note:: -External dependencies ---------------------- - -You'll need the following packages to make CubicWeb interact with your LDAP / -Active Directory server: + There are currently two ldap source types: the older `ldapuser` and + the newer `ldapfeed`. The older will be deprecated anytime soon, as + the newer has now gained all the features of the old and does not + suffer from some of its illnesses. -* python-ldap -* ldaputils if using `ldapfeed` source + The ldapfeed creates real `CWUser` entities, and then + activate/deactivate them depending on their presence/absence in the + corresponding LDAP source. Their attribute and state + (activated/deactivated) are hence managed by the source mechanism; + they should not be altered by other means (as such alterations may + be overridden in some subsequent source synchronisation). -Configurations options ----------------------- -Let us enumerate the options (but please keep in mind that the -authoritative source for these is in the aforementioned python -module), by categories (LDAP server connection, LDAP schema mapping -information, LDAP source internal configuration). +Configurations options of an LDAPfeed source +-------------------------------------------- + +Let us enumerate the options by categories (LDAP server connection, +LDAP schema mapping information). LDAP server connection options: -* `host`, may contain port information using : notation. -* `protocol`, choices are ldap, ldaps, ldapi * `auth-mode`, (choices are simple, cram_md5, digest_md5, gssapi, support for the later being partial as of now) + * `auth-realm`, realm to use when using gssapi/kerberos authentication + * `data-cnx-dn`, user dn to use to open data connection to the ldap (eg used to respond to rql queries) + * `data-cnx-password`, password to use to open data connection to the ldap (eg used to respond to rql queries) @@ -62,21 +68,29 @@ leave data-cnx-dn and data-cnx-password empty. This is, however, quite unlikely in practice. -LDAP schema mapping: +LDAP schema mapping options: * `user-base-dn`, base DN to lookup for users -* `user-scope`, user search scope -* `user-classes`, classes of user -* `user-attrs-map`, map from ldap user attributes to cubicweb attributes -* `user-login-attr`, attribute used as login on authentication + +* `user-scope`, user search scope (valid values: "BASE", "ONELEVEL", + "SUBTREE") -LDAP source internal configuration: +* `user-classes`, classes of user (with Active Directory, you want to + say "user" here) + +* `user-filter`, additional filters to be set in the ldap query to + find valid users + +* `user-login-attr`, attribute used as login on authentication (with + Active Directory, you want to use "sAMAccountName" here) * `user-default-group`, name of a group in which ldap users will be by default. You can set multiple groups by separating them by a comma -* `synchronization-interval`, interval between synchronization with the - ldap directory in seconds (default to once a day) -* `cache-life-time`, life time of query cache in minutes (default to two hours). + +* `user-attrs-map`, map from ldap user attributes to cubicweb + attributes (with Active Directory, you want to use + sAMAccountName:login,mail:email,givenName:firstname,sn:surname) + Other notes ----------- @@ -87,14 +101,13 @@ authenticated but their status will not change (e.g. they will not be deactivated) -* Changing the name of the ldap server in your script is fine, changing the base - DN isn't since it's used to identify already known users from others +* The user-base-dn is a key that helps cubicweb map CWUsers to LDAP + users: beware updating it * When a user is removed from an LDAP source, it is deactivated in the CubicWeb instance; when a deactivated user comes back in the LDAP source, it (automatically) is activated again - * You can use the :class:`CWSourceHostConfig` to have variants for a source configuration according to the host the instance is running on. To do so go on the source's view from the sources management view. diff -r 20bd1cdf86ae -r d537786e52b8 server/ldaputils.py --- a/server/ldaputils.py Fri Jan 25 13:28:23 2013 +0100 +++ b/server/ldaputils.py Fri Jan 25 14:33:40 2013 +0100 @@ -38,6 +38,7 @@ from ldapurl import LDAPUrl from cubicweb import ValidationError, AuthenticationError, Binary +from cubicweb.server import utils from cubicweb.server.sources import ConnectionWrapper _ = unicode @@ -336,7 +337,11 @@ itemdict = {'dn': dn} for key, value in iterator: if self.user_attrs.get(key) == 'upassword': # XXx better password detection - itemdict[key] = Binary(value[0].encode('utf-8')) + value = value[0].encode('utf-8') + # we only support ldap_salted_sha1 for ldap sources, see: server/utils.py + if not value.startswith('{SSHA}'): + value = utils.crypt_password(value) + itemdict[key] = Binary(value) else: for i, val in enumerate(value): value[i] = unicode(val, 'utf-8', 'replace') diff -r 20bd1cdf86ae -r d537786e52b8 server/test/data/ldap_test.ldif --- a/server/test/data/ldap_test.ldif Fri Jan 25 13:28:23 2013 +0100 +++ b/server/test/data/ldap_test.ldif Fri Jan 25 14:33:40 2013 +0100 @@ -31,7 +31,7 @@ gecos: Sylvain Thenault mail: sylvain.thenault@logilab.fr mail: syt@logilab.fr -userPassword: {SSHA}v/8xJQP3uoaTBZz1T7Y0B3qOxRN1cj7D +userPassword: syt dn: uid=adim,ou=People,dc=cubicweb,dc=test loginShell: /bin/bash @@ -53,5 +53,5 @@ gecos: Adrien Di Mascio mail: adim@logilab.fr mail: adrien.dimascio@logilab.fr -userPassword: {SSHA}cPQOWqkkLDlfWFwxcl1m8V2JdySQBHfS +userPassword: adim diff -r 20bd1cdf86ae -r d537786e52b8 server/test/data/slapd.conf.in --- a/server/test/data/slapd.conf.in Fri Jan 25 13:28:23 2013 +0100 +++ b/server/test/data/slapd.conf.in Fri Jan 25 14:33:40 2013 +0100 @@ -49,5 +49,5 @@ rootdn "cn=admin,dc=cubicweb,dc=test" rootpw "cw" # Where the database file are physically stored for database #1 -directory "%(apphome)s/ldapdb" +directory "%(testdir)s" diff -r 20bd1cdf86ae -r d537786e52b8 server/test/unittest_ldapuser.py --- a/server/test/unittest_ldapuser.py Fri Jan 25 13:28:23 2013 +0100 +++ b/server/test/unittest_ldapuser.py Fri Jan 25 14:33:40 2013 +0100 @@ -23,6 +23,7 @@ import time from os.path import join, exists import subprocess +import tempfile from logilab.common.testlib import TestCase, unittest_main, mock_object, Tags @@ -39,16 +40,13 @@ def create_slapd_configuration(cls): global URL + slapddir = tempfile.mkdtemp('cw-unittest-ldap') config = cls.config - basedir = join(config.apphome, "ldapdb") slapdconf = join(config.apphome, "slapd.conf") confin = file(join(config.apphome, "slapd.conf.in")).read() confstream = file(slapdconf, 'w') - confstream.write(confin % {'apphome': config.apphome}) + confstream.write(confin % {'apphome': config.apphome, 'testdir': slapddir}) confstream.close() - if exists(basedir): - shutil.rmtree(basedir) - os.makedirs(basedir) # fill ldap server with some data ldiffile = join(config.apphome, "ldap_test.ldif") config.info('Initing ldap database') @@ -69,6 +67,7 @@ raise EnvironmentError('Cannot start slapd with cmdline="%s" (from directory "%s")' % (" ".join(cmdline), os.getcwd())) URL = u'ldap://%s' % host + return slapddir def terminate_slapd(cls): config = cls.config @@ -89,11 +88,32 @@ def setUpClass(cls): from cubicweb.cwctl import init_cmdline_log_threshold init_cmdline_log_threshold(cls.config, cls.loglevel) - create_slapd_configuration(cls) + cls._tmpdir = create_slapd_configuration(cls) @classmethod def tearDownClass(cls): terminate_slapd(cls) + try: + shutil.rmtree(cls._tmpdir) + except: + pass + +class CheckWrongGroup(LDAPTestBase): + + def test_wrong_group(self): + self.session.create_entity('CWSource', name=u'ldapuser', type=u'ldapfeed', parser=u'ldapfeed', + url=URL, config=CONFIG) + self.commit() + with self.session.repo.internal_session(safe=True) as session: + source = self.session.execute('CWSource S WHERE S type="ldapfeed"').get_entity(0,0) + config = source.repo_source.check_config(source) + # inject a bogus group here, along with at least a valid one + config['user-default-group'] = ('thisgroupdoesnotexists','users') + source.repo_source.update_config(source, config) + session.commit(free_cnxset=False) + # here we emitted an error log entry + stats = source.repo_source.pull_data(session, force=True, raise_on_error=True) + session.commit() def setUp(self): super(LDAPTestBase, self).setUp() @@ -244,10 +264,6 @@ source.pull_data(self.session) rset = self.sexecute('CWUser X WHERE X login %(login)s', {'login': 'syt'}) self.assertEqual(len(rset), 1) - # test some password has been set - cu = self.session.system_sql('SELECT cw_upassword FROM cw_CWUser WHERE cw_eid=%s' % rset[0][0]) - value = str(cu.fetchall()[0][0]) - self.assertEqual(value, '{SSHA}v/8xJQP3uoaTBZz1T7Y0B3qOxRN1cj7D') self.assertTrue(self.repo.system_source.authenticate( self.session, 'syt', password='syt')) diff -r 20bd1cdf86ae -r d537786e52b8 sobjects/ldapparser.py --- a/sobjects/ldapparser.py Fri Jan 25 13:28:23 2013 +0100 +++ b/sobjects/ldapparser.py Fri Jan 25 14:33:40 2013 +0100 @@ -130,8 +130,10 @@ super(DataFeedLDAPAdapter, self).after_entity_copy(entity, sourceparams) if entity.__regid__ == 'EmailAddress': return - groups = [self._get_group(n) for n in self.source.user_default_groups] - entity.cw_set(in_group=groups) + groups = filter(None, [self._get_group(name) + for name in self.source.user_default_groups]) + if groups: + entity.cw_set(in_group=groups) self._process_email(entity, sourceparams) def is_deleted(self, extidplus, etype, eid): @@ -172,5 +174,11 @@ @cached def _get_group(self, name): - return self._cw.execute('Any X WHERE X is CWGroup, X name %(name)s', - {'name': name}).get_entity(0, 0) + try: + return self._cw.execute('Any X WHERE X is CWGroup, X name %(name)s', + {'name': name}).get_entity(0, 0) + except IndexError: + self.error('group %r referenced by source configuration %r does not exist' + % (name, self.source.uri)) + return None +