ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

EMQX Dashboard 修复 SSO 邮箱用户名 URL 编码下的 RBAC 与自助 MFA 禁用问题

EMQX Dashboard 修复 SSO 邮箱用户名 URL 编码下的 RBAC 与自助 MFA 禁用问题 后端物联网消息队列通信【免费下载链接】emqxThe most scalable and reliable MQTT broker for AI, IoT, IIoT and connected vehicles项目地址https://gitcode.com/gh_mirrors/em/emqx点击查看免费下载EMQX Dashboard 在 5.9 起引入多因素认证MFA并为 SSOOIDC/SAML/LDAP用户提供了基于force_mfa的强制 MFA 策略。本篇文章基于 changes/ee/fix-17122.en.md 这条变更记录深入解读一个实际修复当 SSO 用户名是邮箱地址这类包含特殊字符的字符串在 HTTP 层被 URL 编码如编码为%40时Dashboard 的 RBAC 权限检查可能出错导致 viewer 角色的 SSO 用户在force_mfa关闭时无法正常自助禁用自身 MFA。读完本文你将理解该缺陷的触发链路、EMQX 的admin_overrideMFA 策略模型以及如何在源码与测试层面验证这一修复。变更记录解读原变更记录changes/ee/fix-17122.en.md内容为Fixed dashboard RBAC checks for SSO users with URL-encoded usernames such as email addresses, so viewer self-service MFA disable requests work correctly whenforce_mfais disabled.拆解这条记录可以提炼出三个关键要素要素含义修复对象Dashboard RBAC 权限检查emqx_dashboard_rbac模块问题场景SSO 用户名经 URL 编码典型如邮箱jackson-httpexample.com变成jackson-http%40example.com修复效果viewer 角色的 SSO 用户在force_mfa关闭时自助 MFA 禁用请求DELETE /current_user/mfa能够正确工作这是一次针对身份识别链路的回归修复问题不在 MFA 验证算法本身而在于如何从请求中还原出真正的调用者身份以及RBAC 层是否对该身份做出正确授权。问题场景URL 编码用户名如何触发 RBAC 误判SSO 后端SAML、OIDC、LDAP 等返回的用户名通常是任意字符串邮箱地址是常见形态。当这类用户名被放进 URL 路径例如DELETE /api/v5/current_user/mfa虽然不含用户名路径段但涉及其他含:username绑定参数的接口时HTTP 层会按 RFC 3986 把等保留字符百分号编码为%40。从 emqx_dashboard_rbac/test/emqx_dashboard_rbac_SUITE.erl 的测试注释可以看到该场景的精确描述%% /current_user/mfa carries no username path segment, so an SSO %% username containing (percent-encoded as %40 by the HTTP layer) %% needs no decoding or comparison here. Over the full HTTP path this %% pins the contract: the self-MFA lock is driven by the per-user %% admin_override field, not by the SSO backends live force_mfa %% flag, and it holds for an SSO identity whose name needs escaping. t_delete_own_mfa_sso_admin_override_http(_) - SsoBackend saml, SsoUser jackson-httpexample.com, ...测试中的 SSO 用户名jackson-httpexample.com就是典型的邮箱形态。该测试同时验证了两件事admin_override undefined管理员未做任何 MFA 决策时自助DELETE /current_user/mfa返回204即允许admin_override mfa_required时同一请求被拒绝返回403错误码MFA_ADMIN_REQUIRED。也就是说自助 MFA 禁用的最终授权依据是按用户存储的admin_override字段而不是 SSO 后端的实时force_mfa开关。修复前RBAC 层在处理 URL 编码用户名时身份解析不一致导致 viewer 用户被错误拦截。底层机制SSO 用户名、admin_override 与自助 MFA 策略模型SSO 用户名的唯一键形态在 Dashboard 内部本地用户的主键就是用户名二进制串而 SSO 用户的主键是带后端标签的元组。定义见 apps/emqx_dashboard/include/emqx_dashboard.hrl-define(BACKEND_LOCAL, local). -define(SSO_USERNAME(Backend, Name), {Backend, Name}). -type dashboard_sso_username() :: {dashboard_sso_backend(), binary()}. -type dashboard_username() :: binary() | dashboard_sso_username().例如一个 SAML 用户jackson-httpexample.com在内存中的主键是{saml, jackson-httpexample.com}。emqx_dashboard_rbac模块的注释明确说明emqx_dashboard_rbac.erl该 RBAC 检查的Username参数必须是 admin 记录的主键本地用户为 binarySSO 用户为?SSO_USERNAME元组而 token 验证器通过emqx_dashboard_token:resolve_admin_key/1重建该元组。JWT 解析逻辑见 apps/emqx_dashboard/src/emqx_dashboard_token.erlresolve_admin_key(Token) - case lookup(Token) of {ok, #?ADMIN_JWT{username Username, extra #{backend : Backend}}} when Backend / ?BACKEND_LOCAL - ?SSO_USERNAME(Backend, Username); {ok, #?ADMIN_JWT{username Username}} - Username; _ - undefined end.JWT 记录中保存了extra.backend据此把纯用户名字符串还原成{Backend, Name}元组供下游 handler 直接按主键查找调用者记录无需再从请求里猜测后端。admin_override管理员对 force_mfa 策略的逐用户覆盖admin_override是存储在用户extra字段中的三元决策值定义于 emqx_dashboard.hrl%% admin_override values: admins explicit decision to override the SSO %% backend force_mfa policy for a specific user. undefined / field %% absent means no override — fall back to backends live force_mfa. -define(ADMIN_MFA_REQUIRED, mfa_required). -define(ADMIN_MFA_EXEMPTED, mfa_exempted).语义如下mfa_required管理员强制该用户必须启用 MFA即使后端force_mfa关闭mfa_exempted管理员豁免该用户即使后端force_mfa打开undefined或字段缺失跟随 SSO 后端实时的force_mfa配置。读取与写入分别由 emqx_dashboard_admin.erl 的admin_override_of/1与set_admin_override/2承担。自助禁用与管理员禁用的本质区别disable_mfa/2的第二个参数ByAdmin是区分两种语义的关键emqx_dashboard_admin.erldo_disable_mfa(Username, ByAdmin) - case get_mfa_state(Username) of {ok, disabled} - {error, MFA is already disabled}; _ - Res mria:sync_transaction(?DASHBOARD_SHARD, fun() - update_extra(Username, fun(Extra) - maps:without([mfa_pending], Extra#{mfa_state disabled}) end) end), case Res of {atomic, ok} - %% Admin disable expresses this user is exempt from %% MFA policy; self disable doesnt. ok maybe_set_admin_override(Username, ByAdmin, ?ADMIN_MFA_EXEMPTED), ok; ...管理员禁用ByAdmin true额外写入admin_override mfa_exempted表达该用户从此豁免 MFA 策略即使后端之后把force_mfa打开也不会再锁定该用户用户自助禁用ByAdmin false只把mfa_state置为disabled不触碰admin_override因此后端实时的force_mfa依然对该用户生效。后者的设计意图在emqx_dashboard_sso_mfa模块的注释中解释得很清楚apps/emqx_dashboard_sso/src/emqx_dashboard_sso_mfa.erlSelf-disable must NOT bypass the backends liveforce_mfa; otherwise an SSO viewer on aforce_mfatruebackend could MFA-once, self-DELETE their MFA, and log in without MFA next time.即如果允许自助禁用绕过force_mfa一个 viewer 只需首次登录时完成一次 MFA然后自助DELETE /current_user/mfa就能永久绕开强制 MFA——这是明显的安全漏洞。force_mfa 关闭时 viewer 自助禁用 MFA 的完整调用链结合本次修复force_mfa关闭时 viewer 自助禁用 MFA 的判定链路如下DELETE /api/v5/current_user/mfa └─ emqx_dashboard_api:current_user_mfa(delete, Req) [L992-L1002] └─ authorize_self_mfa_disable(Username) [L1028-L1037] └─ emqx_dashboard_admin:admin_override_of(Username) ├─ mfa_required - {deny, 403, MFA_ADMIN_REQUIRED} └─ 其他(undefined / mfa_exempted) - ok └─ emqx_dashboard_admin:disable_mfa(Username, false) └─ mfa_state disabled不写 admin_override关键实现位于 apps/emqx_dashboard/src/emqx_dashboard_api.erlcurrent_user_mfa(delete, Req) - with_caller(Req, fun(#?ADMIN{username Username}) - LogMeta #{msg dashboard_user_mfa_disable, username Username}, case authorize_self_mfa_disable(Username) of ok - mfa_result(emqx_dashboard_admin:disable_mfa(Username, false), LogMeta); {deny, Code, ErrCode, Msg} - ?SLOG(warning, LogMeta#{result denied, reason ErrCode}), {Code, ErrCode, Msg} end end).而authorize_self_mfa_disable/1的授权策略emqx_dashboard_api.erl为authorize_self_mfa_disable(Username) - case emqx_dashboard_admin:admin_override_of(Username) of ?ADMIN_MFA_REQUIRED - {deny, 403, ?MFA_ADMIN_REQUIRED, An administrator requires MFA on this account, so it cannot be turned off here. It can still be re-keyed. }; _ - ok end.注意/current_user/mfa是自服务端点路径上不携带:username绑定参数。RBAC 层对所有已认证的 Dashboard 用户放行此类自服务路由emqx_dashboard_rbac.erl%% Self-service: the caller IS the subject, so the authenticated %% identity alone authorizes the operation. These paths carry no %% :username binding, so there is no target to compare the actor %% against and nothing to spoof. do_check_rbac(#{}, _, ?DASHBOARD_API(_, Fn)) when Fn current_user; Fn current_user_change_pwd; Fn current_user_mfa; Fn change_pwd - true;于是viewer 是否能自助禁用 MFA最终完全取决于admin_override_of(Username)能否用正确的用户主键查到记录——这正是本次修复的核心。调用者记录由with_caller/2通过caller_admin/1获取后者直接使用 JWT 中auth_meta.source即resolve_admin_key/1解析出的主键做精确查找emqx_dashboard_api.erlcaller_admin(#{auth_meta : #{auth_type : jwt_token, source : Username}}) - %% auth_meta.source is the resolved admin record key — for SSO %% users this is ?SSO_USERNAME(Backend, Name) (atom-tagged %% tuple), populated by emqx_dashboard:authorize/2 via %% emqx_dashboard_token:resolve_admin_key/1. Plain lookup is %% sufficient. case emqx_dashboard_admin:lookup_user(Username) of [Admin] - Admin; _ - undefined end;lookup_user/1要求传入的键与 Mnesia 中存储的主键完全一致包括元组形态任何一层把{saml, jackson-httpexample.com}误当成本地用户jackson-httpexample.com或带%40的编码字符串都会查不到记录进而把调用者是自己误判为未找到用户或越权操作。修复前RBAC 层对 URL 编码用户名做解码/比较时形态不统一viewer 的自助禁用请求因此被错误拒绝。修复的关键保证RBAC 不依赖后端实时 force_mfa另一个值得强调的点在本次修复中RBAC 层刻意不读取SSO 后端的实时force_mfa标志授权决定完全交给 handler 层的authorize_self_mfa_disable/1由admin_override驱动。配套测试 t_delete_own_mfa_sso_force_mfa 专门验证了这一策略无关性t_delete_own_mfa_sso_force_mfa(_) - %% RBAC does not consult the SSO backends live force_mfa flag for %% self-MFA: /current_user/mfa is allowed for any authenticated %% user and the decision belongs to the handler %% (authorize_self_mfa_disable/1, driven by admin_override). ... try ok emqx_config:put([dashboard, sso, SsoBackend], SsoConfig#{force_mfa false}), ?assertMatch({ok, #{actor : SsoUser}}, Delete(SsoToken)), ok emqx_config:put([dashboard, sso, SsoBackend], SsoConfig#{force_mfa true}), ?assertMatch({ok, #{actor : SsoUser}}, Delete(SsoToken)), ?assertMatch({ok, #{actor : LocalUser}}, Delete(LocalToken)) after ok emqx_config:put([dashboard, sso, SsoBackend], SsoConfig) end, ok.测试在force_mfa false和force_mfa true两种取值下SSO viewer 与本地 viewer 的自助删除均返回成功——证明 RBAC 放行与后端策略无关。那么force_mfa在哪里起作用在登录阶段。force_mfa属于各 SSO 后端的公共配置项定义于 apps/emqx_dashboard_sso/src/emqx_dashboard_sso_schema.erl{force_mfa, mk( boolean(), #{ desc ?DESC(force_mfa), required false, default false } )},默认值为false即 SSO 后端默认不强制 MFA。登录时由emqx_dashboard_sso_mfa:check_sso_mfa/2综合admin_override与后端实时force_mfa决定用户是否需要先完成 TOTP 绑定或验证emqx_dashboard_sso_mfa.erlget_force_mfa(Backend) - case emqx:get_config(?MOD_KEY_PATH(Backend), undefined) of #{force_mfa : ForceMfa} - ForceMfa; _ - false end. mfa_required_for_user(Username, Backend) - case emqx_dashboard_admin:admin_override_of(Username) of ?ADMIN_MFA_REQUIRED - true; ?ADMIN_MFA_EXEMPTED - false; undefined - get_force_mfa(Backend) end.由此形成完整闭环登录时force_mfa true且用户无admin_override豁免 → 强制绑定/验证 TOTPforce_mfa false→ 直接放行登录自助禁用时本次修复的场景force_mfa关闭 admin_override非mfa_required→ 允许 viewer 自助DELETE /current_user/mfa安全兜底force_mfa打开时即使 viewer 自助禁用了 MFAmfa_state disabled、admin_override undefined下次登录仍会被要求重新绑定 TOTP对应测试t_check_sso_mfa_self_disabled_still_forced见 emqx_dashboard_sso_mfa_SUITE.erl。如何验证与复现仓库中的 Common Test 用例可直接验证本修复两个最相关的用例均位于 apps/emqx_dashboard_rbac/test/emqx_dashboard_rbac_SUITE.erlt_delete_own_mfa_sso_admin_override_httpL318-L333使用 SAML 后端、用户名jackson-httpexample.com邮箱 特殊字符验证admin_override为undefined时自助删除返回204、为mfa_required时返回403t_delete_own_mfa_sso_force_mfaL335-L367验证 RBAC 层在force_mfa两种取值下对 SSO viewer 与本地 viewer 的自助删除均放行。如果要在本地跑这些用例可以按 EMQX 的标准 CT 流程执行例如# 在仓库根目录下运行 emqx_dashboard_rbac 的 CT 套件需要先完成编译与依赖准备 make ct appsapps/emqx_dashboard_rbac运维视角配置建议与注意事项结合本次修复给使用 SSO MFA 的运维人员几点可落地的建议force_mfa默认关闭。各 SSO 后端OIDC/SAML/LDAP的force_mfa配置项默认值均为false见 emqx_dashboard_sso_schema.erl。如需强制所有 SSO 用户启用 MFA可在对应后端的配置块中显式设置dashboard.sso.saml { enable true backend saml force_mfa true # ... 其他 SAML 后端参数 }注意force_mfa是实时读取的后端级开关get_force_mfa/1每次登录都会重新查询配置修改后即时生效无需重启节点。逐用户豁免/强制使用admin_override管理员通过users/{username}/mfa的DELETE写mfa_exempted或POST写mfa_required即emqx_dashboard_admin:reinit_mfa(..., true)可以覆盖后端策略而用户自助禁用不会写入admin_override因此force_mfa true的后端上自助禁用会被下一次登录自动拉回。对邮箱类用户名的场景本次修复保证了含等特殊字符的 SSO 用户名在 RBAC 检查、调用者查找caller_admin/1与is_caller/2emqx_dashboard_api.erl中形态一致——全部以?SSO_USERNAME(Backend, Name)元组为准不依赖对 URL 编码字符串的解码比较从而避免查无此人或误判越权。Dashboard MFA 的整体启用方式Dashboard 的 MFA 默认关闭管理员需设置dashboard.default_mfa为{mechanism: totp}或none后再按用户启用完整的两步登录协议首次登录返回BAD_MFA_TOKEN与secret用于生成二维码二次登录提交 TOTP 校验可参见 apps/emqx_dashboard/README.md。小结本次修复changes/ee/fix-17122.en.md本质上解决的是SSO 用户身份键形态不一致导致的 RBAC 误判问题URL 编码的用户名如邮箱中的%40不再影响admin_override查询与自助 MFA 授权判定。修复后viewer 角色的 SSO 用户在force_mfa关闭时可以正常自助禁用自身 MFA同时force_mfa开启时的强制策略与自助禁用不豁免的安全兜底依然成立——权限判定与策略判定各司其职分别由 RBAC 层的admin_override与登录阶段的force_mfa负责。赞分享后端物联网消息队列通信【免费下载链接】emqxThe most scalable and reliable MQTT broker for AI, IoT, IIoT and connected vehicles项目地址https://gitcode.com/gh_mirrors/em/emqx点击查看免费下载相关推荐EMQX Dashboard RBAC 修复解析命名空间管理员自管 MFA 的权限边界EMQX Dashboard RBAC 修复解析命名空间管理员自管 MFA 的权限边界 本文以 changes/ee/fix 17171.en.md EMQ后端物联网消息队列通信EMQX Dashboard SSO OIDC Issuer 校验修复支持带端口与子路径的 HTTPS Issuer URLEMQX Dashboard SSO OIDC Issuer 校验修复支持带端口与子路径的 HTTPS Issuer URL 本文围绕 EMQX 企业版变更记后端物联网消息队列通信EMQX 5.10.4 细粒度作用域访问控制登录用户 Scope、SSO 强制 MFA 与 Break-Glass 账户保护EMQX 5.10.4 细粒度作用域访问控制登录用户 Scope、SSO 强制 MFA 与 Break Glass 账户保护 本文基于 EMQX 开源仓库中的后端物联网消息队列通信上一篇Apache Paimon 数据快照管理完全指南下一篇Delta Lake最佳实践指南基于delta-rs项目的高效数据管理策略创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表