bugfix in scope assignment

This commit is contained in:
mahdavi 2020-07-21 13:06:39 +04:30
parent 448594660c
commit 3ee2f1936a
19 changed files with 11 additions and 492 deletions

View file

@ -1 +0,0 @@
default_app_config = 'apps.gooyal_dynamic_scopes.apps.AppConfig'

View file

@ -1,12 +0,0 @@
"""
Django admin configuration for the gooyal-dynamic-scopes package.
"""
from django.contrib import admin
from .models import Scope
@admin.register(Scope)
class ScopeAdmin(admin.ModelAdmin):
list_display = ('name', 'description', 'is_default')

View file

@ -1,14 +0,0 @@
"""
Django app config for the gooyal-dynamic-scopes package.
"""
from django.apps import AppConfig as BaseAppConfig
from django.db.models.signals import post_migrate
class AppConfig(BaseAppConfig):
name = 'apps.gooyal_dynamic_scopes'
def ready(self):
from .signals import register_scopes
post_migrate.connect(register_scopes, sender=self)

View file

@ -1,29 +0,0 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.4 on 2017-09-04 13:38
from __future__ import unicode_literals
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
initial = True
dependencies = [
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, to=settings.OAUTH2_PROVIDER_APPLICATION_MODEL)),
],
),
]

View file

@ -1,79 +0,0 @@
"""
Django models for the gooyal-dynamic-scopes package.
"""
from django.db import models
from django.conf import settings
import requests
from oauth2_provider.settings import oauth2_settings
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.'
)
#: 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

@ -1,25 +0,0 @@
"""
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

@ -1,26 +0,0 @@
"""
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

@ -1,103 +0,0 @@
"""
Django views for the gooyal-dynamic-scopes package.
"""
import json
import functools
from django.conf import settings
from django.http import HttpResponse, HttpResponseForbidden
from django.views.decorators.csrf import csrf_exempt
from django.views.decorators.http import require_http_methods, require_POST
from oauthlib.oauth2 import Server
from oauth2_provider.oauth2_backends import OAuthLibCore
from oauth2_provider.oauth2_validators import OAuth2Validator
from oauth2_provider.views import IntrospectTokenView
from .models import Scope
def protected_resource(scopes=None):
"""
Implementation of protected_resource decorator that saves the client on the
request for the view function to use.
Cribbed from django-oauth-toolkit.
"""
_scopes = scopes or []
def decorator(view_func):
@functools.wraps(view_func)
def _validate(request, *args, **kwargs):
validator = OAuth2Validator()
core = OAuthLibCore(Server(validator))
valid, oauthlib_req = core.verify_request(request, scopes=_scopes)
if valid:
request.client = oauthlib_req.client
request.resource_owner = oauthlib_req.user
return view_func(request, *args, **kwargs)
return HttpResponseForbidden()
return _validate
return decorator
@require_http_methods(['GET', 'POST'])
@csrf_exempt
@protected_resource(scopes=[settings.INTROSPECT_SCOPE])
def introspect_token(request):
"""
Version of the introspection view protected by a regular scope instead of
read-write scopes.
Also allows for the required scope to be changed using a setting.
"""
if request.method == 'GET':
token = request.GET.get("token", None)
else:
token = request.POST.get("token", None)
return IntrospectTokenView.get_token_response(token)
@require_POST
@csrf_exempt
@protected_resource(scopes=[settings.REGISTER_SCOPE_SCOPE])
def register_scope(request):
"""
Implements an endpoint for registering a scope.
"""
#  Get the scope data from the request body
scope_data = json.loads(request.body) if request.body else {}
try:
try:
#  If a scope with the given name already exists, find it
scope = Scope.objects.get(name=scope_data['name'])
except Scope.DoesNotExist:
#  If no scope with the given name exists, create it
_ = Scope.objects.create(
application=request.client,
name=scope_data['name'],
description=scope_data['description'],
is_default=scope_data.get('is_default', False)
)
#  Respond with a 201 Created
return HttpResponse(status=201)
except KeyError as exc:
#  A key missing in the data should be reported as a bad request
return HttpResponse(
status=400,
content="'{}' must be given in request data".format(exc.args[0]),
content_type='text/plain'
)
#  If the scope does exist, check that the current application is the
#  owner of the scope before updating it
if scope.application and scope.application == request.client:
scope.description = scope_data['description']
scope.is_default = scope_data.get('is_default', False)
scope.save()
return HttpResponse(status=200)
else:
return HttpResponse(status=403)

View file

