Skip to content

Commit 388023d

Browse files
authored
Merge pull request #54 from eadwinCode/allow_permission_instance
Allow permission instance in permissions parameter
2 parents 0c8bb86 + 125b723 commit 388023d

File tree

9 files changed

+131
-30
lines changed

9 files changed

+131
-30
lines changed

docs/api_controller/api_controller_permission.md

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,37 @@ class PermissionController:
3838
def must_be_authenticated(self, word: str):
3939
return dict(says=word)
4040
```
41+
!!! Note
42+
New in **v0.18.7**
43+
Controller Permission and Route Function `permissions` can now take `BasePermission` instance.
44+
45+
For example, we can pass the `ReadOnly` instance to the `permission` parameter.
46+
```python
47+
from ninja_extra import permissions, api_controller, http_get
48+
49+
class ReadOnly(permissions.BasePermission):
50+
def has_permission(self, request, view):
51+
return request.method in permissions.SAFE_METHODS
52+
53+
@api_controller(permissions=[permissions.IsAuthenticated | ReadOnly()])
54+
...
55+
```
56+
For example:
57+
```python
58+
from ninja_extra import permissions
59+
60+
class UserWithPermission(permissions.BasePermission):
61+
def __init__(self, permission: str) -> None:
62+
self._permission = permission
63+
64+
def has_permission(self, request, view):
65+
return request.user.has_perm(self._permission)
66+
67+
# in controller or route function
68+
permissions=[UserWithPermission('blog.add')]
69+
```
70+
4171
## **Permissions Supported Operands**
4272
- & (and) eg: `permissions.IsAuthenticated & ReadOnly`
4373
- | (or) eg: `permissions.IsAuthenticated | ReadOnly`
44-
- ~ (not) eg: `!(permissions.IsAuthenticated & ReadOnly)`
74+
- ~ (not) eg: `!(permissions.IsAuthenticated & ReadOnly)`

ninja_extra/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
"""Django Ninja Extra - Class Based Utility and more for Django Ninja(Fast Django REST framework)"""
22

3-
__version__ = "0.18.6"
3+
__version__ = "0.18.7"
44

55
import django
66

ninja_extra/controllers/base.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@
3434
from ninja_extra.helper import get_function_name
3535
from ninja_extra.operation import ControllerPathView, Operation
3636
from ninja_extra.permissions import AllowAny, BasePermission
37+
from ninja_extra.permissions.base import OperationHolderMixin
3738
from ninja_extra.shortcuts import (
3839
fail_silently,
3940
get_object_or_exception,
@@ -168,7 +169,10 @@ def _get_permissions(self) -> Iterable[BasePermission]:
168169
return
169170

170171
for permission_class in self.context.permission_classes:
171-
permission_instance = permission_class()
172+
if isinstance(permission_class, (type, OperationHolderMixin)):
173+
permission_instance = permission_class() # type: ignore[operator]
174+
else:
175+
permission_instance = permission_class
172176
yield permission_instance
173177

174178
def check_permissions(self) -> None:

ninja_extra/controllers/route/__init__.py

Lines changed: 25 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,9 @@ def __init__(
5454
exclude_none: bool = False,
5555
url_name: Optional[str] = None,
5656
include_in_schema: bool = True,
57-
permissions: Optional[List[Type[BasePermission]]] = None,
57+
permissions: Optional[
58+
List[Union[Type[BasePermission], BasePermission, Any]]
59+
] = None,
5860
openapi_extra: Optional[Dict[str, Any]] = None,
5961
) -> None:
6062

@@ -112,7 +114,7 @@ def __init__(
112114
)
113115
self.route_params = ninja_route_params
114116
self.is_async = is_async(view_func)
115-
self.permissions = permissions
117+
self.permissions = permissions # type: ignore[assignment]
116118
self.view_func = view_func
117119

118120
@classmethod
@@ -135,7 +137,9 @@ def _create_route_function(
135137
exclude_none: bool = False,
136138
url_name: Optional[str] = None,
137139
include_in_schema: bool = True,
138-
permissions: Optional[List[Type[BasePermission]]] = None,
140+
permissions: Optional[
141+
List[Union[Type[BasePermission], BasePermission, Any]]
142+
] = None,
139143
openapi_extra: Optional[Dict[str, Any]] = None,
140144
) -> RouteFunction:
141145
if response is NOT_SET:
@@ -184,7 +188,9 @@ def get(
184188
exclude_none: bool = False,
185189
url_name: Optional[str] = None,
186190
include_in_schema: bool = True,
187-
permissions: Optional[List[Type[BasePermission]]] = None,
191+
permissions: Optional[
192+
List[Union[Type[BasePermission], BasePermission, Any]]
193+
] = None,
188194
openapi_extra: Optional[Dict[str, Any]] = None,
189195
) -> Callable[[TCallable], RouteFunction]:
190196
"""
@@ -256,7 +262,9 @@ def post(
256262
exclude_none: bool = False,
257263
url_name: Optional[str] = None,
258264
include_in_schema: bool = True,
259-
permissions: Optional[List[Type[BasePermission]]] = None,
265+
permissions: Optional[
266+
List[Union[Type[BasePermission], BasePermission, Any]]
267+
] = None,
260268
openapi_extra: Optional[Dict[str, Any]] = None,
261269
) -> Callable[[TCallable], RouteFunction]:
262270
"""
@@ -328,7 +336,9 @@ def delete(
328336
exclude_none: bool = False,
329337
url_name: Optional[str] = None,
330338
include_in_schema: bool = True,
331-
permissions: Optional[List[Type[BasePermission]]] = None,
339+
permissions: Optional[
340+
List[Union[Type[BasePermission], BasePermission, Any]]
341+
] = None,
332342
openapi_extra: Optional[Dict[str, Any]] = None,
333343
) -> Callable[[TCallable], RouteFunction]:
334344
"""
@@ -400,7 +410,9 @@ def patch(
400410
exclude_none: bool = False,
401411
url_name: Optional[str] = None,
402412
include_in_schema: bool = True,
403-
permissions: Optional[List[Type[BasePermission]]] = None,
413+
permissions: Optional[
414+
List[Union[Type[BasePermission], BasePermission, Any]]
415+
] = None,
404416
openapi_extra: Optional[Dict[str, Any]] = None,
405417
) -> Callable[[TCallable], RouteFunction]:
406418
"""
@@ -473,7 +485,9 @@ def put(
473485
exclude_none: bool = False,
474486
url_name: Optional[str] = None,
475487
include_in_schema: bool = True,
476-
permissions: Optional[List[Type[BasePermission]]] = None,
488+
permissions: Optional[
489+
List[Union[Type[BasePermission], BasePermission, Any]]
490+
] = None,
477491
openapi_extra: Optional[Dict[str, Any]] = None,
478492
) -> Callable[[TCallable], RouteFunction]:
479493
"""
@@ -547,7 +561,9 @@ def generic(
547561
exclude_none: bool = False,
548562
url_name: Optional[str] = None,
549563
include_in_schema: bool = True,
550-
permissions: Optional[List[Type[BasePermission]]] = None,
564+
permissions: Optional[
565+
List[Union[Type[BasePermission], BasePermission, Any]]
566+
] = None,
551567
openapi_extra: Optional[Dict[str, Any]] = None,
552568
) -> Callable[[TCallable], RouteFunction]:
553569
"""

ninja_extra/controllers/route/route_functions.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ def __call__(
4848
context = get_route_execution_context(
4949
request,
5050
temporal_response,
51-
self.route.permissions or _api_controller.permission_classes,
51+
self.route.permissions or _api_controller.permission_classes, # type: ignore[arg-type]
5252
*args,
5353
**kwargs,
5454
)
@@ -199,7 +199,7 @@ async def __call__(
199199
context = get_route_execution_context(
200200
request,
201201
temporal_response,
202-
self.route.permissions or _api_controller.permission_classes,
202+
self.route.permissions or _api_controller.permission_classes, # type: ignore[arg-type]
203203
*args,
204204
**kwargs,
205205
)

ninja_extra/operation.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -130,7 +130,7 @@ def get_execution_context(
130130

131131
_api_controller = route_function.get_api_controller()
132132
permission_classes = (
133-
route_function.route.permissions or _api_controller.permission_classes
133+
route_function.route.permissions or _api_controller.permission_classes # type: ignore[assignment]
134134
)
135135

136136
return get_route_execution_context(

ninja_extra/permissions/base.py

Lines changed: 25 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
Provides a set of pluggable permission policies.
44
"""
55
from abc import ABC, ABCMeta, abstractmethod
6-
from typing import TYPE_CHECKING, Any, Generic, Tuple, Type, TypeVar
6+
from typing import TYPE_CHECKING, Any, Generic, Tuple, Type, TypeVar, Union
77

88
from django.http import HttpRequest
99
from ninja.types import DictStrAny
@@ -17,19 +17,23 @@
1717

1818

1919
class OperationHolderMixin:
20-
def __and__(self, other: Type["BasePermission"]) -> "OperandHolder[AND]":
20+
def __and__(
21+
self, other: Union[Type["BasePermission"], "BasePermission"]
22+
) -> "OperandHolder[AND]":
2123
return OperandHolder(AND, self, other) # type: ignore
2224

23-
def __or__(self, other: Type["BasePermission"]) -> "OperandHolder[OR]":
25+
def __or__(
26+
self, other: Union[Type["BasePermission"], "BasePermission"]
27+
) -> "OperandHolder[OR]":
2428
return OperandHolder(OR, self, other) # type: ignore
2529

2630
def __rand__(
27-
self, other: Type["BasePermission"]
31+
self, other: Union[Type["BasePermission"], "BasePermission"]
2832
) -> "OperandHolder[AND]": # pragma: no cover
2933
return OperandHolder(AND, other, self) # type: ignore
3034

3135
def __ror__(
32-
self, other: Type["BasePermission"]
36+
self, other: Union[Type["BasePermission"], "BasePermission"]
3337
) -> "OperandHolder[OR]": # pragma: no cover
3438
return OperandHolder(OR, other, self) # type: ignore
3539

@@ -68,31 +72,41 @@ def has_object_permission(
6872

6973
class SingleOperandHolder(OperationHolderMixin, Generic[T]):
7074
def __init__(
71-
self, operator_class: Type[BasePermission], op1_class: Type[BasePermission]
75+
self,
76+
operator_class: Type[BasePermission],
77+
op1_class: Union[Type["BasePermission"], "BasePermission"],
7278
) -> None:
7379
super().__init__()
7480
self.operator_class = operator_class
7581
self.op1_class = op1_class
7682

7783
def __call__(self, *args: Tuple[Any], **kwargs: DictStrAny) -> BasePermission:
78-
op1 = self.op1_class()
84+
op1 = self.op1_class
85+
if isinstance(self.op1_class, (type, OperationHolderMixin)):
86+
op1 = self.op1_class()
7987
return self.operator_class(op1) # type: ignore
8088

8189

8290
class OperandHolder(OperationHolderMixin, Generic[T]):
8391
def __init__(
8492
self,
8593
operator_class: Type["BasePermission"],
86-
op1_class: Type["BasePermission"],
87-
op2_class: Type["BasePermission"],
94+
op1_class: Union[Type["BasePermission"], "BasePermission"],
95+
op2_class: Union[Type["BasePermission"], "BasePermission"],
8896
) -> None:
8997
self.operator_class = operator_class
9098
self.op1_class = op1_class
9199
self.op2_class = op2_class
92100

93101
def __call__(self, *args: Tuple[Any], **kwargs: DictStrAny) -> BasePermission:
94-
op1 = self.op1_class()
95-
op2 = self.op2_class()
102+
op1 = self.op1_class
103+
op2 = self.op2_class
104+
105+
if isinstance(self.op1_class, (type, OperationHolderMixin)):
106+
op1 = self.op1_class()
107+
108+
if isinstance(self.op2_class, (type, OperationHolderMixin)):
109+
op2 = self.op2_class()
96110
return self.operator_class(op1, op2) # type: ignore
97111

98112

ninja_extra/types.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
1-
from typing import List, Type, Union
1+
from typing import Any, List, Type, Union
22

33
from ninja_extra.permissions.base import (
44
BasePermission,
55
OperandHolder,
66
SingleOperandHolder,
77
)
88

9-
PermissionType = Union[
10-
List[Type[BasePermission]], List[OperandHolder], List[SingleOperandHolder], List
9+
PermissionType = List[
10+
Union[Type[BasePermission], OperandHolder, SingleOperandHolder, BasePermission, Any]
1111
]

tests/test_permissions.py

Lines changed: 38 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,8 @@
44
import pytest
55
from django.contrib.auth.models import AnonymousUser, User
66

7-
from ninja_extra import permissions
7+
from ninja_extra import ControllerBase, api_controller, http_get, permissions
8+
from ninja_extra.testing import TestClient
89

910
anonymous_request = Mock()
1011
anonymous_request.user = AnonymousUser()
@@ -214,3 +215,39 @@ def test_object_and_lazyness(self):
214215
assert hasperm is False
215216
assert mock_deny.call_count == 1
216217
mock_allow.assert_not_called()
218+
219+
220+
@api_controller(
221+
"permission/", permissions=[permissions.AllowAny, permissions.IsAdminUser()]
222+
)
223+
class Some2Controller(ControllerBase):
224+
@http_get("index/")
225+
def index(self):
226+
return {"success": True}
227+
228+
@http_get(
229+
"permission/",
230+
permissions=[permissions.IsAdminUser() & permissions.IsAuthenticatedOrReadOnly],
231+
)
232+
def permission_accept_type_and_instance(self):
233+
return {"success": True}
234+
235+
236+
@pytest.mark.django_db
237+
@pytest.mark.parametrize("route", ["permission/", "index/"])
238+
def test_permission_controller_instance(route):
239+
user = User.objects.create_user(
240+
username="eadwin",
241+
242+
password="password",
243+
is_staff=True,
244+
is_superuser=True,
245+
)
246+
247+
client = TestClient(Some2Controller)
248+
res = client.get(route, user=AnonymousUser())
249+
assert res.status_code == 403
250+
251+
res = client.get(route, user=user)
252+
assert res.status_code == 200
253+
assert res.json() == {"success": True}

0 commit comments

Comments
 (0)