Hi all,

I developped something that could be called a "Pluggable authentication
frontend framework". The basic idea is to have a configurable "frontend" that
authenticates the user using some "backend" and does some action depending on
the result of authentication. If the authentication fails, it can do anything
it wants (redirect to some path, display login form...).

Within this scenario it is easy to change all login forms at once without a
need to change a lot of code.

Note: I'm not asking for help with coding, but for your opinions (about the
idea, implemantation...). I can create a feature request afterwards (and
create detailed usage documentation). If you find it not useful, I will keep
it just for me.

Rationale: The be able to change login form and it's behaviour easily by
changing few lines of code.

One of reasons: The admin login page/login procedure cannot be changed without
a change to Django core code.

pluggable-auth.patch
--------------------
The first patch adds pluggable authentication frontends framework. It is
pluggable, because it uses authentication decorators and a configuration
variable AUTHENTICATION_FRONTENDS that defines the set of available
frontends. The key for choosing the right frontend is the same as for
backends - the right set of decorator arguments (called frontends_options).
The first matching frontend is used for user login.

The decorators in django.contrib.auth.decorators has common arguments:
frontends (list of frontend classes), frontends_variable (name of
configuration variable that holds the classes), **frontend_options (arguments
used to select the right frontend).

If the frontends argument is specified, it is taken as the list of available
frontends; otherwise the frontends_variable is taken, if specified; or the
AUTHENTICATION_FRONTENDS settings variable is taken, if nothing is specified.

The key class for frontends is in django.contrib.auth.frontends:
AuthenticationFrontend. It has the only method login_decorator that performs
the authentication check and does the real reaction.
BasicAuthenticationFrontend extends the class by really checking the user
permissions (calls the test_func) and by calling appropriate (abstract)
method; RedirectFrontend implements redirection in case the authentication
fails; DefaultRedirectFrontend implements the same redirection, but takes
settings (URL, field name) from the settings.LOGIN_URL and from
auth.REDIRECT_FIELD_NAME as it is currently working. The
DefaultRefirectFrontend is also the default frontend to keep backwards
compatibility.

The result from the patch is that there is no change in external behaviour.
The _CheckLogin uses the right frontend (DefaultRedirectFrontend by default)
to check the user login.


login-dialog-auth.patch
-----------------------
This patch shows the main benefit of using the authentication frontend. It
replaces the admin login dialog with LoginDialogFrontend and adds new
ADMIN_AUTHENTICATION_FRONTENDS settings variable to change the frontend.

The functionality is the same, only converted to a class that inherits from
BasicAuthenticationFrontend.

Two benefits: 1. the admin login can be changed; 2. the login dialog can be
reused anywhere else.

Further directions, notes
-------------------------
I can imagine that the framework could be used to develop multi-login pages
that show multiple login dialogs (views). Or to enhance rapid development -
use some simple login at the beginning and replace it easily by changing the
settings variable when the better login form is developped.

What I didn't speak about is the need to change the login view's html source -
it has to correspond with the used frontend. It could be implemented that the
frontend passes it's type as an argument to view function and the view
function could include appropriate html source. But this is completely on the
frontend author, and not part of this work.

Note: Sorry for my English and possible confusion on used terms.

Best regards,
Oldrich.

--~--~---------~--~----~------------~-------~--~----~
You received this message because you are subscribed to the Google Groups 
"Django developers" group.
To post to this group, send email to django-developers@googlegroups.com
To unsubscribe from this group, send email to [EMAIL PROTECTED]
For more options, visit this group at 
http://groups.google.com/group/django-developers?hl=en
-~----------~----~----~----~------~----~------~--~---

diff --git a/django/conf/global_settings.py b/django/conf/global_settings.py
index 2c9720d..774e3ba 100644
--- a/django/conf/global_settings.py
+++ b/django/conf/global_settings.py
@@ -357,6 +357,8 @@ BANNED_IPS = ()
 
 AUTHENTICATION_BACKENDS = ('django.contrib.auth.backends.ModelBackend',)
 
+AUTHENTICATION_FRONTENDS = ('django.contrib.auth.frontends.DefaultRedirectFrontend',)
+
 LOGIN_URL = '/accounts/login/'
 
 LOGOUT_URL = '/accounts/logout/'
diff --git a/django/contrib/auth/__init__.py b/django/contrib/auth/__init__.py
index 9813d9a..d1eb956 100644
--- a/django/contrib/auth/__init__.py
+++ b/django/contrib/auth/__init__.py
@@ -1,32 +1,10 @@
 import datetime
-from django.core.exceptions import ImproperlyConfigured
+from django.contrib.auth.utils import get_backends, load_backend
 
 SESSION_KEY = '_auth_user_id'
 BACKEND_SESSION_KEY = '_auth_user_backend'
 REDIRECT_FIELD_NAME = 'next'
 
-def load_backend(path):
-    i = path.rfind('.')
-    module, attr = path[:i], path[i+1:]
-    try:
-        mod = __import__(module, {}, {}, [attr])
-    except ImportError, e:
-        raise ImproperlyConfigured, 'Error importing authentication backend %s: "%s"' % (module, e)
-    except ValueError, e:
-        raise ImproperlyConfigured, 'Error importing authentication backends. Is AUTHENTICATION_BACKENDS a correctly defined list or tuple?'
-    try:
-        cls = getattr(mod, attr)
-    except AttributeError:
-        raise ImproperlyConfigured, 'Module "%s" does not define a "%s" authentication backend' % (module, attr)
-    return cls()
-
-def get_backends():
-    from django.conf import settings
-    backends = []
-    for backend_path in settings.AUTHENTICATION_BACKENDS:
-        backends.append(load_backend(backend_path))
-    return backends
-
 def authenticate(**credentials):
     """
     If the given credentials are valid, return a User object.
diff --git a/django/contrib/auth/decorators.py b/django/contrib/auth/decorators.py
index 25bc207..0896ddf 100644
--- a/django/contrib/auth/decorators.py
+++ b/django/contrib/auth/decorators.py
@@ -3,68 +3,108 @@ try:
 except ImportError:
     from django.utils.functional import wraps, update_wrapper  # Python 2.3, 2.4 fallback.
 
-from django.contrib.auth import REDIRECT_FIELD_NAME
-from django.http import HttpResponseRedirect
-from django.utils.http import urlquote
+from django.core.exceptions import ImproperlyConfigured
+from django.contrib.auth.utils import get_frontends
 
-def user_passes_test(test_func, login_url=None, redirect_field_name=REDIRECT_FIELD_NAME):
+def user_passes_test(test_func, frontends=None, frontends_variable=None, 
+                     **frontend_options):
     """
     Decorator for views that checks that the user passes the given test,
-    redirecting to the log-in page if necessary. The test should be a callable
-    that takes the user object and returns True if the user passes.
+    redirecting to the authentication frontend if necessary. The test should
+    be a callable that takes the user object and returns True if the user
+    passes.
     """
     def decorate(view_func):
-        return _CheckLogin(view_func, test_func, login_url, redirect_field_name)
+        return _CheckLogin(view_func, test_func, frontends,
+                           frontends_variable, frontend_options)
     return decorate
 
-def login_required(function=None, redirect_field_name=REDIRECT_FIELD_NAME):
+def login_required(function=None, frontends=None, frontends_variable=None,
+                   **frontend_options):
     """
     Decorator for views that checks that the user is logged in, redirecting
-    to the log-in page if necessary.
+    to the authentication frontend if necessary.
     """
     actual_decorator = user_passes_test(
         lambda u: u.is_authenticated(),
-        redirect_field_name=redirect_field_name
+        frontends,
+        frontends_variable,
+        **frontend_options
+    )
+    if function:
+        return actual_decorator(function)
+    return actual_decorator
+
+def staff_member_required(function=None, frontends=None, frontends_variable=None,
+                          **frontend_options):
+    """
+    Decorator for views that checks that the user is logged in and has staff
+    rights, redirecting to the authentication frontend if necessary.
+    """
+    actual_decorator = user_passes_test(
+        lambda u: u.is_authenticated() and u.is_staff,
+        frontends,
+        frontends_variable,
+        **frontend_options
     )
     if function:
         return actual_decorator(function)
     return actual_decorator
 
-def permission_required(perm, login_url=None):
+def permission_required(perm, frontends=None, frontends_variable=None,
+                        **frontend_options):
     """
     Decorator for views that checks whether a user has a particular permission
-    enabled, redirecting to the log-in page if necessary.
+    enabled, redirecting to the authentication frontend if necessary.
     """
-    return user_passes_test(lambda u: u.has_perm(perm), login_url=login_url)
+    return user_passes_test(lambda u: u.has_perm(perm), frontends,
+                            frontends_variable, **frontend_options)
 
 class _CheckLogin(object):
     """
-    Class that checks that the user passes the given test, redirecting to
-    the log-in page if necessary. If the test is passed, the view function
-    is invoked. The test should be a callable that takes the user object
-    and returns True if the user passes.
+    Class that calls appropriate authentication frontend to check if the user
+    passes the given test. The frontend usually executes the test and if the
+    test passes, it invokes the view function. The test should be a callable 
+    that takes the user object and returns True if the user passes.
+    
+    The frontend can be specified either as a list of class names or as
+    a settings variable name. The suitable frontend is found by checking if the
+    class constructor accepts all given options.   
 
     We use a class here so that we can define __get__. This way, when a
     _CheckLogin object is used as a method decorator, the view function
     is properly bound to its instance.
     """
-    def __init__(self, view_func, test_func, login_url=None, redirect_field_name=REDIRECT_FIELD_NAME):
-        if not login_url:
-            from django.conf import settings
-            login_url = settings.LOGIN_URL
+    def __init__(self, view_func, test_func, frontends=None,
+                 frontends_variable=None, frontend_options={},
+                 loaded_frontend=None):
+        if loaded_frontend:
+            self.frontend = loaded_frontend
+        else:
+            if not frontends:
+                if frontends_variable:
+                    frontends = get_frontends(frontends_variable=frontends_variable,
+                                              options=frontend_options)
+                else:
+                    frontends = get_frontends(options=frontend_options)
+            else:
+                frontends = get_frontends(frontends=frontends,
+                                          options=frontend_options)
+
+            if not frontends:
+                raise ImproperlyConfigured('No authentication frontend found that accepts all options')
+    
+            self.frontend = frontends[0]
+        
         self.view_func = view_func
         self.test_func = test_func
-        self.login_url = login_url
-        self.redirect_field_name = redirect_field_name
         update_wrapper(self, view_func)
         
     def __get__(self, obj, cls=None):
         view_func = self.view_func.__get__(obj, cls)
-        return _CheckLogin(view_func, self.test_func, self.login_url, self.redirect_field_name)
+        return _CheckLogin(view_func, self.test_func, 
+                           loaded_frontend=self.frontend)
     
     def __call__(self, request, *args, **kwargs):
-        if self.test_func(request.user):
-            return self.view_func(request, *args, **kwargs)
-        path = urlquote(request.get_full_path())
-        tup = self.login_url, self.redirect_field_name, path
-        return HttpResponseRedirect('%s?%s=%s' % tup)
+        return self.frontend.login_decorator(request, self.test_func, 
+                                             self.view_func, args, kwargs)
diff --git a/django/contrib/auth/frontends.py b/django/contrib/auth/frontends.py
new file mode 100644
index 0000000..5d2e768
--- /dev/null
+++ b/django/contrib/auth/frontends.py
@@ -0,0 +1,75 @@
+from django.contrib.auth import REDIRECT_FIELD_NAME
+from django.http import HttpResponseRedirect
+from django.utils.http import urlquote
+
+class AuthenticationFrontend(object):
+    """
+    Generic authentication frontend with login decorator. The generic frontend
+    forwards the request directly to the given view function.
+    
+    Login decorator has a test function argument that performs a test on
+    the given user. The user is generally taken from request.user.  
+    """
+    def login_decorator(self, request, test_func, view_func, args, kwargs):
+        return view_func(request, *args, **kwargs)
+
+class BasicAuthenticationFrontend(AuthenticationFrontend):
+    """
+    Basic authentication frontend with user test. The class should not be used
+    directly
+    
+    The login decorator calls the given test function with a user taken from
+    the request. If the test passes, it calls the passed decorator; default
+    is to call the view function. If the test fails, the failed decorator is
+    called; default is to raise NotImplementedError exception.  
+    """
+    def login_decorator(self, request, test_func, view_func, args, kwargs):
+        if test_func(request.user):
+            return self.passed_decorator(request, test_func, view_func, args, kwargs)
+        else:
+            return self.failed_decorator(request, test_func, view_func, args, kwargs)
+    
+    def passed_decorator(self, request, test_func, view_func, args, kwargs):
+        "User has passed the authentication test"
+        return view_func(request, *args, **kwargs)
+
+    def failed_decorator(self, request, test_func, view_func, args, kwargs):
+        "User has failed the authentication test"
+        raise NotImplementedError("The failed authentication is not handled by default")
+
+class RedirectFrontend(BasicAuthenticationFrontend):
+    """
+    Authentication frontend that redirects user to the specified page if the
+    given authentication test fails. Both the login URL and redirect field name
+    has to be specified. 
+    
+    The user is redirected to the given login URL with redirect field name as
+    an argument name and original URL as a value:
+    
+        <login_url>&<redirect_field_name>=<original_url>
+    """
+    def __init__(self, login_url, redirect_field_name):
+        super(RedirectFrontend, self).__init__()
+        self.login_url = login_url
+        self.redirect_field_name = redirect_field_name
+
+    def failed_decorator(self, request, test_func, view_func, args, kwargs):
+        path = urlquote(request.get_full_path())
+        tup = self.login_url, self.redirect_field_name, path
+        return HttpResponseRedirect('%s?%s=%s' % tup)
+    
+class DefaultRedirectFrontend(RedirectFrontend):
+    """
+    Default authentication frontend that redirects user to the specified page
+    if the given authentication test fails. If the login URL is not specified, 
+    it is taken from the settings.LOGIN_URL, if the redirect field name is
+    missing, it is taken from django.contrib.auth.REDIRECT_FIELD_NAME.
+    """
+    def __init__(self, login_url=None, 
+                 redirect_field_name=REDIRECT_FIELD_NAME):
+        if not login_url:
+            from django.conf import settings
+            login_url = settings.LOGIN_URL
+        
+        super(DefaultRedirectFrontend, self).__init__(login_url, 
+                                                      redirect_field_name)
diff --git a/django/contrib/auth/utils.py b/django/contrib/auth/utils.py
new file mode 100644
index 0000000..6ba5e16
--- /dev/null
+++ b/django/contrib/auth/utils.py
@@ -0,0 +1,130 @@
+from django.core.exceptions import ImproperlyConfigured
+
+class ImproperlyConfiguredModule(Exception):
+    "Improperly configured module"
+    def __init__(self, message):
+        super(ImproperlyConfiguredModule, self).__init__(message)
+
+class ImproperlyConfiguredModuleValue(ImproperlyConfiguredModule):
+    "Improperly configured module, invalid variable value"
+    pass
+
+class ImproperlyConfiguredModuleName(ImproperlyConfiguredModule):
+    "Improperly configured module, invalid module name"
+    def __init__(self, message, module):
+        super(ImproperlyConfiguredModuleName, self).__init__(message)
+        self.module = module
+
+class ImproperlyConfiguredModuleClass(ImproperlyConfiguredModuleName):
+    "Improperly configured module, the class cannot be found"
+    def __init__(self, message, module, module_class):
+        super(ImproperlyConfiguredModuleClass, self).__init__(message, module)
+        self.module_class = module_class
+
+def _load_class(path):
+    """
+    Loads module from the supplied path.
+    """
+    if not isinstance(path, basestring):
+        raise ImproperlyConfiguredModuleValue("The class name should be a string")
+    
+    i = path.rfind('.')
+    module, attr = path[:i], path[i+1:]
+    try:
+        mod = __import__(module, {}, {}, [attr])
+    except ValueError, e:
+        raise ImproperlyConfiguredModuleValue(e)
+    except ImportError, e:
+        raise ImproperlyConfiguredModuleName(e, module)
+    try:
+        cls = getattr(mod, attr)
+    except AttributeError, e:
+        raise ImproperlyConfiguredModuleClass(e, module, attr)
+    return cls
+
+def _load_classes(classes, loader):
+    """
+    Loads classes from the supplied list (or a string), calls loader for
+    each found class.
+    """
+    loaded_classes = []
+    if isinstance(classes, basestring):
+        classes = (classes,)
+    for class_path in classes:
+        cls = loader(_load_class(class_path))
+        if cls is not None:
+            loaded_classes.append(cls)
+    return loaded_classes
+
+def _load_variable_classes(settings_variable, loader):
+    """
+    Loads classes from the settings variable, calls loader for each loaded
+    class.
+    """
+    from django.conf import settings
+    classes = settings.__getattr__(settings_variable)
+    return _load_classes(classes, loader)
+
+def get_backends(backends=None, backends_variable="AUTHENTICATION_BACKENDS"):
+    """
+    Load authentication backends from the list or from the settings variable. 
+    """
+    try:
+        if backends:
+            return _load_classes(backends, lambda c: c())
+        else:
+            return _load_variable_classes(backends_variable, lambda c: c())
+    except ImproperlyConfiguredModuleClass, e:
+        raise ImproperlyConfigured('Module "%s" does not define a "%s" authentication backend' % (e.module, e.module_class))
+    except ImproperlyConfiguredModuleName, e:
+        raise ImproperlyConfigured('Error importing authentication backend %s: "%s"' % (e.module, e.message))
+    except ImproperlyConfiguredModuleValue, e:
+        if backends:
+            raise ImproperlyConfigured('Error importing authentication backends. Is backends argument a correctly defined list or tuple?')
+        else:
+            raise ImproperlyConfigured('Error importing authentication backends. Is %s a correctly defined list or tuple?' % (backends_variable,))
+
+def load_backend(backend=None):
+    """
+    Load one particular backend.
+    """
+    loaded_backends = get_backends(backends=backend)
+    if loaded_backends:
+        return loaded_backends[0]
+    else:
+        return None
+
+def _load_frontend(cls, kwargs):
+    """
+    Frontend loader that calls class constructor with supplied arguments.
+    """ 
+    try:
+        return cls(**kwargs)
+    except TypeError:
+        # This frontend doesn't accept these arguments.
+        # Pass to the next one.
+        pass
+
+def get_frontends(options={}, frontends_variable="AUTHENTICATION_FRONTENDS",
+                  frontends=None):
+    """
+    Loads suitable frontends from the listor from the settings variable 
+    The suitable frontend is any that accept all options as keyword arguments
+    to it's constructor.
+    """
+    try:
+        if frontends:
+            return _load_classes(frontends,
+                                 lambda c: _load_frontend(c, options))
+        else:
+            return _load_variable_classes(frontends_variable,
+                                          lambda c: _load_frontend(c, options))
+    except ImproperlyConfiguredModuleClass, e:
+        raise ImproperlyConfigured('Module "%s" does not define a "%s" authentication frontend' % (e.module, e.module_class))
+    except ImproperlyConfiguredModuleName, e:
+        raise ImproperlyConfigured('Error importing authentication frontend %s: "%s"' % (e.module, e.message))
+    except ImproperlyConfiguredModuleValue, e:
+        if frontends:
+            raise ImproperlyConfigured('Error importing authentication frontends. Is frontends argument a correctly defined list or tuple?')
+        else:
+            raise ImproperlyConfigured('Error importing authentication frontends. Is %s a correctly defined list or tuple?' % (frontends_variable,))
diff --git a/django/conf/global_settings.py b/django/conf/global_settings.py
index 774e3ba..423026c 100644
--- a/django/conf/global_settings.py
+++ b/django/conf/global_settings.py
@@ -358,6 +358,7 @@ BANNED_IPS = ()
 AUTHENTICATION_BACKENDS = ('django.contrib.auth.backends.ModelBackend',)
 
 AUTHENTICATION_FRONTENDS = ('django.contrib.auth.frontends.DefaultRedirectFrontend',)
+ADMIN_AUTHENTICATION_FRONTENDS = ('django.contrib.auth.frontends.login.LoginDialogFrontend',)
 
 LOGIN_URL = '/accounts/login/'
 
diff --git a/django/contrib/admin/views/decorators.py b/django/contrib/admin/views/decorators.py
index c27b2e7..8790dbd 100644
--- a/django/contrib/admin/views/decorators.py
+++ b/django/contrib/admin/views/decorators.py
@@ -1,114 +1,9 @@
-import base64
-import md5
-import cPickle as pickle
-try:
-    from functools import wraps
-except ImportError:
-    from django.utils.functional import wraps  # Python 2.3, 2.4 fallback.
-
-from django import http, template
-from django.conf import settings
-from django.contrib.auth.models import User
-from django.contrib.auth import authenticate, login
-from django.shortcuts import render_to_response
-from django.utils.translation import ugettext_lazy, ugettext as _
-from django.utils.safestring import mark_safe
-
-ERROR_MESSAGE = ugettext_lazy("Please enter a correct username and password. Note that both fields are case-sensitive.")
-LOGIN_FORM_KEY = 'this_is_the_login_form'
-
-def _display_login_form(request, error_message=''):
-    request.session.set_test_cookie()
-    if request.POST and 'post_data' in request.POST:
-        # User has failed login BUT has previously saved post data.
-        post_data = request.POST['post_data']
-    elif request.POST:
-        # User's session must have expired; save their post data.
-        post_data = _encode_post_data(request.POST)
-    else:
-        post_data = _encode_post_data({})
-    return render_to_response('admin/login.html', {
-        'title': _('Log in'),
-        'app_path': request.path,
-        'post_data': post_data,
-        'error_message': error_message
-    }, context_instance=template.RequestContext(request))
-
-def _encode_post_data(post_data):
-    pickled = pickle.dumps(post_data)
-    pickled_md5 = md5.new(pickled + settings.SECRET_KEY).hexdigest()
-    return base64.encodestring(pickled + pickled_md5)
-
-def _decode_post_data(encoded_data):
-    encoded_data = base64.decodestring(encoded_data)
-    pickled, tamper_check = encoded_data[:-32], encoded_data[-32:]
-    if md5.new(pickled + settings.SECRET_KEY).hexdigest() != tamper_check:
-        from django.core.exceptions import SuspiciousOperation
-        raise SuspiciousOperation, "User may have tampered with session cookie."
-    return pickle.loads(pickled)
+from django.contrib.auth.decorators import staff_member_required as auth_staff_member_required
 
 def staff_member_required(view_func):
-    """
-    Decorator for views that checks that the user is logged in and is a staff
-    member, displaying the login page if necessary.
-    """
-    def _checklogin(request, *args, **kwargs):
-        if request.user.is_authenticated() and request.user.is_staff:
-            # The user is valid. Continue to the admin page.
-            if 'post_data' in request.POST:
-                # User must have re-authenticated through a different window
-                # or tab.
-                request.POST = _decode_post_data(request.POST['post_data'])
-            return view_func(request, *args, **kwargs)
-
-        assert hasattr(request, 'session'), "The Django admin requires session middleware to be installed. Edit your MIDDLEWARE_CLASSES setting to insert 'django.contrib.sessions.middleware.SessionMiddleware'."
-
-        # If this isn't already the login page, display it.
-        if LOGIN_FORM_KEY not in request.POST:
-            if request.POST:
-                message = _("Please log in again, because your session has expired. Don't worry: Your submission has been saved.")
-            else:
-                message = ""
-            return _display_login_form(request, message)
-
-        # Check that the user accepts cookies.
-        if not request.session.test_cookie_worked():
-            message = _("Looks like your browser isn't configured to accept cookies. Please enable cookies, reload this page, and try again.")
-            return _display_login_form(request, message)
-
-        # Check the password.
-        username = request.POST.get('username', None)
-        password = request.POST.get('password', None)
-        user = authenticate(username=username, password=password)
-        if user is None:
-            message = ERROR_MESSAGE
-            if '@' in username:
-                # Mistakenly entered e-mail address instead of username? Look it up.
-                users = list(User.objects.filter(email=username))
-                if len(users) == 1:
-                    message = _("Your e-mail address is not your username. Try '%s' instead.") % users[0].username
-                else:
-                    # Either we cannot find the user, or if more than 1 
-                    # we cannot guess which user is the correct one.
-                    message = _("Usernames cannot contain the '@' character.")
-            return _display_login_form(request, message)
-
-        # The user data is correct; log in the user in and continue.
-        else:
-            if user.is_active and user.is_staff:
-                login(request, user)
-                # TODO: set last_login with an event.
-                if 'post_data' in request.POST:
-                    post_data = _decode_post_data(request.POST['post_data'])
-                    if post_data and LOGIN_FORM_KEY not in post_data:
-                        # overwrite request.POST with the saved post_data, and continue
-                        request.POST = post_data
-                        request.user = user
-                        return view_func(request, *args, **kwargs)
-                    else:
-                        request.session.delete_test_cookie()
-                        return http.HttpResponseRedirect(request.path)
-            else:
-                return _display_login_form(request, ERROR_MESSAGE)
-
-    return wraps(view_func)(_checklogin)
+    actual_decorator = \
+        auth_staff_member_required(view_func,
+                                   frontends_variable="ADMIN_AUTHENTICATION_FRONTENDS",
+                                   login_template_name="admin/login.html")
+    
+    return actual_decorator
diff --git a/django/contrib/auth/frontends.py b/django/contrib/auth/frontends.py
deleted file mode 100644
index 5d2e768..0000000
--- a/django/contrib/auth/frontends.py
+++ /dev/null
@@ -1,75 +0,0 @@
-from django.contrib.auth import REDIRECT_FIELD_NAME
-from django.http import HttpResponseRedirect
-from django.utils.http import urlquote
-
-class AuthenticationFrontend(object):
-    """
-    Generic authentication frontend with login decorator. The generic frontend
-    forwards the request directly to the given view function.
-    
-    Login decorator has a test function argument that performs a test on
-    the given user. The user is generally taken from request.user.  
-    """
-    def login_decorator(self, request, test_func, view_func, args, kwargs):
-        return view_func(request, *args, **kwargs)
-
-class BasicAuthenticationFrontend(AuthenticationFrontend):
-    """
-    Basic authentication frontend with user test. The class should not be used
-    directly
-    
-    The login decorator calls the given test function with a user taken from
-    the request. If the test passes, it calls the passed decorator; default
-    is to call the view function. If the test fails, the failed decorator is
-    called; default is to raise NotImplementedError exception.  
-    """
-    def login_decorator(self, request, test_func, view_func, args, kwargs):
-        if test_func(request.user):
-            return self.passed_decorator(request, test_func, view_func, args, kwargs)
-        else:
-            return self.failed_decorator(request, test_func, view_func, args, kwargs)
-    
-    def passed_decorator(self, request, test_func, view_func, args, kwargs):
-        "User has passed the authentication test"
-        return view_func(request, *args, **kwargs)
-
-    def failed_decorator(self, request, test_func, view_func, args, kwargs):
-        "User has failed the authentication test"
-        raise NotImplementedError("The failed authentication is not handled by default")
-
-class RedirectFrontend(BasicAuthenticationFrontend):
-    """
-    Authentication frontend that redirects user to the specified page if the
-    given authentication test fails. Both the login URL and redirect field name
-    has to be specified. 
-    
-    The user is redirected to the given login URL with redirect field name as
-    an argument name and original URL as a value:
-    
-        <login_url>&<redirect_field_name>=<original_url>
-    """
-    def __init__(self, login_url, redirect_field_name):
-        super(RedirectFrontend, self).__init__()
-        self.login_url = login_url
-        self.redirect_field_name = redirect_field_name
-
-    def failed_decorator(self, request, test_func, view_func, args, kwargs):
-        path = urlquote(request.get_full_path())
-        tup = self.login_url, self.redirect_field_name, path
-        return HttpResponseRedirect('%s?%s=%s' % tup)
-    
-class DefaultRedirectFrontend(RedirectFrontend):
-    """
-    Default authentication frontend that redirects user to the specified page
-    if the given authentication test fails. If the login URL is not specified, 
-    it is taken from the settings.LOGIN_URL, if the redirect field name is
-    missing, it is taken from django.contrib.auth.REDIRECT_FIELD_NAME.
-    """
-    def __init__(self, login_url=None, 
-                 redirect_field_name=REDIRECT_FIELD_NAME):
-        if not login_url:
-            from django.conf import settings
-            login_url = settings.LOGIN_URL
-        
-        super(DefaultRedirectFrontend, self).__init__(login_url, 
-                                                      redirect_field_name)
diff --git a/django/contrib/auth/frontends/__init__.py b/django/contrib/auth/frontends/__init__.py
new file mode 100644
index 0000000..5d2e768
--- /dev/null
+++ b/django/contrib/auth/frontends/__init__.py
@@ -0,0 +1,75 @@
+from django.contrib.auth import REDIRECT_FIELD_NAME
+from django.http import HttpResponseRedirect
+from django.utils.http import urlquote
+
+class AuthenticationFrontend(object):
+    """
+    Generic authentication frontend with login decorator. The generic frontend
+    forwards the request directly to the given view function.
+    
+    Login decorator has a test function argument that performs a test on
+    the given user. The user is generally taken from request.user.  
+    """
+    def login_decorator(self, request, test_func, view_func, args, kwargs):
+        return view_func(request, *args, **kwargs)
+
+class BasicAuthenticationFrontend(AuthenticationFrontend):
+    """
+    Basic authentication frontend with user test. The class should not be used
+    directly
+    
+    The login decorator calls the given test function with a user taken from
+    the request. If the test passes, it calls the passed decorator; default
+    is to call the view function. If the test fails, the failed decorator is
+    called; default is to raise NotImplementedError exception.  
+    """
+    def login_decorator(self, request, test_func, view_func, args, kwargs):
+        if test_func(request.user):
+            return self.passed_decorator(request, test_func, view_func, args, kwargs)
+        else:
+            return self.failed_decorator(request, test_func, view_func, args, kwargs)
+    
+    def passed_decorator(self, request, test_func, view_func, args, kwargs):
+        "User has passed the authentication test"
+        return view_func(request, *args, **kwargs)
+
+    def failed_decorator(self, request, test_func, view_func, args, kwargs):
+        "User has failed the authentication test"
+        raise NotImplementedError("The failed authentication is not handled by default")
+
+class RedirectFrontend(BasicAuthenticationFrontend):
+    """
+    Authentication frontend that redirects user to the specified page if the
+    given authentication test fails. Both the login URL and redirect field name
+    has to be specified. 
+    
+    The user is redirected to the given login URL with redirect field name as
+    an argument name and original URL as a value:
+    
+        <login_url>&<redirect_field_name>=<original_url>
+    """
+    def __init__(self, login_url, redirect_field_name):
+        super(RedirectFrontend, self).__init__()
+        self.login_url = login_url
+        self.redirect_field_name = redirect_field_name
+
+    def failed_decorator(self, request, test_func, view_func, args, kwargs):
+        path = urlquote(request.get_full_path())
+        tup = self.login_url, self.redirect_field_name, path
+        return HttpResponseRedirect('%s?%s=%s' % tup)
+    
+class DefaultRedirectFrontend(RedirectFrontend):
+    """
+    Default authentication frontend that redirects user to the specified page
+    if the given authentication test fails. If the login URL is not specified, 
+    it is taken from the settings.LOGIN_URL, if the redirect field name is
+    missing, it is taken from django.contrib.auth.REDIRECT_FIELD_NAME.
+    """
+    def __init__(self, login_url=None, 
+                 redirect_field_name=REDIRECT_FIELD_NAME):
+        if not login_url:
+            from django.conf import settings
+            login_url = settings.LOGIN_URL
+        
+        super(DefaultRedirectFrontend, self).__init__(login_url, 
+                                                      redirect_field_name)
diff --git a/django/contrib/auth/frontends/login.py b/django/contrib/auth/frontends/login.py
new file mode 100644
index 0000000..37b72af
--- /dev/null
+++ b/django/contrib/auth/frontends/login.py
@@ -0,0 +1,122 @@
+import base64
+import md5
+import cPickle as pickle
+
+from django.conf import settings
+from django.contrib.auth import authenticate, login
+from django.contrib.auth.frontends import BasicAuthenticationFrontend
+from django.contrib.auth.models import User
+from django.http import HttpResponseRedirect
+from django.template import RequestContext
+from django.shortcuts import render_to_response
+from django.utils.translation import ugettext_lazy, ugettext as _
+
+ERROR_MESSAGE = ugettext_lazy("Please enter a correct username and password. Note that both fields are case-sensitive.")
+LOGIN_FORM_KEY = 'this_is_the_login_form'
+
+class LoginDialogFrontend(BasicAuthenticationFrontend):
+    def __init__(self, login_template_name):
+        super(LoginDialogFrontend, self).__init__()
+        self.login_template_name = login_template_name
+
+    def passed_decorator(self, request, test_func, view_func, args, kwargs):
+        "User has passed the authentication test"
+        post_data = self._get_stored_post_data(request)
+        if post_data is not None:
+            # User must have re-authenticated through a different window
+            # or tab.
+            request.POST = post_data
+        return super(LoginDialogFrontend, self).passed_decorator(request,
+                                                                 test_func,
+                                                                 view_func,
+                                                                 args,
+                                                                 kwargs)
+    
+    def failed_decorator(self, request, test_func, view_func, args, kwargs):
+        "User has failed the authentication test"
+        assert hasattr(request, 'session'), "The Django admin requires session middleware to be installed. Edit your MIDDLEWARE_CLASSES setting to insert 'django.contrib.sessions.middleware.SessionMiddleware'."
+
+        # If this isn't already the login page, display it.
+        if LOGIN_FORM_KEY not in request.POST:
+            if request.POST:
+                message = _("Please log in again, because your session has expired. Don't worry: Your submission has been saved.")
+            else:
+                message = ""
+            return self._display_login_form(request, message)
+
+        # Check that the user accepts cookies.
+        if not request.session.test_cookie_worked():
+            message = _("Looks like your browser isn't configured to accept cookies. Please enable cookies, reload this page, and try again.")
+            return self._display_login_form(request, message)
+
+        # Check the password.
+        username = request.POST.get('username', None)
+        password = request.POST.get('password', None)
+        user = authenticate(username=username, password=password)
+        if user is None:
+            message = ERROR_MESSAGE
+            if '@' in username:
+                # Mistakenly entered e-mail address instead of username? Look it up.
+                users = list(User.objects.filter(email=username))
+                if len(users) == 1:
+                    message = _("Your e-mail address is not your username. Try '%s' instead.") % users[0].username
+                else:
+                    # Either we cannot find the user, or if more than 1 
+                    # we cannot guess which user is the correct one.
+                    message = _("Usernames cannot contain the '@' character.")
+            return self._display_login_form(request, message)
+
+        # The user data is correct; log the user in and continue.
+        else:
+            if user.is_active and test_func(user):
+                login(request, user)
+                # TODO: set last_login with an event.
+                post_data = self._get_stored_post_data(request)
+                if post_data is not None and LOGIN_FORM_KEY not in post_data:
+                    # overwrite request.POST with the saved post_data, and continue
+                    request.POST = post_data
+                    return view_func(request, *args, **kwargs)
+                else:
+                    request.session.delete_test_cookie()
+                    return HttpResponseRedirect(request.path)
+            else:
+                return self._display_login_form(request, ERROR_MESSAGE)
+
+    def _display_login_form(self, request, error_message=''):
+        request.session.set_test_cookie()
+        post_data = self._get_stored_post_data(request, False)
+        if post_data is None:
+            if request.POST:
+                # User's session must have expired; save their post data.
+                post_data = self._encode_post_data(request.POST)
+            else:
+                post_data = self._encode_post_data({})
+        return render_to_response(self.login_template_name, {
+            'title': _('Log in'),
+            'app_path': request.path,
+            'post_data': post_data,
+            'error_message': error_message
+        }, context_instance=RequestContext(request))
+
+    def _get_stored_post_data(self, request, decode=True):
+        if request.POST and 'post_data' in request.POST:
+            # User has previously saved post data
+            if decode:
+                return self._decode_post_data(request.POST['post_data'])
+            else:
+                return request.POST['post_data']
+        else:
+            return None
+
+    def _encode_post_data(self, post_data):
+        pickled = pickle.dumps(post_data)
+        pickled_md5 = md5.new(pickled + settings.SECRET_KEY).hexdigest()
+        return base64.encodestring(pickled + pickled_md5)
+    
+    def _decode_post_data(self, encoded_data):
+        encoded_data = base64.decodestring(encoded_data)
+        pickled, tamper_check = encoded_data[:-32], encoded_data[-32:]
+        if md5.new(pickled + settings.SECRET_KEY).hexdigest() != tamper_check:
+            from django.core.exceptions import SuspiciousOperation
+            raise SuspiciousOperation, "User may have tampered with session cookie."
+        return pickle.loads(pickled)

Reply via email to