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"考務管理-考場管理"), ... )
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
爲毛把全部的權限都放在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))
這實際上是一個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
然而這個我沒什麼能夠說的,繼承下重寫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上面加上對應的權限,雖然徹底沒有問題。