Fixes #11025 -- ability to specify LOGIN_URL as full qualified absolute URL.

auth.views.login now allows for login redirections for different schemes
with the same host (or no host even, e.g. 'https:///login/')

auth.decorators.login_required can now use lazy urls (refs #5925)

git-svn-id: http://code.djangoproject.com/svn/django/trunk@14733 bcc190cf-cafb-0310-a4f2-bffc1f526a37
This commit is contained in:
Chris Beaven 2010-11-27 22:43:33 +00:00
parent 9d3b3d11f4
commit e74edb4d53
4 changed files with 112 additions and 37 deletions

View File

@ -1,12 +1,12 @@
import urlparse
try: try:
from functools import update_wrapper, wraps from functools import wraps
except ImportError: except ImportError:
from django.utils.functional import update_wrapper, wraps # Python 2.4 fallback. from django.utils.functional import wraps # Python 2.4 fallback.
from django.conf import settings
from django.contrib.auth import REDIRECT_FIELD_NAME from django.contrib.auth import REDIRECT_FIELD_NAME
from django.http import HttpResponseRedirect
from django.utils.decorators import available_attrs from django.utils.decorators import available_attrs
from django.utils.http import urlquote
def user_passes_test(test_func, login_url=None, redirect_field_name=REDIRECT_FIELD_NAME): def user_passes_test(test_func, login_url=None, redirect_field_name=REDIRECT_FIELD_NAME):
@ -15,18 +15,24 @@ def user_passes_test(test_func, login_url=None, redirect_field_name=REDIRECT_FIE
redirecting to the log-in page if necessary. The test should be a callable 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. that takes the user object and returns True if the user passes.
""" """
if not login_url:
from django.conf import settings
login_url = settings.LOGIN_URL
def decorator(view_func): def decorator(view_func):
@wraps(view_func, assigned=available_attrs(view_func))
def _wrapped_view(request, *args, **kwargs): def _wrapped_view(request, *args, **kwargs):
if test_func(request.user): if test_func(request.user):
return view_func(request, *args, **kwargs) return view_func(request, *args, **kwargs)
path = urlquote(request.get_full_path()) path = request.build_absolute_uri()
tup = login_url, redirect_field_name, path # If the login url is the same scheme and net location then just
return HttpResponseRedirect('%s?%s=%s' % tup) # use the path as the "next" url.
return wraps(view_func, assigned=available_attrs(view_func))(_wrapped_view) login_scheme, login_netloc = urlparse.urlparse(login_url or
settings.LOGIN_URL)[:2]
current_scheme, current_netloc = urlparse.urlparse(path)[:2]
if ((not login_scheme or login_scheme == current_scheme) and
(not login_netloc or login_netloc == current_netloc)):
path = request.get_full_path()
from django.contrib.auth.views import redirect_to_login
return redirect_to_login(path, login_url, redirect_field_name)
return _wrapped_view
return decorator return decorator

View File

@ -7,7 +7,7 @@ from django.contrib.auth.tests.remote_user \
from django.contrib.auth.tests.models import ProfileTestCase from django.contrib.auth.tests.models import ProfileTestCase
from django.contrib.auth.tests.signals import SignalTestCase from django.contrib.auth.tests.signals import SignalTestCase
from django.contrib.auth.tests.tokens import TokenGeneratorTest from django.contrib.auth.tests.tokens import TokenGeneratorTest
from django.contrib.auth.tests.views \ from django.contrib.auth.tests.views import PasswordResetTest, \
import PasswordResetTest, ChangePasswordTest, LoginTest, LogoutTest ChangePasswordTest, LoginTest, LogoutTest, LoginURLSettings
# The password for the fixture data users is 'password' # The password for the fixture data users is 'password'

View File

@ -5,11 +5,12 @@ import urllib
from django.conf import settings from django.conf import settings
from django.contrib.auth import SESSION_KEY, REDIRECT_FIELD_NAME from django.contrib.auth import SESSION_KEY, REDIRECT_FIELD_NAME
from django.contrib.auth.forms import AuthenticationForm from django.contrib.auth.forms import AuthenticationForm
from django.contrib.sites.models import Site, RequestSite from django.contrib.sites.models import Site
from django.contrib.auth.models import User from django.contrib.auth.models import User
from django.test import TestCase from django.test import TestCase
from django.core import mail from django.core import mail
from django.core.urlresolvers import reverse from django.core.urlresolvers import reverse
from django.http import QueryDict
class AuthViewsTestCase(TestCase): class AuthViewsTestCase(TestCase):
""" """
@ -25,11 +26,8 @@ class AuthViewsTestCase(TestCase):
settings.LANGUAGE_CODE = 'en' settings.LANGUAGE_CODE = 'en'
self.old_TEMPLATE_DIRS = settings.TEMPLATE_DIRS self.old_TEMPLATE_DIRS = settings.TEMPLATE_DIRS
settings.TEMPLATE_DIRS = ( settings.TEMPLATE_DIRS = (
os.path.join( os.path.join(os.path.dirname(__file__), 'templates'),
os.path.dirname(__file__), )
'templates'
)
,)
def tearDown(self): def tearDown(self):
settings.LANGUAGES = self.old_LANGUAGES settings.LANGUAGES = self.old_LANGUAGES
@ -220,16 +218,20 @@ class LoginTest(AuthViewsTestCase):
} }
) )
self.assertEquals(response.status_code, 302) self.assertEquals(response.status_code, 302)
self.assertFalse(bad_url in response['Location'], "%s should be blocked" % bad_url) self.assertFalse(bad_url in response['Location'],
"%s should be blocked" % bad_url)
# Now, these URLs have an other URL as a GET parameter and therefore # These URLs *should* still pass the security check
# should be allowed for good_url in ('/view/?param=http://example.com',
for url_ in ('http://example.com', 'https://example.com', '/view/?param=https://example.com',
'ftp://exampel.com', '//example.com'): '/view?param=ftp://exampel.com',
safe_url = '%(url)s?%(next)s=/view/?param=%(safe_param)s' % { 'view/?param=//example.com',
'https:///',
'//testserver/'):
safe_url = '%(url)s?%(next)s=%(good_url)s' % {
'url': login_url, 'url': login_url,
'next': REDIRECT_FIELD_NAME, 'next': REDIRECT_FIELD_NAME,
'safe_param': urllib.quote(url_) 'good_url': urllib.quote(good_url)
} }
response = self.client.post(safe_url, { response = self.client.post(safe_url, {
'username': 'testclient', 'username': 'testclient',
@ -237,8 +239,66 @@ class LoginTest(AuthViewsTestCase):
} }
) )
self.assertEquals(response.status_code, 302) self.assertEquals(response.status_code, 302)
self.assertTrue('/view/?param=%s' % url_ in response['Location'], "/view/?param=%s should be allowed" % url_) self.assertTrue(good_url in response['Location'],
"%s should be allowed" % good_url)
class LoginURLSettings(AuthViewsTestCase):
urls = 'django.contrib.auth.tests.urls'
def setUp(self):
super(LoginURLSettings, self).setUp()
self.old_LOGIN_URL = settings.LOGIN_URL
def tearDown(self):
super(LoginURLSettings, self).tearDown()
settings.LOGIN_URL = self.old_LOGIN_URL
def get_login_required_url(self, login_url):
settings.LOGIN_URL = login_url
response = self.client.get('/login_required/')
self.assertEquals(response.status_code, 302)
return response['Location']
def test_standard_login_url(self):
login_url = '/login/'
login_required_url = self.get_login_required_url(login_url)
querystring = QueryDict('', mutable=True)
querystring['next'] = '/login_required/'
self.assertEqual(login_required_url,
'http://testserver%s?%s' % (login_url, querystring.urlencode()))
def test_remote_login_url(self):
login_url = 'http://remote.example.com/login'
login_required_url = self.get_login_required_url(login_url)
querystring = QueryDict('', mutable=True)
querystring['next'] = 'http://testserver/login_required/'
self.assertEqual(login_required_url,
'%s?%s' % (login_url, querystring.urlencode()))
def test_https_login_url(self):
login_url = 'https:///login/'
login_required_url = self.get_login_required_url(login_url)
querystring = QueryDict('', mutable=True)
querystring['next'] = 'http://testserver/login_required/'
self.assertEqual(login_required_url,
'%s?%s' % (login_url, querystring.urlencode()))
def test_login_url_with_querystring(self):
login_url = '/login/?pretty=1'
login_required_url = self.get_login_required_url(login_url)
querystring = QueryDict('pretty=1', mutable=True)
querystring['next'] = '/login_required/'
self.assertEqual(login_required_url, 'http://testserver/login/?%s' %
querystring.urlencode())
def test_remote_login_url_with_next_querystring(self):
login_url = 'http://remote.example.com/login/'
login_required_url = self.get_login_required_url('%s?next=/default/' %
login_url)
querystring = QueryDict('', mutable=True)
querystring['next'] = 'http://testserver/login_required/'
self.assertEqual(login_required_url, '%s?%s' % (login_url,
querystring.urlencode()))
class LogoutTest(AuthViewsTestCase): class LogoutTest(AuthViewsTestCase):
urls = 'django.contrib.auth.tests.urls' urls = 'django.contrib.auth.tests.urls'

