diff --git a/CHANGELOG b/CHANGELOG index 8f03a85e0b..53265610c8 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,3 +1,5 @@ +* Use trytond configuration for connection + Version 3.2.0 - 2014-04-21 * Bug fixes (see mercurial logs for details) diff --git a/__init__.py b/__init__.py index 2bc2b00bf0..c79ab3e3b2 100644 --- a/__init__.py +++ b/__init__.py @@ -2,12 +2,10 @@ #this repository contains the full copyright notices and license terms. from trytond.pool import Pool -from .connection import * from .res import * def register(): Pool.register( - Connection, User, module='ldap_authentication', type_='model') diff --git a/connection.py b/connection.py deleted file mode 100644 index 812013250c..0000000000 --- a/connection.py +++ /dev/null @@ -1,36 +0,0 @@ -#This file is part of Tryton. The COPYRIGHT file at the top level of -#this repository contains the full copyright notices and license terms. -from trytond.model import fields -from trytond.pool import PoolMeta - -__all__ = ['Connection'] -__metaclass__ = PoolMeta - - -class Connection: - __name__ = 'ldap.connection' - - auth_uid = fields.Char('UID', help='UID Attribute for authentication', - required=True) - auth_base_dn = fields.Char('Base DN', help='Base DN for authentication', - required=True) - auth_require_filter = fields.Char('Require Filter') - auth_scope = fields.Selection([ - ('base', 'Base'), - ('onelevel', 'OneLevel'), - ('subtree', 'Subtree'), - ], 'Scope', required=True) - auth_create_user = fields.Boolean('Create User', - help='Create user if not in database') - - @staticmethod - def default_auth_uid(): - return 'uid' - - @staticmethod - def default_auth_scope(): - return 'base' - - @staticmethod - def default_auth_create_user(): - return False diff --git a/connection.xml b/connection.xml deleted file mode 100644 index 1b5bb22668..0000000000 --- a/connection.xml +++ /dev/null @@ -1,13 +0,0 @@ - - - - - - ldap.connection - form - - connection_form - - - diff --git a/doc/index.rst b/doc/index.rst new file mode 100644 index 0000000000..f5c48a74be --- /dev/null +++ b/doc/index.rst @@ -0,0 +1,37 @@ +LDAP Authentication Module +########################## + +The LDAP authentication module allows to authenticate users via a LDAP server. + +The configuration of the LDAP connection is set in the `ldap_authentication` +section. + +Configuration +************* + +uri +--- + +The LDAP URL to use to connect to the server following the RFC-2255_. + +bind_pass +--------- + +The LDAP password used to bind if needed. + +active_directory +---------------- + +A boolean to set if the LDAP server is an Active Directory. + +uid +--- + +The UID Attribute for authentication (default is `uid`). + +create_user +----------- + +A boolean to create user if not in the database. + +.. _RFC-2255: http://tools.ietf.org/html/rfc2255 diff --git a/res.py b/res.py index 7bca8866c4..30049d9833 100644 --- a/res.py +++ b/res.py @@ -1,15 +1,56 @@ #This file is part of Tryton. The COPYRIGHT file at the top level of #this repository contains the full copyright notices and license terms. import logging +import urlparse import ldap from trytond.transaction import Transaction from trytond.pool import Pool, PoolMeta +from trytond.config import config, parse_uri __all__ = ['User'] __metaclass__ = PoolMeta logger = logging.getLogger(__name__) +section = 'ldap_authentication' + + +def parse_ldap_url(uri): + unquote = urlparse.unquote + uri = parse_uri(uri) + dn = unquote(uri.path)[1:] + attributes, scope, filter_, extensions = ( + uri.query.split('?') + [''] * 4)[:4] + if not scope: + scope = 'base' + extensions = urlparse.parse_qs(extensions) + return (uri, dn, unquote(attributes), unquote(scope), unquote(filter_), + extensions) + + +def ldap_connection(): + uri = config.get(section, 'uri') + if not uri: + return + uri, _, _, _, _, extensions = parse_ldap_url(uri) + if uri.scheme.startswith('ldaps'): + scheme, port = 'ldaps', 636 + else: + scheme, port = 'ldap', 389 + conn = ldap.initialize('%s://%s:%s/' % ( + scheme, uri.hostname, uri.port or port)) + if config.getboolean(section, 'active_directory', 'False'): + conn.set_option(ldap.OPT_REFERRALS, 0) + if 'tls' in uri.scheme: + conn.start_tls_s() + + bindname = extensions.get('bindname') + if not bindname: + bindname = extensions.get('!bindname') + if bindname: + # XXX find better way to get the password + conn.simple_bind_s(bindname, config.get(section, 'bind_pass')) + return conn class User: @@ -24,26 +65,27 @@ class User: }) @staticmethod - def ldap_search_user(login, con, connection, attrs=None): + def ldap_search_user(login, con, attrs=None): ''' Return the result of a ldap search for the login using the ldap connection con based on connection. The attributes values defined in attrs will be return. ''' + _, dn, _, scope, filter_, _ = parse_ldap_url( + config.get(section, 'uri')) scope = { 'base': ldap.SCOPE_BASE, 'onelevel': ldap.SCOPE_ONELEVEL, 'subtree': ldap.SCOPE_SUBTREE, - }.get(connection.auth_scope) - if connection.auth_require_filter: - filter = '(&(%s=%s)%s)' % (connection.auth_uid, login, - connection.auth_require_filter) + }.get(scope) + uid = config.get(section, 'uid', 'uid') + if filter_: + filter_ = '(&(%s=%s)%s)' % (uid, login, filter_) else: - filter = '(%s=%s)' % (connection.auth_uid, login) + filter_ = '(%s=%s)' % (uid, login) - result = con.search_s(connection.auth_base_dn or '', scope, - filter, attrs) - if connection.active_directory: + result = con.search_s(dn, scope, filter_, attrs) + if config.get(section, 'active_directory'): result = [x for x in result if x[0]] if result and len(result) > 1: logger.info('ldap_search_user found more than 1 user') @@ -51,20 +93,11 @@ class User: @classmethod def _check_passwd_ldap_user(cls, logins): - Connection = Pool().get('ldap.connection') - connection, = Connection.search([], limit=1) find = False try: - con = ldap.initialize(connection.uri) - if connection.active_directory: - con.set_option(ldap.OPT_REFERRALS, 0) - if connection.secure == 'tls': - con.start_tls_s() - if connection.bind_dn: - con.simple_bind_s(connection.bind_dn, connection.bind_pass) + con = ldap_connection() for login in logins: - if cls.ldap_search_user(login, - con, connection, attrs=[]): + if cls.ldap_search_user(login, con, attrs=[]): find = True break except ldap.LDAPError, e: @@ -94,20 +127,12 @@ class User: @classmethod def set_preferences(cls, values, old_password=False): - Connection = Pool().get('ldap.connection') if 'password' in values: - connection, = Connection.search([], limit=1) try: - con = ldap.initialize(connection.uri) - if connection.active_directory: - con.set_option(ldap.OPT_REFERRALS, 0) - if connection.secure == 'tls': - con.start_tls_s() - if connection.bind_dn: - con.simple_bind_s(connection.bind_dn, connection.bind_pass) + con = ldap_connection() user = cls(Transaction().user) - users = cls.ldap_search_user(user.login, con, - connection, attrs=[str(connection.auth_uid)]) + uid = config.get(section, 'uid', 'uid') + users = cls.ldap_search_user(user.login, con, attrs=[uid]) if users and len(users) == 1: [(dn, attrs)] = users if con.simple_bind_s(dn, old_password): @@ -123,20 +148,11 @@ class User: @classmethod def get_login(cls, login, password): pool = Pool() - Connection = pool.get('ldap.connection') LoginAttempt = pool.get('res.user.login.attempt') - connection, = Connection.search([], limit=1) try: - con = ldap.initialize(connection.uri or '') - if connection.active_directory: - con.set_option(ldap.OPT_REFERRALS, 0) - if connection.secure == 'tls': - con.start_tls_s() - if connection.bind_dn: - con.simple_bind_s(connection.bind_dn or '', - connection.bind_pass or '') - users = cls.ldap_search_user(login, con, connection, - attrs=[str(connection.auth_uid)]) + con = ldap_connection() + uid = config.get(section, 'uid', 'uid') + users = cls.ldap_search_user(login, con, attrs=[uid]) if users and len(users) == 1: [(dn, attrs)] = users if password and con.simple_bind_s(dn, password): @@ -144,10 +160,9 @@ class User: if user_id: LoginAttempt.remove(login) return user_id - elif connection.auth_create_user: + elif config.getboolean(section, 'create_user'): user, = cls.create([{ - 'name': attrs.get(str(connection.auth_uid), - [login])[0], + 'name': attrs.get(uid, [login])[0], 'login': login, }]) return user.id diff --git a/tests/test_ldap_authentication.py b/tests/test_ldap_authentication.py index 6c163f284a..f42b5fa31a 100644 --- a/tests/test_ldap_authentication.py +++ b/tests/test_ldap_authentication.py @@ -6,9 +6,14 @@ from mock import patch import ldap import trytond.tests.test_tryton -from trytond.tests.test_tryton import test_view, test_depends +from trytond.tests.test_tryton import test_depends from trytond.tests.test_tryton import POOL, DB_NAME, USER, CONTEXT from trytond.transaction import Transaction +from trytond.config import config + +from trytond.modules.ldap_authentication.res import parse_ldap_url + +section = 'ldap_authentication' class LDAPAuthenticationTestCase(unittest.TestCase): @@ -16,10 +21,11 @@ class LDAPAuthenticationTestCase(unittest.TestCase): def setUp(self): trytond.tests.test_tryton.install_module('ldap_authentication') + config.add_section(section) + config.set(section, 'uri', 'ldap://localhost/dc=tryton,dc=org') - def test0005views(self): - 'Test views' - test_view('ldap_authentication') + def tearDown(self): + config.remove_section(section) def test0006depends(self): 'Test depends' @@ -29,11 +35,6 @@ class LDAPAuthenticationTestCase(unittest.TestCase): 'Test User.get_login' with Transaction().start(DB_NAME, USER, context=CONTEXT): User = POOL.get('res.user') - Connection = POOL.get('ldap.connection') - - connection = Connection(server='localhost', - auth_base_dn='dc=tryton,dc=org') - connection.save() @patch.object(ldap, 'initialize') @patch.object(User, 'ldap_search_user') @@ -55,13 +56,41 @@ class LDAPAuthenticationTestCase(unittest.TestCase): self.assertFalse(get_login('foo', 'bar', True)) # Test create new user - connection.auth_create_user = True - connection.save() + config.set(section, 'create_user', 'True') user_id = get_login('foo', 'bar', True) foo, = User.search([('login', '=', 'foo')]) self.assertEqual(user_id, foo.id) self.assertEqual(foo.name, 'foo') + def test_parse_ldap_url(self): + 'Test parse_ldap_url' + self.assertEqual( + parse_ldap_url('ldap:///o=University%20of%20Michigan,c=US')[1], + 'o=University of Michigan,c=US') + self.assertEqual( + parse_ldap_url( + 'ldap://ldap.itd.umich.edu/o=University%20of%20Michigan,c=US' + )[1], + 'o=University of Michigan,c=US') + self.assertEqual( + parse_ldap_url( + 'ldap://ldap.itd.umich.edu/o=University%20of%20Michigan,' + 'c=US?postalAddress')[2], + 'postalAddress') + self.assertEqual( + parse_ldap_url( + 'ldap://host.com:6666/o=University%20of%20Michigan,' + 'c=US??sub?(cn=Babs%20Jensen)')[3:5], + ('sub', '(cn=Babs Jensen)')) + self.assertEqual( + parse_ldap_url( + 'ldap:///??sub??bindname=cn=Manager%2co=Foo')[5], + {'bindname': ['cn=Manager,o=Foo']}) + self.assertEqual( + parse_ldap_url( + 'ldap:///??sub??!bindname=cn=Manager%2co=Foo')[5], + {'!bindname': ['cn=Manager,o=Foo']}) + def suite(): suite = trytond.tests.test_tryton.suite() diff --git a/tryton.cfg b/tryton.cfg index e3582f56d9..b07f9f1bff 100644 --- a/tryton.cfg +++ b/tryton.cfg @@ -2,7 +2,4 @@ version=3.3.0 depends: ir - ldap_connection res -xml: - connection.xml diff --git a/view/connection_form.xml b/view/connection_form.xml deleted file mode 100644 index 690fed00ad..0000000000 --- a/view/connection_form.xml +++ /dev/null @@ -1,22 +0,0 @@ - - - - - - -