Skip to main content

camel_cli/
lib.rs

1pub mod commands;
2pub mod template;
3
4use camel_api::CamelError;
5use camel_dsl::SecurityCompileContext;
6use std::sync::Arc;
7
8// ---------------------------------------------------------------------------
9// Auth helpers — shared between wasm and non-wasm build paths
10// ---------------------------------------------------------------------------
11
12fn 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
80/// Resolve the authenticator from `[security.*]`.
81///
82/// Chooses at most one of `keycloak`, `oidc`, `native`.  Returns `None` if
83/// none is configured (anonymous routes are allowed).  Errors if more than
84/// one is present.
85fn 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        // oidc alone: leave authenticator None for now (scope creep avoidance)
109        Ok(None)
110    }
111}
112
113/// Register Keycloak UMA permission evaluator from `[security.keycloak.uma]`
114/// config.  No-ops when no UMA config is present.
115fn 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// ---------------------------------------------------------------------------
137// Public entry-point (cfg-gated) — matches the existing signature
138// ---------------------------------------------------------------------------
139
140#[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// ---------------------------------------------------------------------------
210// Tests
211// ---------------------------------------------------------------------------
212
213#[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}