Skip to content

策略语言 ​

面向需要字段确切含义的策略作者、评审者和工具开发者。本页是文档格式(schema: 2)的参考。这些字段如何组合成决策,见安全模型;如何安全地修改文档,见测试策略。

所有授权数据都保存在三种文档中。文档按严格模式读取、整体编译:未知字段、重复的键、JSON 类型不对的值、无法编译的模式或条件、悬空的引用,都会让整个文档失败。失败的文档永远不会被安装,所有需要它的请求都失败即拒绝:除非有 DENY 匹配,否则返回 503(失败即拒绝)。

文档 ​

文档键每个……一份包含
平台文档platform部署所有租户共享的策略、面向任何人(*)和指定主体的绑定、break-glass 绑定、限流规则,以及只有平台能设置的 limits、attributes、boundaries。
应用文档app:<appId>应用应用的目录、权限策略 perm:<appId>/<permission>、系统角色、角色绑定、限流规则。
租户文档tenant:<tenantId>租户租户自己的策略、它的绑定(任何人、它的主体、它的角色)、它的 boundary、自定义角色、限流规则。

键是控制面对文档的称呼。每个请求都读取平台文档和它所属租户的文档(匿名请求:租户 (0))。只有当主体在请求的空间中至少拥有一个角色,或租户文档把该应用的某个权限策略绑定给任何人或某个主体时,请求才会读取其应用的应用文档;被黑名单拦截的请求只读取平台文档。没有发布过的文档按空文档读取。哪些候选适用、按什么顺序,见安全模型。

一个租户文档:

json
{
  "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 路由指定动作和类型化资源:

json
{
  "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/42order/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: 模式:

json
{ "pattern": "http:path:/Reports/{id}", "caseSensitive": false, "decodeAndParseSegments": true }
选项默认值含义
caseSensitivetrue为 false 时比较字面量忽略 ASCII 大小写。
decodeAndParseSegmentstrue为 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 中:

json
{
  "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 并列。每个属性路径和每个操作数类型都在文档编译时检查。

json
{ "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]任一为真;至少一个,从左到右,遇到第一个真即停止
notcondition条件为假

字面量是 JSON 字符串或布尔值,从不是数字;字符串永远不会被当作布尔值。没有 true 或 false 条件:不需要条件时省略 condition。

属性 ​

属性类型值
subject.idString主体 id;匿名主体时缺失。
subject.tenantString请求的租户:主体的租户,匿名时为 (0)。
subject.authenticatedBool凭证是否被接受。
subject.rolesSet主体在任意空间中的角色 id。
subject.scopedRolesSet按声明形式给出的角色:默认空间为 role,其他空间为 role@space。
subject.attributes.<name>String令牌 attributes 声明中的条目,转为文本(认证)。
scope.tenantString同 subject.tenant。
scope.appString请求的应用;没有应用时缺失。
scope.spaceString请求的空间;默认空间为 ""。
scope.rolesSet主体在请求空间中的角色。
action.service、action.verbString动作的两部分。
resource.type、resource.idString资源的两部分。
resource.attributes.<name>String资源的属性。请求管道和文档中列出的检查都不设置;对象级规则把事实写进资源 id(对象级规则)。
resource.var.<name>String路径变量。
environment.ipIp客户端地址(Web 集成)。
environment.timeInstant请求时间;只有 time 读取它。
environment.deviceStringCoSec-Device-Id 参数;为空时缺失。
environment.blockedBool黑名单是否拦截该请求。
http.header.<name>String请求头 <name>;请求没有该头时缺失。见请求头。
http.origin、http.refererStringOrigin 和 Referer 请求头,读取方式与 http.header.* 相同。
http.origin.host、http.referer.hostStringOrigin 和 Referer URL 中的主机,小写;值不是带主机的 URL 时缺失。
http.origin.host.labels、http.referer.host.labelsLabels该主机的标签,倒序(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 读取这个请求头,也不告诉它是哪一个。

匹配角色 ​

角色通过角色绑定授予权限,而不是通过条件。确实需要在条件中判断角色时,检查集合:

json
{ "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。

字段含义
notBeforeISO 日期时间;在它之前条件为假。不带偏移量时按 zone 解读。
notAfterISO 日期时间;从它开始条件为假。必须晚于 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 至少写一个。

json
{ "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 决定:

onMissingDENYALLOW
indeterminate(默认)除非另有 DENY 匹配,决策为 Indeterminate(503)。跳过。若没有其他语句允许,决策为 Indeterminate(503),而不是拒绝。
nomatchDENY 不匹配:属性不可用期间它失效。只有带 onMissingConfirmed 才能编译,该字段由控制面在变更作者确认后写入(测试策略)。跳过,请求在没有它的情况下决策。

不可用的属性永远不会把 Deny 或 Indeterminate 变成 Permit,经确认的 nomatch DENY 除外。

限制 ​

每个条件在编译时都有上限。平台文档为所有文档设置这些上限(limits);未写的取默认值。

限制默认值限定
maxNodes64单个条件的节点数。
maxDepth8单个条件的嵌套深度。
maxInValues1024单个 in 的字面量数,以及单个 time 的天数。
maxCidrBlocks256单个 cidr 的网段数。
maxLiteralBytes1024单个字面量或属性路径的 UTF-8 字节数。
maxCost256单个条件的节点权重之和: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布尔falsebreak-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 声明文档可以读取的外部属性:

json
{ "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。

一个平台文档:

json
{
  "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发布它的变更。
kindPUBLISH、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
{
  "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;路径变量的作用域;属性目录和条件操作数的类型。要检查这些,请编译文档(测试策略)。

下一步: 按角色而不是按主体授予权限,见角色权限。

基于 Apache License 2.0 发布