iii 访问控制实战:用 iii-worker-manager 的 RBAC 监听器为不可信客户端开门

iii 访问控制实战:用 iii-worker-manager 的 RBAC 监听器为不可信客户端开门 iii 访问控制实战用 iii-worker-manager 的 RBAC 监听器为不可信客户端开门【免费下载链接】iiiEffortlessly compose, extend, and observe every service in real-time for the first time ever.项目地址: https://gitcode.com/GitHub_Trending/mo/iii在 iii 引擎中iii-worker-manager是一个强制常驻 worker掌管所有 worker 与引擎之间的 WebSocket 连接它默认在49134端口上运行一个可信监听器而你自建的 worker 正是通过III_URL连入这里。当你需要让浏览器标签页、第三方进程等你并不亲自运行的不可信客户端接入引擎时做法是追加一个RBAC 门控监听器——一个独立端口每条连接都由你编写的auth函数认证每次调用都要对照expose_functions允许清单校验。本文基于仓库文档 worker-manager.mdx结合引擎侧源码engine/src/workers/worker/完整讲清双监听器配置、auth 函数、客户端接入、中间件拦截与注册钩子的全链路用法。可信监听器与 RBAC 监听器为什么需要第二个端口默认的49134监听器是可信的任何能到达它的 worker 都可以注册函数、调用任意函数。这对你自己运行的 worker 完全没问题但绝不能暴露给浏览器或其他不可信 worker。RBAC 监听器则运行在独立端口上每条连接都经过auth函数门控每个会话都被限制在一个函数允许清单之内。一个必须牢记的陷阱原文档以警告形式强调在config.yaml中声明任意iii-worker-manager条目就会替换掉默认的49134可信监听器。因此你必须连同 RBAC 条目一起把自己那份可信的49134监听器也显式声明出来否则你自己的 worker 会失去连接入口。iii-worker-manager始终在运行无需添加直接在config.yaml中配置即可。按文档给出的模式同时声明两个监听器——49134上供自有 worker 使用的可信监听器以及3110上面向浏览器等不可信连接的 RBAC 监听器workers: # ... # 你自己 worker 的可信监听器。会替换默认的 49134。 - name: iii-worker-manager config: port: 49134 # 面向浏览器的 RBAC 门控监听器使用独立端口。 - name: iii-worker-manager config: host: 127.0.0.1 port: 3110 rbac: auth_function_id: auth::browser expose_functions: - match(link::create) - match(stream::*)auth_function_id指定一个函数每来一条连接worker 就调用它一次用它授权授权失败则拒绝该客户端连接。expose_functions则是会话可调用的函数 ID 允许清单。从源码结构看监听器的完整配置结构是 WorkerManagerConfigport默认49134、host默认0.0.0.0、可选的middleware_function_id与rbac块以及一个默认 10000ms 的handshake_timeout_ms限制已接受 TCP 连接完成 WS 升级的时限防止握手挂起的 socket 泄漏。该 worker 在 mod.rs 中以mandatory强制标记注册印证了文档始终在运行、无需添加的说法。仓库内 engine/src/workers/worker/README.md 还给出等价的 map 形式写法第一个iii-worker-manager条目设置主引擎端口iii-worker-manager#rbac这样的#instance条目则开启独立监听器两个示例配置可互换使用。expose_functions通配符与元数据两类过滤器expose_functions的每个条目有两种形式对函数 ID 做 glob 匹配的match(...)或匹配以特定元数据注册的函数的metadata:选择器expose_functions: - match(public::*) # public:: 命名空间下的任意函数 - match(engine::functions::list) # 某个特定函数 - metadata: public: true # 所有 metadata.public 为 true 的函数的函数函数的metadata见 functions 文档是注册函数时设置的一个任意 JSON 对象你可以按访问模型随意设计它的形状并精确地围绕自己选择的字段做门控例如{ public: true }、{ tier: premium }或{ scopes: [read] }。这里有两个必须遵守的安全原则所有访问都应系统侧门控。绝不能依赖不可信 worker 自己声明它能访问什么。例如上面metadata.public true这个标记是由可信侧管理、在注册时由引擎保存的而不是 worker 请求 payload 里携带的字段。过滤器匹配是任一命中即放行OR 语义而单条metadata规则内部所有键必须同时匹配AND 语义。这一点在 rbac_config.rs 的FunctionFilter::matches中可以直接看到metadata 分支对expected的每个键值对都要求all(...)成立。通配符匹配的实现也很直白wildcard_match 支持*匹配任意多字符模式两端锚定match(*)匹配一切、match(api::*::read)这类中段通配也可以。另外从源码的解析器visit_map可以看到过滤器还支持 map 写法{ match: svc::* }与可选的namespace:键——不带 namespace 的规则只作用于default命名空间遇到未知键会直接报错避免把拼写错误静默降级成只对 default 生效的授权配置。编写 auth 函数在 WebSocket 升级前完成认证auth 函数运行在WebSocket 升级过程中、客户端建立连接之前。它收到请求的headers、query_params和客户端ip_address返回该会话的权限抛错即拒绝连接。这一点在引擎侧 Session::authenticate 中可以印证引擎把三项输入打包成 JSON 调用auth_function_id调用失败或返回为空都会以AUTH_ERROR拒绝握手。注意auth 函数本身通过可信的49134端口连接它是你自己的 worker 之一只有不可信客户端才走3110。Node / TypeScriptimport { registerWorker } from iii-sdk; const worker registerWorker(process.env.III_URL ?? ws://localhost:49134, { workerName: auth, }); worker.registerFunction( auth::browser, async (input: { headers: Recordstring, string; query_params: Recordstring, string[]; ip_address: string; }) { const token input.query_params.token?.[0]; if (token ! (process.env.BROWSER_TOKEN ?? dev-token)) { throw new Error(unauthorized); } return { allowed_functions: [], // 在 expose_functions 之外额外放行的 ID forbidden_functions: [], // 即使匹配也拒绝优先级最高 allow_trigger_type_registration: false, // 该 worker 能否注册自己的触发器类型 allow_function_registration: true, // 该 worker 能否注册自己的回调 context: { source: browser }, // 每次调用都会转发给中间件 }; }, );Pythonimport os from iii import register_worker, InitOptions worker register_worker( os.environ.get(III_URL, ws://localhost:49134), InitOptions(worker_nameauth), ) def browser(req: dict) - dict: token (req.get(query_params, {}).get(token) or [None])[0] if token ! os.environ.get(BROWSER_TOKEN, dev-token): raise Exception(unauthorized) return { allowed_functions: [], # 在 expose_functions 之外额外放行的 ID forbidden_functions: [], # 即使匹配也拒绝优先级最高 allow_trigger_type_registration: False, # 注册自己的触发器类型 allow_function_registration: True, # 注册自己的回调 context: {source: browser}, # 每次调用都转发给中间件 } worker.register_function(auth::browser, browser)Rustuse iii_helpers::worker_connection_manager::AuthInput; use iii_sdk::protocol::TriggerRequest; use iii_sdk::{InitOptions, MiddlewareFunctionInput, RegisterFunction, register_worker}; use serde_json::{json, Value}; let url std::env::var(III_URL).unwrap_or_else(|_| ws://localhost:49134.into()); let worker register_worker(url, InitOptions::default()); worker.register_function(auth::browser, RegisterFunction::new(|input: AuthInput| { let token input.query_params.get(token).and_then(|v| v.first()); let expected std::env::var(BROWSER_TOKEN).unwrap_or_else(|_| dev-token.into()); if token.map(String::as_str) ! Some(expected.as_str()) { return Err(iii_sdk::Error::Handler(unauthorized.into())); } Ok(json!({ allowed_functions: [], // 在 expose_functions 之外额外放行的 ID forbidden_functions: [], // 即使匹配也拒绝优先级最高 allow_trigger_type_registration: false, // 注册自己的触发器类型 allow_function_registration: true, // 注册自己的回调 context: { source: browser }, // 每次调用都转发给中间件 })) }));AuthResult 字段全解auth 函数返回的对象即AuthResultrbac_session.rs 中定义了各字段默认值字段含义allowed_functions在expose_functions之外额外允许的函数 ID。forbidden_functions即使匹配也要拒绝的函数 ID。优先级最高。allowed_trigger_types会话可绑定的触发器类型。省略表示全部允许。allow_trigger_type_registration会话能否注册新的触发器类型默认false。allow_function_registration会话能否注册函数默认true见源码default_true。context任意对象每次调用都会转发给中间件默认空对象。function_registration_prefix可选前缀应用于该会话注册的每个函数。关于function_registration_prefixengine/src/workers/worker/README.md 补充了完整机制引擎会透明地给该会话注册的每个函数 ID 加上{prefix}::trigger 注册也会自动为所引用的function_id加前缀而当引擎把调用派发回 worker 时前缀会被剥掉worker SDK 始终看到无前缀的本地 ID。相当于给每个认证会话一个私有命名空间worker 代码无需自己管理前缀。一个 auth 函数可以授予多组授权因此同一个函数可用于授权多类 worker。实践中这是授权相似 worker与把不同 worker 拆到不同iii-worker-manager定义之间的取舍——你可以在配置中定义任意多个iii-worker-manager。连接客户端把浏览器变成 worker浏览器用iii-browser-sdk指向 RBAC 端口即可。由于浏览器无法设置自定义 WebSocket 请求头token 只能走查询参数——正是auth::browser读取的那个参数import { registerWorker } from iii-browser-sdk; const token import.meta.env.VITE_BROWSER_TOKEN ?? dev-token; export const worker registerWorker(ws://localhost:3110?token${encodeURIComponent(token)});此后该会话只能调用expose_functions加上allowed_functions所允许的函数。想做一个把浏览器标签页完整变成一个 worker 的端到端教程可参考仓库 docs/0-21-0/tutorials/linkly 中的 Ch. 7: Bring in the browser 章节。用 middleware 拦截每一次调用在监听器上设置middleware_function_id即可让一个函数处理经由该 RBAC 端口进来的每一次调用。它收到调用本体加 auth 结果中的会话context充当代理角色把可选改写的调用转发出去并返回其结果或者抛错拒绝。- name: iii-worker-manager config: port: 3110 middleware_function_id: auth::middleware rbac: auth_function_id: auth::browser expose_functions: - match(link::create)Node / TypeScriptworker.registerFunction( auth::middleware, async (input: { function_id: string; payload: Recordstring, unknown; context: Recordstring, unknown; }) { // 把已认证的调用者盖进每个 payload然后转发调用 const payload { ...input.payload, _source: input.context.source }; return worker.trigger({ function_id: input.function_id, payload }); }, );Pythondef middleware(req: dict) - dict: # 把已认证的调用者盖进每个 payload然后转发调用 payload {**req[payload], _source: req[context][source]} return worker.trigger({function_id: req[function_id], payload: payload}) worker.register_function(auth::middleware, middleware)Rustlet mw worker.clone(); worker.register_function( auth::middleware, RegisterFunction::new_async(move |input: MiddlewareFunctionInput| { let worker mw.clone(); async move { // 把已认证的调用者盖进每个 payload然后转发调用 let mut payload input.payload.clone(); if let Some(obj) payload.as_object_mut() { obj.insert( _source.into(), input.context.get(source).cloned().unwrap_or(Value::Null), ); } worker .trigger(TriggerRequest { function_id: input.function_id, payload, action: None, timeout_ms: None, }) .await } }), );中间件输入MiddlewareFunctionInput的完整字段见 engine/src/workers/worker/README.mdfunction_id目标函数、payload原始负载、可选actionenqueue、void等路由动作与context来自AuthResult.context未配置 RBAC 时为空对象。按 README 的建议中间件是做请求校验、限流、审计日志的正确位置保持其幂等性避免重试导致重复计费或重复记日志。rbac 块中的注册钩子rbac块还接受三个钩子字段见 RbacConfig 的定义on_function_registration_function_id、on_trigger_registration_function_id与on_trigger_type_registration_function_id。它们分别在该不可信会话注册函数、触发器或触发器类型时运行可以改写注册内容返回映射后的字段省略的字段保持原值或抛错拒绝。钩子输入字段可映射的输出字段on_function_registration_function_idfunction_id含前缀、description、metadata、contextfunction_id、description、metadataon_trigger_registration_function_idtrigger_id、trigger_type、function_id、config、contexttrigger_id、trigger_type、function_id、configon_trigger_type_registration_function_idtrigger_type_id、description、contexttrigger_type_id、description生效规则是注册触发器类型需要allow_trigger_type_registration为true且若配置了钩子钩子返回结果注册触发器需要其trigger_type在allowed_trigger_types之内或该字段省略且钩子若配置返回结果。会话注册的触发器在该 worker 断连时会被自动清理即权限与生命周期严格绑定在连接上。源码级验证每次调用的访问裁决流程RBAC 监听器上每次调用都会走一条固定的裁决链rbac_config.rs 中的注释把它画得很清楚function_id在forbidden_functions中 →拒绝在allowed_functions中 →允许属于始终放行的基础设施函数 →允许任一expose_functions过滤器命中 →允许否则 →拒绝。第 3 步的例外清单 INFRASTRUCTURE_FUNCTIONS 是一组固定的引擎内置 IDengine::channels::create、engine::workers::register、engine::log::info/warn/error/debug/trace、engine::baggage::get/set/get_all。无论操作者配置了多严格的过滤器这些函数永远放行——否则连接建立、日志和上下文传播都会先于你的业务函数坏掉。源码注释同时说明这是 iii 的公共契约同一主版本内只增不删重命名会新旧 ID 并存至少一个主版本。裁决函数本体是 is_function_allowed其中forbidden_functions是全局拒绝连基础设施例外都能压过——但引擎会为此打一条警告日志因为禁用基础设施 ID 会导致 worker 行为不可预测。这套行为在端到端测试 engine/tests/rbac_infrastructure_e2e.rs 与 rbac_config.rs 内的单元测试 中有逐条覆盖包括拒绝优先于允许默认拒绝未暴露的函数基础设施函数恒可达等断言。安全要点小结结合文档的警告与 engine/src/workers/worker/README.md 的 Security Considerations主引擎端口第一个iii-worker-manager条目应保持内网可达只有带 RBAC 的监听器才应面向外部网络并用防火墙规则或网络策略强制这一点。凡面对不可信网络的监听器必须设置auth_function_id——没有rbac块的监听器不认证任何东西。优先使用窄的expose_functions模式而非match(*)每当引擎里新增命名空间时都要复查这份清单。auth 结果里的forbidden_functions是硬性拒绝机制适合做按用户/按角色的拒绝清单操作者的expose_functions无法覆盖它。所有访问判定必须发生在系统侧绝不可把能访问什么的决定权交给不可信 worker。至此从双监听器配置、auth 函数、浏览器接入、中间件改写到注册钩子与裁决流程iii-worker-manager的 RBAC 能力已经完整闭环可信 worker 继续走49134不可信客户端被限制在独立端口、逐连接认证、逐调用校验权限边界完全由你写的函数和config.yaml中的清单决定。【免费下载链接】iiiEffortlessly compose, extend, and observe every service in real-time for the first time ever.项目地址: https://gitcode.com/GitHub_Trending/mo/iii创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考