上一篇,我给同一张发票明细表加了日期、客户、地区、商品、税率和金额等 8 类筛选条件,没有再为每个条件写一套查询接口。
但如果这张表真的接进业务系统,还有一个问题比筛选器更早:谁能看到哪些数据?
这次我仍然使用 WideWorldImporters OLTP 的发票明细,提交同一个 QueryModel、同一份 DSL、同一个日期范围,只替换调用者的权限上下文。
实际结果如下:
| 用户权限 | 可见销售区域 | 发票明细数 | 含税金额 |
|---|---|---|---|
| 东南区用户 | Southeast | 6,497 | 5,844,653.75 |
| 新英格兰区用户 | New England | 1,620 | 1,448,738.78 |
两次请求里没有任何一个地方由 AI 填写"我的权限区域"。
两次生成的 SQL 结构也相同,最后都被强制追加:
sql
and d2.sales_territory = ?
区别只在受信权限上下文绑定的参数:一次是 Southeast,一次是 New England。
这篇不先讲 RBAC、ABAC 或权限治理方法论,只回答一个具体问题:Foggy 接进已有业务系统后,怎样让页面、查询 API 和 AI 继续遵守原来的数据范围?
同一份 DSL 里没有权限参数
两个用户提交的都是下面这份请求:
json
{
"limit": 1,
"columns": [
"count(invoiceLineId) as invoiceLineCount",
"sum(totalIncludingTax) as totalIncludingTax"
],
"slice": [
{
"field": "invoiceDate$id",
"op": "[)",
"value": ["2016-01-01", "2016-06-01"]
}
]
}
这里的日期是业务筛选条件。用户可以改成某个月,也可以继续增加商品、客户或金额范围。
销售区域却不是普通查询参数。
如果把权限也设计成:
json
{"field": "customer$salesTerritory", "value": "Southeast"}
那它只是一个筛选器。调用者可以删除它,也可以把值换成另一个区域。AI 同样可能在拼装请求时漏掉它。
业务条件回答"用户想查什么",系统权限回答"这个用户最多能查到什么"。两者必须从不同入口进入查询。
权限条件在 QM 内部强制生效
我为本轮验证建立了一个权限专用 QM:
javascript
const invoiceLines = loadTableModel('WwiOltpInvoiceLineModel');
export const queryModel = {
name: 'wwi_oltp_invoice_lines_authorized',
model: invoiceLines,
modelPermissions: {
mode: 'resolver',
resolver: (context) => {
let territory;
if (context.authorization == 'Bearer demo-territory-southeast') {
territory = 'Southeast';
} else if (context.authorization == 'Bearer demo-territory-new-england') {
territory = 'New England';
} else {
return { allow: false };
}
return {
allow: true,
rowPredicates: [
context.predicate.eq(
invoiceLines.customer$salesTerritory,
territory
)
]
};
}
}
};
这里的两个 token 只是 disposable 测试身份,便于把证据完整复现出来。真实项目不会在 QM 里维护用户清单,而是由业务系统、身份服务或可信网关校验登录态,再把 opaque Authorization 交给权限解析器。
当前 resolver 能按动作决定是否允许模型访问,并返回类型化的行条件。它拿到的是宿主传入的权限上下文,不是 AI 在 DSL 中声明的角色或区域。
用户主动筛另一个区域会怎样
为了确认这个条件不是"默认筛选",我让东南区用户主动提交:
json
{
"field": "customer$salesTerritory",
"op": "=",
"value": "New England"
}
如果普通 DSL 能覆盖系统权限,这次就会越权看到 New England。
实际生成的 WHERE 同时保留了两条条件:
sql
and d2.sales_territory = ?
and d2.sales_territory = ?
参数依次为:
text
New England
Southeast
结果是 0 行。
用户可以继续收窄自己的范围,但不能把 Southeast 改成 New England,也不能通过删除 slice 去掉它。即使请求完全不带区域条件,resolver 仍然会追加 Southeast。
这才是"系统权限先于页面和 AI 生效",而不是提示 AI 每次记得补一条 WHERE。
没有身份时不是返回全量
本轮还执行了两个反例:
- 不带
Authorization。 - 带一个未配置的 token。
两种情况都拒绝模型执行,返回:
text
The requested model operation is not available.
它没有在解析失败时退回 29,458 条全量发票明细。
模型 describe 也受相同入口控制:无权限时返回 MODEL_ACCESS_DENIED,合法上下文才能取得字段元数据。这一点很重要,因为"不能执行查询,但仍把敏感模型和字段完整暴露出来"也不是一个完整的权限边界。
这和 Runtime 登录不是一回事
权限接入时最容易混淆四件事:
| 层次 | 它回答的问题 |
|---|---|
| Runtime 认证 | 这个调用方能不能访问 Runtime API |
| 模型权限 | 能不能 describe、validate、execute 这个 QM |
| 行权限 | 在这个 QM 中最多能看到哪些业务记录 |
| 字段权限 | 哪些列允许被描述、选择或返回 |
namespace 也不等于用户权限。它用于隔离和定位一组模型、数据源与资源,不能替代"销售 A 只能看自己区域"这样的业务规则。
模型权限不会替代入口认证。如果浏览器可以随便伪造 Bearer demo-territory-southeast,这个演示 token 本身当然不安全。生产部署必须先校验登录态、签名、有效期和调用来源,再把可信权限上下文交给 Foggy。
已有业务系统可以怎样接
企业不一定需要先建设一套新的权限中心。现阶段可以按原系统的部署方式选择接入点。
方案一:独立 Runtime 解析 opaque Authorization
这就是本篇跑通的主链。
Runtime Query API 接收 Authorization,QM 的 modelPermissions.resolver 根据当前动作和身份结果决定:
- 是否允许访问模型;
- 应该强制追加哪些行条件。
它适合已经有统一网关、SSO、服务 token 或权限服务,希望 Foggy 独立部署的项目。
Foggy 当前已经支持 Authorization 透传、模型动作授权、typed row predicate 和 fail closed。生产部署仍需要接入企业实际使用的 token 校验、RBAC / 租户映射与审计,但这不影响权限解析和强制行条件已经可以进入查询主链。
这里不一定要为每种权限系统开发一个 Java 适配器。
FSScript 已经提供全局 get / post。resolver 可以把 context.authorization 原样交给企业已有的权限接口,再把接口结果转换成 Foggy 的模型动作和行条件:
javascript
modelPermissions: {
mode: 'resolver',
resolver: (context) => {
const remote = post({
url: 'https://permission.example.internal/v1/model/resolve',
headers: {
Authorization: context.authorization
},
body: {
namespace: context.namespace,
model: context.model,
action: context.action
},
responseType: 'map'
});
if (remote.allowed !== true) {
return {
allow: false,
decisionId: remote.decisionId,
policyVersion: remote.policyVersion
};
}
return {
allow: true,
attributes: remote.attributes,
rowPredicates: [
context.predicate.in(
invoiceLines.customer$id,
remote.customerIds
)
],
decisionId: remote.decisionId,
policyVersion: remote.policyVersion,
expiresAt: remote.expiresAt
};
}
}
这样,企业可以修改 QM / FSScript 来适配已有权限 API,完成 validate 后刷新或发布模型,不需要修改、重新编译和发布 Java 应用。
开发或自托管 external bundle 还可以通过 watch=true 监听 .tm、.qm 和 .fsscript 变化,自动清理受影响脚本并推进 source revision。但"随写随发布"不等于普通用户可以在线改权限:模型作者属于可信计算基,生产环境仍应限制发布权限,并保留验证、版本和审计流程。
平台 HTTP 客户端已经处理了连接 / 请求超时、最大响应大小、非 2xx 失败、敏感内容不落日志,以及跨 origin 重定向时移除 Authorization 和 Cookie。外部权限接口超时、报错或 resolver 返回非法结构时,受保护模型拒绝操作,而不是退回全量数据。
token 应放在 HTTPS Authorization Header 中,不要为了方便拼进 GET query。query 参数可能进入代理、网关和访问日志,而 Header 才能沿用现有身份链路的脱敏与转发规则。
仍有一个成本必须正视:如果每次查询都同步调用权限服务,权限接口的延迟和可用性会进入查询主链。一次请求内相同 namespace + model + action 的决策会复用,但下一次 HTTP 查询会重新求值。生产设计需要为权限服务准备稳定 SLA,并谨慎设计带有效期的决策与缓存隔离。
方案二:Spring 内嵌时复用宿主 Bean
如果 Foggy 直接嵌入现有 Spring Boot 业务系统,QM 可以通过 accesses 调用宿主 Bean 或 helper,读取当前登录用户、组织、门店或客户范围:
javascript
accesses: [{
queryBuilder: (context) => {
const user = getCurrentUser(context);
context.query.and(sales.store$id, user.storeKey);
}
}]
这种方式不需要把原系统的用户表复制进 Foggy。原业务系统仍然负责"他是谁"和"他属于哪些组织",QM 负责保证这些约束进入每次查询。
Spring Bean 不是内嵌部署的唯一选择。accesses 的 queryBuilder 本身就是 FSScript,也能从 context.authorization 取得 token,再使用相同的 get / post 调用外部权限接口。因此即使 Foggy 跟随 Spring Boot 一起部署,只要现有系统已经提供权限 API,也可以完全通过脚本完成适配,不必新增 Java Bean。
两种做法的选择很直接:已有可复用的本地登录上下文、复杂签名、mTLS 或宿主私有能力时,用 Bean;权限已经以稳定 HTTP API 提供时,用 FSScript get/post,适配代码可以随模型发布。
对于新模型,更推荐把远程权限调用集中在前面的 modelPermissions.resolver:它能同时处理 DESCRIBE、VALIDATE、EXECUTE 等动作,并把权限结果统一转换为 attributes 和 typed row predicates。accesses 仍可直接读取 Authorization 或调用外部接口来兼容既有行权限,但不要在多个 accesses 中重复请求权限系统,否则同一次查询可能得到多份不一致决策,也会放大网络开销。
现有 focused 测试已经验证:用户 slice、排序和权限 WHERE 会同时存在,普通 DSL 不会移除 accesses 注入的条件。本篇的双身份实数则来自前面的 Runtime resolver 主链,两类证据没有混写成同一个案例。
方案三:让数据库原生 RLS 兜底
如果业务身份可以可靠映射到数据库连接或 session context,也可以继续使用数据库 Row-Level Security。
Wide World Importers 本身就带有按 Sales Territory 限制客户范围的 RLS 示例。它的优势是无论查询来自 Foggy、报表还是其他客户端,数据库都执行最后一道限制。
它的代价也很实际:许多业务系统共用同一个数据库账号,真正的用户、角色和组织规则只存在于应用层。此时必须设计连接身份或 session context 的映射,不能因为数据库账号叫 readonly 就认为它已经具备业务行权限。
本轮没有为了文章创建数据库用户或改 RLS 配置,因此这里只把它作为 WWI 已有的数据库侧方案,不冒充本轮双身份执行证据。
方案四:可信业务网关先算好权限投影
Odoo、TMS、ERP 或已有 API 网关也可以先完成身份与权限计算,再调用内部查询引擎。
内部治理契约可以携带:
text
systemSlice
fieldAccess
deniedColumns
policySnapshotId
例如宿主把"当前用户可见客户 ID 范围"转换成 systemSlice,把敏感金额列转换成 deniedColumns,同时保留本次策略快照用于审计。
关键边界是:这些字段只能由可信服务端生成,不能出现在普通 Runtime Query API 或 AI 可自由提交的 DSL 中。否则所谓系统权限又会退化成调用者自己填写的筛选条件。
Foggy 已经提供这些内部治理入口。本轮没有搭建一个真实 ERP 网关做端到端验证,所以它在本文中是接入方案,不是第四个成功案例。
一个 QM 能不能承载所有权限
同一份 QM 可以让页面、API 和 AI 共用字段、JOIN、指标和权限入口,但这不意味着所有用户必须看到完全相同的 schema。
如果只需要按组织、门店、区域或客户归属限制事实行,可以继续使用同一个 QM 的行权限。
如果不同角色的业务含义、可用指标和明细粒度已经明显不同,使用共享 TM / 指标定义下的多个 QM 往往更清楚。例如普通销售只开放客户汇总,财务角色才开放发票明细与利润字段。
这次只完成了行权限证据。我没有为了让文章看起来完整,虚构字段权限已经端到端跑通。
这次能确认什么,不能确认什么
已经确认:
- 同一 QM、同一 DSL 能根据 opaque Authorization 注入不同的强制行条件。
- 两个身份的结果与 WWI 按区域分组的只读基线一致。
- 用户条件只能继续收窄,不能删除或放宽系统权限。
- 无身份和未知身份 fail closed。
- describe、validate、execute 已有动作化权限入口。
尚未确认:
- 任意企业 SSO、JWT、RBAC 或多租户系统可以零配置接入。
- 字段权限和完整审计已经完成本轮端到端验证。
- 一个 demo token 可以代表生产认证安全。
对于有业务系统、有数据库、有 IT 或全栈人员,但还没有成熟 BI / AI 平台的企业,我更希望权限接入从这里开始:先复用原系统已经可信的一小部分用户与数据范围,跑通一个销售 QM,而不是等一套大而全权限平台全部建完才交付第一张表。
下一篇再继续向业务页面走:AI 查到一批客户以后,怎样打开成可筛选、分页、汇总,并能进入客户详情的表格,同时不丢掉这次已经绑定的权限上下文。