From 269985927e14f4429960d2a7bd42f30c7d64489e Mon Sep 17 00:00:00 2001 From: magicfelix <felix@felix-zauberer.de> Date: Sat, 17 Dec 2022 14:00:26 +0100 Subject: [PATCH 1/3] Validate permissions in Queries --- aleksis/core/schema/__init__.py | 28 +++++++++++++++++--------- aleksis/core/schema/base.py | 14 +++++++++++++ aleksis/core/schema/celery_progress.py | 8 ++++++-- aleksis/core/schema/group.py | 9 ++++++--- aleksis/core/schema/notification.py | 8 ++++++-- aleksis/core/schema/person.py | 8 ++++++-- 6 files changed, 57 insertions(+), 18 deletions(-) create mode 100644 aleksis/core/schema/base.py diff --git a/aleksis/core/schema/__init__.py b/aleksis/core/schema/__init__.py index 1f5a2fa8e..69a3bbf82 100644 --- a/aleksis/core/schema/__init__.py +++ b/aleksis/core/schema/__init__.py @@ -8,7 +8,11 @@ from haystack.utils.loading import UnifiedIndex from ..models import Notification, Person, TaskUserAssignment from ..util.apps import AppConfig from ..util.core_helpers import get_allowed_object_ids, get_app_module, get_app_packages, has_person -from .celery_progress import CeleryProgressFetchedMutation, CeleryProgressType +from .celery_progress import ( + CeleryProgressFetchedMutation, + CeleryProgressMetaType, + CeleryProgressType, +) from .group import GroupType # noqa from .installed_apps import AppType from .notification import MarkNotificationReadMutation, NotificationType @@ -37,15 +41,18 @@ class Query(graphene.ObjectType): ) def resolve_notifications(root, info, **kwargs): - # FIXME do permission stuff - return Notification.objects.all() + return NotificationType.get_queryset( + Notification.objects.all().order_by("-created"), + info, + ) def resolve_persons(root, info, **kwargs): - # FIXME do permission stuff - return Person.objects.all() + return PersonType.get_queryset(Person.objects.all(), info).all() def resolve_person_by_id(root, info, id): # noqa - return Person.objects.get(pk=id) + return PersonType.get_queryset( + Person.objects.filter(pk=id), info, "core.view_person_rule" + ).first() def resolve_who_am_i(root, info, **kwargs): if has_person(info.context.user): @@ -60,10 +67,13 @@ class Query(graphene.ObjectType): return [app for app in apps.get_app_configs() if isinstance(app, AppConfig)] def resolve_celery_progress_by_task_id(root, info, task_id, **kwargs): - task = TaskUserAssignment.objects.get(task_result__task_id=task_id) + task = CeleryProgressMetaType.get_queryset( + TaskUserAssignment.objects.filter(task_result__task_id=task_id), + info, + ).first() - if not info.context.user.has_perm("core.view_progress_rule", task): - return None + if not task: + raise PermissionDenied() progress = task.get_progress_with_meta() return progress diff --git a/aleksis/core/schema/base.py b/aleksis/core/schema/base.py new file mode 100644 index 000000000..363c6b8ac --- /dev/null +++ b/aleksis/core/schema/base.py @@ -0,0 +1,14 @@ +from graphene_django import DjangoObjectType + +from ..util.core_helpers import queryset_rules_filter + + +class RulesObjectType(DjangoObjectType): + class Meta: + abstract = True + + @classmethod + def get_queryset(cls, queryset, info, perm): + q = super().get_queryset(queryset, info) + + return queryset_rules_filter(info.context, q, perm) diff --git a/aleksis/core/schema/celery_progress.py b/aleksis/core/schema/celery_progress.py index 941d18f1d..c6fe93dd0 100644 --- a/aleksis/core/schema/celery_progress.py +++ b/aleksis/core/schema/celery_progress.py @@ -2,9 +2,9 @@ from django.contrib.messages.constants import DEFAULT_TAGS import graphene from graphene import ObjectType -from graphene_django import DjangoObjectType from ..models import TaskUserAssignment +from .base import RulesObjectType class CeleryProgressMessage(ObjectType): @@ -28,7 +28,7 @@ class CeleryProgressAdditionalButtonType(ObjectType): icon = graphene.String() -class CeleryProgressMetaType(DjangoObjectType): +class CeleryProgressMetaType(RulesObjectType): additional_button = graphene.Field(CeleryProgressAdditionalButtonType, required=False) task_id = graphene.String(required=True) @@ -47,6 +47,10 @@ class CeleryProgressMetaType(DjangoObjectType): "additional_button", ) + @classmethod + def get_queryset(cls, queryset, info, perm="core.view_progress_rule"): + return super().get_queryset(queryset, info, perm) + def resolve_additional_button(root, info, **kwargs): if not root.additional_button_title or not root.additional_button_url: return None diff --git a/aleksis/core/schema/group.py b/aleksis/core/schema/group.py index 327daff3d..70f00bd1d 100644 --- a/aleksis/core/schema/group.py +++ b/aleksis/core/schema/group.py @@ -1,8 +1,11 @@ -from graphene_django import DjangoObjectType - from ..models import Group +from .base import RulesObjectType -class GroupType(DjangoObjectType): +class GroupType(RulesObjectType): class Meta: model = Group + + @classmethod + def get_queryset(cls, queryset, info, perm="core.view_groups_rule"): + return super().get_queryset(queryset, info, perm) diff --git a/aleksis/core/schema/notification.py b/aleksis/core/schema/notification.py index 114f92b32..105ed1366 100644 --- a/aleksis/core/schema/notification.py +++ b/aleksis/core/schema/notification.py @@ -1,13 +1,17 @@ import graphene -from graphene_django import DjangoObjectType from ..models import Notification +from .base import RulesObjectType -class NotificationType(DjangoObjectType): +class NotificationType(RulesObjectType): class Meta: model = Notification + @classmethod + def get_queryset(cls, queryset, info, perm="core.view_notifications_rule"): + return super().get_queryset(queryset, info, perm) + class MarkNotificationReadMutation(graphene.Mutation): class Arguments: diff --git a/aleksis/core/schema/person.py b/aleksis/core/schema/person.py index a48447273..c7b4fd6ff 100644 --- a/aleksis/core/schema/person.py +++ b/aleksis/core/schema/person.py @@ -1,14 +1,14 @@ from django.utils import timezone import graphene -from graphene_django import DjangoObjectType from graphene_django.forms.mutation import DjangoModelFormMutation from ..forms import PersonForm from ..models import Person +from .base import RulesObjectType -class PersonType(DjangoObjectType): +class PersonType(RulesObjectType): class Meta: model = Person @@ -21,6 +21,10 @@ class PersonType(DjangoObjectType): def resolve_notifications(root: Person, info, **kwargs): return root.notifications.filter(send_at__lte=timezone.now()).order_by("read", "-created") + @classmethod + def get_queryset(cls, queryset, info, perm="core.view_persons_rule"): + return super().get_queryset(queryset, info, perm) + class PersonMutation(DjangoModelFormMutation): person = graphene.Field(PersonType) -- GitLab From 64b57e89516df031144d632ee189abb630b35ed4 Mon Sep 17 00:00:00 2001 From: magicfelix <felix@felix-zauberer.de> Date: Sat, 17 Dec 2022 17:17:30 +0100 Subject: [PATCH 2/3] Validate permissions in Mutations --- aleksis/core/schema/notification.py | 6 ++++-- aleksis/core/schema/person.py | 10 ++++++++++ 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/aleksis/core/schema/notification.py b/aleksis/core/schema/notification.py index 105ed1366..fe1fc19a4 100644 --- a/aleksis/core/schema/notification.py +++ b/aleksis/core/schema/notification.py @@ -22,8 +22,10 @@ class MarkNotificationReadMutation(graphene.Mutation): @classmethod def mutate(cls, root, info, id): # noqa notification = Notification.objects.get(pk=id) - # FIXME permissions + + if not info.context.user.has_perm("core.mark_notification_as_read_rule", notification): + raise PermissionDenied() notification.read = True notification.save() - return notification + return MarkNotificationReadMutation(notification=notification) diff --git a/aleksis/core/schema/person.py b/aleksis/core/schema/person.py index c7b4fd6ff..d329235ad 100644 --- a/aleksis/core/schema/person.py +++ b/aleksis/core/schema/person.py @@ -31,3 +31,13 @@ class PersonMutation(DjangoModelFormMutation): class Meta: form_class = PersonForm + + @classmethod + def perform_mutate(cls, form, info): + if form.initial: + if not info.context.user.has_perm("core.create_person.rule"): + raise PermissionDenied() + else: + if not info.context.user.has_perm("core.edit_person.rule", form.instance): + raise PermissionDenied() + return super().perform_mutate(form, info) -- GitLab From 1408500e5cd707a0e2d71a0a23289e226b9fe8a5 Mon Sep 17 00:00:00 2001 From: magicfelix <felix@felix-zauberer.de> Date: Sat, 17 Dec 2022 17:17:51 +0100 Subject: [PATCH 3/3] Unprotect GraphQL endpoint --- aleksis/core/urls.py | 3 ++- aleksis/core/views.py | 5 ----- 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/aleksis/core/urls.py b/aleksis/core/urls.py index e2e9d0c15..fd36dba48 100644 --- a/aleksis/core/urls.py +++ b/aleksis/core/urls.py @@ -8,6 +8,7 @@ from django.views.i18n import JavaScriptCatalog import calendarweek.django from ckeditor_uploader import views as ckeditor_uploader_views +from graphene_django.views import GraphQLView from health_check.urls import urlpatterns as health_urls from oauth2_provider.views import ConnectDiscoveryInfoView from rules.contrib.views import permission_required @@ -140,7 +141,7 @@ urlpatterns = [ name="oauth2_provider:authorize", ), path("oauth/", include("oauth2_provider.urls", namespace="oauth2_provider")), - path("graphql/", csrf_exempt(views.PrivateGraphQLView.as_view(graphiql=True)), name="graphql"), + path("graphql/", csrf_exempt(GraphQLView.as_view(graphiql=True)), name="graphql"), path("__i18n__/", include("django.conf.urls.i18n")), path( "ckeditor/upload/", diff --git a/aleksis/core/views.py b/aleksis/core/views.py index 886ae4e3b..5a1739a47 100644 --- a/aleksis/core/views.py +++ b/aleksis/core/views.py @@ -44,7 +44,6 @@ from django_celery_results.models import TaskResult from django_filters.views import FilterView from django_tables2 import RequestConfig, SingleTableMixin, SingleTableView from dynamic_preferences.forms import preference_form_builder -from graphene_django.views import GraphQLView from guardian.shortcuts import GroupObjectPermission, UserObjectPermission, get_objects_for_user from haystack.generic_views import SearchView from haystack.inputs import AutoQuery @@ -1585,7 +1584,3 @@ class ICalFeedCreateView(PermissionRequiredMixin, AdvancedCreateView): obj.person = self.request.user.person obj.save() return super().form_valid(form) - - -class PrivateGraphQLView(LoginRequiredMixin, GraphQLView): - """GraphQL view that requires a valid user session.""" -- GitLab