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:
from functools import update_wrapper, wraps
from functools import wraps
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.http import HttpResponseRedirect
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):
@ -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
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):
@wraps(view_func, assigned=available_attrs(view_func))
def _wrapped_view(request, *args, **kwargs):
if test_func(request.user):
return view_func(request, *args, **kwargs)
path = urlquote(request.get_full_path())
tup = login_url, redirect_field_name, path
return HttpResponseRedirect('%s?%s=%s' % tup)
return wraps(view_func, assigned=available_attrs(view_func))(_wrapped_view)
path = request.build_absolute_uri()
# If the login url is the same scheme and net location then just
# use the path as the "next" url.
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

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.signals import SignalTestCase
from django.contrib.auth.tests.tokens import TokenGeneratorTest
from django.contrib.auth.tests.views \
import PasswordResetTest, ChangePasswordTest, LoginTest, LogoutTest
from django.contrib.auth.tests.views import PasswordResetTest, \
ChangePasswordTest, LoginTest, LogoutTest, LoginURLSettings
# The password for the fixture data users is 'password'

View File

@ -5,11 +5,12 @@ import urllib
from django.conf import settings
from django.contrib.auth import SESSION_KEY, REDIRECT_FIELD_NAME
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.test import TestCase
from django.core import mail
from django.core.urlresolvers import reverse
from django.http import QueryDict
class AuthViewsTestCase(TestCase):
"""
@ -25,11 +26,8 @@ class AuthViewsTestCase(TestCase):
settings.LANGUAGE_CODE = 'en'
self.old_TEMPLATE_DIRS = settings.TEMPLATE_DIRS
settings.TEMPLATE_DIRS = (
os.path.join(
os.path.dirname(__file__),
'templates'
)
,)
os.path.join(os.path.dirname(__file__), 'templates'),
)
def tearDown(self):
settings.LANGUAGES = self.old_LANGUAGES
@ -220,16 +218,20 @@ class LoginTest(AuthViewsTestCase):
}
)
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
# should be allowed
for url_ in ('http://example.com', 'https://example.com',
'ftp://exampel.com', '//example.com'):
safe_url = '%(url)s?%(next)s=/view/?param=%(safe_param)s' % {
# These URLs *should* still pass the security check
for good_url in ('/view/?param=http://example.com',
'/view/?param=https://example.com',
'/view?param=ftp://exampel.com',
'view/?param=//example.com',
'https:///',
'//testserver/'):
safe_url = '%(url)s?%(next)s=%(good_url)s' % {
'url': login_url,
'next': REDIRECT_FIELD_NAME,
'safe_param': urllib.quote(url_)
'good_url': urllib.quote(good_url)
}
response = self.client.post(safe_url, {
'username': 'testclient',
@ -237,8 +239,66 @@ class LoginTest(AuthViewsTestCase):
}
)
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):
urls = 'django.contrib.auth.tests.urls'

View File

@ -1,4 +1,5 @@
import re
import urlparse
from django.conf import settings
from django.contrib.auth import REDIRECT_FIELD_NAME
# 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.shortcuts import render_to_response, get_object_or_404
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.utils.http import urlquote, base36_to_int
from django.utils.http import base36_to_int
from django.utils.translation import ugettext as _
from django.contrib.auth.models import User
from django.views.decorators.cache import never_cache
@ -30,16 +31,16 @@ def login(request, template_name='registration/login.html',
if request.method == "POST":
form = authentication_form(data=request.POST)
if form.is_valid():
netloc = urlparse.urlparse(redirect_to)[1]
# Light security check -- make sure redirect_to isn't garbage.
if not redirect_to or ' ' in redirect_to:
redirect_to = settings.LOGIN_REDIRECT_URL
# Heavier security check -- redirects to http://example.com should
# not be allowed, but things like /view/?param=http://example.com
# should be allowed. This regex checks if there is a '//' *before* a
# question mark.
elif '//' in redirect_to and re.match(r'[^\?]*//', redirect_to):
redirect_to = settings.LOGIN_REDIRECT_URL
# Heavier security check -- don't allow redirection to a different
# host.
elif netloc and netloc != request.get_host():
redirect_to = settings.LOGIN_REDIRECT_URL
# Okay, security checks complete. Log the user in.
auth_login(request, form.get_user())
@ -88,11 +89,19 @@ def logout_then_login(request, login_url=None):
login_url = settings.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"
if not 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:
# - password_reset sends the mail