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()].

This commit is contained in:
SergeantPanda 2026-04-10 10:47:50 -05:00
parent 66ee67da30
commit fa9a7868ff
4 changed files with 35 additions and 2 deletions

View file

@ -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(

View file

@ -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)

View file

@ -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

View file

@ -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: