Django Admin复杂用户权限建模:多Unit成员与跨部门权限方案
现有Django实体的一对多层级结构:
- Company - Division - Department - Unit
原权限体系里,每个用户只属于一个Unit,通过给用户分配DepartmentAdmin、DivisionAdmin这类组,就能在Django Admin里管理对应实体的增删改权限——只要顺着Unit往上追溯层级就能判定权限,逻辑简单得很。
现在要重构权限体系:用户可以同时属于多个Unit(主Unit可以保留),而且能拥有任意Department、Division甚至Company层级的管理权限。
之前试过用Membership关联表来存储角色和权限附加信息,但这个方案会产生一堆依赖,权限判定逻辑也绕得慌,示例代码如下:
class Membership(models.Model): user = models.ForeignKey(User, on_delete=models.CASCADE, related_name="other_departments") department = models.ForeignKey(Department, on_delete=models.CASCADE, related_name="other_members") position = models.CharField(_("Position"), max_length=32, null=True, blank=True)
想找个更优的方案,最好能复用Django自带的权限管理功能,求实现思路。
1. 扩展用户与Unit的关联关系
先保留用户的main_unit外键(可选,用来兼容原有业务逻辑),同时新增多对多字段让用户能关联多个Unit:
from django.contrib.auth.models import AbstractUser from django.db import models class Unit(models.Model): # 原有字段(比如名称、所属部门等) members = models.ManyToManyField(AbstractUser, related_name="units", blank=True) class User(AbstractUser): main_unit = models.ForeignKey(Unit, on_delete=models.SET_NULL, null=True, blank=True, related_name="primary_users") # 其他自定义字段按需添加
这样用户直接就能关联多个Unit,除非需要存储加入时间这类额外属性,否则不用额外搞中间表——真要加的话,用through参数扩展中间表就行,尽量别搞复杂。
2. 用原生Group+Permission实现带范围的层级权限
Django自带的Group和Permission完全能适配这个需求,不用重新造轮子:
- 定义带范围的权限组:创建
CompanyAdmin、DivisionAdmin、DepartmentAdmin、UnitAdmin这些组,每个组绑定对应实体的add/change/delete权限(Django会自动给每个Model生成这些基础权限,也可以自己定义特殊权限)。 - 给权限组绑定具体实体:扩展Group模型,加外键关联对应的层级实体,让权限组只作用于特定的业务范围:
from django.contrib.auth.models import Group from django.db import models class ScopedGroup(Group): # 同一组只能关联一个层级的实体,所以用null=True+blank=True做互斥 company = models.ForeignKey(Company, on_delete=models.CASCADE, null=True, blank=True) division = models.ForeignKey(Division, on_delete=models.CASCADE, null=True, blank=True) department = models.ForeignKey(Department, on_delete=models.CASCADE, null=True, blank=True) unit = models.ForeignKey(Unit, on_delete=models.CASCADE, null=True, blank=True) class Meta: verbose_name = "带范围权限组"
比如创建一个DepartmentAdmin组,关联到“市场部”,那加入这个组的用户就只拥有市场部的管理权限,不会影响其他部门。
3. 封装自定义权限判定逻辑
在视图或者Admin里,通过以下逻辑判断用户有没有权限操作某个实体:
拿Department对象dept举例:
- 先查用户是否属于绑定了该
dept的DepartmentAdmin组; - 没有的话,往上查是否属于绑定了
dept.division的DivisionAdmin组; - 再没有就查是否属于绑定了
dept.division.company的CompanyAdmin组; - 同时,用户自己关联的Unit对应的上层实体权限组,也要纳入判定范围。
把这个逻辑封装成可复用的方法:
from django.db.models import Q def has_hierarchy_perm(user, obj, perm_type): # perm_type 传 'add'/'change'/'delete' 就行 perm_codename = f"{obj._meta.app_label}.{perm_type}_{obj._meta.model_name}" # 检查用户是否有该实体的直接权限组 filter_kwargs = {} if isinstance(obj, Department): filter_kwargs["department"] = obj elif isinstance(obj, Division): filter_kwargs["division"] = obj elif isinstance(obj, Company): filter_kwargs["company"] = obj elif isinstance(obj, Unit): filter_kwargs["unit"] = obj if filter_kwargs and user.groups.filter(**filter_kwargs).filter(permissions__codename=perm_codename).exists(): return True # 向上递归检查父层级的权限 if hasattr(obj, 'department'): return has_hierarchy_perm(user, obj.department, perm_type) elif hasattr(obj, 'division'): return has_hierarchy_perm(user, obj.division, perm_type) elif hasattr(obj, 'company'): return has_hierarchy_perm(user, obj.company, perm_type) # 最后检查用户是否有全局权限(比如超级用户) return user.has_perm(perm_codename)
4. Django Admin适配调整
在Admin类里重写权限相关方法,调用上面的自定义判定逻辑,同时在get_queryset里过滤出用户有权限操作的实体:
from django.contrib import admin class DepartmentAdmin(admin.ModelAdmin): def get_queryset(self, request): qs = super().get_queryset(request) if request.user.is_superuser: return qs # 过滤用户能管理的Department:自己关联的Unit所属部门、权限组绑定的部门、父层级权限组覆盖的部门 user_units = request.user.units.all() user_depts = Department.objects.filter(unit__in=user_units).distinct() # 权限组直接绑定的部门 group_depts = Department.objects.filter(id__in=request.user.groups.values_list('department_id', flat=True)).distinct() # 权限组绑定的事业部下属部门 group_divisions = Division.objects.filter(id__in=request.user.groups.values_list('division_id', flat=True)).distinct() division_depts = Department.objects.filter(division__in=group_divisions).distinct() # 权限组绑定的公司下属部门 group_companies = Company.objects.filter(id__in=request.user.groups.values_list('company_id', flat=True)).distinct() company_depts = Department.objects.filter(division__company__in=group_companies).distinct() return qs.filter(id__in=user_depts | group_depts | division_depts | company_depts) def has_change_permission(self, request, obj=None): if request.user.is_superuser: return True if obj is None: # 没有具体对象时,检查用户是否有权限管理任何部门 return self.get_queryset(request).exists() return has_hierarchy_perm(request.user, obj, 'change') # 同理重写 has_add_permission 和 has_delete_permission def has_add_permission(self, request): if request.user.is_superuser: return True # 新增权限可以根据用户的层级权限组判定,比如允许DivisionAdmin在自己的事业部下新增部门 return request.user.groups.filter( Q(division__isnull=False) | Q(company__isnull=False) | Q(department__isnull=False) ).filter(permissions__codename=f"{Department._meta.app_label}.add_department").exists() def has_delete_permission(self, request, obj=None): if request.user.is_superuser: return True if obj is None: return self.get_queryset(request).exists() return has_hierarchy_perm(request.user, obj, 'delete')
内容的提问来源于stack exchange,提问作者Mundi

