Skip to content
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

[WIP] Add baseline set of scopes #455

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
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
30 changes: 30 additions & 0 deletions isic/conftest.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
from django.test.client import Client
from django.utils import timezone
from oauth2_provider.models import get_access_token_model, get_application_model
import pytest
from pytest_factoryboy import register
from rest_framework.test import APIClient
Expand Down Expand Up @@ -65,6 +67,34 @@ def staff_api_client(staff_user) -> APIClient:
return api_client


@pytest.fixture
def oauth_app(user_factory):
user = user_factory()
return get_application_model().objects.create(
name='Test Application',
redirect_uris='http://localhost',
user=user,
client_type=get_application_model().CLIENT_CONFIDENTIAL,
authorization_grant_type=get_application_model().GRANT_AUTHORIZATION_CODE,
)


@pytest.fixture
def oauth_token_client(api_client, oauth_app):
def f(user, scope='identity'): # TODO: settings default scope
token = get_access_token_model().objects.create(
user=user,
scope=scope,
expires=timezone.now() + timezone.timedelta(seconds=300),
token='some-token',
application=oauth_app,
)
api_client.credentials(Authorization=f'Bearer {token}')
return api_client

return f


# To make pytest-factoryboy fixture creation work properly, all factories must be registered at
# this top-level conftest, since the factories have inter-app references.

Expand Down
12 changes: 12 additions & 0 deletions isic/core/api.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
from django.utils.decorators import method_decorator
from drf_yasg.utils import swagger_auto_schema
from oauth2_provider.contrib.rest_framework.permissions import TokenMatchesOASRequirements
from oauth2_provider.decorators import protected_resource
from rest_framework.decorators import action, api_view, permission_classes
from rest_framework.permissions import AllowAny, IsAuthenticated
from rest_framework.response import Response
Expand Down Expand Up @@ -31,6 +33,7 @@ def stats(request):
@swagger_auto_schema(methods=['GET'], operation_summary='Retrieve the currently logged in user.')
@api_view(['GET'])
@permission_classes([IsAuthenticated])
@protected_resource(scopes=['identity'])
def user_me(request):
return Response(UserSerializer(request.user).data)

Expand All @@ -50,6 +53,8 @@ class ImageViewSet(ReadOnlyModelViewSet):
.distinct()
)
filter_backends = [IsicObjectPermissionsFilter]
permission_classes = [TokenMatchesOASRequirements]
required_scopes = ['read:image']
lookup_field = 'isic_id'

