Skip to content

feat: rbac on function discovery#1389

Open
sergiofilhowz wants to merge 2 commits intomainfrom
feat/rbac-on-function-discovery
Open

feat: rbac on function discovery#1389
sergiofilhowz wants to merge 2 commits intomainfrom
feat/rbac-on-function-discovery

Conversation

@sergiofilhowz
Copy link
Copy Markdown
Contributor

@sergiofilhowz sergiofilhowz commented Apr 1, 2026

Summary by CodeRabbit

Release Notes

  • New Features

    • Added session-aware function handlers to support RBAC filtering
    • Functions listed via API now respect session RBAC permissions
  • Tests

    • Added RBAC worker integration tests to verify function visibility based on session permissions across Node, Python, and Rust SDKs
  • Chores

    • Updated package version to 0.10.0-beta.4

@vercel
Copy link
Copy Markdown
Contributor

vercel bot commented Apr 1, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
iii-website Ready Ready Preview, Comment Apr 1, 2026 9:07pm
motia-docs Ready Ready Preview, Comment Apr 1, 2026 9:07pm

Request Review

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Apr 1, 2026

📝 Walkthrough

Walkthrough

This PR extends the engine's function handler system to support RBAC session awareness. It introduces a new SessionHandler type and register_function_handler_with_session API to thread session context through the invocation pipeline. The service macro is updated to detect session parameters and generate appropriate handler registration code. Function handler signatures now accept an optional session parameter for RBAC-aware decisions. RBAC filtering is applied in get_functions() to restrict visibility based on session permissions. Tests verify RBAC filtering behavior in worker contexts.

Changes

Cohort / File(s) Summary
Macro-based Service Handler Generation
engine/function-macros/src/lib.rs
Added type_contains_ident helper to detect Session parameters. Refactored parameter collection to extract non-self params, derive input type, and compute session detection. Conditional generation of method calls and handler/registration based on session presence.
Core Engine Infrastructure
engine/src/engine/mod.rs, engine/src/function.rs, engine/src/invocation/mod.rs
Exposed Handler.f field, introduced SessionHandler<H> type, added register_function_handler_with_session trait method. Extended handler invocation chain to accept and forward optional Session parameter. Updated middleware control flow to skip engine-level functions.
Engine Function Module RBAC
engine/src/modules/engine_fn/mod.rs
Updated get_functions to accept optional session and apply RBAC filtering based on allowed/forbidden function sets from session config.
Mock Engine & Session Types
engine/src/condition.rs, engine/src/modules/worker/rbac_session.rs
Added register_function_handler_with_session implementation to MockEngine. Added Debug trait derivation to AuthResult.
Test Handler Signature Updates
engine/src/modules/bridge_client/mod.rs, engine/src/modules/queue/adapters/builtin/adapter.rs, engine/src/modules/queue/queue.rs, engine/src/modules/telemetry/mod.rs, engine/tests/common/queue_helpers.rs, engine/tests/dlq_redrive_e2e.rs, engine/tests/queue_e2e_fanout.rs, engine/tests/rabbitmq_queue_integration.rs
Updated test handler closures to accept third _session parameter (unused, passed as None). Maintains existing test behavior while conforming to new handler signature.
RBAC Worker Integration Tests
sdk/packages/node/iii/tests/rbac-workers.test.ts, sdk/packages/python/iii/tests/test_rbac_workers.py, sdk/packages/rust/iii/tests/rbac_workers.rs
Added integration tests for listFunctions() RBAC filtering with valid-token and restricted-token headers, verifying function visibility based on session permissions.
Configuration & Version Updates
sdk/fixtures/config-test.yaml, sdk/packages/node/iii-browser/package.json
Added match("engine::functions::list") to RBAC expose_functions config. Bumped browser package version from 0.10.0 to 0.10.0-beta.4.

Sequence Diagram

