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