collect all gooyal oauth modules as gooyal oauth

This commit is contained in:
mahdavi 2020-06-08 10:01:31 +04:30
parent 2f195a2814
commit bfb69353bf
10 changed files with 286 additions and 0 deletions

View file

32
apps/gooyal_oauth/admin.py Executable file
View file

@ -0,0 +1,32 @@
"""
Django admin configuration for the gooyal-restrict-scopes package.
"""
from django.contrib import admin
from django.contrib.admin.sites import NotRegistered
from oauth2_provider.admin import ApplicationAdmin
from .models import Application
from .forms import ApplicationForm
from .models import Scope
# The restricted application is registered by Django OAuth Toolkit, but we want
# to provide our own admin that uses our form
try:
admin.site.unregister(Application)
except NotRegistered:
pass
@admin.register(Application)
class RestrictedApplicationAdmin(ApplicationAdmin):
form = ApplicationForm
@admin.register(Scope)
class ScopeAdmin(admin.ModelAdmin):
list_display = ('name', 'description', 'is_default')
# admin.site.register(RestrictedApplication, RestrictedApplicationAdmin)

5
apps/gooyal_oauth/apps.py Executable file
View file

@ -0,0 +1,5 @@
from django.apps import AppConfig
class GooyalOauthConfig(AppConfig):
name = 'apps.gooyal_oauth'

View file

@ -0,0 +1,51 @@
"""
Django forms for use with the gooyal-restrict-scopes package.
"""
from django import forms
from oauth2_provider.scopes import get_scopes_backend
class DelimitedListField(forms.MultipleChoiceField):
"""
Django form field that allows for the use of list widgets with a text field
containing a delimited list.
"""
delimiter = ','
def __init__(self, delimiter=None, *args, **kwargs):
super().__init__(*args, **kwargs)
self.delimiter = delimiter or self.delimiter
def prepare_value(self, value):
#  If the value is already a list or tuple, just use it as-is
if isinstance(value, (list, tuple)): return value
#  Otherwise, prepare the value by splitting on the delimiter, trimming
#  leading and trailing whitespace and excluding empty values
return [p.strip() for p in value.split(self.delimiter) if p.strip()]
def clean(self, value):
#  Let the parent clean the value first, then join the result using the
# specified delimiter
return self.delimiter.join(super().clean(value))
class ApplicationForm(forms.ModelForm):
"""
Form for creating or updating a restricted application.
"""
#  allowed_scope is a space-delimited list, but we want to present
#  a selection of valid scopes with checkboxes
allowed_scope = DelimitedListField(
label='Allowed scopes',
#  The choices and initial values are callables, because the scopes might
#  not be available at import type, e.g. if coming from the database
choices=lambda: get_scopes_backend().get_all_scopes().items(),
initial=lambda: get_scopes_backend().get_default_scopes(),
delimiter=' ',
widget=forms.CheckboxSelectMultiple
)
class Meta:
exclude = ()

View file

@ -0,0 +1,49 @@
# Generated by Django 3.0.6 on 2020-06-08 05:26
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
import oauth2_provider.generators
class Migration(migrations.Migration):
initial = True
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
migrations.swappable_dependency(settings.OAUTH2_PROVIDER_APPLICATION_MODEL),
]
operations = [
migrations.CreateModel(
name='Scope',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(help_text='The name of the scope.', max_length=255, unique=True)),
('description', models.TextField(help_text='A brief description of the scope. This text is displayed to users when authorising access for the scope.')),
('is_default', models.BooleanField(default=False, help_text='Indicates if this scope should be included in the default scopes.')),
('application', models.ForeignKey(blank=True, help_text='The application to which the scope belongs.', null=True, on_delete=django.db.models.deletion.CASCADE, related_name='scopes', to=settings.OAUTH2_PROVIDER_APPLICATION_MODEL)),
],
),
migrations.CreateModel(
name='Application',
fields=[
('id', models.BigAutoField(primary_key=True, serialize=False)),
('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')),
('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)),
('created', models.DateTimeField(auto_now_add=True)),
('updated', models.DateTimeField(auto_now=True)),
('allowed_scope', models.TextField(blank=True)),
('user', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='gooyal_oauth_application', to=settings.AUTH_USER_MODEL)),
],
options={
'abstract': False,
},
),
]

View file

View file

@ -0,0 +1,97 @@
"""
Django models for the gooyal-restrict-scopes package.
"""
import requests
from django.conf import settings
from django.db import models
from oauth2_provider.models import AbstractApplication
from oauth2_provider.scopes import get_scopes_backend
from oauth2_provider.settings import oauth2_settings
class Application(AbstractApplication):
"""
Application model for use with Django OAuth Toolkit that allows the scopes
available to an application to be restricted on a per-application basis.
"""
allowed_scope = models.TextField(blank=True)
@property
def allowed_scopes(self):
"""
Returns the set of allowed scope names for this application.
"""
all_scopes = set(get_scopes_backend().get_all_scopes().keys())
app_scopes = set(self.allowed_scope.split())
return app_scopes.intersection(all_scopes)
class Scope(models.Model):
"""
Django model for an OAuth scope.
"""
#: The application that created the scope
#  NOTE: This is not used to limit access to the scope in any way - we want the
# scope to be available to other applications in order to request access
#   to the resource it protects!
application = models.ForeignKey(
oauth2_settings.APPLICATION_MODEL,
models.CASCADE,
#  This field is nullable because it is only set for scopes created by
#  external resource servers, which have a corresponding OAuth application
#  record on the authorisation server
blank=True, null=True,
help_text='The application to which the scope belongs.',
related_name='scopes'
)
#: The name of the scope
name = models.CharField(
max_length=255,
unique=True,
help_text='The name of the scope.'
)
#: A brief description of the scope
description = models.TextField(
help_text='A brief description of the scope. This text is displayed '
'to users when authorising access for the scope.'
)
#: Indicates if the scope should be included in the default scopes
is_default = models.BooleanField(
default=False,
help_text='Indicates if this scope should be included in the default scopes.'
)
@classmethod
def register(cls, name, description, is_default=False):
"""
Registers a scope with the given values. It always creates an instance in
the local database, but if this resource server has an external authorisation
server, it will also register the scope there.
Returns ``True`` on success. Should raise on failure.
"""
endpoint = settings.RESOURCE_SERVER_REGISTER_SCOPE_URL
if endpoint:
#  If the endpoint is set, make the callout to the authz server
token = "Bearer {}".format(oauth2_settings.RESOURCE_SERVER_AUTH_TOKEN)
#  Let any failures bubble up
# The idea is to call this method during deployment as a post-migrate
#  hook, so we want failures to halt the deployment
response = requests.post(
endpoint,
json={
'name': name,
'description': description,
'is_default': is_default
},
headers={"Authorization": token}
)
#  Raise the exception for anything other than 20x responses
response.raise_for_status()
#  Always create/update the scope record locally
_ = Scope.objects.update_or_create(
name=name,
defaults={'description': description, 'is_default': is_default}
)
return True

View file

@ -0,0 +1,25 @@
"""
Django OAuth Toolkit scopes backend for the gooyal-dynamic-scopes package.
"""
from django.conf import settings
from django.utils import module_loading
from oauth2_provider.scopes import BaseScopes
from .models import Scope
class DynamicScopes(BaseScopes):
"""
Scopes backend that provides scopes from a Django model.
"""
def get_all_scopes(self):
return {scope.name: scope.description for scope in Scope.objects.all()}
def get_available_scopes(self, application=None, request=None, *args, **kwargs):
return list(self.get_all_scopes().keys())
def get_default_scopes(self, application=None, request=None, *args, **kwargs):
return [scope.name for scope in Scope.objects.filter(is_default=True)]

View file

@ -0,0 +1,26 @@
"""
Django signal handlers for the gooyal-dynamic-scopes package.
"""
from django.conf import settings
from oauth2_provider.settings import oauth2_settings
from .models import Scope
def register_scopes(app_config, verbosity=2, interactive=True, **kwargs):
"""
``post_migrate`` signal handler that ensures the scopes required for the
introspection and register-scope endpoints are registered when running as an
authorisation server.
The signal is connected in ``apps.py``.
"""
#  Register any scopes in the oauth2_provider settings
for name, description in oauth2_settings.SCOPES.items():
Scope.register(
name,
description,
name in oauth2_settings.DEFAULT_SCOPES
)

View file

@ -44,6 +44,7 @@ INSTALLED_APPS = [
'apps.transactions',
'apps.gooyal_restrict_scopes',
'apps.gooyal_dynamic_scopes',
'apps.gooyal_oauth',
]
MIDDLEWARE = [