-
Notifications
You must be signed in to change notification settings - Fork 4.3k
ARCH-135 Add new custom DOT Application model to support OAuth2 per-application scopes. #18416
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| 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), | ||
| ] | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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 | ||
|
|
||
|
|
@@ -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, | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Should this be dependent on the setting - or always related to What if someone changes the setting? That won't result in a new migration. But that should be Ok, I believe.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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, | ||
| ) | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -4,7 +4,6 @@ | |
| # | ||
| # make upgrade | ||
| # | ||
|
|
||
| common/lib/calc | ||
| common/lib/chem | ||
| common/lib/sandbox-packages | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -4,7 +4,6 @@ | |
| # | ||
| # make upgrade | ||
| # | ||
|
|
||
| -e common/lib/calc | ||
| -e common/lib/chem | ||
| -e common/lib/sandbox-packages | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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 | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is this new dependency still needed?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 |
||
| django-oauth-toolkit==0.12.0 | ||
| django-pyfs | ||
| django-ratelimit | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -4,7 +4,6 @@ | |
| # | ||
| # make upgrade | ||
| # | ||
|
|
||
| coverage==4.2 | ||
| diff-cover==0.9.8 | ||
| inflect==0.3.1 # via jinja2-pluralize | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -4,7 +4,6 @@ | |
| # | ||
| # make upgrade | ||
| # | ||
|
|
||
| argh==0.26.2 # via watchdog | ||
| argparse==1.4.0 # via stevedore | ||
| edx-opaque-keys==0.4.4 | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -4,7 +4,6 @@ | |
| # | ||
| # make upgrade | ||
| # | ||
|
|
||
| click==6.7 # via pip-tools | ||
| first==2.0.1 # via pip-tools | ||
| pip-tools==2.0.2 | ||
|
|
||
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.