This guide walks you through enabling RBAC on your iii instance using the WorkerModule. Workers connect via WebSocket to a dedicated RBAC port, separate from the internal engine bridge.
1. Add RBAC Config to the WorkerModule
Add a modules::worker::WorkerModule entry with RBAC config to your engine config file. At minimum you need a port and an expose_functions list inside rbac:
modules:
# ... your existing modules ...
- class: modules::worker::WorkerModule
config:
port: 49135
rbac:
expose_functions:
- match("api::*")
This exposes all functions whose ID starts with api:: on port 49135 with no authentication.
2. Write an Auth Function
For production, authenticate workers by registering a function that validates credentials from the WebSocket upgrade request. The function receives an AuthInput and must return an AuthResult.
Node / TypeScript
Python
Rust
import type { AuthInput, AuthResult } from 'iii-sdk'
import { registerWorker } from 'iii-sdk'
const iii = registerWorker('ws://localhost:49134')
iii.registerFunction(
{ id: 'my-project::auth-function' },
async (input: AuthInput): Promise<AuthResult> => {
const token = input.headers?.['authorization']?.replace(/^Bearer\s+/i, '')
const apiKey = input.query_params?.['api_key']?.[0]
if (!token && !apiKey) {
throw new Error('Missing credentials')
}
const user = await validateCredentials(token || apiKey)
return {
allowed_functions: [],
forbidden_functions: user.role === 'readonly'
? ['api::users::delete', 'api::users::update']
: [],
allowed_trigger_types: user.role === 'admin'
? ['cron', 'webhook']
: undefined,
allow_trigger_type_registration: user.role === 'admin',
context: {
user_id: user.id,
role: user.role,
},
}
},
)
from iii import AuthInput, AuthResult, register_worker
iii = register_worker('ws://localhost:49134')
async def auth_function(data: dict) -> dict:
auth_input = AuthInput(**data)
token = auth_input.headers.get('authorization', '').replace('Bearer ', '', 1)
api_key = (auth_input.query_params.get('api_key') or [None])[0]
if not token and not api_key:
raise Exception('Missing credentials')
user = await validate_credentials(token or api_key)
return AuthResult(
allowed_functions=[],
forbidden_functions=['api::users::delete', 'api::users::update']
if user['role'] == 'readonly' else [],
allowed_trigger_types=['cron', 'webhook']
if user['role'] == 'admin' else None,
allow_trigger_type_registration=user['role'] == 'admin',
context={
'user_id': user['id'],
'role': user['role'],
},
).model_dump()
iii.register_function({'id': 'my-project::auth-function'}, auth_function)
use iii_sdk::{AuthInput, AuthResult, RegisterFunction, register_worker, InitOptions};
use serde_json::json;
let iii = register_worker("ws://localhost:49134", InitOptions::default());
iii.register_function(RegisterFunction::new_async(
"my-project::auth-function",
|auth_input: AuthInput| async move {
let token = auth_input.headers.get("authorization")
.map(|v| v.trim_start_matches("Bearer ").to_string());
let api_key = auth_input.query_params.get("api_key")
.and_then(|v| v.first())
.cloned();
let credential = token.or(api_key)
.ok_or_else(|| iii_sdk::IIIError::Handler("Missing credentials".into()))?;
let user = validate_credentials(&credential).await?;
Ok(AuthResult {
allowed_functions: vec![],
forbidden_functions: if user.role == "readonly" {
vec!["api::users::delete".into(), "api::users::update".into()]
} else {
vec![]
},
allowed_trigger_types: if user.role == "admin" {
Some(vec!["cron".into(), "webhook".into()])
} else {
None
},
allow_trigger_type_registration: user.role == "admin",
context: json!({ "user_id": user.id, "role": user.role }),
})
},
));
Update your config to reference the auth function:
modules:
- class: modules::worker::WorkerModule
config:
port: 49135
rbac:
auth_function_id: my-project::auth-function
expose_functions:
- match("api::*")
3. Write a Middleware Function (Optional)
A middleware sits between the worker and the target function. Use it for validation, rate limiting, audit logging, or enriching the payload with auth context. The function receives a MiddlewareFunctionInput.
Node / TypeScript
Python
Rust
import type { MiddlewareFunctionInput } from 'iii-sdk'
iii.registerFunction(
{ id: 'my-project::middleware-function' },
async (input: MiddlewareFunctionInput) => {
console.log(`[audit] user=${input.context.user_id} invoking ${input.function_id}`)
const enrichedPayload = {
...input.payload,
_caller_id: input.context.user_id,
_caller_role: input.context.role,
}
return iii.trigger({
function_id: input.function_id,
payload: enrichedPayload,
})
},
)
from iii import MiddlewareFunctionInput
async def middleware_function(data: dict) -> dict:
mid = MiddlewareFunctionInput(**data)
print(f"[audit] user={mid.context['user_id']} invoking {mid.function_id}")
enriched_payload = {
**mid.payload,
'_caller_id': mid.context['user_id'],
'_caller_role': mid.context['role'],
}
return await iii.trigger_async({
'function_id': mid.function_id,
'payload': enriched_payload,
})
iii.register_function({'id': 'my-project::middleware-function'}, middleware_function)
use iii_sdk::{MiddlewareFunctionInput, RegisterFunction, TriggerRequest};
use serde_json::json;
let iii_clone = iii.clone();
iii.register_function(RegisterFunction::new_async(
"my-project::middleware-function",
move |input: MiddlewareFunctionInput| {
let iii = iii_clone.clone();
async move {
let mut enriched = input.payload.as_object().cloned().unwrap_or_default();
enriched.insert("_caller_id".into(), json!(input.context.get("user_id")));
enriched.insert("_caller_role".into(), json!(input.context.get("role")));
iii.trigger(TriggerRequest {
function_id: input.function_id,
payload: json!(enriched),
action: None,
timeout_ms: None,
}).await
}
},
));
Add it to your config. Note that middleware_function_id sits at the config level, not inside rbac:
modules:
- class: modules::worker::WorkerModule
config:
port: 49135
middleware_function_id: my-project::middleware-function
rbac:
auth_function_id: my-project::auth-function
expose_functions:
- match("api::*")
4. Connect a Worker
The RBAC port speaks the standard iii engine protocol. Connect using the SDK:
Node / TypeScript
Python
Rust
import { registerWorker } from 'iii-sdk'
const worker = registerWorker('ws://localhost:49135', {
headers: { authorization: 'Bearer my-token' },
})
const result = await worker.trigger({
function_id: 'api::users::list',
payload: { limit: 10 },
})
console.log(result)
from iii import InitOptions, register_worker
worker = register_worker(
'ws://localhost:49135',
InitOptions(headers={'authorization': 'Bearer my-token'}),
)
result = worker.trigger({
'function_id': 'api::users::list',
'payload': {'limit': 10},
})
print(result)
use iii_sdk::{register_worker, InitOptions, TriggerRequest};
use serde_json::json;
use std::collections::HashMap;
let mut headers = HashMap::new();
headers.insert("authorization".into(), "Bearer my-token".into());
let worker = register_worker("ws://localhost:49135", InitOptions {
headers: Some(headers),
..Default::default()
});
let result = worker.trigger(TriggerRequest {
function_id: "api::users::list".into(),
payload: json!({ "limit": 10 }),
action: None,
timeout_ms: None,
}).await.expect("trigger failed");
5. Use Channels Through the RBAC Port
Channels work on the RBAC port exactly as they do on the main engine port. The SDK’s createChannel() works without changes:
const worker = registerWorker('ws://localhost:49135', {
headers: { authorization: 'Bearer my-token' },
})
const channel = await worker.createChannel()
const result = await worker.trigger({
function_id: 'api::files::process',
payload: { reader: channel.readerRef },
})
channel.writer.stream.write(Buffer.from('hello'))
channel.writer.stream.end()
The RBAC port mounts the channel WebSocket endpoint at /ws/channels/{channel_id} on the same port, so channel data flows through the secure port without exposing the main engine’s channel endpoint.
6. RBAC for Trigger Registration
Workers connecting through the RBAC port can register trigger types and triggers, subject to access control.
Auth Result Fields
The auth function controls trigger access via two fields in AuthResult:
allowed_trigger_types — List of trigger type IDs this worker can register triggers for. When omitted, all types are allowed.
allow_trigger_type_registration — Whether this worker can register new trigger types.
Registration Hook Functions
For fine-grained control, configure hook functions that are called before each registration:
modules:
- class: modules::worker::WorkerModule
config:
port: 49135
middleware_function_id: my-project::middleware-function
rbac:
auth_function_id: my-project::auth-function
on_trigger_registration_function_id: my-project::on-trigger-reg
on_trigger_type_registration_function_id: my-project::on-trigger-type-reg
on_function_registration_function_id: my-project::on-function-reg
expose_functions:
- match("api::*")
Each hook receives the registration details and the auth context, and must return true to allow the registration:
Node / TypeScript
Python
Rust
import type {
OnTriggerRegistrationInput,
OnTriggerTypeRegistrationInput,
OnFunctionRegistrationInput,
} from 'iii-sdk'
iii.registerFunction(
{ id: 'my-project::on-trigger-reg' },
async (input: OnTriggerRegistrationInput) => {
if (!input.function_id.startsWith(`${input.context.role}::`)) {
return false
}
return true
},
)
iii.registerFunction(
{ id: 'my-project::on-trigger-type-reg' },
async (input: OnTriggerTypeRegistrationInput) => {
return input.context.role === 'admin'
},
)
iii.registerFunction(
{ id: 'my-project::on-function-reg' },
async (input: OnFunctionRegistrationInput) => {
return !input.function_id.startsWith('internal::')
},
)
from iii import (
OnTriggerRegistrationInput,
OnTriggerTypeRegistrationInput,
OnFunctionRegistrationInput,
)
async def on_trigger_reg(data: dict) -> bool:
input = OnTriggerRegistrationInput(**data)
if not input.function_id.startswith(f"{input.context['role']}::"):
return False
return True
async def on_trigger_type_reg(data: dict) -> bool:
input = OnTriggerTypeRegistrationInput(**data)
return input.context['role'] == 'admin'
async def on_function_reg(data: dict) -> bool:
input = OnFunctionRegistrationInput(**data)
return not input.function_id.startswith('internal::')
iii.register_function({'id': 'my-project::on-trigger-reg'}, on_trigger_reg)
iii.register_function({'id': 'my-project::on-trigger-type-reg'}, on_trigger_type_reg)
iii.register_function({'id': 'my-project::on-function-reg'}, on_function_reg)
use iii_sdk::{
OnTriggerRegistrationInput, OnTriggerTypeRegistrationInput,
OnFunctionRegistrationInput, RegisterFunction,
};
iii.register_function(RegisterFunction::new_async(
"my-project::on-trigger-reg",
|input: OnTriggerRegistrationInput| async move {
let role = input.context.get("role").and_then(|v| v.as_str()).unwrap_or("");
let prefix = format!("{role}::");
Ok::<_, iii_sdk::IIIError>(input.function_id.starts_with(&prefix))
},
));
iii.register_function(RegisterFunction::new_async(
"my-project::on-trigger-type-reg",
|input: OnTriggerTypeRegistrationInput| async move {
let role = input.context.get("role").and_then(|v| v.as_str()).unwrap_or("");
Ok::<_, iii_sdk::IIIError>(role == "admin")
},
));
iii.register_function(RegisterFunction::new_async(
"my-project::on-function-reg",
|input: OnFunctionRegistrationInput| async move {
Ok::<_, iii_sdk::IIIError>(!input.function_id.starts_with("internal::"))
},
));
If your functions register metadata, you can use metadata filters instead of (or in addition to) wildcard patterns:
Node / TypeScript
Python
Rust
iii.registerFunction({
id: 'api::users::list',
metadata: { public: true, tier: 'free' },
}, async (input) => {
// ...
})
iii.register_function({
'id': 'api::users::list',
'metadata': {'public': True, 'tier': 'free'},
}, list_users)
let mut msg = RegisterFunctionMessage::with_id("api::users::list".to_string());
msg.metadata = Some(json!({ "public": true, "tier": "free" }));
iii.register_function((msg, |input: Value| async move {
// ...
Ok(json!({}))
}));
Then filter by metadata in your config:
rbac:
expose_functions:
- metadata:
public: true
- metadata:
tier: "free"
Types Reference
All three SDKs export the following types for RBAC functions.
Input passed to the auth function during the WebSocket upgrade.
| Field | Type | Description |
|---|
headers | Record<string, string> | HTTP headers from the upgrade request. |
query_params | Record<string, string[]> | Query parameters. Each key maps to an array of values to support repeated keys. |
ip_address | string | IP address of the connecting client. |
AuthResult
Return value from the auth function. Controls access and passes context to middleware.
| Field | Type | Default | Description |
|---|
allowed_functions | string[] | [] | Additional function IDs to allow beyond expose_functions. |
forbidden_functions | string[] | [] | Function IDs to deny even if they match expose_functions. Takes precedence. |
allowed_trigger_types | string[] or omitted | omitted (permissive) | Trigger type IDs the worker may register triggers for. When omitted, all types are allowed. |
allow_trigger_type_registration | boolean | false | Whether the worker may register new trigger types. |
allow_function_registration | boolean | true | Whether the worker may register new functions. |
context | Record<string, unknown> | {} | Arbitrary context forwarded to the middleware function on every invocation. |
Input passed to the middleware function on every invocation through the RBAC port.
| Field | Type | Description |
|---|
function_id | string | ID of the function being invoked. |
payload | Record<string, unknown> | Payload sent by the caller. |
action | TriggerAction or omitted | Routing action (enqueue, void), if any. |
context | Record<string, unknown> | Auth context from AuthResult.context for this session. |
Input passed to the on_trigger_type_registration_function_id hook. Return true to allow.
| Field | Type | Description |
|---|
trigger_type_id | string | ID of the trigger type being registered. |
description | string | Human-readable description of the trigger type. |
context | Record<string, unknown> | Auth context from AuthResult.context. |
Input passed to the on_trigger_registration_function_id hook. Return true to allow.
| Field | Type | Description |
|---|
trigger_id | string | ID of the trigger being registered. |
trigger_type | string | Trigger type identifier. |
function_id | string | ID of the function this trigger is bound to. |
config | unknown | Trigger-specific configuration. |
context | Record<string, unknown> | Auth context from AuthResult.context. |
Input passed to the on_function_registration_function_id hook. Return true to allow.
| Field | Type | Description |
|---|
function_id | string | ID of the function being registered. |
description | string or omitted | Human-readable description of the function. |
metadata | Record<string, unknown> or omitted | Arbitrary metadata attached to the function. |
context | Record<string, unknown> | Auth context from AuthResult.context. |
Full Example Config
modules:
- class: modules::worker::WorkerModule
config:
port: 49134
- class: modules::stream::StreamModule
config:
port: 3112
- class: modules::worker::WorkerModule
config:
host: 0.0.0.0
port: 49135
middleware_function_id: my-project::middleware-function
rbac:
auth_function_id: my-project::auth-function
on_trigger_registration_function_id: my-project::on-trigger-reg
on_trigger_type_registration_function_id: my-project::on-trigger-type-reg
on_function_registration_function_id: my-project::on-function-reg
expose_functions:
- match("api::*")
- match("*::public")
- metadata:
public: true
Only expose the RBAC port (49135) to external networks. The main engine port (49134) and stream port (3112) should remain internal. Use firewall rules or network policies to enforce this.