diff --git a/client/src/views/system/role.vue b/client/src/views/system/role.vue index 83a2d67..016bf24 100644 --- a/client/src/views/system/role.vue +++ b/client/src/views/system/role.vue @@ -137,20 +137,24 @@ export default { label: '全部' }, { - value: '本级', - label: '本级' + value: '自定义', + label: '自定义' + }, + { + value: '同级及以下', + label: '同级及以下' }, { value: '本级及以下', label: '本级及以下' }, { - value: '仅本人', - label: '仅本人' + value: '本级', + label: '本级' }, { - value: '自定义', - label: '自定义' + value: '仅本人', + label: '仅本人' } ] } diff --git a/client/src/views/system/user.vue b/client/src/views/system/user.vue index 3da65e2..e0526b3 100644 --- a/client/src/views/system/user.vue +++ b/client/src/views/system/user.vue @@ -226,7 +226,7 @@ export default { dept: null, avatar: "" }, - myHeaders: { Authorization: "JWT " + getToken() }, + myHeaders: { Authorization: "Bearer " + getToken() }, uploadUrl: uploadUrl(), userList: {count:0}, roles: [], diff --git a/server/apps/system/admin.py b/server/apps/system/admin.py index c83ce7f..73fc0dc 100644 --- a/server/apps/system/admin.py +++ b/server/apps/system/admin.py @@ -1,4 +1,5 @@ from django.contrib import admin +from simple_history.admin import SimpleHistoryAdmin from .models import User, Organization, Role, Permission, DictType, Dict # Register your models here. admin.site.register(User) @@ -6,4 +7,4 @@ admin.site.register(Organization) admin.site.register(Role) admin.site.register(Permission) admin.site.register(DictType) -admin.site.register(Dict) \ No newline at end of file +admin.site.register(Dict, SimpleHistoryAdmin) \ No newline at end of file diff --git a/server/apps/system/migrations/0005_historicaldict.py b/server/apps/system/migrations/0005_historicaldict.py new file mode 100644 index 0000000..4d3bed9 --- /dev/null +++ b/server/apps/system/migrations/0005_historicaldict.py @@ -0,0 +1,44 @@ +# Generated by Django 3.0.5 on 2020-05-26 02:57 + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion +import django.utils.timezone +import simple_history.models + + +class Migration(migrations.Migration): + + dependencies = [ + ('system', '0004_auto_20200520_0930'), + ] + + operations = [ + migrations.CreateModel( + name='HistoricalDict', + fields=[ + ('id', models.IntegerField(auto_created=True, blank=True, db_index=True, verbose_name='ID')), + ('create_time', models.DateTimeField(default=django.utils.timezone.now, help_text='创建时间', verbose_name='创建时间')), + ('update_time', models.DateTimeField(blank=True, editable=False, help_text='修改时间', verbose_name='修改时间')), + ('is_deleted', models.BooleanField(default=False, help_text='删除标记', verbose_name='删除标记')), + ('name', models.CharField(db_index=True, max_length=30, verbose_name='名称')), + ('code', models.CharField(blank=True, max_length=30, null=True, verbose_name='代码')), + ('desc', models.TextField(blank=True, null=True, verbose_name='描述')), + ('enabled', models.BooleanField(default=True, verbose_name='是否有效')), + ('sort', models.IntegerField(default=1, verbose_name='排序')), + ('history_id', models.AutoField(primary_key=True, serialize=False)), + ('history_date', models.DateTimeField()), + ('history_change_reason', models.CharField(max_length=100, null=True)), + ('history_type', models.CharField(choices=[('+', 'Created'), ('~', 'Changed'), ('-', 'Deleted')], max_length=1)), + ('history_user', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to=settings.AUTH_USER_MODEL)), + ('pid', models.ForeignKey(blank=True, db_constraint=False, null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='+', to='system.Dict', verbose_name='父')), + ('type', models.ForeignKey(blank=True, db_constraint=False, null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='+', to='system.DictType', verbose_name='类型')), + ], + options={ + 'verbose_name': 'historical 字典', + 'ordering': ('-history_date', '-history_id'), + 'get_latest_by': 'history_date', + }, + bases=(simple_history.models.HistoricalChanges, models.Model), + ), + ] diff --git a/server/apps/system/models.py b/server/apps/system/models.py index 9c81f16..38952b4 100644 --- a/server/apps/system/models.py +++ b/server/apps/system/models.py @@ -4,43 +4,9 @@ import django.utils.timezone as timezone from django.db.models.query import QuerySet from utils.model import SoftModel, BaseModel +from simple_history.models import HistoricalRecords -class DictType(SoftModel): - """ - 数据字典类型 - """ - name = models.CharField('名称', max_length=30) - code = models.CharField('代号', unique=True, max_length=30) - pid = models.ForeignKey('self', null=True, blank=True, - on_delete=models.SET_NULL, verbose_name='父') - class Meta: - verbose_name = '字典类型' - verbose_name_plural = verbose_name - - def __str__(self): - return self.name - -class Dict(SoftModel): - """ - 数据字典 - """ - name = models.CharField('名称', max_length=30, unique=True) - code = models.CharField('代码', max_length=30, null=True, blank=True) - desc = models.TextField('描述', blank=True, null=True) - enabled = models.BooleanField('是否有效', default=True) - type = models.ForeignKey( - DictType, on_delete=models.CASCADE, verbose_name='类型') - sort = models.IntegerField('排序', default=1) - pid = models.ForeignKey('self', null=True, blank=True, - on_delete=models.SET_NULL, verbose_name='父') - - class Meta: - verbose_name = '字典' - verbose_name_plural = verbose_name - - def __str__(self): - return self.name class Position(BaseModel): """ @@ -73,7 +39,8 @@ class Permission(SoftModel): sort = models.IntegerField('排序标记', default=1) pid = models.ForeignKey('self', null=True, blank=True, on_delete=models.SET_NULL, verbose_name='父') - method = models.CharField('方法/代号', max_length=50, unique=True, null=True, blank=True) + method = models.CharField('方法/代号', max_length=50, + unique=True, null=True, blank=True) def __str__(self): return self.name @@ -112,10 +79,11 @@ class Role(SoftModel): """ data_type_choices = ( ('全部', '全部'), - ('本级', '本级'), + ('自定义', '自定义'), + ('同级及以下', '同级及以下'), ('本级及以下', '本级及以下'), - ('仅本人', '仅本人'), - ('自定义', '自定义') + ('本级', '本级'), + ('仅本人', '仅本人') ) name = models.CharField('角色', max_length=32, unique=True) perms = models.ManyToManyField(Permission, blank=True, verbose_name='功能权限') @@ -157,12 +125,53 @@ class User(AbstractUser): def __str__(self): return self.username +class DictType(SoftModel): + """ + 数据字典类型 + """ + name = models.CharField('名称', max_length=30) + code = models.CharField('代号', unique=True, max_length=30) + pid = models.ForeignKey('self', null=True, blank=True, + on_delete=models.SET_NULL, verbose_name='父') + + class Meta: + verbose_name = '字典类型' + verbose_name_plural = verbose_name + + def __str__(self): + return self.name + + +class Dict(SoftModel): + """ + 数据字典 + """ + name = models.CharField('名称', max_length=30, unique=True) + code = models.CharField('代码', max_length=30, null=True, blank=True) + desc = models.TextField('描述', blank=True, null=True) + enabled = models.BooleanField('是否有效', default=True) + type = models.ForeignKey( + DictType, on_delete=models.CASCADE, verbose_name='类型') + sort = models.IntegerField('排序', default=1) + pid = models.ForeignKey('self', null=True, blank=True, + on_delete=models.SET_NULL, verbose_name='父') + history = HistoricalRecords() + + class Meta: + verbose_name = '字典' + verbose_name_plural = verbose_name + + def __str__(self): + return self.name + class CommonModel(SoftModel): """ 业务用基本表 """ - create_by = models.ForeignKey(User, null=True, blank=True, on_delete=models.SET_NULL, verbose_name='创建人', related_name='create_by') - update_by = models.ForeignKey(User, null=True, blank=True, on_delete=models.SET_NULL, verbose_name='最后编辑人', related_name='update_by') + create_by = models.ForeignKey( + User, null=True, blank=True, on_delete=models.SET_NULL, verbose_name='创建人', related_name='create_by') + update_by = models.ForeignKey( + User, null=True, blank=True, on_delete=models.SET_NULL, verbose_name='最后编辑人', related_name='update_by') class Meta: - abstract = True \ No newline at end of file + abstract = True diff --git a/server/apps/system/permission.py b/server/apps/system/permission.py index 4a5d321..7375a07 100644 --- a/server/apps/system/permission.py +++ b/server/apps/system/permission.py @@ -1,8 +1,8 @@ from django.core.cache import cache from rest_framework.permissions import BasePermission - +from utils.queryset import get_child_queryset2 from .models import Permission - +from django.db.models import Q def get_permission_list(user): """ @@ -54,3 +54,39 @@ class RbacPermission(BasePermission): return False else: return False + + def has_object_permission(self, request, view, obj): + """ + Return `True` if permission is granted, `False` otherwise. + """ + has_obj_perm(request.user, obj) + return True + +def has_obj_perm(user, obj): + """ + 数据权限控权 + 返回对象的是否可以操作 + 需要控数据权限的表需有belong_to, create_by, update_by字段(部门, 创建人, 编辑人) + 传入user, obj实例 + """ + roles = user.roles + data_range = roles.values_list('datas', flat=True) + if '全部' in data_range: + return True + elif '自定义' in data_range: + if roles.depts.exists(): + if obj.belong_to not in roles.depts: + return False + elif '同级及以下' in data_range: + if user.dept.pid: + belong_tos = get_child_queryset2(user.dept.pid) + if obj.belong_to not in belong_tos: + return False + elif '本级及以下' in data_range: + belong_tos = get_child_queryset2(user.dept) + if obj.belong_to not in belong_tos: + return False + elif '本级' in data_range: + if obj.belong_to is not user.dept: + return False + return True \ No newline at end of file diff --git a/server/apps/system/permission_data.py b/server/apps/system/permission_data.py new file mode 100644 index 0000000..45173e9 --- /dev/null +++ b/server/apps/system/permission_data.py @@ -0,0 +1,84 @@ +from django.db.models import Q +from django.db.models.query import QuerySet +from rest_framework.generics import GenericAPIView + +from utils.queryset import get_child_queryset2 + + +class RbacFilterSet(GenericAPIView): + """ + 数据权限控权返回的queryset + 在必须的View下继承 + 需要控数据权限的表需有belong_to, create_by, update_by字段(部门, 创建人, 编辑人) + """ + def get_queryset(self): + assert self.queryset is not None, ( + "'%s' should either include a `queryset` attribute, " + "or override the `get_queryset()` method." + % self.__class__.__name__ + ) + + queryset = self.queryset + if isinstance(queryset, QuerySet): + # Ensure queryset is re-evaluated on each request. + queryset = queryset.all() + if hasattr(queryset.model, 'belong_to'): + user = self.request.user + roles = user.roles + data_range = roles.values_list('datas', flat=True) + if '全部' in data_range: + return queryset + elif '自定义' in data_range: + if roles.depts.exists(): + queryset = queryset.filter(belong_to__in = roles.depts) + return queryset + elif '同级及以下' in data_range: + if user.dept.pid: + belong_tos = get_child_queryset2(user.dept.pid) + queryset = queryset.filter(belong_to__in = belong_tos) + return queryset + elif '本级及以下' in data_range: + belong_tos = get_child_queryset2(user.dept) + queryset = queryset.filter(belong_to__in = belong_tos) + return queryset + elif '本级' in data_range: + queryset = queryset.filter(belong_to = user.dept) + return queryset + elif '仅本人' in data_range: + queryset = queryset.filter(Q(create_by=user)|Q(update_by=user)) + return queryset + + return queryset + + +def rbac_filter_queryset(user, queryset): + """ + 数据权限控权返回的queryset方法 + 需要控数据权限的表需有belong_to, create_by, update_by字段(部门, 创建人, 编辑人) + 传入user实例,queryset + """ + roles = user.roles + data_range = roles.values_list('datas', flat=True) + if '全部' in data_range: + return queryset + elif '自定义' in data_range: + if roles.depts.exists(): + queryset = queryset.filter(belong_to__in = roles.depts) + return queryset + elif '同级及以下' in data_range: + if user.dept.pid: + belong_tos = get_child_queryset2(user.dept.pid) + queryset = queryset.filter(belong_to__in = belong_tos) + return queryset + elif '本级及以下' in data_range: + belong_tos = get_child_queryset2(user.dept) + queryset = queryset.filter(belong_to__in = belong_tos) + return queryset + elif '本级' in data_range: + queryset = queryset.filter(belong_to = user.dept) + return queryset + elif '仅本人' in data_range: + queryset = queryset.filter(Q(create_by=user)|Q(update_by=user)) + return queryset + return queryset + diff --git a/server/apps/system/views.py b/server/apps/system/views.py index 1dd0269..ff43b81 100644 --- a/server/apps/system/views.py +++ b/server/apps/system/views.py @@ -1,3 +1,5 @@ +import logging + from django.contrib.auth.hashers import check_password, make_password from django.core.cache import cache from django_filters.rest_framework import DjangoFilterBackend @@ -12,18 +14,20 @@ from rest_framework.permissions import IsAuthenticated from rest_framework.response import Response from rest_framework.views import APIView from rest_framework.viewsets import GenericViewSet, ModelViewSet - from rest_framework_simplejwt.tokens import RefreshToken -from utils.queryset import get_child_queryset -from .models import Organization, Permission, Position, Role, User, DictType, Dict +from utils.queryset import get_child_queryset2 + +from .filters import UserFilter +from .models import (Dict, DictType, Organization, Permission, Position, Role, + User) from .permission import RbacPermission, get_permission_list -from .serializers import (OrganizationSerializer, PermissionSerializer, +from .permission_data import RbacFilterSet +from .serializers import (DictSerializer, DictTypeSerializer, + OrganizationSerializer, PermissionSerializer, PositionSerializer, RoleSerializer, UserCreateSerializer, UserListSerializer, - UserModifySerializer, DictSerializer, DictTypeSerializer) -from .filters import UserFilter -import logging + UserModifySerializer) logger = logging.getLogger('log') # logger.info('请求成功! response_code:{};response_headers:{};response_body:{}'.format(response_code, response_headers, response_body[:251])) @@ -36,11 +40,13 @@ class LogoutView(APIView): def get(self, request, *args, **kwargs): # 可将token加入黑名单 return Response(status=status.HTTP_200_OK) + class DictTypeViewSet(ModelViewSet): """ 数据字典类型:增删改查 """ - perms_map = {'get': '*', 'post': 'dicttype_create', 'put': 'dicttype_update','delete': 'dicttype_delete'} + perms_map = {'get': '*', 'post': 'dicttype_create', + 'put': 'dicttype_update', 'delete': 'dicttype_delete'} queryset = DictType.objects.all() serializer_class = DictTypeSerializer pagination_class = None @@ -53,18 +59,21 @@ class DictViewSet(ModelViewSet): """ 数据字典:增删改查 """ - perms_map = {'get': '*', 'post': 'dict_create', 'put': 'dict_update','delete': 'dict_delete'} + perms_map = {'get': '*', 'post': 'dict_create', + 'put': 'dict_update', 'delete': 'dict_delete'} queryset = Dict.objects.all() serializer_class = DictSerializer search_fields = ['^name'] ordering_fields = ['id'] ordering = 'id' + class PositionViewSet(ModelViewSet): """ 岗位:增删改查 """ - perms_map = {'get': '*', 'post': 'position_create', 'put': 'position_update','delete': 'position_delete'} + perms_map = {'get': '*', 'post': 'position_create', + 'put': 'position_update', 'delete': 'position_delete'} queryset = Position.objects.all() serializer_class = PositionSerializer pagination_class = None @@ -74,7 +83,7 @@ class PositionViewSet(ModelViewSet): class TestView(APIView): - perms_map = {'get':'test_view'} # 单个API控权 + perms_map = {'get': 'test_view'} # 单个API控权 pass @@ -82,7 +91,8 @@ class PermissionViewSet(ModelViewSet): """ 权限:增删改查 """ - perms_map = {'get': '*', 'post': 'perm_create', 'put': 'perm_update','delete': 'perm_delete'} + perms_map = {'get': '*', 'post': 'perm_create', + 'put': 'perm_update', 'delete': 'perm_delete'} queryset = Position.objects.all() queryset = Permission.objects.all() serializer_class = PermissionSerializer @@ -96,11 +106,12 @@ class OrganizationViewSet(ModelViewSet): """ 组织机构:增删改查 """ - perms_map = {'get': '*', 'post': 'org_create', 'put': 'org_update','delete': 'org_delete'} + perms_map = {'get': '*', 'post': 'org_create', + 'put': 'org_update', 'delete': 'org_delete'} queryset = Organization.objects.all() serializer_class = OrganizationSerializer pagination_class = None - search_fields = ['^name','^method'] + search_fields = ['^name', '^method'] ordering_fields = ['id'] ordering = 'id' @@ -109,7 +120,8 @@ class RoleViewSet(ModelViewSet): """ 角色:增删改查 """ - perms_map = {'get': '*', 'post': 'role_create', 'put': 'role_update','delete': 'role_delete'} + perms_map = {'get': '*', 'post': 'role_create', + 'put': 'role_update', 'delete': 'role_delete'} queryset = Role.objects.all() serializer_class = RoleSerializer pagination_class = None @@ -122,7 +134,8 @@ class UserViewSet(ModelViewSet): """ 用户管理:增删改查 """ - perms_map = {'get': '*', 'post': 'user_create', 'put': 'user_update','delete': 'user_delete'} + perms_map = {'get': '*', 'post': 'user_create', + 'put': 'user_update', 'delete': 'user_delete'} queryset = User.objects.all().order_by('-id') serializer_class = UserListSerializer filterset_class = UserFilter @@ -134,7 +147,7 @@ class UserViewSet(ModelViewSet): queryset = self.get_serializer_class().setup_eager_loading(queryset) # 性能优化 dept = self.request.query_params.get('dept', None) # 该部门及其子部门所有员工 if dept is not None: - deptqueryset = get_child_queryset('rbac.Organization', dept) + deptqueryset = get_child_queryset2(Organization.objects.get(pk=dept)) queryset = queryset.filter(dept__in=deptqueryset) return queryset @@ -155,7 +168,7 @@ class UserViewSet(ModelViewSet): password = make_password('0000') serializer = self.get_serializer(data=request.data) serializer.is_valid(raise_exception=True) - serializer.save(password = password) + serializer.save(password=password) return Response(serializer.data) @action(methods=['put'], detail=True, permission_classes=[IsAuthenticated], @@ -179,7 +192,7 @@ class UserViewSet(ModelViewSet): return Response('旧密码错误!', status=status.HTTP_400_BAD_REQUEST) # perms_map={'get':'*'}, 自定义action控权 - @action(methods=['get'], detail=False, url_name='my_info', permission_classes=[IsAuthenticated]) + @action(methods=['get'], detail=False, url_name='my_info', permission_classes=[IsAuthenticated]) def info(self, request, pk=None): """ 初始化用户信息 diff --git a/server/server/settings.py b/server/server/settings.py index 68ebcf0..9f8445b 100644 --- a/server/server/settings.py +++ b/server/server/settings.py @@ -43,6 +43,7 @@ INSTALLED_APPS = [ "django_filters", 'apps.system', 'apps.crm', + 'simple_history' ] MIDDLEWARE = [ @@ -54,6 +55,7 @@ MIDDLEWARE = [ 'django.contrib.auth.middleware.AuthenticationMiddleware', 'django.contrib.messages.middleware.MessageMiddleware', 'django.middleware.clickjacking.XFrameOptionsMiddleware', + 'simple_history.middleware.HistoryRequestMiddleware', ] ROOT_URLCONF = 'server.urls' diff --git a/server/utils/queryset.py b/server/utils/queryset.py index dda4cef..e218c46 100644 --- a/server/utils/queryset.py +++ b/server/utils/queryset.py @@ -2,7 +2,7 @@ from django.db import models from django.apps import apps -def get_child_queryset_x(checkQueryset, fatherQueryset, noneQueryset, hasParent=True): +def get_child_queryset_u(checkQueryset, fatherQueryset, noneQueryset, hasParent=True): ''' 获取所有子集 查checkQueryset @@ -40,3 +40,21 @@ def get_child_queryset(name, pk, hasParent=True): queryset = queryset | child_queryset child_queryset = cls.objects.filter(pid__in=child_queryset) return queryset + +def get_child_queryset2(obj, hasParent=True): + ''' + 获取所有子集 + app.model实例 + 是否包含父默认True + ''' + cls = type(obj) + queryset = cls.objects.none() + fatherQueryset = cls.objects.filter(pk=obj.id) + if fatherQueryset.exists(): + if hasParent: + queryset = queryset | fatherQueryset + child_queryset = cls.objects.filter(pid=fatherQueryset.first()) + while child_queryset: + queryset = queryset | child_queryset + child_queryset = cls.objects.filter(pid__in=child_queryset) + return queryset \ No newline at end of file