Skip to content

Commit 399ac70

Browse files
committed
2 parents c4eda3a + 2e06f5c commit 399ac70

File tree

9 files changed

+176
-106
lines changed

9 files changed

+176
-106
lines changed

docs/api-guide/authentication.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -300,7 +300,7 @@ The only thing needed to make the `OAuth2Authentication` class work is to insert
300300

301301
The command line to test the authentication looks like:
302302

303-
curl -H "Authorization: Bearer <your-access-token>" http://localhost:8000/api/?client_id=YOUR_CLIENT_ID\&client_secret=YOUR_CLIENT_SECRET
303+
curl -H "Authorization: Bearer <your-access-token>" http://localhost:8000/api/
304304

305305
---
306306

docs/topics/release-notes.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,12 @@ You can determine your currently installed version using `pip freeze`:
4040

4141
## 2.2.x series
4242

43+
### Master
44+
45+
* OAuth2 authentication no longer requires unneccessary URL parameters in addition to the token.
46+
* URL hyperlinking in browseable API now handles more cases correctly.
47+
* Bugfix: Fix regression with DjangoFilterBackend not worthing correctly with single object views.
48+
4349
### 2.2.5
4450

4551
**Date**: 26th March 2013

rest_framework/authentication.py

Lines changed: 11 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,16 @@
22
Provides a set of pluggable authentication policies.
33
"""
44
from __future__ import unicode_literals
5+
import base64
6+
from datetime import datetime
7+
58
from django.contrib.auth import authenticate
69
from django.core.exceptions import ImproperlyConfigured
710
from rest_framework import exceptions, HTTP_HEADER_ENCODING
811
from rest_framework.compat import CsrfViewMiddleware
912
from rest_framework.compat import oauth, oauth_provider, oauth_provider_store
10-
from rest_framework.compat import oauth2_provider, oauth2_provider_forms, oauth2_provider_backends
13+
from rest_framework.compat import oauth2_provider, oauth2_provider_forms
1114
from rest_framework.authtoken.models import Token
12-
import base64
1315

1416

1517
def get_authorization_header(request):
@@ -315,21 +317,15 @@ def authenticate_credentials(self, request, access_token):
315317
Authenticate the request, given the access token.
316318
"""
317319

318-
# Authenticate the client
319-
oauth2_client_form = oauth2_provider_forms.ClientAuthForm(request.REQUEST)
320-
if not oauth2_client_form.is_valid():
321-
raise exceptions.AuthenticationFailed('Client could not be validated')
322-
client = oauth2_client_form.cleaned_data.get('client')
323-
324-
# Retrieve the `OAuth2AccessToken` instance from the access_token
325-
auth_backend = oauth2_provider_backends.AccessTokenBackend()
326-
token = auth_backend.authenticate(access_token, client)
327-
if token is None:
320+
try:
321+
token = oauth2_provider.models.AccessToken.objects.select_related('user')
322+
# TODO: Change to timezone aware datetime when oauth2_provider add
323+
# support to it.
324+
token = token.get(token=access_token, expires__gt=datetime.now())
325+
except oauth2_provider.models.AccessToken.DoesNotExist:
328326
raise exceptions.AuthenticationFailed('Invalid token')
329327

330-
user = token.user
331-
332-
if not user.is_active:
328+
if not token.user.is_active:
333329
msg = 'User inactive or deleted: %s' % user.username
334330
raise exceptions.AuthenticationFailed(msg)
335331

rest_framework/compat.py

Lines changed: 31 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -395,6 +395,37 @@ def parse_datetime(value):
395395
kw = dict((k, int(v)) for k, v in kw.iteritems() if v is not None)
396396
return datetime.datetime(**kw)
397397

