Skip to main content

camel_component_grpc/
consumer.rs

1use std::collections::HashMap as StdHashMap;
2use std::path::PathBuf;
3use std::sync::Arc;
4use std::sync::OnceLock;
5use std::sync::atomic::{AtomicU64, Ordering};
6
7use async_trait::async_trait;
8use base64::Engine;
9use bytes::BytesMut;
10use camel_api::security_policy::AuthPrincipal;
11use camel_api::store_principal_properties;
12use camel_api::{Body, CamelError, Exchange, Message, Value};
13use camel_auth::{AuthenticatedPrincipal, CredentialSource, enforce_dispatch, install_carrier};
14use camel_component_api::{
15    ConcurrencyModel, Consumer, ConsumerContext, ConsumerStartupMode, ExchangeEnvelope,
16    SecurityContext,
17};
18use camel_proto_compiler::ProtoCache;
19use prost::Message as _;
20use prost_reflect::{DynamicMessage, MessageDescriptor};
21use tokio::sync::mpsc;
22use tonic::Status;
23use tracing::{debug, info};
24
25use crate::config::GrpcServerConfig;
26use crate::mode::GrpcMode;
27use crate::server::GrpcDispatchTable;
28use crate::server::GrpcKernelAuth;
29use crate::server::GrpcServerRegistry;
30
31static PROTO_CACHE: OnceLock<ProtoCache> = OnceLock::new();
32
33/// Per-request kernel authentication bundle: the sealed principal minted at
34/// the transport boundary plus the plan it must stay bound to
35/// (`unify-transport-auth`, Task 2.1).
36pub(crate) struct KernelRequestAuth {
37    plan: camel_api::security_policy::RouteSecurityPlan,
38    principal: AuthenticatedPrincipal,
39}
40
41impl KernelRequestAuth {
42    /// Install the typed carrier on a fresh request exchange, then enforce
43    /// the route binding. The carrier is installed BEFORE the pipeline runs
44    /// — a fresh exchange is created per request, so every dispatched
45    /// exchange must carry its own principal (the Task 2.9 dispatch check
46    /// relies on this). The principal is also mirrored to exchange
47    /// properties so route processors can observe the subject.
48    /// `enforce_dispatch` fails closed (`Public`
49    /// short-circuits to Ok) and denials map to the transport idiom.
50    fn apply_to(&self, exchange: &mut Exchange) -> Result<(), Status> {
51        store_principal_properties(exchange, self.principal.principal());
52        install_carrier(exchange, &self.principal);
53        enforce_dispatch(&self.plan, exchange).map_err(|e| match e {
54            CamelError::Unauthenticated(msg) => Status::unauthenticated(msg),
55            other => Status::internal(other.to_string()),
56        })
57    }
58}
59
60/// Bind a minted kernel principal to its route plan for one request.
61///
62/// `None` when the route has no kernel state (plan-less: Public
63/// pass-through) or when the request carries no minted principal
64/// (Public plans pass through without extraction).
65fn kernel_request_auth(
66    kernel: Option<&GrpcKernelAuth>,
67    principal: Option<AuthenticatedPrincipal>,
68) -> Option<KernelRequestAuth> {
69    let kernel = kernel?;
70    let principal = principal?;
71    Some(KernelRequestAuth {
72        plan: kernel.plan.clone(),
73        principal,
74    })
75}
76
77fn proto_cache() -> &'static ProtoCache {
78    PROTO_CACHE.get_or_init(ProtoCache::new)
79}
80
81/// Map a pipeline error onto the transport denial idiom.
82///
83/// A pipeline policy denial (`CamelError::Unauthorized`, what
84/// `SecurityPolicyService` returns) is `PERMISSION_DENIED` — the status
85/// the deleted transport-side scratch evaluation used to emit, so denial
86/// semantics survive with enforcement living wholly in the pipeline
87/// layer. Every other pipeline error is system-broken, not a denial:
88/// INTERNAL, unchanged.
89fn pipeline_error_to_status(e: CamelError) -> Status {
90    match e {
91        CamelError::Unauthorized(msg) => Status::permission_denied(msg),
92        other => Status::internal(format!("pipeline error: {other}")),
93    }
94}
95
96/// Resolve the gRPC mode (unary/streaming) for a given method without creating a consumer.
97pub fn resolve_grpc_mode(
98    proto_path: &PathBuf,
99    service_name: &str,
100    method_name: &str,
101) -> Result<GrpcMode, CamelError> {
102    let cache = proto_cache();
103    let pool = cache
104        .get_or_compile(proto_path, std::iter::empty::<&std::path::Path>())
105        .map_err(|e| CamelError::EndpointCreationFailed(format!("failed to compile proto: {e}")))?;
106
107    let svc = pool.get_service_by_name(service_name).ok_or_else(|| {
108        CamelError::EndpointCreationFailed(format!(
109            "service descriptor not found: {}",
110            service_name
111        ))
112    })?;
113
114    let method = svc
115        .methods()
116        .find(|m| m.name() == method_name)
117        .ok_or_else(|| {
118            CamelError::EndpointCreationFailed(format!(
119                "method descriptor not found: {}/{}",
120                service_name, method_name
121            ))
122        })?;
123
124    Ok(GrpcMode::from_method(&method))
125}
126
127const RESERVED_METADATA_KEYS: &[&str] = &[
128    "content-type",
129    "te",
130    "grpc-encoding",
131    "grpc-accept-encoding",
132    "grpc-status",
133    "grpc-message",
134    "grpc-status-details-bin",
135    "user-agent",
136];
137
138fn extract_metadata(metadata: &tonic::metadata::MetadataMap) -> Vec<(String, serde_json::Value)> {
139    let mut headers = Vec::new();
140    for key_and_value in metadata.iter() {
141        use tonic::metadata::KeyAndValueRef;
142        match key_and_value {
143            KeyAndValueRef::Ascii(key, value) => {
144                let key_str = key.as_str();
145                if RESERVED_METADATA_KEYS.contains(&key_str) {
146                    continue;
147                }
148                if let Ok(v) = value.to_str() {
149                    headers.push((
150                        key_str.to_string(),
151                        serde_json::Value::String(v.to_string()),
152                    ));
153                }
154            }
155            KeyAndValueRef::Binary(key, value) => {
156                let key_str = key.as_str();
157                if RESERVED_METADATA_KEYS.contains(&key_str) {
158                    continue;
159                }
160                let encoded = base64::engine::general_purpose::STANDARD.encode(value);
161                headers.push((format!("bin:{key_str}"), serde_json::Value::String(encoded)));
162            }
163        }
164    }
165    headers
166}
167
168pub(crate) enum GrpcStreamItem {
169    Message(Vec<u8>),
170    Error(tonic::Status),
171    Done,
172}
173
174pub(crate) enum GrpcReply {
175    Ok(Vec<u8>),
176    Err(tonic::Status),
177}
178
179/// Request envelope crossing the server→consumer boundary.
180///
181/// `kernel_principal` is the sealed principal minted by
182/// `kernel_authenticate` at the transport boundary
183/// (`unify-transport-auth`, Task 2.1) and is installed as the exchange's
184/// typed carrier before the pipeline runs. Policy evaluation is NOT done
185/// at the transport (the legacy scratch arm was deleted in
186/// `finish-auth-flip`): enforcement lives in the pipeline layer plus the
187/// strict dispatch check.
188pub(crate) enum GrpcRequestEnvelope {
189    Unary {
190        metadata: tonic::metadata::MetadataMap,
191        body: Vec<u8>,
192        reply_tx: tokio::sync::oneshot::Sender<GrpcReply>,
193        kernel_principal: Option<AuthenticatedPrincipal>,
194    },
195    ServerStreaming {
196        metadata: tonic::metadata::MetadataMap,
197        body: Vec<u8>,
198        reply_tx: mpsc::Sender<GrpcStreamItem>,
199        kernel_principal: Option<AuthenticatedPrincipal>,
200    },
201    ClientStreaming {
202        metadata: tonic::metadata::MetadataMap,
203        body_rx: mpsc::Receiver<Vec<u8>>,
204        reply_tx: tokio::sync::oneshot::Sender<GrpcReply>,
205        kernel_principal: Option<AuthenticatedPrincipal>,
206    },
207    Bidi {
208        metadata: tonic::metadata::MetadataMap,
209        body_rx: mpsc::Receiver<Vec<u8>>,
210        reply_tx: mpsc::Sender<GrpcStreamItem>,
211        kernel_principal: Option<AuthenticatedPrincipal>,
212    },
213}
214
215// ── Observer registry ──────────────────────────────────────────────────────
216
217static OBSERVER_REGISTRY: OnceLock<std::sync::Mutex<StdHashMap<String, GrpcStreamObserver>>> =
218    OnceLock::new();
219
220static OBSERVER_COUNTER: AtomicU64 = AtomicU64::new(0);
221
222fn next_observer_id() -> String {
223    let n = OBSERVER_COUNTER.fetch_add(1, Ordering::Relaxed);
224    format!("obs-{n}")
225}
226
227fn observer_registry() -> &'static std::sync::Mutex<StdHashMap<String, GrpcStreamObserver>> {
228    OBSERVER_REGISTRY.get_or_init(|| std::sync::Mutex::new(StdHashMap::new()))
229}
230
231fn register_observer(id: String, observer: GrpcStreamObserver) {
232    let registry = observer_registry();
233    let mut registry = match registry.lock() {
234        Ok(g) => g,
235        Err(poisoned) => poisoned.into_inner(),
236    };
237    registry.insert(id, observer);
238}
239
240fn remove_observer(id: &str) -> Option<GrpcStreamObserver> {
241    let registry = observer_registry();
242    let mut registry = match registry.lock() {
243        Ok(g) => g,
244        Err(poisoned) => poisoned.into_inner(),
245    };
246    registry.remove(id)
247}
248
249pub fn take_stream_observer(exchange: &Exchange) -> Option<GrpcStreamObserver> {
250    let id = exchange
251        .properties
252        .get("CamelGrpcStreamObserverId")?
253        .as_str()?;
254    remove_observer(id)
255}
256
257// ── Observer guard (auto-cleanup on Drop) ──────────────────────────────────
258
259struct ObserverGuard {
260    id: String,
261}
262
263impl ObserverGuard {
264    fn new(id: String) -> Self {
265        Self { id }
266    }
267}
268
269impl Drop for ObserverGuard {
270    fn drop(&mut self) {
271        remove_observer(&self.id);
272    }
273}
274
275// ── GrpcStreamObserver ─────────────────────────────────────────────────────
276
277#[derive(Clone)]
278pub struct GrpcStreamObserver {
279    tx: mpsc::Sender<GrpcStreamItem>,
280    resp_desc: MessageDescriptor,
281}
282
283impl GrpcStreamObserver {
284    pub(crate) fn new(tx: mpsc::Sender<GrpcStreamItem>, resp_desc: MessageDescriptor) -> Self {
285        Self { tx, resp_desc }
286    }
287
288    pub async fn on_next(&self, json: serde_json::Value) -> Result<(), CamelError> {
289        let encoded = json_to_protobuf_bytes(json, self.resp_desc.clone())
290            .map_err(|e| CamelError::ProcessorError(format!("failed to encode protobuf: {e}")))?;
291        self.tx
292            .send(GrpcStreamItem::Message(encoded))
293            .await
294            .map_err(|_| CamelError::ProcessorError("stream observer channel closed".into()))
295    }
296
297    pub async fn on_error(&self, status: Status) {
298        if self.tx.send(GrpcStreamItem::Error(status)).await.is_err() {
299            tracing::debug!("grpc stream observer: failed to send error, channel closed");
300        }
301    }
302
303    pub async fn on_completed(&self) {
304        if self.tx.send(GrpcStreamItem::Done).await.is_err() {
305            tracing::debug!("grpc stream observer: failed to send done, channel closed");
306        }
307    }
308}
309
310// ── Helper ─────────────────────────────────────────────────────────────────
311
312fn json_to_protobuf_bytes(
313    json: serde_json::Value,
314    desc: MessageDescriptor,
315) -> Result<Vec<u8>, Status> {
316    let json_str = serde_json::to_string(&json)
317        .map_err(|e| Status::internal(format!("failed to serialize JSON: {e}")))?;
318    let mut de = serde_json::Deserializer::from_str(&json_str);
319    let resp_dyn = DynamicMessage::deserialize(desc, &mut de)
320        .map_err(|e| Status::internal(format!("failed to parse JSON into protobuf: {e}")))?;
321    let mut buf = BytesMut::new();
322    prost::Message::encode(&resp_dyn, &mut buf)
323        .map_err(|e| Status::internal(format!("failed to encode protobuf: {e}")))?;
324    Ok(buf.to_vec())
325}
326
327// ── GrpcConsumer ───────────────────────────────────────────────────────────
328
329/// Reject credential sources the gRPC transport cannot carry.
330///
331/// gRPC metadata maps to HTTP headers only: `authorization_header` maps to the
332/// `authorization` metadata key and `{header: {name}}` to the same-named
333/// metadata key. Query parameters and cookies have no gRPC metadata
334/// representation, so a route declaring them must fail at load (ADR-0033
335/// fail-closed), not silently authenticate nothing at request time.
336pub(crate) fn validate_credential_sources(sources: &[CredentialSource]) -> Result<(), CamelError> {
337    for source in sources {
338        let source_kind = match source {
339            CredentialSource::QueryParam { .. } => "query_param",
340            CredentialSource::Cookie { .. } => "cookie",
341            _ => continue,
342        };
343        return Err(CamelError::Config(format!(
344            "grpc routes cannot carry {source_kind} credential sources; supported: authorization_header, header" // allow-secret: field names in error text, not values
345        )));
346    }
347    Ok(())
348}
349
350pub struct GrpcConsumer {
351    host: String,
352    port: u16,
353    path: String,
354    proto_path: PathBuf,
355    service_name: String,
356    method_name: String,
357    mode: GrpcMode,
358    security_ctx: Option<SecurityContext>,
359    runtime: Arc<dyn camel_component_api::RuntimeObservability>,
360    server_config: GrpcServerConfig,
361}
362
363impl GrpcConsumer {
364    #[allow(clippy::too_many_arguments)]
365    pub fn new(
366        host: String,
367        port: u16,
368        path: String,
369        proto_path: PathBuf,
370        service_name: String,
371        method_name: String,
372        mode: GrpcMode,
373        runtime: Arc<dyn camel_component_api::RuntimeObservability>,
374        server_config: GrpcServerConfig,
375    ) -> Self {
376        Self {
377            host,
378            port,
379            path,
380            proto_path,
381            service_name,
382            method_name,
383            mode,
384            security_ctx: None,
385            runtime,
386            server_config,
387        }
388    }
389
390    fn resolve_descriptors(&self) -> Result<(MessageDescriptor, MessageDescriptor), CamelError> {
391        let cache = proto_cache();
392        let pool = cache
393            .get_or_compile(&self.proto_path, std::iter::empty::<&std::path::Path>())
394            .map_err(|e| {
395                CamelError::EndpointCreationFailed(format!("failed to compile proto: {e}"))
396            })?;
397
398        let svc = pool
399            .get_service_by_name(&self.service_name)
400            .ok_or_else(|| {
401                CamelError::EndpointCreationFailed(format!(
402                    "service descriptor not found: {}",
403                    self.service_name
404                ))
405            })?;
406
407        let method = svc
408            .methods()
409            .find(|m| m.name() == self.method_name)
410            .ok_or_else(|| {
411                CamelError::EndpointCreationFailed(format!(
412                    "method descriptor not found: {}/{}",
413                    self.service_name, self.method_name
414                ))
415            })?;
416
417        Ok((method.input(), method.output()))
418    }
419
420    /// Validate every credential-source list this route can extract from:
421    /// the configured sources and, when a compiled plan is present, the
422    /// plan's own sources (fail-closed at load, ADR-0033).
423    fn validate_route_credential_sources(&self) -> Result<(), CamelError> {
424        let Some(sec_ctx) = &self.security_ctx else {
425            return Ok(());
426        };
427        validate_credential_sources(&sec_ctx.credential_sources)?;
428        if let Some(plan) = &sec_ctx.plan {
429            validate_credential_sources(&plan.credential_sources)?;
430        }
431        Ok(())
432    }
433
434    pub async fn start_with_listener(
435        &mut self,
436        ctx: ConsumerContext,
437        listener: tokio::net::TcpListener,
438    ) -> Result<(), CamelError> {
439        self.validate_route_credential_sources()?;
440        let dispatch = GrpcServerRegistry::global()
441            .get_or_spawn_with_listener(
442                listener,
443                &self.host,
444                self.port,
445                self.server_config.clone(),
446                Arc::clone(&self.runtime),
447            )
448            .await?;
449        self.start_inner(ctx, dispatch).await
450    }
451
452    async fn start_inner(
453        &mut self,
454        ctx: ConsumerContext,
455        dispatch: GrpcDispatchTable,
456    ) -> Result<(), CamelError> {
457        let (req_desc, resp_desc) = self.resolve_descriptors()?;
458        let mode = self.mode;
459
460        let (env_tx, mut env_rx) = mpsc::channel::<GrpcRequestEnvelope>(64);
461        // Kernel interceptor state is captured HERE, at dispatch-entry
462        // construction, from the security context wired before start
463        // (Task 2.1 construction-order lifecycle fix). The per-request
464        // handlers are built from this entry, so the plan is present
465        // before any request arrives — never patched on afterwards.
466        let kernel = self
467            .security_ctx
468            .as_ref()
469            .and_then(GrpcKernelAuth::from_security_context)
470            .map(Arc::new);
471        {
472            let mut table = dispatch.write().await;
473            if table.contains_key(&self.path) {
474                return Err(CamelError::EndpointCreationFailed(format!(
475                    "duplicate gRPC consumer path: {}",
476                    self.path
477                )));
478            }
479            table.insert(self.path.clone(), (env_tx, mode, kernel.clone()));
480        }
481
482        let path = self.path.clone();
483        let host = self.host.clone();
484        let port = self.port;
485        let sender = ctx.sender();
486
487        info!(
488            path = %path,
489            host = %host,
490            port = port,
491            mode = ?mode,
492            "grpc consumer started, waiting for requests"
493        );
494
495        // NOTE: Long-running bidi streams hold a semaphore permit for their duration.
496        // If this becomes an issue, consider separate concurrency limits for streaming vs unary.
497        let semaphore = std::sync::Arc::new(tokio::sync::Semaphore::new(64));
498        let mut join_set = tokio::task::JoinSet::new();
499
500        loop {
501            tokio::select! {
502                biased;
503                _ = ctx.cancelled() => {
504                    info!(
505                        path = %path,
506                        "grpc consumer cancelled, shutting down"
507                    );
508                    break;
509                }
510                envelope = env_rx.recv() => {
511                    let Some(envelope) = envelope else { break };
512
513                    let sem = semaphore.clone();
514                    let permit = sem.acquire_owned().await.map_err(|_| CamelError::ChannelClosed)?;
515                    let req_desc = req_desc.clone();
516                    let resp_desc = resp_desc.clone();
517                    let sender = sender.clone();
518                    let correlation_id = next_observer_id();
519                    let path_for_log = path.clone();
520                    // Kernel state captured at dispatch-entry construction,
521                    // cloned per request; the principal minted by the
522                    // interceptor binds to this plan for the carrier install.
523                    // Per-request policy evaluation is NOT done here —
524                    // enforcement lives in the pipeline layer plus the
525                    // strict dispatch check.
526                    let kernel = kernel.clone();
527
528                    debug!(
529                        path = %path_for_log,
530                        correlation_id = %correlation_id,
531                        "grpc consumer received request"
532                    );
533
534                    join_set.spawn(async move {
535                        let _permit = permit;
536                        match envelope {
537                            GrpcRequestEnvelope::Unary { metadata, body, reply_tx, kernel_principal } => {
538                                debug!(
539                                    path = %path_for_log,
540                                    correlation_id = %correlation_id,
541                                    size = body.len(),
542                                    "grpc consumer processing unary request"
543                                );
544
545                                let kernel_auth = kernel_request_auth(kernel.as_deref(), kernel_principal);
546                                let result = process_unary_request(
547                                    body, metadata, req_desc, resp_desc, sender, kernel_auth,
548                                ).await;
549                                let reply = match result {
550                                    Ok(bytes) => GrpcReply::Ok(bytes),
551                                    Err(status) => GrpcReply::Err(status),
552                                };
553                                let _ = reply_tx.send(reply);
554                            }
555                            GrpcRequestEnvelope::ServerStreaming { metadata, body, reply_tx, kernel_principal } => {
556                                debug!(
557                                    path = %path_for_log,
558                                    correlation_id = %correlation_id,
559                                    size = body.len(),
560                                    "grpc consumer processing server streaming request"
561                                );
562
563                                let kernel_auth = kernel_request_auth(kernel.as_deref(), kernel_principal);
564                                process_server_streaming_request(
565                                    body, metadata, req_desc, resp_desc, sender, reply_tx, kernel_auth,
566                                ).await;
567                            }
568                            GrpcRequestEnvelope::ClientStreaming { metadata, body_rx, reply_tx, kernel_principal } => {
569                                debug!(
570                                    path = %path_for_log,
571                                    correlation_id = %correlation_id,
572                                    "grpc consumer processing client streaming request"
573                                );
574
575                                let kernel_auth = kernel_request_auth(kernel.as_deref(), kernel_principal);
576                                process_client_streaming_request(
577                                    body_rx, metadata, req_desc, resp_desc, sender, reply_tx, kernel_auth,
578                                ).await;
579                            }
580                            GrpcRequestEnvelope::Bidi { metadata, body_rx, reply_tx, kernel_principal } => {
581                                debug!(
582                                    path = %path_for_log,
583                                    correlation_id = %correlation_id,
584                                    "grpc consumer processing bidi streaming request"
585                                );
586
587                                let kernel_auth = kernel_request_auth(kernel.as_deref(), kernel_principal);
588                                process_bidi_request(
589                                    body_rx, metadata, req_desc, resp_desc, sender, reply_tx, kernel_auth,
590                                ).await;
591                            }
592                        }
593                    });
594                }
595            }
596        }
597
598        join_set.shutdown().await;
599
600        GrpcServerRegistry::global()
601            .unregister(&host, port, &path)
602            .await;
603
604        info!(
605            path = %path,
606            "grpc consumer stopped"
607        );
608
609        Ok(())
610    }
611}
612
613#[async_trait]
614impl Consumer for GrpcConsumer {
615    async fn start(&mut self, ctx: ConsumerContext) -> Result<(), CamelError> {
616        self.validate_route_credential_sources()?;
617        info!(
618            host = %self.host,
619            port = self.port,
620            service = %self.service_name,
621            method = %self.method_name,
622            mode = ?self.mode,
623            "grpc consumer starting"
624        );
625        let dispatch = GrpcServerRegistry::global()
626            .get_or_spawn(
627                &self.host,
628                self.port,
629                self.server_config.clone(),
630                Arc::clone(&self.runtime),
631            )
632            .await?;
633        // gRPC listener is bound inside get_or_spawn (TcpListener::bind
634        // before tokio::spawn). Signal readiness now that the bind succeeded.
635        ctx.mark_ready();
636        self.start_inner(ctx, dispatch).await
637    }
638
639    async fn stop(&mut self) -> Result<(), CamelError> {
640        info!(
641            host = %self.host,
642            port = self.port,
643            service = %self.service_name,
644            method = %self.method_name,
645            "grpc consumer stopping"
646        );
647        GrpcServerRegistry::global()
648            .unregister(&self.host, self.port, &self.path)
649            .await;
650        Ok(())
651    }
652
653    fn concurrency_model(&self) -> ConcurrencyModel {
654        ConcurrencyModel::Concurrent { max: None }
655    }
656
657    fn startup_mode(&self) -> ConsumerStartupMode {
658        ConsumerStartupMode::Explicit
659    }
660
661    fn set_security_context(&mut self, ctx: SecurityContext) {
662        self.security_ctx = Some(ctx);
663    }
664}
665
666// ── Unary processor (unchanged) ────────────────────────────────────────────
667
668async fn process_unary_request(
669    body: Vec<u8>,
670    metadata: tonic::metadata::MetadataMap,
671    req_desc: MessageDescriptor,
672    resp_desc: MessageDescriptor,
673    sender: mpsc::Sender<ExchangeEnvelope>,
674    kernel_auth: Option<KernelRequestAuth>,
675) -> Result<Vec<u8>, Status> {
676    let req_dyn = DynamicMessage::decode(req_desc, body.as_slice())
677        .map_err(|e| Status::invalid_argument(format!("failed to decode protobuf: {e}")))?;
678
679    let json = serde_json::to_value(&req_dyn).map_err(|e| {
680        Status::invalid_argument(format!("failed to convert protobuf to JSON: {e}"))
681    })?;
682
683    let mut msg = Message::new(Body::Json(json));
684    for (k, v) in extract_metadata(&metadata) {
685        msg.set_header(k, v);
686    }
687
688    let mut exchange = Exchange::new(msg);
689    if let Some(auth) = kernel_auth.as_ref() {
690        // Carrier install + route-binding enforcement before the pipeline.
691        auth.apply_to(&mut exchange)?;
692    }
693
694    let (reply_tx, reply_rx) = tokio::sync::oneshot::channel();
695    let envelope = ExchangeEnvelope {
696        exchange,
697        reply_tx: Some(reply_tx),
698    };
699
700    sender
701        .send(envelope)
702        .await
703        .map_err(|_| Status::internal("pipeline channel closed"))?;
704
705    let result = reply_rx
706        .await
707        .map_err(|_| Status::internal("pipeline reply dropped"))?
708        .map_err(pipeline_error_to_status)?;
709
710    let resp_json = match result.input.body {
711        Body::Json(v) => v,
712        other => {
713            return Err(Status::internal(format!(
714                "expected JSON response body from pipeline, got {other:?}"
715            )));
716        }
717    };
718
719    let json_str = serde_json::to_string(&resp_json)
720        .map_err(|e| Status::internal(format!("failed to serialize response JSON: {e}")))?;
721    let mut de = serde_json::Deserializer::from_str(&json_str);
722    let resp_dyn = DynamicMessage::deserialize(resp_desc, &mut de)
723        .map_err(|e| Status::internal(format!("failed to parse JSON into protobuf: {e}")))?;
724
725    let mut buf = BytesMut::new();
726    resp_dyn
727        .encode(&mut buf)
728        .map_err(|e| Status::internal(format!("failed to encode protobuf response: {e}")))?;
729
730    Ok(buf.to_vec())
731}
732
733// ── Server-streaming processor ─────────────────────────────────────────────
734
735async fn process_server_streaming_request(
736    body: Vec<u8>,
737    metadata: tonic::metadata::MetadataMap,
738    req_desc: MessageDescriptor,
739    resp_desc: MessageDescriptor,
740    sender: mpsc::Sender<ExchangeEnvelope>,
741    reply_tx: mpsc::Sender<GrpcStreamItem>,
742    kernel_auth: Option<KernelRequestAuth>,
743) {
744    let req_dyn = match DynamicMessage::decode(req_desc, body.as_slice()) {
745        Ok(m) => m,
746        Err(e) => {
747            let _ = reply_tx
748                .send(GrpcStreamItem::Error(Status::invalid_argument(format!(
749                    "failed to decode protobuf: {e}"
750                ))))
751                .await;
752            return;
753        }
754    };
755
756    let json = match serde_json::to_value(&req_dyn) {
757        Ok(v) => v,
758        Err(e) => {
759            let _ = reply_tx
760                .send(GrpcStreamItem::Error(Status::invalid_argument(format!(
761                    "failed to convert protobuf to JSON: {e}"
762                ))))
763                .await;
764            return;
765        }
766    };
767
768    let mut msg = Message::new(Body::Json(json));
769    for (k, v) in extract_metadata(&metadata) {
770        msg.set_header(k, v);
771    }
772
773    let observer = GrpcStreamObserver::new(reply_tx.clone(), resp_desc);
774    let observer_id = next_observer_id();
775    register_observer(observer_id.clone(), observer.clone());
776    let _guard = ObserverGuard::new(observer_id.clone());
777
778    let mut exchange = Exchange::new(msg);
779    if let Some(auth) = kernel_auth.as_ref()
780        && let Err(status) = auth.apply_to(&mut exchange)
781    {
782        let _ = reply_tx.send(GrpcStreamItem::Error(status)).await;
783        return;
784    }
785    exchange.set_property("CamelGrpcStreamObserverId", Value::String(observer_id));
786
787    // The envelope carries a pipeline reply channel so a pipeline error
788    // (policy denial included) reaches this processor instead of dying
789    // with `reply_tx: None` — the regression where a denial ended the
790    // stream as a silent, empty success.
791    let (pipeline_reply_tx, pipeline_reply_rx) = tokio::sync::oneshot::channel();
792    let envelope = ExchangeEnvelope {
793        exchange,
794        reply_tx: Some(pipeline_reply_tx),
795    };
796
797    if sender.send(envelope).await.is_err() {
798        let _ = reply_tx
799            .send(GrpcStreamItem::Error(Status::internal(
800                "pipeline channel closed",
801            )))
802            .await;
803        return;
804    }
805
806    // The pipeline verdict decides the stream's terminal frame: a
807    // pipeline error is surfaced client-visibly via the observer (the
808    // same denial idiom the deleted transport-side scratch evaluation
809    // emitted). A successful result streamed through the observer adds
810    // nothing; so does a dropped reply sender (route stand-ins that
811    // never reply) — the observer stream stays the truth either way.
812    if let Ok(Err(e)) = pipeline_reply_rx.await {
813        observer.on_error(pipeline_error_to_status(e)).await;
814    }
815
816    // Wait for the stream receiver to be dropped (stream complete).
817    // This keeps the guard alive so the observer stays registered until
818    // the route is done. If take_stream_observer was called, the guard's
819    // Drop is a no-op. If not, the guard cleans up the leaked observer.
820    reply_tx.closed().await;
821}
822
823// ── Client-streaming processor ─────────────────────────────────────────────
824
825async fn process_client_streaming_request(
826    mut body_rx: mpsc::Receiver<Vec<u8>>,
827    metadata: tonic::metadata::MetadataMap,
828    req_desc: MessageDescriptor,
829    resp_desc: MessageDescriptor,
830    sender: mpsc::Sender<ExchangeEnvelope>,
831    reply_tx: tokio::sync::oneshot::Sender<GrpcReply>,
832    kernel_auth: Option<KernelRequestAuth>,
833) {
834    while let Some(body) = body_rx.recv().await {
835        let req_dyn = match DynamicMessage::decode(req_desc.clone(), body.as_slice()) {
836            Ok(d) => d,
837            Err(e) => {
838                let _ = reply_tx.send(GrpcReply::Err(Status::invalid_argument(format!(
839                    "failed to decode protobuf: {e}"
840                ))));
841                return;
842            }
843        };
844
845        let json = match serde_json::to_value(&req_dyn) {
846            Ok(j) => j,
847            Err(e) => {
848                let _ = reply_tx.send(GrpcReply::Err(Status::internal(format!(
849                    "failed to convert protobuf to JSON: {e}"
850                ))));
851                return;
852            }
853        };
854
855        let mut msg = Message::new(Body::Json(json));
856        for (k, v) in extract_metadata(&metadata) {
857            msg.set_header(k, v);
858        }
859        msg.set_header(
860            "CamelGrpcClientStreaming".to_string(),
861            serde_json::Value::Bool(true),
862        );
863
864        let mut exchange = Exchange::new(msg);
865        if let Some(auth) = kernel_auth.as_ref()
866            && let Err(status) = auth.apply_to(&mut exchange)
867        {
868            let _ = reply_tx.send(GrpcReply::Err(status));
869            return;
870        }
871        let (reply_tx_pipe, reply_rx_pipe) = tokio::sync::oneshot::channel();
872        let envelope = ExchangeEnvelope {
873            exchange,
874            reply_tx: Some(reply_tx_pipe),
875        };
876
877        if sender.send(envelope).await.is_err() {
878            let _ = reply_tx.send(GrpcReply::Err(Status::internal("pipeline channel closed")));
879            return;
880        }
881
882        // Intentionally discard intermediate replies — only the completion exchange's reply matters.
883        let _ = reply_rx_pipe.await;
884    }
885
886    // Stream complete — send final Exchange with completion marker
887    let mut completion_msg = Message::new(Body::Json(serde_json::Value::Null));
888    for (k, v) in extract_metadata(&metadata) {
889        completion_msg.set_header(k, v);
890    }
891    completion_msg.set_header(
892        "CamelGrpcClientStreaming".to_string(),
893        serde_json::Value::Bool(true),
894    );
895    completion_msg.set_header(
896        "CamelGrpcClientStreamComplete".to_string(),
897        serde_json::Value::Bool(true),
898    );
899
900    let mut completion_exchange = Exchange::new(completion_msg);
901    if let Some(auth) = kernel_auth.as_ref()
902        && let Err(status) = auth.apply_to(&mut completion_exchange)
903    {
904        let _ = reply_tx.send(GrpcReply::Err(status));
905        return;
906    }
907    let (reply_tx_pipe, reply_rx_pipe) = tokio::sync::oneshot::channel();
908    let envelope = ExchangeEnvelope {
909        exchange: completion_exchange,
910        reply_tx: Some(reply_tx_pipe),
911    };
912
913    if sender.send(envelope).await.is_err() {
914        let _ = reply_tx.send(GrpcReply::Err(Status::internal("pipeline channel closed")));
915        return;
916    }
917
918    // The route's response to the completion Exchange becomes the gRPC response
919    let result = match reply_rx_pipe.await {
920        Ok(Ok(exchange)) => exchange,
921        Ok(Err(e)) => {
922            let _ = reply_tx.send(GrpcReply::Err(pipeline_error_to_status(e)));
923            return;
924        }
925        Err(_) => {
926            let _ = reply_tx.send(GrpcReply::Err(Status::internal("pipeline reply dropped")));
927            return;
928        }
929    };
930
931    let resp_json = match result.input.body {
932        Body::Json(v) => v,
933        other => {
934            let _ = reply_tx.send(GrpcReply::Err(Status::internal(format!(
935                "expected JSON response body from pipeline, got {other:?}"
936            ))));
937            return;
938        }
939    };
940
941    let encoded = match json_to_protobuf_bytes(resp_json, resp_desc) {
942        Ok(b) => b,
943        Err(e) => {
944            let _ = reply_tx.send(GrpcReply::Err(Status::internal(format!(
945                "failed to encode response: {e}",
946            ))));
947            return;
948        }
949    };
950
951    let _ = reply_tx.send(GrpcReply::Ok(encoded));
952}
953
954// ── Bidi-streaming processor ───────────────────────────────────────────────
955
956async fn process_bidi_request(
957    mut body_rx: mpsc::Receiver<Vec<u8>>,
958    metadata: tonic::metadata::MetadataMap,
959    req_desc: MessageDescriptor,
960    resp_desc: MessageDescriptor,
961    sender: mpsc::Sender<ExchangeEnvelope>,
962    reply_tx: mpsc::Sender<GrpcStreamItem>,
963    kernel_auth: Option<KernelRequestAuth>,
964) {
965    let observer = GrpcStreamObserver::new(reply_tx.clone(), resp_desc);
966    let observer_id = next_observer_id();
967    register_observer(observer_id.clone(), observer.clone());
968    let _guard = ObserverGuard::new(observer_id.clone());
969
970    // Spawn a task to forward messages from the client stream to the pipeline
971    let sender_clone = sender.clone();
972    let metadata_clone = metadata.clone();
973    let req_desc_clone = req_desc.clone();
974
975    let forward_task = tokio::spawn(async move {
976        let mut sequence: u64 = 0;
977        while let Some(body) = body_rx.recv().await {
978            let req_dyn = match DynamicMessage::decode(req_desc_clone.clone(), body.as_slice()) {
979                Ok(m) => m,
980                Err(e) => {
981                    let _ = observer
982                        .on_error(Status::invalid_argument(format!(
983                            "failed to decode protobuf: {e}"
984                        )))
985                        .await;
986                    continue;
987                }
988            };
989
990            let json = match serde_json::to_value(&req_dyn) {
991                Ok(v) => v,
992                Err(e) => {
993                    let _ = observer
994                        .on_error(Status::invalid_argument(format!(
995                            "failed to convert protobuf to JSON: {e}"
996                        )))
997                        .await;
998                    continue;
999                }
1000            };
1001
1002            let mut msg = Message::new(Body::Json(json));
1003            for (k, v) in extract_metadata(&metadata_clone) {
1004                msg.set_header(k, v);
1005            }
1006
1007            msg.set_header(
1008                "CamelGrpcBidiSequence",
1009                serde_json::Value::Number(sequence.into()),
1010            );
1011            sequence += 1;
1012
1013            let mut exchange = Exchange::new(msg);
1014            if let Some(auth) = kernel_auth.as_ref()
1015                && let Err(status) = auth.apply_to(&mut exchange)
1016            {
1017                let _ = observer.on_error(status).await;
1018                break;
1019            }
1020            exchange.set_property(
1021                "CamelGrpcStreamObserverId",
1022                Value::String(observer_id.clone()),
1023            );
1024
1025            // Each message envelope carries a pipeline reply channel so
1026            // a pipeline error (policy denial included) becomes a
1027            // client-visible stream error instead of dying with
1028            // `reply_tx: None`. The forwarding loop stays non-blocking:
1029            // a per-message watcher renders the verdict via the
1030            // observer.
1031            let (pipeline_reply_tx, pipeline_reply_rx) = tokio::sync::oneshot::channel();
1032            let envelope = ExchangeEnvelope {
1033                exchange,
1034                reply_tx: Some(pipeline_reply_tx),
1035            };
1036
1037            if sender_clone.send(envelope).await.is_err() {
1038                let _ = observer
1039                    .on_error(Status::internal("pipeline channel closed"))
1040                    .await;
1041                break;
1042            }
1043
1044            let verdict_observer = observer.clone();
1045            tokio::spawn(async move {
1046                if let Ok(Err(e)) = pipeline_reply_rx.await {
1047                    verdict_observer.on_error(pipeline_error_to_status(e)).await;
1048                }
1049            });
1050        }
1051
1052        // Signal completion when client stream ends
1053        observer.on_completed().await;
1054    });
1055
1056    // Wait for the forward task to complete
1057    let _ = forward_task.await;
1058}
1059
1060#[cfg(test)]
1061mod tests {
1062    use super::*;
1063
1064    #[test]
1065    fn grpc_credential_sources_uncarryable_rejected_at_load() {
1066        let query = CredentialSource::QueryParam {
1067            param: "ticket".to_string(),
1068        };
1069        let err = validate_credential_sources(&[query]).unwrap_err();
1070        let msg = err.to_string();
1071        assert!(msg.contains("query_param"), "message was: {msg}");
1072        assert!(msg.contains("grpc"), "message was: {msg}");
1073
1074        let cookie = CredentialSource::Cookie {
1075            name: "session".to_string(),
1076        };
1077        let err = validate_credential_sources(&[cookie]).unwrap_err();
1078        let msg = err.to_string();
1079        assert!(msg.contains("cookie"), "message was: {msg}");
1080        assert!(msg.contains("grpc"), "message was: {msg}");
1081
1082        // Carryable sources pass validation.
1083        let carryable = vec![
1084            CredentialSource::AuthorizationHeader,
1085            CredentialSource::Header {
1086                name: "x-api-key".to_string(),
1087            },
1088        ];
1089        assert!(validate_credential_sources(&carryable).is_ok());
1090        assert!(validate_credential_sources(&[]).is_ok());
1091    }
1092}