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 +181 -25
- strawberry/utils/typing.py +1 -1
- {strawberry_graphql-0.229.2.dist-info → strawberry_graphql-0.229.2.dev1715881453.dist-info}/METADATA +1 -1
- {strawberry_graphql-0.229.2.dist-info → strawberry_graphql-0.229.2.dev1715881453.dist-info}/RECORD +7 -7
- {strawberry_graphql-0.229.2.dist-info → strawberry_graphql-0.229.2.dev1715881453.dist-info}/LICENSE +0 -0
- {strawberry_graphql-0.229.2.dist-info → strawberry_graphql-0.229.2.dev1715881453.dist-info}/WHEEL +0 -0
- {strawberry_graphql-0.229.2.dist-info → strawberry_graphql-0.229.2.dev1715881453.dist-info}/entry_points.txt +0 -0
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:
|
54
|
-
) -> Union[
|
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
|
-
|
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
|
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
|
-
|
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
|
-
|
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
|
-
|
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:
|
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
|
-
|
156
|
-
|
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:
|
319
|
+
**kwargs: object[str, Any],
|
165
320
|
) -> Any:
|
166
321
|
for permission in self.permissions:
|
167
|
-
|
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
|
-
|
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)
|
strawberry/utils/typing.py
CHANGED
@@ -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(
|
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):
|
{strawberry_graphql-0.229.2.dist-info → strawberry_graphql-0.229.2.dev1715881453.dist-info}/RECORD
RENAMED
@@ -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=
|
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=
|
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=
|
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,,
|
{strawberry_graphql-0.229.2.dist-info → strawberry_graphql-0.229.2.dev1715881453.dist-info}/LICENSE
RENAMED
File without changes
|
{strawberry_graphql-0.229.2.dist-info → strawberry_graphql-0.229.2.dev1715881453.dist-info}/WHEEL
RENAMED
File without changes
|
File without changes
|