iT邦幫忙

2023 iThome 鐵人賽

DAY 24
0

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 內建的權限資料表的查詢結果

https://ithelp.ithome.com.tw/upload/images/20231009/20161957jYtltsXQrG.png

然後使用 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 來確認功能:

https://ithelp.ithome.com.tw/upload/images/20231009/20161957NnC8gPvqVi.png

可以看到被擋了下來。

我們再到 Django admin 幫一個使用者增加發布文章的權限:

https://ithelp.ithome.com.tw/upload/images/20231009/20161957X72bkFXnjZ.png

再次執行發布文章的 GraphQL API:

https://ithelp.ithome.com.tw/upload/images/20231009/20161957wuwXrR5pvr.png

現在可以正常執行發布文章了。

如果希望能夠執行發布文章需要同時有瀏覽文章跟發布文章的權限,可以這麼做:

# 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)

https://ithelp.ithome.com.tw/upload/images/20231009/20161957BmcaG5bKO8.png

可以到 Django admin 上把需要的權限都幫當前登入的帳號加上權限:

https://ithelp.ithome.com.tw/upload/images/20231009/20161957jDttohm5oY.png

再接下來我試著使用 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")],
+   )
		# ... 省略

# ... 省略

沒有權限的人就看不到任何標籤資料:

https://ithelp.ithome.com.tw/upload/images/20231009/20161957bvyEKSjyou.png

可以看到當前登入的使用沒有權限,所以回傳的標籤陣列都是空的。

其實 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。

https://ithelp.ithome.com.tw/upload/images/20231009/20161957oVZUw00NE5.png

這次修改內容可以參考 Git commit:

參考資料


上一篇
Day 23:Strawberry Django 認證
下一篇
Day 25:Strawberry Django Channels
系列文
Django 與 Strawberry GraphQL:探索現代 API 開發之路30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言