1pub mod commands;
2pub mod template;
3
4use camel_api::CamelError;
5use camel_dsl::SecurityCompileContext;
6use std::sync::Arc;
7
8fn native_authenticator(
13 native: &camel_config::config::NativeAuthConfig,
14) -> Result<Arc<dyn camel_auth::TokenAuthenticator>, CamelError> {
15 let token = native.bearer_token.clone().ok_or_else(|| {
16 CamelError::Config("security.native.bearer_token is required for route auth".into())
17 })?;
18 let principal = camel_api::security_policy::Principal {
19 subject: native.subject.clone(),
20 issuer: native.issuer.clone().unwrap_or_else(|| "native".into()),
21 audience: Vec::new(),
22 roles: native.roles.clone(),
23 scopes: native.scopes.clone(),
24 claims: serde_json::Value::Object(serde_json::Map::new()),
25 };
26 let store = camel_auth::native_auth::NativeCredentialStore::try_new(vec![
27 camel_auth::NativeCredential {
28 secret: camel_auth::NativeCredentialSecret::Plaintext { value: token },
29 principal,
30 },
31 ])?;
32 Ok(Arc::new(camel_auth::StaticTokenAuthenticator::new(store)))
33}
34
35fn keycloak_authenticator(
36 keycloak: &camel_config::config::KeycloakSecurityConfig,
37) -> Result<Arc<dyn camel_auth::TokenAuthenticator>, CamelError> {
38 let realm = camel_component_keycloak::KeycloakRealmConfig::new(
39 keycloak.server_url.clone(),
40 keycloak.realm.clone(),
41 keycloak.client_id.clone(),
42 )
43 .with_client_secret(keycloak.client_secret.clone());
44
45 match keycloak.validation.method.as_str() {
46 "local" => {
47 let jwks = Arc::new(
48 camel_auth::RemoteJwksProvider::new(realm.jwks_uri())
49 .map_err(|e| CamelError::Config(e.to_string()))?,
50 );
51 let mapper = Arc::new(camel_auth::JsonPointerClaimsMapper::new(
52 camel_component_keycloak::keycloak_claim_paths(&keycloak.client_id),
53 ));
54 Ok(Arc::new(camel_auth::LocalJwtValidator::new(
55 keycloak.validation.audience.clone(),
56 realm.realm_url(),
57 jwks,
58 mapper,
59 )))
60 }
61 "introspection" => {
62 let opts = camel_auth::IntrospectionCacheOptions {
63 max_entries: keycloak.introspection.max_entries,
64 default_ttl: std::time::Duration::from_secs(
65 keycloak.introspection.default_ttl_secs,
66 ),
67 negative_ttl: std::time::Duration::from_secs(
68 keycloak.introspection.negative_ttl_secs,
69 ),
70 };
71 let auth = realm.introspection_authenticator(opts)?;
72 Ok(Arc::new(auth))
73 }
74 other => Err(CamelError::Config(format!(
75 "unsupported security.keycloak.validation.method: {other}"
76 ))),
77 }
78}
79
80fn resolve_authenticator(
86 security: &camel_config::config::SecurityConfig,
87) -> Result<Option<Arc<dyn camel_auth::TokenAuthenticator>>, CamelError> {
88 let has_keycloak = security.keycloak.is_some();
89 let has_oidc = security.oidc.is_some();
90 let has_native = security.native.is_some();
91
92 let count = [has_keycloak, has_oidc, has_native]
93 .iter()
94 .filter(|&&x| x)
95 .count();
96 if count > 1 {
97 return Err(CamelError::Config(
98 "configure only one of security.keycloak, security.oidc, security.native for route authentication"
99 .into(),
100 ));
101 }
102
103 if let Some(ref keycloak) = security.keycloak {
104 Ok(Some(keycloak_authenticator(keycloak)?))
105 } else if let Some(ref native) = security.native {
106 Ok(Some(native_authenticator(native)?))
107 } else {
108 Ok(None)
110 }
111}
112
113fn register_keycloak_uma_evaluator(
116 camel_config: &camel_config::config::CamelConfig,
117 evaluator_registry: &camel_auth::PermissionEvaluatorRegistry,
118) -> Result<(), CamelError> {
119 if let Some(ref keycloak) = camel_config.security.keycloak
120 && let Some(ref uma) = keycloak.uma
121 {
122 let realm = camel_component_keycloak::KeycloakRealmConfig::new(
123 keycloak.server_url.clone(),
124 keycloak.realm.clone(),
125 keycloak.client_id.clone(),
126 )
127 .with_client_secret(keycloak.client_secret.clone());
128 let evaluator = realm
129 .uma_evaluator()
130 .map_err(|e| CamelError::Config(e.to_string()))?;
131 evaluator_registry.register(uma.provider.clone(), evaluator);
132 }
133 Ok(())
134}
135
136#[cfg(feature = "wasm")]
141pub async fn build_security_compile_context_from_config(
142 camel_config: &camel_config::config::CamelConfig,
143 registry: Arc<std::sync::Mutex<camel_core::Registry>>,
144) -> Result<SecurityCompileContext, CamelError> {
145 let authenticator = resolve_authenticator(&camel_config.security)?;
146 let mut security_ctx = SecurityCompileContext::new(authenticator, None);
147
148 let evaluator_registry = camel_auth::PermissionEvaluatorRegistry::new();
149
150 if let Some(ref policies) = camel_config.security.policies {
151 let policy_registry =
152 camel_component_wasm::build_security_policy_registry(&policies.wasm, registry.clone())
153 .await
154 .map_err(|e| CamelError::Config(e.to_string()))?;
155 if !policy_registry.is_empty() {
156 security_ctx = security_ctx.with_security_policy_registry(Arc::new(policy_registry));
157 }
158 }
159
160 if let Some(ref permissions) = camel_config.security.permissions {
161 let wasm_registry = camel_component_wasm::build_permission_registry(permissions, registry)
162 .await
163 .map_err(|e| CamelError::Config(e.to_string()))?;
164 for (name, evaluator) in wasm_registry.entries() {
165 evaluator_registry.register(name, evaluator);
166 }
167 }
168
169 register_keycloak_uma_evaluator(camel_config, &evaluator_registry)?;
170
171 if !evaluator_registry.is_empty() {
172 security_ctx = security_ctx.with_evaluator_registry(Arc::new(evaluator_registry));
173 }
174
175 Ok(security_ctx)
176}
177
178#[cfg(not(feature = "wasm"))]
179pub async fn build_security_compile_context_from_config(
180 camel_config: &camel_config::config::CamelConfig,
181 _registry: Arc<std::sync::Mutex<camel_core::Registry>>,
182) -> Result<SecurityCompileContext, CamelError> {
183 if camel_config.security.permissions.is_some() {
184 return Err(CamelError::Config(
185 "security.permissions requires camel-cli wasm feature".into(),
186 ));
187 }
188
189 if camel_config.security.policies.is_some() {
190 return Err(CamelError::Config(
191 "security.policies requires camel-cli wasm feature".into(),
192 ));
193 }
194
195 let authenticator = resolve_authenticator(&camel_config.security)?;
196 let mut security_ctx = SecurityCompileContext::new(authenticator, None);
197
198 let evaluator_registry = camel_auth::PermissionEvaluatorRegistry::new();
199
200 register_keycloak_uma_evaluator(camel_config, &evaluator_registry)?;
201
202 if !evaluator_registry.is_empty() {
203 security_ctx = security_ctx.with_evaluator_registry(Arc::new(evaluator_registry));
204 }
205
206 Ok(security_ctx)
207}
208
209#[cfg(test)]
214mod tests {
215 use std::sync::Arc;
216
217 #[tokio::test]
218 async fn keycloak_local_validation_builds_authenticator() {
219 let cfg: camel_config::config::CamelConfig = toml::from_str(
220 r#"
221 [security.keycloak]
222 server_url = "https://kc.example.com"
223 realm = "camel"
224 client_id = "camel-api"
225 client_secret = "secret"
226
227 [security.keycloak.validation]
228 method = "local"
229 audience = ["camel-api"]
230 "#,
231 )
232 .expect("config parses");
233
234 let registry = Arc::new(std::sync::Mutex::new(camel_core::Registry::new()));
235 let ctx = crate::build_security_compile_context_from_config(&cfg, registry)
236 .await
237 .expect("security context builds");
238
239 assert!(ctx.authenticator.is_some());
240 }
241
242 #[tokio::test]
243 async fn native_static_token_builds_authenticator() {
244 let cfg: camel_config::config::CamelConfig = toml::from_str(
245 r#"
246 [security.native]
247 subject = "dev-user"
248 issuer = "native"
249 bearer_token = "dev-token"
250 roles = ["admin"]
251 scopes = ["read"]
252 "#,
253 )
254 .expect("config parses");
255
256 let registry = Arc::new(std::sync::Mutex::new(camel_core::Registry::new()));
257 let ctx = crate::build_security_compile_context_from_config(&cfg, registry)
258 .await
259 .expect("security context builds");
260
261 assert!(ctx.authenticator.is_some());
262 }
263
264 #[cfg(feature = "wasm")]
265 #[tokio::test]
266 async fn security_permissions_config_is_consumed_when_building_compile_context() {
267 let cfg: camel_config::config::CamelConfig = toml::from_str(
268 r#"
269 [security.permissions.invoice-policy]
270 provider = "wasm"
271 "#,
272 )
273 .expect("config parses");
274
275 let registry = Arc::new(std::sync::Mutex::new(camel_core::Registry::new()));
276 let err = match crate::build_security_compile_context_from_config(&cfg, registry).await {
277 Ok(_) => {
278 panic!("wasm permission provider without path must fail during registry build")
279 }
280 Err(err) => err,
281 };
282
283 assert!(
284 err.to_string().contains("requires 'path'"),
285 "unexpected error: {err}"
286 );
287 }
288
289 #[tokio::test]
290 async fn multiple_auth_providers_returns_config_error() {
291 let cfg: camel_config::config::CamelConfig = toml::from_str(
292 r#"
293 [security.keycloak]
294 server_url = "https://kc.example.com"
295 realm = "camel"
296 client_id = "camel-api"
297 client_secret = "secret"
298
299 [security.native]
300 subject = "dev-user"
301 issuer = "native"
302 bearer_token = "dev-token"
303 "#,
304 )
305 .expect("config parses");
306
307 let registry = Arc::new(std::sync::Mutex::new(camel_core::Registry::new()));
308 let err = match crate::build_security_compile_context_from_config(&cfg, registry).await {
309 Ok(_) => panic!("multiple providers should fail"),
310 Err(err) => err,
311 };
312
313 assert!(
314 err.to_string().contains("configure only one"),
315 "unexpected error: {err}"
316 );
317 }
318
319 #[tokio::test]
320 async fn keycloak_introspection_builds_authenticator() {
321 let cfg: camel_config::config::CamelConfig = toml::from_str(
322 r#"
323 [security.keycloak]
324 server_url = "https://kc.example.com"
325 realm = "camel"
326 client_id = "camel-api"
327 client_secret = "secret"
328
329 [security.keycloak.validation]
330 method = "introspection"
331 "#,
332 )
333 .expect("config parses");
334
335 let registry = Arc::new(std::sync::Mutex::new(camel_core::Registry::new()));
336 let ctx = crate::build_security_compile_context_from_config(&cfg, registry)
337 .await
338 .expect("security context builds");
339
340 assert!(ctx.authenticator.is_some());
341 }
342
343 #[tokio::test]
344 async fn keycloak_uma_registers_permission_evaluator() {
345 let cfg: camel_config::config::CamelConfig = toml::from_str(
346 r#"
347 [security.keycloak]
348 server_url = "https://kc.example.com"
349 realm = "camel"
350 client_id = "authz-client"
351 client_secret = "secret"
352
353 [security.keycloak.uma]
354 provider = "keycloak-uma"
355 "#,
356 )
357 .expect("config parses");
358
359 let registry = Arc::new(std::sync::Mutex::new(camel_core::Registry::new()));
360 let ctx = crate::build_security_compile_context_from_config(&cfg, registry)
361 .await
362 .expect("security context builds");
363
364 let evaluators = ctx.evaluator_registry.expect("evaluator registry");
365 assert!(evaluators.get("keycloak-uma").is_some());
366 }
367}