sequenceDiagram
    participant Client
    participant Engine
    participant ServiceMacro
    participant HandlerRegistry
    participant InvocationHandler
    participant FunctionHandler
    participant Session as RBAC Session

    Client->>Engine: register_function_handler_with_session(SessionHandler)
    Engine->>HandlerRegistry: store SessionHandler with signature

    Client->>Engine: invoke_function(input, session_context)
    Engine->>InvocationHandler: handle_invocation(input, session)
    
    alt Session Provided
        InvocationHandler->>Session: clone session
        InvocationHandler->>FunctionHandler: call_handler(invocation_id, input, Some(session))
        FunctionHandler->>FunctionHandler: handler(input, Some(session))
    else No Session
        InvocationHandler->>FunctionHandler: call_handler(invocation_id, input, None)
        FunctionHandler->>FunctionHandler: handler(input, None)
    end
    
    FunctionHandler-->>Engine: FunctionResult
    
    alt get_functions with Session
        Client->>Engine: get_functions(session)
        Engine->>HandlerRegistry: filter by RBAC rules
        HandlerRegistry-->>Engine: allowed_functions
        Engine-->>Client: filtered_function_list
    else get_functions without Session
        Client->>Engine: get_functions(None)
        Engine-->>Client: all_functions
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Suggested reviewers

  • guibeira

Poem

🐰 A session now flows through each handler's call,
RBAC checks determine who can access all,
Macros detect parameters with session flair,
Filters enforce permissions everywhere!
🔐✨

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title 'feat: rbac on function discovery' clearly and concisely describes the main feature addition: implementing RBAC (role-based access control) on function discovery. The title directly corresponds to the core changes throughout the codebase where session-aware RBAC filtering is added to function listing and handler invocation.
Docstring Coverage ✅ Passed Docstring coverage is 88.71% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/rbac-on-function-discovery

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@mintlify
Copy link
Copy Markdown

mintlify bot commented Apr 1, 2026

Preview deployment for your docs. Learn more about Mintlify Previews.

Project Status Preview Updated (UTC)
iii 🟢 Ready View Preview Apr 1, 2026, 8:55 PM

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
engine/src/engine/mod.rs (1)

672-754: ⚠️ Potential issue | 🔴 Critical

Don't bypass the normal dispatch path after middleware.

This branch returns before the match action logic and invokes remember_invocation() directly. With middleware enabled, TriggerAction::Enqueue becomes an immediate invoke, TriggerAction::Void still sends an InvocationResult, and the _caller_worker_id injection from spawn_invoke_function() is lost. Please feed the enriched payload back through the same post-action dispatch path so middleware doesn't change invocation semantics.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@engine/src/engine/mod.rs` around lines 672 - 754, The middleware branch is
bypassing the normal dispatch path by calling remember_invocation() directly
(losing semantics for TriggerAction::Enqueue, TriggerAction::Void and the
_caller_worker_id set by spawn_invoke_function()); instead, after obtaining
enriched_payload from engine.call(&middleware_id, ...), re-enter the same
post-action dispatch logic used elsewhere (the match action branch) so the
enriched payload flows through the exact same code path that handles
Enqueue/Void/normal invokes and preserves _caller_worker_id; in practice replace
the direct call to remember_invocation/send_msg in the tokio::spawn with code
that reuses the existing dispatch mechanism (or call the same helper used by
spawn_invoke_function) passing enriched_payload, inv_id, function_id,
traceparent and baggage so behavior remains identical to non-middleware
invocations.
🧹 Nitpick comments (1)
engine/src/function.rs (1)

39-45: Avoid cloning every payload in call_handler.

data is already owned here and isn't reused after dispatch, so data.clone() adds a full extra JSON copy to every invocation on the hot path.

