From 719b3b59b6d5aed24bc0a801ea6c7396e13aea75 Mon Sep 17 00:00:00 2001 From: Tobias Bengfort Date: Mon, 4 Oct 2021 16:34:53 +0200 Subject: [PATCH 1/5] split APIAuthMixin and PermissionRequiredMixin --- castellum/castellum_auth/mixins.py | 34 +++++++++--------------------- castellum/execution/api.py | 3 ++- 2 files changed, 12 insertions(+), 25 deletions(-) diff --git a/castellum/castellum_auth/mixins.py b/castellum/castellum_auth/mixins.py index 13ef0ec86..574b82c45 100644 --- a/castellum/castellum_auth/mixins.py +++ b/castellum/castellum_auth/mixins.py @@ -39,37 +39,25 @@ class PermissionRequiredMixin(auth_mixins.PermissionRequiredMixin): class APIAuthMixin(StrongholdPublicMixin): - """Similar to PermissionRequiredMixin. + """Authenticate with token header instead of login. - Differences: - - Does not redirect to login on error - - Gets request.user from the HTTP_AUTHORIZATION header + This should be the first mixin so that authentication is checked + before anything else. """ - permission_required = None - def handle_no_permission(self): - raise PermissionDenied + raise_exception = True - def get_permission_required(self): - return self.permission_required - - def get_permission_object(self): - return None - - def has_permission(self): - perms = self.get_permission_required() - permission_object = self.get_permission_object() - return self.request.user.has_perms(perms, obj=permission_object) - - def authenticate(self): + def get_token(self): auth = self.request.headers.get('Authorization', '').split() if len(auth) != 2 or auth[0].lower() != 'token': - self.handle_no_permission() + raise PermissionDenied + return auth[1] + def authenticate(self): try: - user = User.objects.get(token=auth[1]) + user = User.objects.get(token=self.get_token()) except User.DoesNotExist: - self.handle_no_permission() + raise PermissionDenied if not user.expiration_date: raise PermissionDenied('Account is not activated.') @@ -80,6 +68,4 @@ class APIAuthMixin(StrongholdPublicMixin): def dispatch(self, request, *args, **kwargs): self.request.user = self.authenticate() - if not self.has_permission(): - return self.handle_no_permission() return super().dispatch(request, *args, **kwargs) diff --git a/castellum/execution/api.py b/castellum/execution/api.py index 617fda957..f7983b3e0 100644 --- a/castellum/execution/api.py +++ b/castellum/execution/api.py @@ -30,6 +30,7 @@ from django.shortcuts import get_object_or_404 from django.views.generic import View from castellum.castellum_auth.mixins import APIAuthMixin +from castellum.castellum_auth.mixins import PermissionRequiredMixin from castellum.pseudonyms.helpers import get_pseudonym from castellum.pseudonyms.helpers import get_subject from castellum.pseudonyms.models import Domain @@ -42,7 +43,7 @@ from castellum.subjects.models import Subject monitoring_logger = logging.getLogger('monitoring.execution') -class BaseAPIView(StudyMixin, APIAuthMixin, View): +class BaseAPIView(APIAuthMixin, StudyMixin, PermissionRequiredMixin, View): permission_required = ['recruitment.conduct_study'] study_status = [Study.EXECUTION] -- GitLab From ad7b7b75f868bfd8a0bb68ef5a511ac594ce5ed6 Mon Sep 17 00:00:00 2001 From: Tobias Bengfort Date: Mon, 4 Oct 2021 17:20:01 +0200 Subject: [PATCH 2/5] add ParamAuthMixin --- castellum/castellum_auth/mixins.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/castellum/castellum_auth/mixins.py b/castellum/castellum_auth/mixins.py index 574b82c45..74c955f3d 100644 --- a/castellum/castellum_auth/mixins.py +++ b/castellum/castellum_auth/mixins.py @@ -69,3 +69,10 @@ class APIAuthMixin(StrongholdPublicMixin): def dispatch(self, request, *args, **kwargs): self.request.user = self.authenticate() return super().dispatch(request, *args, **kwargs) + + +class ParamAuthMixin(APIAuthMixin): + def get_token(self): + if 'token' not in self.request.GET: + raise PermissionDenied + return self.request.GET['token'] -- GitLab From 3123856a7d0c3106cbe4de0f13f78a9bf05057c8 Mon Sep 17 00:00:00 2001 From: Tobias Bengfort Date: Wed, 29 Sep 2021 20:18:32 +0200 Subject: [PATCH 3/5] replace django-ical to better integrate with generic views --- castellum/appointments/mixins.py | 10 +++--- castellum/execution/feeds.py | 34 +++++++++----------- castellum/execution/urls.py | 4 +-- castellum/recruitment/feeds.py | 54 ++++++++++++++------------------ castellum/recruitment/mixins.py | 8 ++--- castellum/recruitment/urls.py | 8 ++--- castellum/utils/feeds.py | 48 ++++++++++++++++++---------- setup.cfg | 1 - 8 files changed, 84 insertions(+), 83 deletions(-) diff --git a/castellum/appointments/mixins.py b/castellum/appointments/mixins.py index 1145ae937..54bee5d13 100644 --- a/castellum/appointments/mixins.py +++ b/castellum/appointments/mixins.py @@ -33,7 +33,7 @@ from castellum.castellum_auth.mixins import PermissionRequiredMixin from castellum.recruitment.mixins import ParticipationMixin from castellum.recruitment.models import Participation from castellum.utils import add_working_days -from castellum.utils.feeds import BaseICalFeed +from castellum.utils.feeds import BaseCalendarFeed from castellum.utils.mail import MailContext from .forms import AppointmentsForm @@ -137,12 +137,12 @@ class BaseCalendarView(DetailView): return super().get(request, *args, **kwargs) -class BaseAppointmentFeed(BaseICalFeed): - def items(self, obj): +class BaseAppointmentFeed(BaseCalendarFeed): + def items(self): return Appointment.objects.filter(participation__status=Participation.INVITED) - def item_start_datetime(self, item): + def item_start(self, item): return item.start - def item_end_datetime(self, item): + def item_end(self, item): return item.end diff --git a/castellum/execution/feeds.py b/castellum/execution/feeds.py index ce3479818..50da91d62 100644 --- a/castellum/execution/feeds.py +++ b/castellum/execution/feeds.py @@ -19,12 +19,13 @@ # License along with Castellum. If not, see # . -from django.core.exceptions import PermissionDenied -from django.shortcuts import get_object_or_404 from django.urls import reverse from django.utils.translation import gettext_lazy as _ from castellum.appointments.mixins import BaseAppointmentFeed +from castellum.castellum_auth.mixins import ParamAuthMixin +from castellum.castellum_auth.mixins import PermissionRequiredMixin +from castellum.studies.mixins import StudyMixin from castellum.studies.models import Study @@ -45,30 +46,25 @@ class BaseExecutionAppointmentFeed(BaseAppointmentFeed): return self.request.build_absolute_uri(self.item_link(item)) -class AppointmentFeedForStudy(BaseExecutionAppointmentFeed): - def get_object(self, request, *args, **kwargs): - study = get_object_or_404(Study, pk=kwargs['pk'], status=Study.EXECUTION) - perms = ['recruitment.conduct_study', 'studies.access_study'] - if not self.request.user.has_perms(perms, obj=study): - raise PermissionDenied - return study +class AppointmentFeedForStudy( + ParamAuthMixin, StudyMixin, PermissionRequiredMixin, BaseExecutionAppointmentFeed +): + permission_required = 'recruitment.conduct_study' + study_status = [Study.EXECUTION] - def items(self, study): - return super().items(study)\ - .filter(session__study=study)\ + def items(self): + return super().items()\ + .filter(session__study=self.study)\ .select_related('participation__subject', 'session__study') -class AppointmentFeedForUser(BaseExecutionAppointmentFeed): - def get_object(self, request, *args, **kwargs): - return self.request.user - - def items(self, user): - items = super().items(user)\ +class AppointmentFeedForUser(ParamAuthMixin, BaseExecutionAppointmentFeed): + def items(self): + items = super().items()\ .filter(session__study__status=Study.EXECUTION)\ .select_related('participation__subject', 'session__study') perms = ['recruitment.conduct_study', 'studies.access_study'] for item in items: - if user.has_perms(perms, obj=item.session.study): + if self.request.user.has_perms(perms, obj=item.session.study): yield item diff --git a/castellum/execution/urls.py b/castellum/execution/urls.py index 51e98ff49..af1e31c6d 100644 --- a/castellum/execution/urls.py +++ b/castellum/execution/urls.py @@ -52,12 +52,12 @@ from .views import TagView app_name = 'execution' urlpatterns = [ - path('calendar/feed/', AppointmentFeedForUser(), name='calendar-user-feed'), + path('calendar/feed/', AppointmentFeedForUser.as_view(), name='calendar-user-feed'), path('/', StudyDetailView.as_view(), name='study-detail'), path('/resolve/', ResolveView.as_view(), name='resolve'), path('/news/', NewsMailView.as_view(), name='news-mail'), path('/calendar/', CalendarView.as_view(), name='calendar'), - path('/calendar/feed/', AppointmentFeedForStudy(), name='calendar-feed'), + path('/calendar/feed/', AppointmentFeedForStudy.as_view(), name='calendar-feed'), path('/criteria/', ExclusionCriteriaView.as_view(), name='criteria'), path('/export/', ExportView.as_view(), name='export'), path('/progress/', ProgressView.as_view(), name='progress'), diff --git a/castellum/recruitment/feeds.py b/castellum/recruitment/feeds.py index 6b9d52a0e..86747a02d 100644 --- a/castellum/recruitment/feeds.py +++ b/castellum/recruitment/feeds.py @@ -19,10 +19,12 @@ # License along with Castellum. If not, see # . -from django.core.exceptions import PermissionDenied from django.shortcuts import get_object_or_404 from castellum.appointments.mixins import BaseAppointmentFeed +from castellum.castellum_auth.mixins import ParamAuthMixin +from castellum.castellum_auth.mixins import PermissionRequiredMixin +from castellum.studies.mixins import StudyMixin from castellum.studies.models import Resource from castellum.studies.models import Study @@ -30,28 +32,21 @@ from .mixins import BaseFollowUpFeed from .models import Participation -class FollowUpFeedForStudy(BaseFollowUpFeed): - def get_object(self, request, *args, **kwargs): - study = get_object_or_404(Study, pk=kwargs.get('study_pk'), status=Study.EXECUTION) - perms = ['recruitment.recruit', 'studies.access_study'] - if not self.request.user.has_perms(perms, obj=study): - raise PermissionDenied - return study +class FollowUpFeedForStudy(ParamAuthMixin, StudyMixin, PermissionRequiredMixin, BaseFollowUpFeed): + permission_required = 'recruitment.recruit' + study_status = [Study.EXECUTION] - def items(self, study): + def items(self): return Participation.objects\ - .filter(study=study, followup_date__isnull=False)\ + .filter(study=self.study, followup_date__isnull=False)\ .order_by('-followup_date', '-followup_time') -class FollowUpFeedForUser(BaseFollowUpFeed): - def get_object(self, request, *args, **kwargs): - return self.request.user - - def items(self, user): +class FollowUpFeedForUser(ParamAuthMixin, BaseFollowUpFeed): + def items(self): perms = ['recruitment.recruit', 'studies.access_study'] for study in Study.objects.filter(status=Study.EXECUTION): - if not user.has_perms(perms, obj=study): + if not self.request.user.has_perms(perms, obj=study): continue qs = Participation.objects\ .filter(study=study, followup_date__isnull=False)\ @@ -60,29 +55,26 @@ class FollowUpFeedForUser(BaseFollowUpFeed): yield participation -class AppointmentFeed(BaseAppointmentFeed): - def get_object(self, request, *args, **kwargs): - study = get_object_or_404(Study, pk=kwargs['pk'], status=Study.EXECUTION) - perms = ['appointments.change_appointment', 'studies.access_study'] - if not self.request.user.has_perms(perms, obj=study): - raise PermissionDenied - return study +class AppointmentFeed(ParamAuthMixin, StudyMixin, PermissionRequiredMixin, BaseAppointmentFeed): + permission_required = 'appointments.change_appointment' + study_status = [Study.EXECUTION] - def items(self, study): - return super().items(study).filter(session__study=study) + def items(self): + return super().items().filter(session__study=self.study) + + def item_title(self, item): + return '' def item_link(self, item): # This is not really a usable link, but it is unique return '/appointments/{}/'.format(item.pk) -class ResourceFeed(BaseAppointmentFeed): - def get_object(self, request, *args, **kwargs): - return get_object_or_404(Resource, pk=kwargs['pk']) - - def items(self, resource): +class ResourceFeed(ParamAuthMixin, BaseAppointmentFeed): + def items(self): + resource = get_object_or_404(Resource, pk=self.kwargs['pk']) return ( - super().items(resource) + super().items() .filter(session__resource=resource) .exclude(session__study__status=Study.FINISHED) ) diff --git a/castellum/recruitment/mixins.py b/castellum/recruitment/mixins.py index d3e446942..1dd50a5be 100644 --- a/castellum/recruitment/mixins.py +++ b/castellum/recruitment/mixins.py @@ -40,7 +40,7 @@ from castellum.castellum_auth.mixins import PermissionRequiredMixin from castellum.studies.mixins import StudyMixin from castellum.subjects.mixins import SubjectMixin from castellum.subjects.models import Subject -from castellum.utils.feeds import BaseICalFeed +from castellum.utils.feeds import BaseCalendarFeed from castellum.utils.mail import MailContext from . import filter_queries @@ -146,17 +146,17 @@ class BaseAttributesUpdateView(PermissionRequiredMixin, UpdateView): return context -class BaseFollowUpFeed(BaseICalFeed): +class BaseFollowUpFeed(BaseCalendarFeed): def item_title(self, item): return '{} - Follow-Up'.format(item.study.name) - def item_start_datetime(self, item): + def item_start(self, item): if item.followup_time: return datetime.datetime.combine(item.followup_date, item.followup_time) else: return item.followup_date - def item_updateddate(self, item): + def item_updated_at(self, item): return item.updated_at def item_link(self, item): diff --git a/castellum/recruitment/urls.py b/castellum/recruitment/urls.py index 261385b18..877ef500c 100644 --- a/castellum/recruitment/urls.py +++ b/castellum/recruitment/urls.py @@ -44,8 +44,8 @@ from .views import SchedulerPingView app_name = 'recruitment' urlpatterns = [ - path('followups/', FollowUpFeedForUser(), name='followups-user-feed'), - path('resources//', ResourceFeed(), name='resource-feed'), + path('followups/', FollowUpFeedForUser.as_view(), name='followups-user-feed'), + path('resources//', ResourceFeed.as_view(), name='resource-feed'), path('scheduler-ping///', SchedulerPingView.as_view()), path('/', RecruitmentViewOpen.as_view(), name='recruitment-open'), path('/invited/', RecruitmentViewInvited.as_view(), name='recruitment-invited'), @@ -67,8 +67,8 @@ urlpatterns = [ ), path('/cleanup/', RecruitmentCleanupView.as_view(), name='cleanup'), path('/calendar/', CalendarView.as_view(), name='calendar'), - path('/calendar/feed/', AppointmentFeed(), name='calendar-feed'), - path('/followups/', FollowUpFeedForStudy(), name='followups-feed'), + path('/calendar/feed/', AppointmentFeed.as_view(), name='calendar-feed'), + path('/followups/', FollowUpFeedForStudy.as_view(), name='followups-feed'), path('//', ContactView.as_view(), name='contact'), path( '//appointments/', diff --git a/castellum/utils/feeds.py b/castellum/utils/feeds.py index 4c32e7921..0a949f881 100644 --- a/castellum/utils/feeds.py +++ b/castellum/utils/feeds.py @@ -19,26 +19,40 @@ # License along with Castellum. If not, see # . -from django.core.exceptions import PermissionDenied +from django.http import HttpResponse +from django.utils import timezone +from django.views.generic import View -from django_ical.views import ICalFeed -from stronghold.decorators import public +from icalendar import Calendar +from icalendar import Event -from castellum.castellum_auth.models import User +class BaseCalendarFeed(View): + def get(self, request, **kwargs): + cal = Calendar() + cal.add('version', '2.0') + cal.add('prodid', 'castellum') -@public -class BaseICalFeed(ICalFeed): - def __call__(self, request, *args, **kwargs): - self.request = request - try: - self.request.user = User.objects.get(token=request.GET.get('token')) - except User.DoesNotExist: - raise PermissionDenied - return super().__call__(request, *args, **kwargs) + for item in self.items(): + event = Event() + event.add('uid', self._item_link(item)) + event.add('dtstamp', self.item_updated_at(item)) + event.add('summary', self.item_title(item)) + event.add('dtstart', self.item_start(item)) + if hasattr(self, 'item_end'): + event.add('dtend', self.item_end(item)) + if hasattr(self, 'item_link'): + event.add('url', self._item_link(item)) + if hasattr(self, 'item_description'): + event.add('description', self.item_description(item)) + cal.add_component(event) - def item_title(self, item): - return None + response = HttpResponse(content_type='text/calendar') + response.write(cal.to_ical()) + return response - def item_description(self, item): - return None + def item_updated_at(self, item): + return timezone.now() + + def _item_link(self, item): + return self.request.build_absolute_uri(self.item_link(item)) diff --git a/setup.cfg b/setup.cfg index f117bf6d5..8732600db 100644 --- a/setup.cfg +++ b/setup.cfg @@ -14,7 +14,6 @@ install_requires = cologne-phonetics == 1.3.0 Django == 3.2.8 django-bootstrap4 == 3.0.1 - django-ical == 1.8.0 django-mfa3 == 0.3.0 django-npm == 1.0.0 django-parler == 2.2.0 -- GitLab From 2c28be62d6caf3645ff2a292f4efec780aae0d9b Mon Sep 17 00:00:00 2001 From: Tobias Bengfort Date: Mon, 18 Oct 2021 16:36:30 +0200 Subject: [PATCH 4/5] split link and UID --- castellum/recruitment/feeds.py | 4 ++-- castellum/utils/feeds.py | 5 ++++- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/castellum/recruitment/feeds.py b/castellum/recruitment/feeds.py index 86747a02d..ba9cd5a14 100644 --- a/castellum/recruitment/feeds.py +++ b/castellum/recruitment/feeds.py @@ -65,7 +65,7 @@ class AppointmentFeed(ParamAuthMixin, StudyMixin, PermissionRequiredMixin, BaseA def item_title(self, item): return '' - def item_link(self, item): + def item_uid(self, item): # This is not really a usable link, but it is unique return '/appointments/{}/'.format(item.pk) @@ -82,6 +82,6 @@ class ResourceFeed(ParamAuthMixin, BaseAppointmentFeed): def item_title(self, item): return item.session.study.name - def item_link(self, item): + def item_uid(self, item): # This is not really a usable link, but it is unique return '/appointments/{}/'.format(item.pk) diff --git a/castellum/utils/feeds.py b/castellum/utils/feeds.py index 0a949f881..633fd9519 100644 --- a/castellum/utils/feeds.py +++ b/castellum/utils/feeds.py @@ -35,7 +35,7 @@ class BaseCalendarFeed(View): for item in self.items(): event = Event() - event.add('uid', self._item_link(item)) + event.add('uid', request.build_absolute_uri(self.item_uid(item))) event.add('dtstamp', self.item_updated_at(item)) event.add('summary', self.item_title(item)) event.add('dtstart', self.item_start(item)) @@ -54,5 +54,8 @@ class BaseCalendarFeed(View): def item_updated_at(self, item): return timezone.now() + def item_uid(self, item): + return self.item_link(item) + def _item_link(self, item): return self.request.build_absolute_uri(self.item_link(item)) -- GitLab From 2777d3749820aced661e63ee6c932cf07e5946a3 Mon Sep 17 00:00:00 2001 From: Tobias Bengfort Date: Wed, 29 Sep 2021 20:22:53 +0200 Subject: [PATCH 5/5] use link for description by default --- castellum/execution/feeds.py | 3 --- castellum/recruitment/mixins.py | 3 --- castellum/utils/feeds.py | 2 ++ 3 files changed, 2 insertions(+), 6 deletions(-) diff --git a/castellum/execution/feeds.py b/castellum/execution/feeds.py index 50da91d62..fbf34e07b 100644 --- a/castellum/execution/feeds.py +++ b/castellum/execution/feeds.py @@ -42,9 +42,6 @@ class BaseExecutionAppointmentFeed(BaseAppointmentFeed): item.session.study.pk, item.participation.pk ]) - def item_description(self, item): - return self.request.build_absolute_uri(self.item_link(item)) - class AppointmentFeedForStudy( ParamAuthMixin, StudyMixin, PermissionRequiredMixin, BaseExecutionAppointmentFeed diff --git a/castellum/recruitment/mixins.py b/castellum/recruitment/mixins.py index 1dd50a5be..4b3639f18 100644 --- a/castellum/recruitment/mixins.py +++ b/castellum/recruitment/mixins.py @@ -162,9 +162,6 @@ class BaseFollowUpFeed(BaseCalendarFeed): def item_link(self, item): return reverse('recruitment:contact', args=[item.study.pk, item.pk]) - def item_description(self, item): - return self.request.build_absolute_uri(self.item_link(item)) - class BaseMailRecruitmentView(StudyMixin, PermissionRequiredMixin, FormView): template_name = 'recruitment/mail.html' diff --git a/castellum/utils/feeds.py b/castellum/utils/feeds.py index 633fd9519..078c4c0e9 100644 --- a/castellum/utils/feeds.py +++ b/castellum/utils/feeds.py @@ -45,6 +45,8 @@ class BaseCalendarFeed(View): event.add('url', self._item_link(item)) if hasattr(self, 'item_description'): event.add('description', self.item_description(item)) + elif hasattr(self, 'item_link'): + event.add('description', self._item_link(item)) cal.add_component(event) response = HttpResponse(content_type='text/calendar') -- GitLab