use std::path::PathBuf;
use std::sync::Arc;
use camel_api::security_policy::{
AccessMode, AuthPrincipal, CredentialSource, PRINCIPAL_SUBJECT_KEY, RouteSecurityPlan,
TransportId,
};
use camel_api::{Body, Exchange, Message};
use camel_auth::native_auth::{
NativeCredential, NativeCredentialSecret, NativeCredentialStore, StaticTokenAuthenticator,
};
use camel_auth::{ProviderEntry, ProviderRegistry, RolePolicy, TokenAuthenticator, read_carrier};
use camel_component_api::{
Consumer, ConsumerContext, ExchangeEnvelope, NoOpComponentContext, RuntimeObservability,
SecurityContext,
};
use camel_component_grpc::GrpcMode;
use camel_component_grpc::config::GrpcServerConfig;
use camel_component_grpc::consumer::GrpcConsumer;
use tokio::net::TcpListener;
use tokio::sync::mpsc;
use tokio_util::sync::CancellationToken;
use zeroize::Zeroizing;
mod helloworld {
tonic::include_proto!("helloworld");
}
const PROVIDER_ID: &str = "idp-grpc";
const TOKEN: &str = "test-token-grpc";
fn test_rt() -> Arc<dyn RuntimeObservability> {
Arc::new(NoOpComponentContext)
}
fn fixture_registry() -> ProviderRegistry {
let store = NativeCredentialStore::try_new(vec![NativeCredential {
secret: NativeCredentialSecret::Plaintext {
value: Zeroizing::new(TOKEN.to_string()),
},
principal: camel_api::security_policy::Principal {
subject: "svc-grpc".to_string(),
issuer: "test".to_string(),
audience: vec![],
scopes: vec![],
roles: vec!["grpc-role".to_string()],
claims: serde_json::Value::Null,
},
}])
.expect("credential store");
let registry = ProviderRegistry::new();
registry.register(
PROVIDER_ID,
ProviderEntry {
authenticator: Arc::new(StaticTokenAuthenticator::new(store)),
audience_binding: None,
},
);
registry
}
fn authenticated_plan(sources: Vec<CredentialSource>) -> RouteSecurityPlan {
RouteSecurityPlan {
access_mode: AccessMode::Authenticated,
provider_ref: Some(PROVIDER_ID.to_string()),
transport: TransportId::Grpc,
credential_sources: sources,
audience_binding: None,
}
}
async fn start_secured_consumer(
plan: RouteSecurityPlan,
providers: Arc<ProviderRegistry>,
) -> (u16, mpsc::Receiver<ExchangeEnvelope>) {
let listener = TcpListener::bind("127.0.0.1:0").await.expect("bind");
let port = listener.local_addr().expect("addr").port();
let proto_path = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("tests/helloworld.proto");
let mut consumer = GrpcConsumer::new(
"127.0.0.1".to_string(),
port,
"/helloworld.Greeter/SayHello".to_string(),
proto_path,
"helloworld.Greeter".to_string(),
"SayHello".to_string(),
GrpcMode::Unary,
test_rt(),
GrpcServerConfig::default(),
);
let entry = providers.resolve(PROVIDER_ID).expect("fixture provider");
let authenticator: Arc<dyn TokenAuthenticator> = Arc::clone(&entry.authenticator);
let policy = RolePolicy::new(vec!["grpc-role".to_string()], true);
let sec_ctx = SecurityContext::new(policy, authenticator)
.with_credential_sources(plan.credential_sources.clone())
.with_plan(plan)
.with_providers(providers);
consumer.set_security_context(sec_ctx);
let (route_tx, route_rx) = mpsc::channel(16);
let cancel_token = CancellationToken::new();
let ctx = ConsumerContext::new(route_tx, cancel_token, "grpc-auth-test-route".to_string());
tokio::spawn(async move {
consumer
.start_with_listener(ctx, listener)
.await
.expect("consumer start");
});
tokio::time::sleep(std::time::Duration::from_millis(150)).await;
(port, route_rx)
}
async fn greeter_client(
port: u16,
) -> helloworld::greeter_client::GreeterClient<tonic::transport::Channel> {
let channel = tonic::transport::Endpoint::from_shared(format!("http://127.0.0.1:{port}"))
.expect("endpoint")
.connect_lazy();
helloworld::greeter_client::GreeterClient::new(channel)
}
fn ok_reply() -> Exchange {
Exchange::new(Message::new(Body::Json(
serde_json::json!({"message": "ok"}),
)))
}
#[tokio::test]
async fn grpc_denies_without_credentials_under_kernel() {
let providers = Arc::new(fixture_registry());
let plan = authenticated_plan(vec![CredentialSource::AuthorizationHeader]);
let (port, mut route_rx) = start_secured_consumer(plan, providers).await;
let mut client = greeter_client(port).await;
let err = client
.say_hello(helloworld::HelloRequest {
name: "World".to_string(),
})
.await
.expect_err("request without credentials must be denied");
assert_eq!(
err.code(),
tonic::Code::Unauthenticated,
"denial must be UNAUTHENTICATED, got: {err}"
);
assert!(
route_rx.try_recv().is_err(),
"denied request must not produce a route exchange"
);
}
#[tokio::test]
async fn grpc_named_header_credential_authenticates() {
let providers = Arc::new(fixture_registry());
let plan = authenticated_plan(vec![CredentialSource::Header {
name: "x-api-key".to_string(),
}]);
let (port, mut route_rx) = start_secured_consumer(plan, providers).await;
let pipeline = tokio::spawn(async move {
let envelope = route_rx.recv().await.expect("exchange reaches route");
let subject = envelope
.exchange
.property(PRINCIPAL_SUBJECT_KEY)
.and_then(|v| v.as_str().map(str::to_string));
if let Some(tx) = envelope.reply_tx {
let _ = tx.send(Ok(ok_reply()));
}
subject
});
let mut client = greeter_client(port).await;
let mut request = tonic::Request::new(helloworld::HelloRequest {
name: "World".to_string(),
});
request
.metadata_mut()
.insert("x-api-key", TOKEN.parse().expect("token metadata value"));
let response = client
.say_hello(request)
.await
.expect("valid named-header credential must authenticate");
assert_eq!(response.into_inner().message, "ok");
let subject = pipeline.await.expect("pipeline join");
assert_eq!(
subject.as_deref(),
Some("svc-grpc"),
"authenticated principal must reach the route"
);
}
#[tokio::test]
async fn grpc_carrier_present_on_second_request_fresh_exchange() {
let providers = Arc::new(fixture_registry());
let plan = authenticated_plan(vec![CredentialSource::AuthorizationHeader]);
let (port, mut route_rx) = start_secured_consumer(plan, providers).await;
let pipeline = tokio::spawn(async move {
let mut carried = 0usize;
for _ in 0..2 {
let envelope = route_rx.recv().await.expect("exchange reaches route");
if let Some(carrier) = read_carrier(&envelope.exchange) {
assert_eq!(carrier.provider_id(), PROVIDER_ID);
assert_eq!(carrier.principal().subject, "svc-grpc");
carried += 1;
}
if let Some(tx) = envelope.reply_tx {
let _ = tx.send(Ok(ok_reply()));
}
}
carried
});
let mut client = greeter_client(port).await;
for i in 0..2 {
let mut request = tonic::Request::new(helloworld::HelloRequest {
name: format!("World-{i}"),
});
request.metadata_mut().insert(
"authorization",
format!("Bearer {TOKEN}")
.parse()
.expect("bearer metadata value"),
);
let response = client
.say_hello(request)
.await
.expect("request {i} must succeed under the kernel path");
assert_eq!(response.into_inner().message, "ok");
}
let carried = pipeline.await.expect("pipeline join");
assert_eq!(
carried, 2,
"typed carrier must be present on EVERY fresh exchange, not just the first"
);
}