Django-xadmin+rule对象级权限的实现方式


Posted in Python onMarch 30, 2020

1. 需求vs现状

1.1 需求

要求做一个ERP后台辅助管理的程序,有以下几项基本要求:

1. 基本的增删改查功能

2. 基于对象的权限控制(如:系统用户分为平台运营人员和商家用户,商家用户小A只能查看编辑所属商家记录,而管理员可以纵览全局)

3. 数据库记录导入导出(xsl, json等),并且拥有对象级的权限控制(如:小A不能导出小B公司的信息,更不能导入小B公司信息进行更新和新增)

1.2 现状

实现需求1:Django-admin让我们能够很方便的实现一个管理后台程序。django-xadmin则在拥有admin基本功能的基础上增加了更为丰富的功能、界面也更加漂亮。类似还有django-suit等,本文使用xadmin(功能更丰富);

实现需求2:django-admin,以及xadmin都只有基于model级的权限控制机制,需要自己扩展或者使用开源解决方案,如django-guardian,django-rules,本文结合django-rules实现了该功能;

实现需求3:xadmin虽然自带导出功能,但是导入功能没有实现,django自带后台结合django-import-export可以很容易实现,但是xadmin并不直接兼容,只有通过xadmin的插件机制实现。

2. 功能实现

本节主要展示对象级权限功能实现。django工程、xadmin替换原生admin的设置,请参照官方文档。

2.1 安装并配置rules

pip安装:pip install django-rules

配置settings.py

# settings.py
INSTALLED_APPS = (
  # ...
  'rules',
)
AUTHENTICATION_BACKENDS = (
  'rules.permissions.ObjectPermissionBackend',
  'django.contrib.auth.backends.ModelBackend',
)

2.2 建立model

新增CompanyUser模型表示商家账户(即对django自带user模块进行扩展,使每个账号绑定自己的公司码),新增Customer模型表示商家的客户信息并包含公司码字段,商家账号只能查看、编辑、导入、导出公司码一致的商家客户信息

# model.py
class CompanyUser(models.Model):
  user = models.OneToOneField(User, verbose_name='用户名')
  is_taixiang_admin = models.BooleanField('是否运营人员', default=False)
  company_code = models.CharField('公司码', max_length=20, blank=True, default='')

  def __unicode__(self):
    return '%s' % self.user

  class Meta:
    verbose_name = '导入账号'
    verbose_name_plural = verbose_name

class Customer(models.Model):
  name = models.CharField('客户姓名', max_length=50)
  phone = models.CharField('客户电话', max_length=12)
  type_choice = ((1, '普通'), (2, '批发'), (3, 'VIP'))
  creator = models.ForeignKey(settings.AUTH_USER_MODEL, verbose_name='创建人', blank=True, null=True)
  company_code = models.CharField('公司码', max_length=20, blank=True, null=True)

  def __unicode__(self):
    return '%s-%s-%s' % (self.company_code, self.name, self.phone1)

  class Meta:
    permissions = (
      ("simulate_import_customer", "允许模拟导入客户"),
      ("import_customer", "允许导入客户至商家系统"),
            )
    verbose_name = "客户"
    verbose_name_plural = verbose_name

2.2 使用rule

在model统计目录新增rules.py,配置该app相关的对象权限

引用rules

# rules.py
# On Python 2, you must also add the following to the top of your rules.py file, or you'll get import errors trying to import django-rules itself
from __future__ import absolute_import

import rules

# 使用修饰符@rules.predicate自定义predicates(判断),返回True表示有权限,False表示无权限

# Predicates

@rules.predicate
def is_colleague(user, entry):
  if not entry or not hasattr(user, 'companyuser'):
    return False
  return entry.company_code == user.companyuser.company_code


@rules.predicate
def is_taixiang_admin(user):
  if not hasattr(user, 'companyuser'):
    return False
  return user.companyuser.is_taixiang_admin

# predicates间可以进行运算
is_colleague_or_taixiang_admin = is_colleague | is_taixiang_admin | rules.is_superuser

# 设置Rules

rules.add_rule('can_view_customer', is_colleague_or_taixiang_admin)
rules.add_rule('can_delete_customer', is_colleague_or_taixiang_admin)
rules.add_perm('can_change_customer', is_colleague_or_taixiang_admin)

# 设置Permissions

rules.add_perm('data_import.view_customer', is_colleague_or_taixiang_admin)
rules.add_perm('data_import.delete_customer', is_colleague_or_taixiang_admin)
rules.add_perm('data_import.add_customer', is_colleague_or_taixiang_admin)
rules.add_perm('data_import.change_customer', is_colleague_or_taixiang_admin)