398+
399+
# smart_urlquote is new on Django 1.4
400+
try:
401+
from django.utils.html import smart_urlquote
402+
except ImportError:
403+
try:
404+
from urllib.parse import quote, urlsplit, urlunsplit
405+
except ImportError: # Python 2
406+
from urllib import quote
407+
from urlparse import urlsplit, urlunsplit
408+
409+
def smart_urlquote(url):
410+
"Quotes a URL if it isn't already quoted."
411+
# Handle IDN before quoting.
412+
scheme, netloc, path, query, fragment = urlsplit(url)
413+
try:
414+
netloc = netloc.encode('idna').decode('ascii') # IDN -> ACE
415+
except UnicodeError: # invalid domain part
416+
pass
417+
else:
418+
url = urlunsplit((scheme, netloc, path, query, fragment))
419+
420+
# An URL is considered unquoted if it contains no % characters or
421+
# contains a % not followed by two hexadecimal digits. See #9655.
422+
if '%' not in url or unquoted_percents_re.search(url):
423+
# See http://bugs.python.org/issue2637
424+
url = quote(force_bytes(url), safe=b'!*\'();:@&=+$,/?#[]~')
425+
426+
return force_text(url)
427+
428+
398429
# Markdown is optional
399430
try:
400431
import markdown
@@ -445,14 +476,12 @@ def apply_markdown(text):
445476
# OAuth 2 support is optional
446477
try:
447478
import provider.oauth2 as oauth2_provider
448-
from provider.oauth2 import backends as oauth2_provider_backends
449479
from provider.oauth2 import models as oauth2_provider_models
450480
from provider.oauth2 import forms as oauth2_provider_forms
451481
from provider import scope as oauth2_provider_scope
452482
from provider import constants as oauth2_constants
453483
except ImportError:
454484
oauth2_provider = None
455-
oauth2_provider_backends = None
456485
oauth2_provider_models = None
457486
oauth2_provider_forms = None
458487
oauth2_provider_scope = None

rest_framework/filters.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,6 @@ def filter_queryset(self, request, queryset, view):
5555
filter_class = self.get_filter_class(view)
5656

5757
if filter_class:
58-
return filter_class(request.QUERY_PARAMS, queryset=queryset)
58+
return filter_class(request.QUERY_PARAMS, queryset=queryset).qs
5959

6060
return queryset

rest_framework/templatetags/rest_framework.py

Lines changed: 46 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,8 @@
44
from django.http import QueryDict
55
from django.utils.html import escape
66
from django.utils.safestring import SafeData, mark_safe
7-
from rest_framework.compat import urlparse
8-
from rest_framework.compat import force_text
9-
from rest_framework.compat import six
10-
import re
11-
import string
7+
from rest_framework.compat import urlparse, force_text, six, smart_urlquote
8+
import re, string
129

1310
register = template.Library()
1411

@@ -112,22 +109,6 @@ def replace_query_param(url, key, val):
112109
class_re = re.compile(r'(?<=class=["\'])(.*)(?=["\'])')
113110

114111

115-
# Bunch of stuff cloned from urlize
116-
LEADING_PUNCTUATION = ['(', '<', '&lt;', '"', "'"]
117-
TRAILING_PUNCTUATION = ['.', ',', ')', '>', '\n', '&gt;', '"', "'"]
118-
DOTS = ['&middot;', '*', '\xe2\x80\xa2', '&#149;', '&bull;', '&#8226;']
119-
unencoded_ampersands_re = re.compile(r'&(?!(\w+|#\d+);)')
120-
word_split_re = re.compile(r'(\s+)')
121-
punctuation_re = re.compile('^(?P<lead>(?:%s)*)(?P<middle>.*?)(?P<trail>(?:%s)*)$' % \
122-
('|'.join([re.escape(x) for x in LEADING_PUNCTUATION]),
123-
'|'.join([re.escape(x) for x in TRAILING_PUNCTUATION])))
124-
simple_email_re = re.compile(r'^\S+@[a-zA-Z0-9._-]+\.[a-zA-Z0-9._-]+$')
125-
link_target_attribute_re = re.compile(r'(<a [^>]*?)target=[^\s>]+')
126-
html_gunk_re = re.compile(r'(?:<br clear="all">|<i><\/i>|<b><\/b>|<em><\/em>|<strong><\/strong>|<\/?smallcaps>|<\/?uppercase>)', re.IGNORECASE)
127-
hard_coded_bullets_re = re.compile(r'((?:<p>(?:%s).*?[a-zA-Z].*?</p>\s*)+)' % '|'.join([re.escape(x) for x in DOTS]), re.DOTALL)
128-
trailing_empty_content_re = re.compile(r'(?:<p>(?:&nbsp;|\s|<br \/>)*?</p>\s*)+\Z')
129-
130-
131112
# And the template tags themselves...
132113

133114
@register.simple_tag
@@ -195,15 +176,25 @@ def add_class(value, css_class):
195176
return value
196177

197178

