1. 程式人生 > 程式設計 >Django-xadmin+rule物件級許可權的實現方式

Django-xadmin+rule物件級許可權的實現方式

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='建立人',null=True)
  company_code = models.CharField('公司碼',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',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='角色組許可權',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__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,**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','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,**kwargs):
    return super().dispatch(request,**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,**kwargs)
class AssetDel(LoginRequiredMixin,View):
  model = Ecs
 
  @role_permission_update_delete
  def post(self,request):
    pass

以上這篇Django-xadmin+rule物件級許可權的實現方式就是小編分享給大家的全部內容了,希望能給大家一個參考,也希望大家多多支援我們。