Merge pull request #3949 from modoboa/fix/missing-permissions

Fixed several security issues.
This commit is contained in:
Antoine Nguyen
2026-02-27 17:43:58 +01:00
committed by GitHub
7 changed files with 67 additions and 32 deletions
+1 -1
View File
@@ -440,5 +440,5 @@ class AlarmViewSet(
"""Delete multiple alarms at the same time."""
serializer = self.get_serializer(data=request.data)
serializer.is_valid(raise_exception=True)
models.Alarm.objects.filter(pk__in=serializer.validated_data["ids"]).delete()
self.get_queryset().filter(pk__in=serializer.validated_data["ids"]).delete()
return response.Response(status=204)
+16
View File
@@ -5,6 +5,7 @@ from django.utils.translation import gettext as _
from rest_framework import serializers
from modoboa.admin import models as admin_models
from modoboa.autoreply import models
@@ -35,6 +36,21 @@ class ARMessageSerializer(serializers.ModelSerializer):
model = models.ARmessage
fields = "__all__"
def validate_mbox(self, value):
user = self.context["request"].user
role = user.role
if role == "SimpleUsers" and user.mailbox != value:
raise serializers.ValidationError(
_("You don't have ownership on this mailbox")
)
elif role in ["DomainAdmins", "Resellers"]:
mailboxes = admin_models.Mailbox.objects.get_for_admin(user)
if value not in mailboxes:
raise serializers.ValidationError(
_("You don't have ownership on this mailbox")
)
return value
def validate(self, data):
"""Check dates."""
if not data.get("fromdate"):
+34 -3
View File
@@ -3,7 +3,7 @@ from django.utils import timezone
from dateutil.relativedelta import relativedelta
from modoboa.admin import factories as admin_factories
from modoboa.admin import factories as admin_factories, models as admin_models
from modoboa.core.models import User
from modoboa.lib.tests import ModoAPITestCase
from modoboa.sievefilters.api.v2.tests import PatcherMixin
@@ -21,6 +21,14 @@ class ARMessageViewSetTestCase(PatcherMixin, ModoAPITestCase):
cls.account2 = User.objects.get(username="user@test2.com")
cls.arm = factories.ARmessageFactory(mbox=cls.account.mailbox)
cls.arm2 = factories.ARmessageFactory(mbox=cls.account2.mailbox)
cls.account3 = admin_factories.UserFactory.create(
username="user3@test.com", groups=("SimpleUsers",)
)
admin_factories.MailboxFactory.create(
address="user3",
domain=admin_models.Domain.objects.get(name="test.com"),
user=cls.account3,
)
def test_update_global_settings(self):
data = {
@@ -61,7 +69,6 @@ class ARMessageViewSetTestCase(PatcherMixin, ModoAPITestCase):
def test_retrieve_armessage_domadmin(self):
admin = User.objects.get(username="admin@test.com")
self.client.logout()
self.client.force_authenticate(admin)
url = reverse("api:armessage-list")
response = self.client.get(url)
@@ -72,8 +79,32 @@ class ARMessageViewSetTestCase(PatcherMixin, ModoAPITestCase):
response = self.client.get(url)
self.assertEqual(response.status_code, 404)
def test_create_armessage_constraints(self):
admin = User.objects.get(username="admin@test.com")
self.client.force_authenticate(admin)
mbox = admin_models.Mailbox.objects.get(user__email="admin@test2.com")
url = reverse("api:armessage-list")
data = {
"mbox": mbox.pk,
"subject": "Je suis absent",
"content": "Je reviens bientôt",
"enabled": True,
}
response = self.client.post(url, data, format="json")
self.assertEqual(response.status_code, 400)
self.assertEqual(
response.json()["mbox"][0], "You don't have ownership on this mailbox"
)
# As Simpluser now
self.client.force_authenticate(self.account3)
response = self.client.post(url, data, format="json")
self.assertEqual(response.status_code, 400)
self.assertEqual(
response.json()["mbox"][0], "You don't have ownership on this mailbox"
)
def test_retrieve_armessage_simpleuser(self):
self.client.logout()
self.client.force_authenticate(self.account)
url = reverse("api:armessage-list")
response = self.client.get(url)
+6 -22
View File
@@ -389,9 +389,6 @@ class EventViewSetTestCase(TestDataMixin, ModoAPITestCase):
def test_get_user_events(self):
"""Test event(s) retrieval."""
data = {"username": "user@test.com", "password": "toto"}
self.client.post(reverse("core:login"), data)
# FIXME: drf nested routers does not handle reverse() properly
url = f"/api/v2/user-calendars/{self.calendar.pk}/events/{1234}/"
response = self.client.get(url)
@@ -404,11 +401,14 @@ class EventViewSetTestCase(TestDataMixin, ModoAPITestCase):
self.assertEqual(response.status_code, 200)
self.assertEqual(len(response.json()), 2)
def test_get_user_events_wrong_calendar(self):
url = f"/api/v2/user-calendars/{self.calendar2.pk}/events/"
url = "{}?start={}&end={}".format(url, "20060712T182145Z", "20070712T182145Z")
response = self.client.get(url)
self.assertEqual(response.status_code, 404)
def test_get_shared_events(self):
"""Test event(s) retrieval."""
data = {"username": "user@test.com", "password": "toto"}
self.client.post(reverse("core:login"), data)
# FIXME: drf nested routers does not handle reverse() properly
url = f"/api/v2/shared-calendars/{self.scalendar.pk}/events/{1234}/"
response = self.client.get(url)
@@ -423,9 +423,6 @@ class EventViewSetTestCase(TestDataMixin, ModoAPITestCase):
def test_create_event(self):
"""Test event creation."""
data = {"username": "user@test.com", "password": "toto"}
self.client.post(reverse("core:login"), data)
url = f"/api/v2/user-calendars/{self.calendar.pk}/events/"
data = {
"title": "Test event",
@@ -456,9 +453,6 @@ class EventViewSetTestCase(TestDataMixin, ModoAPITestCase):
def test_update_event(self):
"""Test event update."""
data = {"username": "user@test.com", "password": "toto"}
self.client.post(reverse("core:login"), data)
url = f"/api/v2/user-calendars/{self.calendar.pk}/events/1234/"
data = {
"title": "Test event",
@@ -479,9 +473,6 @@ class EventViewSetTestCase(TestDataMixin, ModoAPITestCase):
def test_patch_event(self):
"""Test event partial update."""
data = {"username": "user@test.com", "password": "toto"}
self.client.post(reverse("core:login"), data)
url = f"/api/v2/user-calendars/{self.calendar.pk}/events/1234/"
data = {
"start_date": "2018-03-27",
@@ -493,8 +484,6 @@ class EventViewSetTestCase(TestDataMixin, ModoAPITestCase):
def test_move_event_between_cals(self):
"""Move an event."""
data = {"username": "user@test.com", "password": "toto"}
self.client.post(reverse("core:login"), data)
data = {
"title": "Test event",
"start_date": "2018-03-27",
@@ -511,17 +500,12 @@ class EventViewSetTestCase(TestDataMixin, ModoAPITestCase):
def test_delete_event(self):
"""Test event deletion."""
data = {"username": "user@test.com", "password": "toto"}
self.client.post(reverse("core:login"), data)
url = f"/api/v2/shared-calendars/{self.scalendar.pk}/events/1234/"
response = self.client.delete(url)
self.assertEqual(response.status_code, 200)
def test_import_from_file(self):
"""Check import feature."""
data = {"username": "user@test.com", "password": "toto"}
self.client.post(reverse("core:login"), data)
url = reverse("api:user-event-import-from-file", args=[self.calendar.pk])
path = os.path.join(
os.path.abspath(os.path.dirname(__file__)), "test_data/events.ics"
+8 -4
View File
@@ -21,9 +21,7 @@ from . import serializers
def parse_date_from_iso(value):
"""Return a tz aware datetime parsed from an ISO date."""
result = dateutil.parser.parse(value)
print(result)
return result
return dateutil.parser.parse(value)
class CheckTokenMixin:
@@ -91,6 +89,7 @@ class BaseEventViewSet(viewsets.ViewSet):
"""Event viewset."""
lookup_value_regex = r"[0-9a-zA-Z\-\.@]+"
permission_classes = (permissions.IsAuthenticated,)
def get_serializer(self, data=None, **kwargs):
args = []
@@ -212,7 +211,12 @@ class UserEventViewSet(BaseEventViewSet):
def get_calendar(self, pk):
"""Return UserCalendar instance."""
return models.UserCalendar.objects.get(pk=pk)
calendar = models.UserCalendar.objects.filter(
mailbox__user=self.request.user, pk=pk
).first()
if not calendar:
raise http.Http404
return calendar
class SharedEventViewSet(BaseEventViewSet):
+1 -1
View File
@@ -55,7 +55,7 @@ class MaillogViewSet(GetThrottleViewsetMixin, viewsets.ReadOnlyModelViewSet):
ordering = ["-date"]
ordering_fields = "__all__"
pagination_class = pagination.CustomPageNumberPagination
permissions = (permissions.IsAuthenticated,)
permission_classes = (permissions.IsAuthenticated,)
search_fields = ["queue_id", "sender", "rcpt", "original_rcpt", "status"]
serializer_class = serializers.MaillogSerializer
+1 -1
View File
@@ -12,7 +12,7 @@ from ... import backends
class TransportViewSet(GetThrottleViewsetMixin, viewsets.ViewSet):
"""Viewset for Transport."""
permissions = (permissions.IsAuthenticated,)
permission_classes = (permissions.IsAuthenticated,)
@extend_schema(responses={200: serializers.TransportBackendSerializer})
def list(self, request, *args, **kwargs):