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

Hyp 280 #625

Open
wants to merge 6 commits into
base: development
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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,4 @@ app/db.sqlite3
app/hypatio/local_settings.py
.vscode/settings.json
backup
infrastructure
Empty file added app/api/__init__.py
Empty file.
3 changes: 3 additions & 0 deletions app/api/admin.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
from django.contrib import admin

# Register your models here.
86 changes: 86 additions & 0 deletions app/api/api.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
from django.shortcuts import get_object_or_404
from rest_framework import viewsets
from rest_framework import mixins
from rest_framework.response import Response
from rest_framework import authentication
from rest_framework import permissions
from rest_framework.decorators import action
from dbmi_client.authn import DBMIModelUser
from django_filters.rest_framework import DjangoFilterBackend
from drf_spectacular.utils import extend_schema, OpenApiParameter, extend_schema_view

from projects.models import DataProject, HostedFile
from api.serializers import (
DataProjectSerializer,
HostedFileSerializer,
HostedFileDownloadSerializer,
)
from api.auth import HostedFilePermission

import logging
logger = logging.getLogger(__name__)


class DataProjectViewSet(viewsets.ReadOnlyModelViewSet):
"""
A view for listing or retrieving projects.
"""
authentication_classes = [DBMIModelUser]
permission_classes = [permissions.IsAuthenticated]
filter_backends = [DjangoFilterBackend]
queryset = DataProject.objects.filter(visible=True)
serializer_class = DataProjectSerializer
lookup_field = "id"


# Define parameters schema for detail views
detail_parameters = [
OpenApiParameter(
"project_id",
type=int,
description="The unique ID of the project containing the file(s).",
location=OpenApiParameter.PATH,
required=True,
),
]

@extend_schema_view(
list=extend_schema(
parameters=detail_parameters,
),
retrieve=extend_schema(
parameters=detail_parameters,
),
download=extend_schema(
description="A view for downloading a project's files.",
parameters=detail_parameters,
responses=HostedFileDownloadSerializer,
),
)
class HostedFileViewSet(viewsets.ReadOnlyModelViewSet):
"""
A view for listing or retrieving a projects's files.
"""
authentication_classes = [DBMIModelUser]
permission_classes = [permissions.IsAuthenticated, HostedFilePermission]
filter_backends = [DjangoFilterBackend]
queryset = HostedFile.objects.filter(enabled=True)
serializer_class = HostedFileSerializer
lookup_field = "id"
project_lookup = "project_id"

def get_queryset(self):
return HostedFile.objects.filter(
project=self.kwargs[self.project_lookup],
enabled=True,
)

def initial(self, request, *args, **kwargs):
print(kwargs)
self.project = get_object_or_404(DataProject.objects.all(), pk=kwargs[self.project_lookup])
return super(HostedFileViewSet, self).initial(request, args, kwargs)

@action(detail=True, methods=['get'])
def download(self, request, project_id, id):
serializer = HostedFileDownloadSerializer(self.get_object())
return Response(serializer.data)
6 changes: 6 additions & 0 deletions app/api/apps.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
from django.apps import AppConfig


class ApiConfig(AppConfig):
name = 'api'
default_auto_field = 'django.db.models.BigAutoField'
82 changes: 82 additions & 0 deletions app/api/auth.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
from rest_framework import permissions
from dbmi_client import authz

from projects.models import DataProject

import logging
logger = logging.getLogger(__name__)


class DataProjectPermission(permissions.BasePermission):
message = 'Access to data project is prohibited.'

# Permission strings for the Data Portal
READ_PERMISSIONS = ["VIEW", "MANAGE"]
WRITE_PERMISSIONS = ["MANAGE"]

def has_read_permission(self, request, project):
"""
Returns whether the requesting user has a read permission on the passed
project or not.

:param request: The current request
:type request: HttpRequest
:param project: The DataProject the requestor is attempting to read
:type project: DataProject
"""
# Check Participant objects first
if request.user.participant_set.filter(
project=project,
permission__in=self.READ_PERMISSIONS):
return True

if authz.has_a_permission(
request=request,
email=request.user.email,
item=f"Hypatio.{project.project_key}",
permissions=self.READ_PERMISSIONS,
check_parents=True):
return True

