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 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// ---------------------------------------------------------------------------
194// Tests
195// ---------------------------------------------------------------------------
196
197#[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}