@ -15,11 +15,19 @@ class Scopes(BaseScopes):
Scopes backend that provides scopes from a Django model.
"""
def get_queryset(self, application=None):
queryset = Scope.objects.all()
if application:
queryset = queryset.filter(name__in=application.allowed_scopes)
return queryset
def get_all_scopes(self):
return {scope.name: scope.description for scope in Scope.objects.all()}
return {scope.name: scope.description for scope in self.get_queryset().all()}
def get_available_scopes(self, application=None, request=None, *args, **kwargs):
return list(self.get_all_scopes().keys())
scopes = [scope.name for scope in self.get_queryset(application).all()]
return scopes
def get_default_scopes(self, application=None, request=None, *args, **kwargs):
return [scope.name for scope in Scope.objects.filter(is_default=True)]
return [scope.name for scope in self.get_queryset(application).filter(is_default=True).all()]

View file

@ -1,25 +0,0 @@
"""
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 RestrictedApplication
from .forms import RestrictedApplicationForm
# 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(RestrictedApplication)
except NotRegistered:
pass
@admin.register(RestrictedApplication)
class RestrictedApplicationAdmin(ApplicationAdmin):
form = RestrictedApplicationForm
# admin.site.register(RestrictedApplication, RestrictedApplicationAdmin)

View file

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

View file

@ -1,51 +0,0 @@
"""
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 RestrictedApplicationForm(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

@ -1,44 +0,0 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.4 on 2017-09-01 15:44
from __future__ import unicode_literals
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
import oauth2_provider.generators
import oauth2_provider.validators
class Migration(migrations.Migration):
initial = True
run_before = [
('oauth2_provider', '0001_initial'),
]
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.CreateModel(
name='RestrictedApplication',
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_restrict_scopes_restrictedapplication', to=settings.AUTH_USER_MODEL)),
],
options={
'abstract': False,
},
),
]

View file

@ -1,25 +0,0 @@
"""
Django models for the gooyal-restrict-scopes package.
"""
from django.db import models
from oauth2_provider.models import AbstractApplication
from oauth2_provider.scopes import get_scopes_backend
class RestrictedApplication(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)

View file

@ -1,43 +0,0 @@
"""
Django OAuth Toolkit scopes backend for the gooyal-restrict-scopes package.
"""
from django.conf import settings
from django.utils import module_loading
from oauth2_provider.scopes import BaseScopes
class RestrictApplicationScopes(BaseScopes):
"""
Scopes backend that wraps another backend and restricts the scopes available
to an application based on the application's ``allowed_scopes``.
"""
def __init__(self):
# Initialise the wrapped backend from settings
self._wrapped = module_loading.import_string(
getattr(settings, 'GOOYAL_RESTRICT_SCOPES', {}).get(
'WRAPPED_SCOPES_BACKEND_CLASS',
'oauth2_provider.scopes.SettingsScopes'
)
)()
def get_all_scopes(self):
# Just return all the available scopes from the wrapped backend
return self._wrapped.get_all_scopes()
def get_available_scopes(self, application = None, request = None, *args, **kwargs):
# Get the available scopes from the wrapped backend, then filter them
# based on the allowed_scopes of the application
scopes = self._wrapped.get_available_scopes(application, request, *args, **kwargs)
if application:
scopes = [s for s in scopes if s in application.allowed_scopes]
return scopes
def get_default_scopes(self, application = None, request = None, *args, **kwargs):
# Get the default scopes from the wrapped backend, then filter them
# based on the allowed_scopes of the application
scopes = self._wrapped.get_default_scopes(application, request, *args, **kwargs)
if application:
scopes = [s for s in scopes if s in application.allowed_scopes]
return scopes

View file

@ -42,8 +42,6 @@ INSTALLED_APPS = [
'corsheaders',
'apps.users',
'apps.transactions',
# 'apps.gooyal_restrict_scopes',
# 'apps.gooyal_dynamic_scopes',
'apps.gooyal_oauth2',
]
@ -74,11 +72,6 @@ OAUTH2_PROVIDER = {
'OAUTH2_VALIDATOR_CLASS': 'apps.gooyal_oauth2.validators.MultiGatewayOAuth2Validator'
}
# GOOYAL_RESTRICT_SCOPES = {
# 'WRAPPED_SCOPES_BACKEND_CLASS': 'oauth2_provider.scopes.SettingsScopes',
# }
# GOOYAL_DYNAMIC_SCOPES
RESOURCE_SERVER_REGISTER_SCOPE_URL = None
INTROSPECT_SCOPE = None