2.3 admin.py以及adminx.py设置

如果使用原生的django-admin,admin.py做如下设置:

# admin.py
from __future__ import absolute_import

from django.contrib import admin
from rules.contrib.admin import ObjectPermissionsModelAdmin
from .models import Customer

# ModelAdmin class继承ObjectPermissionsModelAdmin即可
class CustomerAdmin(ObjectPermissionsModelAdmin):
  pass

admin.site.register(Customer, CustomerAdmin)

使用xadmin,由于ObjectPermissionsModelAdmin无法直接使用,故参照源码重写has_change_permission和has_delete_permission方法即可。

注意:必须引用rules文件,权限规则才会生效,对于xadmin,添加

from .rules import *即可

# adminx.py
class CustomerAdmin(object):
  def has_change_permission(self, obj=None):
    codename = get_permission_codename('change', self.opts)
    return self.user.has_perm('%s.%s' % (self.app_label, codename), obj)

  def has_delete_permission(self, obj=None):
    codename = get_permission_codename('delete', self.opts)
    return self.user.has_perm('%s.%s' % (self.app_label, codename), obj)

  # 重写queryset()或者get_list_display(),list view的权限也做到了对象级隔离
  def queryset(self):
    qs = super(CustomerAdmin, self).queryset()
    if self.request.user.is_superuser or is_taixiang_admin(self.request.user):
      return qs
    try:
      return qs.filter(company_code=self.request.user.companyuser.company_code)
    except AttributeError:
      return None

class CompanyUserAdmin(object):
  pass

xadmin.sites.site.register(Customer, CustomerAdmin)
xadmin.sites.site.register(CompanyUser, CompanyUserAdmin)

2.4 效果展示

CompanyUser设置:

Django-xadmin+rule对象级权限的实现方式

商家账号只有所属公司信息权限

Django-xadmin+rule对象级权限的实现方式

运营人员拥有所有记录权限

Django-xadmin+rule对象级权限的实现方式

补充知识:django 扩展自带权限,使其支持对象权限

扩展django 自带权限

说明

在不重写 自带权限的基础上,完成支持对象权限,适用于小型项目。

欢迎提出修改意见

软件支持

jsonfield

数据库

新建3个表

from django.db import models
from django.contrib.auth.models import AbstractUser, Group ,User
 
from jsonfield import JSONField
 
class Request(models.Model):
  request = models.CharField(max_length=16, verbose_name='请求类型(大写)')
 
  class Meta:
    db_table = "request"
    verbose_name = "请求类型"
    verbose_name_plural = verbose_name
 
  def __str__(self):
    return self.request
 
class RolePermission(models.Model):
  role = models.CharField(max_length=32, verbose_name='角色组')
  table = models.CharField(max_length=32, verbose_name='表名字')
  request = models.ManyToManyField(Request, verbose_name='请求', related_name='re', )
  permission = JSONField(max_length=1024, verbose_name='权限条件')
 
  class Meta:
    db_table = "role_permission"
    verbose_name = "角色组权限"
    verbose_name_plural = verbose_name
 
  def __str__(self):
    return self.role
 
class Role(models.Model):
  group = models.ForeignKey(Group, verbose_name='用户组', on_delete=models.CASCADE)
  roles = models.ManyToManyField(RolePermission, verbose_name='角色组权限', blank=True,related_name='roles' )
 
  class Meta:
    db_table = "role"
    verbose_name = "角色组关系"
    verbose_name_plural = verbose_name
 
  def __str__(self):
    return self.group.name
system/models
Role         角色组关系  : 系统用户组 <--> 角色组权限
Request       请求类型   : GET ,POST
RolePermission   角色组权限  : 角色 表名字 请求 权限条件(JSON类型)

重点为 RolePermission 表。

例子

以常见的资产 asset 为例

表名字 asset 字段 groups (分组 为 dev,ops)
权限划分
新建用户 hequan
新建组 dev

在Request 表 添加

GET (代表只读)
POST (代表更新 删除)

在RolePermission 添加

角色 asset-dev只读
表名字assset
请求 GET
权限条件 {"groups":'dev'}

在Role 表中 添加

系统用户组 dev
角色组权限 asset-dev只读

权限验证代码

import json
from system.models import Role
from functools import wraps
from django.shortcuts import HttpResponse
 