View File

@ -1,4 +1,5 @@
import re import re
import urlparse
from django.conf import settings from django.conf import settings
from django.contrib.auth import REDIRECT_FIELD_NAME from django.contrib.auth import REDIRECT_FIELD_NAME
# Avoid shadowing the login() view below. # Avoid shadowing the login() view below.
@ -11,9 +12,9 @@ from django.views.decorators.csrf import csrf_protect
from django.core.urlresolvers import reverse from django.core.urlresolvers import reverse
from django.shortcuts import render_to_response, get_object_or_404 from django.shortcuts import render_to_response, get_object_or_404
from django.contrib.sites.models import get_current_site from django.contrib.sites.models import get_current_site
from django.http import HttpResponseRedirect, Http404 from django.http import HttpResponseRedirect, Http404, QueryDict
from django.template import RequestContext from django.template import RequestContext
from django.utils.http import urlquote, base36_to_int from django.utils.http import base36_to_int
from django.utils.translation import ugettext as _ from django.utils.translation import ugettext as _
from django.contrib.auth.models import User from django.contrib.auth.models import User
from django.views.decorators.cache import never_cache from django.views.decorators.cache import never_cache
@ -30,16 +31,16 @@ def login(request, template_name='registration/login.html',
if request.method == "POST": if request.method == "POST":
form = authentication_form(data=request.POST) form = authentication_form(data=request.POST)
if form.is_valid(): if form.is_valid():
netloc = urlparse.urlparse(redirect_to)[1]
# Light security check -- make sure redirect_to isn't garbage. # Light security check -- make sure redirect_to isn't garbage.
if not redirect_to or ' ' in redirect_to: if not redirect_to or ' ' in redirect_to:
redirect_to = settings.LOGIN_REDIRECT_URL redirect_to = settings.LOGIN_REDIRECT_URL
# Heavier security check -- redirects to http://example.com should # Heavier security check -- don't allow redirection to a different
# not be allowed, but things like /view/?param=http://example.com # host.
# should be allowed. This regex checks if there is a '//' *before* a elif netloc and netloc != request.get_host():
# question mark. redirect_to = settings.LOGIN_REDIRECT_URL
elif '//' in redirect_to and re.match(r'[^\?]*//', redirect_to):
redirect_to = settings.LOGIN_REDIRECT_URL
# Okay, security checks complete. Log the user in. # Okay, security checks complete. Log the user in.
auth_login(request, form.get_user()) auth_login(request, form.get_user())
@ -88,11 +89,19 @@ def logout_then_login(request, login_url=None):
login_url = settings.LOGIN_URL login_url = settings.LOGIN_URL
return logout(request, login_url) return logout(request, login_url)
def redirect_to_login(next, login_url=None, redirect_field_name=REDIRECT_FIELD_NAME): def redirect_to_login(next, login_url=None,
redirect_field_name=REDIRECT_FIELD_NAME):
"Redirects the user to the login page, passing the given 'next' page" "Redirects the user to the login page, passing the given 'next' page"
if not login_url: if not login_url:
login_url = settings.LOGIN_URL login_url = settings.LOGIN_URL
return HttpResponseRedirect('%s?%s=%s' % (login_url, urlquote(redirect_field_name), urlquote(next)))
login_url_parts = list(urlparse.urlparse(login_url))
if redirect_field_name:
querystring = QueryDict(login_url_parts[4], mutable=True)
querystring[redirect_field_name] = next
login_url_parts[4] = querystring.urlencode()
return HttpResponseRedirect(urlparse.urlunparse(login_url_parts))
# 4 views for password reset: # 4 views for password reset:
# - password_reset sends the mail # - password_reset sends the mail