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 擴充套件自帶許可權,使其支援物件許可權
擴充套件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物件級許可權的實現方式就是小編分享給大家的全部內容了,希望能給大家一個參考,也希望大家多多支援我們。