Skip to content

Commit 6d394e4

Browse files
committed
permission documentation rewrite
1 parent 6977f05 commit 6d394e4

File tree

1 file changed

+248
-78
lines changed

1 file changed

+248
-78
lines changed
Lines changed: 248 additions & 78 deletions
Original file line numberDiff line numberDiff line change
@@ -1,120 +1,290 @@
1-
# **APIController Permissions**
1+
# Django Ninja Extra Permissions Guide
22

3-
The concept of this permission system came from Django [DRF](https://www.django-rest-framework.org/api-guide/permissions/).
3+
Permissions in Django Ninja Extra provide a flexible way to control access to your API endpoints. The permission system is inspired by [Django REST Framework](https://www.django-rest-framework.org/api-guide/permissions/) and allows you to define both global and endpoint-specific access controls.
44

5-
Permission checks are always run at the very start of the route function, before any other code is allowed to proceed.
6-
Permission checks will typically use the authentication information in the `request.user` and `request.auth` properties to determine if the incoming request should be permitted.
5+
## **How Permissions Work**
76

8-
Permissions are used to grant or deny access for different classes of users to different parts of the API.
7+
Permissions are checked at the start of each route function execution. They use the authentication information available in `request.user` and `request.auth` to determine if the request should be allowed to proceed.
98

10-
The simplest style of permission would be to allow access to any authenticated user, and deny access to any unauthenticated user.
11-
This corresponds to the `IsAuthenticated` class in **Django Ninja Extra**.
9+
## **Built-in Permission Classes**
1210

13-
A slightly less strict style of permission would be to allow full access to authenticated users, but allow read-only access to unauthenticated users.
14-
This corresponds to the `IsAuthenticatedOrReadOnly` class in **Django Ninja Extra**.
11+
Django Ninja Extra comes with several built-in permission classes:
1512

16-
### **Limitations of object level permissions**
17-
During the handling of a request, the `has_permission` method is automatically invoked for all the permissions specified
18-
in the permission list of the route function. However, `has_object_permission` is not triggered since
19-
it requires an object for permission validation. As a result of that, `has_object_permission` method for permissions are
20-
invoked when attempting to retrieve an object using the `get_object_or_exception`
21-
or `get_object_or_none` methods within the controller. Async versions of these methods are supported with `aget_object_or_exception` and `get_object_or_none`.
13+
### **1. AllowAny**
14+
Allows unrestricted access to any endpoint.
2215

23-
## **Custom permissions**
16+
```python
17+
from ninja_extra import permissions, api_controller, http_get
2418

25-
To implement a custom permission, override `BasePermission` and implement either, or both, of the following methods:
19+
@api_controller(permissions=[permissions.AllowAny])
20+
class PublicController:
21+
@http_get("/public")
22+
def public_endpoint(self):
23+
return {"message": "This endpoint is public"}
24+
```
2625

27-
.has_permission(self, request: HttpRequest, controller: "APIController")
28-
.has_object_permission(self, request: HttpRequest, controller: "APIController", obj: Any)
29-
Example
26+
### **2. IsAuthenticated**
27+
Only allows access to authenticated users.
3028

3129
```python
3230
from ninja_extra import permissions, api_controller, http_get
3331

34-
class ReadOnly(permissions.BasePermission):
35-
def has_permission(self, request, view):
36-
return request.method in permissions.SAFE_METHODS
37-
38-
@api_controller(permissions=[permissions.IsAuthenticated | ReadOnly])
39-
class PermissionController:
40-
@http_get('/must_be_authenticated', permissions=[permissions.IsAuthenticated])
41-
def must_be_authenticated(self, word: str):
42-
return dict(says=word)
32+
@api_controller(permissions=[permissions.IsAuthenticated])
33+
class PrivateController:
34+
@http_get("/profile")
35+
def get_profile(self, request):
36+
return {
37+
"username": request.user.username,
38+
"email": request.user.email
39+
}
4340
```
4441

42+
### **3. IsAuthenticatedOrReadOnly**
43+
Allows read-only access to unauthenticated users, but requires authentication for write operations.
4544

46-
## **Permissions Supported Operands**
47-
- & (and) eg: `permissions.IsAuthenticated & ReadOnly`
48-
- | (or) eg: `permissions.IsAuthenticated | ReadOnly`
49-
- ~ (not) eg: `~(permissions.IsAuthenticated & ReadOnly)`
45+
```python
46+
from ninja_extra import permissions, api_controller, http_get, http_post
5047

48+
@api_controller("/posts", permissions=[permissions.IsAuthenticatedOrReadOnly])
49+
class BlogController:
50+
@http_get("/") # Accessible to everyone
51+
def list_posts(self):
52+
return {"posts": ["Post 1", "Post 2"]}
53+
54+
@http_post("/") # Only accessible to authenticated users
55+
def create_post(self, request, title: str):
56+
return {"message": f"Post '{title}' created by {request.user.username}"}
57+
```
5158

52-
## **Using Permission Object in Controllers**
59+
### **4. IsAdminUser**
60+
Only allows access to admin users (users with `is_staff=True`).
5361

54-
The Ninja-Extra permission system provides flexibility in defining permissions either as an instance of a permission class or as a type.
62+
```python
63+
from ninja_extra import permissions, api_controller, http_get
64+
65+
@api_controller("/admin", permissions=[permissions.IsAdminUser])
66+
class AdminController:
67+
@http_get("/stats")
68+
def get_stats(self):
69+
return {"active_users": 100, "total_posts": 500}
70+
```
5571

56-
In the example below, the `ReadOnly` class is defined as a subclass of `permissions.BasePermission` and
57-
its instance is then passed to the `permissions` parameter within the `api_controller` decorator.
72+
## **Custom Permissions**
73+
74+
You can create custom permissions by subclassing `BasePermission`:
5875

5976
```python
60-
from ninja_extra import permissions, api_controller, ControllerBase
77+
from ninja_extra import permissions, api_controller, http_get
78+
from django.http import HttpRequest
79+
80+
class HasAPIKey(permissions.BasePermission):
81+
def has_permission(self, request: HttpRequest, controller):
82+
api_key = request.headers.get('X-API-Key')
83+
return api_key == 'your-secret-key'
84+
85+
@api_controller(permissions=[HasAPIKey])
86+
class APIKeyProtectedController:
87+
@http_get("/protected")
88+
def protected_endpoint(self):
89+
return {"message": "Access granted with valid API key"}
90+
```
6191

62-
class ReadOnly(permissions.BasePermission):
63-
def has_permission(self, request, view):
64-
return request.method in permissions.SAFE_METHODS
92+
### **Object-Level Permissions**
6593

66-
@api_controller(permissions=[permissions.IsAuthenticated | ReadOnly()])
67-
class SampleController(ControllerBase):
68-
pass
94+
For fine-grained control over individual objects:
95+
96+
```python
97+
from ninja_extra import permissions, api_controller, http_get
98+
from django.http import HttpRequest
99+
from django.shortcuts import get_object_or_404
100+
from .models import Post
101+
102+
class IsPostAuthor(permissions.BasePermission):
103+
def has_object_permission(self, request: HttpRequest, controller, obj: Post):
104+
return obj.author == request.user
105+
106+
@api_controller("/posts")
107+
class PostController:
108+
@http_get("/{post_id}", permissions=[permissions.IsAuthenticated & IsPostAuthor()])
109+
def get_post(self, request, post_id: int):
110+
# The has_object_permission method will be called automatically
111+
# when using get_object_or_exception or get_object_or_none
112+
post = self.get_object_or_exception(Post, id=post_id)
113+
return {"title": post.title, "content": post.content}
69114
```
70115

71-
In the provided example, the `UserWithPermission` class is utilized to assess different permissions for distinct controllers or route functions.
116+
## **Combining Permissions**
117+
118+
Django Ninja Extra supports combining permissions using logical operators:
119+
120+
- `&` (AND): Both permissions must pass
121+
- `|` (OR): At least one permission must pass
122+
- `~` (NOT): Inverts the permission
72123

73-
For instance:
74124
```python
75-
from ninja_extra import permissions, api_controller, ControllerBase, http_post, http_delete
125+
from ninja_extra import permissions, api_controller, http_get
126+
127+
class HasPremiumSubscription(permissions.BasePermission):
128+
def has_permission(self, request, controller):
129+
return request.user.has_perm('premium_subscription')
76130

77-
class UserWithPermission(permissions.BasePermission):
78-
def __init__(self, permission: str) -> None:
79-
self._permission = permission
131+
@api_controller("/content")
132+
class ContentController:
133+
@http_get("/basic", permissions=[permissions.IsAuthenticated | HasPremiumSubscription()])
134+
def basic_content(self):
135+
return {"content": "Basic content"}
80136

81-
def has_permission(self, request, view):
82-
return request.user.has_perm(self._permission)
137+
@http_get("/premium", permissions=[permissions.IsAuthenticated & HasPremiumSubscription()])
138+
def premium_content(self):
139+
return {"content": "Premium content"}
83140

141+
@http_get("/non-premium", permissions=[permissions.IsAuthenticated & ~HasPremiumSubscription()])
142+
def non_premium_content(self):
143+
return {"content": "Content for non-premium users"}
144+
```
145+
146+
## **Role-Based Permissions**
147+
148+
You can dynamically check different roles or permissions for a user using a single permission class. Here's an example:
84149

85-
@api_controller('/blog')
86-
class BlogController(ControllerBase):
87-
@http_post('/', permissions=[permissions.IsAuthenticated & UserWithPermission('blog.add')])
88-
def add_blog(self):
89-
pass
150+
```python
151+
from ninja_extra import permissions, api_controller, http_get, http_post, http_delete
152+
153+
class HasRole(permissions.BasePermission):
154+
def __init__(self, required_role: str):
155+
self.required_role = required_role
90156

91-
@http_delete('/', permissions=[permissions.IsAuthenticated & UserWithPermission('blog.delete')])
92-
def delete_blog(self):
93-
pass
157+
def has_permission(self, request, controller):
158+
return request.user.has_perm(self.required_role)
159+
160+
161+
@api_controller("/articles", permissions=[permissions.IsAuthenticated])
162+
class ArticleController:
163+
@http_get("/", permissions=[HasRole("articles.view")])
164+
def list_articles(self):
165+
return {"articles": ["Article 1", "Article 2"]}
166+
167+
@http_post("/", permissions=[HasRole("articles.add")])
168+
def create_article(self, title: str):
169+
return {"message": f"Article '{title}' created"}
170+
171+
@http_delete("/{id}", permissions=[HasRole("articles.delete")])
172+
def delete_article(self, id: int):
173+
return {"message": f"Article {id} deleted"}
94174
```
175+
In the above example, the `HasRole` permission class is used to check if the user has the `articles.view`, `articles.add` or `articles.delete` permission in different routes.
95176

96-
In this scenario, the `UserWithPermission` class is employed to verify whether the user possesses the `blog.add`
97-
permission to access the `add_blog` action and `blog.delete` permission for the `delete_blog` action within the `BlogController`.
98-
The permissions are explicitly configured for each route function, allowing fine-grained control over user access based on specific permissions.
177+
## **Interacting with Route Function Parameters and RouteContext**
99178

100-
## **AllowAny**
101-
The `AllowAny` permission class grants unrestricted access, irrespective of whether the request is authenticated or unauthenticated. While not mandatory, using this permission class is optional, as you can achieve the same outcome by employing an empty list or tuple for the permissions setting.
102-
However, specifying the `AllowAny` class can be beneficial as it explicitly communicates the intention of allowing unrestricted access.
179+
Sometimes you need to access route function parameters within your permission class before the actual route function is executed. Django Ninja Extra provides the `RouteContext` class to handle this scenario.
103180

104-
## **IsAuthenticated**
105-
The `IsAuthenticated` permission class denies permission to unauthenticated users and grants permission to authenticated users.
181+
By default, permission checks are performed before route function parameters are resolved. However, you can explicitly trigger parameter resolution using the `RouteContext` class.
106182

107-
This permission is appropriate if you intend to restrict API access solely to registered users.
183+
### **Basic Route Context Usage**
108184

109-
## **IsAdminUser**
110-
The `IsAdminUser` permission class denies permission to any user, except when `user.is_staff` is `True`,
111-
in which case permission is granted.
185+
```python
186+
from ninja_extra import permissions, api_controller, http_get, ControllerBase
187+
from django.http import HttpRequest
188+
189+
class IsOwner(permissions.BasePermission):
190+
def has_permission(self, request: HttpRequest, controller: ControllerBase):
191+
# Access route context and compute parameters
192+
controller.context.compute_route_parameters()
193+
194+
# Now you can access path and query parameters
195+
user_id = controller.context.kwargs.get('user_id')
196+
return request.user.id == user_id
197+
198+
@api_controller("/users")
199+
class UserController:
200+
@http_get("/{user_id}/profile", permissions=[IsOwner()])
201+
def get_user_profile(self, user_id: int):
202+
return {"message": f"Access granted to profile {user_id}"}
203+
```
112204

113-
This permission is suitable if you intend to restrict API access to a
114-
specific subset of trusted administrators.
205+
### **Advanced Route Context Examples**
206+
207+
Here are more complex examples showing different ways to use route context:
208+
209+
```python
210+
from ninja_extra import permissions, api_controller, http_get, http_post, ControllerBase
211+
from django.http import HttpRequest
212+
from typing import Optional
213+
214+
class HasTeamAccess(permissions.BasePermission):
215+
def has_permission(self, request: HttpRequest, controller: ControllerBase):
216+
# Compute parameters to access both path and query parameters
217+
controller.context.compute_route_parameters()
218+
219+
team_id = controller.context.kwargs.get('team_id')
220+
role = controller.context.kwargs.get('role', 'member') # Default to 'member'
221+
222+
return request.user.has_team_permission(team_id, role)
223+
224+
class HasProjectAccess(permissions.BasePermission):
225+
def __init__(self, required_role: str):
226+
self.required_role = required_role
227+
228+
def has_permission(self, request: HttpRequest, controller: ControllerBase):
229+
controller.context.compute_route_parameters()
230+
231+
# Access multiple parameters
232+
project_id = controller.context.kwargs.get('project_id')
233+
team_id = controller.context.kwargs.get('team_id')
234+
235+
return (
236+
request.user.is_authenticated and
237+
request.user.has_project_permission(project_id, team_id, self.required_role)
238+
)
239+
240+
@api_controller("/teams")
241+
class TeamProjectController:
242+
@http_get("/{team_id}/projects/{project_id}", permissions=[HasTeamAccess() & HasProjectAccess("viewer")])
243+
def get_project(self, team_id: int, project_id: int):
244+
return {"message": f"Access granted to project {project_id} in team {team_id}"}
245+
246+
@http_post("/{team_id}/projects", permissions=[HasTeamAccess() & HasProjectAccess("admin")])
247+
def create_project(self, team_id: int, name: str, description: Optional[str] = None):
248+
return {
249+
"message": f"Created project '{name}' in team {team_id}",
250+
"description": description
251+
}
252+
```
253+
254+
### **Working with Query Parameters**
255+
256+
You can also access query parameters in your permission classes:
257+
258+
```python
259+
from ninja_extra import permissions, api_controller, http_get, ControllerBase
260+
from django.http import HttpRequest
261+
262+
class HasFeatureAccess(permissions.BasePermission):
263+
def has_permission(self, request: HttpRequest, controller: ControllerBase):
264+
controller.context.compute_route_parameters()
265+
266+
# Access query parameters
267+
feature_name = controller.context.kwargs.get('feature')
268+
environment = controller.context.kwargs.get('env', 'production')
269+
270+
return request.user.has_feature_access(feature_name, environment)
271+
272+
@api_controller("/features")
273+
class FeatureController:
274+
@http_get("/check", permissions=[HasFeatureAccess()])
275+
def check_feature(self, feature: str, env: str = "production"):
276+
return {
277+
"feature": feature,
278+
"environment": env,
279+
"status": "enabled"
280+
}
281+
```
115282

116-
## **IsAuthenticatedOrReadOnly**
117-
The `IsAuthenticatedOrReadOnly` permission class allows authenticated users to perform any request.
118-
For unauthenticated users, requests will only be permitted if the method is one of the "safe" methods: GET, HEAD, or OPTIONS.
283+
### **Important Notes**
119284

120-
This permission is appropriate if you want your API to grant read permissions to anonymous users while restricting write permissions to authenticated users.
285+
1. Always call `compute_route_parameters()` before accessing route parameters in permission classes
286+
2. Route parameters are available in `controller.context.kwargs` after computation
287+
3. Both path parameters and query parameters are accessible
288+
4. You can combine route context-based permissions with other permission types
289+
5. Route parameters are computed only once, even if accessed by multiple permission classes
290+
6. The computation results are cached for the duration of the request

0 commit comments

Comments
 (0)