Code

Ticket #1428: multiauth.diff

File multiauth.diff, 20.0 KB (added by jkocherhans, 8 years ago)

works with post-mr trunk now... rev [2849] to be exact

Line 
1Index: django/conf/global_settings.py
2===================================================================
3--- django/conf/global_settings.py      (revision 2849)
4+++ django/conf/global_settings.py      (working copy)
5@@ -265,3 +265,11 @@
6 # A tuple of IP addresses that have been banned from participating in various
7 # Django-powered features.
8 BANNED_IPS = ()
9+
10+##################
11+# AUTHENTICATION #
12+##################
13+
14+AUTHENTICATION_BACKENDS = ('django.contrib.auth.backends.ModelBackend',)
15+CREDENTIAL_PLUGINS = ('django.contrib.auth.credentials.username_password_form',)
16+
17Index: django/contrib/auth/backends.py
18===================================================================
19--- django/contrib/auth/backends.py     (revision 0)
20+++ django/contrib/auth/backends.py     (revision 0)
21@@ -0,0 +1,52 @@
22+from django.conf import settings
23+from django.contrib.auth.models import User
24+from django.contrib.auth.utils import check_password
25+
26+class SettingsBackend:
27+    """
28+    Authenticate against vars in settings.py Use the login name, and a hash
29+    of the password.
30+   
31+    ADMIN_LOGIN = 'admin'
32+    ADMIN_PASSWORD = 'sha1$4e987$afbcf42e21bd417fb71db8c66b321e9fc33051de'
33+    """
34+    def authenticate(self, username=None, password=None):
35+        login_valid = (settings.ADMIN_LOGIN == username)
36+        pwd_valid = check_password(password, settings.ADMIN_PASSWORD)
37+        if login_valid and pwd_valid:
38+            # TODO: This should be abstracted out someplace else.
39+            try:
40+                user = User.objects.get(username=username)
41+            except User.DoesNotExist:
42+                user = User(username=username, password='')
43+                user.is_staff = True
44+                user.is_superuser = True
45+                user.save()
46+            return user
47+        return None
48+
49+    def get_user(self, user_id):
50+        try:
51+            return User.objects.get(pk=user_id)
52+        except User.DoesNotExist:
53+            return None
54+
55+class ModelBackend:
56+    """
57+    Authenticate against django.contrib.auth.models.User
58+    """
59+    # TODO: Model, login attribute name and password attribute name should be
60+    # configurable.
61+    def authenticate(self, username=None, password=None):
62+        try:
63+            user = User.objects.get(username=username)
64+            if user.check_password(password):
65+                return user
66+        except User.DoesNotExist:
67+            return None
68+
69+    def get_user(self, user_id):
70+        try:
71+            return User.objects.get(pk=user_id)
72+        except User.DoesNotExist:
73+            return None
74Index: django/contrib/auth/middleware.py
75===================================================================
76--- django/contrib/auth/middleware.py   (revision 2849)
77+++ django/contrib/auth/middleware.py   (working copy)
78@@ -4,12 +4,8 @@
79 
80     def __get__(self, request, obj_type=None):
81         if self._user is None:
82-            from django.contrib.auth.models import User, AnonymousUser, SESSION_KEY
83-            try:
84-                user_id = request.session[SESSION_KEY]
85-                self._user = User.objects.get(pk=user_id)
86-            except (KeyError, User.DoesNotExist):
87-                self._user = AnonymousUser()
88+            from django.contrib.auth import get_current_user
89+            self._user = get_current_user(request)
90         return self._user
91 
92 class AuthenticationMiddleware:
93Index: django/contrib/auth/views.py
94===================================================================
95--- django/contrib/auth/views.py        (revision 2849)
96+++ django/contrib/auth/views.py        (working copy)
97@@ -3,7 +3,6 @@
98 from django import forms
99 from django.shortcuts import render_to_response
100 from django.template import RequestContext
101-from django.contrib.auth.models import SESSION_KEY
102 from django.contrib.sites.models import Site
103 from django.http import HttpResponse, HttpResponseRedirect
104 from django.contrib.auth.decorators import login_required
105@@ -19,7 +18,8 @@
106             # Light security check -- make sure redirect_to isn't garbage.
107             if not redirect_to or '://' in redirect_to or ' ' in redirect_to:
108                 redirect_to = '/accounts/profile/'
109-            request.session[SESSION_KEY] = manipulator.get_user_id()
110+            from django.contrib.auth import login
111+            login(request, manipulator.get_user())
112             request.session.delete_test_cookie()
113             return HttpResponseRedirect(redirect_to)
114     else:
115@@ -33,8 +33,9 @@
116 
117 def logout(request, next_page=None):
118     "Logs out the user and displays 'You are logged out' message."
119+    from django.contrib.auth import logout
120     try:
121-        del request.session[SESSION_KEY]
122+        logout(request)
123     except KeyError:
124         return render_to_response('registration/logged_out.html', {'title': 'Logged out'}, context_instance=RequestContext(request))
125     else:
126Index: django/contrib/auth/credentials.py
127===================================================================
128--- django/contrib/auth/credentials.py  (revision 0)
129+++ django/contrib/auth/credentials.py  (revision 0)
130@@ -0,0 +1,13 @@
131+def username_password_form(request):
132+    try:
133+        username = request.POST['username']
134+        password = request.POST['password']
135+        return {'username': username, 'password': password}
136+    except KeyError:
137+        return None
138+
139+def token(request):
140+    try:
141+        return request.POST['token']
142+    except KeyError:
143+        return None
144Index: django/contrib/auth/__init__.py
145===================================================================
146--- django/contrib/auth/__init__.py     (revision 2849)
147+++ django/contrib/auth/__init__.py     (working copy)
148@@ -1,2 +1,115 @@
149+from django.core.exceptions import ImproperlyConfigured
150+
151+SESSION_KEY = '_auth_user_id'
152+BACKEND_SESSION_KEY = '_auth_user_backend'
153 LOGIN_URL = '/accounts/login/'
154 REDIRECT_FIELD_NAME = 'next'
155+
156+def load_plugin(path):
157+    i = path.rfind('.')
158+    module, attr = path[:i], path[i+1:]
159+    try:
160+        mod = __import__(module, '', '', [attr])
161+    except ImportError, e:
162+        raise ImproperlyConfigured, 'Error importing credential plugin %s: "%s"' % (module, e)
163+    try:
164+        func = getattr(mod, attr)
165+    except AttributeError:
166+        raise ImproperlyConfigured, 'Module "%s" does not define a "%s" credential plugin' % (module, attr)
167+    return func
168+
169+def load_backend(path):
170+    i = path.rfind('.')
171+    module, attr = path[:i], path[i+1:]
172+    try:
173+        mod = __import__(module, '', '', [attr])
174+    except ImportError, e:
175+        raise ImproperlyConfigured, 'Error importing authentication backend %s: "%s"' % (module, e)
176+    try:
177+        cls = getattr(mod, attr)
178+    except AttributeError:
179+        raise ImproperlyConfigured, 'Module "%s" does not define a "%s" authentication backend' % (module, attr)
180+    return cls()
181+
182+def get_backends():
183+    from django.conf import settings
184+    backends = []
185+    for backend_path in settings.AUTHENTICATION_BACKENDS:
186+        backends.append(load_backend(backend_path))
187+    return backends
188+
189+def get_credential_plugins():
190+    from django.conf import settings
191+    credential_plugins = []
192+    for plugin_path in settings.CREDENTIAL_PLUGINS:
193+        credential_plugins.append(load_plugin(plugin_path))
194+    return credential_plugins
195+       
196+def authenticate_credentials(**credentials):
197+    """
198+    If the given credentials, return a user object.
199+    """
200+    for backend in get_backends():
201+        try:
202+            user = backend.authenticate(**credentials)
203+        except TypeError:
204+            # this backend doesn't accept these credentials as arguments, try the next one.
205+            continue
206+        if user is None:
207+            continue
208+        # annotate the user object with the path of the backend
209+        user.backend = str(backend.__class__)
210+        return user
211+
212+def authenticate_request(request):
213+    """
214+    Use CREDENTIAL_PLUGINS to find credentials in the request and try to
215+    authenticate them.
216+    """
217+    for plugin in get_credential_plugins():
218+        credentials = plugin(request)
219+        if credentials is None:
220+            continue
221+        user = authenticate_credentials(**credentials)
222+        if user is None:
223+            continue
224+        return user
225+
226+def login(request, user=None):
227+    """
228+    Persist a user id and a backend in the request. This way a user doesn't
229+    have to reauthenticate on every request.
230+    """
231+    if user is None:
232+        user = request.user
233+    # TODO: It would be nice to support different login methods, like signed cookies.
234+    request.session[SESSION_KEY] = user.id
235+    request.session[BACKEND_SESSION_KEY] = user.backend
236+
237+def authenticate_request_and_login(request):
238+    """
239+    Convenience function to authenticate a request and log a user in. Returns
240+    the user object, or None if authentication failed.
241+    """
242+    user = authenticate_request(request)
243+    if user is not None:
244+        login(request, user)
245+    return user
246+
247+def logout(request):
248+    """
249+    Remove the authenticated user's id from request.
250+    """
251+    del request.session[SESSION_KEY]
252+    del request.session[BACKEND_SESSION_KEY]
253+
254+def get_current_user(request):
255+    from django.contrib.auth.models import AnonymousUser
256+    try:
257+        user_id = request.session[SESSION_KEY]
258+        backend_path = request.session[BACKEND_SESSION_KEY]
259+        backend = load_backend(backend_path)
260+        user = backend.get_user(user_id) or AnonymousUser()
261+    except KeyError:
262+        user = AnonymousUser()
263+    return user
264Index: django/contrib/auth/utils.py
265===================================================================
266--- django/contrib/auth/utils.py        (revision 0)
267+++ django/contrib/auth/utils.py        (revision 0)
268@@ -0,0 +1,26 @@
269+def encrypt_password(raw_password):
270+    import sha, random
271+    algo = 'sha1'
272+    salt = sha.new(str(random.random())).hexdigest()[:5]
273+    hsh = sha.new(salt+raw_password).hexdigest()
274+    return '%s$%s$%s' % (algo, salt, hsh)
275+
276+def check_password(raw_password, enc_password):
277+    """
278+    Returns a boolean of whether the raw_password was correct. Handles
279+    encryption formats behind the scenes.
280+    """
281+    # Backwards-compatibility check. Older passwords won't include the
282+    # algorithm or salt.
283+    if '$' not in enc_password:
284+        import md5
285+        return enc_password == md5.new(raw_password).hexdigest()
286+    algo, salt, hsh = enc_password.split('$')
287+    if algo == 'md5':
288+        import md5
289+        return hsh == md5.new(salt+raw_password).hexdigest()
290+    elif algo == 'sha1':
291+        import sha
292+        return hsh == sha.new(salt+raw_password).hexdigest()
293+    raise ValueError, "Got unknown password algorithm type in password."
294+
295Index: django/contrib/auth/models.py
296===================================================================
297--- django/contrib/auth/models.py       (revision 2849)
298+++ django/contrib/auth/models.py       (working copy)
299@@ -4,8 +4,6 @@
300 from django.utils.translation import gettext_lazy as _
301 import datetime
302 
303-SESSION_KEY = '_auth_user_id'
304-
305 class SiteProfileNotAvailable(Exception):
306     pass
307 
308Index: django/contrib/auth/forms.py
309===================================================================
310--- django/contrib/auth/forms.py        (revision 2849)
311+++ django/contrib/auth/forms.py        (working copy)
312@@ -1,4 +1,5 @@
313 from django.contrib.auth.models import User
314+from django.contrib.auth import authenticate_request
315 from django.contrib.sites.models import Site
316 from django.template import Context, loader
317 from django.core import validators
318@@ -20,8 +21,7 @@
319         self.fields = [
320             forms.TextField(field_name="username", length=15, maxlength=30, is_required=True,
321                 validator_list=[self.isValidUser, self.hasCookiesEnabled]),
322-            forms.PasswordField(field_name="password", length=15, maxlength=30, is_required=True,
323-                validator_list=[self.isValidPasswordForUser]),
324+            forms.PasswordField(field_name="password", length=15, maxlength=30, is_required=True),
325         ]
326         self.user_cache = None
327 
328@@ -30,16 +30,10 @@
329             raise validators.ValidationError, _("Your Web browser doesn't appear to have cookies enabled. Cookies are required for logging in.")
330 
331     def isValidUser(self, field_data, all_data):
332-        try:
333-            self.user_cache = User.objects.get(username=field_data)
334-        except User.DoesNotExist:
335+        self.user_cache = authenticate_request(self.request)
336+        if self.user_cache is None:
337             raise validators.ValidationError, _("Please enter a correct username and password. Note that both fields are case-sensitive.")
338 
339-    def isValidPasswordForUser(self, field_data, all_data):
340-        if self.user_cache is not None and not self.user_cache.check_password(field_data):
341-            self.user_cache = None
342-            raise validators.ValidationError, _("Please enter a correct username and password. Note that both fields are case-sensitive.")
343-
344     def get_user_id(self):
345         if self.user_cache:
346             return self.user_cache.id
347Index: django/contrib/comments/views/comments.py
348===================================================================
349--- django/contrib/comments/views/comments.py   (revision 2849)
350+++ django/contrib/comments/views/comments.py   (working copy)
351@@ -5,7 +5,6 @@
352 from django.core.exceptions import ObjectDoesNotExist
353 from django.shortcuts import render_to_response
354 from django.template import RequestContext
355-from django.contrib.auth.models import SESSION_KEY
356 from django.contrib.comments.models import Comment, FreeComment, PHOTOS_REQUIRED, PHOTOS_OPTIONAL, RATINGS_REQUIRED, RATINGS_OPTIONAL, IS_PUBLIC
357 from django.contrib.contenttypes.models import ContentType
358 from django.contrib.auth.forms import AuthenticationForm
359@@ -219,7 +218,8 @@
360     # If user gave correct username/password and wasn't already logged in, log them in
361     # so they don't have to enter a username/password again.
362     if manipulator.get_user() and new_data.has_key('password') and manipulator.get_user().check_password(new_data['password']):
363-        request.session[SESSION_KEY] = manipulator.get_user_id()
364+        from django.contrib.auth import login
365+        login(request, manipulator.get_user())
366     if errors or request.POST.has_key('preview'):
367         class CommentFormWrapper(forms.FormWrapper):
368             def __init__(self, manipulator, new_data, errors, rating_choices):
369Index: django/contrib/admin/views/decorators.py
370===================================================================
371--- django/contrib/admin/views/decorators.py    (revision 2849)
372+++ django/contrib/admin/views/decorators.py    (working copy)
373@@ -1,6 +1,7 @@
374 from django import http, template
375 from django.conf import settings
376-from django.contrib.auth.models import User, SESSION_KEY
377+from django.contrib.auth.models import User
378+from django.contrib.auth import authenticate_request, login
379 from django.shortcuts import render_to_response
380 from django.utils.translation import gettext_lazy
381 import base64, datetime, md5
382@@ -69,11 +70,10 @@
383             return _display_login_form(request, message)
384 
385         # Check the password.
386-        username = request.POST.get('username', '')
387-        try:
388-            user = User.objects.get(username=username, is_staff=True)
389-        except User.DoesNotExist:
390+        user = authenticate_request(request)
391+        if user is None:
392             message = ERROR_MESSAGE
393+            username = request.POST.get('username', '')
394             if '@' in username:
395                 # Mistakenly entered e-mail address instead of username? Look it up.
396                 try:
397@@ -86,8 +86,9 @@
398 
399         # The user data is correct; log in the user in and continue.
400         else:
401-            if user.check_password(request.POST.get('password', '')):
402-                request.session[SESSION_KEY] = user.id
403+            if user.is_staff:
404+                login(request, user)
405+                # TODO: set last_login with an event.
406                 user.last_login = datetime.datetime.now()
407                 user.save()
408                 if request.POST.has_key('post_data'):
409Index: docs/authentication.txt
410===================================================================
411--- docs/authentication.txt     (revision 2849)
412+++ docs/authentication.txt     (working copy)
413@@ -269,15 +269,15 @@
414 
415 To log a user in, do the following within a view::
416 
417-    from django.contrib.auth.models import SESSION_KEY
418-    request.session[SESSION_KEY] = some_user.id
419+    from django.contrib.auth import login
420+    login(request)
421 
422-Because this uses sessions, you'll need to make sure you have
423+Because the login functions uses sessions, you'll need to make sure you have
424 ``SessionMiddleware`` enabled. See the `session documentation`_ for more
425 information.
426 
427-This assumes ``some_user`` is your ``User`` instance. Depending on your task,
428-you'll probably want to make sure to validate the user's username and password.
429+Depending on your task, you'll probably want to make sure to validate the
430+user's username and password before you log them in.
431 
432 Limiting access to logged-in users
433 ----------------------------------
434@@ -611,3 +611,73 @@
435 database. To send messages to anonymous users, use the `session framework`_.
436 
437 .. _session framework: http://www.djangoproject.com/documentation/sessions/
438+
439+Other Authentication Sources
440+============================
441+
442+Django supports other authentication sources as well. You can even use
443+multiple sources at the same time.
444+
445+Using multiple backends
446+-----------------------
447+
448+The list of backends to use is controlled by the ``AUTHENTICATION_BACKENDS``
449+setting. This should be a tuple of python path names. It defaults to
450+``('django.contrib.auth.backends.ModelBackend',)``. To add additional backends
451+just add them to your settings.py file. Ordering matters, so if the same
452+username and password is valid in multiple backends, the first one in the
453+list will return a user object, and the remaining ones won't even get a chance.
454+
455+
456+Customizing ModelBackend
457+------------------------
458+
459+TODO: write me
460+
461+
462+Writing an authentication backend
463+---------------------------------
464+
465+An authentication backend is a class that implements 2 methods: ``get_user(id)``
466+and ``authenticate(**credentials)``. The ``get_user`` method takes an id, which
467+could be a username, and database id, whatever, and returns a user object. The
468+``authenticate`` method takes credentials as keyword arguments. Many times it
469+will just look like this::
470+
471+    class MyBackend:
472+        def authenticate(username=None, password=None):
473+            # check the username/password and return a user
474+
475+but it could also authenticate a token like so::
476+
477+    class MyBackend:
478+        def authenticate(token=None):
479+            # check the token and return a user
480+
481+Regardless, ``authenticate`` should check the credentials it gets, and if they
482+are valid, it should return a user object that matches those credentials.
483+
484+The Django admin system is tightly coupled to the Django User object described
485+at the beginning of this document. For now, the best way to deal with this is to
486+create a Django User object for each user that exists for your backend (i.e.
487+in your ldap directory, your external sql database, etc.) You can either
488+write a script to do this in advance, or your ``authenticate`` method can do
489+it the first time a user logs in. `django.contrib.auth.backends.SettingsBackend`_
490+is an example of the latter approach. Note that you don't have to save a user's
491+password in the Django User object. Your backend can still check the password
492+against an external source, and return a Django User object.
493+
494+.. _django.contrib.auth.backends.SettingsBackend: http://code.djangoproject.com/browser/django/branches/magic-removal/django/contrib/auth/backends.py
495+
496+Credential Plugins
497+==================
498+
499+Like metaclasses, 99.999% of people should never need to bother with these.
500+I'm not sure they're even worth keeping... at least conceptually. The work
501+they do is probably best done in a few utility functions that get used in
502+decorators or views. Basically, they allow using different types of
503+credentials for the same view or url. The main use I can think of is REST, or
504+changing your whole app from form/cookie auth, to http basic or digest auth. I
505+don't think that will happen very often, if ever.
506+
507+TODO: write more... maybe
508\ No newline at end of file