Initial commit

This commit is contained in:
2020-11-13 15:31:14 +01:00
parent 2af942da5a
commit 10e2a34143
103 changed files with 3609 additions and 0 deletions

View File

@@ -0,0 +1 @@
default_app_config = 'apps.api.apps.ApiConfig'

View File

@@ -0,0 +1,9 @@
from django.contrib import admin
from .models import Token
@admin.register(Token)
class TokenAdmin(admin.ModelAdmin):
list_display = ('key', 'user','is_supertoken', 'last_access')
ordering = ('-last_access', 'user', )
search_fields = ('key', 'user__username',)
readonly_fields = ('created_at', 'updated_at')

View File

@@ -0,0 +1,66 @@
from django.apps import AppConfig
from django.utils.translation import ugettext_lazy as _
from django.conf import settings
class ApiConfig(AppConfig):
name = 'apps.api'
label = 'api'
verbose_name = _('API')
verbose_name_plural = _('APIs')
try:
assert settings.SWAGGER_SETTINGS
except AttributeError:
# We only load this setting, if it is not available in the overall settings.py file
settings.SWAGGER_SETTINGS = {
'SECURITY_DEFINITIONS': {
'Hawk': {
'type': 'apiKey',
'description': 'HTTP Holder-Of-Key Authentication Scheme, https://github.com/hapijs/hawk, https://hawkrest.readthedocs.io/en/latest/<br /><strong>Ex header:</strong><br />\'Authorization\': \'Hawk mac="F4+S9cu7yZiZEgdtqzMpOOdudvqcV2V2Yzk2WcphECc=", hash="+7fKUX+djeQolvnLTxr0X47e//UHKbkRlajwMw3tx3w=", id="7FI5JET4", ts="1592905433", nonce="DlV-fL"\'',
'name': 'Authorization',
'in': 'header'
}
}
}
try:
assert settings.REST_FRAMEWORK
except AttributeError:
# We only load this setting, if it is not available in the overall settings.py file
# To protect all API views with Hawk by default, put this in your settings:
# https://hawkrest.readthedocs.io/en/latest/usage.html#protecting-api-views-with-hawk
settings.REST_FRAMEWORK = {
'DEFAULT_AUTHENTICATION_CLASSES': (
'apps.api.authentication.APIHawk',
),
'DEFAULT_PERMISSION_CLASSES': (
'rest_framework.permissions.IsAuthenticated',
),
# 'DEFAULT_AUTHENTICATION_CLASSES': (
# 'rest_framework.authentication.TokenAuthentication',
# ),
# 'DEFAULT_PERMISSION_CLASSES': (
# 'rest_framework.permissions.IsAuthenticated', ),
# Use Django's standard `django.contrib.auth` permissions,
# or allow read-only access for unauthenticated users.
#'DEFAULT_PERMISSION_CLASSES': [
# 'rest_framework.permissions.DjangoModelPermissionsOrAnonReadOnly'
#],
'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.PageNumberPagination',
'PAGE_SIZE': 10
}
try:
assert settings.HAWK_MESSAGE_EXPIRATION
except AttributeError:
# We only load this setting, if it is not available in the overall settings.py file
settings.HAWK_MESSAGE_EXPIRATION = 60
def ready(self):
from . import signals

View File

@@ -0,0 +1,72 @@
# import the logging library
import logging
# Get an instance of a logger
logger = logging.getLogger(__name__)
import django.utils
from rest_framework import exceptions
from hawkrest import HawkAuthentication
from .models import Token
class APIHawk(HawkAuthentication):
"""This is the API authentication that is using the HAWK authentication mechanism.
This class will implement a custom credentials and user lookups so that we can dynamically add new users and update tokens.
"""
def hawk_credentials_lookup(self, id):
"""This method will perform the check if the used token is an existing/known token in the database. This will not lookup a user. Only an existing token.
Args:
id (string): The token key to lookup in the database for existing token.
Raises:
exceptions.AuthenticationFailed: If the given token does not exists.
Returns:
dict: The dictionary holds the token id, the token secret and the used hashing algoritem that is used.
"""
try:
token = Token.objects.get(key=id)
except Token.DoesNotExist:
logger.warning('Requested to validate with invalid/non existing token: {}'.format(id))
raise exceptions.AuthenticationFailed('No such token: {}'.format(id))
return {
'id' : id,
'key' : token.secret,
'algorithm' : 'sha256'
}
def hawk_user_lookup(self, request, credentials):
"""Return the user account that is connected to the used token.
Args:
request ([type]): The incoming HTTP/API request
credentials (dict): The credentials from ~hawk_credentials_lookup
Raises:
exceptions.AuthenticationFailed: If the given token does not exists to an existing user
Returns:
tuple: Returns a tuple holding the user as first item
"""
user = None
try:
user = Token.objects.get(key=credentials['id']).user
except Token.DoesNotExist:
logger.warning('Requested to validate non existing user: {}'.format(id))
raise exceptions.AuthenticationFailed('No user for token: {}'.format(credentials['id']))
# Update the date time stamp to now for last access data
user.token.last_access = django.utils.timezone.now()
user.token.save()
return (user,None)
def __repr__(self):
"""Authentication identifier.
Returns:
string: Returns the name of the used authentication mechanism.
"""
return 'Hawk authenticator'

Binary file not shown.

View File

@@ -0,0 +1,67 @@
# SOME DESCRIPTIVE TITLE.
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
# This file is distributed under the same license as the PACKAGE package.
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
#
#, fuzzy
msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2020-07-30 15:42+0200\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
"Language: \n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
#: apps/api/apps.py:9
msgid "API"
msgstr ""
#: apps/api/apps.py:10
msgid "APIs"
msgstr ""
#: apps/api/models.py:28
msgid "token"
msgstr ""
#: apps/api/models.py:29
msgid "tokens"
msgstr ""
#: apps/api/models.py:31
msgid "Select the user for this token"
msgstr ""
#: apps/api/models.py:32
msgid "Key"
msgstr ""
#: apps/api/models.py:32
msgid "The key for this token. This is used for Hawk verification."
msgstr ""
#: apps/api/models.py:33
msgid "Secret"
msgstr ""
#: apps/api/models.py:33
msgid "The secret for this token. This is used for Hawk signing."
msgstr ""
#: apps/api/models.py:34
msgid "Last access"
msgstr ""
#: apps/api/models.py:34
msgid "The date and time when this token is last used."
msgstr ""
#: apps/api/models.py:44
msgid "Super token"
msgstr ""

Binary file not shown.

View File

@@ -0,0 +1,67 @@
# SOME DESCRIPTIVE TITLE.
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
# This file is distributed under the same license as the PACKAGE package.
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
#
msgid ""
msgstr ""
"Project-Id-Version: \n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2020-07-30 15:42+0200\n"
"PO-Revision-Date: 2020-05-27 16:25+0200\n"
"Last-Translator: Joshua Rubingh <j.g.rubingh@rug.nl>\n"
"Language-Team: \n"
"Language: nl\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
"X-Generator: Poedit 2.0.6\n"
#: apps/api/apps.py:9
msgid "API"
msgstr "API"
#: apps/api/apps.py:10
msgid "APIs"
msgstr "APIs"
#: apps/api/models.py:28
msgid "token"
msgstr ""
#: apps/api/models.py:29
msgid "tokens"
msgstr ""
#: apps/api/models.py:31
msgid "Select the user for this token"
msgstr ""
#: apps/api/models.py:32
msgid "Key"
msgstr ""
#: apps/api/models.py:32
msgid "The key for this token. This is used for Hawk verification."
msgstr ""
#: apps/api/models.py:33
msgid "Secret"
msgstr ""
#: apps/api/models.py:33
msgid "The secret for this token. This is used for Hawk signing."
msgstr ""
#: apps/api/models.py:34
msgid "Last access"
msgstr ""
#: apps/api/models.py:34
msgid "The date and time when this token is last used."
msgstr ""
#: apps/api/models.py:44
msgid "Super token"
msgstr ""

View File

@@ -0,0 +1,35 @@
# Generated by Django 3.0.8 on 2020-07-30 14:15
import apps.api.models
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
import django_cryptography.fields
class Migration(migrations.Migration):
initial = True
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.CreateModel(
name='Token',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('created_at', models.DateTimeField(auto_now_add=True, help_text='The date and time this model has been created', verbose_name='Date created')),
('updated_at', models.DateTimeField(auto_now=True, help_text='The date and time this model has been updated', verbose_name='Date updated')),
('key', models.CharField(default=apps.api.models.get_random_key, help_text='The key for this token. This is used for Hawk verification.', max_length=16, unique=True, verbose_name='Key')),
('secret', django_cryptography.fields.encrypt(models.CharField(default=apps.api.models.get_random_secret, help_text='The secret for this token. This is used for Hawk signing.', max_length=64, verbose_name='Secret'))),
('last_access', models.DateTimeField(auto_now_add=True, help_text='The date and time when this token is last used.', verbose_name='Last access')),
('user', models.OneToOneField(help_text='Select the user for this token', on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
],
options={
'verbose_name': 'token',
'verbose_name_plural': 'tokens',
},
),
]

View File

@@ -0,0 +1,70 @@
from django.contrib.auth.models import User
from django.db import models
from django.utils.translation import gettext_lazy as _
from django_cryptography.fields import encrypt
from lib.utils.general import get_random_string
from lib.models.base import MetaDataModel
def get_random_key():
return get_random_string(8)
def get_random_secret():
return get_random_string(32)
class TokenManager(models.Manager):
"""
Custom queryset which will prefetch related user table data when requesting a token from the database as the user is mostly needed every time the token is requested.
"""
def get_queryset(self):
return super(TokenManager, self).get_queryset().select_related('user')
class Token(MetaDataModel):
"""Token model that holds all the tokens that are used for the API authentication.
A new token is generated every time when a new user is created. So there is no need for manual token creating. This is done through a signal :attr:`~apps.api.signals.create_user_token`
Attributes
----------
user : :class:`~django.contrib.auth.models.User`
The user to which this token belongs too
key : str
The key value that is used for token lookups
secret : str
The secret that is used for encrypting/signing the API messages
last_access : datetime
The date and time when the token is last used (logged in)
"""
class Meta:
verbose_name = _('token')
verbose_name_plural = _('tokens')
user = models.OneToOneField(User, on_delete=models.CASCADE, help_text=_('Select the user for this token'))
key = models.CharField(_('Key') , unique=True, default=get_random_key, max_length=16, help_text=_('The key for this token. This is used for Hawk verification.'))
secret = encrypt(models.CharField(_('Secret') ,max_length=64, default=get_random_secret, help_text=_('The secret for this token. This is used for Hawk signing.')))
last_access = models.DateTimeField(_('Last access'),auto_now_add=True, help_text=_('The date and time when this token is last used.'))
# Custom manager that will retrieve the related user table as well.
objects = TokenManager()
def is_supertoken(self):
"""Boolean check if the token is belonging to a user with super user rights. Then this token is a super token.
Returns:
bool: Returns true when the token belongs to a super user.
"""
# TODO: Is it allowed to be a super user and researcher? Could give conflict of interests. With the API token you can read other researchers data...
return self.user.is_superuser == True
is_supertoken.boolean = True
is_supertoken.short_description = _('Super token')
def __str__(self):
"""
Print the full name of the researcher based on the first and last name fields of the User model.
"""
return '{} ({})'.format(self.key,self.user.get_full_name())

View File

@@ -0,0 +1,24 @@
from django.conf import settings
from django.db.models.signals import post_save
from django.dispatch import receiver
from .models import Token
@receiver(post_save, sender=settings.AUTH_USER_MODEL)
def create_user_token(sender, instance=None, created=False, **kwargs):
"""
When a new user is created, this signal will also create a new API token for this user. So every user will have an API token.
Arguments
----------
sender : sender
The model that has triggered the signal
instance: :attr:`~django.contrib.auth.models.User`
The newly created user model data
created : boolean
Wether the object was created (True) or updated (False).
"""
if created:
Token.objects.create(user=instance)

View File

@@ -0,0 +1,3 @@
from django.test import TestCase
# Create your tests here.

View File

