news 2026/9/23 2:37:42

Teleport RFD 212 解析:使用 `jsonpath` 插值处理任意 JSON OIDC Claims

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Teleport RFD 212 解析:使用 `jsonpath` 插值处理任意 JSON OIDC Claims

Teleport RFD 212 解析:使用jsonpath插值处理任意 JSON OIDC Claims

【免费下载链接】teleportThe easiest, and most secure way to access and protect all of your infrastructure.项目地址: https://gitcode.com/gh_mirrors/tel/teleport

导读

本篇文章围绕 Teleport 的 RFD 212(JSONPath Interpolation)展开,讲解 Teleport 如何通过新增的jsonpath表达式函数,将任意 JSON 结构的 OIDC Claims 映射为标准用户 Trait(特质),进而用于claims_to_roles角色映射、角色模板(role templating)与标签表达式等场景。读完本文,你将掌握jsonpath函数的语法与在login_rule中的完整配置方式,理解 Teleport 为什么选择"登录规则优先"而非"直接把 JSON 塞进用户特质"的架构决策,并能基于仓库源码与配置示例构建一套可运行的自定义 IdP 接入方案。

一、背景:为什么需要 JSONPath 插值

Teleport 的 OIDC 登录流程假设 Claims 要么是字符串、要么是字符串列表。但从技术上讲,OIDC 规范允许 Claims 是任意 JSON 对象。Teleport 在实际集成中遇到过依赖这种能力的自定义 OIDC 方案,因此需要一种方式来处理任意结构的 JSON Claims。

jsonpath函数应运而生:它是一个 trait 表达式函数,专门用于从任意 JSON Claims 中插值出字符串或字符串列表。它只在login_rule表达式中受支持(即traits_maptraits_expression),用于把 JSON Claims 映射为标准的用户 Trait,供后续的角色分配与权限计算使用。

RFD 文档中给出了三个最基础的查询示例,JSON 对象为:

{ "a": ["1", "2", "3"], "b": { "c": "d" } }
  • jsonpath("$.a")["1", "2", "3"]
  • jsonpath("$.b.*")["d"]
  • jsonpath("$.*.*")["1", "2", "3", "d"]

二、JSONPath 语言与 Go 库选型

2.1 JSONPath 的演进背景

JSONPath 是一种在 JSON 对象中查询值的查询语言。虽然它在 2024 年有了官方 RFC(RFC 9535),但其起源可以追溯到 2007 年的一篇文章。由于原始文章遗留了许多未解答的问题,不同 JSONPath 项目各自给出了自己的答案,导致这门语言衍生出多种方言。RFC 的发布帮助这些项目重新对齐,但社区中仍然存在不少未解决的语法分歧。

2.2 为什么选择 ojg

RFD 作者参考了社区维护的 JSONPath 对比项目,结论是:在列出的 Go 项目中,github.com/ohler55/ojg是当前最接近 RFC 规范的实现。选择一个贴近规范的库,就可以有选择地依赖官方 JSONPath 文档与在线沙箱来验证查询行为。

在当前仓库的 go.mod 中,可以确认该依赖确实已经引入:

github.com/ohler55/ojg v1.27.0

2.3 升级上游库时的注意事项

RFD 特别提醒:在升级上游 ojg 库时必须谨慎。因为该库仍在向 RFC 靠拢,且社区中部分语法分歧尚未解决,一些语法可能在升级后发生变化。这意味着依赖jsonpath的登录规则表达式,需要在上游库升级时做回归验证。

三、OIDC Claims 在 Teleport 登录中的双重用途

在 OIDC 登录过程中,用户的 Claims 承担两个职责:

  1. 设置为 Teleport 用户 Trait:可选地通过 login rule 进行自定义的 Claims 到 Trait 的映射。
  2. 决定授予用户的角色:通过 OIDC Connector 的claims_to_roles字段,将用户 Trait 映射为角色。

