Skip to content

Commit 91d12c6

Browse files
committed
Fixed issue 40
1 parent 96fbbf7 commit 91d12c6

File tree

10 files changed

+291
-2
lines changed

10 files changed

+291
-2
lines changed

oauth2_provider/models.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -221,6 +221,13 @@ def revoke(self):
221221
"""
222222
self.delete()
223223

224+
@property
225+
def scopes(self):
226+
"""
227+
Returns a dictionary of allowed scope names (as keys) with their descriptions (as values)
228+
"""
229+
return {name: desc for name, desc in oauth2_settings.SCOPES.items() if name in self.scope.split()}
230+
224231
def __str__(self):
225232
return self.token
226233

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
{% extends "oauth2_provider/base.html" %}
2+
3+
{% load i18n %}
4+
{% block content %}
5+
<form action="" method="post">{% csrf_token %}
6+
<p>{% trans "Are you sure you want to delete this token?" %}</p>
7+
<input type="submit" value="{% trans "Delete" %}" />
8+
</form>
9+
{% endblock %}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
{% extends "oauth2_provider/base.html" %}
2+
3+
{% load i18n %}
4+
{% load url from compat %}
5+
{% block content %}
6+
<div class="block-center">
7+
<h1>{% trans "Tokens" %}</h1>
8+
<ul>
9+
{% for authorized_token in authorized_tokens %}
10+
<li>
11+
{{ authorized_token.application }}
12+
(<a href="{% url 'oauth2_provider:authorized-token-delete' authorized_token.pk %}">revoke</a>)
13+
</li>
14+
<ul>
15+
{% for scope_name, scope_description in authorized_token.scopes.items %}
16+
<li>{{ scope_name }}: {{ scope_description }}</li>
17+
{% endfor %}
18+
</ul>
19+
{% empty %}
20+
<li>{% trans "There are no authorized tokens yet." %}</li>
21+
{% endfor %}
22+
</ul>
23+
</div>
24+
{% endblock %}

oauth2_provider/tests/test_application_views.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,3 +99,10 @@ def test_application_detail_not_owner(self):
9999

100100
response = self.client.get(reverse('oauth2_provider:detail', args=(self.app_bar_1.pk,)))
101101
self.assertEqual(response.status_code, 404)
102+
103+
def test_delete_view_deletes(self):
104+
self.client.login(username="foo_user", password="123456")
105+
response = self.client.post(reverse('oauth2_provider:delete', args=(self.app_foo_1.pk,)))
106+
107+
self.assertFalse(Application.objects.filter(pk=self.app_foo_1.pk).exists())
108+
self.assertRedirects(response, reverse('oauth2_provider:list'))

oauth2_provider/tests/test_models.py

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,36 @@ def test_str(self):
8181
app.name = "test_app"
8282
self.assertEqual("%s" % app, "test_app")
8383

84+
def test_scopes_property(self):
85+
self.client.login(username="test_user", password="123456")
86+
87+
app = Application.objects.create(
88+
name="test_app",
89+
redirect_uris="http://localhost http://example.com http://example.it",
90+
user=self.user,
91+
client_type=Application.CLIENT_CONFIDENTIAL,
92+
authorization_grant_type=Application.GRANT_AUTHORIZATION_CODE,
93+
)
94+
95+
access_token = AccessToken(
96+
user=self.user,
97+
scope='read write',
98+
expires=0,
99+
token='',
100+
application=app
101+
)
102+
103+
access_token2 = AccessToken(
104+
user=self.user,
105+
scope='write',
106+
expires=0,
107+
token='',
108+
application=app
109+
)
110+
111+
self.assertEqual(access_token.scopes, {'read': 'Reading scope', 'write': 'Writing scope'})
112+
self.assertEqual(access_token2.scopes, {'write': 'Writing scope'})
113+
84114

85115
@skipIf(django.VERSION < (1, 5), "Behavior is broken on 1.4 and there is no solution")
86116
@override_settings(OAUTH2_PROVIDER_APPLICATION_MODEL='tests.TestApplication')
Lines changed: 166 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,166 @@
1+
from __future__ import unicode_literals
2+
3+
import datetime
4+
5+
from django.core.urlresolvers import reverse
6+
from django.test import TestCase
7+
from django.utils import timezone
8+
9+
from ..models import get_application_model, AccessToken
10+
from ..compat import get_user_model
11+
12+
Application = get_application_model()
13+
UserModel = get_user_model()
14+
15+
16+
class TestAuthorizedTokenViews(TestCase):
17+
def setUp(self):
18+
self.foo_user = UserModel.objects.create_user("foo_user", "[email protected]", "123456")
19+
self.bar_user = UserModel.objects.create_user("bar_user", "[email protected]", "123456")
20+
21+
self.application = Application(
22+
name="Test Application",
23+
redirect_uris="http://localhost http://example.com http://example.it",
24+
user=self.bar_user,
25+
client_type=Application.CLIENT_CONFIDENTIAL,
26+
authorization_grant_type=Application.GRANT_AUTHORIZATION_CODE,
27+
)
28+
self.application.save()
29+
30+
def tearDown(self):
31+
self.foo_user.delete()
32+
self.bar_user.delete()
33+
34+
def test_list_view_authorization_required(self):
35+
"""
36+
Test that the view redirects to login page if user is not logged-in.
37+
"""
38+
response = self.client.get(reverse('oauth2_provider:authorized-token-list'))
39+
self.assertEqual(response.status_code, 302)
40+
self.assertTrue('/accounts/login/?next=' in response['Location'])
41+
42+
def test_empty_list_view(self):
43+
"""
44+
Test that when you have no tokens, an appropriate message is shown
45+
"""
46+
self.client.login(username="foo_user", password="123456")
47+
48+
response = self.client.get(reverse('oauth2_provider:authorized-token-list'))
49+
self.assertEqual(response.status_code, 200)
50+
self.assertIn(b'There are no authorized tokens yet.', response.content)
51+
52+
def test_list_view_one_token(self):
53+
"""
54+
Test that the view shows your token
55+
"""
56+
self.client.login(username="bar_user", password="123456")
57+
AccessToken.objects.create(user=self.bar_user, token='1234567890',
58+
application=self.application,
59+
expires=timezone.now() + datetime.timedelta(days=1),
60+
scope='read write')
61+
62+
response = self.client.get(reverse('oauth2_provider:authorized-token-list'))
63+
self.assertEqual(response.status_code, 200)
64+
self.assertIn(b'read', response.content)
65+
self.assertIn(b'write', response.content)
66+
self.assertNotIn(b'There are no authorized tokens yet.', response.content)
67+
68+
def test_list_view_two_tokens(self):
69+
"""
70+
Test that the view shows your tokens
71+
"""
72+
self.client.login(username="bar_user", password="123456")
73+
AccessToken.objects.create(user=self.bar_user, token='1234567890',
74+
application=self.application,
75+
expires=timezone.now() + datetime.timedelta(days=1),
76+
scope='read write')
77+
AccessToken.objects.create(user=self.bar_user, token='0123456789',
78+
application=self.application,
79+
expires=timezone.now() + datetime.timedelta(days=1),
80+
scope='read write')
81+
82+
response = self.client.get(reverse('oauth2_provider:authorized-token-list'))
83+
self.assertEqual(response.status_code, 200)
84+
print(response.content.decode())
85+
self.assertNotIn(b'There are no authorized tokens yet.', response.content)
86+
87+
def test_list_view_shows_correct_user_token(self):
88+
"""
89+
Test that only currently logged-in user's tokens are shown
90+
"""
91+
self.client.login(username="bar_user", password="123456")
92+
AccessToken.objects.create(user=self.foo_user, token='1234567890',
93+
application=self.application,
94+
expires=timezone.now() + datetime.timedelta(days=1),
95+
scope='read write')
96+
97+
response = self.client.get(reverse('oauth2_provider:authorized-token-list'))
98+
self.assertEqual(response.status_code, 200)
99+
self.assertIn(b'There are no authorized tokens yet.', response.content)
100+
101+
def test_delete_view_authorization_required(self):
102+
"""
103+
Test that the view redirects to login page if user is not logged-in.
104+
"""
105+
self.token = AccessToken.objects.create(user=self.foo_user, token='1234567890',
106+
application=self.application,
107+
expires=timezone.now() + datetime.timedelta(days=1),
108+
scope='read write')
109+
110+
response = self.client.get(reverse('oauth2_provider:authorized-token-delete', kwargs={'pk': self.token.pk}))
111+
self.assertEqual(response.status_code, 302)
112+
self.assertTrue('/accounts/login/?next=' in response['Location'])
113+
114+
def test_delete_view_works(self):
115+
"""
116+
Test that a GET on this view returns 200 if the token belongs to the logged-in user.
117+
"""
118+
self.token = AccessToken.objects.create(user=self.foo_user, token='1234567890',
119+
application=self.application,
120+
expires=timezone.now() + datetime.timedelta(days=1),
121+
scope='read write')
122+
123+
self.client.login(username="foo_user", password="123456")
124+
response = self.client.get(reverse('oauth2_provider:authorized-token-delete', kwargs={'pk': self.token.pk}))
125+
self.assertEqual(response.status_code, 200)
126+
127+
def test_delete_view_token_belongs_to_user(self):
128+
"""
129+
Test that a 404 is returned when trying to GET this view with someone else's tokens.
130+
"""
131+
self.token = AccessToken.objects.create(user=self.foo_user, token='1234567890',
132+
application=self.application,
133+
expires=timezone.now() + datetime.timedelta(days=1),
134+
scope='read write')
135+
136+
self.client.login(username="bar_user", password="123456")
137+
response = self.client.get(reverse('oauth2_provider:authorized-token-delete', kwargs={'pk': self.token.pk}))
138+
self.assertEqual(response.status_code, 404)
139+
140+
def test_delete_view_post_actually_deletes(self):
141+
"""
142+
Test that a POST on this view works if the token belongs to the logged-in user.
143+
"""
144+
self.token = AccessToken.objects.create(user=self.foo_user, token='1234567890',
145+
application=self.application,
146+
expires=timezone.now() + datetime.timedelta(days=1),
147+
scope='read write')
148+
149+
self.client.login(username="foo_user", password="123456")
150+
response = self.client.post(reverse('oauth2_provider:authorized-token-delete', kwargs={'pk': self.token.pk}))
151+
self.assertFalse(AccessToken.objects.exists())
152+
self.assertRedirects(response, reverse('oauth2_provider:authorized-token-list'))
153+
154+
def test_delete_view_only_deletes_user_own_token(self):
155+
"""
156+
Test that a 404 is returned when trying to POST on this view with someone else's tokens.
157+
"""
158+
self.token = AccessToken.objects.create(user=self.foo_user, token='1234567890',
159+
application=self.application,
160+
expires=timezone.now() + datetime.timedelta(days=1),
161+
scope='read write')
162+
163+
self.client.login(username="bar_user", password="123456")
164+
response = self.client.post(reverse('oauth2_provider:authorized-token-delete', kwargs={'pk': self.token.pk}))
165+
self.assertTrue(AccessToken.objects.exists())
166+
self.assertEqual(response.status_code, 404)

oauth2_provider/urls.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,3 +17,9 @@
1717
url(r'^applications/(?P<pk>\d+)/delete/$', views.ApplicationDelete.as_view(), name="delete"),
1818
url(r'^applications/(?P<pk>\d+)/update/$', views.ApplicationUpdate.as_view(), name="update"),
1919
)
20+
21+
urlpatterns += (
22+
url(r'^authorized_tokens/$', views.AuthorizedTokensListView.as_view(), name="authorized-token-list"),
23+
url(r'^authorized_tokens/(?P<pk>\d+)/delete/$', views.AuthorizedTokenDeleteView.as_view(),
24+
name="authorized-token-delete"),
25+
)

oauth2_provider/views/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,3 +2,4 @@
22
from .application import ApplicationRegistration, ApplicationDetail, ApplicationList, \
33
ApplicationDelete, ApplicationUpdate
44
from .generic import ProtectedResourceView, ScopedProtectedResourceView, ReadWriteScopedResourceView
5+
from .token import AuthorizedTokensListView, AuthorizedTokenDeleteView

oauth2_provider/views/application.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
from django.core.urlresolvers import reverse_lazy
1+
from django.core.urlresolvers import reverse
22
from django.forms.models import modelform_factory
33
from django.views.generic import CreateView, DetailView, DeleteView, ListView, UpdateView
44

@@ -59,9 +59,11 @@ class ApplicationDelete(ApplicationOwnerIsUserMixin, DeleteView):
5959
View used to delete an application owned by the request.user
6060
"""
6161
context_object_name = 'application'
62-
success_url = reverse_lazy('oauth2_provider:list')
6362
template_name = "oauth2_provider/application_confirm_delete.html"
6463

