check and warn before saving a network access setting that could block current client access

This commit is contained in:
dekzter 2025-06-10 08:46:36 -04:00
parent 789d29c97a
commit 82f35d2aef
5 changed files with 148 additions and 29 deletions

View file

@ -11,6 +11,10 @@
**/.toolstarget
**/.vs
**/.vscode
**/.history
**/media
**/models
**/static
**/*.*proj.user
**/*.dbmdl
**/*.jfm
@ -26,3 +30,4 @@
**/values.dev.yaml
LICENSE
README.md
data/

View file

@ -3,33 +3,45 @@ from rest_framework.response import Response
from .models import M3UAccount, M3UFilter, ServerGroup, M3UAccountProfile
from core.models import UserAgent
from apps.channels.models import ChannelGroup, ChannelGroupM3UAccount
from apps.channels.serializers import ChannelGroupM3UAccountSerializer, ChannelGroupSerializer
from apps.channels.serializers import (
ChannelGroupM3UAccountSerializer,
ChannelGroupSerializer,
)
import logging
logger = logging.getLogger(__name__)
class M3UFilterSerializer(serializers.ModelSerializer):
"""Serializer for M3U Filters"""
channel_groups = ChannelGroupM3UAccountSerializer(source='m3u_account', many=True)
channel_groups = ChannelGroupM3UAccountSerializer(source="m3u_account", many=True)
class Meta:
model = M3UFilter
fields = ['id', 'filter_type', 'regex_pattern', 'exclude', 'channel_groups']
fields = ["id", "filter_type", "regex_pattern", "exclude", "channel_groups"]
from rest_framework import serializers
from .models import M3UAccountProfile
class M3UAccountProfileSerializer(serializers.ModelSerializer):
class Meta:
model = M3UAccountProfile
fields = ['id', 'name', 'max_streams', 'is_active', 'is_default', 'current_viewers', 'search_pattern', 'replace_pattern']
read_only_fields = ['id']
fields = [
"id",
"name",
"max_streams",
"is_active",
"is_default",
"current_viewers",
"search_pattern",
"replace_pattern",
]
read_only_fields = ["id"]
def create(self, validated_data):
m3u_account = self.context.get('m3u_account')
m3u_account = self.context.get("m3u_account")
# Use the m3u_account when creating the profile
validated_data['m3u_account_id'] = m3u_account.id
validated_data["m3u_account_id"] = m3u_account.id
return super().create(validated_data)
@ -43,12 +55,14 @@ class M3UAccountProfileSerializer(serializers.ModelSerializer):
if instance.is_default:
return Response(
{"error": "Default profiles cannot be deleted."},
status=status.HTTP_400_BAD_REQUEST
status=status.HTTP_400_BAD_REQUEST,
)
return super().destroy(request, *args, **kwargs)
class M3UAccountSerializer(serializers.ModelSerializer):
"""Serializer for M3U Account"""
filters = M3UFilterSerializer(many=True, read_only=True)
# Include user_agent as a mandatory field using its primary key.
user_agent = serializers.PrimaryKeyRelatedField(
@ -57,28 +71,48 @@ class M3UAccountSerializer(serializers.ModelSerializer):
allow_null=True,
)
profiles = M3UAccountProfileSerializer(many=True, read_only=True)
read_only_fields = ['locked', 'created_at', 'updated_at']
read_only_fields = ["locked", "created_at", "updated_at"]
# channel_groups = serializers.SerializerMethodField()
channel_groups = ChannelGroupM3UAccountSerializer(source='channel_group', many=True, required=False)
channel_groups = ChannelGroupM3UAccountSerializer(
source="channel_group", many=True, required=False
)
class Meta:
model = M3UAccount
fields = [
'id', 'name', 'server_url', 'file_path', 'server_group',
'max_streams', 'is_active', 'created_at', 'updated_at', 'filters', 'user_agent', 'profiles', 'locked',
'channel_groups', 'refresh_interval', 'custom_properties', 'account_type', 'username', 'password', 'stale_stream_days',
'status', 'last_message',
"id",
"name",
"server_url",
"file_path",
"server_group",
"max_streams",
"is_active",
"created_at",
"updated_at",
"filters",
"user_agent",
"profiles",
"locked",
"channel_groups",
"refresh_interval",
"custom_properties",
"account_type",
"username",
"password",
"stale_stream_days",
"status",
"last_message",
]
extra_kwargs = {
'password': {
'required': False,
'allow_blank': True,
"password": {
"required": False,
"allow_blank": True,
},
}
def update(self, instance, validated_data):
# Pop out channel group memberships so we can handle them manually
channel_group_data = validated_data.pop('channel_group', [])
channel_group_data = validated_data.pop("channel_group", [])
# First, update the M3UAccount itself
for attr, value in validated_data.items():
@ -88,13 +122,12 @@ class M3UAccountSerializer(serializers.ModelSerializer):
# Prepare a list of memberships to update
memberships_to_update = []
for group_data in channel_group_data:
group = group_data.get('channel_group')
enabled = group_data.get('enabled')
group = group_data.get("channel_group")
enabled = group_data.get("enabled")
try:
membership = ChannelGroupM3UAccount.objects.get(
m3u_account=instance,
channel_group=group
m3u_account=instance, channel_group=group
)
membership.enabled = enabled
memberships_to_update.append(membership)
@ -103,13 +136,16 @@ class M3UAccountSerializer(serializers.ModelSerializer):
# Perform the bulk update
if memberships_to_update:
ChannelGroupM3UAccount.objects.bulk_update(memberships_to_update, ['enabled'])
ChannelGroupM3UAccount.objects.bulk_update(
memberships_to_update, ["enabled"]
)
return instance
class ServerGroupSerializer(serializers.ModelSerializer):
"""Serializer for Server Group"""
class Meta:
model = ServerGroup
fields = ['id', 'name']
fields = ["id", "name"]

View file

@ -1,5 +1,7 @@
# core/api_views.py
import json
import ipaddress
from rest_framework import viewsets, status
from rest_framework.response import Response
from django.shortcuts import get_object_or_404
@ -9,7 +11,7 @@ from .serializers import (
StreamProfileSerializer,
CoreSettingsSerializer,
)
from rest_framework.decorators import api_view, permission_classes
from rest_framework.decorators import api_view, permission_classes, action
from drf_yasg.utils import swagger_auto_schema
import socket
import requests
@ -18,6 +20,7 @@ from core.tasks import rehash_streams
from apps.accounts.permissions import (
Authenticated,
)
from dispatcharr.utils import get_client_ip
class UserAgentViewSet(viewsets.ModelViewSet):
@ -56,6 +59,23 @@ class CoreSettingsViewSet(viewsets.ModelViewSet):
return response
@action(detail=False, methods=["post"], url_path="check")
def check(self, request, *args, **kwargs):
data = request.data
client_ip = ipaddress.ip_address(get_client_ip(request))
in_network = []
key = data.get("key")
value = json.loads(data.get("value", "{}"))
for key, val in value.items():
cidrs = val.split(",")
for cidr in cidrs:
network = ipaddress.ip_network(cidr)
if client_ip not in network:
in_network.append(cidr)
return Response(in_network, status=status.HTTP_200_OK)
@swagger_auto_schema(
method="get",

View file

@ -1085,6 +1085,21 @@ export default class API {
}
}
static async checkSetting(values) {
const { id, ...payload } = values;
try {
const response = await request(`${host}/api/core/settings/check/`, {
method: 'POST',
body: payload,
});
return response;
} catch (e) {
errorNotification('Failed to update settings', e);
}
}
static async updateSetting(values) {
const { id, ...payload } = values;

View file

@ -24,6 +24,7 @@ import StreamProfilesTable from '../components/tables/StreamProfilesTable';
import useLocalStorage from '../hooks/useLocalStorage';
import useAuthStore from '../store/auth';
import { USER_LEVELS, NETWORK_ACCESS_OPTIONS } from '../constants';
import ConfirmationDialog from '../components/ConfirmationDialog';
const SettingsPage = () => {
const settings = useSettingsStore((s) => s.settings);
@ -33,6 +34,10 @@ const SettingsPage = () => {
const [accordianValue, setAccordianValue] = useState(null);
const [networkAccessSaved, setNetworkAccessSaved] = useState(false);
const [networkAccessConfirmOpen, setNetworkAccessConfirmOpen] =
useState(false);
const [netNetworkAccessConfirmCIDRs, setNetNetworkAccessConfirmCIDRs] =
useState([]);
// UI / local storage settings
const [tableSize, setTableSize] = useLocalStorage('table-size', 'default');
@ -315,7 +320,6 @@ const SettingsPage = () => {
useEffect(() => {
if (settings) {
console.log(settings);
const formValues = Object.entries(settings).reduce(
(acc, [key, value]) => {
// Modify each value based on its own properties
@ -378,7 +382,21 @@ const SettingsPage = () => {
};
const onNetworkAccessSubmit = async () => {
let result = null;
setNetworkAccessSaved(false);
const check = await API.checkSetting({
...settings['network-access'],
value: JSON.stringify(networkAccessForm.getValues()),
});
if (check.length == 0) {
return saveNetworkAccess();
}
setNetNetworkAccessConfirmCIDRs(check);
setNetworkAccessConfirmOpen(true);
};
const saveNetworkAccess = async () => {
setNetworkAccessSaved(false);
try {
await API.updateSetting({
@ -386,6 +404,7 @@ const SettingsPage = () => {
value: JSON.stringify(networkAccessForm.getValues()),
});
setNetworkAccessSaved(true);
setNetworkAccessConfirmOpen(false);
} catch (e) {
const errors = {};
for (const key in e.body.value) {
@ -644,6 +663,30 @@ const SettingsPage = () => {
)}
</Accordion>
</Box>
<ConfirmationDialog
opened={networkAccessConfirmOpen}
onClose={() => setNetworkAccessConfirmOpen(false)}
onConfirm={saveNetworkAccess}
title={`Confirm Network Access Blocks`}
message={
<>
<Text>
Your client is included in the following CIDRs and could block
access Are you sure you want to proceed?
</Text>
<ul>
{netNetworkAccessConfirmCIDRs.map((cidr) => (
<li>{cidr}</li>
))}
</ul>
</>
}
confirmLabel="Save"
cancelLabel="Cancel"
size="md"
/>
</Center>
);
};