diff --git a/django/contrib/auth/handlers/modwsgi.py b/django/contrib/auth/handlers/modwsgi.py new file mode 100644 index 0000000000..0e543ef368 --- /dev/null +++ b/django/contrib/auth/handlers/modwsgi.py @@ -0,0 +1,43 @@ +from django.contrib.auth.models import User +from django import db +from django.utils.encoding import force_bytes + + +def check_password(environ, username, password): + """ + Authenticates against Django's auth database + + mod_wsgi docs specify None, True, False as return value depending + on whether the user exists and authenticates. + """ + + # db connection state is managed similarly to the wsgi handler + # as mod_wsgi may call these functions outside of a request/response cycle + db.reset_queries() + + try: + try: + user = User.objects.get(username=username, is_active=True) + except User.DoesNotExist: + return None + return user.check_password(password) + finally: + db.close_connection() + + +def groups_for_user(environ, username): + """ + Authorizes a user based on groups + """ + + db.reset_queries() + + try: + try: + user = User.objects.get(username=username, is_active=True) + except User.DoesNotExist: + return [] + + return [force_bytes(group.name) for group in user.groups.all()] + finally: + db.close_connection() diff --git a/django/contrib/auth/tests/__init__.py b/django/contrib/auth/tests/__init__.py index 094a595238..b3007ea484 100644 --- a/django/contrib/auth/tests/__init__.py +++ b/django/contrib/auth/tests/__init__.py @@ -7,6 +7,7 @@ from django.contrib.auth.tests.forms import * from django.contrib.auth.tests.remote_user import * from django.contrib.auth.tests.management import * from django.contrib.auth.tests.models import * +from django.contrib.auth.tests.handlers import * from django.contrib.auth.tests.hashers import * from django.contrib.auth.tests.signals import * from django.contrib.auth.tests.tokens import * diff --git a/django/contrib/auth/tests/handlers.py b/django/contrib/auth/tests/handlers.py new file mode 100644 index 0000000000..f061042ce3 --- /dev/null +++ b/django/contrib/auth/tests/handlers.py @@ -0,0 +1,45 @@ +from __future__ import unicode_literals + +from django.contrib.auth.handlers.modwsgi import check_password, groups_for_user +from django.contrib.auth.models import User, Group +from django.test import TestCase + + +class ModWsgiHandlerTestCase(TestCase): + """ + Tests for the mod_wsgi authentication handler + """ + + def setUp(self): + user1 = User.objects.create_user('test', 'test@example.com', 'test') + User.objects.create_user('test1', 'test1@example.com', 'test1') + + group = Group.objects.create(name='test_group') + user1.groups.add(group) + + def test_check_password(self): + """ + Verify that check_password returns the correct values as per + http://code.google.com/p/modwsgi/wiki/AccessControlMechanisms#Apache_Authentication_Provider + """ + + # User not in database + self.assertTrue(check_password({}, 'unknown', '') is None) + + # Valid user with correct password + self.assertTrue(check_password({}, 'test', 'test')) + + # Valid user with incorrect password + self.assertFalse(check_password({}, 'test', 'incorrect')) + + def test_groups_for_user(self): + """ + Check that groups_for_user returns correct values as per + http://code.google.com/p/modwsgi/wiki/AccessControlMechanisms#Apache_Group_Authorisation + """ + + # User not in database + self.assertEqual(groups_for_user({}, 'unknown'), []) + + self.assertEqual(groups_for_user({}, 'test'), [b'test_group']) + self.assertEqual(groups_for_user({}, 'test1'), []) diff --git a/docs/howto/apache-auth.txt b/docs/howto/apache-auth.txt deleted file mode 100644 index 719fbc1769..0000000000 --- a/docs/howto/apache-auth.txt +++ /dev/null @@ -1,45 +0,0 @@ -========================================================= -Authenticating against Django's user database from Apache -========================================================= - -Since keeping multiple authentication databases in sync is a common problem when -dealing with Apache, you can configuring Apache to authenticate against Django's -:doc:`authentication system ` directly. This requires Apache -version >= 2.2 and mod_wsgi >= 2.0. For example, you could: - -* Serve static/media files directly from Apache only to authenticated users. - -* Authenticate access to a Subversion_ repository against Django users with - a certain permission. - -* Allow certain users to connect to a WebDAV share created with mod_dav_. - -.. _Subversion: http://subversion.tigris.org/ -.. _mod_dav: http://httpd.apache.org/docs/2.2/mod/mod_dav.html - -Configuring Apache -================== - -To check against Django's authorization database from a Apache configuration -file, you'll need to set 'wsgi' as the value of ``AuthBasicProvider`` or -``AuthDigestProvider`` directive and then use the ``WSGIAuthUserScript`` -directive to set the path to your authentification script: - -.. code-block:: apache - - - AuthType Basic - AuthName "example.com" - AuthBasicProvider wsgi - WSGIAuthUserScript /usr/local/wsgi/scripts/auth.wsgi - Require valid-user - - -Your auth.wsgi script will have to implement either a -``check_password(environ, user, password)`` function (for ``AuthBasicProvider``) -or a ``get_realm_hash(environ, user, realm)`` function (for ``AuthDigestProvider``). - -See the `mod_wsgi documentation`_ for more details about the implementation -of such a solution. - -.. _mod_wsgi documentation: http://code.google.com/p/modwsgi/wiki/AccessControlMechanisms#Apache_Authentication_Provider diff --git a/docs/howto/deployment/wsgi/apache-auth.txt b/docs/howto/deployment/wsgi/apache-auth.txt new file mode 100644 index 0000000000..36e3d0233c --- /dev/null +++ b/docs/howto/deployment/wsgi/apache-auth.txt @@ -0,0 +1,122 @@ +========================================================= +Authenticating against Django's user database from Apache +========================================================= + +Since keeping multiple authentication databases in sync is a common problem when +dealing with Apache, you can configure Apache to authenticate against Django's +:doc:`authentication system ` directly. This requires Apache +version >= 2.2 and mod_wsgi >= 2.0. For example, you could: + +* Serve static/media files directly from Apache only to authenticated users. + +* Authenticate access to a Subversion_ repository against Django users with + a certain permission. + +* Allow certain users to connect to a WebDAV share created with mod_dav_. + +.. _Subversion: http://subversion.tigris.org/ +.. _mod_dav: http://httpd.apache.org/docs/2.2/mod/mod_dav.html + +Authentication with mod_wsgi +============================ + +Make sure that mod_wsgi is installed and activated and that you have +followed the steps to setup +:doc:`Apache with mod_wsgi ` + +Next, edit your Apache configuration to add a location that you want +only authenticated users to be able to view: + +.. code-block:: apache + + WSGIScriptAlias / /path/to/mysite/config/mysite.wsgi + + WSGIProcessGroup %{GLOBAL} + WSGIApplicationGroup django + + + AuthType Basic + AuthName "Top Secret" + Require valid-user + AuthBasicProvider wsgi + WSGIAuthUserScript /path/to/mysite/config/mysite.wsgi + + +The ``WSGIAuthUserScript`` directive tells mod_wsgi to execute the +``check_password`` function in specified wsgi script, passing the user name and +password that it receives from the prompt. In this example, the +``WSGIAuthUserScript`` is the same as the ``WSGIScriptAlias`` that defines your +application :doc:`that is created by django-admin.py startproject +`. + +.. admonition:: Using Apache 2.2 with authentication + + Make sure that ``mod_auth_basic`` and ``mod_authz_user`` are loaded. + + These might be compiled statically into Apache, or you might need to use + LoadModule to load them dynamically in your ``httpd.conf``: + + .. code-block:: apache + + LoadModule auth_basic_module modules/mod_auth_basic.so + LoadModule authz_user_module modules/mod_authz_user.so + +Finally, edit your WSGI script ``mysite.wsgi`` to tie Apache's +authentication to your site's authentication mechanisms by importing the +check_user function: + +.. code-block:: python + + import os + import sys + + os.environ['DJANGO_SETTINGS_MODULE'] = 'mysite.settings' + + from django.contrib.auth.handlers.modwsgi import check_user + + from django.core.handlers.wsgi import WSGIHandler + application = WSGIHandler() + + +Requests beginning with ``/secret/`` will now require a user to authenticate. + +The mod_wsgi `access control mechanisms documentation`_ provides additional +details and information about alternative methods of authentication. + +.. _access control mechanisms documentation: http://code.google.com/p/modwsgi/wiki/AccessControlMechanisms + +Authorization with mod_wsgi and Django groups +--------------------------------------------- + +mod_wsgi also provides functionality to restrict a particular location to +members of a group. + +In this case, the Apache configuration should look like this: + +.. code-block:: apache + + WSGIScriptAlias / /path/to/mysite/config/mysite.wsgi + + WSGIProcessGroup %{GLOBAL} + WSGIApplicationGroup django + + + AuthType Basic + AuthName "Top Secret" + AuthBasicProvider wsgi + WSGIAuthUserScript /path/to/mysite/config/mysite.wsgi + WSGIAuthGroupScript /path/to/mysite/config/mysite.wsgi + Require group secret-agents + Require valid-user + + +To support the ``WSGIAuthGroupScript`` directive, the same WSGI script +``mysite.wsgi`` must also import the ``groups_for_user`` function which +returns a list groups the given user belongs to. + +.. code-block:: python + + from django.contrib.auth.handlers.modwsgi import check_user, groups_for_user + +Requests for ``/secret/`` will now also require user to be a member of the +"secret-agents" group. diff --git a/docs/howto/deployment/wsgi/index.txt b/docs/howto/deployment/wsgi/index.txt index ecb302cee3..769d406b1b 100644 --- a/docs/howto/deployment/wsgi/index.txt +++ b/docs/howto/deployment/wsgi/index.txt @@ -16,6 +16,7 @@ documentation for the following WSGI servers: :maxdepth: 1 modwsgi + apache-auth gunicorn uwsgi diff --git a/docs/howto/deployment/wsgi/modwsgi.txt b/docs/howto/deployment/wsgi/modwsgi.txt index 8398f12eb7..b525255dbd 100644 --- a/docs/howto/deployment/wsgi/modwsgi.txt +++ b/docs/howto/deployment/wsgi/modwsgi.txt @@ -177,6 +177,13 @@ other approaches: 3. Copy the admin static files so that they live within your Apache document root. +Authenticating against Django's user database from Apache +========================================================= + +Django provides a handler to allow Apache to authenticate users directly +against Django's authentication backends. See the :doc:`mod_wsgi authentication +documentation `. + If you get a UnicodeEncodeError =============================== diff --git a/docs/howto/index.txt b/docs/howto/index.txt index 737ee71da4..d39222be26 100644 --- a/docs/howto/index.txt +++ b/docs/howto/index.txt @@ -9,7 +9,6 @@ you quickly accomplish common tasks. .. toctree:: :maxdepth: 1 - apache-auth auth-remote-user custom-management-commands custom-model-fields diff --git a/docs/releases/1.5.txt b/docs/releases/1.5.txt index df8d89c185..fddd03d421 100644 --- a/docs/releases/1.5.txt +++ b/docs/releases/1.5.txt @@ -146,6 +146,9 @@ Django 1.5 also includes several smaller improvements worth noting: configuration duplication. More information can be found in the :func:`~django.contrib.auth.decorators.login_required` documentation. +* Django now provides a mod_wsgi :doc:`auth handler + ` + Backwards incompatible changes in 1.5 =====================================