Files
SMANAGE/docs/msync_remote_app_ram_design.md

16 KiB
Raw Permalink Blame History

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):

  1. 公司过滤:公有 + 当前公司私有(CompanyUUID)
  2. 祖先链校验:祖先不属于当前公司 → 整分支移除
  3. 管理员特权:admin:super/admin:tenant 跳过权限过滤(公司过滤仍生效)
  4. 权限过滤(非管理员):PermissionAttr 权限码 + PermissionLevel
  5. 应用过滤:未安装 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)

注入缓存分层:

  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 表结构(新迁移)

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 可连主服务器。

# 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)——建议本期不接管,预留