策略语言
面向需要字段确切含义的策略作者、评审者和工具开发者。本页是文档格式(schema: 2)的参考。这些字段如何组合成决策,见安全模型;如何安全地修改文档,见测试策略。
所有授权数据都保存在三种文档中。文档按严格模式读取、整体编译:未知字段、重复的键、JSON 类型不对的值、无法编译的模式或条件、悬空的引用,都会让整个文档失败。失败的文档永远不会被安装,所有需要它的请求都失败即拒绝:除非有 DENY 匹配,否则返回 503(失败即拒绝)。
文档
| 文档 | 键 | 每个……一份 | 包含 |
|---|---|---|---|
| 平台文档 | platform | 部署 | 所有租户共享的策略、面向任何人(*)和指定主体的绑定、break-glass 绑定、限流规则,以及只有平台能设置的 limits、attributes、boundaries。 |
| 应用文档 | app:<appId> | 应用 | 应用的目录、权限策略 perm:<appId>/<permission>、系统角色、角色绑定、限流规则。 |
| 租户文档 | tenant:<tenantId> | 租户 | 租户自己的策略、它的绑定(任何人、它的主体、它的角色)、它的 boundary、自定义角色、限流规则。 |
键是控制面对文档的称呼。每个请求都读取平台文档和它所属租户的文档(匿名请求:租户 (0))。只有当主体在请求的空间中至少拥有一个角色,或租户文档把该应用的某个权限策略绑定给任何人或某个主体时,请求才会读取其应用的应用文档;被黑名单拦截的请求只读取平台文档。没有发布过的文档按空文档读取。哪些候选适用、按什么顺序,见安全模型。
一个租户文档:
{
"schema": 2,
"revision": 3,
"publishedAt": "2026-11-01T08:00:00Z",
"policies": [
{
"id": "order-reader",
"statements": [
{
"name": "read-orders",
"effect": "allow",
"action": "order:read",
"resource": "order/**"
}
]
}
],
"bindings": [
{
"id": "support-reads-orders",
"subject": { "role": "support" },
"app": "order",
"space": "",
"policy": "order-reader"
}
]
}文档头
| 字段 | 类型 | 必填 | 含义 |
|---|---|---|---|
schema | 整数 | 是 | 格式版本,固定为 2。其他值都会让文档失败。 |
revision | 整数 | 是 | 正数。每次发布写入下一个修订。 |
publishedAt | 字符串 | 是 | 带 Z 或偏移量的 ISO-8601 时刻,例如 2026-11-01T08:00:00Z。 |
provenance | 对象 | 否 | 该修订是如何发布的(来源记录)。由控制面和 cosec-migrate 写入。 |
通过控制面提交整份文档(:apply)时,文档头必须存在且合法,但控制面发布时会写入它自己的 revision、publishedAt 和 provenance。
各文档的字段
| 字段 | 平台 | 应用 | 租户 |
|---|---|---|---|
policies、bindings、rateLimits | 是 | 是 | 是 |
catalog | 是 | ||
roles | 是 | 是 | |
limits、attributes | 是 | ||
boundaries | 是 | ||
boundary | 是 |
文档出现其种类不接受的字段时,该文档失败。
策略
策略是一组有名字的语句。只有绑定或附加策略让它适用于某人时,它才起作用。
| 字段 | 类型 | 默认值 | 含义 |
|---|---|---|---|
id | 字符串 | 必填 | 在所属文档内唯一。规则见下。 |
attachable | 布尔 | false | 主体能否附加它。 |
boundary | 布尔 | false | 平台边界策略。 |
condition | 条件 | 无 | 守护策略的每条语句。它不能读取路径变量。每个请求最多求值一次,并且只在某条语句的动作和资源已匹配之后求值。 |
statements | 数组 | 必填 | 语句。 |
策略 id:
perm:<appId>/<permission>是应用的权限策略。只有该应用的文档能定义它,且它不能被附加(角色权限)。cosec:blacklist、cosec:root、cosec:platform-admin、cosec:tenant-admin和cosec:tenant-viewer是内置策略,任何文档都不能定义它们。控制面还会拒绝以cosec:、g:或r:开头的新 id,这些前缀保留给内置策略和cosec-migrate。- 租户策略不能复用平台策略的 id。控制面会拒绝;在数据面,绑定或附加策略指向这样的 id 时,除非有 DENY 匹配或两个策略内容相同,请求为 Indeterminate(
503)。
语句
当语句的某个动作模式匹配请求的动作、某个资源模式匹配请求的资源、其条件为真且其所属策略的条件为真时,语句匹配请求。
| 字段 | 类型 | 默认值 | 含义 |
|---|---|---|---|
name | 字符串 | "" | 出现在决策、追踪和 explain 中。 |
effect | "allow" 或 "deny" | 必填 | |
action | 动作模式,或其非空数组 | 必填 | 所有动作写 "*"。 |
resource | 资源模式或模式对象,或其非空数组 | 任意资源 | |
condition | 条件 | 恒为真 | |
onMissing | "indeterminate" 或 "nomatch" | "indeterminate" | 无法获取的外部属性意味着什么(外部属性)。只用于读取外部属性的条件。 |
onMissingConfirmed | 对象 | 无 | 谁在哪个变更中确认了一条 onMissing: "nomatch" 的 DENY:{"by": {"tenant", "principal"}, "change"}。只有控制面写入它。 |
DENY 不能写裸资源类型,例如 order:它只覆盖 id 为空的那个资源。要覆盖所有订单,写 order/**。这样的语句无法编译。
动作
| 模式 | 匹配 |
|---|---|
service:verb | 仅该动作。 |
service:* | 该服务的所有动词。 |
* | 所有动作。 |
没有其他通配符。服务和动词精确比较,区分大小写(http:get 不是 http:GET)。两者都不能为空,也不能包含 :、*、空白或控制字符。
请求的动作从哪里来:
- 路由在服务路由映射中的 HTTP 请求,动作和类型化资源取自该路由的
service:verb。 - 其他 HTTP 请求的动作是
http:<METHOD>(按收到的方法,例如http:GET),资源类型为http、id 为请求路径。针对service:verb的语句永远不会匹配它,针对http:*的语句也永远不会匹配已映射的请求。 - 代码中的检查自己指定动作和资源(代码中的检查)。
路由映射
路由映射(cosec-routes.json,从 cosec.authorization.routes.location 加载,见配置)为服务的 HTTP 路由指定动作和类型化资源:
{
"service": "order",
"routes": [
{ "method": "GET", "path": "/orders/{orderId}", "action": "order:read", "resource": "order/{orderId}" },
{ "method": "POST", "path": "/orders/{orderId}/cancel", "action": "order:cancel", "resource": "order/{orderId}" }
]
}path 是 HTTP 路径模式。resource 是类型化资源,其片段是字面量或 path 中的变量。路由映射要么整体加载,要么不加载:没有路由的映射、有格式错误的路由,或同一方法下两条可能匹配同一请求的路由,都会让启动失败。包含 /(解码后的 %2F)的捕获值会改变资源 id 的片段,因此这样的请求会以 400 被拒绝。网关不加载路由映射:它的请求留在 http 命名空间。cosecServiceGenerator/routes 可以从服务的 OpenAPI 文档生成路由映射(角色权限)。
资源
资源由类型和 id 组成:order/42 是 order 和 42,HTTP 路径是 http 和 /orders/42。
类型化资源
type,或 type/segment/...。每个片段是以下之一:
| 片段 | 匹配 |
|---|---|
| 字面量 | 完全相同的文本,区分大小写 |
* | 一个非空片段 |
{name} | 一个非空片段,捕获为路径变量 |
**,只能在最后 | 任意数量的后续片段,包括零个 |
| 模式 | order(id 为空) | order/42 | order/42/items |
|---|---|---|---|
order | 是 | ||
order/* | 是 | ||
order/{orderId} | 是 | ||
order/** | 是 | 是 | 是 |
order/*/items | 是 |
id 按 / 切分并按原样比较:从不解码,从不去除空白。类型不能是 http,也不能包含 :;字面量不能包含 *、?、{、}、空白或控制字符。
HTTP 路径
http:path:<pattern> 匹配类型为 http、id 为请求路径(经管道规范化,见 Web 集成)的资源。在以下子集上,它与 Spring 的 PathPattern 匹配结果一致:
| 元素 | 匹配 |
|---|---|
| 字面量片段 | 该片段 |
? | 一个字符 |
片段内的 * | 零个或多个字符,例如 /files/*.pdf |
作为片段的 * | 一个片段 |
{name} | 一个片段,并捕获 |
** 或 {*name} | 任意数量的片段,{*name} 把路径余下部分捕获下来;只能在开头或结尾,每个模式至多一个。/admin/** 也匹配 /admin。 |
以下写法无法编译:{name:regex}、空模式、除单独的 * 之外可能匹配空值的片段(例如 {a}{b} 或 *{a})、{} 和 {*},以及只含代理对的一半、缺另一半的文本(基本多文种平面之外的字符,例如 emoji,要完整写出)。
资源可以写成带选项的模式对象,仅限 http:path: 模式:
{ "pattern": "http:path:/Reports/{id}", "caseSensitive": false, "decodeAndParseSegments": true }| 选项 | 默认值 | 含义 |
|---|---|---|
caseSensitive | true | 为 false 时比较字面量忽略 ASCII 大小写。 |
decodeAndParseSegments | true | 为 true 时按百分号解码、去掉 ; 参数后的值匹配每个片段;为 false 时按收到的原文匹配,但 %2F 仍读作 /。 |
路径模式的上界
一个请求匹配某个文档的所有模式的开销有上界(DOC-4),与条件一样(上限)。超出上界的模式无法编译:
| 上界 | 限制 |
|---|---|
http:path: 模式 | http:path: 之后最多 2,048 个字符,即管道接受的最长路径 |
| 类型化资源模式 | 最多 2,058 个字符 |
把 *、? 或 {name} 与其他文本混在一起的片段,例如 report-*.pdf | 最多 16 个部分:每个 *、? 和 {name},以及它们之间的每段文本。连续的 * 算一个:a**b 就是 a*b |
| 一个文档中所有这样的片段,跨越它的策略、边界和限流规则 | 合计最多 2,048 个通配单位:每个片段的部分数加上其中文本的字符数,所以 report-*.pdf 是 3 个部分和 11 个字符,共 14 个单位。前导 ** 或 {*name} 之后的片段计两次,key 读取捕获变量的限流规则也计两次 |
字面量片段、整段的 * 或 {name}、** 和 {*name} 不计开销:只有把它们与文本混在一起的片段需要搜索匹配,而其中的文本可能在请求片段的每个位置上都被比较一次,所以一个字符与一个部分的开销相同。前导 ** 之后的片段会与两个片段比较,以捕获变量为 key 的规则先匹配再捕获:因此它们计两次。这些上界在 8.0 中是固定的。在数据面上,超出上界的已存储文档会被装入为失败,读取它的请求失败即拒绝。
无论有多少条绑定生效,每个策略在每一遍中对每个绑定它的文档最多求值一次,另加附加策略的一次求值(ALG-9)。因此预算限定了匹配一个文档的模式给一个请求带来的开销:一个文档把全部预算都用在对每条路径都是候选的模式上时,对管道接受的最长路径片段(256 个字符),最坏情况下每个请求约 0.5 ms,其策略同时被附加时约为两倍(GlobBoundBenchmark 的最坏输入,在 CI 运行器上)。最坏的是由代理对(例如 emoji)组成、对着 ? 和 * 匹配的片段;ASCII 时约 0.33 ms。它分配所捕获的值:ASCII 值时每个请求约 0.25 MB,Latin-1 以外的值时约 0.6 MB。影子窗口还会求值它的候选文档,候选文档有自己的预算。
路径变量
类型化或 HTTP 模式中的 {name},以及 HTTP 模式中的 {*name},会捕获一个变量。只有捕获它的那条语句能读取它,形式为 resource.var.<name>,用在该语句自己的条件或限流规则的 key 中:
{
"name": "own-profile",
"effect": "allow",
"action": "http:*",
"resource": "http:path:/users/{userId}/**",
"condition": { "eq": ["resource.var.userId", { "attr": "subject.id" }] }
}- 语句的每个资源模式都必须捕获它读取的变量,否则语句无法编译:某个模式没有捕获的变量会读作缺失,针对它写的 DENY 会悄悄失效。策略条件不能读取任何变量。
- 变量名是 Java 标识符,另外允许包含
-;同一模式中一个名字只能捕获一次。 - 捕获的值是该片段解码后的值(
{*name}为路径余下部分)。值只会被比较,永远不会成为模式的一部分,所以 id 为**的主体无法扩大任何范围。
条件
条件是只含一个运算符的 JSON 对象;ignoreCase 可以与 eq 和 prefix 并列。每个属性路径和每个操作数类型都在文档编译时检查。
{ "and": [
{ "eq": ["subject.authenticated", true] },
{ "in": ["scope.space", ["team-a", "team-b"]] },
{ "not": { "cidr": ["environment.ip", ["10.0.0.0/8"]] } },
{ "eq": ["resource.var.ownerId", { "attr": "subject.id" }] }
] }运算符
| 运算符 | 形式 | 为真当 | 属性类型 |
|---|---|---|---|
eq | [attribute, literal] | 值等于该字面量。加 "ignoreCase": true(仅 String)时忽略 ASCII 大小写。Ip 属性与地址比较。 | String、Bool、Ip |
eq | [attribute, {"attr": attribute}] | 两个值都存在且相等;两个属性类型相同 | String、Bool、Ip |
in | [attribute, [literals]] | 值是其中某个字面量:1 到 maxInValues 个 | String、Bool、Ip |
in | [literal, attribute] | 集合包含该字符串字面量 | Set |
prefix | [attribute, "text"] | 值以该文本开头;可加 ignoreCase。对 Labels 属性按完整标签匹配:com.example 匹配 example.com 和 api.example.com,不匹配 badexample.com。 | String、Labels |
cidr | [attribute, ["block", ...]] | 地址落在某个 IPv4 或 IPv6 网段中:1 到 maxCidrBlocks 个 | Ip |
time | {...} | 请求时间在时间窗口内(时间条件) | |
exists | "attribute" | 属性存在(Labels 属性看其主机) | 任意 |
and | [conditions] | 全部为真;至少一个,从左到右,遇到第一个假即停止 | |
or | [conditions] | 任一为真;至少一个,从左到右,遇到第一个真即停止 | |
not | condition | 条件为假 |
字面量是 JSON 字符串或布尔值,从不是数字;字符串永远不会被当作布尔值。没有 true 或 false 条件:不需要条件时省略 condition。
属性
| 属性 | 类型 | 值 |
|---|---|---|
subject.id | String | 主体 id;匿名主体时缺失。 |
subject.tenant | String | 请求的租户:主体的租户,匿名时为 (0)。 |
subject.authenticated | Bool | 凭证是否被接受。 |
subject.roles | Set | 主体在任意空间中的角色 id。 |
subject.scopedRoles | Set | 按声明形式给出的角色:默认空间为 role,其他空间为 role@space。 |
subject.attributes.<name> | String | 令牌 attributes 声明中的条目,转为文本(认证)。 |
scope.tenant | String | 同 subject.tenant。 |
scope.app | String | 请求的应用;没有应用时缺失。 |
scope.space | String | 请求的空间;默认空间为 ""。 |
scope.roles | Set | 主体在请求空间中的角色。 |
action.service、action.verb | String | 动作的两部分。 |
resource.type、resource.id | String | 资源的两部分。 |
resource.attributes.<name> | String | 资源的属性。请求管道和文档中列出的检查都不设置;对象级规则把事实写进资源 id(对象级规则)。 |
resource.var.<name> | String | 路径变量。 |
environment.ip | Ip | 客户端地址(Web 集成)。 |
environment.time | Instant | 请求时间;只有 time 读取它。 |
environment.device | String | CoSec-Device-Id 参数;为空时缺失。 |
environment.blocked | Bool | 黑名单是否拦截该请求。 |
http.header.<name> | String | 请求头 <name>;请求没有该头时缺失。见请求头。 |
http.origin、http.referer | String | Origin 和 Referer 请求头,读取方式与 http.header.* 相同。 |
http.origin.host、http.referer.host | String | Origin 和 Referer URL 中的主机,小写;值不是带主机的 URL 时缺失。 |
http.origin.host.labels、http.referer.host.labels | Labels | 该主机的标签,倒序(api.example.com 为 com.example.api),小写;供 prefix 使用。 |
request.attributes.<name> | String | 宿主掌握的 HTTP 之外的请求信息。网关设置 request.attributes.cosec.audit.routeId,即请求经过的路由 id;还设置 request.attributes.cosec.identity.forward:在接收转发身份的路由上(lb:// 路由,或元数据为 cosec-identity: true 的路由)为 true,在其他路由上为 false。WebFlux 和 WebMvc 不设置任何值。 |
ext.<provider>.<name> | 声明的类型 | 外部属性。 |
不在此表中的路径无法编译。
请求头
请求头名称是 ASCII token(RFC 9110:字母、数字和 !#$%&'*+-.^_`|~),其他名称无法编译。名称不区分大小写,只折叠 ASCII 字母,文档一侧和请求一侧都是如此:http.header.X-Team 就是 http.header.x-team,任何 Unicode 大小写映射都不会让两个不同的名称相同。
任何属性引用都不能读取携带凭证的请求头:http.header.authorization、proxy-authorization、cookie、set-cookie、 x-api-key 和 cosec-identity,大小写不限,条件和限流规则的 key 都是如此。含有这种引用的文档无法编译,无论经由控制面还是 cosec-migrate:在 authorization 上用 prefix 写的 DENY 会让文档作者逐个字符地获知其他主体的令牌,以它为 key 则会按令牌计数。cosec-identity 是网关转发给后端服务的请求头,在网关上它只是客户端的声明。这份列表是固定的 (ConditionChecker.CREDENTIAL_HEADERS), 再加上宿主读取 API key 的请求头(用 cosec.pipeline.api-key-header 改名时):starter 在数据面和控制面编译每个文档时也拒绝它,管道也从不为条件读取它。
同一请求头出现多次且每份值都相同时,读作该值。各份值不同时,该请求头没有单一的值,第一份或最后一份值也永远不会单独决定结果: 只要请求所依据的文档中任何语句、策略条件、边界或限流 key 读取这个请求头,管道就在求值之前回答 400 invalid-request(PIP-12)。 无论语句的效果是什么、能否作用于该请求都是如此,因此这个回答既不告诉客户端有 DENY 读取这个请求头,也不告诉它是哪一个。
匹配角色
角色通过角色绑定授予权限,而不是通过条件。确实需要在条件中判断角色时,检查集合:
{ "in": ["auditor", "scope.roles"] }scope.roles 只包含请求空间中的角色,通常这就是你要的;subject.roles 包含角色而不论它在哪个空间;subject.scopedRoles 包含 role@space 形式的字符串。匿名主体没有角色。
缺失值
请求没有某属性的值时,该属性缺失,例如匿名主体的 subject.id,或没有应用的请求的 scope.app。
- 对缺失值,
eq、in、prefix、cidr和time为假;exists为假。 - 缺失值不等于任何值:不等于
"",也不等于另一个缺失值。 not是普通的取反,所以对缺失值比较取not为真。写成{"not": {"eq": [...]}}的 DENY 也会匹配属性缺失的请求。
时间条件
time 读取 environment.time。
| 字段 | 含义 |
|---|---|
notBefore | ISO 日期时间;在它之前条件为假。不带偏移量时按 zone 解读。 |
notAfter | ISO 日期时间;从它开始条件为假。必须晚于 notBefore。 |
zone | 时区 id:UTC(默认)、偏移量如 +08:00 或 UTC+8,或地区如 Asia/Shanghai。 |
daysOfWeek | 非空数组,元素为 MON … SUN,拼写必须完全一致:其他大小写或名称无法编译。 |
from、to | 一天中的时间,HH:mm、HH:mm:ss 或 HH:mm:ss.fff;要么都写要么都不写,且不能相等。窗口为 [from, to)。from 晚于 to 时窗口跨越午夜,凌晨部分属于窗口开始的那一天。 |
notBefore、notAfter、daysOfWeek 或 from/to 至少写一个。
{ "time": { "zone": "Asia/Shanghai", "daysOfWeek": ["MON", "TUE", "WED", "THU", "FRI"], "from": "09:00", "to": "18:00" } }外部属性与 onMissing
ext.<provider>.<name> 属性来自属性提供者 AttributeProvider(扩展),只在请求的候选语句读取它们时获取。它们的类型是声明的:string、bool、stringSet 或 ip。没有任何提供者、也没有平台 attributes 条目声明的路径无法编译。每个实例都按自己的提供者和平台的 attributes 编译,控制面检查变更时也是如此;因此每台主机上的提供者必须相同(测试策略)。
提供者可能不返回值(即上文的缺失),也可能失败、超时或没有被调用:此时属性不可用,由语句的 onMissing 决定:
onMissing | DENY | ALLOW |
|---|---|---|
indeterminate(默认) | 除非另有 DENY 匹配,决策为 Indeterminate(503)。 | 跳过。若没有其他语句允许,决策为 Indeterminate(503),而不是拒绝。 |
nomatch | DENY 不匹配:属性不可用期间它失效。只有带 onMissingConfirmed 才能编译,该字段由控制面在变更作者确认后写入(测试策略)。 | 跳过,请求在没有它的情况下决策。 |
不可用的属性永远不会把 Deny 或 Indeterminate 变成 Permit,经确认的 nomatch DENY 除外。
限制
每个条件在编译时都有上限。平台文档为所有文档设置这些上限(limits);未写的取默认值。
| 限制 | 默认值 | 限定 |
|---|---|---|
maxNodes | 64 | 单个条件的节点数。 |
maxDepth | 8 | 单个条件的嵌套深度。 |
maxInValues | 1024 | 单个 in 的字面量数,以及单个 time 的天数。 |
maxCidrBlocks | 256 | 单个 cidr 的网段数。 |
maxLiteralBytes | 1024 | 单个字面量或属性路径的 UTF-8 字节数。 |
maxCost | 256 | 单个条件的节点权重之和:and、or、not、exists 为 1;eq、prefix 为 2;in、cidr 为 4;time 为 8。 |
绑定
绑定表示:主体在应用和空间中受策略约束。
| 字段 | 类型 | 默认值 | 含义 |
|---|---|---|---|
id | 字符串 | 必填 | 在所属文档内唯一。控制面拒绝以 cosec:、g: 或 r: 开头的新 id。 |
subject | "*" 或对象 | 必填 | 对谁生效,见下。 |
app | 字符串 | 必填 | 应用 id,或 * 表示任意应用。不能为空。 |
space | 字符串 | 必填 | 空间 id,"" 表示默认空间,* 表示任意空间。以 ( 开头的空间 id 归 CoSec 所有,在这种空间中的绑定不能编译。 |
policy | 字符串 | 必填 | 策略 id。 |
expiresAt | 字符串 | 无 | ISO-8601 时刻。从该时刻起绑定不再适用。 |
breakGlass | 布尔 | false | break-glass 绑定(安全模型)。 |
app 和 space 精确比较,区分大小写;* 表示任意,且只表示任意。
| 主体 | 平台 | 应用 | 租户 |
|---|---|---|---|
"*":任何人,包括匿名主体 | 是 | 是 | |
{"principal": "<id>", "tenant": "<tenant>"} | 是,tenant 必填 | 是,tenant 可省略(本租户) | |
{"role": "<id>"},可选 "tenant" | 是;无 tenant:任意租户 | 是;无 tenant:本租户 |
租户文档中指向其他租户的绑定、限定到其他应用的应用文档绑定,以及平台文档中的角色绑定都无法编译:它们永远不可能适用,DENY 会悄悄丢失。
policy 可以指向:
- 本文档中的策略;
- 在应用或租户文档中,平台策略。它在每个请求时查找:如果平台已不再有该策略,它所适用的请求在没有 DENY 匹配时为 Indeterminate(
503)。控制面拒绝发布无法解析的引用,也拒绝删除被其他文档引用的平台策略; - 应用的权限策略:只能在该应用的文档中,或在
app设为该应用的租户文档中(角色权限); cosec:root,只能由 break-glass 绑定指向;cosec:platform-admin、cosec:tenant-admin和cosec:tenant-viewer,用于授权控制面。cosec:platform-admin只能在平台文档和平台租户(platform)自己的文档中绑定:绑定它的租户或应用文档不能编译。
边界策略和 cosec:blacklist 永远不能被绑定。break-glass 绑定只在平台文档中,绑定 (platform) 租户的主体,并带有 expiresAt。一个文档至多绑定 1,000 个主体:要授权用户,请通过令牌中的角色或 MembershipProvider。
附加策略
主体的成员关系可以列出直接附加的策略:令牌的 policies 声明,或 MembershipProvider(认证)。附加策略在所有应用和空间中都适用。该策略必须是 attachable: true,并定义在主体所属租户的文档或平台文档中。指向其他任何策略的附加策略视为丢失,除非有 DENY 匹配,请求为 Indeterminate(503)。匿名主体没有附加策略。
目录
应用文档的 catalog 列出应用提供的权限,供控制台和生成器使用。写了 action 的条目同时定义这个权限:策略 perm:<appId>/<id>,一条 ALLOW 语句。其他条目不参与求值。
| 字段 | 类型 | 含义 |
|---|---|---|
groups | 数组,必填 | 权限分组。 |
groups[].name | 字符串,必填 | |
groups[].description | 字符串 | |
groups[].spaced | 布尔,默认 false | 用 action 写的权限在哪里生效:持有它的角色生效的任何空间;不写时只在默认空间,靠条件 { "eq": ["scope.space", ""] }。 |
groups[].permissions | 数组,必填 | |
groups[].permissions[].id | 字符串,必填 | 权限 id;其策略为 perm:<appId>/<id>。 |
groups[].permissions[].name、.description | 字符串 | |
groups[].permissions[].action | 动作模式或非空数组 | 定义这个权限。写了 action 的条目和同 id 的策略不能同时存在;id 不能重复。 |
groups[].permissions[].resource | 同语句的 resource;需要 action | 不写表示任何资源。 |
没有任何检查确保没有 action 的条目有对应的策略,反之亦然;这样的条目只用于显示,角色不能列出它。见角色权限。
角色
应用文档(系统角色)和租户文档(自定义角色)可以有 roles:谁得到哪些权限,只写一次。概念和示例见角色权限。
| 字段 | 文档 | 类型 | 含义 |
|---|---|---|---|
id | 两种 | 字符串,必填 | 文档内唯一;不是 *,不含 @。 |
app | 租户 | 字符串,必填 | 角色所属的应用;合法的应用 id。 |
space | 租户 | 字符串 | 不写:任何空间。否则只在那个空间,"" 是默认空间;不能是保留 id。 |
name、description | 两种 | 字符串 | 用于显示。 |
permissions | 两种 | id 数组,应用文档也可以写 "*" | 角色的权限,可以为空。 |
角色为每个权限派生一条绑定 role:<roleId>@<app>:<permission>,策略是 perm:<app>/<permission>,主体是该角色(应用文档里不限租户,租户文档里是本租户),app 是角色的应用,space 是角色的空间或 *,排在手写绑定之后。与手写绑定相撞无法编译;role: 保留给这些绑定,变更不能写入。一份文档最多派生 50,000 条。
限流规则
rateLimits 是有序的规则数组。被允许的请求在它匹配的每条规则上消耗一个许可(限流)。
| 字段 | 类型 | 默认值 | 含义 |
|---|---|---|---|
id | 字符串 | 必填 | 字母、数字、.、_ 和 -。在所属文档内唯一。 |
action | 动作模式或其非空数组 | 必填 | 哪些请求计数。 |
resource | 资源模式或其非空数组 | 任意资源 | |
condition | 条件 | 恒为真 | |
onMissing | "indeterminate" 或 "nomatch" | "indeterminate" | 无法获取的外部属性:indeterminate 交给 strictFailure 决定;nomatch 跳过该规则。nomatch 只在平台文档中能编译:与 DENY 不同,限流规则没有确认流程。 |
key | 属性路径 | 一个共享桶 | String 或 Ip 属性,其值决定桶。resource.var.<name> 必须被每个资源模式捕获。 |
permitsPerSecond | 数字 | 必填 | 大于 0。 |
windowSeconds | 整数 | 1 | 至少为 1。 |
strictFailure | 布尔 | false | 限流器无法计数时是否拒绝请求(503)。 |
规则按 DENY 语句的方式匹配:动作、资源和条件,使用它自己的路径变量;裸资源类型无法编译。
平台专属字段
限制与属性
limits 设置所有文档编译时使用的条件限制。attributes 声明文档可以读取的外部属性:
{ "path": "ext.geo.country", "type": "string" }path 为 ext.<provider>.<name>;type 为 string、bool、stringSet 或 ip;每个路径只能出现一次。平台的某个修订改变了这两个字段之一时,每个实例都会重新编译应用和租户文档;不再能编译的文档失败即拒绝。控制面检查此类变更时会给出警告。
边界
边界限定一个租户最多能被允许什么。"boundary": true 的策略就是边界:只在平台文档中,只有 ALLOW 语句,至多 64 条,不可附加,不可绑定。boundaries.default 指定约束所有租户的边界;租户文档的 boundary 指定改为约束该租户的平台边界。(platform) 租户没有边界。此时 Permit 只有在租户边界中也有语句匹配时才成立;否则变为 Deny,若边界语句出错或指定的边界不存在则为 Indeterminate。
一个平台文档:
{
"schema": 2,
"revision": 12,
"publishedAt": "2026-11-01T08:00:00Z",
"limits": { "maxCost": 128 },
"attributes": [{ "path": "ext.geo.country", "type": "string" }],
"boundaries": { "default": "boundary-standard" },
"policies": [
{
"id": "deny-admin-outside-office",
"statements": [
{
"effect": "deny",
"action": "*",
"resource": "http:path:/admin/**",
"condition": { "not": { "cidr": ["environment.ip", ["10.0.0.0/8"]] } }
}
]
},
{
"id": "public-health",
"statements": [{ "effect": "allow", "action": "http:GET", "resource": "http:path:/actuator/health" }]
},
{
"id": "boundary-standard",
"boundary": true,
"statements": [{ "effect": "allow", "action": ["http:*", "order:*"] }]
}
],
"bindings": [
{ "id": "deny-admin", "subject": "*", "app": "*", "space": "*", "policy": "deny-admin-outside-office" },
{ "id": "health", "subject": "*", "app": "*", "space": "*", "policy": "public-health" },
{
"id": "ops-platform-admin",
"subject": { "principal": "ops-1", "tenant": "(platform)" },
"app": "*",
"space": "*",
"policy": "cosec:platform-admin"
}
]
}来源记录
控制面和 cosec-migrate 记录每个修订是如何发布的。它不参与任何决策。
| 字段 | 含义 |
|---|---|
parent | 被本修订替换的头修订的 {"revision", "digest"}(digest:SHA-256: 加 64 位小写十六进制,即存储的摘要);第一个修订没有。 |
change | 发布它的变更。 |
kind | PUBLISH、ROLLBACK、BREAK_GLASS、EXPIRE、MIGRATE 或 BOOTSTRAP。 |
publishedBy | {"tenant", "principal"}。 |
approvedBy | 变更需要审批时的审批人。 |
shadow | {"window", "skipped"}:变更经过的影子窗口,或表示窗口被跳过。 |
发布文档
文档保存在 Redis 中,只能通过控制面(cosec-control 模块)或 cosec-migrate publish 和 bootstrap 写入,见存储。控制面在发布前检查并编译每个变更,还可以先在线上流量上试运行:见测试策略。
JSON Schema
schema/ 中的 JSON Schema 可用于编辑器补全和 CI 检查:
| 文件 | 校验 |
|---|---|
cosec-snapshot-document.schema.json | 平台、应用或租户文档 |
cosec-binding.schema.json | 单个绑定 |
cosec-catalog.schema.json | 应用的目录 |
definitions.schema.json | 共享定义 |
problem.schema.json | 错误响应体(Web 集成) |
文档是封闭的,所以不要在文档中加 $schema 字段:在编辑器中把 schema 映射到你的文件,例如在 VS Code 的 settings.json 中:
{
"json.schemas": [
{
"fileMatch": ["policies/*.json"],
"url": "https://raw.githubusercontent.com/Ahoo-Wang/CoSec/refs/heads/main/schema/cosec-snapshot-document.schema.json"
}
]
}这些文件由 cosec-runtime 中的格式描述符(DocumentFormats)生成,从不手工编辑。有测试保证它们与文档读取器一致:一方接受的,另一方也接受。
通过 schema 校验是必要条件,而非充分条件。JSON Schema 无法表达的内容包括:文件是三种文档中的哪一种;id 唯一和文档之间的引用;时区是否存在,以及 2 月 30 日这样的日期;from 等于 to 的时间窗口,以及 notBefore 晚于 notAfter;路径变量的作用域;属性目录和条件操作数的类型。要检查这些,请编译文档(测试策略)。
下一步: 按角色而不是按主体授予权限,见角色权限。