summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorAlexander Sulfrian <alexander@sulfrian.net>2016-01-30 04:22:56 +0100
committerAlexander Sulfrian <alexander@sulfrian.net>2016-02-02 04:23:27 +0100
commitbf49e5c05ffcc1fc4245cb6a77702e05dcce2ed3 (patch)
treea43819960cdcb538526b220f387c9cd802c25c23
parentc6caa836b4fc897b6327fb573cbbcd67764d1cbd (diff)
downloadweb-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.py274
-rw-r--r--requirements.txt2
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