return False

def has_permission(self, request, view):
if not request.user.is_authenticated:
return False

def has_object_permission(self, request, view, obj):

# Ensure user has permission on the given project.

# Get the project
try:
project = DataProject.objects.get(pk=obj)
except DataProject.DoesNotExist:
logger.error(f"DataProject does not exist for PK: '{obj}'")
return False

# Check Participant objects first
if self.has_read_permission(request, project):
return True

logger.debug(f"Access denied on '{obj}' for: {request.user.email}")
return False


class HostedFilePermission(DataProjectPermission):
"""
This BasePermission subclass ensures the requesting user has adequate
permissions on the DataProject to which the HostedFile belongs.
"""
message = 'Access to file is prohibited.'

def has_permission(self, request, view):
if not request.user.is_authenticated:
return False

# Check Participant objects first
if self.has_read_permission(request, view.project):
return True

def has_object_permission(self, request, view, obj):
return True
Empty file added app/api/migrations/__init__.py
Empty file.
3 changes: 3 additions & 0 deletions app/api/models.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
from django.db import models

# Create your models here.
17 changes: 17 additions & 0 deletions app/api/scheme.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
from django.utils.translation import gettext_lazy as _
from drf_spectacular.extensions import OpenApiAuthenticationExtension


class JWTScheme(OpenApiAuthenticationExtension):
target_class = 'dbmi_client.authn.DBMIModelUser'
name = 'JWT Authentication'

def get_security_definition(self, auto_schema):
return {
'type': 'apiKey',
'in': 'header',
'name': 'Authorization',
'description': _(
'Token-based authentication with required prefix "%s" (JWT can be viewed when logged in at https://authentication.dbmi.hms.harvard.edu/)'
) % "JWT"
}
43 changes: 43 additions & 0 deletions app/api/serializers.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
from rest_framework import serializers
from datetime import datetime, timedelta

from drf_spectacular.utils import extend_schema_field
from drf_spectacular.types import OpenApiTypes

from projects.models import DataProject
from projects.models import HostedFile
from hypatio.file_services import get_download_url


class DataProjectSerializer(serializers.ModelSerializer):
class Meta:
model = DataProject
fields = [
'id', 'name', 'project_key', 'description',
'short_description', 'created', 'modified', #TODO: 'group',
]

class HostedFileSerializer(serializers.ModelSerializer):
class Meta:
model = HostedFile
fields = [
'id', 'project', 'uuid', 'long_name', 'description',
'file_name', 'file_location', 'created', 'modified'
]


class HostedFileDownloadSerializer(serializers.ModelSerializer):
url = serializers.SerializerMethodField()
expires = serializers.SerializerMethodField()

class Meta:
model = HostedFile
fields = ['url', 'expires']

@extend_schema_field(OpenApiTypes.URI)
def get_url(self, obj):
return get_download_url(obj.file_location + "/" + obj.file_name)

@extend_schema_field(OpenApiTypes.DATETIME)
def get_expires(self, obj):
return datetime.now() + timedelta(hours=1)
3 changes: 3 additions & 0 deletions app/api/tests.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
from django.test import TestCase

# Create your tests here.
21 changes: 21 additions & 0 deletions app/api/urls.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
from django.urls import include, re_path
from django.http import HttpResponse
from drf_spectacular.views import SpectacularAPIView, SpectacularSwaggerView, SpectacularRedocView
from rest_framework_nested import routers
from api.api import DataProjectViewSet, HostedFileViewSet

router = routers.SimpleRouter()
router.register(r'projects', DataProjectViewSet)

projects_router = routers.NestedSimpleRouter(router, r'projects', lookup='project')
projects_router.register(r'files', HostedFileViewSet, basename='project-files')

app_name = 'api'
urlpatterns = [
re_path(r'^schema/?$', SpectacularAPIView.as_view(), name='schema'),
re_path(r'^schema/redoc/', SpectacularRedocView.as_view(url_name='api:schema'), name='redoc'),
re_path(r'^schema/swagger/?$', SpectacularSwaggerView.as_view(url_name='api:schema'), name='swagger'),
re_path(r'^v1/', include(router.urls)),
re_path(r'^v1/', include(projects_router.urls)),
re_path(r'^', lambda request: HttpResponse(status=404)),
]
3 changes: 3 additions & 0 deletions app/api/views.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
from django.shortcuts import render

