Merge pull request #18416 from edx/douglashall/oauth_scopes_part1

ARCH-135 Add new custom DOT Application model to support OAuth2 per-application scopes.
This commit is contained in:
Douglas Hall
2018-06-20 16:58:58 -04:00
committed by GitHub
7 changed files with 180 additions and 2 deletions

View File

@@ -0,0 +1,46 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.13 on 2018-06-20 18:22
from __future__ import unicode_literals
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
import django_mysql.models
import oauth2_provider.generators
import oauth2_provider.validators
class Migration(migrations.Migration):
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
migrations.swappable_dependency(settings.OAUTH2_PROVIDER_APPLICATION_MODEL),
('oauth_dispatch', '0001_initial'),
]
operations = [
migrations.CreateModel(
name='ScopedApplication',
fields=[
('client_id', models.CharField(db_index=True, default=oauth2_provider.generators.generate_client_id, max_length=100, unique=True)),
('redirect_uris', models.TextField(blank=True, help_text='Allowed URIs list, space separated', validators=[oauth2_provider.validators.validate_uris])),
('client_type', models.CharField(choices=[('confidential', 'Confidential'), ('public', 'Public')], max_length=32)),
('authorization_grant_type', models.CharField(choices=[('authorization-code', 'Authorization code'), ('implicit', 'Implicit'), ('password', 'Resource owner password-based'), ('client-credentials', 'Client credentials')], max_length=32)),
('client_secret', models.CharField(blank=True, db_index=True, default=oauth2_provider.generators.generate_client_secret, max_length=255)),
('name', models.CharField(blank=True, max_length=255)),
('skip_authorization', models.BooleanField(default=False)),
('id', models.IntegerField(primary_key=True, serialize=False)),
('scopes', django_mysql.models.ListCharField(models.CharField(max_length=32), help_text='Comma-separated list of scopes that this application will be allowed to request.', max_length=825, size=25)),
('user', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='oauth_dispatch_scopedapplication', to=settings.AUTH_USER_MODEL)),
],
),
migrations.CreateModel(
name='ScopedApplicationOrganization',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('short_name', models.CharField(help_text='The short_name of an existing Organization.', max_length=255)),
('provider_type', models.CharField(choices=[(b'content_org', 'Content Provider')], default=b'content_org', max_length=32)),
('application', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='organizations', to=settings.OAUTH2_PROVIDER_APPLICATION_MODEL)),
],
),
]

View File

@@ -0,0 +1,40 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.13 on 2018-06-05 13:19
from __future__ import unicode_literals
from django.conf import settings
from django.db import migrations
def migrate_application_data(apps, schema_editor):
"""
Migrate existing DOT Application models to new ScopedApplication models.
"""
Application = apps.get_model(settings.OAUTH2_PROVIDER_APPLICATION_MODEL)
ScopedApplication = apps.get_model('oauth_dispatch', 'ScopedApplication')
for application in Application.objects.all():
ScopedApplication.objects.update_or_create(
id=application.id,
defaults={
'client_id': application.client_id,
'user': application.user,
'redirect_uris': application.redirect_uris,
'client_type': application.client_type,
'authorization_grant_type': application.authorization_grant_type,
'client_secret': application.client_secret,
'name': application.name,
'skip_authorization': application.skip_authorization,
}
)
class Migration(migrations.Migration):
dependencies = [
('oauth_dispatch', '0002_scopedapplication_scopedapplicationorganization'),
]
operations = [
migrations.RunPython(migrate_application_data, reverse_code=migrations.RunPython.noop),
]

View File

