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