# Create your views here.
18 changes: 12 additions & 6 deletions app/hypatio/auth0authenticate.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
from django.core.exceptions import PermissionDenied
from dbmi_client.settings import dbmi_settings
from dbmi_client.settings import dbmi_settings
from dbmi_client.authn import validate_request, login_redirect_url
from dbmi_client.authn import validate_request, login_redirect_url, get_jwt_payload, get_jwt

logger = logging.getLogger(__name__)

Expand Down Expand Up @@ -219,7 +219,7 @@ def jwt_login(request, jwt_payload):

request.session['profile'] = jwt_payload

user = django_auth.authenticate(**jwt_payload)
user = django_auth.authenticate(request, **jwt_payload)

if user:
login(request, user)
Expand Down Expand Up @@ -259,14 +259,20 @@ def logout_redirect(request):
class Auth0Authentication(object):

def authenticate(self, request, **token_dictionary):
logger.debug("Authenticate User: {}/{}".format(token_dictionary.get('sub'), token_dictionary.get('email')))

# Get the JWT payload
payload = get_jwt_payload(request, verify=True)
if not payload:
return None

logger.debug("Authenticate User: {}/{}".format(payload.get('sub'), payload.get('email')))

try:
user = User.objects.get(username=token_dictionary["email"])
user = User.objects.get(username=payload["email"])
except User.DoesNotExist:
logger.debug("User not found, creating: {}".format(token_dictionary.get('email')))
logger.debug("User not found, creating: {}".format(payload.get('email')))

user = User(username=token_dictionary["email"], email=token_dictionary["email"])
user = User(username=payload["email"], email=payload["email"])
user.save()
return user

Expand Down
41 changes: 41 additions & 0 deletions app/hypatio/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,9 @@
'storages',
'django_jsonfield_backport',
'django_q',
'rest_framework',
'drf_spectacular',
'django_filters',
'django_ses',
]

Expand Down Expand Up @@ -301,6 +304,44 @@

#####################################################################################

#####################################################################################
# Django Rest Framework settings
#####################################################################################

REST_FRAMEWORK = {
'DEFAULT_AUTHENTICATION_CLASSES': [
'dbmi_client.authn.DBMIModelUser',
],
'DEFAULT_PERMISSION_CLASSES': [
'rest_framework.permissions.IsAuthenticated',
],
'DEFAULT_RENDERER_CLASSES': [
'rest_framework.renderers.JSONRenderer',
],
'DEFAULT_PARSER_CLASSES': [
'rest_framework.parsers.JSONParser',
],
'DEFAULT_FILTER_BACKENDS': [
'django_filters.rest_framework.DjangoFilterBackend'
],
'DEFAULT_SCHEMA_CLASS': 'drf_spectacular.openapi.AutoSchema',
}

#####################################################################################
# API Schema settings
#####################################################################################

SPECTACULAR_SETTINGS = {
'TITLE': 'HMS DBMI Portal API',
'DESCRIPTION': 'API for Portal projects and data sets',
'VERSION': '1.0.0',
'SERVE_INCLUDE_SCHEMA': False,
'SERVE_AUTHENTICATION': [],
# OTHER SETTINGS
}

#####################################################################################

LOGGING = {
'version': 1,
'disable_existing_loggers': False,
Expand Down
1 change: 1 addition & 0 deletions app/hypatio/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
re_path(r'^manage/', include('manage.urls', namespace='manage')),
re_path(r'^projects/', include('projects.urls', namespace='projects')),
re_path(r'^profile/', include('profile.urls', namespace='profile')),
re_path(r'^api/', include('api.urls', namespace='api')),
re_path(r'^data-sets/$', list_data_projects, name='data-sets'),
re_path(r'^data-challenges/$', list_data_challenges, name='data-challenges'),
re_path(r'^software-projects/$', list_software_projects, name='software-projects'),
Expand Down
Loading