Skip to content

Commit 539bd13

Browse files
author
Damien de Lemeny
committed
Make Resource Owner model configurable
- change relevant ForeignKeys - add resource owner extractors in backend and validator - add configurable user lookup in views
1 parent 34f3b7b commit 539bd13

File tree

10 files changed

+111
-20
lines changed

10 files changed

+111
-20
lines changed
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
# -*- coding: utf-8 -*-
2+
# Generated by Django 1.10.4 on 2016-12-21 19:06
3+
from __future__ import unicode_literals
4+
5+
from django.conf import settings
6+
from django.db import migrations, models
7+
import django.db.models.deletion
8+
from oauth2_provider.settings import oauth2_settings
9+
10+
11+
class Migration(migrations.Migration):
12+
13+
dependencies = [
14+
('oauth2_provider', '0004_auto_20160525_1623'),
15+
migrations.swappable_dependency(oauth2_settings.RESOURCE_OWNER_MODEL),
16+
]
17+
18+
operations = [
19+
migrations.AlterField(
20+
model_name='accesstoken',
21+
name='user',
22+
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to=oauth2_settings.RESOURCE_OWNER_MODEL),
23+
),
24+
migrations.AlterField(
25+
model_name='grant',
26+
name='user',
27+
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=oauth2_settings.RESOURCE_OWNER_MODEL),
28+
),
29+
migrations.AlterField(
30+
model_name='refreshtoken',
31+
name='user',
32+
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=oauth2_settings.RESOURCE_OWNER_MODEL),
33+
),
34+
]

oauth2_provider/models.py

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -142,7 +142,6 @@ class Application(AbstractApplication):
142142
class Meta(AbstractApplication.Meta):
143143
swappable = 'OAUTH2_PROVIDER_APPLICATION_MODEL'
144144

145-
146145
@python_2_unicode_compatible
147146
class Grant(models.Model):
148147
"""
@@ -159,7 +158,7 @@ class Grant(models.Model):
159158
* :attr:`redirect_uri` Self explained
160159
* :attr:`scope` Required scopes, optional
161160
"""
162-
user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE)
161+
user = models.ForeignKey(oauth2_settings.RESOURCE_OWNER_MODEL, on_delete=models.CASCADE)
163162
code = models.CharField(max_length=255, unique=True) # code comes from oauthlib
164163
application = models.ForeignKey(oauth2_settings.APPLICATION_MODEL,
165164
on_delete=models.CASCADE)
@@ -197,7 +196,7 @@ class AccessToken(models.Model):
197196
* :attr:`expires` Date and time of token expiration, in DateTime format
198197
* :attr:`scope` Allowed scopes
199198
"""
200-
user = models.ForeignKey(settings.AUTH_USER_MODEL, blank=True, null=True,
199+
user = models.ForeignKey(oauth2_settings.RESOURCE_OWNER_MODEL, blank=True, null=True,
201200
on_delete=models.CASCADE)
202201
token = models.CharField(max_length=255, unique=True)
203202
application = models.ForeignKey(oauth2_settings.APPLICATION_MODEL,
@@ -270,7 +269,7 @@ class RefreshToken(models.Model):
270269
* :attr:`access_token` AccessToken instance this refresh token is
271270
bounded to
272271
"""
273-
user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE)
272+
user = models.ForeignKey(oauth2_settings.RESOURCE_OWNER_MODEL, on_delete=models.CASCADE)
274273
token = models.CharField(max_length=255, unique=True)
275274
application = models.ForeignKey(oauth2_settings.APPLICATION_MODEL,
276275
on_delete=models.CASCADE)
@@ -289,6 +288,20 @@ def __str__(self):
289288
return self.token
290289

291290

291+
def get_resource_owner_model():
292+
""" Return the Resource Owner model that is active in this project. """
293+
try:
294+
app_label, model_name = oauth2_settings.RESOURCE_OWNER_MODEL.split('.')
295+
except ValueError:
296+
e = "RESOURCE_OWNER_MODEL must be of the form 'app_label.model_name'"
297+
raise ImproperlyConfigured(e)
298+
app_model = apps.get_model(app_label, model_name)
299+
if app_model is None:
300+
e = "RESOURCE_OWNER_MODEL refers to model {0} that has not been installed"
301+
raise ImproperlyConfigured(e.format(oauth2_settings.RESOURCE_OWNER_MODEL))
302+
return app_model
303+
304+
292305
def get_application_model():
293306
""" Return the Application model that is active in this project. """
294307
try:

oauth2_provider/oauth2_backends.py

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,15 @@ def _get_escaped_full_path(self, request):
3232

3333
return urlunparse(parsed)
3434

