Skip to content
Snippets Groups Projects
Commit e04e1db6 authored by Jonathan Weth's avatar Jonathan Weth :keyboard:
Browse files

Merge branch '1205-unprotected-batchpatchmutations' into 'master'

Resolve "Unprotected BatchPatchMutations"

Closes #1205

See merge request !1718
parents 7fe8b8b1 65c332a7
No related branches found
No related tags found
1 merge request!1718Resolve "Unprotected BatchPatchMutations"
Pipeline #196065 failed
from collections.abc import Iterable
from typing import Optional from typing import Optional
import django.apps import django.apps
from django.core.checks import Tags, Warning, register # noqa from django.core.checks import Error, Tags, Warning, register # noqa
from .mixins import ExtensibleModel, GlobalPermissionModel, PureDjangoModel from .mixins import ExtensibleModel, GlobalPermissionModel, PureDjangoModel
from .schema.base import BaseBatchCreateMutation, BaseBatchDeleteMutation, BaseBatchPatchMutation
from .util.apps import AppConfig from .util.apps import AppConfig
...@@ -71,3 +73,28 @@ def check_app_models_base_class( ...@@ -71,3 +73,28 @@ def check_app_models_base_class(
) )
return results return results
@register(Tags.security)
def check_all_mutations_with_permissions(
app_configs: Optional[django.apps.registry.Apps] = None, **kwargs
) -> list:
results = []
for base_class in [BaseBatchCreateMutation, BaseBatchPatchMutation, BaseBatchDeleteMutation]:
for subclass in base_class.__subclasses__():
if (
not isinstance(subclass._meta.permissions, Iterable)
or not subclass._meta.permissions
):
results.append(
Error(
f"Mutation {subclass.__name__} doesn't set required permission",
hint=(
"Ensure that the mutation is protected by setting the "
"permissions attribute in the mutation's Meta class."
),
obj=subclass,
id="aleksis.core.E001",
)
)
return results
...@@ -4,6 +4,7 @@ from django.db.models import Q ...@@ -4,6 +4,7 @@ from django.db.models import Q
import graphene import graphene
import graphene_django_optimizer import graphene_django_optimizer
from graphene.types.resolver import dict_or_attr_resolver, set_default_resolver
from guardian.shortcuts import get_objects_for_user from guardian.shortcuts import get_objects_for_user
from haystack.inputs import AutoQuery from haystack.inputs import AutoQuery
from haystack.query import SearchQuerySet from haystack.query import SearchQuerySet
...@@ -81,6 +82,17 @@ from .two_factor import TwoFactorType ...@@ -81,6 +82,17 @@ from .two_factor import TwoFactorType
from .user import UserType from .user import UserType
def custom_default_resolver(attname, default_value, root, info, **args):
"""Custom default resolver to ensure resolvers are set for all queries."""
if info.parent_type.name == "GlobalQuery":
raise NotImplementedError(f"No own resolver defined for {attname}")
return dict_or_attr_resolver(attname, default_value, root, info, **args)
set_default_resolver(custom_default_resolver)
class Query(graphene.ObjectType): class Query(graphene.ObjectType):
ping = graphene.String(payload=graphene.String()) ping = graphene.String(payload=graphene.String())
......
...@@ -111,7 +111,8 @@ class PermissionBatchCreateMixin: ...@@ -111,7 +111,8 @@ class PermissionBatchCreateMixin:
@classmethod @classmethod
def after_create_obj(cls, root, info, data, obj, input): # noqa def after_create_obj(cls, root, info, data, obj, input): # noqa
if isinstance(cls._meta.permissions, Iterable) and not info.context.user.has_perms( super().after_create_obj(root, info, data, obj, input)
if not isinstance(cls._meta.permissions, Iterable) or not info.context.user.has_perms(
cls._meta.permissions, obj cls._meta.permissions, obj
): ):
raise PermissionDenied() raise PermissionDenied()
...@@ -129,7 +130,8 @@ class PermissionBatchPatchMixin: ...@@ -129,7 +130,8 @@ class PermissionBatchPatchMixin:
@classmethod @classmethod
def after_update_obj(cls, root, info, input, obj, full_input): # noqa def after_update_obj(cls, root, info, input, obj, full_input): # noqa
if isinstance(cls._meta.permissions, Iterable) and not info.context.user.has_perms( super().after_update_obj(root, info, input, obj, full_input)
if not isinstance(cls._meta.permissions, Iterable) or not info.context.user.has_perms(
cls._meta.permissions, obj cls._meta.permissions, obj
): ):
raise PermissionDenied() raise PermissionDenied()
...@@ -147,10 +149,12 @@ class PermissionBatchDeleteMixin: ...@@ -147,10 +149,12 @@ class PermissionBatchDeleteMixin:
@classmethod @classmethod
def before_save(cls, root, info, ids, qs_to_delete): # noqa def before_save(cls, root, info, ids, qs_to_delete): # noqa
if isinstance(cls._meta.permissions, Iterable): super().before_save(root, info, ids, qs_to_delete)
for obj in qs_to_delete: if not isinstance(cls._meta.permissions, Iterable):
if not info.context.user.has_perms(cls._meta.permissions, obj): raise PermissionDenied()
raise PermissionDenied() for obj in qs_to_delete:
if not info.context.user.has_perms(cls._meta.permissions, obj):
raise PermissionDenied()
class PermissionPatchMixin: class PermissionPatchMixin:
...@@ -264,10 +268,12 @@ class ModelValidationMixin: ...@@ -264,10 +268,12 @@ class ModelValidationMixin:
@classmethod @classmethod
def after_update_obj(cls, root, info, data, obj, full_input): def after_update_obj(cls, root, info, data, obj, full_input):
super().after_update_obj(root, info, data, obj, full_input)
obj.full_clean() obj.full_clean()
@classmethod @classmethod
def before_create_obj(cls, info, data, obj): def before_create_obj(cls, info, data, obj):
super().before_create_obj(info, data, obj)
obj.full_clean() obj.full_clean()
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment