From 37dca7d57019ca016d6367a1560924b36a1258a2 Mon Sep 17 00:00:00 2001 From: ingo <ingo.leindecker@fro.at> Date: Mon, 26 Feb 2018 16:17:11 +0100 Subject: [PATCH] Made OIDC Authentication work with REST API * added corsheaders middleware to work properly * fixed bug when updating a timeslot * after updating a timeslot, its repetition will be returned if there's one * included field 'ppoi' in shows See #22 #23 --- program/auth.py | 26 +++++++++++++++++++ program/migrations/0016_auto_20180222_1253.py | 21 +++++++++++++++ program/models.py | 4 +-- program/serializers.py | 5 +++- program/views.py | 21 +++++++++++---- pv/settings.py | 23 ++++++++-------- pv/urls.py | 2 -- 7 files changed, 80 insertions(+), 22 deletions(-) create mode 100644 program/auth.py create mode 100644 program/migrations/0016_auto_20180222_1253.py diff --git a/program/auth.py b/program/auth.py new file mode 100644 index 00000000..958b9a74 --- /dev/null +++ b/program/auth.py @@ -0,0 +1,26 @@ +# -*- coding: utf-8 -*- + +from rest_framework import authentication +from rest_framework import exceptions + +from oidc_provider.models import Token +from oidc_provider.lib.utils.oauth2 import extract_access_token + + +class OidcOauth2Auth(authentication.BaseAuthentication): + def authenticate(self, request): + access_token = extract_access_token(request) + + if not access_token: + # not this kind of auth + return None + oauth2_token = None + try: + oauth2_token = Token.objects.get(access_token=access_token) + except Token.DoesNotExist: + raise exceptions.AuthenticationFailed("The oauth2 token is invalid") + + if oauth2_token.has_expired(): + raise exceptions.AuthenticationFailed("The oauth2 token has expired") + + return oauth2_token.user, None \ No newline at end of file diff --git a/program/migrations/0016_auto_20180222_1253.py b/program/migrations/0016_auto_20180222_1253.py new file mode 100644 index 00000000..d9cb5da4 --- /dev/null +++ b/program/migrations/0016_auto_20180222_1253.py @@ -0,0 +1,21 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.3 on 2018-02-22 12:53 +from __future__ import unicode_literals + +from django.db import migrations +import versatileimagefield.fields + + +class Migration(migrations.Migration): + + dependencies = [ + ('program', '0015_auto_20180218_1111'), + ] + + operations = [ + migrations.AlterField( + model_name='note', + name='image', + field=versatileimagefield.fields.VersatileImageField(blank=True, height_field='height', help_text="Upload an image to your note. Images are automatically cropped around the 'Primary Point of Interest'. Click in the image to change it and press Save.", null=True, upload_to='note_images', verbose_name='Featured image', width_field='width'), + ), + ] diff --git a/program/models.py b/program/models.py index a1f047e7..4a3fe716 100644 --- a/program/models.py +++ b/program/models.py @@ -780,7 +780,7 @@ class Schedule(models.Model): return {'detail': _("Start and until dates mustn't be the same")} if schedule.until < schedule.dstart: - return {'detail': _("Until date mustn't before start")} + return {'detail': _("Until date mustn't be before start")} num_conflicts = len([pr for pr in conflicts['projected'] if len(pr['collisions']) > 0]) @@ -1168,7 +1168,7 @@ class Note(models.Model): ppoi = PPOIField('Image PPOI') height = models.PositiveIntegerField('Image Height', blank=True, null=True, editable=False) width = models.PositiveIntegerField('Image Width', blank=True, null=True,editable=False) - image = VersatileImageField(_("Featured image"), blank=True, null=True, upload_to='note_images', width_field='width', height_field='height', ppoi_field='ppoi', help_text=_("Upload an image to your show. Images are automatically cropped around the 'Primary Point of Interest'. Click in the image to change it and press Save.")) + image = VersatileImageField(_("Featured image"), blank=True, null=True, upload_to='note_images', width_field='width', height_field='height', ppoi_field='ppoi', help_text=_("Upload an image to your note. Images are automatically cropped around the 'Primary Point of Interest'. Click in the image to change it and press Save.")) status = models.IntegerField(_("Status"), choices=STATUS_CHOICES, default=1) start = models.DateTimeField(editable=False) show = models.ForeignKey(Show, related_name='notes', editable=True) diff --git a/program/serializers.py b/program/serializers.py index dcc20c72..696f8eed 100644 --- a/program/serializers.py +++ b/program/serializers.py @@ -128,6 +128,7 @@ class HostSerializer(serializers.ModelSerializer): instance.dorftv_url = validated_data.get('dorftv_url', instance.dorftv_url) instance.cba_url = validated_data.get('cba_url', instance.cba_url) instance.image = validated_data.get('image', instance.image) + instance.ppoi = validated_data.get('ppoi', instance.ppoi) instance.save() return instance @@ -251,7 +252,7 @@ class ShowSerializer(serializers.HyperlinkedModelSerializer): class Meta: model = Show - fields = ('id', 'name', 'slug', 'image', 'logo', 'short_description', 'description', + fields = ('id', 'name', 'slug', 'image', 'ppoi', 'logo', 'short_description', 'description', 'email', 'website', 'created', 'last_updated', 'type', 'fundingcategory', 'predecessor_id', 'cba_series_id', 'fallback_id', 'category', 'hosts', 'owners', 'language', 'topic', 'musicfocus', 'thumbnails') @@ -292,6 +293,7 @@ class ShowSerializer(serializers.HyperlinkedModelSerializer): instance.name = validated_data.get('name', instance.name) instance.slug = validated_data.get('slug', instance.slug) instance.image = validated_data.get('image', instance.image) + instance.ppoi = validated_data.get('ppoi', instance.ppoi) instance.logo = validated_data.get('logo', instance.logo) instance.short_description = validated_data.get('short_description', instance.short_description) instance.description = validated_data.get('description', instance.description) @@ -424,6 +426,7 @@ class NoteSerializer(serializers.ModelSerializer): instance.summary = validated_data.get('summary', instance.summary) instance.content = validated_data.get('content', instance.content) instance.image = validated_data.get('image', instance.image) + instance.ppoi = validated_data.get('ppoi', instance.ppoi) instance.status = validated_data.get('status', instance.status) instance.host = validated_data.get('host', instance.host) instance.cba_id = validated_data.get('cba_id', instance.cba_id) diff --git a/program/views.py b/program/views.py index 5aa7a5aa..7bebcfb4 100644 --- a/program/views.py +++ b/program/views.py @@ -678,6 +678,7 @@ class APIScheduleViewSet(viewsets.ModelViewSet): + class APITimeSlotViewSet(viewsets.ModelViewSet): """ /api/v1/timeslots Returns timeslots of the next 60 days (GET) - Timeslots may only be added by creating/updating a schedule @@ -687,7 +688,7 @@ class APITimeSlotViewSet(viewsets.ModelViewSet): /api/v1/shows/1/timeslots/1 Returns a timeslots by its ID (GET, PUT, DELETE) /api/v1/shows/1/timeslots?start=2017-01-01&end=2017-02-01 Returns timeslots of the show within the given timerange /api/v1/shows/1/schedules/1/timeslots Returns all timeslots of the schedule (GET, POST) - /api/v1/shows/1/schedules/1/timeslots/1 Returns a timeslot by its ID (GET, PUT, DELETE) + /api/v1/shows/1/schedules/1/timeslots/1 Returns a timeslot by its ID (GET, DELETE). If PUT, the next repetition is returned or nothing if the next timeslot isn't one /api/v1/shows/1/schedules/1/timeslots?start=2017-01-01&end=2017-02-01 Returns all timeslots of the schedule within the given timerange """ @@ -762,16 +763,26 @@ class APITimeSlotViewSet(viewsets.ModelViewSet): def update(self, request, pk=None, schedule_pk=None, show_pk=None): """Link a playlist_id to a timeslot""" - timeslot = get_object_or_404(TimeSlot, pk=pk, schedule=schedule_pk, show=show_pk) - # Update is only allowed when calling /shows/1/schedules/1/timeslots/1 and if user owns the show - if schedule_pk == None or show_pk == None or not Show.is_editable(self, timeslot.show_id): + if schedule_pk == None or show_pk == None or not Show.is_editable(self, show_pk): return Response(status=status.HTTP_401_UNAUTHORIZED) + timeslot = get_object_or_404(TimeSlot, pk=pk, schedule=schedule_pk, show=show_pk) + serializer = TimeSlotSerializer(timeslot, data=request.data) if serializer.is_valid(): serializer.save() - return Response(serializer.data, status=status.HTTP_200_OK) + + # Return the next repetition + # We do this because the Dashboard needs to update the repetition timeslot as well + # but with another playlist containing the recording instead of the original playlist + ts = TimeSlot.objects.filter(show=show_pk, start__gt=timeslot.start)[0] + if ts.is_repetition: + serializer = TimeSlotSerializer(ts) + return Response(serializer.data, status=status.HTTP_200_OK) + + # ...or nothing if there isn't one + return Response(status=status.HTTP_200_OK) return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) diff --git a/pv/settings.py b/pv/settings.py index 7c40541c..ae747e6e 100644 --- a/pv/settings.py +++ b/pv/settings.py @@ -30,9 +30,6 @@ DATABASE_ROUTERS = ['nop.dbrouter.NopRouter'] TIME_ZONE = 'Europe/Vienna' -USE_TZ = True # django-oidc-provider needs timezones in database -LOGIN_URL = '/admin/login/' # Login page OIDC redirects to - LANGUAGE_CODE = 'de' SITE_ID = 1 @@ -73,12 +70,13 @@ TEMPLATES = [ }, ] -MIDDLEWARE_CLASSES = ( +MIDDLEWARE = ( 'django.middleware.common.CommonMiddleware', 'django.contrib.sessions.middleware.SessionMiddleware', 'django.middleware.csrf.CsrfViewMiddleware', 'django.contrib.auth.middleware.AuthenticationMiddleware', 'django.contrib.messages.middleware.MessageMiddleware', + 'corsheaders.middleware.CorsMiddleware', ) ROOT_URLCONF = 'pv.urls' @@ -90,16 +88,11 @@ REST_FRAMEWORK = { #'rest_framework.permissions.IsAuthenticatedOrReadOnly', 'rest_framework.permissions.DjangoModelPermissionsOrAnonReadOnly' ], - 'DEFAULT AUTHENTICATION_CLASSES': [ + 'DEFAULT_AUTHENTICATION_CLASSES': [ + 'program.auth.OidcOauth2Auth', ], } -CORS_ALLOW_CREDENTIALS = True -CORS_ORIGIN_WHITELIST = ( - 'localhost', - 'localhost:8080', -) - INSTALLED_APPS = ( 'django.contrib.auth', 'django.contrib.contenttypes', @@ -117,7 +110,7 @@ INSTALLED_APPS = ( 'rest_framework_nested', 'frapp', 'oidc_provider', - 'corsheaders' + 'corsheaders', ) THUMBNAIL_SIZES = ['640x480', '200x200', '150x150'] @@ -165,6 +158,12 @@ CBA_AJAX_URL = CBA_URL + '/wp-admin/admin-ajax.php' CBA_REST_API_URL = CBA_URL + '/wp-json/wp/v2/' +# OIDC Provider Settings + +USE_TZ = True # django-oidc-provider needs timezones in database +LOGIN_URL = '/admin/login/' # Login page OIDC redirects to + + try: from .local_settings import * except ImportError: diff --git a/pv/urls.py b/pv/urls.py index e7baee15..1ce1b772 100644 --- a/pv/urls.py +++ b/pv/urls.py @@ -5,8 +5,6 @@ from django.conf.urls import url, include from django.contrib import admin from django.views.static import serve from rest_framework_nested import routers -from rest_framework.authtoken import views -from oidc_provider import urls from program.views import APIUserViewSet, APIHostViewSet, APIShowViewSet, APIScheduleViewSet, APITimeSlotViewSet, APINoteViewSet, APICategoryViewSet, APITypeViewSet, APITopicViewSet, APIMusicFocusViewSet, APIFundingCategoryViewSet, APILanguageViewSet, json_day_schedule, json_playout, json_timeslots_specials -- GitLab