[Phase 2] Pass caller identity (RequestContext) through RPC dispatch pipeline #11

Closed
opened 2026-03-26 11:25:34 +00:00 by timur · 3 comments
Owner

Summary

The current rpc_handler() in crates/osis/src/rpc/server.rs does not extract or propagate caller identity from HTTP headers/request extensions. For auth enforcement at the OSIS level (ACL on contexts), handlers need to know WHO is making the request.

Current State

rpc_handler() signature (server.rs:1667)

async fn rpc_handler(
    Path((context, domain)): Path<(String, String)>,
    State(state): State<Arc<OServerState>>,
    Json(request): Json<JsonRpcRequest>,
) -> impl IntoResponse

No HTTP headers are extracted. No auth middleware exists in the Axum router built by OServer.

JsonRpcRequest (jsonrpc.rs)

pub struct JsonRpcRequest {
    pub jsonrpc: String,
    pub method: String,
    pub params: Option<Value>,
    pub id: Option<Value>,
}

No auth/identity fields.

ACL module exists but is not wired

crates/osis/src/acl.rs (~820 lines) has:

  • AclEntry with circle/group resolution
  • Rights enum (Admin, Write, Read)
  • check_access() logic
  • But it is never called from the dispatch pipeline.

Proposed Changes

1. Add RequestContext struct

pub struct RequestContext {
    pub caller_pubkey: Option<String>,
    pub caller_email: Option<String>,
    pub auth_method: AuthMethod,  // None, Bearer, OAuth, Signature
}

2. Extract identity from request headers/extensions

In the rpc_handler(), extract identity from:

  • Proxy-injected headers (when behind hero_proxy): X-Proxy-User-Email, X-Proxy-Auth-Method
  • Direct auth headers (when accessed directly): X-Public-Key, X-Signature, Authorization: Bearer
  • Request extensions (when middleware has already validated): request.extensions().get::<AuthContext>()

3. Pass RequestContext to CRUD handlers

The auto-generated CRUD method dispatch needs to receive RequestContext so it can:

  • Check caller against context ACL (from hero_osis Context type)
  • Pass identity to lifecycle hooks (before_create, before_update, etc.)
  • Include caller in audit logging

4. Optional: Auth middleware layer

Add an optional auth middleware to the Axum router that validates signatures/tokens before the request reaches rpc_handler(). This would complement (not replace) application-level auth like znzfreezone_backend's rpc_auth.rs.

Design Considerations

  • Backwards compatible: RequestContext fields should be Option so existing deployments without auth continue to work
  • Not prescriptive about auth method: hero_rpc should extract and propagate identity, not mandate how auth is done. Different deployments use different auth (OAuth via proxy, secp256k1 signatures, API keys, etc.)
  • Separation of concerns: hero_rpc extracts identity; hero_osis checks ACL; application code checks business rules
  • lhumina_code/hero_osis: ACL fields on Context type (separate issue)
  • lhumina_code/hero_proxy: Identity header injection (separate issue)
  • znzfreezone_code/znzfreezone_backend issue #29: Cross-context auth architecture
  • znzfreezone_code/znzfreezone_backend rpc_auth.rs: Example of application-level RPC auth middleware
## Summary The current `rpc_handler()` in `crates/osis/src/rpc/server.rs` does not extract or propagate caller identity from HTTP headers/request extensions. For auth enforcement at the OSIS level (ACL on contexts), handlers need to know WHO is making the request. ## Current State ### `rpc_handler()` signature (server.rs:1667) ```rust async fn rpc_handler( Path((context, domain)): Path<(String, String)>, State(state): State<Arc<OServerState>>, Json(request): Json<JsonRpcRequest>, ) -> impl IntoResponse ``` No HTTP headers are extracted. No auth middleware exists in the Axum router built by `OServer`. ### `JsonRpcRequest` (jsonrpc.rs) ```rust pub struct JsonRpcRequest { pub jsonrpc: String, pub method: String, pub params: Option<Value>, pub id: Option<Value>, } ``` No auth/identity fields. ### ACL module exists but is not wired `crates/osis/src/acl.rs` (~820 lines) has: - `AclEntry` with circle/group resolution - `Rights` enum (Admin, Write, Read) - `check_access()` logic - But it is never called from the dispatch pipeline. ## Proposed Changes ### 1. Add `RequestContext` struct ```rust pub struct RequestContext { pub caller_pubkey: Option<String>, pub caller_email: Option<String>, pub auth_method: AuthMethod, // None, Bearer, OAuth, Signature } ``` ### 2. Extract identity from request headers/extensions In the `rpc_handler()`, extract identity from: - **Proxy-injected headers** (when behind hero_proxy): `X-Proxy-User-Email`, `X-Proxy-Auth-Method` - **Direct auth headers** (when accessed directly): `X-Public-Key`, `X-Signature`, `Authorization: Bearer` - **Request extensions** (when middleware has already validated): `request.extensions().get::<AuthContext>()` ### 3. Pass `RequestContext` to CRUD handlers The auto-generated CRUD method dispatch needs to receive `RequestContext` so it can: - Check caller against context ACL (from hero_osis Context type) - Pass identity to lifecycle hooks (`before_create`, `before_update`, etc.) - Include caller in audit logging ### 4. Optional: Auth middleware layer Add an optional auth middleware to the Axum router that validates signatures/tokens before the request reaches `rpc_handler()`. This would complement (not replace) application-level auth like znzfreezone_backend's `rpc_auth.rs`. ## Design Considerations - **Backwards compatible**: `RequestContext` fields should be `Option` so existing deployments without auth continue to work - **Not prescriptive about auth method**: hero_rpc should extract and propagate identity, not mandate how auth is done. Different deployments use different auth (OAuth via proxy, secp256k1 signatures, API keys, etc.) - **Separation of concerns**: hero_rpc extracts identity; hero_osis checks ACL; application code checks business rules ## Related - `lhumina_code/hero_osis`: ACL fields on Context type (separate issue) - `lhumina_code/hero_proxy`: Identity header injection (separate issue) - `znzfreezone_code/znzfreezone_backend` issue #29: Cross-context auth architecture - `znzfreezone_code/znzfreezone_backend` `rpc_auth.rs`: Example of application-level RPC auth middleware
Author
Owner

