From 7cdf4055110fd15012d34d03b05d0ea030adf32b Mon Sep 17 00:00:00 2001 From: "d.krivokhizhin" Date: Thu, 15 May 2025 13:56:44 +0300 Subject: [PATCH 1/3] add: tools for creating combined permissions --- strawberry_django/permissions.py | 107 +++++++++++++++++++++++++++++++ 1 file changed, 107 insertions(+) diff --git a/strawberry_django/permissions.py b/strawberry_django/permissions.py index e97595a1d..cd0ac6ae5 100644 --- a/strawberry_django/permissions.py +++ b/strawberry_django/permissions.py @@ -52,6 +52,91 @@ _M = TypeVar("_M", bound=Model) +# Borrowed from the DRF project +class OperationHolderMixin: + def __and__(self, other): + return OperandHolder(AND, self, other) + + def __or__(self, other): + return OperandHolder(OR, self, other) + + def __rand__(self, other): + return OperandHolder(AND, other, self) + + def __ror__(self, other): + return OperandHolder(OR, other, self) + + def __invert__(self): + return SingleOperandHolder(NOT, self) + + +class SingleOperandHolder(OperationHolderMixin): + def __init__(self, operator_class, op1_class): + self.operator_class = operator_class + self.op1_class = op1_class + + def __call__(self, *args, **kwargs): + op1 = self.op1_class(*args, **kwargs) + return self.operator_class(op1) + + +class OperandHolder(OperationHolderMixin): + def __init__(self, operator_class, op1_class, op2_class): + self.operator_class = operator_class + self.op1_class = op1_class + self.op2_class = op2_class + + def __call__(self, *args, **kwargs): + op1 = self.op1_class(*args, **kwargs) + op2 = self.op2_class(*args, **kwargs) + return self.operator_class(op1, op2) + + def __eq__(self, other): + return ( + isinstance(other, OperandHolder) and + self.operator_class == other.operator_class and + self.op1_class == other.op1_class and + self.op2_class == other.op2_class + ) + + def __hash__(self): + return hash((self.operator_class, self.op1_class, self.op2_class)) + + +class AND: + def __init__(self, op1, op2): + self.op1 = op1 + self.op2 = op2 + + def has_permission(self, user: UserType) -> bool: + return self.op1.has_permission(user=user) and self.op2.has_permission(user=user) + + +class OR: + def __init__(self, op1, op2): + self.op1 = op1 + self.op2 = op2 + + def has_permission(self, user: UserType) -> bool: + return self.op1.has_permission(user=user) or self.op2.has_permission(user=user) + + +class NOT: + def __init__(self, op1): + self.op1 = op1 + + def has_permission(self, user: UserType): + return not self.op1.has_permission(user=user) + + +class BasePermissionMetaclass(OperationHolderMixin, type): + pass + + +class BasePermission(metaclass=BasePermissionMetaclass): + def has_permission(self, user: UserType) -> bool: + return True + @functools.lru_cache def _get_user_or_anonymous_getter() -> Optional[Callable[[UserType], UserType]]: @@ -930,3 +1015,25 @@ class HasRetvalPerm(HasPerm): "Will check if the user has any/all permissions for the resolved " "value of this field before returning it.", ) + +class HasPermissionClasses(DjangoPermissionExtension): + def __init__(self, *args, permission_classes: Iterable, **kwargs): + super().__init__(*args, **kwargs) + self.permission_classes = permission_classes + + def resolve_for_user( # pragma: no cover + self, + resolver: Callable, + user: UserType | None, + *, + info: Info, + source: Any, + ) -> AwaitableOrValue[Any]: + if not user.is_authenticated or not user.is_active: + raise DjangoNoPermission + + permissions = [permission() for permission in self.permission_classes] + for permission in permissions: + if not permission.has_permission(user=user): + raise DjangoNoPermission + return resolver() \ No newline at end of file From 34a45e1a30fd68a708ea3bcb2df56e63efd1b4e2 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Thu, 15 May 2025 11:01:04 +0000 Subject: [PATCH 2/3] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- strawberry_django/permissions.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/strawberry_django/permissions.py b/strawberry_django/permissions.py index cd0ac6ae5..e791277cb 100644 --- a/strawberry_django/permissions.py +++ b/strawberry_django/permissions.py @@ -52,6 +52,7 @@ _M = TypeVar("_M", bound=Model) + # Borrowed from the DRF project class OperationHolderMixin: def __and__(self, other): @@ -93,10 +94,10 @@ def __call__(self, *args, **kwargs): def __eq__(self, other): return ( - isinstance(other, OperandHolder) and - self.operator_class == other.operator_class and - self.op1_class == other.op1_class and - self.op2_class == other.op2_class + isinstance(other, OperandHolder) + and self.operator_class == other.operator_class + and self.op1_class == other.op1_class + and self.op2_class == other.op2_class ) def __hash__(self): @@ -1016,6 +1017,7 @@ class HasRetvalPerm(HasPerm): "value of this field before returning it.", ) + class HasPermissionClasses(DjangoPermissionExtension): def __init__(self, *args, permission_classes: Iterable, **kwargs): super().__init__(*args, **kwargs) @@ -1036,4 +1038,4 @@ def resolve_for_user( # pragma: no cover for permission in permissions: if not permission.has_permission(user=user): raise DjangoNoPermission - return resolver() \ No newline at end of file + return resolver() From e0764dad4ffdb8fa33bc94623054ae4afe6bbaa7 Mon Sep 17 00:00:00 2001 From: "d.krivokhizhin" Date: Wed, 21 May 2025 13:05:31 +0300 Subject: [PATCH 3/3] resolve typing --- strawberry_django/permissions.py | 23 ++++++++++++++++------- 1 file changed, 16 insertions(+), 7 deletions(-) diff --git a/strawberry_django/permissions.py b/strawberry_django/permissions.py index cd0ac6ae5..c34e13b43 100644 --- a/strawberry_django/permissions.py +++ b/strawberry_django/permissions.py @@ -52,6 +52,7 @@ _M = TypeVar("_M", bound=Model) + # Borrowed from the DRF project class OperationHolderMixin: def __and__(self, other): @@ -93,10 +94,10 @@ def __call__(self, *args, **kwargs): def __eq__(self, other): return ( - isinstance(other, OperandHolder) and - self.operator_class == other.operator_class and - self.op1_class == other.op1_class and - self.op2_class == other.op2_class + isinstance(other, OperandHolder) + and self.operator_class == other.operator_class + and self.op1_class == other.op1_class + and self.op2_class == other.op2_class ) def __hash__(self): @@ -1016,8 +1017,16 @@ class HasRetvalPerm(HasPerm): "value of this field before returning it.", ) + class HasPermissionClasses(DjangoPermissionExtension): - def __init__(self, *args, permission_classes: Iterable, **kwargs): + def __init__( + self, + *args, + permission_classes: Iterable[ + type[BasePermission | SingleOperandHolder | OperandHolder] + ], + **kwargs, + ): super().__init__(*args, **kwargs) self.permission_classes = permission_classes @@ -1029,11 +1038,11 @@ def resolve_for_user( # pragma: no cover info: Info, source: Any, ) -> AwaitableOrValue[Any]: - if not user.is_authenticated or not user.is_active: + if not user: raise DjangoNoPermission permissions = [permission() for permission in self.permission_classes] for permission in permissions: if not permission.has_permission(user=user): raise DjangoNoPermission - return resolver() \ No newline at end of file + return resolver()