35+
def _extract_resource_owner(self, request):
36+
"""
37+
Extracts the resource owner object from the Django request object
38+
:param request: The current django.http.HttpRequest object
39+
:return: the Resource Owner object
40+
"""
41+
return request.user
42+
43+
3544
def _get_extra_credentials(self, request):
3645
"""
3746
Produce extra credentials for token response. This dictionary will be
@@ -106,14 +115,14 @@ def create_authorization_response(self, request, scopes, credentials, allow):
106115
:param scopes: A list of provided scopes
107116
:param credentials: Authorization credentials dictionary containing
108117
`client_id`, `state`, `redirect_uri`, `response_type`
109-
:param allow: True if the user authorize the client, otherwise False
118+
:param allow: True if the resource owner authorize the client, otherwise False
110119
"""
111120
try:
112121
if not allow:
113122
raise oauth2.AccessDeniedError()
114123

115-
# add current user to credentials. this will be used by OAUTH2_VALIDATOR_CLASS
116-
credentials['user'] = request.user
124+
# add current resource owner to credentials. this will be used by OAUTH2_VALIDATOR_CLASS
125+
credentials['user'] = self._extract_resource_owner(request)
117126

118127
headers, body, status = self.server.create_authorization_response(
119128
uri=credentials['redirect_uri'], scopes=scopes, credentials=credentials)

oauth2_provider/oauth2_validators.py

Lines changed: 14 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,13 @@
3131

3232

3333
class OAuth2Validator(RequestValidator):
34+
35+
def _extract_token_user(self, token):
36+
return token.user
37+
38+
def _extract_resource_owner(self, request):
39+
return request.user
40+
3441
def _extract_basic_auth(self, request):
3542
"""
3643
Return authentication string if request contains basic auth credentials,
@@ -238,7 +245,7 @@ def validate_bearer_token(self, token, scopes, request):
238245
token=token)
239246
if access_token.is_valid(scopes):
240247
request.client = access_token.application
241-
request.user = access_token.user
248+
request.user = self._extract_token_user(access_token)
242249
request.scopes = scopes
243250

244251
# this is needed by django rest framework
@@ -253,7 +260,7 @@ def validate_code(self, client_id, code, client, request, *args, **kwargs):
253260
grant = Grant.objects.get(code=code, application=client)
254261
if not grant.is_expired():
255262
request.scopes = grant.scope.split(' ')
256-
request.user = grant.user
263+
request.user = self._extract_token_user(grant)
257264
return True
258265
return False
259266

@@ -296,7 +303,7 @@ def validate_redirect_uri(self, client_id, redirect_uri, request, *args, **kwarg
296303
def save_authorization_code(self, client_id, code, request, *args, **kwargs):
297304
expires = timezone.now() + timedelta(
298305
seconds=oauth2_settings.AUTHORIZATION_CODE_EXPIRE_SECONDS)
299-
g = Grant(application=request.client, user=request.user, code=code['code'],
306+
g = Grant(application=request.client, user=self._extract_resource_owner(request), code=code['code'],
300307
expires=expires, redirect_uri=request.redirect_uri,
301308
scope=' '.join(request.scopes))
302309
g.save()
@@ -344,7 +351,7 @@ def save_bearer_token(self, token, request, *args, **kwargs):
344351
access_token = AccessToken.objects.select_for_update().get(
345352
pk=refresh_token_instance.access_token.pk
346353
)
347-
access_token.user = request.user
354+
access_token.user = self._extract_resource_owner(request)
348355
access_token.scope = token['scope']
349356
access_token.expires = expires
350357
access_token.token = token['access_token']
@@ -365,7 +372,7 @@ def save_bearer_token(self, token, request, *args, **kwargs):
365372
access_token = self._create_access_token(expires, request, token)
366373

367374
refresh_token = RefreshToken(
368-
user=request.user,
375+
user=self._extract_resource_owner(request),
369376
token=refresh_token_code,
370377
application=request.client,
371378
access_token=access_token
@@ -381,7 +388,7 @@ def save_bearer_token(self, token, request, *args, **kwargs):
381388

382389
def _create_access_token(self, expires, request, token):
383390
access_token = AccessToken(
384-
user=request.user,
391+
user=self._extract_resource_owner(request),
385392
scope=token['scope'],
386393
expires=expires,
387394
token=token['access_token'],
@@ -437,7 +444,7 @@ def validate_refresh_token(self, refresh_token, client, request, *args, **kwargs
437444
"""
438445
try:
439446
rt = RefreshToken.objects.get(token=refresh_token)
440-
request.user = rt.user
447+
request.user = self._extract_token_user(rt)
441448
request.refresh_token = rt.token
442449
# Temporary store RefreshToken instance to be reused by get_original_scopes.
443450
request.refresh_token_instance = rt

oauth2_provider/settings.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@
4343
'REFRESH_TOKEN_EXPIRE_SECONDS': None,
4444
'ROTATE_REFRESH_TOKEN': True,
4545
'APPLICATION_MODEL': getattr(settings, 'OAUTH2_PROVIDER_APPLICATION_MODEL', 'oauth2_provider.Application'),
46+
'RESOURCE_OWNER_MODEL': getattr(settings, 'OAUTH2_PROVIDER_RESOURCE_OWNER_MODEL', settings.AUTH_USER_MODEL),
4647
'REQUEST_APPROVAL_PROMPT': 'force',
4748
'ALLOWED_REDIRECT_URI_SCHEMES': ['http', 'https'],
4849

