diff options
author | Alexander Sulfrian <alexander@sulfrian.net> | 2016-01-30 04:22:56 +0100 |
---|---|---|
committer | Alexander Sulfrian <alexander@sulfrian.net> | 2016-02-02 04:23:27 +0100 |
commit | bf49e5c05ffcc1fc4245cb6a77702e05dcce2ed3 (patch) | |
tree | a43819960cdcb538526b220f387c9cd802c25c23 | |
parent | c6caa836b4fc897b6327fb573cbbcd67764d1cbd (diff) | |
download | web-bf49e5c05ffcc1fc4245cb6a77702e05dcce2ed3.tar.gz web-bf49e5c05ffcc1fc4245cb6a77702e05dcce2ed3.tar.bz2 web-bf49e5c05ffcc1fc4245cb6a77702e05dcce2ed3.zip |
backend/user/ldap: New LdapBackend
Change the schema used in the LDAP server. The service passwords are now
associated by the hierarchy:
dn: uid=test,ou=users,...
dn: cn=service1,uid=test,ou=users,...
dn: cn=service2,uid=test,ou=users,...
...
This is the new structure used with the new Slapi plugin. Additional to
that, the new backend uses the python-ldap3 module, because python-ldap
has no python3 compatibility.
-rw-r--r-- | accounts/backend/user/ldap.py | 274 | ||||
-rw-r--r-- | requirements.txt | 2 |
2 files changed, 120 insertions, 156 deletions
diff --git a/accounts/backend/user/ldap.py b/accounts/backend/user/ldap.py index da77b29..ed7b792 100644 --- a/accounts/backend/user/ldap.py +++ b/accounts/backend/user/ldap.py @@ -1,18 +1,38 @@ # -*- coding: utf-8 -*- from __future__ import absolute_import -import ldap +import ldap3 +from ldap3.utils.conv import escape_filter_chars +from ldap3.utils.dn import escape_attribute_value -from . import Backend +from . import Backend, InvalidPasswordError, NoSuchUserError from accounts.models import Account +def _escape(value, wildcard=False): + if not wildcard: + value = escape_filter_chars(value) + return escape_attribute_value(value) + + +def _change_password(conn, dn, passwords, as_admin=False): + old_password, new_password = passwords + if as_admin: + conn.extend.standard.modify_password(dn, None, new_password) + else: + try: + conn.extend.standard.modify_password(dn, old_password, + new_password) + except ldap3.LDAPException: + raise InvalidPasswordError('Invalid password') + + class LdapBackend(Backend): def __init__(self, app): super(LdapBackend, self).__init__(app) - self.ldap_host = self.app.config['LDAP_HOST'] + self.host = self.app.config['LDAP_HOST'] self.base_dn = self.app.config['LDAP_BASE_DN'] self.admin_user = self.app.config['LDAP_ADMIN_USER'] self.admin_pass = self.app.config['LDAP_ADMIN_PASS'] @@ -26,55 +46,49 @@ class LdapBackend(Backend): Tries to authenticate a user with a given password. If the authentication is successful an Account object will be returned. """ - self._bind_anonymous() - dn = self._format_dn([('uid', username), ('ou','users')]) - - try: - dn_user, data_user = self.connection.search_s(dn, ldap.SCOPE_SUBTREE)[0] - except ldap.NO_SUCH_OBJECT: - raise self.NoSuchUserError('No such user') - - self._bind_as_user(username, password) - uid = data_user['uid'][0] - mail = data_user['mail'][0] - - dn = self._format_dn([('ou', 'services')]) - filterstr = '(uid=%s)' % self._escape(uid) - data_service = self.connection.search_s(dn, ldap.SCOPE_SUBTREE, filterstr) + user_dn = self._format_dn([('uid', username), ('ou', 'users')]) + conn = self._connect(user_dn, password) + uid = None + mail = None services = [] - for entry in data_service: - cn = filter(lambda x: x.startswith('cn='), entry[0].split(','))[0] - services.append(cn.split('=')[-1]) - - acc = Account(uid, mail, services, dn_user, password) + conn.search(user_dn, '(objectClass=*)', + attributes=['objectClass', 'uid', 'mail', 'cn']) + for entry in conn.entries: + if 'inetOrgPerson' in entry.objectClass.values: + uid = entry.uid.value + mail = entry.mail.value + elif 'servicePassword' in entry.objectClass.value: + services.append(entry.cn.value) - self._unbind() + if uid is None or mail is None: + raise NoSuchUserError("User not found") - return acc + return Account(uid, mail, services, user_dn, password) - def find(self, filters={}, wildcard=False): + def find(self, filters=None, wildcard=False): """ Find accounts by a given filter. """ - self._bind_anonymous() + if filters is None: + filters = dict() filters['objectClass'] = 'inetOrgPerson' + filter_as_list = ['(%s=%s)' % (attr, _escape(value, wildcard)) + for attr, value in filters.items()] + filterstr = '(&%s)' % ''.join(filter_as_list) - - filter_as_list = ['(%s=%s)' % (k,self._escape(v, wildcard)) for k,v in filters.items()] - filterstr = ''.join(filter_as_list) - if len(filter_as_list) > 1: - filterstr = '(&%s)' % filterstr - - dn = self._format_dn([('ou','users')]) - data = self.connection.search_s(dn, ldap.SCOPE_SUBTREE, filterstr) + conn = self._connect() + base_dn = self._format_dn([('ou', 'users')]) accounts = [] - for a in data: - accounts.append(Account(a[1]['uid'][0], a[1]['mail'][0])) - - self._unbind() + try: + conn.search(base_dn, filterstr, search_scope=ldap3.LEVEL, + attributes=['uid', 'mail']) + for entry in conn.entries: + accounts.append(Account(entry.uid.value, entry.mail.value)) + except ldap3.LDAPException: + pass return accounts @@ -82,156 +96,106 @@ class LdapBackend(Backend): """ Persists an account in the backend. """ - self._bind_as_admin() - - dn = self._format_dn([('uid', account.uid),('ou','users')]) - uid = self._escape(account.uid) - attr = [ - ('objectClass', ['top','inetOrgPerson']), - ('uid', uid), ('sn', 'n/a'), ('cn', uid), - ('mail', account.attributes['mail']) - ] - self.connection.add_s(dn, attr) - account.dn = dn - + conn = self._connect_as_admin() + + user_dn = self._format_dn([('uid', account.uid), ('ou', 'users')]) + attrs = { + 'objectClass': ['top', 'inetOrgPerson'], + 'uid': _escape(account.uid), + 'sn': 'n/a', + 'cn': _escape(account.uid), + 'mail': _escape(account.mail), + } + + conn.add(user_dn, attributes=attrs) + account.dn = user_dn account.new_password_root = (None, account.password) - self._alter_passwords(account) - - self._unbind() + self._alter_passwords(conn, account) def update(self, account, as_admin=False): """ Updates account informations like passwords or email. """ + conn = None + user_dn = self._format_dn([('uid', account.uid), ('ou', 'users')]) if as_admin: - self._bind_as_admin() + conn = self._connect_as_admin() else: - self._bind_as_user(account.uid, account.password) - - attr = [(ldap.MOD_REPLACE, k, v) for k, v in account.attributes.items()] - dn = self._format_dn([('uid',account.uid),('ou','users')]) - self.connection.modify_s(dn, attr) - self._alter_passwords(account, as_admin=as_admin) + conn = self._connect(user_dn, account.password) - self._unbind() + attrs = {key: [(ldap3.MODIFY_REPLACE, [value])] + for key, value in account.attributes.items()} + conn.modify(user_dn, attrs) + self._alter_passwords(conn, account, as_admin=as_admin) def delete(self, account, as_admin=False): """ Deletes an account permanently. """ - if isinstance(account, basestring): - raise NotImplementedError() - else: - user = account.uid - password = account.password - + conn = None if as_admin: - self._bind_as_admin() + conn = self._connect_as_admin() else: - self._bind_as_user(user, password) + conn = self._connect(account.uid, account.password) - dn = [self._format_dn([('uid',user),('cn',s),('ou','services')]) for s.id in account.services] - dn.append(self._format_dn([('uid', user), ('ou','users')])) + dns = [[('cn', service.id), ('uid', account.uid), ('ou', 'users')] + for service in account.services] + \ + [[('uid', account.uid), ('ou', 'users')]] - for x in dn: - self.connection.delete_s(x) + for dn in dns: + conn.delete(self._format_dn(dn)) - self._unbind() - - def _format_dn(self, attr, with_base_dn = True): + def _format_dn(self, attr, with_base_dn=True): if with_base_dn: - attr.extend(self.base_dn) + attr.extend(self.base_dn) - dn = ['%s=%s' % (item[0], self._escape(item[1])) for item in attr] + dn = ['%s=%s' % (item[0], _escape(item[1])) + for item in attr] return ','.join(dn) - def _bind(self, dn, password): - self.connection = ldap.initialize(self.ldap_host) - self.connection.version = ldap.VERSION3 + def _connect(self, user=None, password=None): + server = ldap3.Server(self.host) + conn = ldap3.Connection(server, user, password, raise_exceptions=True) try: - self.connection.simple_bind_s(dn, password) - except ldap.INVALID_CREDENTIALS: - raise self.InvalidPasswordError("Invalid Password") - - def _bind_as_admin(self): - if self.binded: - self._unbind() - - dn = self._format_dn([('cn', self.admin_user)]) - self._bind(dn, self.admin_pass) - - self.admin = True - self.binded = True - - def _bind_as_user(self, username, password): - if self.binded: - self._unbind() + conn.bind() + except ldap3.LDAPInvalidCredentialsResult: + raise InvalidPasswordError('Invalid password') - dn = self._format_dn([('uid', username),('ou', 'users')]) - self._bind(dn, password) + return conn - self.binded = True - self.admin = True + def _connect_as_admin(self): + admin_dn = self._format_dn([('cn', self.admin_user)]) + return self._connect(admin_dn, self.admin_pass) - def _bind_anonymous(self): - if self.binded: - self._unbind() - - self._bind('','') - - self.binded = True - - def _unbind(self): - self.connection.unbind_s() - self.admin = False - self.binded = False - - def _alter_passwords(self, account, as_admin=False): + def _alter_passwords(self, conn, account, as_admin=False): if account.new_password_root: - dn = self._format_dn([('uid',account.uid),('ou','users')]) - old, new = account.new_password_root - if as_admin: - self.connection.passwd_s(dn, None, new) - else: - try: - self.connection.passwd_s(dn, old, new) - except ldap.UNWILLING_TO_PERFORM: - raise InvalidPasswordError() - - account.password = new - - account.new_password_root = None + user_dn = self._format_dn([('uid', account.uid), ('ou', 'users')]) + _change_password(conn, user_dn, account.new_password_root, as_admin) + _, account.password = account.new_password_root + account.new_password_root = None for service, passwords in account.new_password_services.items(): - dn = self._format_dn([('uid',account.uid),('cn',service),('ou','services')]) - old, new = passwords + service_id = service.lower() + service_dn = self._format_dn([('cn', service_id), ('uid', account.uid), + ('ou', 'users')]) + _, new = passwords if new != None: - if service not in account.services: - attr = [('objectClass', ['top', 'servicePassword']), ('uid', account.uid)] - self.connection.add_s(dn, attr) - - if as_admin: - self.connection.passwd_s(dn, None, new) - else: - self.connection.passwd_s(dn, old, new) - + if service_id not in account.services: + attrs = { + 'objectClass': ['top', 'servicePassword'], + 'cn': _escape(service_id), + } + conn.add(service_dn, attributes=attrs) + account.services.append(service_id) + + _change_password(conn, service_dn, passwords, as_admin) else: - s = service.lower() - if s in account.services: - self.connection.delete_s(dn) - account.services.remove(s) - - account.new_password_services = {} - - def _escape(self, s, wildcard=False): - chars_to_escape = ['\\',',','=','+','<','>',';','"','\'','#','(',')','\0'] - - if not wildcard: - chars_to_escape.append('*') + if service_id in account.services: + conn.delete(service_dn) + account.services.remove(service_id) - escape = lambda x,y: x.replace(y,'\%02X' % ord(y)) + del account.new_password_services[service] - return reduce(escape, chars_to_escape, s) diff --git a/requirements.txt b/requirements.txt index 0d0a1f4..c07bff2 100644 --- a/requirements.txt +++ b/requirements.txt @@ -7,4 +7,4 @@ Jinja2>=2.4 WTForms>=1.0 itsdangerous pycrypto -python-ldap +ldap3 |