feat(gerp): 授权审批限定公司/租户/平台管理员,并按其级别收窄数据范围
- 菜单 app-pending 加权限属性 admin:company(迁移 0047):公司管理员 + 租户管理员 + 平台管理员可见 - AppPendingView/AppApproveView 权限放宽到 IsCompanyAdminUser,按管理员级别解析可管辖公司 - ApproveUserApp 增加越权校验;返回值统一为三元组(失败原为 2 元组,视图解包会抛 ValueError) - CheckAdmin 下发 is_company_admin 供前端判定
This commit is contained in:
1 parent
5236821c97
commit
24db976efd
4 files changed
+133
-23
No files matched your search
@@ -17,7 +17,7 @@
|
||||
import datetime
|
||||
import uuid
|
||||
|
||||
from gvsdsdk.auth.managers import AppManager as SdkAppManager, CompanyManager, PositionManager
|
||||
from gvsdsdk.auth.managers import AppManager as SdkAppManager, CompanyManager, PositionManager, RoleManager
|
||||
from gvsdsdk.model_utils import UUIDObj
|
||||
|
||||
# 投放形态
|
||||
@@ -1027,17 +1027,30 @@ class AppManager:
|
||||
return True, ''
|
||||
|
||||
@staticmethod
|
||||
def ApproveUserApp(OperatorUUID, UserAppUUID, Approved=True):
|
||||
"""租户管理员审批个人授权申请。
|
||||
def ApproveUserApp(OperatorUUID, UserAppUUID, Approved=True, AllowedCompanyUUIDs=None):
|
||||
"""公司/租户/平台管理员审批个人授权申请。
|
||||
|
||||
AllowedCompanyUUIDs:
|
||||
- None:不限制(平台管理员 admin:super)
|
||||
- 集合:申请人的公司必须命中该集合,否则拒绝(越权保护)
|
||||
|
||||
Returns:
|
||||
(ok, err, flat)
|
||||
(ok, err, flat):三种返回值固定为三元组(失败时 flat 为 None),
|
||||
避免调用方按三元组解包时在失败分支抛 ValueError。
|
||||
"""
|
||||
ua = SdkAppManager.GetUserAppByUUID(UserAppUUID)
|
||||
if ua is None:
|
||||
return False, '申请记录不存在'
|
||||
return False, '申请记录不存在', None
|
||||
if ua.ApplyStatus != APPLY_PENDING:
|
||||
return False, '该申请已处理(非待审批状态)'
|
||||
return False, '该申请已处理(非待审批状态)', None
|
||||
if AllowedCompanyUUIDs is not None:
|
||||
allowed = [c for c in AllowedCompanyUUIDs if c]
|
||||
applicant_companies = {
|
||||
_ToBytes(ur.CompanyUUID)
|
||||
for ur in RoleManager.GetUserRoleDetails(_ToBytes(ua.UserUUID))
|
||||
}
|
||||
if not allowed or not (applicant_companies & {_ToBytes(c) for c in allowed}):
|
||||
return False, '无权审批:申请人不在您的管理范围内', None
|
||||
apply_status = APPLY_APPROVED if Approved else APPLY_DENIED
|
||||
use_status = 1 if Approved else 0
|
||||
approve_by = _ToBytes(OperatorUUID)
|
||||
@@ -1054,10 +1067,15 @@ class AppManager:
|
||||
return True, '', _AppFlat(app, user_record=ua)
|
||||
|
||||
@staticmethod
|
||||
def ListPendingApplications(TenantUUID):
|
||||
"""租户内待审批的个人授权申请(供租户管理员审批台)"""
|
||||
def ListPendingApplications(AllowedCompanyUUIDs=None):
|
||||
"""待审批的个人授权申请(供审批台)
|
||||
|
||||
AllowedCompanyUUIDs:
|
||||
- None:不限制(平台管理员 admin:super)
|
||||
- 集合:仅申请人的公司命中该集合的申请(公司/租户管理员,由视图计算)
|
||||
"""
|
||||
rows = []
|
||||
for ua in SdkAppManager.ListPendingApplications(TenantUUID):
|
||||
for ua in SdkAppManager.ListPendingApplications(AllowedCompanyUUIDs=AllowedCompanyUUIDs):
|
||||
app = AppManager.GetApp(ua.AppUUID)
|
||||
if app is None:
|
||||
continue
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
"""Data Migration: 授权审批菜单限定为「公司管理员及以上」可见
|
||||
|
||||
背景:
|
||||
``Menu`` 表中 ``app-pending``(授权审批,pages/app/pending.html)的
|
||||
``PermissionAttr`` 为空 → 菜单对所有角色可见,普通成员也能看到审批台。
|
||||
改为 ``admin:company`` 后:持有该权限码的角色(公司管理员)+ admin:tenant
|
||||
(租户管理员)+ admin:super(平台管理员)可见,其余角色不可见。
|
||||
过滤逻辑见 gvsds/apps/gerp/views/menu.py 的菜单权限过滤段
|
||||
(perm_attrs 含 admin:super 时按 IsSuperAdmin 判定;否则非管理员走
|
||||
PermissionVerifier 按权限码判定,而 PermissionVerifier 对两种管理员直接放行)。
|
||||
|
||||
实现说明:
|
||||
Menu 走 historical model(字段名为小写形式,db_column 才是 PascalCase)。
|
||||
幂等:重复执行只是重复写入相同值。
|
||||
|
||||
注:``Permission`` 表中 ``admin:company`` 虽有两行,但分属两个不同租户
|
||||
(TenantUUID 不同,各 0 条 RolePermission 引用),是各租户各自的权限定义,
|
||||
并非误插重复数据,故**不清理**——删除会使其中一个租户在权限管理中失去该项。
|
||||
"""
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
APPRINT_MENU_NAME = 'app-pending'
|
||||
SCOPE_PERM_CODE = 'admin:company'
|
||||
|
||||
|
||||
def ApplyAppPendingScope(apps, schema_editor):
|
||||
"""正向:菜单加权限属性(幂等)"""
|
||||
Menu = apps.get_model('gerp', 'Menu')
|
||||
Menu.objects.filter(menu_name=APPRINT_MENU_NAME).update(
|
||||
permission_attr=SCOPE_PERM_CODE,
|
||||
permission_level=1.0,
|
||||
)
|
||||
|
||||
|
||||
def RevertAppPendingScope(apps, schema_editor):
|
||||
"""逆向:还原为空权限属性"""
|
||||
Menu = apps.get_model('gerp', 'Menu')
|
||||
Menu.objects.filter(menu_name=APPRINT_MENU_NAME).update(
|
||||
permission_attr='',
|
||||
permission_level=1.0,
|
||||
)
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('gerp', '0046_talk_models_state'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RunPython(ApplyAppPendingScope, reverse_code=RevertAppPendingScope),
|
||||
]
|
||||
@@ -12,17 +12,17 @@ URL 路径最后一段作为 action 名称:
|
||||
POST /API/App/Enable - 个人:启用应用(AuthRequired=1 需先获授权)
|
||||
POST /API/App/Disable - 个人:停用应用
|
||||
POST /API/App/Apply - 个人:申请企业授权
|
||||
POST /API/App/Approve - 租户管理员:审批个人授权申请
|
||||
GET /API/App/Pending - 租户管理员:待审批申请列表
|
||||
POST /API/App/Approve - 公司/租户/平台管理员:审批个人授权申请
|
||||
GET /API/App/Pending - 公司/租户/平台管理员:待审批申请列表
|
||||
"""
|
||||
import logging
|
||||
|
||||
from rest_framework.permissions import IsAuthenticated
|
||||
from rest_framework.response import Response
|
||||
|
||||
from gvsdsdk.auth.admin_service import IsAdmin
|
||||
from gvsdsdk.auth.admin_service import IsAdmin, IsSuperAdmin, IsTenantAdmin
|
||||
from gvsdsdk.auth.managers import CompanyManager, RoleManager
|
||||
from gvsdsdk.auth.permissions import IsAdminUser
|
||||
from gvsdsdk.auth.permissions import IsAdminUser, IsCompanyAdminUser
|
||||
from gvsdsdk.model_utils import UUIDObj
|
||||
|
||||
from . import ELTAuthenticatedAPIView
|
||||
@@ -75,6 +75,25 @@ def _GetCompanyBytes(request):
|
||||
return None
|
||||
|
||||
|
||||
def _ResolveApproveScopeCompanies(request):
|
||||
"""解析授权审批的可管辖公司范围(返回公司 UUID 列表;None = 不限制)
|
||||
|
||||
与菜单「授权审批」的可见范围一致(admin:company / admin:tenant / admin:super):
|
||||
- 平台管理员(admin:super):None,可见全部租户
|
||||
- 租户管理员(admin:tenant):本租户下全部公司
|
||||
- 公司管理员(admin:company):仅当前公司
|
||||
"""
|
||||
if IsSuperAdmin(request=request):
|
||||
return None
|
||||
if IsTenantAdmin(request=request):
|
||||
tenant_bytes = _GetTenantBytes(request)
|
||||
if not tenant_bytes:
|
||||
return []
|
||||
return CompanyManager.GetCompanyUUIDs(tenant_bytes)
|
||||
company_bytes = _GetCompanyBytes(request)
|
||||
return [company_bytes] if company_bytes else []
|
||||
|
||||
|
||||
def _ResolveRoleUUID(request):
|
||||
"""当前角色 UUID bytes(Cookie CurrentRoleUUID)"""
|
||||
role_uuid_str = request.COOKIES.get('CurrentRoleUUID', '')
|
||||
@@ -440,13 +459,16 @@ class AppApplyView(ELTAuthenticatedAPIView):
|
||||
ok, err = AppManager.ApplyUserApp(user_bytes, app_uuid)
|
||||
if not ok:
|
||||
return Response({'code': 1, 'msg': err}, status=400)
|
||||
return Response({'code': 0, 'msg': '申请已提交,等待租户管理员审批'})
|
||||
return Response({'code': 0, 'msg': '申请已提交,等待管理员审批'})
|
||||
|
||||
|
||||
class AppApproveView(ELTAuthenticatedAPIView):
|
||||
"""租户管理员:审批个人授权申请。"""
|
||||
"""公司/租户/平台管理员:审批个人授权申请。
|
||||
|
||||
permission_classes = [IsAuthenticated, IsAdminUser]
|
||||
仅能审批自己管理范围内(本公司 / 本租户 / 全部)申请人的申请。
|
||||
"""
|
||||
|
||||
permission_classes = [IsAuthenticated, IsCompanyAdminUser]
|
||||
|
||||
def post(self, request):
|
||||
user_app_uuid = (request.data.get('UserAppUUID') or '').strip()
|
||||
@@ -456,22 +478,28 @@ class AppApproveView(ELTAuthenticatedAPIView):
|
||||
if isinstance(approved, str):
|
||||
approved = approved.lower() in ('1', 'true', 'yes')
|
||||
user_bytes = _GetUserBytes(request)
|
||||
ok, err, flat = AppManager.ApproveUserApp(user_bytes, user_app_uuid, Approved=bool(approved))
|
||||
ok, err, flat = AppManager.ApproveUserApp(
|
||||
user_bytes, user_app_uuid,
|
||||
Approved=bool(approved),
|
||||
AllowedCompanyUUIDs=_ResolveApproveScopeCompanies(request),
|
||||
)
|
||||
if not ok:
|
||||
return Response({'code': 1, 'msg': err}, status=400)
|
||||
return Response({'code': 0, 'msg': '已通过' if approved else '已拒绝', 'data': flat})
|
||||
|
||||
|
||||
class AppPendingView(ELTAuthenticatedAPIView):
|
||||
"""租户管理员:待审批的个人授权申请列表。"""
|
||||
"""公司/租户/平台管理员:待审批的个人授权申请列表。
|
||||
|
||||
permission_classes = [IsAuthenticated, IsAdminUser]
|
||||
数据范围:平台管理员=全部;租户管理员=本租户;公司管理员=本公司。
|
||||
"""
|
||||
|
||||
permission_classes = [IsAuthenticated, IsCompanyAdminUser]
|
||||
|
||||
def get(self, request):
|
||||
tenant_bytes = _GetTenantBytes(request)
|
||||
if not tenant_bytes:
|
||||
return Response({'code': 0, 'data': {'list': [], 'total': 0}})
|
||||
rows = AppManager.ListPendingApplications(tenant_bytes)
|
||||
rows = AppManager.ListPendingApplications(
|
||||
AllowedCompanyUUIDs=_ResolveApproveScopeCompanies(request),
|
||||
)
|
||||
return Response({'code': 0, 'data': {'list': rows, 'total': len(rows)}})
|
||||
|
||||
|
||||
|
||||
@@ -52,6 +52,15 @@ def _is_tenant_admin(request):
|
||||
return IsTenantAdmin(request)
|
||||
|
||||
|
||||
def _is_company_admin(request):
|
||||
"""判断用户是否为公司管理员及以上(admin:company / admin:tenant / admin:super)
|
||||
|
||||
委托给 IsCompanyAdmin:租户管理员与跨租户管理员自动继承公司管理员能力。
|
||||
"""
|
||||
from gvsdsdk.auth.admin_service import IsCompanyAdmin
|
||||
return IsCompanyAdmin(request)
|
||||
|
||||
|
||||
def _make_service(request, user):
|
||||
"""构造 FormService,自动传入当前公司 UUID、角色 UUID 和租户 UUID。
|
||||
上下文来自 CurrentRoleMiddleware 注入的 request 属性。
|
||||
@@ -407,6 +416,7 @@ class CheckAdminView(ELTAuthenticatedAPIView):
|
||||
is_admin = _is_admin(request) # admin:super 或 admin:tenant
|
||||
is_super_admin = _is_super_admin(request) # 仅 admin:super(跨租户,最高角色级)
|
||||
is_tenant_admin = _is_tenant_admin(request) # 仅 admin:tenant(租户内,中角色级)
|
||||
is_company_admin = _is_company_admin(request) # admin:company / admin:tenant / admin:super
|
||||
current_role_uuid, current_company_uuid, current_tenant_uuid = _get_current_context(request)
|
||||
# 获取角色名
|
||||
role_name = ''
|
||||
@@ -450,6 +460,7 @@ class CheckAdminView(ELTAuthenticatedAPIView):
|
||||
'is_superuser': is_superuser,
|
||||
'is_super_admin': is_super_admin,
|
||||
'is_tenant_admin': is_tenant_admin,
|
||||
'is_company_admin': is_company_admin,
|
||||
'current_role_name': role_name,
|
||||
'current_role_uuid': UUIDObj(current_role_uuid).str if current_role_uuid else None,
|
||||
'current_company_uuid': UUIDObj(current_company_uuid).str if current_company_uuid else None,
|
||||
|
||||
Reference in new issue
Block a user