1pub mod commands;
2pub mod template;
3
4use camel_api::CamelError;
5use camel_dsl::SecurityCompileContext;
6use std::sync::Arc;
7
8macro_rules! register_bundle_empty {
17 ($ctx:expr, $Bundle:ty) => {{
18 let key = <$Bundle as camel_component_api::ComponentBundle>::config_key();
19 match <$Bundle as camel_component_api::ComponentBundle>::from_toml(
20 ::toml::Value::Table(::toml::map::Map::new()),
21 ) {
22 Ok(bundle) => {
23 <$Bundle as camel_component_api::ComponentBundle>::register_all(
24 bundle,
25 &mut *$ctx,
26 );
27 }
28 Err(err) => {
29 tracing::warn!(
30 bundle = %key,
31 error = %err,
32 "lint catalog: empty-config build failed; skipping bundle (surfaces as unverified-scheme)"
33 );
34 }
35 }
36 }};
37}
38
39macro_rules! register_datasource_bundle_empty {
43 ($ctx:expr, $Bundle:ty, $catalog:expr) => {{
44 let key = <$Bundle as camel_component_api::ComponentBundle>::config_key();
45 match <$Bundle as camel_component_api::ComponentBundle>::from_toml(
46 ::toml::Value::Table(::toml::map::Map::new()),
47 ) {
48 Ok(bundle) => {
49 let bundle = bundle.with_catalog(::std::sync::Arc::clone(&$catalog));
50 <$Bundle as camel_component_api::ComponentBundle>::register_all(
51 bundle,
52 &mut *$ctx,
53 );
54 }
55 Err(err) => {
56 tracing::warn!(
57 bundle = %key,
58 error = %err,
59 "lint catalog: empty-config build failed; skipping datasource bundle (surfaces as unverified-scheme)"
60 );
61 }
62 }
63 }};
64}
65
66pub fn register_builtin_components_for_lint(ctx: &mut camel_core::CamelContext) {
89 use camel_api::datasource::DatasourceCatalog;
90 use camel_core::datasource::RuntimeDatasourceCatalog;
91
92 ctx.register_component(camel_component_timer::TimerComponent::new());
94 ctx.register_component(camel_component_cron::CronComponent::new());
95 ctx.register_component(camel_component_log::LogComponent::new());
96 ctx.register_component(camel_component_direct::DirectComponent::new());
97 ctx.register_component(camel_component_seda::SedaComponent::new());
98 ctx.register_component(camel_component_mock::MockComponent::new());
99 ctx.register_component(camel_component_controlbus::ControlBusComponent::new());
100
101 ctx.register_component(camel_component_validator::ValidatorComponent::new());
104 ctx.register_component(camel_xslt::XsltComponent::default());
107 ctx.register_component(camel_xj::XjComponent::default());
108
109 let datasource_catalog: Arc<dyn DatasourceCatalog> = Arc::new(RuntimeDatasourceCatalog::new(
111 std::collections::HashMap::new(),
112 ));
113
114 register_bundle_empty!(ctx, camel_component_http::HttpBundle);
116 #[cfg(feature = "http-static")]
117 register_bundle_empty!(ctx, camel_component_http::HttpStaticBundle);
118 register_bundle_empty!(ctx, camel_component_ws::WsBundle);
119 register_bundle_empty!(ctx, camel_component_file::FileBundle);
120 register_bundle_empty!(ctx, camel_component_container::ContainerBundle);
121 register_bundle_empty!(ctx, camel_template::TemplateBundle);
122 register_bundle_empty!(ctx, camel_master::MasterBundle);
123 register_bundle_empty!(ctx, camel_component_opensearch::OpenSearchBundle);
124 register_bundle_empty!(ctx, camel_component_redis::RedisBundle);
125
126 register_bundle_empty!(ctx, camel_component_jms::JmsBundle);
129 register_bundle_empty!(ctx, camel_component_cxf::CxfBundle);
130
131 register_datasource_bundle_empty!(ctx, camel_component_sql::SqlBundle, datasource_catalog);
133 #[cfg(feature = "surrealdb")]
134 register_datasource_bundle_empty!(
135 ctx,
136 camel_component_surrealdb::SurrealDbBundle,
137 datasource_catalog
138 );
139
140 #[cfg(feature = "kafka")]
142 register_bundle_empty!(ctx, camel_component_kafka::KafkaBundle);
143 #[cfg(feature = "mqtt")]
144 register_bundle_empty!(ctx, camel_component_mqtt::MqttBundle);
145 #[cfg(feature = "grpc")]
146 register_bundle_empty!(ctx, camel_component_grpc::GrpcBundle);
147 #[cfg(feature = "llm")]
148 register_bundle_empty!(ctx, camel_component_llm::LlmBundle);
149
150 }
155
156fn native_authenticator(
161 native: &camel_config::config::NativeAuthConfig,
162) -> Result<Arc<dyn camel_auth::TokenAuthenticator>, CamelError> {
163 let token = native.bearer_token.clone().ok_or_else(|| {
164 CamelError::Config("security.native.bearer_token is required for route auth".into())
165 })?;
166 let principal = camel_api::security_policy::Principal {
167 subject: native.subject.clone(),
168 issuer: native.issuer.clone().unwrap_or_else(|| "native".into()),
169 audience: Vec::new(),
170 roles: native.roles.clone(),
171 scopes: native.scopes.clone(),
172 claims: serde_json::Value::Object(serde_json::Map::new()),
173 };
174 let store = camel_auth::native_auth::NativeCredentialStore::try_new(vec![
175 camel_auth::NativeCredential {
176 secret: camel_auth::NativeCredentialSecret::Plaintext {
177 value: token.into(),
178 },
179 principal,
180 },
181 ])?;
182 Ok(Arc::new(camel_auth::StaticTokenAuthenticator::new(store)))
183}
184
185async fn keycloak_authenticator(
186 keycloak: &camel_config::config::KeycloakSecurityConfig,
187) -> Result<Arc<dyn camel_auth::TokenAuthenticator>, CamelError> {
188 let realm = camel_component_keycloak::KeycloakRealmConfig::new(
189 keycloak.server_url.clone(),
190 keycloak.realm.clone(),
191 keycloak.client_id.clone(),
192 )
193 .with_client_secret(keycloak.client_secret.clone())
194 .with_allow_internal(keycloak.allow_internal);
195
196 match keycloak.validation.method.as_str() {
197 "local" => {
198 let jwks = Arc::new(
199 camel_auth::RemoteJwksProvider::new(realm.jwks_uri(), realm.policy())
200 .await
201 .map_err(|e| CamelError::Config(e.to_string()))?,
202 );
203 let mapper = Arc::new(camel_auth::JsonPointerClaimsMapper::new(
204 camel_component_keycloak::keycloak_claim_paths(&keycloak.client_id),
205 ));
206 Ok(Arc::new(camel_auth::LocalJwtValidator::new(
207 keycloak.validation.audience.clone(),
208 realm.realm_url(),
209 jwks,
210 mapper,
211 )))
212 }
213 "introspection" => {
214 let opts = camel_auth::IntrospectionCacheOptions {
215 max_entries: keycloak.introspection.max_entries,
216 default_ttl: std::time::Duration::from_secs(
217 keycloak.introspection.default_ttl_secs,
218 ),
219 negative_ttl: std::time::Duration::from_secs(
220 keycloak.introspection.negative_ttl_secs,
221 ),
222 };
223 let auth = realm.introspection_authenticator(opts).await?;
224 Ok(Arc::new(auth))
225 }
226 other => Err(CamelError::Config(format!(
227 "unsupported security.keycloak.validation.method: {other}"
228 ))),
229 }
230}
231
232async fn resolve_authenticator(
238 security: &camel_config::config::SecurityConfig,
239) -> Result<Option<Arc<dyn camel_auth::TokenAuthenticator>>, CamelError> {
240 let has_keycloak = security.keycloak.is_some();
241 let has_oidc = security.oidc.is_some();
242 let has_native = security.native.is_some();
243
244 let count = [has_keycloak, has_oidc, has_native]
245 .iter()
246 .filter(|&&x| x)
247 .count();
248 if count > 1 {
249 return Err(CamelError::Config(
250 "configure only one of security.keycloak, security.oidc, security.native for route authentication"
251 .into(),
252 ));
253 }
254
255 if let Some(ref keycloak) = security.keycloak {
256 Ok(Some(keycloak_authenticator(keycloak).await?))
257 } else if let Some(ref native) = security.native {
258 Ok(Some(native_authenticator(native)?))
259 } else {
260 Ok(None)
262 }
263}
264
265async fn register_keycloak_uma_evaluator(
268 camel_config: &camel_config::config::CamelConfig,
269 evaluator_registry: &camel_auth::PermissionEvaluatorRegistry,
270) -> Result<(), CamelError> {
271 if let Some(ref keycloak) = camel_config.security.keycloak
272 && let Some(ref uma) = keycloak.uma
273 {
274 let realm = camel_component_keycloak::KeycloakRealmConfig::new(
275 keycloak.server_url.clone(),
276 keycloak.realm.clone(),
277 keycloak.client_id.clone(),
278 )
279 .with_client_secret(keycloak.client_secret.clone())
280 .with_allow_internal(keycloak.allow_internal);
281 let evaluator = realm
282 .uma_evaluator()
283 .await
284 .map_err(|e| CamelError::Config(e.to_string()))?;
285 evaluator_registry.register(uma.provider.clone(), evaluator);
286 }
287 Ok(())
288}
289
290#[cfg(feature = "wasm")]
295pub async fn build_security_compile_context_from_config(
296 camel_config: &camel_config::config::CamelConfig,
297 registry: Arc<std::sync::Mutex<camel_core::Registry>>,
298) -> Result<SecurityCompileContext, CamelError> {
299 let wasm_ctx: Arc<dyn camel_component_api::ComponentContext> =
300 Arc::new(camel_core::RegistryComponentContext::new(registry));
301 let authenticator = resolve_authenticator(&camel_config.security).await?;
302 let mut security_ctx = SecurityCompileContext::new(authenticator, None);
303
304 let evaluator_registry = camel_auth::PermissionEvaluatorRegistry::new();
305
306 if let Some(ref policies) = camel_config.security.policies {
307 let policy_registry =
308 camel_component_wasm::build_security_policy_registry(&policies.wasm, wasm_ctx.clone())
309 .await
310 .map_err(|e| CamelError::Config(e.to_string()))?;
311 if !policy_registry.is_empty() {
312 security_ctx = security_ctx.with_security_policy_registry(Arc::new(policy_registry));
313 }
314 }
315
316 if let Some(ref permissions) = camel_config.security.permissions {
317 let wasm_registry = camel_component_wasm::build_permission_registry(permissions, wasm_ctx)
318 .await
319 .map_err(|e| CamelError::Config(e.to_string()))?;
320 for (name, evaluator) in wasm_registry.entries() {
321 evaluator_registry.register(name, evaluator);
322 }
323 }
324
325 register_keycloak_uma_evaluator(camel_config, &evaluator_registry).await?;
326
327 if !evaluator_registry.is_empty() {
328 security_ctx = security_ctx.with_evaluator_registry(Arc::new(evaluator_registry));
329 }
330
331 Ok(security_ctx)
332}
333
334#[cfg(not(feature = "wasm"))]
335pub async fn build_security_compile_context_from_config(
336 camel_config: &camel_config::config::CamelConfig,
337 _registry: Arc<std::sync::Mutex<camel_core::Registry>>,
338) -> Result<SecurityCompileContext, CamelError> {
339 if camel_config.security.permissions.is_some() {
340 return Err(CamelError::Config(
341 "security.permissions requires camel-cli wasm feature".into(),
342 ));
343 }
344
345 if camel_config.security.policies.is_some() {
346 return Err(CamelError::Config(
347 "security.policies requires camel-cli wasm feature".into(),
348 ));
349 }
350
351 let authenticator = resolve_authenticator(&camel_config.security).await?;
352 let mut security_ctx = SecurityCompileContext::new(authenticator, None);
353
354 let evaluator_registry = camel_auth::PermissionEvaluatorRegistry::new();
355
356 register_keycloak_uma_evaluator(camel_config, &evaluator_registry).await?;
357
358 if !evaluator_registry.is_empty() {
359 security_ctx = security_ctx.with_evaluator_registry(Arc::new(evaluator_registry));
360 }
361
362 Ok(security_ctx)
363}
364
365#[cfg(test)]
370mod tests {
371 use std::sync::Arc;
372
373 #[tokio::test]
374 async fn native_static_token_builds_authenticator() {
375 let cfg: camel_config::config::CamelConfig = toml::from_str(
376 r#"
377 [security.native]
378 subject = "dev-user"
379 issuer = "native"
380 bearer_token = "dev-token"
381 roles = ["admin"]
382 scopes = ["read"]
383 "#,
384 )
385 .expect("config parses");
386
387 let registry = Arc::new(std::sync::Mutex::new(camel_core::Registry::new()));
388 let ctx = crate::build_security_compile_context_from_config(&cfg, registry)
389 .await
390 .expect("security context builds");
391
392 assert!(ctx.authenticator.is_some());
393 }
394
395 #[cfg(feature = "wasm")]
396 #[tokio::test]
397 async fn security_permissions_config_is_consumed_when_building_compile_context() {
398 let cfg: camel_config::config::CamelConfig = toml::from_str(
399 r#"
400 [security.permissions.invoice-policy]
401 provider = "wasm"
402 "#,
403 )
404 .expect("config parses");
405
406 let registry = Arc::new(std::sync::Mutex::new(camel_core::Registry::new()));
407 let err = match crate::build_security_compile_context_from_config(&cfg, registry).await {
408 Ok(_) => {
409 panic!("wasm permission provider without path must fail during registry build")
410 }
411 Err(err) => err,
412 };
413
414 assert!(
415 err.to_string().contains("requires 'path'"),
416 "unexpected error: {err}"
417 );
418 }
419
420 #[tokio::test]
421 async fn multiple_auth_providers_returns_config_error() {
422 let cfg: camel_config::config::CamelConfig = toml::from_str(
423 r#"
424 [security.keycloak]
425 server_url = "https://kc.example.com"
426 realm = "camel"
427 client_id = "camel-api"
428 client_secret = "secret"
429
430 [security.native]
431 subject = "dev-user"
432 issuer = "native"
433 bearer_token = "dev-token"
434 "#,
435 )
436 .expect("config parses");
437
438 let registry = Arc::new(std::sync::Mutex::new(camel_core::Registry::new()));
439 let err = match crate::build_security_compile_context_from_config(&cfg, registry).await {
440 Ok(_) => panic!("multiple providers should fail"),
441 Err(err) => err,
442 };
443
444 assert!(
445 err.to_string().contains("configure only one"),
446 "unexpected error: {err}"
447 );
448 }
449}