def role_permission_get_list(function):
  """
  列表页面 控制权限
  :param function:
  :return:
  """
 
  @wraps(function)
  def wrapped(self):
    user = self.request.user
    groups = [x['name'] for x in self.request.user.groups.values()]
    request_type = self.request.method
    model = str(self.model._meta).split(".")[1]
 
    filter_dict = {}
    not_list = ['page', 'order_by', 'csrfmiddlewaretoken']
    for k, v in dict(self.request.GET).items():
      if [i for i in v if i != ''] and (k not in not_list):
        if '__in' in k:
          filter_dict[k] = v
        else:
          filter_dict[k] = v[0]
 
    if not user.is_superuser:
      role_groups = Role.objects.filter(group__name__in=groups).values_list('roles__table',
                                         'roles__request__request',
                                         'roles__permission')
 
      permission_dict = {}
      for i in role_groups:
        if i[0] == model and i[1] == request_type:
          permission_dict = json.loads(i[2])
 
      if permission_dict:
        if filter_dict:
          for k, v in permission_dict.items():
            if '__in' in k:
              k1 = k.replace('__in', '')
            if '__gt' in k:
              k1 = k.replace('__gt', '')
            if '__lt' in k:
              k1 = k.replace('__lt', '')
            else:
              k1 = k
            if k1 in list(filter_dict.keys()):
              del filter_dict[k1]
 
          if filter_dict:
            filter_dict.update(**permission_dict)
          else:
            print('查询条件处理后为空,默认权限')
            filter_dict = permission_dict
        else:
          print('查询条件为空,默认权限')
          filter_dict = permission_dict
      else:
        print('没有权限')
        filter_dict = {'id': -1}
 
    self.filter_dict = filter_dict
    result = function(self)
    return result
 
  return wrapped
 
def role_permission_detail(function):
  """
  详情页面 控制权限
  :param function:
  :return:
  """
 
  @wraps(function)
  def wrapped(self, request, *args, **kwargs):
    user = self.request.user
 
    if not user.is_superuser:
      groups = [x['name'] for x in self.request.user.groups.values()]
      request_type = self.request.method
      model = str(self.model._meta).split(".")[1]
      pk = self.kwargs.get(self.pk_url_kwarg, None)
 
      role_groups = Role.objects.filter(group__name__in=groups).values_list('roles__table',
                                         'roles__request__request',
                                         'roles__permission')
 
      permission_dict = {}
      for i in role_groups:
        if i[0] == model and i[1] == request_type:
          permission_dict = json.loads(i[2])
 
      permission_dict['id'] = pk
      obj = self.model.objects.filter(**permission_dict).count()
      if not obj:
        return HttpResponse(status=403)
 
    result = function(self, request, *args, **kwargs)
    return result
 
  return wrapped
 
def role_permission_update_delete(function):
  """
  详情页面 控制权限
  :param function:
  :return:
  """
 
  @wraps(function)
  def wrapped(self, request):
    user = self.request.user
    if not user.is_superuser:
 
      groups = [x['name'] for x in self.request.user.groups.values()]
      request_type = self.request.method
      model = str(self.model._meta).split(".")[1]
      pk = self.request.POST.get('nid', None)
 
      role_groups = Role.objects.filter(group__name__in=groups).values_list('roles__table',
                                         'roles__request__request',
                                         'roles__permission')
 
      permission_dict = {}
      for i in role_groups:
        if i[0] == model and i[1] == request_type:
          permission_dict = json.loads(i[2])
 
      permission_dict['id'] = pk
      obj = self.model.objects.filter(**permission_dict).count()
      if not obj:
        ret = {'status': None, 'error': "没有权限,拒绝", 'msg': 'Without permission, rejected'}
        return HttpResponse(json.dumps(ret))
 
    result = function(self, request)
    return result
 
  return wrapped

CBV 例子

省略部分代码

class AssetListAll(LoginRequiredMixin, ListView):
  model = Ecs
 
  @role_permission_get_list
  def get_queryset(self):
    filter_dict = self.filter_dict
    self.queryset = self.model.objects.filter(**filter_dict)
    return self.queryset
class AssetChange(LoginRequiredMixin, UpdateView):
  model = Ecs
 
  @role_permission_detail
  def dispatch(self, request, *args, **kwargs):
    return super().dispatch(request, *args, **kwargs)
 
  @role_permission_update_delete
  def form_valid(self, form):
    self.object = form.save()
    return super().form_valid(form)
class AssetDetail(LoginRequiredMixin, DetailView):
  model = Ecs
 
  @role_permission_detail
  def dispatch(self, request, *args, **kwargs):
    return super().dispatch(request, *args, **kwargs)
