diff --git a/aleksis/core/checks.py b/aleksis/core/checks.py index 68ccb06bacae8e0b1dde9e14e4056e18e339e95e..d206c808dd273909e286d163260b80596267cb46 100644 --- a/aleksis/core/checks.py +++ b/aleksis/core/checks.py @@ -1,9 +1,11 @@ +from collections.abc import Iterable from typing import Optional 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 .schema.base import BaseBatchCreateMutation, BaseBatchDeleteMutation, BaseBatchPatchMutation from .util.apps import AppConfig @@ -71,3 +73,28 @@ def check_app_models_base_class( ) 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 diff --git a/aleksis/core/schema/__init__.py b/aleksis/core/schema/__init__.py index 696e2867e85219523041d21c9172422c1f577b05..97de056ca8ce6febc28697cfb67dae76c167bd5e 100644 --- a/aleksis/core/schema/__init__.py +++ b/aleksis/core/schema/__init__.py @@ -4,6 +4,7 @@ from django.db.models import Q import graphene 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 haystack.inputs import AutoQuery from haystack.query import SearchQuerySet @@ -81,6 +82,17 @@ from .two_factor import TwoFactorType 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): ping = graphene.String(payload=graphene.String()) diff --git a/aleksis/core/schema/base.py b/aleksis/core/schema/base.py index cd9c29f1c157460f4df8ecf25dfff0b530808707..896585444d4a30b80cc27d2a69f098ff732db5af 100644 --- a/aleksis/core/schema/base.py +++ b/aleksis/core/schema/base.py @@ -111,7 +111,8 @@ class PermissionBatchCreateMixin: @classmethod 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 ): raise PermissionDenied() @@ -129,7 +130,8 @@ class PermissionBatchPatchMixin: @classmethod 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 ): raise PermissionDenied() @@ -147,10 +149,12 @@ class PermissionBatchDeleteMixin: @classmethod def before_save(cls, root, info, ids, qs_to_delete): # noqa - if isinstance(cls._meta.permissions, Iterable): - for obj in qs_to_delete: - if not info.context.user.has_perms(cls._meta.permissions, obj): - raise PermissionDenied() + super().before_save(root, info, ids, qs_to_delete) + if not isinstance(cls._meta.permissions, Iterable): + raise PermissionDenied() + for obj in qs_to_delete: + if not info.context.user.has_perms(cls._meta.permissions, obj): + raise PermissionDenied() class PermissionPatchMixin: @@ -264,10 +268,12 @@ class ModelValidationMixin: @classmethod def after_update_obj(cls, root, info, data, obj, full_input): + super().after_update_obj(root, info, data, obj, full_input) obj.full_clean() @classmethod def before_create_obj(cls, info, data, obj): + super().before_create_obj(info, data, obj) obj.full_clean()