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上面加上对应的权限,虽然彻底没有问题。