class AssetDel(LoginRequiredMixin, View):
  model = Ecs
 
  @role_permission_update_delete
  def post(self, request):
    pass

以上这篇Django-xadmin+rule对象级权限的实现方式就是小编分享给大家的全部内容了,希望能给大家一个参考,也希望大家多多支持三水点靠木。

Python 相关文章推荐
用Python写的图片蜘蛛人代码
Aug 27 Python
python实现定时同步本机与北京时间的方法
Mar 24 Python
Python使用scrapy采集数据时为每个请求随机分配user-agent的方法
Apr 08 Python
Python设置Socket代理及实现远程摄像头控制的例子
Nov 13 Python
Python学习小技巧总结
Jun 10 Python
Linux下python制作名片示例
Jul 20 Python
解决Pycharm出现的部分快捷键无效问题
Oct 22 Python
如何不用安装python就能在.NET里调用Python库
Jul 12 Python
基于python 微信小程序之获取已存在模板消息列表
Aug 05 Python
基于opencv实现简单画板功能
Aug 02 Python
用60行代码实现Python自动抢微信红包
Feb 04 Python
Python requests库参数提交的注意事项总结
Mar 29 Python
Python3 hashlib密码散列算法原理详解
Mar 30 #Python
django xadmin action兼容自定义model权限教程
Mar 30 #Python
使用Django xadmin 实现修改时间选择器为不可输入状态
Mar 30 #Python
Django admin 实现search_fields精确查询实例
Mar 30 #Python
Django模型中字段属性choice使用说明
Mar 30 #Python
Django+python服务器部署与环境部署教程详解
Mar 30 #Python
Python GUI编程学习笔记之tkinter中messagebox、filedialog控件用法详解
Mar 30 #Python
You might like
IIS7.X配置PHP运行环境小结
2011/06/09 PHP
使用配置类定义Codeigniter全局变量
2014/06/12 PHP
php操作redis缓存方法分享
2015/06/03 PHP
php基于双向循环队列实现历史记录的前进后退等功能
2015/08/08 PHP
javascript 点击整页变灰的效果(可做退出效果)。
2008/01/09 Javascript
jQuery操作Select的Option上下移动及移除添加等等
2013/11/18 Javascript
使用POST方式弹出窗口的两种方法示例介绍
2014/01/29 Javascript
jquery使用正则表达式验证email地址的方法
2015/01/22 Javascript
Jquery操作cookie记住用户名
2016/03/29 Javascript
精彩的Bootstrap案例分享 重点在注释!(选项卡、栅格布局)
2016/07/01 Javascript
vue v-model表单控件绑定详解
2017/05/17 Javascript
详解node nvm进行node多版本管理
2017/10/21 Javascript
微信小程序利用canvas 绘制幸运大转盘功能
2018/07/06 Javascript
vue使用v-for实现hover点击效果
2018/09/29 Javascript
JavaScript查看代码运行效率console.time()与console.timeEnd()用法
2019/01/18 Javascript
Vue 实现从小到大的横向滑动效果详解
2019/10/16 Javascript
JavaScript中的this基本问题实例小结
2020/03/09 Javascript
解决Vue + Echarts 使用markLine标线(precision精度问题)
2020/07/20 Javascript
Python采用raw_input读取输入值的方法
2014/08/18 Python
Python实现将Excel转换为json的方法示例
2017/08/05 Python
对Python w和w+权限的区别详解
2019/01/23 Python
python高斯分布概率密度函数的使用详解
2019/07/10 Python
Python 继承,重写,super()调用父类方法操作示例
2019/09/29 Python
python 图像的离散傅立叶变换实例
2020/01/02 Python
完美解决pyinstaller打包报错找不到依赖pypiwin32或pywin32-ctypes的错误
2020/04/01 Python
浅谈keras使用中val_acc和acc值不同步的思考
2020/06/18 Python
党员个人思想汇报
2013/12/28 职场文书
大学班长的职责
2014/01/27 职场文书
学习十八届三中全会精神实施方案
2014/02/17 职场文书
采购求职信
2014/03/17 职场文书
优秀学生评语大全
2014/04/25 职场文书
公司授权委托书
2014/10/17 职场文书
2014年创卫工作总结
2014/11/24 职场文书
自主招生英文自荐信
2015/03/25 职场文书
民政局未婚证明
2015/06/15 职场文书
《窃读记》教学反思
2016/02/18 职场文书