16 KiB
MSYNC 远端 App 注入 + RAM 资源访问管理 —— 方案设计
生成日期:2026-08-13 状态:方案草案(待评审) 关联:docs/architecture_debt_governance.md(治理进度)
1. 背景与目标
构想 A(MSYNC 联动页面):子服务器(如 ATestCServer)通过 MSYNC 连接主服务器后,把自己的定制页面作为"App"上报注册,主服务器将其安装到指定公司并注入员工菜单——即 GERP App 机制的跨服务器版,支撑定制化项目。
构想 B(权限精细化 + RAM):精细化租户/公司/角色权限系统,引入 RAM(Resource Access Management,资源访问管理) 角色机制,实现"指定公司的指定角色/员工可见指定资源"的角色粒度授权。
目标:两构想结合,形成定制化项目端到端能力:子服务器页面 → MSYNC 上报 → 主服务器注册 → 公司安装 → RAM 角色授权 → 员工菜单注入 → iframe 加载子服务器页面。
2. 现状盘点
2.1 App 机制(主服务器,已完整)
- app_manifests.py:
AppManifest清单(AppCode/AppName/Menus/FrontendRoute/ConfigKeys)+SyncAppManifests/InjectAppMenus/RemoveAppMenus;已预留 MSYNC 扩展:_REMOTE_MANIFESTS/RegisterRemoteManifest/Source='msync'(L222-248) - App 表(gerp/models.py L782):AppType(enterprise/personal/both)、MenuRootUUID、FrontendRoute/FrontendType/LegacySrc
- 安装矩阵:
TenantApp(租户级)/CompanyApp(公司级)/UserApp(个人级),AppManager 统一管理 - 访问校验:
AppAccessPermission(AppCode)+AppManager.CheckAccess(租户装 ∨ 公司装 ∨ 个人启用) - 菜单注入:安装 →
InjectAppMenus→ Menu 表 MenuStatus=1
2.2 菜单下发(MenuListCompatView)
四级过滤链(views/menu.py L34-219):
- 公司过滤:公有 + 当前公司私有(CompanyUUID)
- 祖先链校验:祖先不属于当前公司 → 整分支移除
- 管理员特权:admin:super/admin:tenant 跳过权限过滤(公司过滤仍生效)
- 权限过滤(非管理员):PermissionAttr 权限码 + PermissionLevel
- 应用过滤:未安装 App 的 Manifest 菜单全部隐藏(严格收紧,含管理员)
2.3 MSYNC 通信(主 ↔ 子,已完整)
- 服务端:sio.py(Socket.IO /msync 命名空间),事件注册/认证/心跳/发现/OGM/事务;业务事件统一
_RequireAuth(sid)校验 - 客户端:sio_client.py(MSyncSocketClient:注册/认证/心跳/Discover/OGM/Tx/CallRemote)
- 服务注册表:
ServiceNode(msync/models.py L19,含 ServiceDomain/ServiceMetadata/ServiceScope) - 服务令牌:
ServiceToken+ 远程自省/api/msync/auth/introspect/(子服务器 SubServerServiceAuthentication 用)
2.4 权限体系(现状缺口)
- 数据权限:RoleScope 四级(admin/dept_manager/workgroup_lead/member)+ TableScopeFilter 策略注册表(role_scope.py)——成熟
- 菜单权限:PermissionAttr 权限码二维(菜单↔权限码),非"角色↔资源"直连
- 缺口①:App 可见性 = 安装状态驱动(公司装了→全员可见),无角色粒度
- 缺口②:无统一"主体→资源→操作"授权模型(RAM)
3. 构想 A:MSYNC 远端 App 注入
3.1 总体流程
子服务器(定制项目) 主服务器(gvsds)
────────────────── ──────────────────
① MSYNC 连接/注册/认证(已有)
② 上报 AppManifest 清单
── msync:app:manifest:report ──────→ ③ 校验服务身份(已注册+认证)
(AppCode/AppName/Menus/ ④ 写入远端清单 → 同步 App 表(Source=msync)
FrontendRoute/LegacySrc) ⑤ 注入缓存:App/Menu 元数据落库
⑥ 管理员在应用中心安装(复用现有流程)
⑦ InjectAppMenus 注入菜单
⑧ RAM 角色授权(构想 B)
⑨ 员工登录 → MenuList 过滤 → 菜单出现 ──→ ⑩ 员工点击菜单
⑪ iframe 加载子服务器页面(子服务器托管)
⑫ 页面内调子服务器 API(用户令牌透传,见 3.5)
3.2 清单上报协议(msync:app:manifest:report)
- 事件:
msync:app:manifest:report(sio.py 新增 handler,模式同msync:ogm:register) - 鉴权:
_RequireAuth(sid)(服务必须已完成密钥认证);服务身份 =_sid_service_map[sid],清单归属该服务,防伪造 - 请求体(PascalCase):
{
"ServiceName": "atest-cserver",
"Manifests": [
{
"AppCode": "custom-project-x",
"AppName": "定制项目X",
"AppIcon": "Custom",
"AppDesc": "ATestCServer 注入的定制项目页面",
"AppType": "enterprise",
"FrontendRoute": "/remote/custom-project-x",
"FrontendType": "legacy",
"LegacySrc": "http://127.0.0.1:25080/pages/custom-project-x/home.html",
"Menus": [
{ "MenuName": "custom-x", "MenuTitle": "项目X", "MenuIcon": "Custom", "MenuSort": 1 },
{ "MenuName": "custom-x-home", "MenuTitle": "项目X首页", "MenuIcon": "Home",
"MenuLink": "http://127.0.0.1:25080/pages/custom-project-x/home.html",
"ParentMenuName": "custom-x", "MenuSort": 1 }
],
"ConfigKeys": []
}
]
}
- 响应:
{code:200, data:{Registered:[AppCode], Failed:[{AppCode, Reason}]}}(幂等 upsert) - 幂等:按 AppCode upsert;AppCode 冲突(本地/其他服务已占用)返回失败,需主服务器管理员仲裁
3.3 主服务器注册与注入缓存
App 表扩展(新迁移):
| 字段 | 类型 | 说明 |
|---|---|---|
| Source | CharField(16), default='local' | local / msync(清单来源) |
| ServiceName | CharField(128), null=True | 提供方服务名(msync 服务注册名) |
| ServiceDomain | CharField(256), null=True | 提供方服务域名(页面 base,iframe 用) |
| AppStatus | SmallIntegerField | 1启用/0禁用;远端 App 增加"离线"语义(见 3.6) |
注入缓存分层:
- DB 持久化(权威):App 表(Source/ServiceName/ServiceDomain)+ Menu 表注入行 + ServiceNode 注册表
- 进程级缓存(读快):
_REMOTE_MANIFESTS注册中心(现有)+ 远端清单→App 映射 - 不缓存:子服务器页面本体(子服务器托管,主服务器只缓存"入口元数据")
注册流程:on_manifest_report → 逐清单 _SyncRemoteApp(manifest, service)(写 App 表)+ 加入 _REMOTE_MANIFESTS + 回执。复用 _SyncAppRecord 模式。
3.4 角色粒度菜单注入
- 安装:复用
CompanyApp(公司级,指定公司安装)→InjectAppMenus注入菜单 - 角色粒度:
InjectAppMenus后菜单默认"公司内全员可见";要限定角色 → 走 RAM 授权(构想 B):- 菜单可见性 = 公司安装 AND RAM(角色→App, view)
- 菜单协议:注入的远端菜单
MenuLink为子服务器绝对 URL(如http://127.0.0.1:25080/pages/...);前端 legacy iframe 直接加载
3.5 跨服务器鉴权(iframe + 用户令牌透传)
现状:SubServerServiceAuthentication(Service 令牌远程自省)只覆盖"主→子服务调用";员工 iframe 直接访问子服务器页面时无用户身份。
方案(用户令牌透传):
- 主服务器新增事件
msync:user:token:issue:主服务器签发的 user-bound 短期令牌(JWT,claims: UserUUID/RoleUUID/CompanyUUID/TenantUUID,TTL 短如 10min,可刷新) - 前端(GERP)进入远端 App 时:请求主服务器 API → 取用户令牌 → 拼到子服务器页面 URL(
?token=或 header 注入) - 子服务器新增
UserTokenAuthentication:远程自省主服务器/api/msync/user/token/introspect/(模式同 ServiceToken 自省)→ 得到用户上下文 → 业务 API 按用户鉴权 - 安全:令牌 TTL 短 + 可撤销(撤销列表)+ 仅限目标服务域使用(claim
Audience=ServiceName)
备选简化(若定制项目不需要用户身份,只读公开页):iframe 加载公开页面,无令牌;有写操作的 API 走主服务器代理转发(主→子 Service 令牌)。
3.6 生命周期管理
| 事件 | 处理 |
|---|---|
子服务器 heartbeat 超时/msync:service:offline |
远端 App 标记 ServiceOnline=0;菜单保留但前端显示"服务离线"提示(或按租户配置自动停用 AppStatus=0) |
| 子服务器重新上线 | 重新上报清单 → 恢复 ServiceOnline=1 |
| 子服务器注销(deregister) | 远端 App 停用(AppStatus=0),菜单 MenuStatus=0(复用 RemoveAppMenus) |
| 清单更新(同 AppCode 新上报) | 幂等 upsert 菜单/App 字段 |
4. 构想 B:RAM 资源访问管理(Resource Access Management)
4.1 模型设计
借鉴 AWS RAM/IAM 资源授权思想,与现有 RBAC 融合:
| 概念 | 定义 | 现网对应 |
|---|---|---|
| 主体 Principal | 资源访问者 | Role(角色)/ User(用户) |
| 资源类型 ResourceType | 可被授权的资源类别 | App(应用)/ Menu(菜单)/ data_scope(数据权限,预留) |
| 资源 ResourceUUID | 具体资源实例 | AppUUID / MenuUUID |
| 操作 Action | 访问能力 | view(可见)/ use(使用)/ manage(管理)/ grant(授权) |
| 作用域 Scope | 资源归属维度 | TenantUUID(租户)/ CompanyUUID(公司) |
核心语义:RAMGrant 声明"某个主体 对 某资源 拥有 某操作 权限",且权限只在 作用域(租户/公司)内 生效。
4.2 表结构(新迁移)
class RAMGrant(QModel):
GrantUUID = UUIDField(primary_key=True) # 授权UUID
PrincipalType = CharField(16) # role / user
PrincipalUUID = UUIDField() # 角色或用户UUID
ResourceType = CharField(32) # app / menu / data_scope
ResourceUUID = UUIDField() # 资源UUID(AppUUID/MenuUUID)
Action = CharField(16) # view / use / manage / grant
TenantUUID = UUIDField(null=True) # 归属租户(null=跨租户,仅 super 可授)
CompanyUUID = UUIDField(null=True) # 归属公司(null=租户内全部公司)
GrantStatus = SmallIntegerField(default=1) # 1生效/0撤销
GrantBy = UUIDField(null=True) # 授权人
GrantTime = DateTimeField(auto_now_add=True)
class Meta:
db_table = 'RAMGrant'
unique_together = [('PrincipalType', 'PrincipalUUID', 'ResourceType',
'ResourceUUID', 'Action', 'TenantUUID', 'CompanyUUID')]
4.3 服务层(gvsdsdk/auth/ram.py,RAMManager)
GrantAccess(principal_type, principal_uuid, resource_type, resource_uuid,
action, tenant_uuid, company_uuid=None, grant_by=None) -> (ok, err)
RevokeAccess(grant_uuid) / RevokeByResource(resource_type, resource_uuid)
CheckAccess(principal_uuid, resource_type, resource_uuid, action,
tenant_uuid, company_uuid=None) -> bool
ListGrants(resource_type=None, resource_uuid=None, principal_uuid=None, ...)
ListResourcesForPrincipal(principal_uuid, resource_type, action, tenant_uuid,
company_uuid=None) -> [resource_uuid]
判定优先级(与现有体系一致):管理员(admin:super/tenant/company)→ True(豁免 RAM 校验);RoleType=1 管理岗默认可看其管理范围;其余按 RAMGrant 命中。
4.4 与现有权限体系融合
| 层 | 现有机制 | RAM 增强 |
|---|---|---|
| App 可见性 | AppManager.CheckAccess(安装状态) | AND RAM(角色→App, view):安装∩角色授权 |
| 菜单可见性 | PermissionAttr 权限码 | AND RAM(角色→Menu, view)(可选,默认不启用防性能损耗) |
| 数据权限 | RoleScope 四级 + TableScopeFilter | 预留 ResourceType=data_scope(本期不接管) |
菜单可见性公式(更新 MenuListCompatView):
可见 = 公司过滤(现状)
∩ (管理员特权 ∨ (权限码校验(现状) ∧ 应用安装(现状)
∧ (App 未配置 RAM ∨ RAM.CheckAccess(角色→App, view))))
性能:RAM 查询走 DB 索引 + 进程级缓存(角色→授权集合,TTL 短);仅对 Source=msync 或配置了 RAM 的 App 启用 RAM 过滤,本地 App 保持现状路径。
4.5 角色粒度授权示例
场景:公司 A 安装"定制项目X"(ATestCServer 上报),仅"财务角色"员工可见。
- 子服务器上报清单 → 主服务器注册 App(Source=msync)
- 管理员:
CompanyApp安装到公司 A →InjectAppMenus注入菜单 - 管理员:
RAMManager.GrantAccess('role', 财务角色UUID, 'app', AppX_UUID, 'view', 租户, 公司A) - 员工登录(财务角色,公司 A)→ MenuList 过滤 → 项目X 菜单出现
- 其他角色(公司 A)→ 菜单不出现;其他公司 → 公司过滤已拦截
5. 两构想结合:定制化项目端到端场景
① 子服务器(定制项目)MSYNC 连接 → 上报 AppManifest
② 主服务器注册远端 App(Source=msync)+ 注入缓存
③ 租户管理员应用中心安装(指定公司)
④ RAM 授权(角色粒度:谁的公司、哪个角色可见)
⑤ 该角色员工登录 → 菜单注入 → 点击 → iframe 加载子服务器页面
⑥ 页面内调子服务器 API → 用户令牌自省 → 业务执行 → 数据可经 OGM 回写主库
与现有能力的关系:
- 复用:AppManifest/AppManager/InjectAppMenus/菜单过滤/MSYNC 注册认证/ServiceToken 自省/OGM
- 新增:
msync:app:manifest:report事件、App.Source/ServiceName/ServiceDomain 字段、RAM 模型+服务层、用户令牌透传、生命周期离线管理、应用中心"远端来源"标记
6. 实施计划(建议分阶段)
| 阶段 | 内容 | 涉及仓库 | 状态 |
|---|---|---|---|
| P1 RAM 基础 | RAMGrant 表 + RAMManager + 菜单过滤集成(默认不启用 RAM 分支,向后兼容) | gvsdsdk + gvsds | ✅ 完成(2026-08-13) |
| P2 MSYNC 上报 | msync:app:manifest:report 事件 + App.Source/ServiceName 字段 + 注册同步 |
gvsdsdk + gvsds + ATestCServer | ✅ 完成(2026-08-13) |
| P3 角色粒度注入 | 安装/授权 UI(应用中心选公司+角色)+ 菜单过滤 RAM 启用 + 离线生命周期 | gvsds + GERP | ✅ 完成(2026-08-13) |
| P4 用户令牌透传 | 用户令牌签发/自省 + 子服务器 UserTokenAuthentication + 前端进入页 | gvsdsdk + gvsds + ATestCServer + GERP | ✅ 完成(2026-08-13) |
| P5 端到端 demo | ATestCServer 上报定制 App → 主服安装 → RAM 授权 → 员工菜单 → iframe 页面 | 全部 | ⏳ 待部署窗口(主服务器需重启加载新代码) |
P5 验证手册(部署后执行)
前置:主服务器重启加载新代码(App.Source 字段 / msync:app:manifest:report / RAM / 用户令牌端点),ATestCServer 可连主服务器。
# 1. 启动 ATestCServer(自动 MSYNC 桥接)
cd d:\SMANAGE\ATestCServer
.\start.ps1
# 2. 上报定制 App 清单(主服务器应用中心出现 custom-demo,Source=msync)
python manage.py report_manifest --app-code custom-demo --app-name 定制项目演示
# 3. 主服务器侧验证(Django shell)
# - App 表出现 Source=msync 记录
# - 应用中心安装到指定公司(可带 GrantRoles 角色授权)
# - 员工登录 → 菜单注入 → 点击 → iframe 加载子服务器页面
清理:App.objects.filter(AppCode='custom-demo').delete()(卸载演示 App)。
7. 待确认决策点
- App 表 Source/ServiceName 字段:直接加字段 vs AppCode 命名空间前缀(
{service}.{code})?——建议加字段(显式、可查询) - RAM 生效范围:仅对远端(Source=msync)App 启用 RAM 过滤,还是本地 App 也可配置角色授权?——建议先远端,本地后续
- 离线策略:子服务器下线时菜单自动隐藏(ServiceOnline=0)vs 保留+提示?——建议保留+提示(可配置)
- 用户令牌 TTL:10min 可刷新 vs 跟随主登录会话?——建议 10min + 刷新
- P1 阶段 RAM 是否接管数据权限(data_scope)——建议本期不接管,预留