@@ -5,6 +5,9 @@ Specialized models for oauth_dispatch djangoapp
from datetime import datetime
from django.db import models
from django.utils.translation import ugettext_lazy as _
from django_mysql.models import ListCharField
from oauth2_provider.models import AbstractApplication
from oauth2_provider.settings import oauth2_settings
from pytz import utc
@@ -43,3 +46,88 @@ class RestrictedApplication(models.Model):
is set at the beginning of the epoch which is Jan. 1, 1970
"""
return access_token.expires == datetime(1970, 1, 1, tzinfo=utc)
class ScopedApplication(AbstractApplication):
"""
Custom Django OAuth Toolkit Application model that enables the definition
of scopes that are authorized for the given Application.
"""
FILTER_USER_ME = 'user:me'
# TODO: Remove the id field once we perform the inital migrations for this model.
# We need to copy data over from the oauth2_provider.models.Application model to
# this new model with the intial migration and the model IDs will need to match
# so that existing AccessTokens will still work when switching over to the new model.
# Once we have the data copied over we can move back to an auto-increment primary key.
id = models.IntegerField(primary_key=True)
scopes = ListCharField(
base_field=models.CharField(max_length=32),
size=25,
max_length=(25 * 33), # 25 * 32 character scopes, plus commas
help_text=_('Comma-separated list of scopes that this application will be allowed to request.'),
)
class Meta:
app_label = 'oauth_dispatch'
def __unicode__(self):
"""
Return a unicode representation of this object.
"""
return u"<ScopedApplication '{name}'>".format(
name=self.name
)
@property
def authorization_filters(self):
"""
Return the list of authorization filters for this application.
"""
filters = [':'.join([org.provider_type, org.short_name]) for org in self.organizations.all()]
if self.authorization_grant_type == self.GRANT_CLIENT_CREDENTIALS:
filters.append(self.FILTER_USER_ME)
return filters
class ScopedApplicationOrganization(models.Model):
"""
Associates an organization to a given ScopedApplication including the
provider type of the organization so that organization-based filters
can be added to access tokens provided to the given Application.
See openedx/core/djangoapps/oauth_dispatch/docs/decisions/0007-include-organizations-in-tokens.rst
for the intended use of this model.
"""
CONTENT_PROVIDER_TYPE = 'content_org'
ORGANIZATION_PROVIDER_TYPES = (
(CONTENT_PROVIDER_TYPE, _('Content Provider')),
)
# In practice, short_name should match the short_name of an Organization model.
# This is not a foreign key because the organizations app is not installed by default.
short_name = models.CharField(
max_length=255,
help_text=_('The short_name of an existing Organization.'),
)
provider_type = models.CharField(
max_length=32,
choices=ORGANIZATION_PROVIDER_TYPES,
default=CONTENT_PROVIDER_TYPE,
)
application = models.ForeignKey(
oauth2_settings.APPLICATION_MODEL,
related_name='organizations',
)
class Meta:
app_label = 'oauth_dispatch'
def __unicode__(self):
"""
Return a unicode representation of this object.
"""
return u"<ScopedApplicationOrganization '{application_name}':'{org}'>".format(
application_name=self.application.name,
org=self.short_name,
)

View File

@@ -46,6 +46,7 @@ django-memcached-hashring
django-method-override==0.1.0
django-model-utils==3.0.0
django-mptt>=0.8.6,<0.9
django-mysql
django-oauth-toolkit==0.12.0
django-pyfs
django-ratelimit

View File

@@ -83,6 +83,7 @@ django-method-override==0.1.0
django-model-utils==3.0.0
django-mptt==0.8.7
django-multi-email-field==0.5.1 # via edx-enterprise
django-mysql==2.3.0
django-oauth-toolkit==0.12.0
django-object-actions==0.10.0 # via edx-enterprise
django-pyfs==2.0

View File

@@ -103,6 +103,7 @@ django-method-override==0.1.0
django-model-utils==3.0.0
django-mptt==0.8.7
django-multi-email-field==0.5.1
django-mysql==2.3.0
django-oauth-toolkit==0.12.0
django-object-actions==0.10.0
django-pyfs==2.0
@@ -269,7 +270,7 @@ pytest-django==3.1.2
pytest-forked==0.2
pytest-randomly==1.2.3
pytest-xdist==1.22.2
pytest==3.6.1
pytest==3.6.2
python-dateutil==2.4.0
python-levenshtein==0.12.0
python-memcached==1.48

View File

@@ -99,6 +99,7 @@ django-method-override==0.1.0
django-model-utils==3.0.0
django-mptt==0.8.7
django-multi-email-field==0.5.1
django-mysql==2.3.0
django-oauth-toolkit==0.12.0
django-object-actions==0.10.0
django-pyfs==2.0
@@ -258,7 +259,7 @@ pytest-django==3.1.2
pytest-forked==0.2 # via pytest-xdist
pytest-randomly==1.2.3
pytest-xdist==1.22.2
pytest==3.6.1
pytest==3.6.2
python-dateutil==2.4.0
python-levenshtein==0.12.0
python-memcached==1.48