@@ -0,0 +1,71 @@
from django.urls import path, re_path, include
from rest_framework import permissions, routers
from drf_yasg2.views import get_schema_view
from drf_yasg2 import openapi
from . import views
from apps.dropoff.api.views import DatadropViewSet
from apps.invitation.api.views import InvitationViewSet
from apps.researcher.api.views import ResearcherViewSet
from apps.storage.api.views import StorageEngineViewSet, StorageLocationViewSet
from apps.study.api.views import StudyViewSet
from apps.virtual_machine.api.views import (VirtualMachineViewSet,
VirtualMachineOperatingSystemViewSet,
VirtualMachineProfileViewSet,
VirtualMachineMemoryViewSet,
VirtualMachineNetworkViewSet,
VirtualMachineStorageViewSet,
VirtualMachineGPUViewSet)
schema_view = get_schema_view(
openapi.Info(
title="Virtual Research Environment API",
default_version='v1',
description="Here you can see a list of API endpoints and actions that are available to communicate with the VRE API",
terms_of_service="https://www.rug.nl",
contact=openapi.Contact(email="vre_team@rug.nl"),
license=openapi.License(name="MIT License"),
),
public=True,
permission_classes=(permissions.AllowAny,),
)
api_router_v1 = routers.DefaultRouter()
api_router_v1.register(r'researchers', ResearcherViewSet)
api_router_v1.register(r'studies', StudyViewSet)
api_router_v1.register(r'dropoffs', DatadropViewSet)
api_router_v1.register(r'invitations', InvitationViewSet)
api_router_v1.register(r'storageengines', StorageEngineViewSet)
api_router_v1.register(r'storagelocations', StorageLocationViewSet)
# Order is important for virtual machines. Longest match first
api_router_v1.register(r'virtualmachines/profiles', VirtualMachineProfileViewSet)
api_router_v1.register(r'virtualmachines/storage', VirtualMachineStorageViewSet)
api_router_v1.register(r'virtualmachines/memory', VirtualMachineMemoryViewSet)
api_router_v1.register(r'virtualmachines/network', VirtualMachineNetworkViewSet)
api_router_v1.register(r'virtualmachines/gpu', VirtualMachineGPUViewSet)
api_router_v1.register(r'virtualmachines/os', VirtualMachineOperatingSystemViewSet)
api_router_v1.register(r'virtualmachines', VirtualMachineViewSet)
# Main namespace for the API urls
app_name = 'api'
urlpatterns = [
re_path(r'^swagger(?P<format>\.json|\.yaml)$', schema_view.without_ui(cache_timeout=0), name='schema-json'),
path('swagger/', schema_view.with_ui('swagger', cache_timeout=0), name='schema-swagger-ui'),
path('redoc/', schema_view.with_ui('redoc', cache_timeout=0), name='schema-redoc'),
# Extra /api/info path for checking if the Hawk authentication is working.
# Also this will give the full url to the OpenAPI documentation
path('info/', views.Info.as_view(), name='info'),
# Add extra namespace for versioning the API
path('v1/', include((api_router_v1.urls,'api'),namespace='v1')),
]

View File

@@ -0,0 +1,52 @@
from rest_framework.views import APIView
from rest_framework.response import Response
from rest_framework.decorators import schema
from django.urls import reverse
from lib.utils.general import get_ip_address
@schema(None)
class Info(APIView):
"""
Show some API information. Also this can be used to check if the Hawk credentials are working.
Make sure your request does contain the header 'Content-Type': 'application/json'
"""
def get(self, request, format=None):
"""
Default API get action will return the following information in a dict:
- Connected user
- Used authentication scheme
- The remote IP of the connection
- The used content type
- The full url to the API documentation (OpenAPI)
- If a super token is used
"""
data = {
'type' : 'anonymous',
'auth' : 'none',
'remote_ip' : get_ip_address(request),
'content_type' : request.content_type,
'openapi' : request.build_absolute_uri(reverse('api:schema-redoc')),
}
if request.user.is_authenticated:
data['user'] = request.user.username
data['type'] = 'authenticated'
data['auth'] = str(request.successful_authenticator)
if request.user.token.is_supertoken:
data['type'] = 'supertoken'
else:
try:
assert request.user.researcher
data['type'] = 'researcher'
except AttributeError:
pass
return Response(data)