From 730889b9a14c3a13db07772519457806ad6b8617 Mon Sep 17 00:00:00 2001 From: Jan Koppe Date: Fri, 1 May 2020 12:38:57 +0200 Subject: [PATCH] add object permissions with guardian; users only have access to their own streams/restreamconfigs --- portier/common/handlers.py | 10 +++++++ portier/settings.py | 6 ++++ requirements.txt | 1 + restream/admin.py | 3 +- restream/forms.py | 16 ++++++++++ restream/models.py | 5 ++++ .../restream/restreamconfig_list.html | 7 +++++ restream/views.py | 29 ++++++++++++++++++- rtmp/admin.py | 5 ++-- rtmp/models.py | 9 +++++- rtmp/templates/rtmp/stream_list.html | 7 +++++ rtmp/views.py | 21 ++++++++++++++ 12 files changed, 114 insertions(+), 5 deletions(-) create mode 100644 portier/common/handlers.py create mode 100644 restream/forms.py diff --git a/portier/common/handlers.py b/portier/common/handlers.py new file mode 100644 index 0000000..39451de --- /dev/null +++ b/portier/common/handlers.py @@ -0,0 +1,10 @@ +from django.contrib.contenttypes.models import ContentType +from django.db.models import Q +from guardian.models import UserObjectPermission +from guardian.models import GroupObjectPermission + + +def remove_obj_perms_connected_with_user(sender, instance, **kwargs): + filters = Q(content_type=ContentType.objects.get_for_model(instance), object_pk=instance.pk) + UserObjectPermission.objects.filter(filters).delete() + GroupObjectPermission.objects.filter(filters).delete() diff --git a/portier/settings.py b/portier/settings.py index 03a9577..2d47842 100644 --- a/portier/settings.py +++ b/portier/settings.py @@ -36,6 +36,7 @@ INSTALLED_APPS = [ 'django.contrib.sessions', 'django.contrib.messages', 'django.contrib.staticfiles', + 'guardian', 'django_registration', 'bootstrap4', 'fa', @@ -55,6 +56,11 @@ MIDDLEWARE = [ 'django.middleware.clickjacking.XFrameOptionsMiddleware', ] +AUTHENTICATION_BACKENDS = ( + 'django.contrib.auth.backends.ModelBackend', + 'guardian.backends.ObjectPermissionBackend', +) + ROOT_URLCONF = 'portier.urls' TEMPLATES = [ diff --git a/requirements.txt b/requirements.txt index 7643576..5960f3e 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,6 +1,7 @@ django>=3.0 django-registration>=3.1 django-bootstrap4 +django-guardian django-fa celery>=4.4 gunicorn>=20 diff --git a/restream/admin.py b/restream/admin.py index 1fe1551..ef945a4 100644 --- a/restream/admin.py +++ b/restream/admin.py @@ -1,8 +1,9 @@ from django.contrib import admin +from guardian.admin import GuardedModelAdmin from .models import RestreamConfig -class RestreamConfigAdmin(admin.ModelAdmin): +class RestreamConfigAdmin(GuardedModelAdmin): fields = ['name', 'active', 'stream', 'target'] diff --git a/restream/forms.py b/restream/forms.py new file mode 100644 index 0000000..7497e5d --- /dev/null +++ b/restream/forms.py @@ -0,0 +1,16 @@ +from django.forms import ModelForm +from guardian.shortcuts import get_objects_for_user +from . import models + + +class RestreamConfigFilteredStreamForm(ModelForm): + class Meta: + model = models.RestreamConfig + fields = ['name', 'stream', 'target', 'active'] + + def __init__(self, *args, **kwargs): + user = kwargs.pop('user', None) + super().__init__(*args, **kwargs) + + # limit the stream selection to user-accessible streams + self.fields['stream'].queryset = get_objects_for_user(user, 'rtmp.view_stream') diff --git a/restream/models.py b/restream/models.py index 99473a9..0f7682f 100644 --- a/restream/models.py +++ b/restream/models.py @@ -1,6 +1,8 @@ from django.db import models from django.urls import reverse from django.utils.translation import gettext_lazy as _ +from django.db.models.signals import pre_delete +from portier.common import handlers from rtmp.models import Stream @@ -23,3 +25,6 @@ class RestreamConfig(models.Model): def __str__(self): return '{} to {}'.format(self.stream, self.name) + + +pre_delete.connect(handlers.remove_obj_perms_connected_with_user, sender=RestreamConfig) diff --git a/restream/templates/restream/restreamconfig_list.html b/restream/templates/restream/restreamconfig_list.html index 3c7fa4a..ae4fe8b 100644 --- a/restream/templates/restream/restreamconfig_list.html +++ b/restream/templates/restream/restreamconfig_list.html @@ -2,6 +2,8 @@ {% load i18n %} {% load bootstrap4 %} {% load font_awesome %} +{% load guardian_tags %} + {% block 'content' %}
{% trans "restreamconfig_configuration_header" %}

@@ -19,15 +21,20 @@ {% for object in object_list %} + {% get_obj_perms user for object as "obj_perms" %} + {% if "view_restreamconfig" in obj_perms %} {{ object.name }}
{% trans "details" %} + {% if "delete_restreamconfig" in obj_perms %} {% fa 'trash' %} + {% endif %}
+ {% endif %} {% endfor %} diff --git a/restream/views.py b/restream/views.py index 4e7635b..1026dc4 100644 --- a/restream/views.py +++ b/restream/views.py @@ -2,27 +2,54 @@ from django.urls import reverse_lazy from django.contrib.auth.decorators import login_required from django.utils.decorators import method_decorator from django.views.generic import ListView, DetailView, CreateView, DeleteView +from guardian.decorators import permission_required_or_403 +from guardian.shortcuts import assign_perm from . import models +from . import forms @method_decorator(login_required, name='dispatch') +@method_decorator(permission_required_or_403('restream.add_restreamconfig'), + name='dispatch') class RestreamConfigList(ListView): model = models.RestreamConfig @method_decorator(login_required, name='dispatch') +@method_decorator(permission_required_or_403('restream.view_restreamconfig', + (models.RestreamConfig, 'pk', 'pk')), + name='dispatch') class RestreamConfigDetail(DetailView): model = models.RestreamConfig @method_decorator(login_required, name='dispatch') +@method_decorator(permission_required_or_403('restream.add_restreamconfig'), + name='dispatch') class RestreamConfigCreate(CreateView): model = models.RestreamConfig - fields = ["name", "stream", "target", "active"] + form_class = forms.RestreamConfigFilteredStreamForm + + def get_form_kwargs(self): + kwargs = super().get_form_kwargs() + kwargs['user'] = self.request.user + return kwargs + + def form_valid(self, form): + valid = super().form_valid(form) + if valid: + user = self.request.user + assign_perm('view_restreamconfig', user, self.object) + assign_perm('change_restreamconfig', user, self.object) + assign_perm('delete_restreamconfig', user, self.object) + return valid @method_decorator(login_required, name='dispatch') +@method_decorator(permission_required_or_403('restream.delete_restreamconfig', + (models.RestreamConfig, 'pk', 'pk')), + name='dispatch') class RestreamConfigDelete(DeleteView): model = models.RestreamConfig success_url = reverse_lazy('restream:restreamconfig_list') diff --git a/rtmp/admin.py b/rtmp/admin.py index 025978a..cbbe9da 100644 --- a/rtmp/admin.py +++ b/rtmp/admin.py @@ -1,12 +1,13 @@ from django.contrib import admin +from guardian.admin import GuardedModelAdmin from .models import Application, Stream -class ApplicationAdmin(admin.ModelAdmin): +class ApplicationAdmin(GuardedModelAdmin): fields = ['name'] -class StreamAdmin(admin.ModelAdmin): +class StreamAdmin(GuardedModelAdmin): fields = ['application', 'stream', 'name', 'publish_counter'] diff --git a/rtmp/models.py b/rtmp/models.py index 7db5b5b..f4b9e67 100644 --- a/rtmp/models.py +++ b/rtmp/models.py @@ -1,7 +1,10 @@ +import uuid + from django.db import models from django.urls import reverse from django.utils.translation import gettext as _ -import uuid +from django.db.models.signals import pre_delete +from portier.common import handlers from . import signals @@ -66,3 +69,7 @@ class Stream(models.Model): def __str__(self): return self.name + + +pre_delete.connect(handlers.remove_obj_perms_connected_with_user, sender=Application) +pre_delete.connect(handlers.remove_obj_perms_connected_with_user, sender=Stream) diff --git a/rtmp/templates/rtmp/stream_list.html b/rtmp/templates/rtmp/stream_list.html index 445c74f..b314a62 100644 --- a/rtmp/templates/rtmp/stream_list.html +++ b/rtmp/templates/rtmp/stream_list.html @@ -2,6 +2,8 @@ {% load i18n %} {% load bootstrap4 %} {% load font_awesome %} +{% load guardian_tags %} + {% block 'content' %}
{% trans "stream_configuration_header" %}

@@ -19,15 +21,20 @@ {% for object in object_list %} + {% get_obj_perms user for object as "obj_perms" %} + {% if "view_stream" in obj_perms %} {{ object.name }}
{% trans "details" %} + {% if "delete_stream" in obj_perms %} {% fa 'trash' %} + {% endif %}
+ {% endif %} {% endfor %} diff --git a/rtmp/views.py b/rtmp/views.py index 1f26e7f..ae589a8 100644 --- a/rtmp/views.py +++ b/rtmp/views.py @@ -9,6 +9,8 @@ from django.contrib.admin.utils import NestedObjects from django.utils.decorators import method_decorator from django.views.decorators.csrf import csrf_exempt from django.views.generic import ListView, DetailView, CreateView, DeleteView +from guardian.decorators import permission_required_or_403 +from guardian.shortcuts import assign_perm from . import models @@ -48,22 +50,41 @@ def callback_srs(request): @method_decorator(login_required, name='dispatch') +@method_decorator(permission_required_or_403('rtmp.add_stream'), + name='dispatch') class StreamList(ListView): model = models.Stream @method_decorator(login_required, name='dispatch') +@method_decorator(permission_required_or_403('rtmp.view_stream', + (models.Stream, 'pk', 'pk')), + name='dispatch') class StreamDetail(DetailView): model = models.Stream @method_decorator(login_required, name='dispatch') +@method_decorator(permission_required_or_403('rtmp.add_stream'), + name='dispatch') class StreamCreate(CreateView): model = models.Stream fields = ["name", "application"] + def form_valid(self, form): + valid = super().form_valid(form) + if valid: + user = self.request.user + assign_perm('view_stream', user, self.object) + assign_perm('change_stream', user, self.object) + assign_perm('delete_stream', user, self.object) + return valid + @method_decorator(login_required, name='dispatch') +@method_decorator(permission_required_or_403('rtmp.delete_stream', + (models.Stream, 'pk', 'pk')), + name='dispatch') class StreamDelete(DeleteView): model = models.Stream success_url = reverse_lazy('rtmp:stream_list')