179+
# Bunch of stuff cloned from urlize
180+
TRAILING_PUNCTUATION = ['.', ',', ':', ';', '.)', '"', "'"]
181+
WRAPPING_PUNCTUATION = [('(', ')'), ('<', '>'), ('[', ']'), ('&lt;', '&gt;'),
182+
('"', '"'), ("'", "'")]
183+
word_split_re = re.compile(r'(\s+)')
184+
simple_url_re = re.compile(r'^https?://\w', re.IGNORECASE)
185+
simple_url_2_re = re.compile(r'^www\.|^(?!http)\w[^@]+\.(com|edu|gov|int|mil|net|org)$', re.IGNORECASE)
186+
simple_email_re = re.compile(r'^\S+@\S+\.\S+$')
187+
188+
198189
@register.filter
199190
def urlize_quoted_links(text, trim_url_limit=None, nofollow=True, autoescape=True):
200191
"""
201192
Converts any URLs in text into clickable links.
202193
203-
Works on http://, https://, www. links and links ending in .org, .net or
204-
.com. Links can have trailing punctuation (periods, commas, close-parens)
205-
and leading punctuation (opening parens) and it'll still do the right
206-
thing.
194+
Works on http://, https://, www. links, and also on links ending in one of
195+
the original seven gTLDs (.com, .edu, .gov, .int, .mil, .net, and .org).
196+
Links can have trailing punctuation (periods, commas, close-parens) and
197+
leading punctuation (opening parens) and it'll still do the right thing.
207198
208199
If trim_url_limit is not None, the URLs in link text longer than this limit
209200
will truncated to trim_url_limit-3 characters and appended with an elipsis.
@@ -216,24 +207,41 @@ def urlize_quoted_links(text, trim_url_limit=None, nofollow=True, autoescape=Tru
216207
trim_url = lambda x, limit=trim_url_limit: limit is not None and (len(x) > limit and ('%s...' % x[:max(0, limit - 3)])) or x
217208
safe_input = isinstance(text, SafeData)
218209
words = word_split_re.split(force_text(text))
219-
nofollow_attr = nofollow and ' rel="nofollow"' or ''
220210
for i, word in enumerate(words):
221211
match = None
222212
if '.' in word or '@' in word or ':' in word:
223-
match = punctuation_re.match(word)
224-
if match:
225-
lead, middle, trail = match.groups()
213+
# Deal with punctuation.
214+
lead, middle, trail = '', word, ''
215+
for punctuation in TRAILING_PUNCTUATION:
216+
if middle.endswith(punctuation):
217+
middle = middle[:-len(punctuation)]
218+
trail = punctuation + trail
219+
for opening, closing in WRAPPING_PUNCTUATION:
220+
if middle.startswith(opening):
221+
middle = middle[len(opening):]
222+
lead = lead + opening
223+
# Keep parentheses at the end only if they're balanced.
224+
if (middle.endswith(closing)
225+
and middle.count(closing) == middle.count(opening) + 1):
226+
middle = middle[:-len(closing)]
227+
trail = closing + trail
228+
226229
# Make URL we want to point to.
227230
url = None
228-
if middle.startswith('http://') or middle.startswith('https://'):
229-
url = middle
230-
elif middle.startswith('www.') or ('@' not in middle and \
231-
middle and middle[0] in string.ascii_letters + string.digits and \
232-
(middle.endswith('.org') or middle.endswith('.net') or middle.endswith('.com'))):
233-
url = 'http://%s' % middle
234-
elif '@' in middle and not ':' in middle and simple_email_re.match(middle):
235-
url = 'mailto:%s' % middle
231+
nofollow_attr = ' rel="nofollow"' if nofollow else ''
232+
if simple_url_re.match(middle):
233+
url = smart_urlquote(middle)
234+
elif simple_url_2_re.match(middle):
235+
url = smart_urlquote('http://%s' % middle)
236+
elif not ':' in middle and simple_email_re.match(middle):
237+
local, domain = middle.rsplit('@', 1)
238+
try:
239+
domain = domain.encode('idna').decode('ascii')
240+
except UnicodeError:
241+
continue
242+
url = 'mailto:%s@%s' % (local, domain)
236243
nofollow_attr = ''
244+
237245
# Make link.
238246
if url:
239247
trimmed = trim_url(middle)
@@ -251,4 +259,4 @@ def urlize_quoted_links(text, trim_url_limit=None, nofollow=True, autoescape=Tru
251259
words[i] = mark_safe(word)
252260
elif autoescape:
253261
words[i] = escape(word)
254-
return mark_safe(''.join(words))
262+
return ''.join(words)

rest_framework/tests/authentication.py

Lines changed: 11 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -466,17 +466,13 @@ def setUp(self):
466466
def _create_authorization_header(self, token=None):
467467
return "Bearer {0}".format(token or self.access_token.token)
468468

469-
def _client_credentials_params(self):
470-
return {'client_id': self.CLIENT_ID, 'client_secret': self.CLIENT_SECRET}
471-
472469
@unittest.skipUnless(oauth2_provider, 'django-oauth2-provider not installed')
473470
def test_get_form_with_wrong_authorization_header_token_type_failing(self):
474471
"""Ensure that a wrong token type lead to the correct HTTP error status code"""
475472
auth = "Wrong token-type-obsviously"
476473
response = self.csrf_client.get('/oauth2-test/', {}, HTTP_AUTHORIZATION=auth)
477474
self.assertEqual(response.status_code, 401)
478-
params = self._client_credentials_params()
479-
response = self.csrf_client.get('/oauth2-test/', params, HTTP_AUTHORIZATION=auth)
475+
response = self.csrf_client.get('/oauth2-test/', HTTP_AUTHORIZATION=auth)
480476
self.assertEqual(response.status_code, 401)
481477

482478
@unittest.skipUnless(oauth2_provider, 'django-oauth2-provider not installed')
@@ -485,8 +481,7 @@ def test_get_form_with_wrong_authorization_header_token_format_failing(self):
485481
auth = "Bearer wrong token format"
486482
response = self.csrf_client.get('/oauth2-test/', {}, HTTP_AUTHORIZATION=auth)
487483
self.assertEqual(response.status_code, 401)
488-
params = self._client_credentials_params()
489-
response = self.csrf_client.get('/oauth2-test/', params, HTTP_AUTHORIZATION=auth)
484+
response = self.csrf_client.get('/oauth2-test/', HTTP_AUTHORIZATION=auth)
490485
self.assertEqual(response.status_code, 401)
491486

492487
@unittest.skipUnless(oauth2_provider, 'django-oauth2-provider not installed')
@@ -495,50 +490,36 @@ def test_get_form_with_wrong_authorization_header_token_failing(self):
495490
auth = "Bearer wrong-token"
496491
response = self.csrf_client.get('/oauth2-test/', {}, HTTP_AUTHORIZATION=auth)
497492
self.assertEqual(response.status_code, 401)
498-
params = self._client_credentials_params()
499-
response = self.csrf_client.get('/oauth2-test/', params, HTTP_AUTHORIZATION=auth)
500-
self.assertEqual(response.status_code, 401)
501-
502-
@unittest.skipUnless(oauth2_provider, 'django-oauth2-provider not installed')
503-
def test_get_form_with_wrong_client_data_failing_auth(self):
504-
"""Ensure GETing form over OAuth with incorrect client credentials fails"""
505-
auth = self._create_authorization_header()
506-
params = self._client_credentials_params()
507-
params['client_id'] += 'a'
508-
response = self.csrf_client.get('/oauth2-test/', params, HTTP_AUTHORIZATION=auth)
493+
response = self.csrf_client.get('/oauth2-test/', HTTP_AUTHORIZATION=auth)
509494
self.assertEqual(response.status_code, 401)
510495

511496
@unittest.skipUnless(oauth2_provider, 'django-oauth2-provider not installed')
512497
def test_get_form_passing_auth(self):
513498
"""Ensure GETing form over OAuth with correct client credentials succeed"""
514499
auth = self._create_authorization_header()
515-
params = self._client_credentials_params()
516-
response = self.csrf_client.get('/oauth2-test/', params, HTTP_AUTHORIZATION=auth)
500+
response = self.csrf_client.get('/oauth2-test/', HTTP_AUTHORIZATION=auth)
517501
self.assertEqual(response.status_code, 200)
518502

519503
@unittest.skipUnless(oauth2_provider, 'django-oauth2-provider not installed')
520504
def test_post_form_passing_auth(self):
521505
"""Ensure POSTing form over OAuth with correct credentials passes and does not require CSRF"""
522506
auth = self._create_authorization_header()
523-
params = self._client_credentials_params()
524-
response = self.csrf_client.post('/oauth2-test/', params, HTTP_AUTHORIZATION=auth)
507+
response = self.csrf_client.post('/oauth2-test/', HTTP_AUTHORIZATION=auth)
525508
self.assertEqual(response.status_code, 200)
526509

527510
@unittest.skipUnless(oauth2_provider, 'django-oauth2-provider not installed')
528511
def test_post_form_token_removed_failing_auth(self):
529512
"""Ensure POSTing when there is no OAuth access token in db fails"""
530513
self.access_token.delete()
531514
auth = self._create_authorization_header()
532-
params = self._client_credentials_params()
533-
response = self.csrf_client.post('/oauth2-test/', params, HTTP_AUTHORIZATION=auth)
515+
response = self.csrf_client.post('/oauth2-test/', HTTP_AUTHORIZATION=auth)
534516
self.assertIn(response.status_code, (status.HTTP_401_UNAUTHORIZED, status.HTTP_403_FORBIDDEN))
535517

536518
@unittest.skipUnless(oauth2_provider, 'django-oauth2-provider not installed')
537519
def test_post_form_with_refresh_token_failing_auth(self):
538520
"""Ensure POSTing with refresh token instead of access token fails"""
539521
auth = self._create_authorization_header(token=self.refresh_token.token)
540-
params = self._client_credentials_params()
541-
response = self.csrf_client.post('/oauth2-test/', params, HTTP_AUTHORIZATION=auth)
522+
response = self.csrf_client.post('/oauth2-test/', HTTP_AUTHORIZATION=auth)
542523
self.assertIn(response.status_code, (status.HTTP_401_UNAUTHORIZED, status.HTTP_403_FORBIDDEN))
543524

544525
@unittest.skipUnless(oauth2_provider, 'django-oauth2-provider not installed')
@@ -547,8 +528,7 @@ def test_post_form_with_expired_access_token_failing_auth(self):
547528
self.access_token.expires = datetime.datetime.now() - datetime.timedelta(seconds=10) # 10 seconds late
548529
self.access_token.save()
549530
auth = self._create_authorization_header()
550-
params = self._client_credentials_params()
551-
response = self.csrf_client.post('/oauth2-test/', params, HTTP_AUTHORIZATION=auth)
531+
response = self.csrf_client.post('/oauth2-test/', HTTP_AUTHORIZATION=auth)
552532
self.assertIn(response.status_code, (status.HTTP_401_UNAUTHORIZED, status.HTTP_403_FORBIDDEN))
553533
self.assertIn('Invalid token', response.content)
554534

@@ -559,10 +539,9 @@ def test_post_form_with_invalid_scope_failing_auth(self):
559539
read_only_access_token.scope = oauth2_provider_scope.SCOPE_NAME_DICT['read']
560540
read_only_access_token.save()
561541
auth = self._create_authorization_header(token=read_only_access_token.token)
562-
params = self._client_credentials_params()
563-
response = self.csrf_client.get('/oauth2-with-scope-test/', params, HTTP_AUTHORIZATION=auth)
542+
response = self.csrf_client.get('/oauth2-with-scope-test/', HTTP_AUTHORIZATION=auth)
564543
self.assertEqual(response.status_code, 200)
565-
response = self.csrf_client.post('/oauth2-with-scope-test/', params, HTTP_AUTHORIZATION=auth)
544+
response = self.csrf_client.post('/oauth2-with-scope-test/', HTTP_AUTHORIZATION=auth)
566545
self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
567546

568547
@unittest.skipUnless(oauth2_provider, 'django-oauth2-provider not installed')
@@ -572,6 +551,5 @@ def test_post_form_with_valid_scope_passing_auth(self):
572551
read_write_access_token.scope = oauth2_provider_scope.SCOPE_NAME_DICT['write']
573552
read_write_access_token.save()
574553
auth = self._create_authorization_header(token=read_write_access_token.token)
575-
params = self._client_credentials_params()
576-
response = self.csrf_client.post('/oauth2-with-scope-test/', params, HTTP_AUTHORIZATION=auth)
554+
response = self.csrf_client.post('/oauth2-with-scope-test/', HTTP_AUTHORIZATION=auth)
577555
self.assertEqual(response.status_code, 200)

0 commit comments

Comments
 (0)