use std::sync::Arc;
use camel_api::CamelError;
use camel_api::security_policy::{
AccessMode, AudienceBinding, CredentialSource, RouteSecurityPlan, TransportId,
};
use camel_auth::{ProviderEntry, ProviderRegistry};
use crate::lifecycle::application::route_definition::RouteDefinition;
pub(crate) fn transport_from_uri(uri: &str) -> TransportId {
let scheme = uri
.split([':', '?'])
.next()
.unwrap_or("")
.to_ascii_lowercase();
match scheme.as_str() {
"http" | "https" => TransportId::Http,
"ws" | "wss" => TransportId::Ws,
"grpc" => TransportId::Grpc,
"mcp" => TransportId::Mcp,
"wasm" => TransportId::Wasm,
_ => TransportId::Http,
}
}
fn consumer_transport_from_uri(uri: &str) -> Option<TransportId> {
let scheme = uri
.split([':', '?'])
.next()
.unwrap_or("")
.to_ascii_lowercase();
match scheme.as_str() {
"http" | "https" => Some(TransportId::Http),
"ws" | "wss" => Some(TransportId::Ws),
"grpc" => Some(TransportId::Grpc),
"mcp" => Some(TransportId::Mcp),
"wasm" => Some(TransportId::Wasm),
_ => None,
}
}
fn transport_name(transport: TransportId) -> &'static str {
match transport {
TransportId::Http => "http",
TransportId::Ws => "ws",
TransportId::Grpc => "grpc",
TransportId::Mcp => "mcp",
TransportId::Wasm => "wasm",
}
}
fn credential_source_allowed(transport: TransportId, source: &CredentialSource) -> bool {
match (transport, source) {
(TransportId::Http, _) | (TransportId::Wasm, _) => true,
(TransportId::Ws, CredentialSource::QueryParam { .. })
| (TransportId::Mcp, CredentialSource::QueryParam { .. }) => false,
(TransportId::Ws, _) | (TransportId::Mcp, _) => true,
(
TransportId::Grpc,
CredentialSource::AuthorizationHeader | CredentialSource::Header { .. },
) => true,
(TransportId::Grpc, _) => false,
}
}
fn resolve_plan_provider(
route_id: &str,
declared: Option<&str>,
providers: &ProviderRegistry,
) -> Result<(String, Arc<ProviderEntry>), CamelError> {
let names = providers.names();
match declared {
Some(name) => match providers.resolve(name) {
Some(entry) => Ok((name.to_string(), entry)),
None => Err(CamelError::RouteError(format!(
"route '{route_id}' declares security provider '{name}' but it is not registered (available: [{}])",
names.join(", ")
))),
},
None => match names.len() {
0 => Err(CamelError::RouteError(format!(
"route '{route_id}' declares security but no authentication provider is registered; \
register a provider or declare security_policy.provider"
))),
1 => {
let name = names.into_iter().next().expect("len checked == 1"); let entry = providers
.resolve(&name)
.expect("sole provider must resolve"); Ok((name, entry))
}
_ => Err(CamelError::RouteError(format!(
"route '{route_id}' declares security but multiple providers are registered \
([{}]); declare security_policy.provider to select one",
names.join(", ")
))),
},
}
}
pub fn compile_route_security_plan(
definition: &RouteDefinition,
providers: &ProviderRegistry,
) -> Result<Option<RouteSecurityPlan>, CamelError> {
let Some(transport) = consumer_transport_from_uri(definition.from_uri()) else {
return Ok(None);
};
let route_id = definition.route_id();
let policy = definition.security_policy_config();
let declared_provider = definition.security_provider();
if policy.is_none()
&& declared_provider.is_none()
&& definition.security_authenticator().is_none()
{
return Ok(Some(RouteSecurityPlan {
access_mode: AccessMode::Public,
provider_ref: None,
transport,
credential_sources: Vec::new(),
audience_binding: None,
}));
}
let (provider_ref, entry) = resolve_plan_provider(route_id, declared_provider, providers)?;
let audience_binding = match definition.security_audiences() {
Some(audiences) => Some(AudienceBinding {
issuers: entry
.audience_binding
.as_ref()
.map(|b| b.issuers.clone())
.unwrap_or_default(),
audiences: audiences.to_vec(),
}),
None => entry.audience_binding.clone(),
};
let (access_mode, credential_sources) = match policy {
Some(sp) => (
AccessMode::Authorized(Arc::clone(&sp.policy)),
sp.credential_sources.clone(),
),
None => (
AccessMode::Authenticated,
vec![CredentialSource::AuthorizationHeader],
),
};
for source in &credential_sources {
if !credential_source_allowed(transport, source) {
return Err(CamelError::RouteError(format!(
"route '{route_id}': credential source {} is not supported on {} transport", source.variant_name(),
transport_name(transport)
)));
}
}
Ok(Some(RouteSecurityPlan {
access_mode,
provider_ref: Some(provider_ref),
transport,
credential_sources,
audience_binding,
}))
}
#[cfg(test)]
#[path = "route_compiler_ext_tests.rs"]
mod compile_route_security;