From 345abe88bb812ca5161032f7c93d20d55bacf4a3 Mon Sep 17 00:00:00 2001 From: TermiNexus Date: Thu, 13 Aug 2026 23:55:55 +0800 Subject: [PATCH] =?UTF-8?q?feat(ram):=20RAMGrant=20=E5=BB=BA=E8=A1=A8?= =?UTF-8?q?=E8=BF=81=E7=A7=BB=20+=20=E8=8F=9C=E5=8D=95=E8=BF=87=E6=BB=A4?= =?UTF-8?q?=20RAM=20=E8=A7=92=E8=89=B2=E7=B2=92=E5=BA=A6=E9=9B=86=E6=88=90?= =?UTF-8?q?=EF=BC=88=E6=97=A0=E7=AD=96=E7=95=A5=E4=BF=9D=E6=8C=81=E5=85=A8?= =?UTF-8?q?=E5=91=98=E5=8F=AF=E8=A7=81=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- gvsds/apps/gerp/migrations/0030_ramgrant.py | 43 ++++++++++++++ gvsds/apps/gerp/views/menu.py | 64 ++++++++++++++++++--- 2 files changed, 98 insertions(+), 9 deletions(-) create mode 100644 gvsds/apps/gerp/migrations/0030_ramgrant.py diff --git a/gvsds/apps/gerp/migrations/0030_ramgrant.py b/gvsds/apps/gerp/migrations/0030_ramgrant.py new file mode 100644 index 00000000..fe1b90cf --- /dev/null +++ b/gvsds/apps/gerp/migrations/0030_ramgrant.py @@ -0,0 +1,43 @@ +"""创建 RAMGrant 表——RAM 资源访问管理(Resource Access Management)授权记录 + +表由 gvsds 主项目管理(主库),gvsdsdk RAMGrant 模型为 managed=False 映射。 +字段对齐 gvsdsdk/models.py RAMGrant: + 主体 Principal(role/user)× 资源 Resource(app/menu/data_scope)× 操作 Action + (view/use/manage/grant)× 作用域(TenantUUID/CompanyUUID,null=全范围) +""" +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('gerp', '0029_fk_to_uuidfield'), + ] + + operations = [ + migrations.RunSQL( + sql=""" +CREATE TABLE IF NOT EXISTS `RAMGrant` ( + `GrantUUID` BINARY(16) NOT NULL PRIMARY KEY, + `PrincipalType` VARCHAR(16) NOT NULL, + `PrincipalUUID` BINARY(16) NOT NULL, + `ResourceType` VARCHAR(32) NOT NULL, + `ResourceUUID` BINARY(16) NOT NULL, + `Action` VARCHAR(16) NOT NULL, + `TenantUUID` BINARY(16) NULL, + `CompanyUUID` BINARY(16) NULL, + `GrantStatus` SMALLINT NOT NULL DEFAULT 1, + `GrantBy` BINARY(16) NULL, + `GrantTime` DATETIME NOT NULL +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + +CREATE UNIQUE INDEX `uniq_ram_grant` ON `RAMGrant` + (`PrincipalType`, `PrincipalUUID`, `ResourceType`, `ResourceUUID`, `Action`, `TenantUUID`, `CompanyUUID`); +CREATE INDEX `idx_ram_principal` ON `RAMGrant` (`PrincipalUUID`, `ResourceType`); +CREATE INDEX `idx_ram_resource` ON `RAMGrant` (`ResourceType`, `ResourceUUID`); +""", + reverse_sql=""" +DROP TABLE IF EXISTS `RAMGrant`; +""", + ), + ] diff --git a/gvsds/apps/gerp/views/menu.py b/gvsds/apps/gerp/views/menu.py index 49f2ac11..13144243 100644 --- a/gvsds/apps/gerp/views/menu.py +++ b/gvsds/apps/gerp/views/menu.py @@ -163,7 +163,10 @@ class MenuListCompatView(ELTAuthenticatedAPIView): # 未安装应用(租户/公司/个人均未启用)的 Manifest 声明的全部菜单一律隐藏, # 含散挂在其他父节点下的页面(如"邮箱管理"挂公司下)。 # 管理员仅应用中心管理(安装/停用)豁免,业务菜单同样按安装状态隐藏。 + # RAM 角色粒度:配置了 RAM 策略(存在 app 级 view 授权)的 App, + # 仅对获得授权的角色可见;无策略的 App 保持全员可见(向后兼容)。 try: + from django.db.models import Q as _DJQ from ..models import App from ..app_manager import AppManager from ..app_manifests import GetAppManifest @@ -175,22 +178,25 @@ class MenuListCompatView(ELTAuthenticatedAPIView): if _role_obj is not None: current_tenant_uuid = _role_obj.TenantUUID + def _ToUUIDBytes(value): + if value is None: + return None + if hasattr(value, 'bytes'): + return value.bytes + if isinstance(value, bytes): + return value + return value + def _CollectAppSub(uuid_str, acc): acc.add(uuid_str) for uid, item in menu_map.items(): if item['ParentUUID'] == uuid_str and uid not in acc: _CollectAppSub(uid, acc) - for _app in App.objects.filter(AppStatus=1): - if AppManager.CheckAccess( - current_tenant_uuid, user.UserUUID, _app.AppCode, - CompanyUUID=current_company, - ) is not None: - continue - manifest = GetAppManifest(_app.AppCode) + def _HideAppMenus(app_code, manifest): + """收集并移除指定应用 Manifest 声明的全部菜单(含子节点)""" if manifest is None: - continue - # 收集该应用 Manifest 声明的全部菜单(MenuName 匹配 Id 字段) + return app_menu_names = {s.MenuName for s in manifest.Menus} _invalid = set() for _uid, _item in menu_map.items(): @@ -198,6 +204,46 @@ class MenuListCompatView(ELTAuthenticatedAPIView): _CollectAppSub(_uid, _invalid) for _u in _invalid: menu_map.pop(_u, None) + + # RAM 策略/授权集合(bytes 维度,一次查询取齐) + ram_policy_apps = set() + ram_granted_apps = set() + try: + from gvsdsdk.models import RAMGrant as _SdkRAMGrant + from gvsdsdk.auth import RAMManager as _SdkRAMManager + _tenant_bytes = _ToUUIDBytes(current_tenant_uuid) + _company_bytes = _ToUUIDBytes(current_company) + ram_qs = _SdkRAMGrant.objects.filter( + ResourceType='app', Action='view', GrantStatus=1, + ) + if _tenant_bytes is not None: + ram_qs = ram_qs.filter( + _DJQ(TenantUUID__isnull=True) | _DJQ(TenantUUID=_tenant_bytes)) + if _company_bytes is not None: + ram_qs = ram_qs.filter( + _DJQ(CompanyUUID__isnull=True) | _DJQ(CompanyUUID=_company_bytes)) + ram_policy_apps = set(ram_qs.values_list('ResourceUUID', flat=True)) + if ram_policy_apps and role_uuid_bytes: + ram_granted_apps = set(_SdkRAMManager.ListResourcesForPrincipal( + role_uuid_bytes, 'app', 'view', + _tenant_bytes, _company_bytes, + )) + except Exception: + logger.exception('MenuListCompatView RAM 策略解析失败') + + for _app in App.objects.filter(AppStatus=1): + _app_uuid_bytes = _ToUUIDBytes(_app.AppUUID) + if AppManager.CheckAccess( + current_tenant_uuid, user.UserUUID, _app.AppCode, + CompanyUUID=current_company, + ) is None: + # 未安装 → 隐藏(现状) + _HideAppMenus(_app.AppCode, GetAppManifest(_app.AppCode)) + continue + # 已安装 → RAM 角色粒度过滤:配置了策略但当前角色未授权 → 隐藏 + if (_app_uuid_bytes in ram_policy_apps + and _app_uuid_bytes not in ram_granted_apps): + _HideAppMenus(_app.AppCode, GetAppManifest(_app.AppCode)) except Exception: logger.exception('MenuListCompatView 应用菜单过滤失败')