在不考慮資料驗證、敏感數據暴露與限制流量(這個Day20會展開)的角度,單以身份認證與權限設計的角度來說,我們現在的API有幾個問題:
在Django REST framework(DRF)提供了認證(Authentication)與權限(Permission)的機制來幫助我們在開發Web API時,能夠處理上述的相關問題。今天會先初步介紹DRF中認證與權限的區別,接著深入探討我們在權限上能做哪些設計與應用,以符合更成熟的設計方式
今日程式碼:https://github.com/class83108/drf_demo/tree/drf_permission
今日重點:
雖然我們常常會把認證與權限綁在一起說明,並且兩者的確息息相關,但是他們有不同的職責
我們的模型中,User模型就是認證的基礎,而Workspace與Document模型則是可以針對權限的設計進行展開
DRF API權限相關類別中有以下幾種
queryset
屬性或是 get_queryset()
才能使用class Task(models.Model):
...
class Meta:
permissions = [
("change_task_status", "Can change the status of tasks"),
("close_task", "Can remove a task by setting its status as closed"),
]
在視圖或是視圖集中直接設置屬性
from rest_framework.permissions import IsAuthenticated
class WorkspaceDetail(GenericAPIView):
queryset = Workspace.objects.all()
serializer_class = WorkspaceDetailSerializer
permission_classes = [IsAuthenticated]
....
當我們還沒登入時
而登入後就能調用API
至於登入的方式不是透過Django admin,我們可以設置DRF內置的用戶登入方式
# 根目錄urls.py
urlpatterns = [
...
path("api-auth/", include("rest_framework.urls")),
...
]
瀏覽器輸入 http://127.0.0.1:8000/api-auth/login/ ,就能看到登入畫面了
首先自定義權限可能有分兩種:
我們可以藉由繼承permission來自定義權限,例如我們希望只有身為Workspace的owner或是member才有該workspace的權限
# views.py
from rest_framework import permissions
class IsWorkspaceOwnerOrMembers(permissions.BasePermission):
def has_object_permission(self, request, view, obj):
return obj.owner == request.user or request.user in obj.members.all()
class WorkspaceDetail(GenericAPIView):
queryset = Workspace.objects.all()
serializer_class = WorkspaceDetailSerializer
permission_classes = [IsAuthenticated, IsWorkspaceOwnerOrMembers] # 進行配置
因為admin不是owner或是members所以不能訪問,改用owner後便能成功訪問
根據官方文檔,想要自定義權限就需改寫BasePermission
,同時可以選擇以下方法進行改寫
.has_permission(self, request, view)
.has_object_permission(self, request, view, obj)
class BasePermission(metaclass=BasePermissionMetaclass):
"""
A base class from which all permission classes should inherit.
"""
def has_permission(self, request, view):
"""
Return `True` if permission is granted, `False` otherwise.
"""
return True
def has_object_permission(self, request, view, obj):
"""
Return `True` if permission is granted, `False` otherwise.
"""
return True
需要注意的是,has_object_permission是等到has_permission有通過才會執行該方法
也就是即使我們沒有在這邊納入IsAuthenticated,依然要登入才能使用
class WorkspaceDetail(GenericAPIView):
queryset = Workspace.objects.all()
serializer_class = WorkspaceDetailSerializer
permission_classes = [IsWorkspaceOwnerOrMembers]
我們先前是在視圖中進行用戶是否具備某些權限的判斷,但這樣的缺點是管理權限的邏輯分散在不同的視圖之中,我們能透過角色存取控制的方式來更好的管理權限
所謂的RBAC不是對每個使用者分發權限,而是設定好角色並且設置相對應的權線。例如公司中,身為主管的角色就可能具備修改的權限,但是基層員工就只有只讀的權限
使用RBAC時需要注意以下幾點:
而RBAC也有以下缺點:
了解RBAC後,我們要怎麼將這個概念套用在DRF的權限設計上呢?
我們先訂制我們的角色:
根據這些條件去重新定義我們的model,然後進行遷移
User = get_user_model()
class Workspace(models.Model):
name = models.CharField(max_length=100)
owner = models.ForeignKey(
User, on_delete=models.CASCADE, related_name="owned_workspaces"
)
# members = models.ManyToManyField(User, related_name="workspaces")
created_at = models.DateTimeField(auto_now_add=True)
def __str__(self):
return self.name
class WorkspaceMember(models.Model):
ROLE_CHOICES = (
("OWNER", "Owner"),
("EDITOR", "Editor"),
("READER", "Reader"),
)
workspace = models.ForeignKey(
Workspace, on_delete=models.CASCADE, related_name="memberships"
)
user = models.ForeignKey(
User, on_delete=models.CASCADE, related_name="workspace_memberships"
)
role = models.CharField(max_length=10, choices=ROLE_CHOICES, default="READER")
def __str__(self):
return f"{self.user.username} - {self.workspace.name} ({self.role})"
class Document(models.Model):
title = models.CharField(max_length=200)
content = models.TextField()
workspace = models.ForeignKey(
Workspace, on_delete=models.CASCADE, related_name="documents"
)
created_by = models.ForeignKey(
User, on_delete=models.CASCADE, related_name="created_documents"
)
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
def __str__(self):
return self.title
主要的改變:
我們需要對Workspace模型設置一些自定義的方法來檢查權限
class Workspace(models.Model):
name = models.CharField(max_length=100)
owner = models.ForeignKey(
User, on_delete=models.CASCADE, related_name="owned_workspaces"
)
# members = models.ManyToManyField(User, related_name="workspaces")
created_at = models.DateTimeField(auto_now_add=True)
def __str__(self):
return self.name
def get_user_role(self, user):
"""
Return the role of the user in the workspace.
"""
if user == self.owner:
return "OWNER"
membership = self.memberships.filter(user=user).first()
return membership.role if membership else None
def can_read(self, user):
return self.get_user_role(user) in ["OWNER", "EDITOR", "READER"]
def can_edit(self, user):
return self.get_user_role(user) in ["OWNER", "EDITOR"]
def can_delete(self, user):
return self.get_user_role(user) == "OWNER"
def can_manage_members(self, user):
return self.get_user_role(user) == "OWNER"
def add_member(self, user, role="READER"):
"""
Add a user to the workspace with the given role.
"""
if role not in ["OWNER", "EDITOR", "READER"]:
raise ValueError("Invalid role")
WorkspaceMember.objects.create(workspace=self, user=user, role=role)
def remove_member(self, user):
self.memberships.filter(user=user).delete()
def change_member_role(self, user, new_role):
if new_role not in ["EDITOR", "READER"]:
raise ValueError("Invalid role")
membership = self.memberships.get(user=user)
membership.role = new_role
membership.save()
然後重新定義序列化器
class WorkspaceMemberSerializer(serializers.ModelSerializer):
username = serializers.CharField(source="user.username", read_only=True)
class Meta:
model = WorkspaceMember
fields = ["user", "username", "role"]
class WorkspaceSerializer(serializers.ModelSerializer):
owner = serializers.CharField(source="owner.username", read_only=True)
members = WorkspaceMemberSerializer(source="memberships", many=True, read_only=True)
user_role = serializers.SerializerMethodField()
class Meta:
model = Workspace
fields = ["id", "name", "owner", "members", "user_role", "created_at"]
def get_user_role(self, obj):
user = self.context["request"].user
return obj.get_user_role(user)
class DocumentSerializer(serializers.ModelSerializer):
created_by = serializers.CharField(source="created_by.username", read_only=True)
class Meta:
model = Document
fields = [
"id",
"title",
"content",
"workspace",
"created_by",
"created_at",
"updated_at",
]
WorkspaceMemberSerializer
就是針對WorkspaceMember模型的序列化器
DocumentSerializer
則是單純對應Document模型
WorkspaceSerializer
對應Workspace模型,但是我們自定義get_user_role
方法,根據上下文來判斷客戶端的角色是什麼
最後我們在視圖集合中進行使用
from rest_framework import viewsets, permissions
from rest_framework.decorators import action
from rest_framework.response import Response
from django.contrib.auth.models import User
from .models import Workspace
from .serializers import WorkspaceSerializer
class WorkspaceViewSet(viewsets.ModelViewSet):
queryset = Workspace.objects.all()
serializer_class = WorkspaceSerializer
permission_classes = [permissions.IsAuthenticated]
def get_queryset(self):
# 用戶只能看到他們是成員的工作區
return Workspace.objects.filter(memberships__user=self.request.user)
def perform_create(self, serializer):
workspace = serializer.save(owner=self.request.user)
workspace.add_member(self.request.user, role="OWNER")
def perform_destroy(self, instance):
if instance.can_delete(self.request.user):
instance.delete()
else:
raise permissions.PermissionDenied(
"You don't have permission to delete this workspace."
)
@action(detail=True, methods=["post"])
def add_member(self, request, pk=None):
workspace = self.get_object()
if not workspace.can_manage_members(request.user):
raise permissions.PermissionDenied(
"You don't have permission to add members."
)
user_id = request.data.get("user_id")
role = request.data.get("role", "READER")
user = User.objects.get(pk=user_id)
workspace.add_member(user, role)
return Response({"status": "member added"})
@action(detail=True, methods=["post"])
def change_member_role(self, request, pk=None):
workspace = self.get_object()
if not workspace.can_manage_members(request.user):
raise permissions.PermissionDenied(
"You don't have permission to change member roles."
)
user_id = request.data.get("user_id")
new_role = request.data.get("role")
user = User.objects.get(pk=user_id)
workspace.change_member_role(user, new_role)
return Response({"status": "member role changed"})
@action(detail=True, methods=["post"])
def remove_member(self, request, pk=None):
workspace = self.get_object()
if not workspace.can_manage_members(request.user):
raise permissions.PermissionDenied(
"You don't have permission to remove members."
)
user_id = request.data.get("user_id")
user = User.objects.get(pk=user_id)
workspace.remove_member(user)
return Response({"status": "member removed"})
permission_classes
中定permissions.IsAuthenticated
,確保使用API的為註冊後的用戶get_queryset
中Workspace.objects.filter(memberships__user=self.request.user)
,讓用戶只能看到他們屬於memberships的workspaceperform_create
時將用戶註冊成owner欄位,並且將角色:owner賦予給該用戶perform_destroy
只有具備owner的member可以調用方法最後配置好路由
from rest_framework.routers import DefaultRouter
from .views import WorkspaceViewSet
router = DefaultRouter()
router.register(r"workspaces", WorkspaceViewSet)
urlpatterns = [
path("", include(router.urls)),
]
我們嘗試進行相關的操作,我們使用bob用戶建立新的workspace,然後分發角色給alice,並且將alice的角色修改成editor
看出來我們新的權限設計在運行上沒有什麼太大的問題,並且透過角色的分配,對於不同用戶的權限分配有了更明確且清晰的劃分