Django 內建一套權限系統,主要提供 Django 應用程式的模型新增、修改、刪除、瀏覽權限管理,除此之外還有系統管理員、管理員與認證檢查,當然 Django 也有提供一定的彈性給我們自定義權限。
下面我們幫模型增加自定義的權限,並且在 GraphQL 上增加權限限制。
首先幫文章的模型增加一個發布文章的權限:
# server/app/blog/models.py
# ... 省略
class Post(BaseModel):
# ... 省略
class Meta:
verbose_name = "文章"
verbose_name_plural = "文章"
ordering = ["-created_at"]
+ permissions = [
+ ("publish_post", "Can publish post"),
+ ]
publish_post
:權限代碼名稱,對應權限資料表的codename
。Can publish post
:權限名稱,對應權限資料表的name
。幫模型增加權限後,需要用資料庫遷移的方式新增權限到資料庫中的權限表。
$ python manage.py makemigrations
Migrations for 'blog':
server/app/blog/migrations/0003_alter_post_options.py
- Change Meta options on post
$ python manage.py migrate
Operations to perform:
Apply all migrations: admin, auth, blog, contenttypes, sessions
Running migrations:
Applying blog.0003_alter_post_options... OK
下面是 Django 內建的權限資料表的查詢結果
然後使用 strawberry_django 的權限擴充工具來驗證權限:
# server/app/blog/graph/mutations.py
# ... 省略
from strawberry.file_uploads import Upload
from strawberry.types import Info
from strawberry.utils.str_converters import to_camel_case
+from strawberry_django.permissions import (
+ HasPerm,
+)
# ... 省略
@strawberry.type
class Mutation:
# ... 省略
@strawberry_django.input_mutation(
handle_django_errors=True,
+ extensions=[HasPerm("blog.publish_post")],
)
def publish_post(self, id: uuid.UUID) -> blog_types.Post: # noqa: A002
post = blog_models.Post.objects.get(pk=id)
if not post.published:
post.published = True
post.published_at = timezone.now()
post.save()
return typing.cast(blog_types.Post, post)
接著把 strawberry_django 設定進 Django 應用程式中:
# server/settings.py
INSTALLED_APPS = [
"django.contrib.admin",
"django.contrib.auth",
"django.contrib.contenttypes",
"django.contrib.sessions",
"django.contrib.messages",
"django.contrib.staticfiles",
"django_extensions",
+ "strawberry_django",
"server.app.blog",
"server.app.authentication",
]
然後就可以執行匯出 GraphQL Schema 的指令:
$ python manage.py export_schema server.schema:schema > schema.graphql
我們打開 schema.graphql 來看看:
directive @hasPerm(permissions: [PermDefinition!]!, any: Boolean! = true) repeatable on FIELD_DEFINITION
type Mutation {
updatePost(data: PostInputPartial!): UpdatePostPayload!
createPost(data: PostInput!): CreatePostResult!
publishPost(
"""Input data for `publishPost` mutation"""
input: PublishPostInput!
): PublishPostPayload! @hasPerm(permissions: [{app: "blog", permission: "publish_post"}], any: true)
uploadPostCoverImage(postId: UUID!, file: Upload!): UploadPostCoverImagePayload!
userRegister(data: UserRegisterInput!): UserRegisterPayload!
userEdit(data: UserEditInput!): UserEditPayload!
login(username: String!, password: String!): User!
logout: Boolean!
}
可以看到hasPerm
是一個 Schema 指令,publishPost
上也標註上hasPerm
這個指令。
接下來執行看看發布文章的 GraphQL API 來確認功能:
可以看到被擋了下來。
我們再到 Django admin 幫一個使用者增加發布文章的權限:
再次執行發布文章的 GraphQL API:
現在可以正常執行發布文章了。
如果希望能夠執行發布文章需要同時有瀏覽文章跟發布文章的權限,可以這麼做:
# server/app/blog/graph/mutations.py
# ... 省略
@strawberry.type
class Mutation:
# ... 省略
@strawberry_django.input_mutation(
handle_django_errors=True,
+ extensions=[HasPerm(["blog.publish_post", "blog.view_post"], any_perm=False)],
)
def publish_post(self, id: uuid.UUID) -> blog_types.Post: # noqa: A002
post = blog_models.Post.objects.get(pk=id)
if not post.published:
post.published = True
post.published_at = timezone.now()
post.save()
return typing.cast(blog_types.Post, post)
將HasPerm
中原本傳入一個 Django 權限字串的地方改成一個權限字串的列表,any_perm=False
代表要符合所有權限,預設True
是符合任意權限就通過。
那如果還需要有管理者權限才能執行,可以這麼做:
# server/app/blog/graph/mutations.py
# ... 省略
from strawberry_django.permissions import (
HasPerm,
+ IsStaff,
)
# ... 省略
@strawberry.type
class Mutation:
# ... 省略
@strawberry_django.input_mutation(
handle_django_errors=True,
extensions=[
HasPerm(["blog.publish_post", "blog.view_post"], any_perm=False),
+ IsStaff(),
],
)
def publish_post(self, id: uuid.UUID) -> blog_types.Post: # noqa: A002
post = blog_models.Post.objects.get(pk=id)
if not post.published:
post.published = True
post.published_at = timezone.now()
post.save()
return typing.cast(blog_types.Post, post)
可以到 Django admin 上把需要的權限都幫當前登入的帳號加上權限:
再接下來我試著使用 strawberry_django 提供的回傳資料的權限檢查。
幫文章的查詢加上有修改文章權限的人才能看到文章標籤:
# server/app/blog/graph/types.py
# ... 省略
from strawberry.types import Info
+from strawberry_django.permissions import (
+ HasRetvalPerm,
+)
# ... 省略
@strawberry_django.type(blog_models.Post)
class Post(relay.Node):
id: relay.NodeID[uuid.UUID] # noqa: A003
slug: str
author: auth_types.User
title: str
content: str
published_at: datetime.datetime | None
published: bool | None
+ tags: list["Tag"] = strawberry_django.field(
+ extensions=[HasRetvalPerm("blog.change_post")],
+ )
# ... 省略
# ... 省略
沒有權限的人就看不到任何標籤資料:
可以看到當前登入的使用沒有權限,所以回傳的標籤陣列都是空的。
其實 Strawberry 本身也有提供權限驗證的功能,下面我們實作一個文章作者本人才能更新文章的功能:
# server/app/blog/graph/mutations.py
# ... 省略
from django.core.exceptions import ValidationError
from django.utils import timezone
from strawberry.file_uploads import Upload
+from strawberry.permission import BasePermission
from strawberry.types import Info
from strawberry.utils.str_converters import to_camel_case
from strawberry_django.permissions import (
HasPerm,
IsStaff,
)
# ... 省略
+class IsAuthor(BasePermission):
+ message = "You must be the author of this post to perform this action."
+
+ def has_permission(self, source: typing.Any, info: Info, **kwargs) -> bool:
+ data = kwargs["data"]
+ post = data["id"].resolve_node_sync(info, ensure_type=blog_models.Post)
+ user = info.context.request.user
+ if post.author == user:
+ return True
+ return False
# ... 省略
@strawberry.type
class Mutation:
@strawberry_django.mutation(
handle_django_errors=True,
+ permission_classes=[IsAuthor],
)
def update_post(
self,
data: blog_types.PostInputPartial,
info: Info,
) -> blog_types.Post:
post = data.id.resolve_node_sync(info, ensure_type=blog_models.Post)
input_data = vars(data)
for field, value in input_data.items():
if field in ("id", "tags", "categories"):
continue
if value and hasattr(post, field):
setattr(post, field, value)
post.save()
- if data.tags is not None:
+ if data.tags and isinstance(data.tags, list):
tags = [
tag_id.resolve_node_sync(info, ensure_type=blog_models.Tag)
for tag_id in data.tags
]
post.tags.set(tags)
- if data.categories is not None:
+ if data.categories and isinstance(data.categories, list):
categories = [
category_id.resolve_node_sync(info, ensure_type=blog_models.Category)
for category_id in data.categories
]
post.categories.set(categories)
return typing.cast(blog_types.Post, post)
# ... 省略
# ... 省略
順便修個修改分類與標籤的 Bug。
這次修改內容可以參考 Git commit: