strawberry-graphql 0.229.2__py3-none-any.whl → 0.229.2.dev1715881453__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
strawberry/permission.py CHANGED
@@ -8,12 +8,15 @@ from typing import (
8
8
  TYPE_CHECKING,
9
9
  Any,
10
10
  Awaitable,
11
- Dict,
12
11
  List,
12
+ Literal,
13
13
  Optional,
14
+ Tuple,
14
15
  Type,
16
+ TypedDict,
15
17
  Union,
16
18
  )
19
+ from typing_extensions import deprecated
17
20
 
18
21
  from strawberry.exceptions import StrawberryGraphQLError
19
22
  from strawberry.exceptions.permission_fail_silently_requires_optional import (
@@ -35,6 +38,15 @@ if TYPE_CHECKING:
35
38
  from strawberry.types import Info
36
39
 
37
40
 
41
+ def unpack_maybe(
42
+ value: Union[object, Tuple[bool, object]], default: object = None
43
+ ) -> Tuple[object, object]:
44
+ if isinstance(value, tuple) and len(value) == 2:
45
+ return value
46
+ else:
47
+ return value, default
48
+
49
+
38
50
  class BasePermission(abc.ABC):
39
51
  """
40
52
  Base class for creating permissions
@@ -50,18 +62,41 @@ class BasePermission(abc.ABC):
50
62
 
51
63
  @abc.abstractmethod
52
64
  def has_permission(
53
- self, source: Any, info: Info, **kwargs: Any
54
- ) -> Union[bool, Awaitable[bool]]:
65
+ self, source: Any, info: Info, **kwargs: object
66
+ ) -> Union[
67
+ bool,
68
+ Awaitable[bool],
69
+ Tuple[Literal[False], dict],
70
+ Awaitable[Tuple[Literal[False], dict]],
71
+ ]:
72
+ """
73
+ This method is a required override in the permission class. It checks if the user has the necessary permissions to access a specific field.
74
+
75
+ The method should return a boolean value:
76
+ - True: The user has the necessary permissions.
77
+ - False: The user does not have the necessary permissions. In this case, the `on_unauthorized` method will be invoked.
78
+
79
+ Avoid raising exceptions in this method. Instead, use the `on_unauthorized` method to handle errors and customize the error response.
80
+
81
+ If there's a need to pass additional information to the `on_unauthorized` method, return a tuple. The first element should be False, and the second element should be a dictionary containing the additional information.
82
+
83
+ Args:
84
+ source (Any): The source field that the permission check is being performed on.
85
+ info (Info): The GraphQL resolve info associated with the field.
86
+ **kwargs (Any): Additional arguments that are typically passed to the field resolver.
87
+
88
+ Returns:
89
+ bool or tuple: Returns True if the user has the necessary permissions. Returns False or a tuple (False, additional_info) if the user does not have the necessary permissions. In the latter case, the `on_unauthorized` method will be invoked.
90
+ """
55
91
  raise NotImplementedError(
56
92
  "Permission classes should override has_permission method"
57
93
  )
58
94
 
59
- def on_unauthorized(self) -> None:
95
+ def on_unauthorized(self, **kwargs: object) -> None:
60
96
  """
61
97
  Default error raising for permissions.
62
98
  This can be overridden to customize the behavior.
63
99
  """
64
-
65
100
  # Instantiate error class
66
101
  error = self.error_class(self.message or "")
67
102
 
@@ -74,6 +109,9 @@ class BasePermission(abc.ABC):
74
109
  raise error
75
110
 
76
111
  @property
112
+ @deprecated(
113
+ "@schema_directive is deprecated and will be disabled by default on 31.12.2024 with future removal planned. Use the new @permissions directive instead."
114
+ )
77
115
  def schema_directive(self) -> object:
78
116
  if not self._schema_directive:
79
117
 
@@ -89,6 +127,111 @@ class BasePermission(abc.ABC):
89
127
 
90
128
  return self._schema_directive
91
129
 
130
+ @cached_property
131
+ def is_async(self) -> bool:
132
+ return iscoroutinefunction(self.has_permission)
133
+
134
+ def __and__(self, other: BasePermission):
135
+ return AndPermission([self, other])
136
+
137
+ def __or__(self, other: BasePermission):
138
+ return OrPermission([self, other])
139
+
140
+
141
+ class CompositePermissionContext(TypedDict):
142
+ failed_permissions: List[Tuple[BasePermission, dict]]
143
+
144
+
145
+ class CompositePermission(BasePermission, abc.ABC):
146
+ def __init__(self, child_permissions: List[BasePermission]):
147
+ self.child_permissions = child_permissions
148
+
149
+ def on_unauthorized(self, **kwargs: object) -> Any:
150
+ failed_permissions = kwargs.get("failed_permissions", [])
151
+ for permission, context in failed_permissions:
152
+ permission.on_unauthorized(**context)
153
+
154
+ @cached_property
155
+ def is_async(self) -> bool:
156
+ return any(x.is_async for x in self.child_permissions)
157
+
158
+
159
+ class AndPermission(CompositePermission):
160
+ def has_permission(
161
+ self, source: Any, info: Info, **kwargs: object
162
+ ) -> Union[
163
+ bool,
164
+ Awaitable[bool],
165
+ Tuple[Literal[False], CompositePermissionContext],
166
+ Awaitable[Tuple[Literal[False], CompositePermissionContext]],
167
+ ]:
168
+ if self.is_async:
169
+ return self._has_permission_async(source, info, **kwargs)
170
+
171
+ for permission in self.child_permissions:
172
+ has_permission, context = unpack_maybe(
173
+ permission.has_permission(source, info, **kwargs), {}
174
+ )
175
+ if not has_permission:
176
+ return False, {"failed_permissions": [(permission, context)]}
177
+ return True
178
+
179
+ async def _has_permission_async(
180
+ self, source: Any, info: Info, **kwargs: object
181
+ ) -> Union[bool, Tuple[Literal[False], CompositePermissionContext]]:
182
+ for permission in self.child_permissions:
183
+ permission_response = await await_maybe(
184
+ permission.has_permission(source, info, **kwargs)
185
+ )
186
+ has_permission, context = unpack_maybe(permission_response, {})
187
+ if not has_permission:
188
+ return False, {"failed_permissions": [(permission, context)]}
189
+ return True
190
+
191
+ def __and__(self, other: BasePermission):
192
+ return AndPermission([*self.child_permissions, other])
193
+
194
+
195
+ class OrPermission(CompositePermission):
196
+ def has_permission(
197
+ self, source: Any, info: Info, **kwargs: object
198
+ ) -> Union[
199
+ bool,
200
+ Awaitable[bool],
201
+ Tuple[Literal[False], dict],
202
+ Awaitable[Tuple[Literal[False], dict]],
203
+ ]:
204
+ if self.is_async:
205
+ return self._has_permission_async(source, info, **kwargs)
206
+ failed_permissions = []
207
+ for permission in self.child_permissions:
208
+ has_permission, context = unpack_maybe(
209
+ permission.has_permission(source, info, **kwargs), {}
210
+ )
211
+ if has_permission:
212
+ return True
213
+ failed_permissions.append((permission, context))
214
+
215
+ return False, {"failed_permissions": failed_permissions}
216
+
217
+ async def _has_permission_async(
218
+ self, source: Any, info: Info, **kwargs: object
219
+ ) -> Union[bool, Tuple[Literal[False], dict]]:
220
+ failed_permissions = []
221
+ for permission in self.child_permissions:
222
+ permission_response = await await_maybe(
223
+ permission.has_permission(source, info, **kwargs)
224
+ )
225
+ has_permission, context = unpack_maybe(permission_response, {})
226
+ if has_permission:
227
+ return True
228
+ failed_permissions.append((permission, context))
229
+
230
+ return False, {"failed_permissions": failed_permissions}
231
+
232
+ def __or__(self, other: BasePermission):
233
+ return OrPermission([*self.child_permissions, other])
234
+
92
235
 
93
236
  class PermissionExtension(FieldExtension):
94
237
  """
@@ -100,8 +243,8 @@ class PermissionExtension(FieldExtension):
100
243
 
101
244
  NOTE:
102
245
  Currently, this is automatically added to the field, when using
103
- field.permission_classes
104
- This is deprecated behavior, please manually add the extension to field.extensions
246
+ field.permission_classes. You are free to use whichever method you prefer.
247
+ Use PermissionExtension if you want additional customization.
105
248
  """
106
249
 
107
250
  def __init__(
@@ -117,12 +260,16 @@ class PermissionExtension(FieldExtension):
117
260
 
118
261
  def apply(self, field: StrawberryField) -> None:
119
262
  """
120
- Applies all of the permission directives to the schema
263
+ Applies all the permission directives to the schema
121
264
  and sets up silent permissions
122
265
  """
123
266
  if self.use_directives:
124
267
  field.directives.extend(
125
- p.schema_directive for p in self.permissions if p.schema_directive
268
+ [
269
+ p.schema_directive
270
+ for p in self.permissions
271
+ if not isinstance(p, CompositePermission)
272
+ ]
126
273
  )
127
274
  # We can only fail silently if the field is optional or a list
128
275
  if self.fail_silently:
@@ -132,28 +279,36 @@ class PermissionExtension(FieldExtension):
132
279
  elif isinstance(field.type, StrawberryList):
133
280
  self.return_empty_list = True
134
281
  else:
135
- errror = PermissionFailSilentlyRequiresOptionalError(field)
136
- raise errror
282
+ raise PermissionFailSilentlyRequiresOptionalError(field)
137
283
 
138
- def _on_unauthorized(self, permission: BasePermission) -> Any:
284
+ def _on_unauthorized(self, permission: BasePermission, **kwargs: object) -> Any:
139
285
  if self.fail_silently:
140
286
  return [] if self.return_empty_list else None
141
- return permission.on_unauthorized()
287
+
288
+ if kwargs in (None, {}):
289
+ return permission.on_unauthorized()
290
+ return permission.on_unauthorized(**kwargs)
142
291
 
143
292
  def resolve(
144
293
  self,
145
294
  next_: SyncExtensionResolver,
146
295
  source: Any,
147
296
  info: Info,
148
- **kwargs: Dict[str, Any],
297
+ **kwargs: object[str, Any],
149
298
  ) -> Any:
150
299
  """
151
300
  Checks if the permission should be accepted and
152
301
  raises an exception if not
153
302
  """
303
+
154
304
  for permission in self.permissions:
155
- if not permission.has_permission(source, info, **kwargs):
156
- return self._on_unauthorized(permission)
305
+ has_permission, context = unpack_maybe(
306
+ permission.has_permission(source, info, **kwargs), {}
307
+ )
308
+
309
+ if not has_permission:
310
+ return self._on_unauthorized(permission, **context)
311
+
157
312
  return next_(source, info, **kwargs)
158
313
 
159
314
  async def resolve_async(
@@ -161,15 +316,21 @@ class PermissionExtension(FieldExtension):
161
316
  next_: AsyncExtensionResolver,
162
317
  source: Any,
163
318
  info: Info,
164
- **kwargs: Dict[str, Any],
319
+ **kwargs: object[str, Any],
165
320
  ) -> Any:
166
321
  for permission in self.permissions:
167
- has_permission = await await_maybe(
322
+ permission_response = await await_maybe(
168
323
  permission.has_permission(source, info, **kwargs)
169
324
  )
170
325
 
326
+ context = {}
327
+ if isinstance(permission_response, tuple):
328
+ has_permission, context = permission_response
329
+ else:
330
+ has_permission = permission_response
331
+
171
332
  if not has_permission:
172
- return self._on_unauthorized(permission)
333
+ return self._on_unauthorized(permission, **context)
173
334
  next = next_(source, info, **kwargs)
174
335
  if inspect.isasyncgen(next):
175
336
  return next
@@ -179,9 +340,4 @@ class PermissionExtension(FieldExtension):
179
340
  def supports_sync(self) -> bool:
180
341
  """The Permission extension always supports async checking using await_maybe,
181
342
  but only supports sync checking if there are no async permissions"""
182
- async_permissions = [
183
- True
184
- for permission in self.permissions
185
- if iscoroutinefunction(permission.has_permission)
186
- ]
187
- return len(async_permissions) == 0
343
+ return all(not permission.is_async for permission in self.permissions)
@@ -302,7 +302,7 @@ def _get_namespace_from_ast(
302
302
  # here to resolve lazy types by execing the annotated args, resolving the
303
303
  # type directly and then adding it to extra namespace, so that _eval_type
304
304
  # can properly resolve it later
305
- type_name = args[0].strip(" '\"\n")
305
+ type_name = args[0].strip()
306
306
  for arg in args[1:]:
307
307
  evaled_arg = eval(arg, globalns, localns) # noqa: PGH001, S307
308
308
  if isinstance(evaled_arg, StrawberryLazyReference):
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: strawberry-graphql
3
- Version: 0.229.2
3
+ Version: 0.229.2.dev1715881453
4
4
  Summary: A library for creating GraphQL APIs
5
5
  Home-page: https://strawberry.rocks/
6
6
  License: MIT
@@ -162,7 +162,7 @@ strawberry/litestar/handlers/graphql_ws_handler.py,sha256=vVpjd5rJOldF8aQWEGjmmN
162
162
  strawberry/mutation.py,sha256=NROPvHJU1BBxZB7Wj4Okxw4hDIYM59MCpukAGEmSNYA,255
163
163
  strawberry/object_type.py,sha256=iQL2NqO7I28bS0moWgxPmCDc0w1HewPcXq8Xyh-xAWI,12539
164
164
  strawberry/parent.py,sha256=rmedKjN4zdg4KTnNV8DENrzgNYVL67rXpHjHoBofMS4,825
165
- strawberry/permission.py,sha256=dcKx4Zlg4ZhcxEDBOSWzz0CUN4WPkcc_kJUVuvLLs6w,5925
165
+ strawberry/permission.py,sha256=7ePOn4P_n32N_msMnWytzqKZa77HyOQ4u0hp9RliYJo,12098
166
166
  strawberry/printer/__init__.py,sha256=DmepjmgtkdF5RxK_7yC6qUyRWn56U-9qeZMbkztYB9w,62
167
167
  strawberry/printer/ast_from_value.py,sha256=MFIX2V51d9ocRvD0Njemjk8YIzKh2BB1g2iUcX6a3d8,4946
168
168
  strawberry/printer/printer.py,sha256=HUUecFETXWgfQtMzxjx9pNOXP3tDevqlU3sG7TC3AD8,17449
@@ -240,9 +240,9 @@ strawberry/utils/inspect.py,sha256=6z-tJpiWWm6E4-O6OUfhu689W9k1uY0m3FDwAfVCiNs,2
240
240
  strawberry/utils/logging.py,sha256=flS7hV0JiIOEdXcrIjda4WyIWix86cpHHFNJL8gl1y4,713
241
241
  strawberry/utils/operation.py,sha256=Um-tBCPl3_bVFN2Ph7o1mnrxfxBes4HFCj6T0x4kZxE,1135
242
242
  strawberry/utils/str_converters.py,sha256=avIgPVLg98vZH9mA2lhzVdyyjqzLsK2NdBw9mJQ02Xk,813
243
- strawberry/utils/typing.py,sha256=u9vUmf0F57s9OL_qBK4oLlQtkI__XuLRAQ8ryNBnPgQ,13491
244
- strawberry_graphql-0.229.2.dist-info/LICENSE,sha256=m-XnIVUKqlG_AWnfi9NReh9JfKhYOB-gJfKE45WM1W8,1072
245
- strawberry_graphql-0.229.2.dist-info/METADATA,sha256=9TO-t0AvKV4wkZcofWrX9JQuBjl9C7GIOpLzDiYgQrI,7821
246
- strawberry_graphql-0.229.2.dist-info/WHEEL,sha256=sP946D7jFCHeNz5Iq4fL4Lu-PrWrFsgfLXbbkciIZwg,88
247
- strawberry_graphql-0.229.2.dist-info/entry_points.txt,sha256=Nk7-aT3_uEwCgyqtHESV9H6Mc31cK-VAvhnQNTzTb4k,49
248
- strawberry_graphql-0.229.2.dist-info/RECORD,,
243
+ strawberry/utils/typing.py,sha256=SQVOw1nuFZk2Pe3iz0o8ebzpoyvBVoGSQZVZj6-8k7I,13483
244
+ strawberry_graphql-0.229.2.dev1715881453.dist-info/LICENSE,sha256=m-XnIVUKqlG_AWnfi9NReh9JfKhYOB-gJfKE45WM1W8,1072
245
+ strawberry_graphql-0.229.2.dev1715881453.dist-info/METADATA,sha256=alSm87ziLGjzNKwRfESeLKCG_cYypmHsmo7_U3_19_4,7835
246
+ strawberry_graphql-0.229.2.dev1715881453.dist-info/WHEEL,sha256=sP946D7jFCHeNz5Iq4fL4Lu-PrWrFsgfLXbbkciIZwg,88
247
+ strawberry_graphql-0.229.2.dev1715881453.dist-info/entry_points.txt,sha256=Nk7-aT3_uEwCgyqtHESV9H6Mc31cK-VAvhnQNTzTb4k,49
248
+ strawberry_graphql-0.229.2.dev1715881453.dist-info/RECORD,,