jsonpath函数被设计为只支持 login rule 的 trait 映射。这样,任何被映射为 Trait 的 JSON OIDC Claims,都会自动进入 Traits-to-Roles 映射的可用范围。为什么不让 JSON 值直接成为用户 Trait?这是设计团队经过 POC 验证后的明确决策,其理由在本文第六节详述。

四、登录规则(Login Rule):Claims 到 Trait 的映射

4.1 LoginRule 资源结构

从源码看,LoginRule是 Teleport 中的一等资源,其 Protobuf 定义位于 api/proto/teleport/loginrule/v1/loginrule.proto:

  • priority:登录规则在集群中的相对优先级,数值越小越先被评估;
  • traits_map:Trait 键到谓词表达式列表的映射,每个表达式应求值为该 Trait 的目标值;
  • traits_expression:一个谓词表达式,登录时返回用户的目标 Trait 集合。

4.2 基础示例:把 JSON 对象映射为 Trait

假设 IdP 返回的 Claims 中有一个 JSON 对象而非字符串数组:

{ // groups 是 JSON 对象而非字符串数组 "groups": { "roles": ["template"], "logins": ["alice"], "env": ["staging", "dev"], } }

对应的login_rule如下:

kind: login_rule version: v1 metadata: name: my-loginrule spec: priority: 0 traits_map: roles: # 求值为 ["template"] - jsonpath("$.groups.roles") logins: # 求值为 ["alice"] - jsonpath("$.groups.logins") env: # 求值为 ["staging", "dev"] - jsonpath("$.groups.env")

4.3 映射后的 Trait 如何被消费

这些 Trait 可以继续用于:

  • claims_to_roles映射;
  • 角色模板(role templating);
  • 标签表达式(label expressions)等。

例如在 OIDC Connector 中按 Trait 分配角色:

kind: oidc version: v2 metadata: name: my-idp spec: ... claims_to_roles: - claim: "roles" value: "template" roles: ["template"]

再配合角色模板消费外部 Trait:

kind: role version: v7 metadata: name: template spec: ... allow: logins: '{{external.logins}}' node_labels_expression: 'contains(external.env, labels["env"])'

4.4 底层求值模型

从 lib/loginrule/evaluator.go 的源码结构可以看到登录规则的求值输入与输出:

  • EvaluationInput.Traits:外部 IdP 提供的 Trait(SSO 用户)或内部静态 Trait(本地用户),作为登录规则求值的输入;
  • EvaluationInput.Claims:原始的、未解析的 Provider Claims,每个 Claim 可能是标准字符串/列表,也可能是任意 JSON 对象(类型为map[string]any)——这正是jsonpath函数处理的对象;
  • EvaluationOutput.Traits:登录规则求值的最终输出 Trait;
  • EvaluationOutput.AppliedRules:实际应用成功的规则名称列表。

此外还有一个NullEvaluator,当集群未启用登录规则时原样返回输入 Trait,保证该特性关闭时行为与旧版本一致。

五、用户故事:两个典型自定义 IdP 场景

5.1 场景一:返回任意 JSON Claims 的 IdP

假设某个自定义 IdP 直接支持为用户设置任意 JSON Claims,用户alice的 Claim 对象如下:

{ "groups": { "teleport": { "roles": ["template"], "node": { "logins": "alice", "labels": { "host": "*" } }, "app": { "labels": { "env": "staging" } } } } }

目标:把groups.teleport.rolesClaim 映射为 Teleport 角色,把 logins 与 labels 映射为角色模板中的角色条件。

第一步:创建login_rule将任意 JSON 对象映射为一组用户 Trait:

kind: login_rule version: v1 metadata: name: arbitrary-json-idp spec: priority: 0 traits_map: roles: # 求值为 ["template"] - jsonpath("$.groups.teleport.roles") logins: # 求值为 ["alice"] - jsonpath("$.groups.teleport.node.logins") node_labels_*: # 求值为 "*" - jsonpath("$.groups.teleport.node.labels['*']") node_labels_env: # 求值为 [] - jsonpath("$.groups.teleport.node.labels.env") app_labels_*: # 求值为 [] - jsonpath("$.groups.teleport.app.labels['*']") app_labels_env: # 求值为 "staging" - jsonpath("$.groups.teleport.app.labels.env")

注意:在引入 JSONPath-Plus 语法(见第六节)之前,无法直接抓取属性的键名,因此只能映射我们事先知晓的标签。本例只查找*env标签;如果 IdP 新增了类似"team": "devops"的 Claim,在没有额外traits_map规则的情况下不会被映射。

第二步:在 OIDC Connector 的claims_to_roles中引用映射后的 Trait,把template角色分配给用户:

kind: oidc version: v2 metadata: name: arbitrary-json-idp spec: ... claims_to_roles: - claim: "roles" value: "template" roles: ["template"]

第三步:创建模板角色并消费映射后的 Trait:

kind: role version: v7 metadata: name: template spec: allow: logins: '{{external.logins}}' node_labels: '*': '{{external.node_labels_*}}' 'env': '{{external.node_labels_env}}' app_labels: '*': '{{external.app_labels_*}}' 'env': '{{external.app_labels_env}}'

最终 Alice 的有效角色为:

kind: role version: v7 metadata: name: template spec: allow: logins: ['alice'] node_labels: '*': '*' app_labels: 'env': 'staging'

5.2 场景二:分布式 IdP(多 Provider 聚合)

设想一个分布式 IdP,从多个 Provider 源为同一用户聚合 Claims,每个 Provider 在 Teleport 中关联不同的资源集合:

{ "aggregated_claims": { "okta": { "logins": "alice", "env": ["staging", "dev"] }, "auth0": { "logins": "devops", "env": ["prod"] }, "github": { // 该用户没有来自 github 的 claims } } }

同样先创建login_rule。与场景一不同,这里刻意保持每个 Provider 各自的 Trait 分离,并额外用一个自定义teamsTrait 聚合顶层属性名(如okta):

kind: login_rule version: v1 metadata: name: distributed-idp spec: priority: 0 traits_map: okta_logins: # 求值为 ["alice"] - jsonpath("$.aggregated_claims.okta.logins") okta_env: # 求值为 ["staging", "dev"] - jsonpath("$.aggregated_claims.okta.env") auth0_logins: # 求值为 ["devops"] - jsonpath("$.aggregated_claims.auth0.logins") auth0_env: # 求值为 ["prod"] - jsonpath("$.aggregated_claims.auth0.env") github_logins: # 求值为 [] - jsonpath("$.aggregated_claims.github.logins") github_env: # 求值为 [] - jsonpath("$.aggregated_claims.github.env") teams: # 求值为 ["okta", "auth0"] - 'ifelse( !isempty( jsonpath("$.aggregated_claims.okta") ), set("okta"), set())' - 'ifelse( !isempty( jsonpath("$.aggregated_claims.auth0") ), set("auth0"), set())' - 'ifelse( !isempty( jsonpath("$.aggregated_claims.github") ), set("github"), set())'

注意jsonpath可以与ifelseisemptyset等既有表达式函数组合使用,实现对 JSON 结构的条件判断。

然后在 OIDC Connector 的claims_to_roles中按teams分配角色,利用正则捕获组$1直接把 Provider 名作为角色名:

kind: oidc version: v2 metadata: name: distributed-idp spec: ... claims_to_roles: - claim: "teams" value: "^(okta|auth0|github)$" # 求值为 ["okta", "auth0"] roles: ["$1"]

最后为oktaauth0创建带模板的角色,引用各自相关的 Trait:

kind: role version: v7 metadata: name: okta spec: allow: logins: '{{external.okta_logins}}' node_labels: 'env': '{{external.okta_env}}' 'team': "okta" --- kind: role version: v7 metadata: name: auth0 spec: allow: logins: '{{external.auth0_logins}}' node_labels: 'env': '{{external.auth0_env}}' 'team': "auth0"

六、设计权衡:为什么不直接存储 JSON Trait

RFD 的初始设计曾打算直接把任意 JSON OIDC Claims 设置为用户 Trait(例如把整个groups对象原样存进spec.traits),再用jsonpath在角色模板、claims_to_traits等任何 Trait 映射逻辑中插值。该方案在 POC 阶段被否决,原因有三个:

6.1 用户 Trait 的 Protobuf 消息只接受字符串值

从 api/types/wrappers 相关的 Protobuf 定义可以看到,Traits本质上是map<string, StringValues>,而StringValuesrepeated string。也就是说 Trait 的值只能是字符串列表。

// UserSpecV2 is a specification for V2 user message UserSpecV2 { ... // Traits are key/value pairs received from an identity provider (through // OIDC claims or SAML assertions) or from a system administrator for local // accounts. Traits are used to populate role variables. wrappers.LabelValues Traits = 5 [...]; } // StringValues is a list of strings. message StringValues { repeated string Values = 1; } // LabelValues is a list of key value pairs, where key is a string // and value is a list of string values. message LabelValues { map<string, StringValues> Values = 1 [...]; }

要把 JSON 块作为 Trait 值存储,只能二选一:

  • 把 Protobuf map 改成字符串到oneof的映射(允许字符串或 bytes/JSON),这几乎必然要新增TraitsV2字段,并引入随之而来的前后向兼容性问题;
  • 把 JSON 块以字符串形式塞进 Trait 值,jsonpath使用时先尝试反序列化再插值,同时为了tctl get user可读性还要写自定义 marshalling 逻辑——简单但"hacky",会积累技术债并引入潜在性能问题。

6.2 JSON 块 Trait 会撑爆用户 Trait

以分布式 IdP 为例:用户来自不同 Provider 的 Claims 全量作为 Trait 存储,会造成大量冗余。而用登录规则在映射时做一次扁平化聚合,Trait 会小得多:

kind: login_rule version: v1 metadata: name: distributed-idp spec: priority: 0 traits_map: logins: - jsonpath("$.aggregated_claims.*.logins") env: - jsonpath("$.aggregated_claims.*.env")

最终用户 Trait 简洁且无冗余:

kind: user metadata: name: alice spec: ... traits: logins: ["alice", "devops"] env: ["staging", "dev", "prod"]

6.3 JSON Trait 难以推理与维护

如果 JSON 直接进 Trait,管理员在角色模板等处编写jsonpath查询时会很难推断结果;一旦 IdP 侧改了 Claims 结构,管理员需要逐个更新所有jsonpath查询,而不是只改 OIDC Connector 与关联的登录规则。

结论(TLDR):登录规则方案提供了更好的管理 UX、避免了超大用户 Trait 的副作用、并降低了整个特性的实现复杂度。最佳实践是:在 Connector 与 LoginRule 中一次性完成 Claims 到角色/Trait 的映射,而不是把jsonpath插值散落到角色模板各处。

6.4 扩展讨论:JSONPath-Plus 与jsonpathprop

社区中有一个远超 JSONPath 规范的库 JSONPath-Plus,其中最有用的能力是抓取属性名(~)而非仅抓取值。如果能使用属性名,场景一的节点标签映射可以简化成通用表达式。对应设计有两种思路:

  1. 引入put_many表达式,配合 JSONPath-Plus 的~语法:
    kind: login_rule version: v1 metadata: name: arbitrary-json-idp spec: priority: 0 traits_expression: | external.put_many(jsonpath("$.groups.teleport.node.labels~"), jsonpath("$.groups.teleport.node.labels"))
  2. 仅新增一个jsonpathprop函数,在 JSONPath 求值结束时抓取末尾的属性名:
    kind: login_rule version: v1 metadata: name: arbitrary-json-idp spec: priority: 0 traits_expression: | external.put_many(jsonpathprop("$.groups.teleport.node.labels"), jsonpath("$.groups.teleport.node.labels"))

这两种思路在 RFD 中属于"Additional Considerations",是后续演进的候选方向,而非当前已实现的能力。

七、审计、安全与调试

7.1 审计事件

所有未经修改的 OIDC Claims(包括未映射到 Trait 的 Claims)都会原样包含在user.login审计事件中。

目前并没有专门针对"登录规则应用过程"的审计事件。要检查登录规则映射与 Claims 映射逻辑,最直接的方式是使用tctl sso test,配合--debug标志可以输出哪些登录规则成功应用。

7.2 安全考量

RFD 认为该特性不会引入超出标签表达式 RFD(RFD 116)中已覆盖范围之外的安全问题。也就是说,jsonpath只处理 Claims 数据提取与类型转换,其安全边界与既有的标签表达式机制保持一致。

7.3 调试建议

结合上述内容,推荐的实际调试路径是:

  1. tctl sso test --debug验证登录规则是否生效、Trait 映射结果是否符合预期;
  2. 通过user.login审计事件核对 IdP 实际下发的原始 Claims(包含未映射部分);
  3. 使用 JSONPath 在线沙箱先行验证查询表达式,再落到login_rule配置中。

八、总结

RFD 212 为 Teleport 的 OIDC 集成补齐了处理任意 JSON Claims 的能力:通过jsonpath表达式函数与login_ruletraits_map/traits_expression组合,管理员可以把复杂的嵌套 JSON Claims 精确地扁平化为标准 Trait,再无缝接入claims_to_roles、角色模板与标签表达式。其底层选用了最贴近 RFC 9535 的 Go 实现 ojg(go.mod 中为 v1.27.0),并刻意把"JSON 直接入 Trait"的方案排除在外,以保证 Trait 模型的简单性、管理体验与长期可维护性。

从仓库源码来看,该能力已经落地:LoginRule Protobuf 资源 定义了prioritytraits_maptraits_expression三个核心字段;登录规则求值器 通过EvaluationInput.Claimsmap[string]any)接收原始 JSON Claims,并通过EvaluationOutput.Traits输出最终 Trait。想要深入验证的读者,可以继续阅读原始 RFD 文档及其关联的标签表达式 RFD。

【免费下载链接】teleportThe easiest, and most secure way to access and protect all of your infrastructure.项目地址: https://gitcode.com/gh_mirrors/tel/teleport

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/23 2:36:37

分布式事务从原理到落地:五大方案对比与选型指南

上周有个同事跑来问我&#xff1a;订单服务和库存服务拆开之后&#xff0c;用户下单成功&#xff0c;订单状态显示已支付&#xff0c;库存却扣了两次&#xff0c;数据库事务到底还能不能保证一致性&#xff1f;这个问题背后牵扯出来的东西&#xff0c;恰恰就是分布式事务的核心…

作者头像 李华
网站建设 2026/9/23 2:36:05

趋势与季节性时间序列预测:从STL分解到SARIMA建模实战

简介&#xff1a;面向有一定Python基础、希望掌握气候数据预测的时间序列分析初学者&#xff0c;这套实战内容围绕趋势与季节性两个核心维度&#xff0c;结合Pandas、statsmodels、Matplotlib等常用库&#xff0c;系统演示了移动平均提取趋势、STL季节分解、ARIMA/SARIMA建模、…

作者头像 李华
网站建设 2026/9/23 2:34:51

多功能记事本小程序开发:数据模型、同步与防乱码实践

简介&#xff1a;这是一套面向高校计算机相关专业毕业设计场景的多功能记事本系统项目资料&#xff0c;集成记事、分类管理、记录检索等常见功能模块&#xff0c;采用Java技术栈实现前后台分离&#xff0c;适合需要快速完成系统设计、源码阅读或二次开发的学生使用。资源包整体…

作者头像 李华
网站建设 2026/9/23 2:33:14

AI论文网站实测:开题报告从0到1的8个神器组合

“救命神器”这个标题不是我起的&#xff0c;但等我把8个AI论文网站挨个测完之后&#xff0c;我承认这四个字确实不夸张。上个月接到一位学弟的求助&#xff0c;说开题报告堆了三周还没写完&#xff0c;核心问题就三个&#xff1a;文献看不完、研究现状理不清、创新点不知道怎么…

作者头像 李华