@swagger_auto_schema(
Expand Down Expand Up @@ -167,3 +172,10 @@ class CollectionViewSet(ReadOnlyModelViewSet):
serializer_class = CollectionSerializer
queryset = Collection.objects.all()
filter_backends = [IsicObjectPermissionsFilter]
permission_classes = [TokenMatchesOASRequirements]
required_alternate_scopes = {
'GET': [['read:collection']],
'POST': [['write:collection']],
'PUT': [['write:collection']],
'DELETE': [['write:collection']],
}
8 changes: 8 additions & 0 deletions isic/core/tests/test_api_contributor.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,14 @@
from pytest import lazy_fixture


@pytest.mark.django_db
def test_core_api_contributor_oauth(user, oauth_token_client):
client = oauth_token_client(user, 'read:ingest')
r = client.get('/api/v2/contributors/')

assert r.status_code == 200, r.data


@pytest.mark.django_db
@pytest.mark.parametrize(
'client,contributors_,num_visible',
Expand Down
32 changes: 16 additions & 16 deletions isic/ingest/api.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
from django.db import transaction
from django.shortcuts import get_object_or_404
from django.utils.decorators import method_decorator
from drf_yasg.utils import swagger_auto_schema
from oauth2_provider.contrib.rest_framework import TokenMatchesOASRequirements
from rest_framework import mixins, status, viewsets
from rest_framework.decorators import action
from rest_framework.permissions import BasePermission, IsAdminUser
from rest_framework.permissions import IsAdminUser
from rest_framework.response import Response

from isic.core.permissions import IsicObjectPermissionsFilter
Expand All @@ -21,23 +21,18 @@
)
from isic.ingest.tasks import apply_metadata_task, process_accession_task


class AccessionPermissions(BasePermission):
def has_permission(self, request, view):
if request.user.is_staff:
return True

if request.method == 'POST':
if 'cohort' in request.data:
cohort = get_object_or_404(Cohort, pk=request.data['cohort'])
return request.user.has_perm('ingest.add_accession', cohort)

return False
REQUIRED_ALTERNATE_SCOPES = {
'GET': [['read:ingest']],
'POST': [['write:ingest']],
'PUT': [['write:ingest']],
'DELETE': [['write:ingest']],
}


class AccessionViewSet(mixins.UpdateModelMixin, mixins.CreateModelMixin, viewsets.GenericViewSet):
queryset = Accession.objects.all()
permission_classes = [AccessionPermissions]
permission_classes = [TokenMatchesOASRequirements]
required_alternate_scopes = REQUIRED_ALTERNATE_SCOPES

def get_serializer_class(self):
if self.action == 'create':
Expand Down Expand Up @@ -121,6 +116,8 @@ class CohortViewSet(mixins.CreateModelMixin, viewsets.ReadOnlyModelViewSet):
serializer_class = CohortSerializer
queryset = Cohort.objects.all()
filter_backends = [IsicObjectPermissionsFilter]
permission_classes = [TokenMatchesOASRequirements]
required_alternate_scopes = REQUIRED_ALTERNATE_SCOPES


@method_decorator(
Expand All @@ -138,14 +135,17 @@ class ContributorViewSet(mixins.CreateModelMixin, viewsets.ReadOnlyModelViewSet)
serializer_class = ContributorSerializer
queryset = Contributor.objects.all()
filter_backends = [IsicObjectPermissionsFilter]
permission_classes = [TokenMatchesOASRequirements]
required_alternate_scopes = REQUIRED_ALTERNATE_SCOPES


class MetadataFileViewSet(
mixins.UpdateModelMixin, mixins.DestroyModelMixin, viewsets.GenericViewSet
):
serializer_class = MetadataFileSerializer
queryset = MetadataFile.objects.all()
permission_classes = [IsAdminUser]
permission_classes = [IsAdminUser & TokenMatchesOASRequirements]
required_alternate_scopes = REQUIRED_ALTERNATE_SCOPES

swagger_schema = None

Expand Down
1 change: 1 addition & 0 deletions isic/login/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ def get_girder_token(request):
@swagger_auto_schema(methods=['PUT'], operation_summary='Accept the terms of use.')
@api_view(['PUT'])
@permission_classes([IsAuthenticated])
@protected_resource(scopes=['identity'])
def accept_terms_of_use(request):
if not request.user.profile.accepted_terms:
request.user.profile.accepted_terms = timezone.now()
Expand Down
12 changes: 9 additions & 3 deletions isic/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,10 +48,16 @@ def mutate_configuration(configuration: ComposedConfiguration) -> None:
{
'PKCE_REQUIRED': True,
'SCOPES': {
'identity': 'Access to your basic profile information',
'image:read': 'Read access to images',
'image:write': 'Write access to images',
'identity': 'View your basic user information',
'read:ingest': 'View cohorts, contributors, accessions, and metadata.',
'write:ingest': 'Add or remove cohorts, contributors, accessions, and metadata.', # noqa: E501
'read:image': 'View images and metadata.',
'read:collection': 'View collections.',
'write:collection': 'Add or remove collections, and publish DOIs.',
'read:study': 'View studies.',
'write:study': 'Add, remove, or participate in studies.',
},
'ERROR_RESPONSE_WITH_SCOPES': True,
'DEFAULT_SCOPES': ['identity'],
# Allow setting DJANGO_OAUTH_ALLOWED_REDIRECT_URI_SCHEMES to override this on the
# sandbox instance.
Expand Down
17 changes: 14 additions & 3 deletions isic/studies/api.py
Original file line number Diff line number Diff line change
@@ -1,29 +1,40 @@
from oauth2_provider.contrib.rest_framework.permissions import TokenMatchesOASRequirements
from rest_framework import viewsets
from rest_framework.permissions import IsAdminUser

from isic.studies.models import Annotation, Study, StudyTask
from isic.studies.serializers import AnnotationSerializer, StudySerializer, StudyTaskSerializer

REQUIRED_ALTERNATE_SCOPES = {
'GET': [['read:study']],
'POST': [['write:study']],
'PUT': [['write:study']],
'DELETE': [['write:study']],
}


class StudyTaskViewSet(viewsets.ReadOnlyModelViewSet):
serializer_class = StudyTaskSerializer
queryset = StudyTask.objects.all()
permission_classes = [IsAdminUser]
permission_classes = [IsAdminUser & TokenMatchesOASRequirements]
required_alternate_scopes = REQUIRED_ALTERNATE_SCOPES

swagger_schema = None


class StudyViewSet(viewsets.ReadOnlyModelViewSet):
serializer_class = StudySerializer
queryset = Study.objects.all()
permission_classes = [IsAdminUser]
permission_classes = [IsAdminUser & TokenMatchesOASRequirements]
required_alternate_scopes = REQUIRED_ALTERNATE_SCOPES

swagger_schema = None


class AnnotationViewSet(viewsets.ReadOnlyModelViewSet):
serializer_class = AnnotationSerializer
queryset = Annotation.objects.all()
permission_classes = [IsAdminUser]
permission_classes = [IsAdminUser & TokenMatchesOASRequirements]
required_alternate_scopes = REQUIRED_ALTERNATE_SCOPES

swagger_schema = None