Skip to content

Commit 2d5c11c

Browse files
authored
Merge pull request #305 from stefanfoulis/feature/swappable-models
Add OIDC_CLIENT_MODEL setting to enable client model swapping (rebased)
2 parents 5ac1cdf + 3cc27bd commit 2d5c11c

File tree

13 files changed

+138
-14
lines changed

13 files changed

+138
-14
lines changed

docs/sections/settings.rst

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,15 @@ If not specified, it will be automatically generated using ``request.scheme`` an
2121

2222
For example ``http://localhost:8000``.
2323

24+
OIDC_CLIENT_MODEL
25+
=================
26+
27+
OPTIONAL. ``str``. The client model.
28+
29+
If not specified, the default oidc_provider.Client model is used. This is typically used when
30+
you need to override the Client model to add custom properties on the class. The custom class
31+
should override the oidc_provider.AbstractClient model.
32+
2433
OIDC_AFTER_USERLOGIN_HOOK
2534
=========================
2635

oidc_provider/lib/endpoints/authorize.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,13 +26,14 @@
2626
encode_id_token,
2727
)
2828
from oidc_provider.models import (
29-
Client,
3029
UserConsent,
30+
get_client_model
3131
)
3232
from oidc_provider import settings
3333
from oidc_provider.lib.utils.common import get_browser_state_or_default
3434

3535
logger = logging.getLogger(__name__)
36+
Client = get_client_model()
3637

3738

3839
class AuthorizeEndpoint(object):

oidc_provider/lib/endpoints/token.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,13 +17,14 @@
1717
encode_id_token,
1818
)
1919
from oidc_provider.models import (
20-
Client,
2120
Code,
2221
Token,
22+
get_client_model
2323
)
2424
from oidc_provider import settings
2525

2626
logger = logging.getLogger(__name__)
27+
Client = get_client_model()
2728

2829

2930
class TokenEndpoint(object):

oidc_provider/migrations/0001_initial.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,15 @@
33

44
from django.db import models, migrations
55
from django.conf import settings
6+
from oidc_provider import settings as oidc_settings
67

78

89
class Migration(migrations.Migration):
910

1011
dependencies = [
1112
('auth', '0001_initial'),
1213
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
14+
migrations.swappable_dependency(oidc_settings.get('OIDC_CLIENT_MODEL')),
1315
]
1416