Update: Identity Comes from Proxy Headers

Following discussion on hero_proxy#8, authentication is moving to hero_proxy. The proxy verifies secp256k1 signatures at the edge and injects:

X-Proxy-User-Pubkey: 02abc...def
X-Proxy-Auth-Method: signature | oauth | bearer
X-Proxy-Signature-Verified: true

For the RequestContext proposed in this issue, the primary extraction path becomes:

pub struct RequestContext {
    pub caller_pubkey: Option<String>,   // from X-Proxy-User-Pubkey
    pub caller_email: Option<String>,    // from X-Proxy-User-Email (OAuth)
    pub auth_method: AuthMethod,         // from X-Proxy-Auth-Method
    pub signature_verified: bool,        // from X-Proxy-Signature-Verified
}

The rpc_handler() should extract these from HTTP headers (when behind hero_proxy) and pass them through to CRUD handlers and lifecycle hooks.

API keys are being dropped in favor of keypair-only auth, so RequestContext doesn't need an api_key_id field.

The trust model: hero_rpc trusts X-Proxy-* headers because the connection from hero_proxy is via Unix socket. The proxy strips any externally-supplied X-Proxy-* headers before injecting its own verified ones.

## Update: Identity Comes from Proxy Headers Following discussion on [hero_proxy#8](https://forge.ourworld.tf/lhumina_code/hero_proxy/issues/8), authentication is moving to hero_proxy. The proxy verifies secp256k1 signatures at the edge and injects: ``` X-Proxy-User-Pubkey: 02abc...def X-Proxy-Auth-Method: signature | oauth | bearer X-Proxy-Signature-Verified: true ``` For the `RequestContext` proposed in this issue, the primary extraction path becomes: ```rust pub struct RequestContext { pub caller_pubkey: Option<String>, // from X-Proxy-User-Pubkey pub caller_email: Option<String>, // from X-Proxy-User-Email (OAuth) pub auth_method: AuthMethod, // from X-Proxy-Auth-Method pub signature_verified: bool, // from X-Proxy-Signature-Verified } ``` The `rpc_handler()` should extract these from HTTP headers (when behind hero_proxy) and pass them through to CRUD handlers and lifecycle hooks. API keys are being dropped in favor of keypair-only auth, so `RequestContext` doesn't need an `api_key_id` field. The trust model: hero_rpc trusts `X-Proxy-*` headers because the connection from hero_proxy is via Unix socket. The proxy strips any externally-supplied `X-Proxy-*` headers before injecting its own verified ones.
timur changed title from Pass caller identity (RequestContext) through RPC dispatch pipeline to [Phase 2] Pass caller identity (RequestContext) through RPC dispatch pipeline 2026-03-26 13:04:39 +00:00
Author
Owner

Implementation: RequestContext through RPC dispatch pipeline

Implemented on the development branch. Here's a summary of the changes:

New types (crates/osis/src/rpc/request_context.rs)

  • RequestContext — carries caller identity through the dispatch pipeline:

    • caller_pubkey: Option<String> — from X-Public-Key or proxy headers
    • caller_email: Option<String> — from X-Proxy-User-Email
    • bearer_token: Option<String> — from Authorization: Bearer <token> or JSON body token field
    • auth_method: AuthMethod — enum: None, Bearer, Signature, Proxy
  • RequestContext::from_headers() — extracts identity from HTTP headers

  • RequestContext::merge_from_json_params() — extracts token field from JSON-RPC body (WASM client path)

  • All fields are Option → fully backwards compatible

Changes to dispatch pipeline

