Ory Permission Language 规范详解:用 TypeScript 子集定义 Keto 细粒度权限模型
后端应用安全云原生【免费下载链接】ketoThe most scalable and customizable permission server on the market. Fix your slow or broken permission system with Googles proven Zanzibar approach. Supports ACL, RBAC, and more. Written in Go, cloud native, headless, API-first. Available as a service on Ory Network and for self-hosters.项目地址https://gitcode.com/gh_mirrors/ke/keto点击查看免费下载Ory Permission Language简称 OPL是 Ory Keto 官方推荐的权限配置语言它以 TypeScript 的一个语法子集来描述命名空间Namespace、关系Relation与权限Permission让开发者几乎零学习成本地写出谁可以对什么做什么的细粒度权限模型。本文以仓库中的《Ory Permission Language 规范》为骨架结合 Keto 源码中的词法分析器、解析器、AST 与执行引擎实现完整讲解 OPL 的 EBNF 文法、词法元素、语句结构、类型检查规则并给出可直接运行的配置示例与源码级佐证。读完本文你将能够独立编写、校验并深入理解 Keto 的 OPL 权限配置。为什么是TypeScript 子集细粒度权限fine-grained permissions是成熟技术方案中保护隐私与身份的关键构件。业界已有多种权限描述语言例如 Authzed 的 Schema Language 与 Auth0 FGA。然而大多数权限模型由普通开发者定义他们通常最熟悉 JavaScript / TypeScript 等 Web 技术。Keto 因此选择将权限配置语言定义为最常见的通用编程语言JavaScript/TypeScript的子集使绝大多数开发者几乎不用额外学习就能理解和使用它。OPL 是 TypeScript 的语法子集配合为语言语法元素如Namespace、Context提供的类型定义开发者在 IDE 中书写配置时即可获得上下文补全与类型提示。仓库中的 contrib/namespace-type-lib/index.d.ts 正是这套类型定义的实现它以/// reference no-default-libtrue/屏蔽标准全局类型通过declare module ory/keto-namespace-types导出Context、Namespace、SubjectSet等类型为Array扩展了includes与traverse两个语义方法后文详述。语法表示法EBNFOPL 语法使用扩展巴科斯-瑙尔范式EBNF描述Production production_name [ Expression ] . . Expression Alternative { | Alternative } . Alternative Term { Term } . Term production_name | token [ … token ] | Group | Option | Repetition . Group ( Expression ) . Option [ Expression ] . Repetition { Expression } .产生式Production由终结符Term与下列操作符构成按优先级递增排列| alternation () grouping [] option (0 or 1 times) {} repetition (0 to n times)约定如下小写开头的产生式名表示词法记号lexical tokenCamelCase 表示非终结符non-terminal词法记号以双引号或单引号包裹形如a … b表示从a到b的字符集合作为候选水平省略号…亦用于表示各种未进一步展开的枚举或代码片段。配置文本表示配置以 UTF-8 编码。词法元素注释OPL 支持三类注释行注释以字符序列//开始到行尾结束普通块注释以/*开始到第一个*/结束文档注释以/**开始到第一个*/结束。在词法分析器实现中scanCommentBegin检测//与/*前缀分别进入lexLineComment与lexBlockComment状态未闭合的块注释会触发unclosed comment错误。解析器通过nextNonCommentItem跳过注释记号因此注释可出现在表达式内部的任意位置见解析器测试用例中(p) /* comment */ 的写法。标识符标识符用于命名变量、类型等程序实体由字母和数字组成首字符必须是字母identifier letter { letter | digit } . digit 0 … 9 . letter A … Z | a … z | _ .注意下划线_被算作字母。在 lexer.go 中letters abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ_scanIdentifier先要求一个字母开头再连续吞入字母与数字。字符串字面量字符串字面量表示字符序列常量string_lit single_quoted | double_quoted . single_quoted identifier . double_quoted identifier .规范要求字符串内容满足identifier的形态实际应用中字符串字面量常用来书写带特殊字符的关系/权限名例如解析器测试中的scope.relation、member等。关键字配置语言保留以下关键字classimplementsrelatedpermitsthisctxidimportsexportsas从实现角度看lexer.go 的keywords表将class、implements、this、ctx识别为专门的关键字记号itemKeywordClass等而related、permits由解析器按标识符值在class体内匹配imports、exports、as、id则作为保留字服务于 TypeScript 风格的导入声明。顶层解析对class以外的语句例如import … from ory/keto-namespace-types采取宽松处理因此导入语句可以原样书写而不会导致解析失败。内置类型OPL 内置以下类型ContextNamespaceNamespace[]booleanstringSubjectSetT, R在 TypeScript 中它们等价于如下定义type Context { subject: never } interface Namespace { related?: { [relation: string]: Namespace[] } permits?: { [method: string]: (ctx: Context) boolean } } interface ArrayNamespace { includes(element: Namespace): boolean traverse(iteratorfn: (element: Namespace) boolean): boolean } type SubjectSetA extends Namespace, R extends keyof A[related] A[related][R] extends Arrayinfer T ? T : never可以看到Context目前只携带subject字段Namespace由可选的related关系集合与permits权限方法集合组成ArrayNamespace被扩展出两个权限语义方法SubjectSetT, R从类型T的关系R中推导出可作主语的元素类型。操作符布尔操作符操作符签名语义xy当且仅当x与y均为真\|\|x\|\|y当且仅当x或y至少一个为真!!x当且仅当x为假杂项操作符操作符示例语义(,)x()调用函数x[]T[]T是数组类型,SubjectSetGroup, members对Group命名空间members关系的类型引用{,}class x {}作用域定界符list.transitive(el f(el))Lambda 定义记号.this.x属性遍历记号:relation: type关系/类型分隔记号permits {...}赋值记号,{x: 1, y: 2}属性分隔符string单引号字符串字面量string双引号字符串字面量*import * from ory/permission-language导入通配\|(User \| Group)[]类型联合在 lexer.go 中、||、以多字符记号multiRuneTokens优先匹配其余单字符操作符由oneRuneTokens表映射为对应记号类型itemOperatorAnd、itemOperatorOr、itemOperatorNot、itemTypeUnion、itemOperatorArrow等。语句结构Namespace 的三层模型OPL 配置文件的最顶层是若干class声明每个class代表一个命名空间Namespace由关系声明related与权限定义permits构成Config [ ClassDecl ] . ClassDecl class identifier implements Namespace { ClassSpec } . ClassSpec [ RelationDecls ] | [ PermissionDefns] .下面这个例子声明了类型Userclass User implements Namespace {}关系声明relatedrelated段定义了关系relation。与普通 TypeScript 不同RelationName必须是唯一标识符它将直接作为关系元组relation tuple中的关系字符串使用TypeName必须是某个已定义或随后定义的命名空间类型在 TypeScript 中即实现了Namespace的 class。类型联合|表示一个关系可以拥有多种类型的主语例如viewers: (User | SubjectSetGroup, members)[]意思是viewer关系的主语既可以是User也可以是主题集Group#members。RelationDecls related { { RelationName : ArrayType } } . RelationName identifier . ArrayType RelationType | ( ( RelationType { | RelationType } ) ) [] . RelationType SubjectType | SubjectSetType . SubjectType TypeName . SubjectSetType SubjectSet TypeName, string_lit . TypeName identifier .注意所有关系都被声明为数组类型T[]因为 Keto 中天然只存在多对多关系。例如声明一个Document类型包含owners与viewers两个以User为主语的关系以及一个类型为Document的parent关系class User {} class Document { related { parents: Document[] owners: User[] viewers: User[] } }实现要点写法差异提醒规范 EBNF 中RelationDecls写作related { ... }但解析器实现的parseRelated实际匹配的是p.match(:, {)即related:后接花括号同理parsePermits匹配的是p.match(, {)即permits {...}。因此在编写真实配置文件时请使用related: { ... }的写法这也是规范示例章节与生成模板的写法permits { ... }的写法保持不变。从解析器还能看到ArrayType的多种等价写法均被支持ArrayFile、ArraySubjectSetGroup, members、(User | Group)[]、User[]以及带引号的属性名如owners: ...——见parseRelated中对Array、SubjectSet、(、普通标识符与字符串字面量的分支处理。权限定义permits权限被定义为 class 声明内的函数接收类型为Context的参数ctx求值结果为布尔值true或false。ctx与返回值的类型注解都是可选的。PermissionDefns permits { Permission [ , Permission ] } . Permission PermissionSign PermissionBody . PermissionSign PermissionName : ( ctx [ : Context ] ) [ : boolean ] . PermissionName identifier .ctx是一个固定参数内含本次权限检查所针对的subjectctx { subject: some_user_id }未来Context将包含更多字段例如请求来源 IP 段或地理位置、一天中的时间段、发起请求设备的安全等级等。权限体PermissionBody的文法如下PermissionBody ( ( PermissionBody ) ) | ( ! PermissionBody ) | ( PermissionCheck | { Operator PermissionBody } ) . Operator || | . PermissionCheck TransitiveCheck | IncludesCheck .权限体的每个分支要么是IncludesCheck——检查某元素是否属于某个集合例如this.related.viewers.includes(ctx.subject)IncludesCheck Var . related . RelationName . includes ( ctx . subject ) . Var identifier .TransitiveCheck——对某个关系调用权限例如this.related.parents.transitive(p p.permits.view(ctx))TransitiveCheck this . related . RelationName . transitive ( Var ( PermissionCall | IncludesCheck ) ) . PermissionCall Var . permits . PermissionName ( ctx ) .实现要点transitive 与 traverse规范 EBNF 中的关键字写作transitive但解析器的parsePermissionExpression实际只接受traverse分支case traverse: child p.parseTupleToSubjectSet(name)内置类型定义与全部示例也都使用traverse。这是规范正文与实现之间的一处不一致实际可运行的写法以traverse为准。除了 EBNF 中列出的两种基本检查实现还额外支持对this.permits的直接权限调用例如this.permits.read(ctx)见parsePermissionExpression的permits分支一元取反!例如!this.related.owners.includes(ctx.subject)parseNotExpression生成ast.InvertResult任意层级的括号分组与/||组合。这些能力在解析器测试中均有覆盖例如comment: (ctx: Context) this.permits.read(ctx)与取反案例not: (ctx: Context) !this.related.owners.includes(ctx.subject)。与 Zanzibar 概念的映射Implementation NotesIncludesCheck与TransitiveCheck分别翻译为 Zanzibar 的 AST 概念Keto ConfigZanzibar ASTthis.related.R.includes(ctx.subject)computed_userset { relation: R } }this.related.R.transitive(x x.permits.P(ctx.subject))tuple_to_userset { tupleset { relation: R } computed_userset { relation: P } }这条映射关系在 Keto 的执行引擎中有一一对应的实现见内部检查模块ComputedSubjectSet即includes检查对应 Zanzibar 的computed_userset由 ComputedUsersetStep 执行它把请求元组的关系改写为被计算的关系R再递归执行IsAllowedStepTupleToSubjectSet即traverse检查对应 Zanzibar 的tuple_to_userset由 TraverseStep 执行它先加载请求对象上匹配关系R的全部元组再对每个以主题集SubjectSet为主语的元组在其命名空间/对象上按ComputedSubjectSetRelation即P继续做成员检查取反!由InvertResult节点表示执行时对子检查结果取反由/||组合出的表达式树对应SubjectSetRewrite节点由 RewriteStep 统一求值||走并集RunUnion走交集RunIntersection。类型检查规则配置被完整解析后会执行如下类型检查对应类型检查实现给定TypeName为X例如出现在RelationDecls中检查存在X的 class 声明——对应checkNamespaceExists给定SubjectSetType为SubjectSetT, R检查R是T定义的关系——对应checkNamespaceHasRelation给定IncludesCheck为this.related.R.includes(ctx.subject)检查R是当前命名空间定义的关系——对应checkCurrentNamespaceHasRelation给定TransitiveCheck为this.related.R.transitive(x x.permits.P(ctx.subject))检查R是当前命名空间定义的关系且P是R引用的所有类型都定义的权限——对应checkAllRelationsTypesHaveRelation给定TransitiveCheck为this.related.R.transitive(x x.related.S.includes(ctx.subject))检查R是当前命名空间定义的关系且S是R引用的所有类型都定义的关系——同样由checkAllRelationsTypesHaveRelation递归完成。实现细节解析过程中addCheck将检查函数收集到p.checksParse只在无语法错误时调用typeCheck()见 parser.go 的parse方法。对R引用的类型做所有类型都必须具备某关系/权限的检查时若类型是主题集SubjectSet…还会递归下钻其关系类型深度上限为tupleToSubjectSetTypeCheckMaxDepth 10见 limits.go。完整示例User / Group / Folder / File规范给出的完整示例同时演示了class声明、关系声明、权限定义、traverse递归与||组合class User implements Namespace { related: { manager: User[] } } class Group implements Namespace { related: { members: (User | Group)[] } } class Folder implements Namespace { related: { parents: File[] viewers: (User | SubjectSetGroup, members)[] } permits { view: (ctx: Context): boolean this.related.viewers.includes(ctx.subject), } } class File implements Namespace { related: { parents: (File | Folder)[] viewers: (User | SubjectSetGroup, members)[] owners: (User | SubjectSetGroup, members)[] siblings: File[] } permits { view: (ctx: Context): boolean this.related.parents.traverse((p) p.related.viewers.includes(ctx.subject), ) || this.related.parents.traverse((p) p.permits.view(ctx)) || this.related.viewers.includes(ctx.subject) || this.related.owners.includes(ctx.subject), edit: (ctx: Context) this.related.owners.includes(ctx.subject), rename: (ctx: Context) this.related.siblings.traverse((s) s.permits.edit(ctx)), } }逐条解读这段模型File.view由四路候选取并集构成任何父级parents的viewers中包含当前主语、任何父级允许view自顶向下递归继承、自身viewers包含主语、自身owners包含主语。这正体现了关系直接判断 沿关系树递归的权限建模方式File.edit只允许ownersFile.rename沿siblings同级文件递归任一兄弟可编辑即可重命名这是 Zanzibar 式tuple_to_userset的典型用法。规范还提示该配置可用 TypeScript 在strict模式下进行类型检查配合noLib选项屏蔽标准全局与strictPropertyInitialization选项允许未初始化属性。源码实现纵深从词法分析到执行词法分析lexer.go 实现了一个基于通道channel与状态机stateFn的扫描器其设计灵感来自 Rob Pike 在 2012 年关于 Go 模板 lexer 的演讲。它按顺序处理空白、双字符记号、||、、注释、单字符记号、字符串字面量、标识符/关键字并对未闭合注释与字符串报告unclosed comment/unclosed string literal错误。每个词法记号携带Start、End位置信息供后续产生带源码位置的解析错误。递归下降解析parser.go 是核心解析器遵循 EBNF 结构parseClass消费class name implements Namespace {然后在}之前循环分发related/permits分号被当作空语句跳过parseRelated解析related: { name: type[] ... }支持上文提到的多种类型写法并为后续类型检查登记checkNamespaceExists/checkNamespaceHasRelationparsePermits解析permits { name: (ctx[: Context])[: boolean] body }其中ctx与boolean注解均为可选权限体经parsePermissionExpressions与parseNotExpression递归解析为 AST嵌套深度受expressionNestingMaxDepth 10限制见 limits.go超限会报expression nested too deeplyparsePermissionExpression识别this.related.rel.traverse(...)/this.related.rel.includes(ctx.subject)/this.permits.perm(ctx)三种叶子形态并登记对应的类型检查。AST 表示internal/namespace/ast/ast_definitions.go 定义了编译产物Relation关系或权限统一表示为Name 可选Types关系类型列表或SubjectSetRewrite权限重写树ComputedSubjectSet对应includes检查Zanzibarcomputed_usersetTupleToSubjectSet对应traverse检查Zanzibartuple_to_userset记录Relation与ComputedSubjectSetRelationInvertResult取反节点SubjectSetRewriteOperatorOr/OperatorAnd组合的 n 元树。解析完成后namespace.Namespace结构见 internal/namespace/definitions.go以NameRelations []ast.Relation的形式向运行时暴露配置并可通过FindRelation/ASTRelationFor按名查询关系。表达式化简parser.go 中的simplifyExpression会把嵌套的二元操作树化简为 n 元操作单子包装AND(OR(A)) ≡ AND(A)与同操作符合并OR(OR(A,B),C) ≡ OR(A,B,C)都会被扁平化取反内部同样递归化简见Test_simplify的多个用例从而降低后续执行引擎的树深度。权限检查执行引擎internal/check/step/dispatch.go 中的IsAllowedStep是中央语义调度器它根据请求元组对应关系的 AST 规则组合出三类可执行的检查步骤——RewriteStep若存在SubjectSetRewrite、DirectStep直接元组匹配与ExpandSubjectStep展开主题集再经RunUnion汇总。当 OPL 声明了关系的主题集类型时展开可被限制在声明的类型范围内strict mode 下仅执行 OPL 显式允许的步骤。此外rewrite.go 的planRewrite还实现了 OR 短路优化把 OR 下的多个ComputedSubjectSet子节点合并为一次DirectMultiStep批量检查减少查询往返。在 Keto 中落地 OPL加载、生成与验证命名空间的加载与热更新OPL 配置最终会被解析为一系列namespace.Namespace由NamespaceManager管理。Keto 提供两种管理器内存实现静态加载与文件监视实现基于 watcherx 监听目录或文件检测到变更即重新解析解析失败时回滚到上一个可用版本并记录错误日志。后者的存在意味着修改namespaces.ts后无需重启即可让新模型生效。生成模板与 IDE 类型库keto namespace子命令见 cmd/namespace/root.go当前注册了校验命令opl_generate.go 展示了如何依据命名空间列表生成 OPL 骨架配置其输出由模板文件控制每个命名空间生成一个class Name implements Namespace { related: {...} permits {...} }的空模板并在注释中给出parents: (File | Folder)[]、viewers: SubjectSetGroup, members[]、view权限递归等示例提示。配套的 contrib/namespace-type-lib/index.d.ts 类型库则可让 IDE 在编写includes/traverse时给出签名与 JSDoc 说明。仓库内的真实示例contrib/cat-videos-example/namespaces.ts声明User与Video两个命名空间Video拥有view允许User或Video#owner主题集与owner关系配合目录下relation-tuples/*.json与docker-compose.yml构成一个可运行的示例contrib/docs-code-samples/list-api-display-objects-subjectset/namespaces.tsUser与Chat模型演示member关系的定义更多写法与进阶能力引号属性名、this.permits直接调用、!取反、多层括号可参见解析器测试中的用例它们同时也是解析器行为的可执行文档。用 keto check 验证模型配置完成后可通过keto check命令cmd/check/root.go对具体的主语-关系-对象三元组发起权限检查keto check User:Alice view Doc:readme该命令把请求转发给 CheckService 的 gRPC 接口返回{allowed: true|false}并支持--max-depth-d限制搜索树深度。结合 OPL 中permits的递归语义max-depth是控制图遍历开销的关键参数。小结Ory Permission Language 用一套极小的 TypeScript 子集覆盖了 Zanzibar 风格权限模型的核心表达力class implements Namespace声明命名空间related声明多对多关系含SubjectSetT, R主题集引用与类型联合permits用includes/traverse/!//||组合出可递归求值的权限表达式。从词法分析器到解析器、AST再到执行引擎整条链路在 Keto 仓库中完整开源既有严格的类型检查保证配置正确性又有基于 Zanzibarcomputed_userset/tuple_to_userset的成熟求值语义。掌握这份规范你就能为任何业务对象建立清晰、可扩展、可审计的细粒度权限模型。赞分享后端应用安全云原生【免费下载链接】ketoThe most scalable and customizable permission server on the market. Fix your slow or broken permission system with Googles proven Zanzibar approach. Supports ACL, RBAC, and more. Written in Go, cloud native, headless, API-first. Available as a service on Ory Network and for self-hosters.项目地址https://gitcode.com/gh_mirrors/ke/keto点击查看免费下载相关推荐Ory Keto权限语言规范详解Ory Keto权限语言规范详解 前言 在现代应用开发中细粒度的权限控制是保护用户隐私和数据安全的关键组件。Ory Keto项目提供了一种创新的权限语言规范—后端应用安全云原生3 步跑通 JimuReport 积木报表让月度经营报表当天就能打出来3 步跑通 JimuReport 积木报表让月度经营报表当天就能打出来 月底交付报表不用再手动拼 Excel 每个月底业务方都要一份能对账、能打印的月度经后端应用安全云原生Ory Keto与Kubernetes集成如何在容器化环境中实现细粒度权限控制Ory Keto与Kubernetes集成如何在容器化环境中实现细粒度权限控制 Ory Keto是一个开源的授权系统基于Google的Zanzibar架构实后端应用安全云原生上一篇终极iOS激活锁绕过解决方案applera1n完整使用指南下一篇如何轻松实现AI智能分层Layerdivider完整使用教程创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考