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 {
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
84/// Resolve the authenticator from `[security.*]`.
85///
86/// Chooses at most one of `keycloak`, `oidc`, `native`.  Returns `None` if
87/// none is configured (anonymous routes are allowed).  Errors if more than
88/// one is present.
89async 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        // oidc alone: leave authenticator None for now (scope creep avoidance)
113        Ok(None)
114    }
115}
116
117/// Register Keycloak UMA permission evaluator from `[security.keycloak.uma]`
118/// config.  No-ops when no UMA config is present.
119async 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// ---------------------------------------------------------------------------
143// Public entry-point (cfg-gated) — matches the existing signature
144// ---------------------------------------------------------------------------
145
146#[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// ---------------------------------------------------------------------------
216// Tests
217// ---------------------------------------------------------------------------
218
219#[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}