
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 212JSONPath 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_map或traits_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 年有了官方 RFCRFC 9535但其起源可以追溯到 2007 年的一篇文章。由于原始文章遗留了许多未解答的问题不同 JSONPath 项目各自给出了自己的答案导致这门语言衍生出多种方言。RFC 的发布帮助这些项目重新对齐但社区中仍然存在不少未解决的语法分歧。2.2 为什么选择 ojgRFD 作者参考了社区维护的 JSONPath 对比项目结论是在列出的 Go 项目中github.com/ohler55/ojg是当前最接近 RFC 规范的实现。选择一个贴近规范的库就可以有选择地依赖官方 JSONPath 文档与在线沙箱来验证查询行为。在当前仓库的 go.mod 中可以确认该依赖确实已经引入github.com/ohler55/ojg v1.27.02.3 升级上游库时的注意事项RFD 特别提醒在升级上游 ojg 库时必须谨慎。因为该库仍在向 RFC 靠拢且社区中部分语法分歧尚未解决一些语法可能在升级后发生变化。这意味着依赖jsonpath的登录规则表达式需要在上游库升级时做回归验证。三、OIDC Claims 在 Teleport 登录中的双重用途在 OIDC 登录过程中用户的 Claims 承担两个职责设置为 Teleport 用户 Trait可选地通过 login rule 进行自定义的 Claims 到 Trait 的映射。决定授予用户的角色通过 OIDC Connector 的claims_to_roles字段将用户 Trait 映射为角色。jsonpath函数被设计为只支持 login rule 的 trait 映射。这样任何被映射为 Trait 的 JSON OIDC Claims都会自动进入 Traits-to-Roles 映射的可用范围。为什么不让 JSON 值直接成为用户 Trait这是设计团队经过 POC 验证后的明确决策其理由在本文第六节详述。四、登录规则Login RuleClaims 到 Trait 的映射4.1 LoginRule 资源结构从源码看LoginRule是 Teleport 中的一等资源其 Protobuf 定义位于 api/proto/teleport/loginrule/v1/loginrule.protopriority登录规则在集群中的相对优先级数值越小越先被评估traits_mapTrait 键到谓词表达式列表的映射每个表达式应求值为该 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]再配合角色模板消费外部 Traitkind: 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 提供的 TraitSSO 用户或内部静态 Trait本地用户作为登录规则求值的输入EvaluationInput.Claims原始的、未解析的 Provider Claims每个 Claim 可能是标准字符串/列表也可能是任意 JSON 对象类型为map[string]any——这正是jsonpath函数处理的对象EvaluationOutput.Traits登录规则求值的最终输出 TraitEvaluationOutput.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 对象映射为一组用户 Traitkind: 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]第三步创建模板角色并消费映射后的 Traitkind: 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: staging5.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 聚合顶层属性名如oktakind: 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可以与ifelse、isempty、set等既有表达式函数组合使用实现对 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]最后为okta与auth0创建带模板的角色引用各自相关的 Traitkind: 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 TraitRFD 的初始设计曾打算直接把任意 JSON OIDC Claims 设置为用户 Trait例如把整个groups对象原样存进spec.traits再用jsonpath在角色模板、claims_to_traits等任何 Trait 映射逻辑中插值。该方案在 POC 阶段被否决原因有三个6.1 用户 Trait 的 Protobuf 消息只接受字符串值从 api/types/wrappers 相关的 Protobuf 定义可以看到Traits本质上是mapstring, StringValues而StringValues是repeated 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 { mapstring, 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其中最有用的能力是抓取属性名~而非仅抓取值。如果能使用属性名场景一的节点标签映射可以简化成通用表达式。对应设计有两种思路引入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))仅新增一个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 认为该特性不会引入超出标签表达式 RFDRFD 116中已覆盖范围之外的安全问题。也就是说jsonpath只处理 Claims 数据提取与类型转换其安全边界与既有的标签表达式机制保持一致。7.3 调试建议结合上述内容推荐的实际调试路径是用tctl sso test --debug验证登录规则是否生效、Trait 映射结果是否符合预期通过user.login审计事件核对 IdP 实际下发的原始 Claims包含未映射部分使用 JSONPath 在线沙箱先行验证查询表达式再落到login_rule配置中。八、总结RFD 212 为 Teleport 的 OIDC 集成补齐了处理任意 JSON Claims 的能力通过jsonpath表达式函数与login_rule的traits_map/traits_expression组合管理员可以把复杂的嵌套 JSON Claims 精确地扁平化为标准 Trait再无缝接入claims_to_roles、角色模板与标签表达式。其底层选用了最贴近 RFC 9535 的 Go 实现 ojggo.mod 中为 v1.27.0并刻意把JSON 直接入 Trait的方案排除在外以保证 Trait 模型的简单性、管理体验与长期可维护性。从仓库源码来看该能力已经落地LoginRule Protobuf 资源 定义了priority、traits_map、traits_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),仅供参考