Django REST framework的各類技巧——3.權限

django內置強大的權限系統,restframework也徹底支持,爲何不用呢?數據庫

Django REST framework的各類技巧【目錄索引】django

文檔

django permission的文檔
restframework permission的文檔segmentfault

權限的類型

  • 用戶是否有訪問某個api的權限api

  • 用戶對於相同的api不一樣權限看到不一樣的數據(其實一個filter)restful

  • 不一樣權限用戶對於api的訪問頻次,其餘限制等等app

  • 假刪除,各類級聯假刪除ide

基本講解

首先在django中,group以及user均可以有不少的permission,一個user會有他自身permission+全部隸屬group的permission。好比:user可能顯示的有3個permission,但他隸屬於3個組,每一個組有2個不一樣的權限,那麼他有3+2*3個權限。post

permission會在api的models.py種統一創建,在Meta中添加對應的permission而後跑migrate數據庫就會有新的權限。測試

因爲django對每個不一樣的Model都會創建幾個基本的權限,我會在api/models.py裏面單獨建一個ModulePermission的Model,沒有任何其餘屬性,就只有一個Meta class上面對應各類權限,這就是爲了syncdb用的,另外還一個緣由後面再說。ui

class ModulePermission(models.Model):
    class Meta:
        # 命名爲cms設計稿裏面對應 '菜單權限' 的地方, 例如用戶管理
        permissions = ( 
            ("information.announcement", u"資訊管理-通知公告"),
            ("information.examinfo", u"資訊管理-考試信息"),
            ("information.memberschool", u"資訊管理-會員學校"),
            ("school.school", u"學校管理-學校管理"),
            ("course.course", u"課程管理-課程管理"),
            ("student.student", u"學生管理-學生管理"),
            ("exam.exam", u"考務管理-考試管理"),
            ("exam.room", u"考務管理-考場管理"),
            ...
        )

api訪問權限的具體使用

permission_classes = (IsAuthenticated, ModulePermission)有一個ModulePermission,說明須要有對應module的權限才能夠訪問這個api,什麼權限捏?module_perms = ['course_course'],擁有了這個api的訪問權限後能夠看到這個。
功能是跟學校權限有關的東西,因此須要過濾學校數據 filter_backends = [SchoolPermissionFilterBackend,],這個後面再表。

class CourseDetailView(UnActiveModelMixin, DeleteForeignObjectRelModelMixin, RetrieveUpdateDestroyAPIView):

    filter_backends = [SchoolPermissionFilterBackend,]
    serializer_class = CourseSerializer
    permission_classes = (IsAuthenticated, ModulePermission)
    queryset = Course.objects.filter(is_active=True).order_by('-id')
    module_perms = ['course.course']

    def get_serializer_class(self):
        if self.request.method in SAFE_METHODS:
            return CourseFullMessageSerializer
        else:
            return CourseSerializer

ModulePermission的實現

爲毛把全部的權限都放在api裏面呢?就是爲了下面這個東西好寫,由於直接從user.get_all_permissions拿到的permission會帶模塊名,放在api.models下的東東都會叫api.xxx,看下面實現就懂了

# -*- coding: utf-8 -*-
from rest_framework.permissions import BasePermission, SAFE_METHODS

class ModulePermission(BasePermission):
    '''
    ModulePermission, 檢查一個用戶是否有對應某些module的權限

    APIView須要實現module_perms屬性:
        type: list
        example: ['information.information', 'school.school']

    權限說明:
        1. is_superuser有超級權限
        2. 權限列表請在api.models.Permission的class Meta中添加(請不要用數據庫直接添加)
        3. 只要用戶有module_perms的一條符合結果即認爲有權限, 因此module_perms是or的意思
    '''

    authenticated_users_only = True

    def has_perms(self, user, perms):
        user_perms = user.get_all_permissions()
        for perm in perms:
            if perm in user_perms:
                return True
        return False

    def get_module_perms(self, view):
        return ['api.{}'.format(perm) for perm in view.module_perms]

    def has_permission(self, request, view):
        '''
        is_superuser用戶有上帝權限,測試的時候注意帳號
        '''
        # Workaround to ensure DjangoModelPermissions are not applied
        # to the root view when using DefaultRouter.
        # is_superuser用戶有上帝權限
        if request.user.is_superuser:
            return True

        assert view.module_perms or not isinstance(view.module_perms, list), (
            u"view須要override module屬性,例如['information.information', 'school.school']"
        )

        if getattr(view, '_ignore_model_permissions', False):
            return True

        if hasattr(view, 'get_queryset'):
            queryset = view.get_queryset()
        else:
            queryset = getattr(view, 'queryset', None)

        assert queryset is not None, (
            'Cannot apply DjangoModelPermissions on a view that '
            'does not set `.queryset` or have a `.get_queryset()` method.'
        )

        return (
            request.user and
            (request.user.is_authenticated() or not self.authenticated_users_only) and
            self.has_perms(request.user, self.get_module_perms(view))
        )


class ModulePermissionOrReadOnly(ModulePermission):
    """
    The request is authenticated with ModulePermission, or is a read-only request.
    """

    def has_permission(self, request, view):
        return (request.method in SAFE_METHODS or super(ModulePermissionOrReadOnly, self).has_permission(request, view))

用戶對於相同的api不一樣權限看到不一樣的數據

這實際上是一個filter

# -*- coding: utf-8 -*-
from django.db import models
from django_extensions.db.models import TimeStampedModel
from django.db.models.signals import post_save
from .signals import create_permisson
 
 
class School(TimeStampedModel):
    MIDDLE_SCHOOL = 1
    COLLEGE = 2
    school_choices = (
        (MIDDLE_SCHOOL, u"中學"),
        (COLLEGE, u"高校")
    )
    category = models.SmallIntegerField(
        choices=school_choices, db_index=True, default=MIDDLE_SCHOOL)
    name = models.CharField(max_length=255, db_index=True)
    ...
    is_active = models.BooleanField(default=True, db_index=True)
 
    class Meta:
        # 建立學校時會建立學校權限, 默認有全部學校權限
        permissions = (
            ("schoolpermission__all", u"所有學校"),
        )
 
    def __unicode__(self):
        return self.name
 
 
post_save.connect(create_permisson, sender=School)

上面的signal,當學校對象建立修改時會對應更新permission裏面的記錄,保持一致性。

# -*- coding: utf-8 -*-
from django.contrib.auth.models import Permission
from django.contrib.contenttypes.models import ContentType


def create_permisson(sender, instance, created=False, *args, **kwargs):
    '''建立學校時會建立學校權限'''
    from .models import School
    school = instance
    content_type = ContentType.objects.get_for_model(School)
    codename = 'schoolpermission__{}'.format(school.id)
    name = school.name
    if created:
        permission = Permission.objects.create(codename=codename,
                                               name=school.name,
                                               content_type=content_type)
    else:
        if school.is_active:
            # 若是學校建在,有可能老師把學校名字改了,更新學學校名字
            permissions = Permission.objects.filter(codename=codename, content_type=content_type)
            if not permissions.exists():
                Permission.objects.create(codename=codename,
                                          name=school.name,
                                          content_type=content_type)
            else:
                permission = permissions[0]
                if permission.name != school.name:
                    permission.name = school.name
                    permission.save()
        else:
            Permission.objects.filter(codename=codename, content_type=content_type).delete()
    return instance

這樣只要跟學校有關的東西都必須有個叫作school的外鍵字段,在view中添加filter_backends = [SchoolPermissionFilterBackend,],便可根據學校權限過濾

# -*- coding: utf-8 -*-
from django.db.models.fields.related import ReverseSingleRelatedObjectDescriptor
 
class SchoolPermissionFilterBackend(object):
 
    def get_perms(self, user):
        '''獲取用戶對於學校的權限, return (True, [])
 
            超級管理員有全部用戶權限,
 
        '''
        ALL_SCHOOL_PERMISSION = (True, [])
        if user.is_superuser:
            has_all_school_permission = True
            return ALL_SCHOOL_PERMISSION
 
        permissions = user.get_all_permissions()
        perms = []
        for permission in permissions:
            if permission.startswith('school.schoolpermission__'):
                perm = permission.split('school.schoolpermission__')[-1]
                if perm == 'all':
                    return ALL_SCHOOL_PERMISSION
                perms.append(perm)
        return (False, perms)
 
    def filter_queryset(self, request, queryset, view):
        user = request.user
        model_cls = queryset.model
        has_all_school_permission, perms = self.get_perms(user)
        # 若是有全部學校權限則不過濾數據
        if has_all_school_permission:
            return queryset
        # 不然根據用戶擁有的學校權限過來學校有關數據
        if hasattr(model_cls, 'school') and isinstance(model_cls.school, ReverseSingleRelatedObjectDescriptor):
            queryset = queryset.filter(school__in=perms, school__is_active=True)
        return queryset

