Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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)),
],
),
]
Original file line number Diff line number Diff line change
@@ -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),

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think you may actually want to implement a proper reverse here because if we end up making changes to the new table, we will probably want to propagate them back to the old table so auth does not break. This is an unlikely case but I'd rather have it implemented correctly if we need it.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We won't need it. The new table will not be accessible from the app until we are certain we won't be rolling back.

]
88 changes: 88 additions & 0 deletions openedx/core/djangoapps/oauth_dispatch/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@
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

Expand Down Expand Up @@ -43,3 +46,88 @@ def verify_access_token_as_expired(cls, access_token):
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,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this be dependent on the setting - or always related to ScopedApplication since they seem to be sister models? Each way has its own trade-offs, of course. I suppose the current way is more extensible, right?

What if someone changes the setting? That won't result in a new migration. But that should be Ok, I believe.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, I think this is good as is.

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,
)
1 change: 0 additions & 1 deletion requirements/edx-sandbox/base.txt
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@
#
# make upgrade
#

common/lib/calc
common/lib/chem
common/lib/sandbox-packages
Expand Down
1 change: 0 additions & 1 deletion requirements/edx-sandbox/shared.txt
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@
#
# make upgrade
#

-e common/lib/calc
-e common/lib/chem
-e common/lib/sandbox-packages
Expand Down
1 change: 1 addition & 0 deletions requirements/edx/base.in
Original file line number Diff line number Diff line change
Expand Up @@ -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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this new dependency still needed?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, I'm using this for the list of strings filed that stores the scopes on ScopedApplication.

django-oauth-toolkit==0.12.0
django-pyfs
django-ratelimit
Expand Down
4 changes: 2 additions & 2 deletions requirements/edx/base.txt
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@
#
# make upgrade
#

-e git+https://github.com/edx/acid-block.git@e46f9cda8a03e121a00c7e347084d142d22ebfb7#egg=acid-xblock
-e common/lib/calc
-e common/lib/capa
Expand Down Expand Up @@ -84,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
Expand All @@ -93,7 +93,7 @@ django-require==1.0.11
django-rest-swagger==2.2.0
django-sekizai==0.10.0
django-ses==0.8.4
django-simple-history==2.1.0
django-simple-history==2.1.1
django-splash==0.2.2
django-statici18n==1.4.0
django-storages==1.4.1
Expand Down
1 change: 0 additions & 1 deletion requirements/edx/coverage.txt
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@
#
# make upgrade
#

coverage==4.2
diff-cover==0.9.8
inflect==0.3.1 # via jinja2-pluralize
Expand Down
12 changes: 6 additions & 6 deletions requirements/edx/development.txt
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@
#
# make upgrade
#

-e git+https://github.com/edx/acid-block.git@e46f9cda8a03e121a00c7e347084d142d22ebfb7#egg=acid-xblock
-e common/lib/calc
-e common/lib/capa
Expand Down Expand Up @@ -44,7 +43,7 @@ git+https://github.com/edx-solutions/xblock-drag-and-drop-v2@v2.1.6#egg=xblock-d
git+https://github.com/open-craft/xblock-poll@add89e14558c30f3c8dc7431e5cd6536fff6d941#egg=xblock-poll==1.5.1
git+https://github.com/edx/xblock-utils.git@v1.1.1#egg=xblock-utils==1.1.1
-e common/lib/xmodule
alabaster==0.7.10 # via sphinx
alabaster==0.7.11 # via sphinx
amqp==1.4.9
analytics-python==1.1.0
anyjson==0.3.3
Expand Down Expand Up @@ -104,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
Expand All @@ -113,7 +113,7 @@ django-require==1.0.11
django-rest-swagger==2.2.0
django-sekizai==0.10.0
django-ses==0.8.4
django-simple-history==2.1.0
django-simple-history==2.1.1
django-splash==0.2.2
django-statici18n==1.4.0
django-storages==1.4.1
Expand Down Expand Up @@ -156,7 +156,7 @@ event-tracking==0.2.4
execnet==1.5.0
extras==1.0.0
factory_boy==2.8.1
faker==0.8.15
faker==0.8.16
feedparser==5.1.3
firebase-token-generator==1.3.2
first==2.0.1
Expand Down Expand Up @@ -270,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
Expand Down Expand Up @@ -316,7 +316,7 @@ sqlparse==0.2.4 # via django-debug-toolbar
stevedore==1.10.0
sure==1.4.11
sympy==0.7.1
testfixtures==6.1.0
testfixtures==6.2.0
testtools==2.3.0
text-unidecode==1.2
tox-battery==0.5.1
Expand Down
1 change: 0 additions & 1 deletion requirements/edx/paver.txt
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@
#
# make upgrade
#

argh==0.26.2 # via watchdog
argparse==1.4.0 # via stevedore
edx-opaque-keys==0.4.4
Expand Down
1 change: 0 additions & 1 deletion requirements/edx/pip-tools.txt
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@
#
# make upgrade
#

click==6.7 # via pip-tools
first==2.0.1 # via pip-tools
pip-tools==2.0.2
Expand Down
10 changes: 5 additions & 5 deletions requirements/edx/testing.txt
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@
#
# make upgrade
#

-e git+https://github.com/edx/acid-block.git@e46f9cda8a03e121a00c7e347084d142d22ebfb7#egg=acid-xblock
-e common/lib/calc
-e common/lib/capa
Expand Down Expand Up @@ -100,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
Expand All @@ -109,7 +109,7 @@ django-require==1.0.11
django-rest-swagger==2.2.0
django-sekizai==0.10.0
django-ses==0.8.4
django-simple-history==2.1.0
django-simple-history==2.1.1
django-splash==0.2.2
django-statici18n==1.4.0
django-storages==1.4.1
Expand Down Expand Up @@ -150,7 +150,7 @@ event-tracking==0.2.4
execnet==1.5.0 # via pytest-xdist
extras==1.0.0 # via python-subunit, testtools
factory_boy==2.8.1
faker==0.8.15 # via factory-boy
faker==0.8.16 # via factory-boy
feedparser==5.1.3
firebase-token-generator==1.3.2
fixtures==3.0.0 # via testtools
Expand Down Expand Up @@ -259,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
Expand Down Expand Up @@ -300,7 +300,7 @@ splinter==0.8.0
stevedore==1.10.0
sure==1.4.11
sympy==0.7.1
testfixtures==6.1.0
testfixtures==6.2.0
testtools==2.3.0 # via fixtures, python-subunit
text-unidecode==1.2 # via faker
tox-battery==0.5.1
Expand Down