mirror of
https://github.com/django/django.git
synced 2024-11-29 22:56:46 +01:00
Merged multi-auth branch to trunk. See the authentication docs for the ramifications of this change. Many, many thanks to Joseph Kocherhans for the hard work!
git-svn-id: http://code.djangoproject.com/svn/django/trunk@3226 bcc190cf-cafb-0310-a4f2-bffc1f526a37
This commit is contained in:
parent
4ea7a11659
commit
aab3a418ac
@ -281,3 +281,9 @@ COMMENTS_FIRST_FEW = 0
|
||||
# A tuple of IP addresses that have been banned from participating in various
|
||||
# Django-powered features.
|
||||
BANNED_IPS = ()
|
||||
|
||||
##################
|
||||
# AUTHENTICATION #
|
||||
##################
|
||||
|
||||
AUTHENTICATION_BACKENDS = ('django.contrib.auth.backends.ModelBackend',)
|
||||
|
@ -1,6 +1,7 @@
|
||||
from django import http, template
|
||||
from django.conf import settings
|
||||
from django.contrib.auth.models import User, SESSION_KEY
|
||||
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 gettext_lazy
|
||||
import base64, datetime, md5
|
||||
@ -69,10 +70,10 @@ def staff_member_required(view_func):
|
||||
return _display_login_form(request, message)
|
||||
|
||||
# Check the password.
|
||||
username = request.POST.get('username', '')
|
||||
try:
|
||||
user = User.objects.get(username=username, is_staff=True)
|
||||
except User.DoesNotExist:
|
||||
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.
|
||||
@ -86,8 +87,9 @@ def staff_member_required(view_func):
|
||||
|
||||
# The user data is correct; log in the user in and continue.
|
||||
else:
|
||||
if user.check_password(request.POST.get('password', '')):
|
||||
request.session[SESSION_KEY] = user.id
|
||||
if user.is_staff:
|
||||
login(request, user)
|
||||
# TODO: set last_login with an event.
|
||||
user.last_login = datetime.datetime.now()
|
||||
user.save()
|
||||
if request.POST.has_key('post_data'):
|
||||
|
@ -1,2 +1,71 @@
|
||||
from django.core.exceptions import ImproperlyConfigured
|
||||
|
||||
SESSION_KEY = '_auth_user_id'
|
||||
BACKEND_SESSION_KEY = '_auth_user_backend'
|
||||
LOGIN_URL = '/accounts/login/'
|
||||
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)
|
||||
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, return a user object.
|
||||
"""
|
||||
for backend in get_backends():
|
||||
try:
|
||||
user = backend.authenticate(**credentials)
|
||||
except TypeError:
|
||||
# this backend doesn't accept these credentials as arguments, try the next one.
|
||||
continue
|
||||
if user is None:
|
||||
continue
|
||||
# annotate the user object with the path of the backend
|
||||
user.backend = str(backend.__class__)
|
||||
return user
|
||||
|
||||
def login(request, user):
|
||||
"""
|
||||
Persist a user id and a backend in the request. This way a user doesn't
|
||||
have to reauthenticate on every request.
|
||||
"""
|
||||
if user is None:
|
||||
user = request.user
|
||||
# TODO: It would be nice to support different login methods, like signed cookies.
|
||||
request.session[SESSION_KEY] = user.id
|
||||
request.session[BACKEND_SESSION_KEY] = user.backend
|
||||
|
||||
def logout(request):
|
||||
"""
|
||||
Remove the authenticated user's id from request.
|
||||
"""
|
||||
del request.session[SESSION_KEY]
|
||||
del request.session[BACKEND_SESSION_KEY]
|
||||
|
||||
def get_user(request):
|
||||
from django.contrib.auth.models import AnonymousUser
|
||||
try:
|
||||
user_id = request.session[SESSION_KEY]
|
||||
backend_path = request.session[BACKEND_SESSION_KEY]
|
||||
backend = load_backend(backend_path)
|
||||
user = backend.get_user(user_id) or AnonymousUser()
|
||||
except KeyError:
|
||||
user = AnonymousUser()
|
||||
return user
|
||||
|
21
django/contrib/auth/backends.py
Normal file
21
django/contrib/auth/backends.py
Normal file
@ -0,0 +1,21 @@
|
||||
from django.contrib.auth.models import User, check_password
|
||||
|
||||
class ModelBackend:
|
||||
"""
|
||||
Authenticate against django.contrib.auth.models.User
|
||||
"""
|
||||
# TODO: Model, login attribute name and password attribute name should be
|
||||
# configurable.
|
||||
def authenticate(self, username=None, password=None):
|
||||
try:
|
||||
user = User.objects.get(username=username)
|
||||
if user.check_password(password):
|
||||
return user
|
||||
except User.DoesNotExist:
|
||||
return None
|
||||
|
||||
def get_user(self, user_id):
|
||||
try:
|
||||
return User.objects.get(pk=user_id)
|
||||
except User.DoesNotExist:
|
||||
return None
|
@ -1,4 +1,5 @@
|
||||
from django.contrib.auth.models import User
|
||||
from django.contrib.auth import authenticate
|
||||
from django.contrib.sites.models import Site
|
||||
from django.template import Context, loader
|
||||
from django.core import validators
|
||||
@ -20,8 +21,7 @@ class AuthenticationForm(forms.Manipulator):
|
||||
self.fields = [
|
||||
forms.TextField(field_name="username", length=15, maxlength=30, is_required=True,
|
||||
validator_list=[self.isValidUser, self.hasCookiesEnabled]),
|
||||
forms.PasswordField(field_name="password", length=15, maxlength=30, is_required=True,
|
||||
validator_list=[self.isValidPasswordForUser]),
|
||||
forms.PasswordField(field_name="password", length=15, maxlength=30, is_required=True),
|
||||
]
|
||||
self.user_cache = None
|
||||
|
||||
@ -30,16 +30,10 @@ class AuthenticationForm(forms.Manipulator):
|
||||
raise validators.ValidationError, _("Your Web browser doesn't appear to have cookies enabled. Cookies are required for logging in.")
|
||||
|
||||
def isValidUser(self, field_data, all_data):
|
||||
try:
|
||||
self.user_cache = User.objects.get(username=field_data)
|
||||
except User.DoesNotExist:
|
||||
raise validators.ValidationError, _("Please enter a correct username and password. Note that both fields are case-sensitive.")
|
||||
|
||||
def isValidPasswordForUser(self, field_data, all_data):
|
||||
username = field_data
|
||||
password = all_data.get('password', None)
|
||||
self.user_cache = authenticate(username=username, password=password)
|
||||
if self.user_cache is None:
|
||||
return
|
||||
if not self.user_cache.check_password(field_data):
|
||||
self.user_cache = None
|
||||
raise validators.ValidationError, _("Please enter a correct username and password. Note that both fields are case-sensitive.")
|
||||
elif not self.user_cache.is_active:
|
||||
raise validators.ValidationError, _("This account is inactive.")
|
||||
|
@ -4,12 +4,8 @@ class LazyUser(object):
|
||||
|
||||
def __get__(self, request, obj_type=None):
|
||||
if self._user is None:
|
||||
from django.contrib.auth.models import User, AnonymousUser, SESSION_KEY
|
||||
try:
|
||||
user_id = request.session[SESSION_KEY]
|
||||
self._user = User.objects.get(pk=user_id)
|
||||
except (KeyError, User.DoesNotExist):
|
||||
self._user = AnonymousUser()
|
||||
from django.contrib.auth import get_user
|
||||
self._user = get_user(request)
|
||||
return self._user
|
||||
|
||||
class AuthenticationMiddleware(object):
|
||||
|
@ -4,7 +4,19 @@ from django.contrib.contenttypes.models import ContentType
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
import datetime
|
||||
|
||||
SESSION_KEY = '_auth_user_id'
|
||||
def check_password(raw_password, enc_password):
|
||||
"""
|
||||
Returns a boolean of whether the raw_password was correct. Handles
|
||||
encryption formats behind the scenes.
|
||||
"""
|
||||
algo, salt, hsh = enc_password.split('$')
|
||||
if algo == 'md5':
|
||||
import md5
|
||||
return hsh == md5.new(salt+raw_password).hexdigest()
|
||||
elif algo == 'sha1':
|
||||
import sha
|
||||
return hsh == sha.new(salt+raw_password).hexdigest()
|
||||
raise ValueError, "Got unknown password algorithm type in password."
|
||||
|
||||
class SiteProfileNotAvailable(Exception):
|
||||
pass
|
||||
@ -141,14 +153,7 @@ class User(models.Model):
|
||||
self.set_password(raw_password)
|
||||
self.save()
|
||||
return is_correct
|
||||
algo, salt, hsh = self.password.split('$')
|
||||
if algo == 'md5':
|
||||
import md5
|
||||
return hsh == md5.new(salt+raw_password).hexdigest()
|
||||
elif algo == 'sha1':
|
||||
import sha
|
||||
return hsh == sha.new(salt+raw_password).hexdigest()
|
||||
raise ValueError, "Got unknown password algorithm type in password."
|
||||
return check_password(raw_password, self.password)
|
||||
|
||||
def get_group_permissions(self):
|
||||
"Returns a list of permission strings that this user has through his/her groups."
|
||||
|
@ -3,7 +3,6 @@ from django.contrib.auth.forms import PasswordResetForm, PasswordChangeForm
|
||||
from django import forms
|
||||
from django.shortcuts import render_to_response
|
||||
from django.template import RequestContext
|
||||
from django.contrib.auth.models import SESSION_KEY
|
||||
from django.contrib.sites.models import Site
|
||||
from django.http import HttpResponse, HttpResponseRedirect
|
||||
from django.contrib.auth.decorators import login_required
|
||||
@ -19,7 +18,8 @@ def login(request, template_name='registration/login.html'):
|
||||
# Light security check -- make sure redirect_to isn't garbage.
|
||||
if not redirect_to or '://' in redirect_to or ' ' in redirect_to:
|
||||
redirect_to = '/accounts/profile/'
|
||||
request.session[SESSION_KEY] = manipulator.get_user_id()
|
||||
from django.contrib.auth import login
|
||||
login(request, manipulator.get_user())
|
||||
request.session.delete_test_cookie()
|
||||
return HttpResponseRedirect(redirect_to)
|
||||
else:
|
||||
@ -33,8 +33,9 @@ def login(request, template_name='registration/login.html'):
|
||||
|
||||
def logout(request, next_page=None, template_name='registration/logged_out.html'):
|
||||
"Logs out the user and displays 'You are logged out' message."
|
||||
from django.contrib.auth import logout
|
||||
try:
|
||||
del request.session[SESSION_KEY]
|
||||
logout(request)
|
||||
except KeyError:
|
||||
return render_to_response(template_name, {'title': _('Logged out')}, context_instance=RequestContext(request))
|
||||
else:
|
||||
|
@ -5,7 +5,6 @@ from django.http import Http404
|
||||
from django.core.exceptions import ObjectDoesNotExist
|
||||
from django.shortcuts import render_to_response
|
||||
from django.template import RequestContext
|
||||
from django.contrib.auth.models import SESSION_KEY
|
||||
from django.contrib.comments.models import Comment, FreeComment, PHOTOS_REQUIRED, PHOTOS_OPTIONAL, RATINGS_REQUIRED, RATINGS_OPTIONAL, IS_PUBLIC
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.contrib.auth.forms import AuthenticationForm
|
||||
@ -219,7 +218,8 @@ def post_comment(request):
|
||||
# If user gave correct username/password and wasn't already logged in, log them in
|
||||
# so they don't have to enter a username/password again.
|
||||
if manipulator.get_user() and new_data.has_key('password') and manipulator.get_user().check_password(new_data['password']):
|
||||
request.session[SESSION_KEY] = manipulator.get_user_id()
|
||||
from django.contrib.auth import login
|
||||
login(request, manipulator.get_user())
|
||||
if errors or request.POST.has_key('preview'):
|
||||
class CommentFormWrapper(forms.FormWrapper):
|
||||
def __init__(self, manipulator, new_data, errors, rating_choices):
|
||||
|
@ -267,17 +267,25 @@ previous section). You can tell them apart with ``is_anonymous()``, like so::
|
||||
How to log a user in
|
||||
--------------------
|
||||
|
||||
To log a user in, do the following within a view::
|
||||
Depending on your task, you'll probably want to make sure to validate the
|
||||
user's username and password before you log them in. The easiest way to do so
|
||||
is to use the built-in ``authenticate`` and ``login`` functions from within a
|
||||
view::
|
||||
|
||||
from django.contrib.auth.models import SESSION_KEY
|
||||
request.session[SESSION_KEY] = some_user.id
|
||||
from django.contrib.auth import authenticate, login
|
||||
username = request.POST['username']
|
||||
password = request.POST['password']
|
||||
user = authenticate(username=username, password=password)
|
||||
if user is not None:
|
||||
login(request, user)
|
||||
|
||||
Because this uses sessions, you'll need to make sure you have
|
||||
``SessionMiddleware`` enabled. See the `session documentation`_ for more
|
||||
information.
|
||||
``authenticate`` checks the username and password. If they are valid it
|
||||
returns a user object, otherwise it returns ``None``. ``login`` makes it so
|
||||
your users don't have send a username and password for every request. Because
|
||||
the ``login`` function uses sessions, you'll need to make sure you have
|
||||
``SessionMiddleware`` enabled. See the `session documentation`_ for
|
||||
more information.
|
||||
|
||||
This assumes ``some_user`` is your ``User`` instance. Depending on your task,
|
||||
you'll probably want to make sure to validate the user's username and password.
|
||||
|
||||
Limiting access to logged-in users
|
||||
----------------------------------
|
||||
@ -672,3 +680,84 @@ Finally, note that this messages framework only works with users in the user
|
||||
database. To send messages to anonymous users, use the `session framework`_.
|
||||
|
||||
.. _session framework: http://www.djangoproject.com/documentation/sessions/
|
||||
|
||||
Other Authentication Sources
|
||||
============================
|
||||
|
||||
Django supports other authentication sources as well. You can even use
|
||||
multiple sources at the same time.
|
||||
|
||||
Using multiple backends
|
||||
-----------------------
|
||||
|
||||
The list of backends to use is controlled by the ``AUTHENTICATION_BACKENDS``
|
||||
setting. This should be a tuple of python path names. It defaults to
|
||||
``('django.contrib.auth.backends.ModelBackend',)``. To add additional backends
|
||||
just add them to your settings.py file. Ordering matters, so if the same
|
||||
username and password is valid in multiple backends, the first one in the
|
||||
list will return a user object, and the remaining ones won't even get a chance.
|
||||
|
||||
Writing an authentication backend
|
||||
---------------------------------
|
||||
|
||||
An authentication backend is a class that implements 2 methods:
|
||||
``get_user(id)`` and ``authenticate(**credentials)``. The ``get_user`` method
|
||||
takes an id, which could be a username, and database id, whatever, and returns
|
||||
a user object. The ``authenticate`` method takes credentials as keyword
|
||||
arguments. Many times it will just look like this::
|
||||
|
||||
class MyBackend:
|
||||
def authenticate(username=None, password=None):
|
||||
# check the username/password and return a user
|
||||
|
||||
but it could also authenticate a token like so::
|
||||
|
||||
class MyBackend:
|
||||
def authenticate(token=None):
|
||||
# check the token and return a user
|
||||
|
||||
Regardless, ``authenticate`` should check the credentials it gets, and if they
|
||||
are valid, it should return a user object that matches those credentials.
|
||||
|
||||
The Django admin system is tightly coupled to the Django User object described
|
||||
at the beginning of this document. For now, the best way to deal with this is
|
||||
to create a Django User object for each user that exists for your backend
|
||||
(i.e. in your LDAP directory, your external SQL database, etc.) You can either
|
||||
write a script to do this in advance, or your ``authenticate`` method can do
|
||||
it the first time a user logs in. Here's an example backend that
|
||||
authenticates against a username and password variable defined in your
|
||||
``settings.py`` file and creates a Django user object the first time they
|
||||
authenticate::
|
||||
|
||||
from django.conf import settings
|
||||
from django.contrib.auth.models import User, check_password
|
||||
|
||||
class SettingsBackend:
|
||||
"""
|
||||
Authenticate against vars in settings.py Use the login name, and a hash
|
||||
of the password. For example:
|
||||
|
||||
ADMIN_LOGIN = 'admin'
|
||||
ADMIN_PASSWORD = 'sha1$4e987$afbcf42e21bd417fb71db8c66b321e9fc33051de'
|
||||
"""
|
||||
def authenticate(self, username=None, password=None):
|
||||
login_valid = (settings.ADMIN_LOGIN == username)
|
||||
pwd_valid = check_password(password, settings.ADMIN_PASSWORD)
|
||||
if login_valid and pwd_valid:
|
||||
try:
|
||||
user = User.objects.get(username=username)
|
||||
except User.DoesNotExist:
|
||||
# Create a new user. Note that we can set password to anything
|
||||
# as it won't be checked, the password from settings.py will.
|
||||
user = User(username=username, password='get from settings.py')
|
||||
user.is_staff = True
|
||||
user.is_superuser = True
|
||||
user.save()
|
||||
return user
|
||||
return None
|
||||
|
||||
def get_user(self, user_id):
|
||||
try:
|
||||
return User.objects.get(pk=user_id)
|
||||
except User.DoesNotExist:
|
||||
return None
|
||||
|
Loading…
Reference in New Issue
Block a user