不一樣權限用戶對於api的訪問頻次,其餘限制

官方文檔

然而這個我沒什麼能夠說的,繼承下重寫allow_request便可。

細節

因爲對restful規範的理解,咱們知道若是定義了一個/terms/接口,那麼get請求是得到list,post是新建一個term,然而若是這兩個的權限不同GenericView應該怎麼寫呢?答案是重寫get_permissions方法(你應該怎麼知道的呢?看源碼)

class TermsView(ListCreateAPIView):

    serializer_class = TermSerializer
    permission_classes = (IsAuthenticated, ModulePermission)
    queryset = Term.objects.filter(is_active=True).order_by('-id')
    module_perms = ['sysadmin.term']

    def get_permissions(self):
        if self.request.method in SAFE_METHODS:
            return [IsAuthenticated()]
        else:
            return [permission() for permission in self.permission_classes]

刪除權限

需求是這樣的:

  • 假刪除

  • 若是沒有其餘東西對他有外鍵,則直接能夠刪

  • 若是其餘東西對他有了外鍵,則須要給提示(用戶肯定後能夠刪除)

首先看實現 注意繼承順序必定不能錯!!!
UnActiveModelMixin對應假刪除
DeleteForeignObjectRelModelMixin對應外鍵檢查
RetrieveUpdateDestroyAPIView是默認的rest的view,提供delete支持,主要是須要xxxDestroyAPIView

class UnActiveModelMixin(object):
    """ 
    刪除一個對象,並不真刪除,級聯將對應外鍵對象的is_active設置爲false,須要外鍵對象都有is_active字段.
    """
    def perform_destroy(self, instance):
        rel_fileds = [f for f in instance._meta.get_fields() if isinstance(f, ForeignObjectRel)]
        links = [f.get_accessor_name() for f in rel_fileds]

        for link in links:
            manager = getattr(instance, link, None)
            if not manager:
                continue
            if isinstance(manager, models.Model):
                if hasattr(manager, 'is_active') and manager.is_active:
                    manager.is_active = False
                    manager.save()
                    raise ForeignObjectRelDeleteError(u'{} 上有關聯數據'.format(link))
            else:
                if not manager.count():
                    continue
                try:
                    manager.model._meta.get_field('is_active')
                    manager.filter(is_active=True).update(is_active=False)
                except FieldDoesNotExist as ex: 
                    # 理論上,級聯刪除的model上面應該也有is_active字段,不然代碼邏輯應該有問題
                    logger.warn(ex)
                    raise ModelDontHaveIsActiveFiled(
                            '{}.{} 沒有is_active字段, 請檢查程序邏輯'.format(
                                manager.model.__module__,
                                manager.model.__class__.__name__
                    ))
        instance.is_active = False
        instance.save()



class DeleteForeignObjectRelModelMixin(object):
    '''刪除一個對象,若是他已有外鍵關聯則拋出異常'''
    @POST_OR_GET('force_delete', type='bool', default=False)
    def destroy(self, request, force_delete, *args, **kwargs):
        instance = self.get_object()
        if not force_delete:
            rel_fileds = [f for f in instance._meta.get_fields() if isinstance(f, ForeignObjectRel)]
            links = [f.get_accessor_name() for f in rel_fileds]
            for link in links:
                manager = getattr(instance, link, None)
                if not manager:
                    continue      
                # one to one
                if isinstance(manager, models.Model):
                    if hasattr(manager, 'is_active') and manager.is_active:
                        raise ForeignObjectRelDeleteError(u'{} 上有關聯數據'.format(link))
                else:
                    try:
                        manager.model._meta.get_field('is_active')
                        if manager.filter(is_active=True).count():
                            raise ForeignObjectRelDeleteError(u'{} 上有關聯數據'.format(link))
                    except FieldDoesNotExist as ex:
                        if manager.count():
                            raise ForeignObjectRelDeleteError(u'{} 上有關聯數據'.format(link))
        self.perform_destroy(instance)
        return Response(status=status.HTTP_204_NO_CONTENT)

哦怎麼用呢

例如你在admin裏面先新建一個group,而後把group上面勾上對應的權限,而後用戶的group那兒對應的把組加上便可(固然你能夠本身寫對應的代碼而不用admin)。不太建議直接在user上面加上對應的權限,雖然徹底沒有問題。

相關文章
相關標籤/搜索