File Change
protocol.rs Added request_context: RequestContext to RpcRequest
server.rs (HTTP handler) Extracts HeaderMap, builds RequestContext, passes through dispatch
dispatch.rs All dispatch functions now accept and thread RequestContext
unix_server.rs Uses RequestContext::default() (anonymous) for raw UDS
domain_server.rs Updated DispatchFn type, rpc_handler, domain_dispatch
handler.rs No change needed — handle_request(&RpcRequest) gets context via RpcRequest

Trait extensions (backwards-compatible defaults)

  • OsisAppRpcHandler: Added handle_rpc_call_with_context() and handle_service_call_with_context() — default implementations delegate to the existing methods, ignoring context
  • CustomMethodHandler::handle(): Added request_context: &RequestContext parameter
  • ServerState::try_custom_method(): Now passes RequestContext through

Code generator (rust_osis.rs)

Generated RpcRequest struct now includes request_context: hero_rpc_osis::rpc::RequestContext.
All existing generated files updated.

Headers extracted

Header Maps to
X-Public-Key / X-Proxy-User-Pubkey caller_pubkey
X-Proxy-User-Email caller_email
Authorization: Bearer <token> bearer_token + AuthMethod::Bearer
X-Proxy-Auth-Method AuthMethod::Proxy
X-Signature AuthMethod::Signature
JSON body token field bearer_token (WASM client fallback)

What's NOT in scope (as per the issue)

  • ACL enforcement (separate issue — the ACL module exists but isn't wired)
  • Auth validation middleware (optional follow-up)
  • Generated code that consumes RequestContext for auth (consumers opt in)

Testing

  • cargo build --workspace
  • cargo test --workspace --lib (all 77 tests pass; 1 pre-existing failure in scaffold test)
  • All RequestContext unit tests pass (4 tests)
  • Protocol tests pass
  • ACL tests pass
## Implementation: RequestContext through RPC dispatch pipeline Implemented on the `development` branch. Here's a summary of the changes: ### New types (`crates/osis/src/rpc/request_context.rs`) - **`RequestContext`** — carries caller identity through the dispatch pipeline: - `caller_pubkey: Option<String>` — from `X-Public-Key` or proxy headers - `caller_email: Option<String>` — from `X-Proxy-User-Email` - `bearer_token: Option<String>` — from `Authorization: Bearer <token>` or JSON body `token` field - `auth_method: AuthMethod` — enum: `None`, `Bearer`, `Signature`, `Proxy` - **`RequestContext::from_headers()`** — extracts identity from HTTP headers - **`RequestContext::merge_from_json_params()`** — extracts `token` field from JSON-RPC body (WASM client path) - All fields are `Option` → fully backwards compatible ### Changes to dispatch pipeline | File | Change | |------|--------| | `protocol.rs` | Added `request_context: RequestContext` to `RpcRequest` | | `server.rs` (HTTP handler) | Extracts `HeaderMap`, builds `RequestContext`, passes through dispatch | | `dispatch.rs` | All dispatch functions now accept and thread `RequestContext` | | `unix_server.rs` | Uses `RequestContext::default()` (anonymous) for raw UDS | | `domain_server.rs` | Updated `DispatchFn` type, `rpc_handler`, `domain_dispatch` | | `handler.rs` | No change needed — `handle_request(&RpcRequest)` gets context via `RpcRequest` | ### Trait extensions (backwards-compatible defaults) - **`OsisAppRpcHandler`**: Added `handle_rpc_call_with_context()` and `handle_service_call_with_context()` — default implementations delegate to the existing methods, ignoring context - **`CustomMethodHandler::handle()`**: Added `request_context: &RequestContext` parameter - **`ServerState::try_custom_method()`**: Now passes `RequestContext` through ### Code generator (`rust_osis.rs`) Generated `RpcRequest` struct now includes `request_context: hero_rpc_osis::rpc::RequestContext`. All existing generated files updated. ### Headers extracted | Header | Maps to | |--------|--------| | `X-Public-Key` / `X-Proxy-User-Pubkey` | `caller_pubkey` | | `X-Proxy-User-Email` | `caller_email` | | `Authorization: Bearer <token>` | `bearer_token` + `AuthMethod::Bearer` | | `X-Proxy-Auth-Method` | `AuthMethod::Proxy` | | `X-Signature` | `AuthMethod::Signature` | | JSON body `token` field | `bearer_token` (WASM client fallback) | ### What's NOT in scope (as per the issue) - ACL enforcement (separate issue — the ACL module exists but isn't wired) - Auth validation middleware (optional follow-up) - Generated code that consumes `RequestContext` for auth (consumers opt in) ### Testing - `cargo build --workspace` ✅ - `cargo test --workspace --lib` ✅ (all 77 tests pass; 1 pre-existing failure in scaffold test) - All `RequestContext` unit tests pass (4 tests) - Protocol tests pass - ACL tests pass
timur closed this issue 2026-03-26 14:42:55 +00:00
Author
Owner

Pushed as commit d4d4b22 on the development branch.

Pushed as commit [`d4d4b22`](https://forge.ourworld.tf/lhumina_code/hero_rpc/commit/d4d4b22) on the `development` branch.
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
lhumina_code/hero_rpc#11
No description provided.