feat(app): 应用级访问控制——AppAccessPermission 权限工厂挂载邮箱/Gitea/商店/储值/钱包/企业 API(未安装租户拒绝,管理员豁免)
This commit is contained in:
1 parent
1066d82c75
commit
904e225b90
4 files changed
+79
-21
No files matched your search
+14
-7
@@ -28,6 +28,13 @@ from rest_framework.permissions import IsAuthenticated, AllowAny
|
||||
from rest_framework.response import Response
|
||||
|
||||
from gvsdsdk.model_utils import UUIDObj
|
||||
from gvsds.apps.gerp.app_access import AppAccessPermission
|
||||
|
||||
# 各业务应用编码(未安装的企业租户无法使用对应业务功能)
|
||||
_APP_ACCESS_WALLET = AppAccessPermission('wallet')
|
||||
_APP_ACCESS_STORE_VALUE = AppAccessPermission('store-value')
|
||||
_APP_ACCESS_SHOP = AppAccessPermission('shop')
|
||||
_APP_ACCESS_ENTERPRISE = AppAccessPermission('enterprise')
|
||||
|
||||
from .business import (
|
||||
StoreValueRule,
|
||||
@@ -334,7 +341,7 @@ class CmeBalancesViewSet(CompanyPrivateAccessMixin, viewsets.ModelViewSet):
|
||||
"""
|
||||
queryset = CmeBalances.objects.all()
|
||||
serializer_class = CmeBalancesSerializer
|
||||
permission_classes = [IsAuthenticated]
|
||||
permission_classes = [IsAuthenticated, _APP_ACCESS_WALLET]
|
||||
company_private_page = 'pages/private/CreateE/wallet.html'
|
||||
|
||||
@action(detail=False, methods=['get'])
|
||||
@@ -668,7 +675,7 @@ class EnterpriseViewSet(CompanyPrivateAccessMixin, viewsets.ModelViewSet):
|
||||
"""
|
||||
queryset = Enterprise.objects.all()
|
||||
serializer_class = EnterpriseSerializer
|
||||
permission_classes = [IsAuthenticated]
|
||||
permission_classes = [IsAuthenticated, _APP_ACCESS_ENTERPRISE]
|
||||
company_private_page = 'pages/private/CreateE/enterprise.html'
|
||||
company_private_exempt = {'get_enterprise_card'}
|
||||
|
||||
@@ -1084,7 +1091,7 @@ class EnterpriseUserViewSet(CompanyPrivateAccessMixin, viewsets.ModelViewSet):
|
||||
"""
|
||||
queryset = EnterpriseUser.objects.all()
|
||||
serializer_class = EnterpriseUserSerializer
|
||||
permission_classes = [IsAuthenticated]
|
||||
permission_classes = [IsAuthenticated, _APP_ACCESS_ENTERPRISE]
|
||||
company_private_page = 'pages/private/CreateE/enterprise.html'
|
||||
company_private_exempt = {'get_members'}
|
||||
|
||||
@@ -1282,7 +1289,7 @@ class CmeStoreValueViewSet(CompanyPrivateAccessMixin, viewsets.ModelViewSet):
|
||||
"""
|
||||
queryset = CmeStoreValue.objects.all()
|
||||
serializer_class = CmeStoreValueSerializer
|
||||
permission_classes = [IsAuthenticated]
|
||||
permission_classes = [IsAuthenticated, _APP_ACCESS_STORE_VALUE]
|
||||
company_private_page = 'pages/private/CreateE/StoreValue.html'
|
||||
company_private_exempt = {'get_store_value_rules'}
|
||||
|
||||
@@ -1544,7 +1551,7 @@ class ShopProductViewSet(CompanyPrivateAccessMixin, viewsets.ModelViewSet):
|
||||
|
||||
queryset = ShopProduct.objects.all()
|
||||
serializer_class = ShopProductSerializer
|
||||
permission_classes = [IsAuthenticated]
|
||||
permission_classes = [IsAuthenticated, _APP_ACCESS_SHOP]
|
||||
company_private_page = 'pages/private/CreateE/shop.html'
|
||||
company_private_company_uuid = 'ef67bb82-a590-402e-a893-81ae6127fb08'
|
||||
company_private_exempt = {'list_public', 'detail'}
|
||||
@@ -1813,7 +1820,7 @@ class ShopCartViewSet(CompanyPrivateAccessMixin, viewsets.ModelViewSet):
|
||||
|
||||
queryset = ShopCartItem.objects.all()
|
||||
serializer_class = ShopCartItemSerializer
|
||||
permission_classes = [IsAuthenticated]
|
||||
permission_classes = [IsAuthenticated, _APP_ACCESS_SHOP]
|
||||
company_private_page = 'pages/private/CreateE/shop.html'
|
||||
company_private_company_uuid = 'ef67bb82-a590-402e-a893-81ae6127fb08'
|
||||
|
||||
@@ -2000,7 +2007,7 @@ class ShopOrderViewSet(CompanyPrivateAccessMixin, viewsets.ModelViewSet):
|
||||
|
||||
queryset = ShopOrder.objects.all()
|
||||
serializer_class = ShopOrderSerializer
|
||||
permission_classes = [IsAuthenticated]
|
||||
permission_classes = [IsAuthenticated, _APP_ACCESS_SHOP]
|
||||
company_private_page = 'pages/private/CreateE/shop.html'
|
||||
company_private_company_uuid = 'ef67bb82-a590-402e-a893-81ae6127fb08'
|
||||
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
"""模块:应用级访问控制(AppAccess)
|
||||
|
||||
将功能/应用体系的 CheckAccess 硬校验挂到各应用 API 入口:
|
||||
- 管理员(admin:super / admin:tenant):豁免——与菜单过滤逻辑一致,管理员全览
|
||||
- 普通用户:按 AppManager.CheckAccess 校验(企业功能按租户安装状态、个人应用按个人启用状态)
|
||||
- 未安装/未启用:返回 403(DRF permission 拒绝)
|
||||
|
||||
用法(视图 permission_classes):
|
||||
permission_classes = [IsAuthenticated, AppAccessPermission('mailbox')]
|
||||
|
||||
注意:同一视图类中的 AllowAny 公开 action(如商品列表 list_public)不受影响——
|
||||
DRF 的 action 级 permission_classes 会覆盖 viewset 级。
|
||||
"""
|
||||
|
||||
from rest_framework import permissions
|
||||
|
||||
|
||||
def AppAccessPermission(AppCode):
|
||||
"""生成应用级访问控制权限类(工厂)
|
||||
|
||||
Args:
|
||||
AppCode: 应用编码(如 mailbox / gitea / shop / store-value / wallet / enterprise)
|
||||
"""
|
||||
class _AppAccessPermission(permissions.BasePermission):
|
||||
message = '该功能未开通,请联系企业管理员'
|
||||
|
||||
def has_permission(self, request, view):
|
||||
user = getattr(request, 'user', None)
|
||||
if not (user and getattr(user, 'IsAuthenticated', False)):
|
||||
return False
|
||||
# 管理员豁免(与菜单过滤一致:admin:super/admin:tenant 全览)
|
||||
from gvsdsdk.auth.admin_service import IsAdmin
|
||||
if IsAdmin(request=request, user=user):
|
||||
return True
|
||||
# 解析当前租户/用户上下文(CurrentRoleMiddleware 已设置 request.current_tenant_uuid)
|
||||
tenant_uuid = getattr(request, 'current_tenant_uuid', None)
|
||||
user_uuid = getattr(user, 'UserUUID', None)
|
||||
if not tenant_uuid or not user_uuid:
|
||||
return False
|
||||
from .app_manager import AppManager
|
||||
return AppManager.CheckAccess(tenant_uuid, user_uuid, AppCode) is not None
|
||||
|
||||
return _AppAccessPermission
|
||||
@@ -22,6 +22,10 @@ from rest_framework.response import Response
|
||||
from gvsds import secrets as app_secrets
|
||||
from gvsdsdk.model_utils import UUIDObj
|
||||
from gvsdsdk.auth.permissions import IsAdminUser
|
||||
from ..app_access import AppAccessPermission
|
||||
|
||||
# Gitea 功能应用编码(未安装的企业租户员工无法使用代码托管)
|
||||
_GITEA_ACCESS = AppAccessPermission('gitea')
|
||||
|
||||
from . import ELTAuthenticatedAPIView
|
||||
from ..models import UserGiteaAccount, UserMailbox
|
||||
@@ -105,7 +109,7 @@ def _IsOwnerRole(user_bytes):
|
||||
class GiteaSessionView(ELTAuthenticatedAPIView):
|
||||
"""返回 Gitea 连接配置(BaseUrl / Org / OwnerRole)。"""
|
||||
|
||||
permission_classes = [IsAuthenticated]
|
||||
permission_classes = [IsAuthenticated, _GITEA_ACCESS]
|
||||
|
||||
def get(self, request):
|
||||
return Response({'code': 0, 'data': GiteaApiClient.SessionConfig()})
|
||||
@@ -114,7 +118,7 @@ class GiteaSessionView(ELTAuthenticatedAPIView):
|
||||
class GiteaApplyView(ELTAuthenticatedAPIView):
|
||||
"""员工自助申请 Gitea 账号(自选用户名 + 自设密码)。"""
|
||||
|
||||
permission_classes = [IsAuthenticated]
|
||||
permission_classes = [IsAuthenticated, _GITEA_ACCESS]
|
||||
|
||||
def post(self, request):
|
||||
user_bytes = _GetUserBytes(request)
|
||||
@@ -205,7 +209,7 @@ class GiteaApplyView(ELTAuthenticatedAPIView):
|
||||
class GiteaMyAccountView(ELTAuthenticatedAPIView):
|
||||
"""我的 Gitea 账号信息 + 连接配置。"""
|
||||
|
||||
permission_classes = [IsAuthenticated]
|
||||
permission_classes = [IsAuthenticated, _GITEA_ACCESS]
|
||||
|
||||
def get(self, request):
|
||||
acct = UserGiteaAccount.objects.filter(UserUUID=request.user).first()
|
||||
@@ -231,7 +235,7 @@ class GiteaMyAccountView(ELTAuthenticatedAPIView):
|
||||
class GiteaChangePasswordView(ELTAuthenticatedAPIView):
|
||||
"""修改 Gitea 账号密码(管理员 API,无需旧密码)。"""
|
||||
|
||||
permission_classes = [IsAuthenticated]
|
||||
permission_classes = [IsAuthenticated, _GITEA_ACCESS]
|
||||
|
||||
def post(self, request):
|
||||
acct = UserGiteaAccount.objects.filter(UserUUID=request.user).first()
|
||||
@@ -255,7 +259,7 @@ class GiteaAvatarView(ELTAuthenticatedAPIView):
|
||||
故用用户的 Gitea 密码做 Basic Auth;密码仅此一次传输,不落库。
|
||||
"""
|
||||
|
||||
permission_classes = [IsAuthenticated]
|
||||
permission_classes = [IsAuthenticated, _GITEA_ACCESS]
|
||||
|
||||
def post(self, request):
|
||||
acct = UserGiteaAccount.objects.filter(UserUUID=request.user).first()
|
||||
|
||||
@@ -27,6 +27,10 @@ from rest_framework.response import Response
|
||||
from gvsds import secrets as app_secrets
|
||||
from gvsdsdk.model_utils import UUIDObj
|
||||
from gvsdsdk.auth.permissions import IsAdminUser
|
||||
from ..app_access import AppAccessPermission
|
||||
|
||||
# 邮箱功能应用编码(未安装的企业租户员工无法使用邮箱)
|
||||
_MAIL_ACCESS = AppAccessPermission('mailbox')
|
||||
|
||||
from . import ELTAuthenticatedAPIView
|
||||
from ..models import UserMailbox
|
||||
@@ -204,7 +208,7 @@ def _CheckCompanyAllowed(user_bytes):
|
||||
class MailSessionView(ELTAuthenticatedAPIView):
|
||||
"""返回浏览器直连 JMAP 所需的连接配置。"""
|
||||
|
||||
permission_classes = [IsAuthenticated]
|
||||
permission_classes = [IsAuthenticated, _MAIL_ACCESS]
|
||||
|
||||
def get(self, request):
|
||||
return Response({'code': 0, 'data': StalwartMailManager.SessionConfig()})
|
||||
@@ -213,7 +217,7 @@ class MailSessionView(ELTAuthenticatedAPIView):
|
||||
class MailApplyView(ELTAuthenticatedAPIView):
|
||||
"""员工自助申请邮箱(xxx@gvsds.com)。"""
|
||||
|
||||
permission_classes = [IsAuthenticated]
|
||||
permission_classes = [IsAuthenticated, _MAIL_ACCESS]
|
||||
|
||||
def post(self, request):
|
||||
user_bytes = _GetUserBytes(request)
|
||||
@@ -290,9 +294,9 @@ class MailApplyView(ELTAuthenticatedAPIView):
|
||||
|
||||
|
||||
class MailChangePasswordView(ELTAuthenticatedAPIView):
|
||||
"""修改自己的邮箱密码。"""
|
||||
"""修改邮箱密码(校验旧密码)。"""
|
||||
|
||||
permission_classes = [IsAuthenticated]
|
||||
permission_classes = [IsAuthenticated, _MAIL_ACCESS]
|
||||
|
||||
def post(self, request):
|
||||
mailbox_name = (request.data.get('MailboxName') or '').strip().lower()
|
||||
@@ -323,7 +327,7 @@ class MailChangePasswordView(ELTAuthenticatedAPIView):
|
||||
class MailMyMailboxView(ELTAuthenticatedAPIView):
|
||||
"""我的邮箱列表 + JMAP 连接配置(供浏览器直连)。"""
|
||||
|
||||
permission_classes = [IsAuthenticated]
|
||||
permission_classes = [IsAuthenticated, _MAIL_ACCESS]
|
||||
|
||||
def get(self, request):
|
||||
session = StalwartMailManager.SessionConfig()
|
||||
@@ -409,7 +413,7 @@ class MailLoginView(ELTAuthenticatedAPIView):
|
||||
响应:{code: 0, data: {Token, MailboxName}}
|
||||
"""
|
||||
|
||||
permission_classes = [IsAuthenticated]
|
||||
permission_classes = [IsAuthenticated, _MAIL_ACCESS]
|
||||
|
||||
def post(self, request):
|
||||
mailbox_name = (request.data.get('MailboxName') or '').strip().lower()
|
||||
@@ -434,7 +438,7 @@ class MailLoginView(ELTAuthenticatedAPIView):
|
||||
class MailLogoutView(ELTAuthenticatedAPIView):
|
||||
"""邮箱登出——吊销会话 token(删除内存中的邮箱凭据)。"""
|
||||
|
||||
permission_classes = [IsAuthenticated]
|
||||
permission_classes = [IsAuthenticated, _MAIL_ACCESS]
|
||||
|
||||
def post(self, request):
|
||||
_RevokeMailToken(request.data.get('Token') or '')
|
||||
@@ -453,7 +457,7 @@ class MailJmapView(ELTAuthenticatedAPIView):
|
||||
响应:{code: 0, data: {AccountId, Data}},Data 为 Stalwart 原始 methodResponses。
|
||||
"""
|
||||
|
||||
permission_classes = [IsAuthenticated]
|
||||
permission_classes = [IsAuthenticated, _MAIL_ACCESS]
|
||||
|
||||
def post(self, request):
|
||||
entry = _GetMailToken(request.data.get('Token') or '')
|
||||
@@ -524,7 +528,7 @@ class MailContactsView(ELTAuthenticatedAPIView):
|
||||
POST /API/Mail/Contacts - 新增/更新联系人(ContactEmail 必填)
|
||||
"""
|
||||
|
||||
permission_classes = [IsAuthenticated]
|
||||
permission_classes = [IsAuthenticated, _MAIL_ACCESS]
|
||||
|
||||
def get(self, request):
|
||||
from django.db.models import Q
|
||||
|
||||
Reference in new issue
Block a user