♻️ Proposed fix
     pub async fn call_handler(
         self,
         invocation_id: Option<Uuid>,
         data: Value,
         session: Option<Arc<Session>>,
     ) -> FunctionResult<Option<Value>, ErrorBody> {
-        (self.handler)(invocation_id, data.clone(), session).await
+        (self.handler)(invocation_id, data, session).await
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@engine/src/function.rs` around lines 39 - 45, call_handler is cloning the
owned JSON payload unnecessarily; instead of calling
(self.handler)(invocation_id, data.clone(), session).await, pass the owned data
directly: (self.handler)(invocation_id, data, session).await. Update this call
in the call_handler function (and if the handler signature currently expects a
reference, adjust the handler/type to accept Value by value or otherwise move
the value) so no extra JSON copy is performed on the hot path.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@engine/function-macros/src/lib.rs`:
- Around line 163-183: The helper type_contains_ident is too permissive: before
generating the SessionHandler closure you must validate the function's second
parameter shape exactly (not just that it contains the identifier "Session");
update has_session_param or add an explicit check right before constructing
SessionHandler to accept only Option<::std::sync::Arc<Session>> (or the exact
allowed variants) and otherwise emit a compile_error! with a clear message;
reference the symbols type_contains_ident, has_session_param, and SessionHandler
so the check is placed just before the SessionHandler generation block and
rejects/diagnoses unsupported shapes rather than letting the generated closure
produce opaque type errors.

---

Outside diff comments:
In `@engine/src/engine/mod.rs`:
- Around line 672-754: The middleware branch is bypassing the normal dispatch
path by calling remember_invocation() directly (losing semantics for
TriggerAction::Enqueue, TriggerAction::Void and the _caller_worker_id set by
spawn_invoke_function()); instead, after obtaining enriched_payload from
engine.call(&middleware_id, ...), re-enter the same post-action dispatch logic
used elsewhere (the match action branch) so the enriched payload flows through
the exact same code path that handles Enqueue/Void/normal invokes and preserves
_caller_worker_id; in practice replace the direct call to
remember_invocation/send_msg in the tokio::spawn with code that reuses the
existing dispatch mechanism (or call the same helper used by
spawn_invoke_function) passing enriched_payload, inv_id, function_id,
traceparent and baggage so behavior remains identical to non-middleware
invocations.

---

Nitpick comments:
In `@engine/src/function.rs`:
- Around line 39-45: call_handler is cloning the owned JSON payload
unnecessarily; instead of calling (self.handler)(invocation_id, data.clone(),
session).await, pass the owned data directly: (self.handler)(invocation_id,
data, session).await. Update this call in the call_handler function (and if the
handler signature currently expects a reference, adjust the handler/type to
accept Value by value or otherwise move the value) so no extra JSON copy is
performed on the hot path.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: a7bada93-39ae-4846-8dc0-c7964144760d

📥 Commits

Reviewing files that changed from the base of the PR and between 632c2b8 and d1fcbce.

📒 Files selected for processing (21)
  • docs/how-to/worker-rbac.mdx
  • engine/function-macros/src/lib.rs
  • engine/src/condition.rs
  • engine/src/engine/mod.rs
  • engine/src/function.rs
  • engine/src/invocation/mod.rs
  • engine/src/modules/bridge_client/mod.rs
  • engine/src/modules/engine_fn/mod.rs
  • engine/src/modules/queue/adapters/builtin/adapter.rs
  • engine/src/modules/queue/queue.rs
  • engine/src/modules/telemetry/mod.rs
  • engine/src/modules/worker/rbac_session.rs
  • engine/tests/common/queue_helpers.rs
  • engine/tests/dlq_redrive_e2e.rs
  • engine/tests/queue_e2e_fanout.rs
  • engine/tests/rabbitmq_queue_integration.rs
  • sdk/fixtures/config-test.yaml
  • sdk/packages/node/iii-browser/package.json
  • sdk/packages/node/iii/tests/rbac-workers.test.ts
  • sdk/packages/python/iii/tests/test_rbac_workers.py
  • sdk/packages/rust/iii/tests/rbac_workers.rs

Comment on lines +163 to +183
fn type_contains_ident(ty: &syn::Type, name: &str) -> bool {
match ty {
syn::Type::Path(type_path) => type_path.path.segments.iter().any(|seg| {
if seg.ident == name {
return true;
}
if let syn::PathArguments::AngleBracketed(args) = &seg.arguments {
args.args.iter().any(|arg| {
if let syn::GenericArgument::Type(inner_ty) = arg {
type_contains_ident(inner_ty, name)
} else {
false
}
})
} else {
false
}
}),
_ => false,
}
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

python <<'PY'
import pathlib, re

pattern = re.compile(
    r'#\s*\[\s*function(?:\([^\]]*\))?\s*\]\s*'
    r'(?:pub\s+)?async\s+fn\s+([A-Za-z_]\w*)\s*\((.*?)\)',
    re.S,
)

for path in pathlib.Path('.').rglob('*.rs'):
    text = path.read_text(encoding='utf-8', errors='ignore')
    for match in pattern.finditer(text):
        signature = " ".join(match.group(2).split())
        if "Session" not in signature:
            continue
        print(f"{path}:{match.group(1)}")
        print(f"  signature: ({signature})")
PY

Repository: iii-hq/iii

Length of output: 183


🏁 Script executed:

cd engine/function-macros && wc -l src/lib.rs && echo "---" && cat -n src/lib.rs | sed -n '150,250p'

Repository: iii-hq/iii

Length of output: 4307


🏁 Script executed:

cd engine/function-macros && cat -n src/lib.rs | sed -n '237,330p'

Repository: iii-hq/iii

Length of output: 5494


🏁 Script executed:

cd engine/function-macros && cat -n src/lib.rs | sed -n '323,370p'

Repository: iii-hq/iii

Length of output: 3054


🏁 Script executed:

cd engine/function-macros && grep -n "has_session_param\|type_contains_ident" src/lib.rs

Repository: iii-hq/iii

Length of output: 495


🏁 Script executed:

cd engine && find . -name "*.rs" -type f -exec grep -l "SessionHandler\|register_function_handler_with_session" {} \; | head -10

Repository: iii-hq/iii

Length of output: 152


🏁 Script executed:

cd engine && cat -n src/engine/mod.rs | grep -A 10 "fn register_function_handler_with_session" | head -20

Repository: iii-hq/iii

Length of output: 871


🏁 Script executed:

cd engine && python3 <<'VERIFY'
import re

# Read the lib.rs file
with open('function-macros/src/lib.rs', 'r') as f:
    content = f.read()

# Extract the handler_and_registration block for session case
session_block = re.search(
    r'let handler_and_registration = if has_session_param \{(.*?)\} else \{',
    content,
    re.DOTALL
)

if session_block:
    block_text = session_block.group(1)
    # Look for the exact type of the session parameter
    session_param = re.search(
        r'SessionHandler::new\(move \|input: Value, session: ([^|]+)\|',
        block_text
    )
    if session_param:
        print("Session parameter type in generated code:")
        print(f"  {session_param.group(1)}")

# Now check how has_session_param is determined
has_session_check = re.search(
    r'let has_session_param = non_self_params\.get\(1\)\.map_or\(false, \|arg\| \{.*?type_contains_ident\(&pat_type\.ty, "([^"]+)"\)',
    content,
    re.DOTALL
)

if has_session_check:
    print("\nDetection mechanism: substring search for identifier")
    print(f"  Identifier: {has_session_check.group(1)}")
    print("  Method: type_contains_ident recursively checks if identifier exists anywhere in type tree")

# Analyze the discrepancy
print("\n⚠️ ISSUE CONFIRMED:")
print("- Macro detects if type tree CONTAINS 'Session' anywhere")
print("- Macro generates closure with hardcoded: Option<::std::sync::Arc<Session>>")
print("- These do NOT match for types like: Session, Arc<Session>, Option<Session>, etc.")
print("- Result: Opaque type mismatch errors during compilation")

VERIFY

Repository: iii-hq/iii

Length of output: 592


Validate second parameter type exactly before generating SessionHandler.

The type_contains_ident function (lines 163–183) recursively searches for "Session" anywhere in the type tree, causing has_session_param (lines 237–243) to trigger for any second parameter containing that identifier. However, the generated SessionHandler closure (line 326) hardcodes the parameter as Option<::std::sync::Arc<Session>>. This mismatch means signatures like Session, Arc<Session>, or Option<Session> will be accepted by the detection but rejected by the generated code with opaque type errors instead of clear validation.

Add an exact-type check before line 323, or emit a compile_error! for unsupported shapes.

Also applies to: lines 237–243, 323–361

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@engine/function-macros/src/lib.rs` around lines 163 - 183, The helper
type_contains_ident is too permissive: before generating the SessionHandler
closure you must validate the function's second parameter shape exactly (not
just that it contains the identifier "Session"); update has_session_param or add
an explicit check right before constructing SessionHandler to accept only
Option<::std::sync::Arc<Session>> (or the exact allowed variants) and otherwise
emit a compile_error! with a clear message; reference the symbols
type_contains_ident, has_session_param, and SessionHandler so the check is
placed just before the SessionHandler generation block and rejects/diagnoses
unsupported shapes rather than letting the generated closure produce opaque type
errors.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
engine/src/engine/mod.rs (1)

1303-1314: ⚠️ Potential issue | 🟠 Major

Engine::call() still strips the active session.

Line 1313 hardcodes None, so every self.call(...) inside router_msg()—notably middleware and the RBAC registration hooks—invokes session-aware handlers without the worker's session. That means register_function_handler_with_session cannot actually see fields like allowed_functions or function_registration_prefix on those paths. Please add a session-aware internal call path and use it from the worker-session branches.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@engine/src/engine/mod.rs` around lines 1303 - 1314, Engine::call currently
passes None for the session into self.invocations.handle_invocation (see
invocations.handle_invocation(..., None, ...)), which strips the active worker
session for all router_msg paths; add a session-aware internal call path that
forwards the active session (the worker/session variable used in the
worker-session branches) into handle_invocation and update
Engine::call/router_msg to use that path for middleware, RBAC hooks and
register_function_handler_with_session so those handlers can read
allowed_functions and function_registration_prefix from the session.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Outside diff comments:
In `@engine/src/engine/mod.rs`:
- Around line 1303-1314: Engine::call currently passes None for the session into
self.invocations.handle_invocation (see invocations.handle_invocation(..., None,
...)), which strips the active worker session for all router_msg paths; add a
session-aware internal call path that forwards the active session (the
worker/session variable used in the worker-session branches) into
handle_invocation and update Engine::call/router_msg to use that path for
middleware, RBAC hooks and register_function_handler_with_session so those
handlers can read allowed_functions and function_registration_prefix from the
session.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 480dd6c2-2634-4901-941d-521db4cf3e1a

📥 Commits

Reviewing files that changed from the base of the PR and between d1fcbce and 240bd80.

📒 Files selected for processing (4)
  • engine/src/engine/mod.rs
  • sdk/packages/node/iii/tests/rbac-workers.test.ts
  • sdk/packages/python/iii/tests/test_rbac_workers.py
  • sdk/packages/rust/iii/tests/rbac_workers.rs
✅ Files skipped from review due to trivial changes (1)
  • sdk/packages/python/iii/tests/test_rbac_workers.py
🚧 Files skipped from review as they are similar to previous changes (1)
  • sdk/packages/node/iii/tests/rbac-workers.test.ts

engine.send_msg(&w, response).await;
});
return Ok(());
if !function_id.starts_with("engine::") {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we include a logger message saying this prefix is not allowed?

request: RegisterFunctionRequest,
handler: SessionHandler<H>,
) where
H: Fn(Value, Option<Arc<Session>>) -> F + Send + Sync + 'static,
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I noticed we use this types in many places. Can we create a type name like this:

type JsonHandler<F> = dyn Fn(Value, Option<Arc<Session>>) -> F + Send + Sync + 'static;

same as the other one

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants