collect all gooyal oauth modules as gooyal oauth
This commit is contained in:
parent
2f195a2814
commit
bfb69353bf
10 changed files with 286 additions and 0 deletions
0
apps/gooyal_oauth/__init__.py
Normal file
0
apps/gooyal_oauth/__init__.py
Normal file
32
apps/gooyal_oauth/admin.py
Executable file
32
apps/gooyal_oauth/admin.py
Executable 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
5
apps/gooyal_oauth/apps.py
Executable file
|
|
@ -0,0 +1,5 @@
|
||||||
|
from django.apps import AppConfig
|
||||||
|
|
||||||
|
|
||||||
|
class GooyalOauthConfig(AppConfig):
|
||||||
|
name = 'apps.gooyal_oauth'
|
||||||
51
apps/gooyal_oauth/forms.py
Normal file
51
apps/gooyal_oauth/forms.py
Normal 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 = ()
|
||||||
49
apps/gooyal_oauth/migrations/0001_initial.py
Normal file
49
apps/gooyal_oauth/migrations/0001_initial.py
Normal 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,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
]
|
||||||
0
apps/gooyal_oauth/migrations/__init__.py
Normal file
0
apps/gooyal_oauth/migrations/__init__.py
Normal file
97
apps/gooyal_oauth/models.py
Normal file
97
apps/gooyal_oauth/models.py
Normal 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
|
||||||
25
apps/gooyal_oauth/scopes.py
Normal file
25
apps/gooyal_oauth/scopes.py
Normal 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)]
|
||||||
26
apps/gooyal_oauth/signals.py
Normal file
26
apps/gooyal_oauth/signals.py
Normal 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
|
||||||
|
)
|
||||||
|
|
@ -44,6 +44,7 @@ INSTALLED_APPS = [
|
||||||
'apps.transactions',
|
'apps.transactions',
|
||||||
'apps.gooyal_restrict_scopes',
|
'apps.gooyal_restrict_scopes',
|
||||||
'apps.gooyal_dynamic_scopes',
|
'apps.gooyal_dynamic_scopes',
|
||||||
|
'apps.gooyal_oauth',
|
||||||
]
|
]
|
||||||
|
|
||||||
MIDDLEWARE = [
|
MIDDLEWARE = [
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue