diff --git a/docs/index.rst b/docs/index.rst index c4a6dbca29040997c9b3f642e0bef6f17100a948..1e7e964849376d70c5d3a8735c170cc9b3877a7d 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -82,8 +82,14 @@ directives: Default: '(&(objectclass=Group)(userPrincipalName={}))' ``LDAP_GROUP_MEMBERS_FIELD`` The field to return when searching for a group's members. Default: 'member' -``LDAP_LOGIN_VIEW`` The view to redirect to when a user needs to log-in. - Default: 'login'. +``LDAP_LOGIN_VIEW`` Views decorated with :meth:`.login_required()` or + :meth:`.group_required()` will redirect + unauthenticated requests to this view. Default: + 'login'. +``LDAP_REALM_NAME`` Views decorated with + :meth:`.basic_auth_required()` will use this as + the "realm" part of HTTP Basic Authentication when + responding to unauthenticated requests. ============================ =================================================== @@ -109,4 +115,4 @@ Changes: - 0.1: August 9, 2014 - - Initial Release \ No newline at end of file + - Initial Release diff --git a/flask_simpleldap/__init__.py b/flask_simpleldap/__init__.py index 0a7359a225c5434f94edb265a39c543138681122..f2a2328bd5ff47b333fe0f81de6b38f895e1056c 100644 --- a/flask_simpleldap/__init__.py +++ b/flask_simpleldap/__init__.py @@ -5,7 +5,8 @@ from functools import wraps import re import ldap -from flask import abort, current_app, g, redirect, url_for +from flask import abort, current_app, g, redirect, url_for, request +from flask import make_response try: from flask import _app_ctx_stack as stack @@ -62,6 +63,7 @@ class LDAP(object): '(&(objectclass=Group)(userPrincipalName={0}))') app.config.setdefault('LDAP_GROUP_MEMBERS_FIELD', 'member') app.config.setdefault('LDAP_LOGIN_VIEW', 'login') + app.config.setdefault('LDAP_REALM_NAME', 'LDAP authentication') if app.config['LDAP_USE_SSL'] or app.config['LDAP_USE_TLS']: ldap.set_option(ldap.OPT_X_TLS_REQUIRE_CERT, @@ -266,3 +268,48 @@ class LDAP(object): return func(*args, **kwargs) return wrapped return wrapper + + def basic_auth_required(self, func): + """When applied to a view function, any unauthenticated requests are + asked to authenticate via HTTP's standard Basic Authentication system. + Requests with credentials are checked with :meth:`.bind_user()`. + + The user's browser will typically show them the contents of + LDAP_REALM_NAME as a prompt for which username and password to enter. + + If the request's credentials are accepted by the LDAP server, the + username is stored in ``flask.g.ldap_username`` and the password in + ``flask.g.ldap_password``. + + :param func: The view function to decorate. + """ + def make_auth_required_response(): + response = make_response("Authorization required", 401) + response.www_authenticate.set_basic( + current_app.config['LDAP_REALM_NAME']) + return response + + @wraps(func) + def wrapped(*args, **kwargs): + if request.authorization is None: + req_username = None + req_password = None + else: + req_username = request.authorization.username + 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") + 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)) + return make_auth_required_response() + + g.ldap_username = req_username + g.ldap_password = req_password + + return func(*args, **kwargs) + + return wrapped