2023-02-10 12:01:32 +11:00

416 lines
14 KiB
Python

"""
"""
from django.shortcuts import get_object_or_404, redirect, resolve_url
from django.views.generic import RedirectView
from django.views.generic.detail import DetailView
from django.views.generic.list import ListView
from django.views.generic.edit import CreateView, UpdateView
from django.core.exceptions import SuspiciousOperation
from django.http import Http404, HttpResponseRedirect
from django.db.models import Q
from django.utils import timezone
from markdown2 import markdown
from . import models, forms
from interface.utils import check_signed_url
import logging
logger = logging.getLogger(__name__)
class AuthorizedResourceMixin(object):
"""
Handles these parts of the permission system:
* signed urls
* persistent authenticated resources
* Admin enforcing
"""
SESSION_KEY = 'authorized'
admin_required = False
def is_authorized(self):
"By default check if superuser or a signed request"
if self.request.is_admin:
logger.debug("is_authorized: superuser")
return True
if 'sig' in self.request.GET:
check_signed_url(self.request.get_full_path())
self.on_signed_request()
return True
return False
def on_signed_request(self):
pass
def is_authorized_key(self, resource, key, auth):
current = self.get_authorized_keys(resource).get(str(key), None)
if current is None:
logger.debug("is_authorized_key: %s %s not in session", resource, key)
return False
if auth == current:
return True
logger.info("Authorisation revoked")
self.del_authorized_key(resource, key)
return False
def get_authorized_keys(self, resource):
'Returns a set of authorized keys for this resource'
return self._authorized.get(resource, {})
def add_authorized_key(self, resource, key, auth):
'Adds a key to the authorized list for this resource'
current = self.get_authorized_keys(resource)
current[str(key)] = auth
self._authorized[resource] = current
self.request.session[self.SESSION_KEY] = self._authorized
def del_authorized_key(self, resource, key):
logger.info("Revoking authorization for %s %s", resource, key)
current = self.get_authorized_keys(resource)
if current.pop(key, None) is None:
return False
if current:
self._authorized[resource] = current
else:
self._authorized.pop(resource)
self.request.session[self.SESSION_KEY] = self._authorized
return True
def request_denied(self):
raise Http404("Either the given resource doesn't exist or you dont have access to it.")
def dispatch(self, request, *args, **kwargs):
self._authorized = request.session.get('authorized', {})
request.is_admin = request.user.is_superuser
if not self.is_authorized():
return self.request_denied()
if self.admin_required and not request.is_admin:
return self.request_denied()
return super().dispatch(request, *args, **kwargs)
class ForgetResourceView(AuthorizedResourceMixin, RedirectView):
def is_authorized(self):
return True
def get_redirect_url(self, resource, key):
self.del_authorized_key(resource, key)
return "/"
class EnsembleMixin(AuthorizedResourceMixin):
ensemble_slug_kwarg = 'ensemble'
limited_project_access = False
def is_authorized(self):
ensemble_slug = self.kwargs[self.ensemble_slug_kwarg]
self.ensemble = get_object_or_404(models.Ensemble, slug=ensemble_slug)
if super().is_authorized():
return True
if self.ensemble.has_admin(self.request.user):
self.request.is_admin = True
return True
if self.is_authorized_key('ensemble', ensemble_slug, self.ensemble.nonce):
return True
authorized = set([ int(x) for x in self.get_authorized_keys('project').keys() ])
projects = set(self.ensemble.projects.values_list('pk', flat=True))
logger.debug("is_authorized: %r & %r", authorized, projects)
if authorized & projects:
self.limited_project_access = True
logger.debug("is_authorized: allowing due to project link")
return True
return False
def get_object(self):
return self.ensemble
class ProjectMixin(AuthorizedResourceMixin):
project_kwarg = 'project'
def is_authorized(self):
project_id = self.kwargs[self.project_kwarg]
self.project = get_object_or_404(models.Project.objects.select_related('ensemble'), pk=project_id)
if super().is_authorized():
return True
# check if the current user is an admin on the ensemble
if self.project.ensemble.has_admin(self.request.user):
logger.debug("is_authorized: ensemble admin for project")
self.request.is_admin = True
return True
if self.is_authorized_key('ensemble', self.project.ensemble.pk, self.project.ensemble.nonce):
logger.debug('is_authorized: has ensemble link for project')
return True
if self.is_authorized_key('project', project_id, self.project.nonce):
logger.debug('is_authorized: has project link')
return True
return False
# filter any generated querysets
def get_queryset(self):
return super().get_queryset().filter(project=self.project)
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
if 'project' in self.kwargs:
context['project'] = self.project
context['modules'] = self.project.active_modules
return context
class CrispyFormMixin(object):
cancel_url = None
def get_cancel_url(self):
return self.cancel_url
""" ENSEMBLE VIEWS """
class EnsembleListView(AuthorizedResourceMixin, ListView):
def is_authorized(self):
return True
def get_queryset(self):
ensembles = models.Ensemble.objects.all()
if self.request.is_admin:
return ensembles
# limit to registered ensembles
f = Q(slug__in=self.get_authorized_keys('ensemble').keys()) | Q(projects__in=self.get_authorized_keys('project').keys())
# or ensembles where the user is admin
if self.request.user.is_authenticated:
f |= Q(admins=self.request.user.pk)
return ensembles.filter(f).distinct()
class EnsembleDetailView(EnsembleMixin, DetailView):
def request_denied(self):
if 'auth' in self.request.GET:
if self.request.GET['auth'] != self.ensemble.auth():
raise SuspiciousOperation("Bad ensemble link")
self.add_authorized_key('ensemble', self.ensemble.slug, self.ensemble.nonce)
return HttpResponseRedirect(resolve_url('ensemble_detail', self.ensemble.slug))
return super().request_denied()
def get_context_data(self, **kwargs):
data = super().get_context_data(**kwargs)
inactive = 'inactive' in self.request.GET and self.request.is_admin
projects = self.ensemble.projects.all()
if self.limited_project_access:
projects = projects.filter(pk__in=self.get_authorized_keys('project').keys())
if inactive:
projects = projects.order_by('-pk')
else:
projects = projects.filter(active=True, event_date__gte=timezone.now()-timezone.timedelta(7))
data['inactive'] = inactive
data['object_list'] = projects
if self.request.is_admin:
#data['ensemble_link'] = signed_url(f'{self.request.path}?nonce={self.ensemble.nonce}')
data['ensemble_link'] = self.request.path + "?auth=" + self.ensemble.auth()
return data
""" PROJECT VIEWS """
class ProjectListView(ProjectMixin, ListView):
def is_authorized(self):
return True
def get_project_queryset(self):
projects = models.Project.objects.select_related('ensemble')
if self.request.is_admin:
return projects
f = Q(pk__in=self.get_authorized_keys('project').keys()) | Q(ensemble__slug__in=self.get_authorized_keys('ensemble').keys())
if self.request.user.is_authenticated:
f |= Q(ensemble__admins=self.request.user.pk)
return projects.filter(f)
def get_queryset(self):
qs = self.get_project_queryset()
return qs.filter(active=True, event_date__gte=timezone.now()-timezone.timedelta(7))
class ProjectCreateView(EnsembleMixin, CreateView):
admin_required = True
model = models.Project
template_name = "interface/default_form.html"
title = "Add a new project"
form_class = forms.ProjectForm
def form_valid(self, form):
self.object = form.save(commit=False)
self.object.ensemble = self.ensemble
self.object.owner = self.request.user
self.object.save()
return redirect('project_detail', project=self.object.pk)
class ProjectDetailView(ProjectMixin, DetailView):
def request_denied(self):
if 'auth' in self.request.GET:
if self.request.GET['auth'] != self.project.auth():
raise SuspiciousOperation("Bad project link")
self.add_authorized_key('project', self.project.pk, self.project.nonce)
return HttpResponseRedirect(resolve_url('project_detail', self.project.pk))
return super().request_denied()
def get_object(self, queryset=None):
return self.project
def get_context_data(self, **kwargs):
data = super().get_context_data(**kwargs)
if self.request.is_admin:
#data['project_link'] = signed_url(f'{self.request.path}?nonce={self.project.nonce}')
data['project_link'] = self.request.path + "?auth=" + self.project.auth()
return data
class ProjectUpdateView(ProjectMixin, UpdateView):
admin_required = True
template_name = "interface/default_form.html"
pk_url_kwarg = 'project'
form_class = forms.ProjectForm
def get_object(self):
return self.project
@property
def cancel_url(self):
return self.get_success_url()
def get_success_url(self):
return resolve_url('project_detail', project=self.kwargs['project'])
# Old Makefile from submission module
#class ProjectMakefileView(EnsembleMixin, DetailView):
# template_name = 'interface/project_submissions.mk'
# content_type = 'text/plain'
#
# def get_queryset(self):
# if self.request.is_admin:
# return models.Project.objects.all()
#
# return models.Project.objects.filter(ensemble=self.request.ensemble_id)
#
# def get_context_data(self, **kwargs):
# data = super().get_context_data(**kwargs)
#
# data['submissions'] = []
# data['targets'] = []
# for s in self.object.submissions:
# name = s.short_name
# data['targets'].append(name)
# data['submissions'].append({
# 'url': self.request.build_absolute_uri(signed_url('submission_download', project=self.kwargs['pk'], pk=s.pk)),
# 'name': name,
# })
#
# return data
""" WIKI VIEWS """
class WikiView(ProjectMixin, DetailView):
template_name = 'interface/wiki.html'
model = models.WikiPage
def get_context_data(self, **kwargs):
data = super().get_context_data(**kwargs)
data['wiki_html'] = markdown(self.object.markdown)
return data
class WikiCreateView(ProjectMixin, CreateView):
admin_required = True
model = models.WikiPage
template_name = 'interface/default_form.html'
form_class = forms.WikiForm
def cancel_url(self):
return resolve_url('project_detail', self.kwargs['project'])
def form_valid(self, form):
self.object = form.save(commit=False)
self.object.project = self.project
self.object.save()
return redirect('wiki', project=self.object.project_id, pk=self.object.pk)
class WikiEditView(ProjectMixin, UpdateView):
admin_required = True
model = models.WikiPage
form_class = forms.WikiForm
def cancel_url(self):
return resolve_url('wiki', self.kwargs['project'], self.kwargs['pk'])
""" RESOURCE VIEWS """
class ResourceCreateView(ProjectMixin, CreateView):
admin_required = True
model = models.Resource
form_class = forms.ResourceForm
template_name = 'interface/default_form.html'
title = "Add a new resource"
def form_valid(self, form):
self.object = form.save(commit=False)
self.object.project = self.project
self.object.save()
return redirect('resource_upload', project=self.object.project_id, pk=self.object.pk)
class ResourceUploadView(ProjectMixin, UpdateView):
admin_required = True
model = models.Resource
fields = ['file']
template_name = 'interface/default_form.html'
def get_success_url(self):
return resolve_url('resource_list', project=self.kwargs['project'])
class ResourceListView(ProjectMixin, ListView):
model = models.Resource
def get_queryset(self):
qs = super().get_queryset()
if not self.request.is_admin:
qs = qs.filter(visible=True)
return qs
class ResourceEditView(ProjectMixin, UpdateView):
admin_required = True
model = models.Resource
fields = ['name', 'description', 'visible']
template_name = 'interface/default_form.html'
def get_success_url(self):
return resolve_url('resource_list', project=self.kwargs['project'])