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 {
29 value: token.into(),
30 },
31 principal,
32 },
33 ])?;
34 Ok(Arc::new(camel_auth::StaticTokenAuthenticator::new(store)))
35}
36
37async fn keycloak_authenticator(
38 keycloak: &camel_config::config::KeycloakSecurityConfig,
39) -> Result<Arc<dyn camel_auth::TokenAuthenticator>, CamelError> {
40 let realm = camel_component_keycloak::KeycloakRealmConfig::new(
41 keycloak.server_url.clone(),
42 keycloak.realm.clone(),
43 keycloak.client_id.clone(),
44 )
45 .with_client_secret(keycloak.client_secret.clone());
46
47 match keycloak.validation.method.as_str() {
48 "local" => {
49 let jwks = Arc::new(
50 camel_auth::RemoteJwksProvider::new(realm.jwks_uri())
51 .await
52 .map_err(|e| CamelError::Config(e.to_string()))?,
53 );
54 let mapper = Arc::new(camel_auth::JsonPointerClaimsMapper::new(
55 camel_component_keycloak::keycloak_claim_paths(&keycloak.client_id),
56 ));
57 Ok(Arc::new(camel_auth::LocalJwtValidator::new(
58 keycloak.validation.audience.clone(),
59 realm.realm_url(),
60 jwks,
61 mapper,
62 )))
63 }
64 "introspection" => {
65 let opts = camel_auth::IntrospectionCacheOptions {
66 max_entries: keycloak.introspection.max_entries,
67 default_ttl: std::time::Duration::from_secs(
68 keycloak.introspection.default_ttl_secs,
69 ),
70 negative_ttl: std::time::Duration::from_secs(
71 keycloak.introspection.negative_ttl_secs,
72 ),
73 };
74 let auth = realm.introspection_authenticator(opts).await?;
75 Ok(Arc::new(auth))
76 }
77 other => Err(CamelError::Config(format!(
78 "unsupported security.keycloak.validation.method: {other}"
79 ))),
80 }
81}
82
83async fn resolve_authenticator(
89 security: &camel_config::config::SecurityConfig,
90) -> Result<Option<Arc<dyn camel_auth::TokenAuthenticator>>, CamelError> {
91 let has_keycloak = security.keycloak.is_some();
92 let has_oidc = security.oidc.is_some();
93 let has_native = security.native.is_some();
94
95 let count = [has_keycloak, has_oidc, has_native]
96 .iter()
97 .filter(|&&x| x)
98 .count();
99 if count > 1 {
100 return Err(CamelError::Config(
101 "configure only one of security.keycloak, security.oidc, security.native for route authentication"
102 .into(),
103 ));
104 }
105
106 if let Some(ref keycloak) = security.keycloak {
107 Ok(Some(keycloak_authenticator(keycloak).await?))
108 } else if let Some(ref native) = security.native {
109 Ok(Some(native_authenticator(native)?))
110 } else {
111 Ok(None)
113 }
114}
115
116async fn register_keycloak_uma_evaluator(
119 camel_config: &camel_config::config::CamelConfig,
120 evaluator_registry: &camel_auth::PermissionEvaluatorRegistry,
121) -> Result<(), CamelError> {
122 if let Some(ref keycloak) = camel_config.security.keycloak
123 && let Some(ref uma) = keycloak.uma
124 {
125 let realm = camel_component_keycloak::KeycloakRealmConfig::new(
126 keycloak.server_url.clone(),
127 keycloak.realm.clone(),
128 keycloak.client_id.clone(),
129 )
130 .with_client_secret(keycloak.client_secret.clone());
131 let evaluator = realm
132 .uma_evaluator()
133 .await
134 .map_err(|e| CamelError::Config(e.to_string()))?;
135 evaluator_registry.register(uma.provider.clone(), evaluator);
136 }
137 Ok(())
138}
139
140#[cfg(feature = "wasm")]
145pub async fn build_security_compile_context_from_config(
146 camel_config: &camel_config::config::CamelConfig,
147 registry: Arc<std::sync::Mutex<camel_core::Registry>>,
148) -> Result<SecurityCompileContext, CamelError> {
149 let authenticator = resolve_authenticator(&camel_config.security).await?;
150 let mut security_ctx = SecurityCompileContext::new(authenticator, None);
151
152 let evaluator_registry = camel_auth::PermissionEvaluatorRegistry::new();
153
154 if let Some(ref policies) = camel_config.security.policies {
155 let policy_registry =
156 camel_component_wasm::build_security_policy_registry(&policies.wasm, registry.clone())
157 .await
158 .map_err(|e| CamelError::Config(e.to_string()))?;
159 if !policy_registry.is_empty() {
160 security_ctx = security_ctx.with_security_policy_registry(Arc::new(policy_registry));
161 }
162 }
163
164 if let Some(ref permissions) = camel_config.security.permissions {
165 let wasm_registry = camel_component_wasm::build_permission_registry(permissions, registry)
166 .await
167 .map_err(|e| CamelError::Config(e.to_string()))?;
168 for (name, evaluator) in wasm_registry.entries() {
169 evaluator_registry.register(name, evaluator);
170 }
171 }
172
173 register_keycloak_uma_evaluator(camel_config, &evaluator_registry).await?;
174
175 if !evaluator_registry.is_empty() {
176 security_ctx = security_ctx.with_evaluator_registry(Arc::new(evaluator_registry));
177 }
178
179 Ok(security_ctx)
180}
181
182#[cfg(not(feature = "wasm"))]
183pub async fn build_security_compile_context_from_config(
184 camel_config: &camel_config::config::CamelConfig,
185 _registry: Arc<std::sync::Mutex<camel_core::Registry>>,
186) -> Result<SecurityCompileContext, CamelError> {
187 if camel_config.security.permissions.is_some() {
188 return Err(CamelError::Config(
189 "security.permissions requires camel-cli wasm feature".into(),
190 ));
191 }
192
193 if camel_config.security.policies.is_some() {
194 return Err(CamelError::Config(
195 "security.policies requires camel-cli wasm feature".into(),
196 ));
197 }
198
199 let authenticator = resolve_authenticator(&camel_config.security).await?;
200 let mut security_ctx = SecurityCompileContext::new(authenticator, None);
201
202 let evaluator_registry = camel_auth::PermissionEvaluatorRegistry::new();
203
204 register_keycloak_uma_evaluator(camel_config, &evaluator_registry).await?;
205
206 if !evaluator_registry.is_empty() {
207 security_ctx = security_ctx.with_evaluator_registry(Arc::new(evaluator_registry));
208 }
209
210 Ok(security_ctx)
211}
212
213#[cfg(test)]
218mod tests {
219 use std::sync::Arc;
220
221 #[tokio::test]
222 async fn native_static_token_builds_authenticator() {
223 let cfg: camel_config::config::CamelConfig = toml::from_str(
224 r#"
225 [security.native]
226 subject = "dev-user"
227 issuer = "native"
228 bearer_token = "dev-token"
229 roles = ["admin"]
230 scopes = ["read"]
231 "#,
232 )
233 .expect("config parses");
234
235 let registry = Arc::new(std::sync::Mutex::new(camel_core::Registry::new()));
236 let ctx = crate::build_security_compile_context_from_config(&cfg, registry)
237 .await
238 .expect("security context builds");
239
240 assert!(ctx.authenticator.is_some());
241 }
242
243 #[cfg(feature = "wasm")]
244 #[tokio::test]
245 async fn security_permissions_config_is_consumed_when_building_compile_context() {
246 let cfg: camel_config::config::CamelConfig = toml::from_str(
247 r#"
248 [security.permissions.invoice-policy]
249 provider = "wasm"
250 "#,
251 )
252 .expect("config parses");
253
254 let registry = Arc::new(std::sync::Mutex::new(camel_core::Registry::new()));
255 let err = match crate::build_security_compile_context_from_config(&cfg, registry).await {
256 Ok(_) => {
257 panic!("wasm permission provider without path must fail during registry build")
258 }
259 Err(err) => err,
260 };
261
262 assert!(
263 err.to_string().contains("requires 'path'"),
264 "unexpected error: {err}"
265 );
266 }
267
268 #[tokio::test]
269 async fn multiple_auth_providers_returns_config_error() {
270 let cfg: camel_config::config::CamelConfig = toml::from_str(
271 r#"
272 [security.keycloak]
273 server_url = "https://kc.example.com"
274 realm = "camel"
275 client_id = "camel-api"
276 client_secret = "secret"
277
278 [security.native]
279 subject = "dev-user"
280 issuer = "native"
281 bearer_token = "dev-token"
282 "#,
283 )
284 .expect("config parses");
285
286 let registry = Arc::new(std::sync::Mutex::new(camel_core::Registry::new()));
287 let err = match crate::build_security_compile_context_from_config(&cfg, registry).await {
288 Ok(_) => panic!("multiple providers should fail"),
289 Err(err) => err,
290 };
291
292 assert!(
293 err.to_string().contains("configure only one"),
294 "unexpected error: {err}"
295 );
296 }
297}