From 191e587a3c87f4ea1a0457e7e34866c720418635 Mon Sep 17 00:00:00 2001 From: admiralobvious <aferlandqc@gmail.com> Date: Sun, 7 Dec 2014 14:17:28 -0500 Subject: [PATCH] version 0.2.0 implements HTTP Basic Auth and fixes issue #4. --- docs/conf.py | 4 +- docs/index.rst | 11 +++++- examples/basic_auth/app.py | 2 +- flask_simpleldap/__init__.py | 77 +++++++++++++++++++++++------------- setup.py | 2 +- 5 files changed, 62 insertions(+), 34 deletions(-) diff --git a/docs/conf.py b/docs/conf.py index 6913f76..d962ebd 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -63,9 +63,9 @@ copyright = u'2014, Alexandre Ferland' # built documents. # # The short X.Y version. -version = '0.1.0' +version = '0.2.0' # The full version, including alpha/beta/rc tags. -release = '0.1.0' +release = '0.2.0' # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. diff --git a/docs/index.rst b/docs/index.rst index 1e7e964..0080e37 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -73,13 +73,13 @@ directives: ``LDAP_USER_FIELDS`` ``list`` of fields to return when searching for a user's object details. Default: ``list`` (all). ``LDAP_USER_OBJECT_FILTER`` The filter to use when searching for a user object. - Default: '(&(objectclass=Person)(userPrincipalName={}))' + Default: '(&(objectclass=Person)(userPrincipalName=%s))' ``LDAP_USER_GROUPS_FIELD`` The field to return when searching for a user's groups. Default: 'memberOf'. ``LDAP_GROUP_FIELDS`` ``list`` of fields to return when searching for a group's object details. Default: ``list`` (all). ``LDAP_GROUP_OBJECT_FILTER`` The filter to use when searching for a group object. - Default: '(&(objectclass=Group)(userPrincipalName={}))' + Default: '(&(objectclass=Group)(userPrincipalName=%s))' ``LDAP_GROUP_MEMBERS_FIELD`` The field to return when searching for a group's members. Default: 'member' ``LDAP_LOGIN_VIEW`` Views decorated with :meth:`.login_required()` or @@ -108,6 +108,13 @@ History Changes: +- 0.2.0: December 7, 2014 + + - Added HTTP Basic Authentication. Thanks to OptiverTimAll on GitHub. + - Fix GitHub issue `#4 <https://github.com/admiralobvious/flask-simpleldap/issues/4>`_, + User or group queries are vulnerable to LDAP injection. + Make sure you update your filters to use '%s' instead of the old '{}'! + - 0.1.1: September 6, 2014 - Fix GitHub issue `#3 <https://github.com/admiralobvious/flask-simpleldap/issues/3>`_, diff --git a/examples/basic_auth/app.py b/examples/basic_auth/app.py index e8905bd..40c97a9 100644 --- a/examples/basic_auth/app.py +++ b/examples/basic_auth/app.py @@ -15,7 +15,7 @@ ldap = LDAP(app) @app.route('/') @ldap.basic_auth_required def index(): - return "Welcome, {0}!".format(g.ldap_username) + return 'Welcome, {0}!'.format(g.ldap_username) if __name__ == '__main__': app.run() diff --git a/flask_simpleldap/__init__.py b/flask_simpleldap/__init__.py index f2a2328..8beb126 100644 --- a/flask_simpleldap/__init__.py +++ b/flask_simpleldap/__init__.py @@ -5,8 +5,9 @@ from functools import wraps import re import ldap -from flask import abort, current_app, g, redirect, url_for, request -from flask import make_response +import ldap.filter +from flask import abort, current_app, g, make_response, redirect, url_for, \ + request try: from flask import _app_ctx_stack as stack @@ -56,11 +57,11 @@ class LDAP(object): app.config.setdefault('LDAP_OBJECTS_DN', 'distinguishedName') app.config.setdefault('LDAP_USER_FIELDS', []) app.config.setdefault('LDAP_USER_OBJECT_FILTER', - '(&(objectclass=Person)(userPrincipalName={0}))') + '(&(objectclass=Person)(userPrincipalName=%s))') app.config.setdefault('LDAP_USER_GROUPS_FIELD', 'memberOf') app.config.setdefault('LDAP_GROUP_FIELDS', []) app.config.setdefault('LDAP_GROUP_OBJECT_FILTER', - '(&(objectclass=Group)(userPrincipalName={0}))') + '(&(objectclass=Group)(userPrincipalName=%s))') app.config.setdefault('LDAP_GROUP_MEMBERS_FIELD', 'member') app.config.setdefault('LDAP_LOGIN_VIEW', 'login') app.config.setdefault('LDAP_REALM_NAME', 'LDAP authentication') @@ -111,8 +112,9 @@ class LDAP(object): conn = self.initialize try: - conn.simple_bind_s(current_app.config['LDAP_USERNAME'], - current_app.config['LDAP_PASSWORD'].encode('utf-8')) + conn.simple_bind_s( + current_app.config['LDAP_USERNAME'].encode('utf-8'), + current_app.config['LDAP_PASSWORD'].encode('utf-8')) return conn except ldap.LDAPError as e: raise LDAPException(self.error(e)) @@ -152,11 +154,13 @@ class LDAP(object): if user is not None: if not dn_only: fields = current_app.config['LDAP_USER_FIELDS'] - query = current_app.config['LDAP_USER_OBJECT_FILTER'].format(user) + query = ldap.filter.filter_format( + current_app.config['LDAP_USER_OBJECT_FILTER'], (user,)) elif group is not None: if not dn_only: fields = current_app.config['LDAP_GROUP_FIELDS'] - query = current_app.config['LDAP_GROUP_OBJECT_FILTER'].format(group) + query = ldap.filter.filter_format( + current_app.config['LDAP_GROUP_OBJECT_FILTER'], (group,)) conn = self.bind try: records = conn.search_s(current_app.config['LDAP_BASE_DN'], @@ -166,7 +170,8 @@ class LDAP(object): if records: if dn_only: if current_app.config['LDAP_OBJECTS_DN'] in records[0][1]: - dn = records[0][1][current_app.config['LDAP_OBJECTS_DN']] + dn = records[0][1][ + current_app.config['LDAP_OBJECTS_DN']] return dn[0] for k, v in records[0][1].items(): result[k] = v @@ -183,14 +188,19 @@ class LDAP(object): conn = self.bind try: - records = conn.search_s(current_app.config['LDAP_BASE_DN'], ldap.SCOPE_SUBTREE, - current_app.config['LDAP_USER_OBJECT_FILTER'].format(user), - [current_app.config['LDAP_USER_GROUPS_FIELD']]) + records = conn.search_s( + current_app.config['LDAP_BASE_DN'], ldap.SCOPE_SUBTREE, + ldap.filter.filter_format( + current_app.config['LDAP_USER_OBJECT_FILTER'], (user,)), + [current_app.config['LDAP_USER_GROUPS_FIELD']]) conn.unbind_s() if records: - if current_app.config['LDAP_USER_GROUPS_FIELD'] in records[0][1]: - groups = records[0][1][current_app.config['LDAP_USER_GROUPS_FIELD']] - result = [re.findall('(?:cn=|CN=)(.*?),', group)[0] for group in groups] + if current_app.config['LDAP_USER_GROUPS_FIELD'] in records[0][ + 1]: + groups = records[0][1][ + current_app.config['LDAP_USER_GROUPS_FIELD']] + result = [re.findall('(?:cn=|CN=)(.*?),', group)[0] for + group in groups] return result except ldap.LDAPError as e: raise LDAPException(self.error(e)) @@ -204,13 +214,17 @@ class LDAP(object): conn = self.bind try: - records = conn.search_s(current_app.config['LDAP_BASE_DN'], ldap.SCOPE_SUBTREE, - current_app.config['LDAP_GROUP_OBJECT_FILTER'].format(group), - [current_app.config['LDAP_GROUP_MEMBERS_FIELD']]) + records = conn.search_s( + current_app.config['LDAP_BASE_DN'], ldap.SCOPE_SUBTREE, + ldap.filter.filter_format( + current_app.config['LDAP_GROUP_OBJECT_FILTER'], (group,)), + [current_app.config['LDAP_GROUP_MEMBERS_FIELD']]) conn.unbind_s() if records: - if current_app.config['LDAP_GROUP_MEMBERS_FIELD'] in records[0][1]: - members = records[0][1][current_app.config['LDAP_GROUP_MEMBERS_FIELD']] + if current_app.config['LDAP_GROUP_MEMBERS_FIELD'] in \ + records[0][1]: + members = records[0][1][ + current_app.config['LDAP_GROUP_MEMBERS_FIELD']] return members except ldap.LDAPError as e: raise LDAPException(self.error(e)) @@ -240,6 +254,7 @@ class LDAP(object): if g.user is None: return redirect(url_for(current_app.config['LDAP_LOGIN_VIEW'])) return func(*args, **kwargs) + return wrapped @staticmethod @@ -253,20 +268,25 @@ class LDAP(object): user and ``flask.g.ldap_groups`` to the authenticated's user's groups if the credentials are acceptable. - :param list groups: List of groups that should be able to access the view - function. + :param list groups: List of groups that should be able to access the + view function. """ def wrapper(func): @wraps(func) def wrapped(*args, **kwargs): if g.user is None: - return redirect(url_for(current_app.config['LDAP_LOGIN_VIEW'])) + return redirect( + url_for(current_app.config['LDAP_LOGIN_VIEW'])) + match = [group for group in groups if group in g.ldap_groups] if not match: abort(401) + return func(*args, **kwargs) + return wrapped + return wrapper def basic_auth_required(self, func): @@ -283,10 +303,11 @@ class LDAP(object): :param func: The view function to decorate. """ + def make_auth_required_response(): - response = make_response("Authorization required", 401) + response = make_response('Unauthorized', 401) response.www_authenticate.set_basic( - current_app.config['LDAP_REALM_NAME']) + current_app.config['LDAP_REALM_NAME']) return response @wraps(func) @@ -299,12 +320,12 @@ class LDAP(object): req_password = request.authorization.password if req_username is None or req_password is None: - current_app.logger.debug("Got a request without auth data") + current_app.logger.debug('Got a request without auth data') return make_auth_required_response() if not self.bind_user(req_username, req_password): - current_app.logger.debug("User {0!r} gave wrong " - "password".format(req_username)) + current_app.logger.debug('User {0!r} gave wrong ' + 'password'.format(req_username)) return make_auth_required_response() g.ldap_username = req_username diff --git a/setup.py b/setup.py index e8270cc..33c5b72 100644 --- a/setup.py +++ b/setup.py @@ -9,7 +9,7 @@ from setuptools import setup setup( name='Flask-SimpleLDAP', - version='0.1.1', + version='0.2.0', url='https://github.com/admiralobvious/flask-simpleldap', license='MIT', author='Alexandre Ferland', -- GitLab