oauth2_provider/tests/models.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
1+
from django.conf import settings
12
from django.db import models
23
from oauth2_provider.models import AbstractApplication
34

45

56
class TestApplication(AbstractApplication):
67
custom_field = models.CharField(max_length=255)
8+
9+
class TestResourceOwner(models.Model):
10+
user = models.ForeignKey(settings.AUTH_USER_MODEL, related_name="resource_owners")

oauth2_provider/tests/settings.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -126,3 +126,5 @@
126126
},
127127
}
128128
}
129+
130+
OAUTH2_PROVIDER_RESOURCE_OWNER_MODEL = 'auth.User'

oauth2_provider/tests/test_models.py

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,16 +7,20 @@
77
from django.test.utils import override_settings
88
from django.utils import timezone
99

10-
from ..models import get_application_model, Grant, AccessToken, RefreshToken
10+
from ..models import get_application_model, get_resource_owner_model
11+
from ..models import Grant, AccessToken, RefreshToken
1112

1213

1314
Application = get_application_model()
15+
ResourceOwnerModel = get_resource_owner_model()
1416
UserModel = get_user_model()
1517

1618

19+
1720
class TestModels(TestCase):
1821
def setUp(self):
1922
self.user = UserModel.objects.create_user("test_user", "[email protected]", "123456")
23+
self.resource_owner = self.user
2024

2125
def test_allow_scopes(self):
2226
self.client.login(username="test_user", password="123456")
@@ -29,7 +33,7 @@ def test_allow_scopes(self):
2933
)
3034

3135
access_token = AccessToken(
32-
user=self.user,
36+
user=self.resource_owner,
3337
scope='read write',
3438
expires=0,
3539
token='',
@@ -88,15 +92,15 @@ def test_scopes_property(self):
8892
)
8993

9094
access_token = AccessToken(
91-
user=self.user,
95+
user=self.resource_owner,
9296
scope='read write',
9397
expires=0,
9498
token='',
9599
application=app
96100
)
97101

98102
access_token2 = AccessToken(
99-
user=self.user,
103+
user=self.resource_owner,
100104
scope='write',
101105
expires=0,
102106
token='',
@@ -150,6 +154,7 @@ def test_expires_can_be_none(self):
150154
class TestAccessTokenModel(TestCase):
151155
def setUp(self):
152156
self.user = UserModel.objects.create_user("test_user", "[email protected]", "123456")
157+
self.resource_owner = self.user
153158

154159
def test_str(self):
155160
access_token = AccessToken(token="test_token")
@@ -177,3 +182,12 @@ class TestRefreshTokenModel(TestCase):
177182
def test_str(self):
178183
refresh_token = RefreshToken(token="test_token")
179184
self.assertEqual("%s" % refresh_token, refresh_token.token)
185+
186+
187+
@override_settings(OAUTH2_PROVIDER={'RESOURCE_OWNER_MODEL':'tests.TestResourceOwner'})
188+
class TestCustomResourceOwnerModel(TestCase):
189+
def setUp(self):
190+
self.user = UserModel.objects.create_user("test_user", "[email protected]", "123456")
191+
192+
def test_model(self):
193+
self.user.resource_owners.all()

oauth2_provider/tests/test_oauth2_backends.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,9 @@ class MyOAuthLibCore(OAuthLibCore):
5050
def _get_extra_credentials(self, request):
5151
return 1
5252

53+
def extract_resource_owner(self, request):
54+
return request.organization_user
55+
5356
def setUp(self):
5457
self.factory = RequestFactory()
5558

oauth2_provider/views/token.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,13 +14,15 @@ class AuthorizedTokensListView(LoginRequiredMixin, ListView):
1414
context_object_name = 'authorized_tokens'
1515
template_name = 'oauth2_provider/authorized-tokens.html'
1616
model = AccessToken
17+
user_lookup_attr = 'user'
1718

1819
def get_queryset(self):
1920
"""
2021
Show only user's tokens
2122
"""
2223
return super(AuthorizedTokensListView, self).get_queryset()\
23-
.select_related('application').filter(user=self.request.user)
24+
.select_related('application')\
25+
.filter(**{self.user_lookup_attr:self.request.user})
2426

2527

2628
class AuthorizedTokenDeleteView(LoginRequiredMixin, DeleteView):
@@ -30,6 +32,8 @@ class AuthorizedTokenDeleteView(LoginRequiredMixin, DeleteView):
3032
template_name = 'oauth2_provider/authorized-token-delete.html'
3133
success_url = reverse_lazy('oauth2_provider:authorized-token-list')
3234
model = AccessToken
35+
user_lookup_attr = 'user'
3336

3437
def get_queryset(self):
35-
return super(AuthorizedTokenDeleteView, self).get_queryset().filter(user=self.request.user)
38+
return super(AuthorizedTokenDeleteView, self).get_queryset()\
39+
.filter(**{self.user_lookup_attr:self.request.user})

0 commit comments

Comments
 (0)