Files
SMANAGE/docs/msync_remote_app_ram_design.md

287 lines
16 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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](file:///d:/SMANAGE/gvsds/apps/gerp/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):
1. 公司过滤:公有 + 当前公司私有(CompanyUUID)
2. 祖先链校验:祖先不属于当前公司 → 整分支移除
3. 管理员特权:admin:super/admin:tenant 跳过权限过滤(公司过滤仍生效)
4. 权限过滤(非管理员):PermissionAttr 权限码 + PermissionLevel
5. 应用过滤:未安装 App 的 Manifest 菜单全部隐藏(严格收紧,含管理员)
### 2.3 MSYNC 通信(主 ↔ 子,已完整)
- 服务端:[sio.py](file:///d:/SMANAGE/gvsdsdk/msync/sio.py)(Socket.IO /msync 命名空间),事件注册/认证/心跳/发现/OGM/事务;业务事件统一 `_RequireAuth(sid)` 校验
- 客户端:[sio_client.py](file:///d:/SMANAGE/ATestCServer/msync_client/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):
```json
{
"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) |
**注入缓存分层**:
1. **DB 持久化**(权威):App 表(Source/ServiceName/ServiceDomain)+ Menu 表注入行 + ServiceNode 注册表
2. **进程级缓存**(读快):`_REMOTE_MANIFESTS` 注册中心(现有)+ 远端清单→App 映射
3. **不缓存**:子服务器页面本体(子服务器托管,主服务器只缓存"入口元数据")
**注册流程**:`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 直接访问子服务器页面时**无用户身份**。
**方案(用户令牌透传)**:
1. 主服务器新增事件 `msync:user:token:issue`:主服务器签发的 **user-bound 短期令牌**(JWT,claims: UserUUID/RoleUUID/CompanyUUID/TenantUUID,TTL 短如 10min,可刷新)
2. 前端(GERP)进入远端 App 时:请求主服务器 API → 取用户令牌 → 拼到子服务器页面 URL(`?token=` 或 header 注入)
3. 子服务器新增 `UserTokenAuthentication`:远程自省主服务器 `/api/msync/user/token/introspect/`(模式同 ServiceToken 自省)→ 得到用户上下文 → 业务 API 按用户鉴权
4. **安全**:令牌 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 表结构(新迁移)
```python
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 上报),仅"财务角色"员工可见。
1. 子服务器上报清单 → 主服务器注册 App(Source=msync)
2. 管理员:`CompanyApp` 安装到公司 A → `InjectAppMenus` 注入菜单
3. 管理员:`RAMManager.GrantAccess('role', 财务角色UUID, 'app', AppX_UUID, 'view', 租户, 公司A)`
4. 员工登录(财务角色,公司 A)→ MenuList 过滤 → 项目X 菜单出现
5. 其他角色(公司 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 可连主服务器。
```powershell
# 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. 待确认决策点
1. **App 表 Source/ServiceName 字段**:直接加字段 vs AppCode 命名空间前缀(`{service}.{code}`)?——建议加字段(显式、可查询)
2. **RAM 生效范围**:仅对远端(Source=msync)App 启用 RAM 过滤,还是本地 App 也可配置角色授权?——建议先远端,本地后续
3. **离线策略**:子服务器下线时菜单自动隐藏(ServiceOnline=0)vs 保留+提示?——建议保留+提示(可配置)
4. **用户令牌 TTL**:10min 可刷新 vs 跟随主登录会话?——建议 10min + 刷新
5. **P1 阶段 RAM 是否接管数据权限**(data_scope)——建议本期不接管,预留