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/examples/basic_auth/app.py b/examples/basic_auth/app.py new file mode 100644 index 0000000000000000000000000000000000000000..e8905bd179c5f2c0b77a3f7a270b064701533981 --- /dev/null +++ b/examples/basic_auth/app.py @@ -0,0 +1,21 @@ +from flask import Flask, g, request, session, redirect, url_for +from flask.ext.simpleldap import LDAP + +app = Flask(__name__) +app.secret_key = 'dev key' +app.debug = True + +app.config['LDAP_HOST'] = 'ldap.example.org' +app.config['LDAP_BASE_DN'] = 'OU=users,dc=example,dc=org' +app.config['LDAP_USERNAME'] = 'CN=user,OU=Users,DC=example,DC=org' +app.config['LDAP_PASSWORD'] = 'password' + +ldap = LDAP(app) + +@app.route('/') +@ldap.basic_auth_required +def index(): + 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 be9171a344cbd224ca09f5a1aefcb604fb5627ea..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, @@ -222,8 +224,13 @@ class LDAP(object): @staticmethod def login_required(func): - """Used to decorate a view function to require LDAP login but does NOT - require membership from a specific group. + """When applied to a view function, any unauthenticated requests will + be redirected to the view named in LDAP_LOGIN_VIEW. Authenticated + requests do NOT require membership from a specific group. + + The login view is responsible for asking for credentials, checking + them, and setting ``flask.g.user`` to the name of the authenticated + user if the credentials are acceptable. :param func: The view function to decorate. """ @@ -237,8 +244,14 @@ class LDAP(object): @staticmethod def group_required(groups=None): - """Used to decorate a view function to require LDAP login AND membership - from one of the groups within the groups list. + """When applied to a view function, any unauthenticated requests will + be redirected to the view named in LDAP_LOGIN_VIEW. Authenticated + requests are only permitted if they belong to one of the listed groups. + + The login view is responsible for asking for credentials, checking + them, and setting ``flask.g.user`` to the name of the authenticated + 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. @@ -255,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