1517
operations = [
@@ -26,6 +28,8 @@ class Migration(migrations.Migration):
2628
('_redirect_uris', models.TextField(default=b'')),
2729
],
2830
options={
31+
'abstract': False,
32+
'swappable': 'OIDC_CLIENT_MODEL'
2933
},
3034
bases=(models.Model,),
3135
),
@@ -36,7 +40,7 @@ class Migration(migrations.Migration):
3640
('expires_at', models.DateTimeField()),
3741
('_scope', models.TextField(default=b'')),
3842
('code', models.CharField(unique=True, max_length=255)),
39-
('client', models.ForeignKey(to='oidc_provider.Client', on_delete=models.CASCADE)),
43+
('client', models.ForeignKey(oidc_settings.get('OIDC_CLIENT_MODEL'), on_delete=models.CASCADE)),
4044
],
4145
options={
4246
'abstract': False,
@@ -51,7 +55,7 @@ class Migration(migrations.Migration):
5155
('_scope', models.TextField(default=b'')),
5256
('access_token', models.CharField(unique=True, max_length=255)),
5357
('_id_token', models.TextField()),
54-
('client', models.ForeignKey(to='oidc_provider.Client', on_delete=models.CASCADE)),
58+
('client', models.ForeignKey(oidc_settings.get('OIDC_CLIENT_MODEL'), on_delete=models.CASCADE)),
5559
],
5660
options={
5761
'abstract': False,

oidc_provider/migrations/0002_userconsent.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44
from django.db import models, migrations
55
from django.conf import settings
66

7+
from oidc_provider import settings as oidc_settings
8+
79

810
class Migration(migrations.Migration):
911

@@ -19,7 +21,7 @@ class Migration(migrations.Migration):
1921
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
2022
('expires_at', models.DateTimeField()),
2123
('_scope', models.TextField(default=b'')),
22-
('client', models.ForeignKey(to='oidc_provider.Client', on_delete=models.CASCADE)),
24+
('client', models.ForeignKey(to=oidc_settings.get('OIDC_CLIENT_MODEL'), on_delete=models.CASCADE)),
2325
('user', models.ForeignKey(to=settings.AUTH_USER_MODEL, on_delete=models.CASCADE)),
2426
],
2527
options={

oidc_provider/migrations/0016_userconsent_and_verbosenames.py

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@
88
import django.db.models.deletion
99
from django.utils.timezone import utc
1010

11+
from oidc_provider import settings as oidc_settings
12+
1113

1214
class Migration(migrations.Migration):
1315

@@ -79,7 +81,7 @@ class Migration(migrations.Migration):
7981
model_name='code',
8082
name='client',
8183
field=models.ForeignKey(
82-
on_delete=django.db.models.deletion.CASCADE, to='oidc_provider.Client', verbose_name='Client'),
84+
on_delete=django.db.models.deletion.CASCADE, to=oidc_settings.get('OIDC_CLIENT_MODEL'), verbose_name='Client'),
8385
),
8486
migrations.AlterField(
8587
model_name='code',
@@ -141,7 +143,7 @@ class Migration(migrations.Migration):
141143
model_name='token',
142144
name='client',
143145
field=models.ForeignKey(
144-
on_delete=django.db.models.deletion.CASCADE, to='oidc_provider.Client', verbose_name='Client'),
146+
on_delete=django.db.models.deletion.CASCADE, to=oidc_settings.get('OIDC_CLIENT_MODEL'), verbose_name='Client'),
145147
),
146148
migrations.AlterField(
147149
model_name='token',
@@ -168,7 +170,7 @@ class Migration(migrations.Migration):
168170
model_name='userconsent',
169171
name='client',
170172
field=models.ForeignKey(
171-
on_delete=django.db.models.deletion.CASCADE, to='oidc_provider.Client', verbose_name='Client'),
173+
on_delete=django.db.models.deletion.CASCADE, to=oidc_settings.get('OIDC_CLIENT_MODEL'), verbose_name='Client'),
172174
),
173175
migrations.AlterField(
174176
model_name='userconsent',
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
# -*- coding: utf-8 -*-
2+
# Generated by Django 1.11.4 on 2018-12-07 14:12
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+
9+
10+
class Migration(migrations.Migration):
11+
12+
dependencies = [
13+
('oidc_provider', '0026_client_multiple_response_types'),
14+
]
15+
16+
operations = [
17+
migrations.AlterField(
18+
model_name='client',
19+
name='owner',
20+
field=models.ForeignKey(blank=True, default=None, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='oidc_provider_client_set', to=settings.AUTH_USER_MODEL, verbose_name='Owner'),
21+
),
22+
migrations.AlterField(
23+
model_name='client',
24+
name='response_types',
25+
field=models.ManyToManyField(related_name='oidc_provider_client_set', to='oidc_provider.ResponseType'),
26+
),
27+
]

oidc_provider/models.py

Lines changed: 22 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,14 @@
44
from hashlib import md5, sha256
55
import json
66

7+
from django.apps import apps
78
from django.db import models
89
from django.utils import timezone
910
from django.utils.translation import ugettext_lazy as _
1011
from django.conf import settings
1112

13+
from oidc_provider import settings as oidc_settings
14+
1215

1316
CLIENT_TYPE_CHOICES = [
1417
('confidential', 'Confidential'),
@@ -54,12 +57,13 @@ def __str__(self):
5457
return u'{0}'.format(self.description)
5558

5659

57-
class Client(models.Model):
60+
class AbstractClient(models.Model):
5861

5962
name = models.CharField(max_length=100, default='', verbose_name=_(u'Name'))
6063
owner = models.ForeignKey(
6164
settings.AUTH_USER_MODEL, verbose_name=_(u'Owner'), blank=True,
62-
null=True, default=None, on_delete=models.SET_NULL, related_name='oidc_clients_set')
65+
null=True, default=None, on_delete=models.SET_NULL,
66+
related_name='%(app_label)s_%(class)s_set')
6367
client_type = models.CharField(
6468
max_length=30,
6569
choices=CLIENT_TYPE_CHOICES,
@@ -69,7 +73,8 @@ class Client(models.Model):
6973
u' of their credentials. <b>Public</b> clients are incapable.'))
7074
client_id = models.CharField(max_length=255, unique=True, verbose_name=_(u'Client ID'))
7175
client_secret = models.CharField(max_length=255, blank=True, verbose_name=_(u'Client SECRET'))
72-
response_types = models.ManyToManyField(ResponseType)
76+
response_types = models.ManyToManyField(
77+
ResponseType, related_name='%(app_label)s_%(class)s_set')
7378
jwt_alg = models.CharField(
7479
max_length=10,
7580
choices=JWT_ALGS,
@@ -115,6 +120,7 @@ class Client(models.Model):
115120
class Meta:
116121
verbose_name = _(u'Client')
117122
verbose_name_plural = _(u'Clients')
123+
abstract = True
118124

119125
def __str__(self):
120126
return u'{0}'.format(self.name)
@@ -158,9 +164,21 @@ def default_redirect_uri(self):
158164
return self.redirect_uris[0] if self.redirect_uris else ''
159165

160166

167+
class Client(AbstractClient):
168+
class Meta(AbstractClient.Meta):
169+
swappable = 'OIDC_CLIENT_MODEL'
170+
171+
172+
def get_client_model():
173+
""" Return the Application model that is active in this project. """
174+
return apps.get_model(oidc_settings.get('OIDC_CLIENT_MODEL'))
175+
176+
161177
class BaseCodeTokenModel(models.Model):
162178

163-
client = models.ForeignKey(Client, verbose_name=_(u'Client'), on_delete=models.CASCADE)
179+
client = models.ForeignKey(
180+
oidc_settings.get('OIDC_CLIENT_MODEL'), verbose_name=_(u'Client'),
181+
on_delete=models.CASCADE)
164182
expires_at = models.DateTimeField(verbose_name=_(u'Expiration Date'))
165183
_scope = models.TextField(default='', verbose_name=_(u'Scopes'))
166184

oidc_provider/settings.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,14 @@ def SITE_URL(self):
2525
"""
2626
return None
2727

28+
@property
29+
def OIDC_CLIENT_MODEL(self):
30+
"""
31+
OPTIONAL. Use a custom client model, typically used to extend the client model
32+
with custom fields. The custom model should override oidc_provider.AbstractClient.
33+
"""
34+
return 'oidc_provider.Client'
35+
2836
@property
2937
def OIDC_AFTER_USERLOGIN_HOOK(self):
3038
"""
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
from django.test import TestCase, override_settings
2+
from django.contrib.auth import get_user_model
3+
4+
from oidc_provider.models import get_client_model, Client
5+
from oidc_provider.tests.models import Client as CustomClient
6+
7+
UserModel = get_user_model()
8+
9+
10+
class TestModels(TestCase):
11+
12+
def test_retrieve_default_client_model(self):
13+
client = get_client_model()
14+
self.assertEqual(Client, client)
15+
16+
@override_settings(OIDC_CLIENT_MODEL='tests.Client')
17+
def test_retrireve_custom_client_model(self):
18+
client = get_client_model()
19+
self.assertEqual(CustomClient, client)
20+
21+
22+
@override_settings(OIDC_CLIENT_MODEL='tests.Client')
23+
class TestCustomClientModel(TestCase):
24+
25+
def test_custom_client_model(self):
26+
"""
27+
If a custom client model is installed, it should be present in
28+
the related objects.
29+
"""
30+
related_object_names = [
31+
f.name for f in UserModel._meta.get_fields()
32+
if (f.one_to_many or f.one_to_one) and f.auto_created and not f.concrete
33+
]
34+
self.assertIn("tests_client_set", related_object_names)
35+
36+
@override_settings(OIDC_CLIENT_MODEL='IncorrectModelFormat')
37+
def test_custom_application_model_incorrect_format_1(self):
38+
self.assertRaises(ValueError, get_client_model)
39+
40+
@override_settings(OIDC_CLIENT_MODEL='tests.ClientNotInstalled')
41+
def test_custom_application_model_incorrect_format_2(self):
42+
self.assertRaises(LookupError, get_client_model)

0 commit comments

Comments
 (0)