diff --git a/pyobs_archive/api/apps.py b/pyobs_archive/api/apps.py index 59f8f98..f61543e 100644 --- a/pyobs_archive/api/apps.py +++ b/pyobs_archive/api/apps.py @@ -2,4 +2,4 @@ class QueryConfig(AppConfig): - name = 'pyobs_archive.archive' + name = 'pyobs_archive.api' diff --git a/pyobs_archive/api/models.py b/pyobs_archive/api/models.py index 8fa4dff..d04da85 100644 --- a/pyobs_archive/api/models.py +++ b/pyobs_archive/api/models.py @@ -140,7 +140,7 @@ def get_info(self): info['related_frames'] = [f.id for f in self.related.all()] # add url - info['url'] = urljoin(settings.ROOT_URL, 'frames/%d/download/' % self.id) + info['url'] = 'frames/%d/download/' % self.id # finished return info diff --git a/pyobs_archive/api/views.py b/pyobs_archive/api/views.py index d742989..8e9d342 100644 --- a/pyobs_archive/api/views.py +++ b/pyobs_archive/api/views.py @@ -11,9 +11,7 @@ from astropy.io import fits from django.conf import settings from django.db.models import F -from rest_framework import exceptions -from rest_framework.decorators import permission_classes, authentication_classes, api_view -from rest_framework.authentication import TokenAuthentication +from rest_framework.decorators import permission_classes, api_view from rest_framework.permissions import IsAdminUser, IsAuthenticated from pyobs_archive.api.models import Frame @@ -22,23 +20,6 @@ log = logging.getLogger(__name__) -# define classes for authentication -if settings.TOKEN_AUTH is None: - AUTH_CLASSES = [] - POST_AUTH_CLASSES = [] - AUTHENTICATED = [] -else: - class PostAuthentication(settings.TOKEN_AUTH): - def authenticate(self, request): - if 'auth_token' not in request.POST: - raise exceptions.AuthenticationFailed('Missing token.') - token = request.POST['auth_token'] - return self.authenticate_credentials(token) - AUTH_CLASSES = [settings.TOKEN_AUTH] - POST_AUTH_CLASSES = [PostAuthentication] - AUTHENTICATED = [IsAuthenticated] - - def _frame(frame_id): # get frame try: @@ -55,7 +36,6 @@ def _frame(frame_id): @api_view(['POST']) -@authentication_classes(AUTH_CLASSES) @permission_classes([IsAdminUser]) def create_view(request): # loop all incoming files @@ -78,7 +58,6 @@ def create_view(request): @api_view(['GET']) -@authentication_classes(AUTH_CLASSES) @permission_classes([IsAdminUser]) def delete_view(request, frame_id): # get frame and filename @@ -165,8 +144,7 @@ def filter_frames(data, request): @api_view(['GET']) -@authentication_classes(AUTH_CLASSES) -@permission_classes(AUTHENTICATED) +@permission_classes([IsAuthenticated]) def frames_view(request): # get offset and limit offset = request.GET.get('offset', default=None) @@ -195,8 +173,7 @@ def frames_view(request): @api_view(['GET']) -@authentication_classes(AUTH_CLASSES) -@permission_classes(AUTHENTICATED) +@permission_classes([IsAuthenticated]) def aggregate_view(request): # get response data = Frame.objects @@ -227,8 +204,7 @@ def aggregate_view(request): @api_view(['GET']) -@authentication_classes(AUTH_CLASSES) -@permission_classes(AUTHENTICATED) +@permission_classes([IsAuthenticated]) def frame_view(request, frame_id): # get data frame, filename = _frame(frame_id) @@ -236,8 +212,7 @@ def frame_view(request, frame_id): @api_view(['GET']) -@authentication_classes(AUTH_CLASSES) -@permission_classes(AUTHENTICATED) +@permission_classes([IsAuthenticated]) def download_view(request, frame_id): # get frame and filename frame, filename = _frame(frame_id) @@ -251,8 +226,7 @@ def download_view(request, frame_id): @api_view(['GET']) -@authentication_classes(AUTH_CLASSES) -@permission_classes(AUTHENTICATED) +@permission_classes([IsAuthenticated]) def related_view(request, frame_id): # get frame frame, filename = _frame(frame_id) @@ -263,8 +237,7 @@ def related_view(request, frame_id): @api_view(['GET']) -@authentication_classes(AUTH_CLASSES) -@permission_classes(AUTHENTICATED) +@permission_classes([IsAuthenticated]) def headers_view(request, frame_id): # get frame and filename frame, filename = _frame(frame_id) @@ -278,6 +251,7 @@ def headers_view(request, frame_id): @api_view(['GET']) +@permission_classes([IsAuthenticated]) def preview_view(request, frame_id): import matplotlib matplotlib.use('Agg') @@ -317,8 +291,7 @@ def preview_view(request, frame_id): @api_view(['POST']) -@authentication_classes(POST_AUTH_CLASSES) -@permission_classes(AUTHENTICATED) +@permission_classes([IsAuthenticated]) def zip_view(request): # get archive root root = settings.ARCHIVE_ROOT @@ -345,8 +318,7 @@ def zip_view(request): @api_view(['GET']) -@authentication_classes(AUTH_CLASSES) -@permission_classes(AUTHENTICATED) +@permission_classes([IsAuthenticated]) def catalog_view(request, frame_id): # get frame and filename frame, filename = _frame(frame_id) diff --git a/pyobs_archive/authentication/admin.py b/pyobs_archive/authentication/admin.py new file mode 100644 index 0000000..8c38f3f --- /dev/null +++ b/pyobs_archive/authentication/admin.py @@ -0,0 +1,3 @@ +from django.contrib import admin + +# Register your models here. diff --git a/pyobs_archive/authentication/apps.py b/pyobs_archive/authentication/apps.py new file mode 100644 index 0000000..1545495 --- /dev/null +++ b/pyobs_archive/authentication/apps.py @@ -0,0 +1,5 @@ +from django.apps import AppConfig + + +class AuthenticationConfig(AppConfig): + name = 'pyobs_archive.authentication' diff --git a/pyobs_archive/authentication/authentication.py b/pyobs_archive/authentication/authentication.py deleted file mode 100644 index 2fb146f..0000000 --- a/pyobs_archive/authentication/authentication.py +++ /dev/null @@ -1,60 +0,0 @@ -import requests -from rest_framework import exceptions -from rest_framework.authentication import BaseAuthentication, get_authorization_header - - -class RemoteToken: - @property - def key(self): - return "key" - - @property - def user(self): - return "user" - - -class RemoteTokenAuthentication(BaseAuthentication): - keyword = 'Token' - - def authenticate(self, request): - auth = get_authorization_header(request).split() - - if not auth or auth[0].lower() != self.keyword.lower().encode(): - return None - - if len(auth) == 1: - msg = 'Invalid token header. No credentials provided.' - raise exceptions.AuthenticationFailed(msg) - elif len(auth) > 2: - msg = 'Invalid token header. Token string should not contain spaces.' - raise exceptions.AuthenticationFailed(msg) - - try: - token = auth[1].decode() - except UnicodeError: - msg = 'Invalid token header. Token string should not contain invalid characters.' - raise exceptions.AuthenticationFailed(msg) - - return self.authenticate_credentials(token) - - def authenticate_credentials(self, key): - # call portal - res = requests.get('https://observe.monet.uni-goettingen.de/api/profile/', - headers={'Authorization': 'Token ' + key}) - - # check - response = res.json() - if 'username' in response: - # get or create user - from django.contrib.auth.models import User - user, _ = User.objects.get_or_create(username=response['username']) - else: - raise exceptions.AuthenticationFailed('Invalid token.') - - if not user.is_active: - raise exceptions.AuthenticationFailed('User inactive or deleted.') - - return user, key - - def authenticate_header(self, request): - return self.keyword diff --git a/pyobs_archive/authentication/backends.py b/pyobs_archive/authentication/backends.py new file mode 100644 index 0000000..c586937 --- /dev/null +++ b/pyobs_archive/authentication/backends.py @@ -0,0 +1,72 @@ +from django.contrib.auth.models import User +from pyobs_archive.authentication.models import Profile +from django.conf import settings +from rest_framework import authentication, exceptions +import requests + + +class OAuth2Backend(object): + """ + Authenticate against the Oauth backend, using + grant_type: password + """ + + def authenticate(self, request, username=None, password=None): + if username == 'eng': + return None # disable eng account + response = requests.post( + settings.OAUTH_CLIENT['TOKEN_URL'], + data={ + 'grant_type': 'password', + 'username': username, + 'password': password, + 'client_id': settings.OAUTH_CLIENT['CLIENT_ID'], + 'client_secret': settings.OAUTH_CLIENT['CLIENT_SECRET'] + } + ) + if response.status_code == 200: + user, _ = User.objects.get_or_create(username=username) + Profile.objects.update_or_create( + user=user, + defaults={ + 'access_token': response.json()['access_token'], + 'refresh_token': response.json()['refresh_token'] + } + ) + return user + return None + + def get_user(self, user_id): + try: + return User.objects.get(pk=user_id) + except User.DoesNotExist: + return None + + +class BearerAuthentication(authentication.BaseAuthentication): + """ + Allows users to authenticate using the bearer token recieved from + the odin auth server + """ + def authenticate(self, request): + auth_header = request.META.get('HTTP_AUTHORIZATION', '') + if 'Bearer' not in auth_header: + return None + + bearer = auth_header.split('Bearer')[1].strip() + response = requests.get( + settings.OAUTH_CLIENT['PROFILE_URL'], + headers={'Authorization': 'Bearer {}'.format(bearer)} + ) + + if not response.status_code == 200: + raise exceptions.AuthenticationFailed('No Such User') + + user, _ = User.objects.get_or_create(username=response.json()['email']) + Profile.objects.update_or_create( + user=user, + defaults={ + 'access_token': bearer, + } + ) + return (user, None) diff --git a/pyobs_archive/authentication/migrations/0001_initial.py b/pyobs_archive/authentication/migrations/0001_initial.py new file mode 100644 index 0000000..56c64c4 --- /dev/null +++ b/pyobs_archive/authentication/migrations/0001_initial.py @@ -0,0 +1,26 @@ +# Generated by Django 3.2.4 on 2021-06-16 09:37 + +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.AUTH_USER_MODEL), + ] + + operations = [ + migrations.CreateModel( + name='Profile', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('access_token', models.CharField(default='', max_length=255)), + ('refresh_token', models.CharField(default='', max_length=255)), + ('user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), + ], + ), + ] diff --git a/pyobs_archive/authentication/migrations/__init__.py b/pyobs_archive/authentication/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/pyobs_archive/authentication/models.py b/pyobs_archive/authentication/models.py new file mode 100644 index 0000000..89dae9f --- /dev/null +++ b/pyobs_archive/authentication/models.py @@ -0,0 +1,21 @@ +from django.db import models +from django.contrib.auth.models import User +from django.conf import settings +from django.db.models.signals import post_save +from django.dispatch import receiver +from rest_framework.authtoken.models import Token +import logging + +logger = logging.getLogger() + + +class Profile(models.Model): + user = models.OneToOneField(User, on_delete=models.CASCADE) + access_token = models.CharField(max_length=255, default='') + refresh_token = models.CharField(max_length=255, default='') + + +@receiver(post_save, sender=settings.AUTH_USER_MODEL) +def create_auth_token(sender, instance=None, created=False, **kwargs): + if created: + Token.objects.create(user=instance) diff --git a/pyobs_archive/authentication/serializers.py b/pyobs_archive/authentication/serializers.py deleted file mode 100644 index c4d31c7..0000000 --- a/pyobs_archive/authentication/serializers.py +++ /dev/null @@ -1,44 +0,0 @@ -import requests -from django.contrib.auth.models import User -from rest_framework import serializers - - -class AuthTokenSerializer(serializers.Serializer): - username = serializers.CharField(label="Username") - password = serializers.CharField( - label="Password", - style={'input_type': 'password'}, - trim_whitespace=False - ) - - def validate(self, attrs): - username = attrs.get('username') - password = attrs.get('password') - - if username and password: - # call portal - res = requests.post('https://observe.monet.uni-goettingen.de/api/api-token-auth/', - data={'username': username, 'password': password}) - - # got a token? - user = None - response = res.json() - if 'token' in response: - # get or create user - user, created = User.objects.get_or_create(username=username) - - # store it - attrs['user'] = user - attrs['token'] = response['token'] - - # The authenticate call simply returns None for is_active=False - # users. (Assuming the default ModelBackend authentication - # backend.) - if not user: - msg = 'Unable to log in with provided credentials.' - raise serializers.ValidationError(msg, code='authorization') - else: - msg = 'Must include "username" and "password".' - raise serializers.ValidationError(msg, code='authorization') - - return attrs diff --git a/pyobs_archive/authentication/tests.py b/pyobs_archive/authentication/tests.py new file mode 100644 index 0000000..7ce503c --- /dev/null +++ b/pyobs_archive/authentication/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/pyobs_archive/authentication/views.py b/pyobs_archive/authentication/views.py index f0ca49e..91ea44a 100644 --- a/pyobs_archive/authentication/views.py +++ b/pyobs_archive/authentication/views.py @@ -1,26 +1,3 @@ -import logging -from rest_framework.response import Response -from rest_framework import parsers, renderers -from rest_framework.views import APIView +from django.shortcuts import render -from pyobs_archive.authentication.serializers import AuthTokenSerializer - -log = logging.getLogger(__name__) - - -class ObtainAuthToken(APIView): - throttle_classes = () - permission_classes = () - parser_classes = (parsers.FormParser, parsers.MultiPartParser, parsers.JSONParser,) - renderer_classes = (renderers.JSONRenderer,) - serializer_class = AuthTokenSerializer - - def post(self, request, *args, **kwargs): - serializer = self.serializer_class(data=request.data, - context={'request': request}) - serializer.is_valid(raise_exception=True) - token = serializer.validated_data['token'] - return Response({'token': token}) - - -obtain_auth_token = ObtainAuthToken.as_view() +# Create your views here. diff --git a/pyobs_archive/frontend/static/css/styles.css b/pyobs_archive/frontend/static/css/styles.css index 029bed1..7080801 100644 --- a/pyobs_archive/frontend/static/css/styles.css +++ b/pyobs_archive/frontend/static/css/styles.css @@ -35,6 +35,12 @@ h1, h2, h3, h4, h5, h6, th, nav, label { font-family: 'Alegreya', serif; } +.navbar-brand { + color: white; + font-size: x-large; + margin-right: 1em; +} + #wrap { min-height: 100%; } @@ -91,3 +97,12 @@ footer { padding-top: 0.5em; font-size: smaller; } + +.loginform { + min-width: 500px; + position: absolute; + text-align: center; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); +} \ No newline at end of file diff --git a/pyobs_archive/frontend/static/js/app.js b/pyobs_archive/frontend/static/js/app.js index aea1c5c..ce41311 100644 --- a/pyobs_archive/frontend/static/js/app.js +++ b/pyobs_archive/frontend/static/js/app.js @@ -53,7 +53,7 @@ $(function () { }); $('#table').bootstrapTable({ - url: rootURL + 'frames/', + url: '/frames/', ajaxOptions: { beforeSend: function (xhr) { setRequestHeader(xhr); @@ -83,7 +83,7 @@ $(function () { title: 'Name', sortable: true, formatter: function(value, row) { - let url = rootURL + 'frames/' + row.id + '/download/'; + let url = '/frames/' + row.id + '/download/'; return '' + value + '.fits.fz'; } }, { @@ -180,7 +180,7 @@ $(function () { } function detailFormatter(index, row, $detail) { - $.getJSON(rootURL + 'frames/' + row.id + '/related/', function (data) { + $.getJSON('/frames/' + row.id + '/related/', function (data) { // build HTML let div = $detail.html(`
@@ -216,11 +216,11 @@ $(function () { }); // image - div.find('img').attr('src', rootURL + 'frames/' + row.id + '/preview/'); + div.find('img').attr('src', '/frames/' + row.id + '/preview/'); // click on button div.find('button').click(function () { - $.getJSON(rootURL + 'frames/' + row.id + '/headers/', function (data) { + $.getJSON('/frames/' + row.id + '/headers/', function (data) { // build table let table = ''; for (let i = 0; i < data.results.length; i++) { @@ -260,7 +260,7 @@ $(function () { } function login(username, password, callback) { - $.post(rootURL + 'api-token-auth/', { + $.post('/api-token-auth/', { 'username': username, 'password': password }).done(function (data) { @@ -314,7 +314,7 @@ $(function () { } // get options - $.getJSON(rootURL + 'frames/aggregate/', function (data) { + $.getJSON('/frames/aggregate/', function (data) { // set options setOptions($('#imagetype'), data.imagetypes); setOptions($('#binning'), data.binnings); @@ -379,7 +379,7 @@ $(function () { }); if (frames.length > 0) { - $.fileDownload(rootURL + 'frames/zip/', { + $.fileDownload('/frames/zip/', { httpMethod: 'POST', data: {'frame_ids': frames, 'auth_token': localStorage.getItem('token')}, headers: {} diff --git a/pyobs_archive/frontend/templates/archive/index.html b/pyobs_archive/frontend/templates/archive/index.html index c88561a..938b6cd 100644 --- a/pyobs_archive/frontend/templates/archive/index.html +++ b/pyobs_archive/frontend/templates/archive/index.html @@ -1,180 +1,119 @@ - -{% load static version %} - - - pyobs archive - - - - - - - - - - - - -
- - - -
-
-
-
-
-
-
-  
-   -
- - + + - - + + - -
- -
- -
+ +
+ +
+
-
-
- - -
+
+
+
+ +
+
- - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - + - + - -
-
-
-
-
-
- -
+ +
+
+
+
+
+
+
-
+
+ -