64+
def get_success_url(self):
65+
return reverse('oauth2_provider:list')
66+
6567

6668
class ApplicationUpdate(ApplicationOwnerIsUserMixin, UpdateView):
6769
"""

oauth2_provider/views/token.py

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
from __future__ import absolute_import, unicode_literals
2+
3+
from django.core.urlresolvers import reverse
4+
from django.views.generic import ListView, DeleteView
5+
from braces.views import LoginRequiredMixin
6+
7+
from ..models import AccessToken
8+
9+
10+
class AuthorizedTokensListView(LoginRequiredMixin, ListView):
11+
"""
12+
Show a page where the current logged-in user can see his tokens so they can revoke them
13+
"""
14+
context_object_name = 'authorized_tokens'
15+
template_name = 'oauth2_provider/authorized-tokens.html'
16+
model = AccessToken
17+
18+
def get_queryset(self):
19+
"""
20+
Show only user's tokens
21+
"""
22+
return super(AuthorizedTokensListView, self).get_queryset()\
23+
.select_related('application').filter(user=self.request.user)
24+
25+
26+
class AuthorizedTokenDeleteView(LoginRequiredMixin, DeleteView):
27+
"""
28+
View for revoking a specific token
29+
"""
30+
template_name = 'oauth2_provider/authorized-token-delete.html'
31+
model = AccessToken
32+
33+
def get_success_url(self):
34+
return reverse('oauth2_provider:authorized-token-list')
35+
36+
def get_queryset(self):
37+
return super(AuthorizedTokenDeleteView, self).get_queryset().filter(user=self.request.user)

0 commit comments

Comments
 (0)