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 permissions) = camel_config.security.permissions {
151 let wasm_registry = camel_component_wasm::build_permission_registry(permissions, registry)
152 .await
153 .map_err(|e| CamelError::Config(e.to_string()))?;
154 for (name, evaluator) in wasm_registry.entries() {
155 evaluator_registry.register(name, evaluator);
156 }
157 }
158
159 register_keycloak_uma_evaluator(camel_config, &evaluator_registry)?;
160
161 if !evaluator_registry.is_empty() {
162 security_ctx = security_ctx.with_evaluator_registry(Arc::new(evaluator_registry));
163 }
164
165 Ok(security_ctx)
166}
167
168#[cfg(not(feature = "wasm"))]
169pub async fn build_security_compile_context_from_config(
170 camel_config: &camel_config::config::CamelConfig,
171 _registry: Arc<std::sync::Mutex<camel_core::Registry>>,
172) -> Result<SecurityCompileContext, CamelError> {
173 if camel_config.security.permissions.is_some() {
174 return Err(CamelError::Config(
175 "security.permissions requires camel-cli wasm feature".into(),
176 ));
177 }
178
179 let authenticator = resolve_authenticator(&camel_config.security)?;
180 let mut security_ctx = SecurityCompileContext::new(authenticator, None);
181
182 let evaluator_registry = camel_auth::PermissionEvaluatorRegistry::new();
183
184 register_keycloak_uma_evaluator(camel_config, &evaluator_registry)?;
185
186 if !evaluator_registry.is_empty() {
187 security_ctx = security_ctx.with_evaluator_registry(Arc::new(evaluator_registry));
188 }
189
190 Ok(security_ctx)
191}
192
193#[cfg(test)]
198mod tests {
199 use std::sync::Arc;
200
201 #[tokio::test]
202 async fn keycloak_local_validation_builds_authenticator() {
203 let cfg: camel_config::config::CamelConfig = toml::from_str(
204 r#"
205 [security.keycloak]
206 server_url = "https://kc.example.com"
207 realm = "camel"
208 client_id = "camel-api"
209 client_secret = "secret"
210
211 [security.keycloak.validation]
212 method = "local"
213 audience = ["camel-api"]
214 "#,
215 )
216 .expect("config parses");
217
218 let registry = Arc::new(std::sync::Mutex::new(camel_core::Registry::new()));
219 let ctx = crate::build_security_compile_context_from_config(&cfg, registry)
220 .await
221 .expect("security context builds");
222
223 assert!(ctx.authenticator.is_some());
224 }
225
226 #[tokio::test]
227 async fn native_static_token_builds_authenticator() {
228 let cfg: camel_config::config::CamelConfig = toml::from_str(
229 r#"
230 [security.native]
231 subject = "dev-user"
232 issuer = "native"
233 bearer_token = "dev-token"
234 roles = ["admin"]
235 scopes = ["read"]
236 "#,
237 )
238 .expect("config parses");
239
240 let registry = Arc::new(std::sync::Mutex::new(camel_core::Registry::new()));
241 let ctx = crate::build_security_compile_context_from_config(&cfg, registry)
242 .await
243 .expect("security context builds");
244
245 assert!(ctx.authenticator.is_some());
246 }
247
248 #[cfg(feature = "wasm")]
249 #[tokio::test]
250 async fn security_permissions_config_is_consumed_when_building_compile_context() {
251 let cfg: camel_config::config::CamelConfig = toml::from_str(
252 r#"
253 [security.permissions.invoice-policy]
254 provider = "wasm"
255 "#,
256 )
257 .expect("config parses");
258
259 let registry = Arc::new(std::sync::Mutex::new(camel_core::Registry::new()));
260 let err = match crate::build_security_compile_context_from_config(&cfg, registry).await {
261 Ok(_) => {
262 panic!("wasm permission provider without path must fail during registry build")
263 }
264 Err(err) => err,
265 };
266
267 assert!(
268 err.to_string().contains("requires 'path'"),
269 "unexpected error: {err}"
270 );
271 }
272
273 #[tokio::test]
274 async fn multiple_auth_providers_returns_config_error() {
275 let cfg: camel_config::config::CamelConfig = toml::from_str(
276 r#"
277 [security.keycloak]
278 server_url = "https://kc.example.com"
279 realm = "camel"
280 client_id = "camel-api"
281 client_secret = "secret"
282
283 [security.native]
284 subject = "dev-user"
285 issuer = "native"
286 bearer_token = "dev-token"
287 "#,
288 )
289 .expect("config parses");
290
291 let registry = Arc::new(std::sync::Mutex::new(camel_core::Registry::new()));
292 let err = match crate::build_security_compile_context_from_config(&cfg, registry).await {
293 Ok(_) => panic!("multiple providers should fail"),
294 Err(err) => err,
295 };
296
297 assert!(
298 err.to_string().contains("configure only one"),
299 "unexpected error: {err}"
300 );
301 }
302
303 #[tokio::test]
304 async fn keycloak_introspection_builds_authenticator() {
305 let cfg: camel_config::config::CamelConfig = toml::from_str(
306 r#"
307 [security.keycloak]
308 server_url = "https://kc.example.com"
309 realm = "camel"
310 client_id = "camel-api"
311 client_secret = "secret"
312
313 [security.keycloak.validation]
314 method = "introspection"
315 "#,
316 )
317 .expect("config parses");
318
319 let registry = Arc::new(std::sync::Mutex::new(camel_core::Registry::new()));
320 let ctx = crate::build_security_compile_context_from_config(&cfg, registry)
321 .await
322 .expect("security context builds");
323
324 assert!(ctx.authenticator.is_some());
325 }
326
327 #[tokio::test]
328 async fn keycloak_uma_registers_permission_evaluator() {
329 let cfg: camel_config::config::CamelConfig = toml::from_str(
330 r#"
331 [security.keycloak]
332 server_url = "https://kc.example.com"
333 realm = "camel"
334 client_id = "authz-client"
335 client_secret = "secret"
336
337 [security.keycloak.uma]
338 provider = "keycloak-uma"
339 "#,
340 )
341 .expect("config parses");
342
343 let registry = Arc::new(std::sync::Mutex::new(camel_core::Registry::new()));
344 let ctx = crate::build_security_compile_context_from_config(&cfg, registry)
345 .await
346 .expect("security context builds");
347
348 let evaluators = ctx.evaluator_registry.expect("evaluator registry");
349 assert!(evaluators.get("keycloak-uma").is_some());
350 }
351}