From fa9a7868ff836d9d91cc8ffdb36200a82b987c94 Mon Sep 17 00:00:00 2001 From: SergeantPanda Date: Fri, 10 Apr 2026 10:47:50 -0500 Subject: [PATCH] security: proxy streaming endpoints (`stream_ts`, `stream_xc`, `stream_vod`, `head_vod`, `stream_xc_movie`, `stream_xc_episode`) use `@permission_classes([AllowAny])` (access is controlled by the per-stream-type network allow-list inside the view body); the `UserAgentViewSet`, `StreamProfileViewSet`, `CoreSettingsViewSet`, and `ProxySettingsViewSet` gained `get_permissions()` methods mapping read actions to `IsStandardUser` and write actions to `IsAdmin`; and `AuthViewSet.logout` was updated to return `[Authenticated()]`. --- apps/accounts/api_views.py | 3 +-- apps/proxy/ts_proxy/views.py | 3 +++ apps/proxy/vod_proxy/views.py | 5 +++++ core/api_views.py | 26 ++++++++++++++++++++++++++ 4 files changed, 35 insertions(+), 2 deletions(-) diff --git a/apps/accounts/api_views.py b/apps/accounts/api_views.py index 6a70fd53..10e9272c 100644 --- a/apps/accounts/api_views.py +++ b/apps/accounts/api_views.py @@ -159,8 +159,7 @@ class AuthViewSet(viewsets.ViewSet): Login doesn't require auth, but logout does """ if self.action == 'logout': - from rest_framework.permissions import IsAuthenticated - return [IsAuthenticated()] + return [Authenticated()] return [] @extend_schema( diff --git a/apps/proxy/ts_proxy/views.py b/apps/proxy/ts_proxy/views.py index 5750616b..b3ea295a 100644 --- a/apps/proxy/ts_proxy/views.py +++ b/apps/proxy/ts_proxy/views.py @@ -19,6 +19,7 @@ from apps.m3u.models import M3UAccount, M3UAccountProfile from apps.accounts.models import User from core.models import UserAgent, CoreSettings, PROXY_PROFILE_NAME from rest_framework.decorators import api_view, permission_classes +from rest_framework.permissions import AllowAny from rest_framework.response import Response from apps.accounts.permissions import ( IsAdmin, @@ -46,6 +47,7 @@ logger = get_logger() @api_view(["GET"]) +@permission_classes([AllowAny]) def stream_ts(request, channel_id, user=None): if not network_access_allowed(request, "STREAMS"): return JsonResponse({"error": "Forbidden"}, status=403) @@ -554,6 +556,7 @@ def stream_ts(request, channel_id, user=None): @api_view(["GET"]) +@permission_classes([AllowAny]) def stream_xc(request, username, password, channel_id): user = get_object_or_404(User, username=username) diff --git a/apps/proxy/vod_proxy/views.py b/apps/proxy/vod_proxy/views.py index fdebdcea..e5d16e90 100644 --- a/apps/proxy/vod_proxy/views.py +++ b/apps/proxy/vod_proxy/views.py @@ -15,6 +15,7 @@ from apps.m3u.models import M3UAccountProfile from apps.proxy.vod_proxy.multi_worker_connection_manager import MultiWorkerVODConnectionManager, infer_content_type_from_url, get_vod_client_stop_key from .utils import get_client_info from rest_framework.decorators import api_view, permission_classes +from rest_framework.permissions import AllowAny from apps.accounts.models import User from apps.accounts.permissions import IsAdmin from apps.proxy.utils import check_user_stream_limits @@ -291,6 +292,7 @@ def _transform_url(original_url, m3u_profile): return original_url @api_view(["GET"]) +@permission_classes([AllowAny]) def stream_vod(request, content_type, content_id, session_id=None, profile_id=None, user=None): """ Stream VOD content (movies or series episodes) with session-based connection reuse @@ -500,6 +502,7 @@ def stream_vod(request, content_type, content_id, session_id=None, profile_id=No return HttpResponse(f"Streaming error: {str(e)}", status=500) @api_view(["HEAD"]) +@permission_classes([AllowAny]) def head_vod(request, content_type, content_id, session_id=None, profile_id=None): """ Handle HEAD requests for FUSE filesystem integration @@ -987,6 +990,7 @@ def stop_vod_client(request): return JsonResponse({'error': str(e)}, status=500) @api_view(["GET"]) +@permission_classes([AllowAny]) def stream_xc_movie(request, username, password, stream_id, extension): from apps.vod.models import M3UMovieRelation @@ -1017,6 +1021,7 @@ def stream_xc_movie(request, username, password, stream_id, extension): return stream_vod(request._request, 'movie', movie_relation.movie.uuid, session_id, profile_id, user) @api_view(["GET"]) +@permission_classes([AllowAny]) def stream_xc_episode(request, username, password, stream_id, extension): from apps.vod.models import M3UEpisodeRelation diff --git a/core/api_views.py b/core/api_views.py index d01d8619..596da877 100644 --- a/core/api_views.py +++ b/core/api_views.py @@ -35,6 +35,9 @@ import os from core.tasks import rehash_streams from apps.accounts.permissions import ( Authenticated, + IsAdmin, + IsStandardUser, + permission_classes_by_action, ) from dispatcharr.utils import get_client_ip @@ -50,6 +53,12 @@ class UserAgentViewSet(viewsets.ModelViewSet): queryset = UserAgent.objects.all() serializer_class = UserAgentSerializer + def get_permissions(self): + try: + return [perm() for perm in permission_classes_by_action[self.action]] + except KeyError: + return [Authenticated()] + class StreamProfileViewSet(viewsets.ModelViewSet): """ @@ -59,6 +68,12 @@ class StreamProfileViewSet(viewsets.ModelViewSet): queryset = StreamProfile.objects.all() serializer_class = StreamProfileSerializer + def get_permissions(self): + try: + return [perm() for perm in permission_classes_by_action[self.action]] + except KeyError: + return [Authenticated()] + class CoreSettingsViewSet(viewsets.ModelViewSet): """ @@ -69,6 +84,12 @@ class CoreSettingsViewSet(viewsets.ModelViewSet): queryset = CoreSettings.objects.all() serializer_class = CoreSettingsSerializer + def get_permissions(self): + try: + return [perm() for perm in permission_classes_by_action[self.action]] + except KeyError: + return [Authenticated()] + def update(self, request, *args, **kwargs): instance = self.get_object() old_value = instance.value @@ -171,6 +192,11 @@ class ProxySettingsViewSet(viewsets.ViewSet): """ serializer_class = ProxySettingsSerializer + def get_permissions(self): + if self.action in ('list', 'retrieve'): + return [IsStandardUser()] + return [IsAdmin()] + def _get_or_create_settings(self): """Get or create the proxy settings CoreSettings entry""" try: