""" """ 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'])