This is an automated email from the ASF dual-hosted git repository.

oscerd pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/camel.git


The following commit(s) were added to refs/heads/main by this push:
     new 672d94a894a8 CAMEL-24830: camel-opa - add in-process WASM evaluation 
mode to OpaSecurityPolicy (#26670)
672d94a894a8 is described below

commit 672d94a894a8932550b98a6363d8554bcd1289fc
Author: Andrea Cosentino <[email protected]>
AuthorDate: Fri Sep 25 11:04:48 2026 +0200

    CAMEL-24830: camel-opa - add in-process WASM evaluation mode to 
OpaSecurityPolicy (#26670)
    
    * CAMEL-24830: camel-opa - add in-process WASM evaluation mode to 
OpaSecurityPolicy
    
    OpaSecurityPolicy could only use the REST evaluator, so a route that wanted 
the
    in-process WASM engine - the mode to prefer for a hot path such as 
authorizing an
    AI tool call - had to fall back to interceptFrom plus the opa: producer and 
a
    choice/stop. The policy now takes evaluationMode/policyBundle (and
    entrypoint/poolSize/borrowTimeout) and builds an OpaWasmEvaluator in wasm 
mode.
    
    The build of a wasm evaluator - validation, the entrypoint default, loading 
the
    bundle - moves to a single OpaWasmEvaluator.create factory that the 
producer and
    the policy both call, so the two construct it identically. Both evaluators 
extend
    OpaPolicyEvaluator, so the decision contract is unchanged: the same
    CamelOpaDecision headers, a fail-closed default, and failOpen governing an
    evaluation failure. A deny still throws CamelAuthorizationException.
    
    No readiness check is registered in wasm mode: the policy builds no client, 
so
    ownsClient stays false and registerHealthCheck already skips - there is no 
server
    to probe, consistent with CAMEL-24743.
    
    OpaSecurityPolicyWasmTest covers allow and deny through a wasm bundle, and, 
with a
    rest policy as a positive control, that only the rest policy registers a 
check.
    
    The camel-examples ai-tools-spiffe-opa sample can now use .policy(...) 
instead of
    the interceptFrom workaround; that lives in a separate repository and is 
left as a
    follow-up.
    
    Co-Authored-By: Claude Opus 4.8 <[email protected]>
    Signed-off-by: Andrea Cosentino <[email protected]>
    
    * Regen
    
    * CAMEL-24830: camel-opa - cover that a wasm bundle which loads but is not 
a valid module fails the route start
    
    beforeWrap cannot throw a checked exception, and OpaWasmEvaluator borrows an
    instance at startup, so the policy must surface a bad-bundle failure rather 
than
    swallow it. The test drives it with the Rego source as the bundle: it loads 
as
    bytes but is not a compiled module.
    
    Co-Authored-By: Claude Opus 4.8 <[email protected]>
    Signed-off-by: Andrea Cosentino <[email protected]>
    
    * CAMEL-24830: camel-opa - warn on ignored server options in wasm-mode 
policy, and cover the validation paths
    
    Review feedback:
    - OpaSecurityPolicy now warns at startup when 
serverUrl/bearerToken/opaClient are set with evaluationMode=wasm,
      matching OpaEndpoint.warnAboutIgnoredServerOptions so both entry points 
behave alike (davsclaus).
    - The bad-bundle tests assert the startup failure specifically: a bundle 
that cannot be loaded surfaces the
      RuntimeCamelException wrapper (Could not load the wasm policy bundle), 
the loads-but-invalid case a RuntimeException
      from addRoutes - both proving the route never starts rather than 
authorizing nothing on the first exchange.
    - Added tests through OpaSecurityPolicy.buildEvaluator for an unknown 
evaluationMode, a missing policyBundle and
      poolSize<1, which is a separate entry point from the endpoint's 
validation.
    - The readiness-check assertion checks the full security-policy:opa- id 
prefix, not only the hostname.
    
    Co-Authored-By: Claude Opus 4.8 <[email protected]>
    Signed-off-by: Andrea Cosentino <[email protected]>
    
    * CAMEL-24830: warn about an ignored opaClient on the endpoint too
    
    Addresses review feedback on #26670.
    
    warnIgnoredServerOptions on the policy warns about an injected opaClient
    in wasm mode; OpaEndpoint.warnAboutIgnoredServerOptions did not, and the
    endpoint's wasm branch goes straight to createWasmEvaluator() without
    ever reading configuration.getOpaClient(). So the parity the policy's
    javadoc claims ran the other way: the endpoint was the one dropping an
    option silently.
    
    The review suggested this might belong to #26669 instead; that PR is
    merged, so this is where it can land.
    
    No test. The warning is log-only, and neither of the two warnings it
    joins - bearerToken and serverUrl - is covered on either side today.
    Asserting on it needs a log-capturing appender, and LogCaptureAppender
    is duplicated per module (camel-netty and camel-netty-http each carry
    their own) rather than shared, so covering this one line means a third
    copy. If that coverage is wanted, it is worth a shared harness pinning
    all three warnings on both classes rather than only the new one.
    
    Co-Authored-By: Claude Opus 5 <[email protected]>
    Signed-off-by: Andrea Cosentino <[email protected]>
    
    ---------
    
    Signed-off-by: Andrea Cosentino <[email protected]>
    Co-authored-by: Claude Opus 4.8 <[email protected]>
    Co-authored-by: Guillaume Nodet <[email protected]>
---
 .../apache/camel/catalog/docs/opa-component.adoc   |  20 +++
 .../camel-opa/src/main/docs/opa-component.adoc     |  20 +++
 .../apache/camel/component/opa/OpaEndpoint.java    |  44 ++---
 .../camel/component/opa/OpaWasmEvaluator.java      |  27 ++++
 .../component/opa/security/OpaSecurityPolicy.java  | 149 +++++++++++++++--
 .../opa/security/OpaSecurityPolicyWasmTest.java    | 177 +++++++++++++++++++++
 6 files changed, 395 insertions(+), 42 deletions(-)

diff --git 
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/opa-component.adoc
 
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/opa-component.adoc
index e4a344a4eb86..d674ffa40aed 100644
--- 
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/opa-component.adoc
+++ 
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/opa-component.adoc
@@ -87,6 +87,26 @@ from("platform-http:/orders")
 The decision headers are set here too, so an 
`onException(CamelAuthorizationException.class)` handler can read
 `CamelOpaDecision` to build a meaningful error response.
 
+The policy can also evaluate a WebAssembly bundle in-process, which is the 
mode to prefer for a hot path such as
+authorizing an AI tool call, where a network hop per decision is not wanted:
+
+[source,java]
+------------------------------------------------------------
+OpaSecurityPolicy opaPolicy = new OpaSecurityPolicy();
+opaPolicy.setPolicyPath("authz/orders/allow");
+opaPolicy.setEvaluationMode("wasm");
+opaPolicy.setPolicyBundle("classpath:orders-bundle.tar.gz");
+
+from("platform-http:/orders")
+    .policy(opaPolicy)
+        .to("direct:handleOrder");
+------------------------------------------------------------
+
+The decision contract is identical to `rest` mode - the same 
`CamelOpaDecision` headers, and a
+`CamelAuthorizationException` on a deny - so a route need not know which 
engine evaluated it. `serverUrl`,
+`bearerToken` and the readiness check do not apply in `wasm` mode; the 
*Evaluation modes* section below covers
+building a bundle and what each mode gives up.
+
 == The input document
 
 Camel sends OPA an `input` document shaped like this:
diff --git a/components/camel-opa/src/main/docs/opa-component.adoc 
b/components/camel-opa/src/main/docs/opa-component.adoc
index e4a344a4eb86..d674ffa40aed 100644
--- a/components/camel-opa/src/main/docs/opa-component.adoc
+++ b/components/camel-opa/src/main/docs/opa-component.adoc
@@ -87,6 +87,26 @@ from("platform-http:/orders")
 The decision headers are set here too, so an 
`onException(CamelAuthorizationException.class)` handler can read
 `CamelOpaDecision` to build a meaningful error response.
 
+The policy can also evaluate a WebAssembly bundle in-process, which is the 
mode to prefer for a hot path such as
+authorizing an AI tool call, where a network hop per decision is not wanted:
+
+[source,java]
+------------------------------------------------------------
+OpaSecurityPolicy opaPolicy = new OpaSecurityPolicy();
+opaPolicy.setPolicyPath("authz/orders/allow");
+opaPolicy.setEvaluationMode("wasm");
+opaPolicy.setPolicyBundle("classpath:orders-bundle.tar.gz");
+
+from("platform-http:/orders")
+    .policy(opaPolicy)
+        .to("direct:handleOrder");
+------------------------------------------------------------
+
+The decision contract is identical to `rest` mode - the same 
`CamelOpaDecision` headers, and a
+`CamelAuthorizationException` on a deny - so a route need not know which 
engine evaluated it. `serverUrl`,
+`bearerToken` and the readiness check do not apply in `wasm` mode; the 
*Evaluation modes* section below covers
+building a bundle and what each mode gives up.
+
 == The input document
 
 Camel sends OPA an `input` document shaped like this:
diff --git 
a/components/camel-opa/src/main/java/org/apache/camel/component/opa/OpaEndpoint.java
 
b/components/camel-opa/src/main/java/org/apache/camel/component/opa/OpaEndpoint.java
index d8bafb72c338..8a621193f8b4 100644
--- 
a/components/camel-opa/src/main/java/org/apache/camel/component/opa/OpaEndpoint.java
+++ 
b/components/camel-opa/src/main/java/org/apache/camel/component/opa/OpaEndpoint.java
@@ -121,13 +121,20 @@ public class OpaEndpoint extends DefaultEndpoint {
     }
 
     /**
-     * {@code serverUrl} and {@code bearerToken} address and authenticate to 
an OPA server, of which there is none in
-     * {@code wasm} mode, so they are ignored - a startup warning is clearer 
than silence for an operator who set one
-     * and expects it to take effect. {@code failOpen} is deliberately not 
among these: a {@code wasm} evaluation can
-     * still fail (a busy pool, a bad bundle), and {@code failOpen} governs 
that outcome exactly as in {@code rest}
-     * mode, so it applies in both.
+     * {@code opaClient}, {@code serverUrl} and {@code bearerToken} reach and 
authenticate to an OPA server, of which
+     * there is none in {@code wasm} mode, so they are ignored - a startup 
warning is clearer than silence for an
+     * operator who set one and expects it to take effect. {@code failOpen} is 
deliberately not among these: a
+     * {@code wasm} evaluation can still fail (a busy pool, a bad bundle), and 
{@code failOpen} governs that outcome
+     * exactly as in {@code rest} mode, so it applies in both.
+     * <p/>
+     * Kept in step with {@code OpaSecurityPolicy.warnIgnoredServerOptions}: 
the two configure the same evaluators, so
+     * an option that is silently dropped by one and warned about by the other 
is a trap for anyone moving a policy path
+     * between the producer and {@code .policy(...)}.
      */
     private void warnAboutIgnoredServerOptions() {
+        if (configuration.getOpaClient() != null) {
+            LOG.warn("opaClient is ignored when evaluationMode=wasm: the 
policy is evaluated in-process");
+        }
         if (ObjectHelper.isNotEmpty(configuration.getBearerToken())) {
             LOG.warn("bearerToken is ignored when evaluationMode=wasm: there 
is no server to authenticate to");
         }
@@ -139,28 +146,11 @@ public class OpaEndpoint extends DefaultEndpoint {
     }
 
     private OpaPolicyEvaluator createWasmEvaluator() throws Exception {
-        if (ObjectHelper.isEmpty(configuration.getPolicyBundle())) {
-            throw new IllegalArgumentException(
-                    "policyBundle is required when evaluationMode=wasm; build 
one with"
-                                               + " opa build -t wasm -e 
<entrypoint> <policy.rego>");
-        }
-        if (configuration.getPoolSize() < 1) {
-            // OpaPolicyPool.create rejects this too, but as "maxSize must be 
positive" - naming its own parameter
-            // rather than the option the operator set, on a component where 
poolSize is the only pool they see
-            throw new IllegalArgumentException(
-                    "poolSize must be at least 1 when evaluationMode=wasm, was 
" + configuration.getPoolSize());
-        }
-        // the entrypoint is fixed at build time and is not the same thing as 
a data path, but opa build names it
-        // after the rule, so the policy path is the right default
-        String entrypoint = 
ObjectHelper.isNotEmpty(configuration.getEntrypoint())
-                ? configuration.getEntrypoint() : policyPath;
-        OpaWasmEvaluator.Bundle bundle
-                = OpaWasmEvaluator.loadPolicy(getCamelContext(), 
configuration.getPolicyBundle());
-        return new OpaWasmEvaluator(
-                bundle.wasm(), bundle.data(), entrypoint, 
configuration.getPoolSize(),
-                configuration.getBorrowTimeout(), policyPath, 
configuration.getAllowKey(),
-                configuration.getIncludeHeaders(), 
configuration.getIncludeProperties(),
-                configuration.isIncludeBody(), configuration.isFailOpen());
+        return OpaWasmEvaluator.create(
+                getCamelContext(), configuration.getPolicyBundle(), 
configuration.getEntrypoint(),
+                configuration.getPoolSize(), configuration.getBorrowTimeout(), 
policyPath, configuration.getAllowKey(),
+                configuration.getIncludeHeaders(), 
configuration.getIncludeProperties(), configuration.isIncludeBody(),
+                configuration.isFailOpen());
     }
 
     @Override
diff --git 
a/components/camel-opa/src/main/java/org/apache/camel/component/opa/OpaWasmEvaluator.java
 
b/components/camel-opa/src/main/java/org/apache/camel/component/opa/OpaWasmEvaluator.java
index 89d2eac94df4..e67ca9bc276d 100644
--- 
a/components/camel-opa/src/main/java/org/apache/camel/component/opa/OpaWasmEvaluator.java
+++ 
b/components/camel-opa/src/main/java/org/apache/camel/component/opa/OpaWasmEvaluator.java
@@ -27,6 +27,7 @@ import com.fasterxml.jackson.databind.ObjectMapper;
 import com.styra.opa.wasm.OpaPolicy;
 import org.apache.camel.CamelContext;
 import org.apache.camel.support.ResourceHelper;
+import org.apache.camel.util.ObjectHelper;
 import org.apache.commons.compress.archivers.tar.TarArchiveEntry;
 import org.apache.commons.compress.archivers.tar.TarArchiveInputStream;
 import org.apache.commons.compress.compressors.gzip.GzipCompressorInputStream;
@@ -95,6 +96,32 @@ public class OpaWasmEvaluator extends OpaPolicyEvaluator 
implements AutoCloseabl
         }
     }
 
+    /**
+     * Builds a wasm evaluator from a bundle location, applying the validation 
and the entrypoint default in one place
+     * so that the producer and the {@code OpaSecurityPolicy} construct it 
identically.
+     */
+    public static OpaWasmEvaluator create(
+            CamelContext camelContext, String policyBundle, String entrypoint, 
int poolSize, long borrowTimeout,
+            String policyPath, String allowKey, String includeHeaders, String 
includeProperties, boolean includeBody,
+            boolean failOpen)
+            throws Exception {
+        if (ObjectHelper.isEmpty(policyBundle)) {
+            throw new IllegalArgumentException(
+                    "policyBundle is required when evaluationMode=wasm; build 
one with"
+                                               + " opa build -t wasm -e 
<entrypoint> <policy.rego>");
+        }
+        if (poolSize < 1) {
+            throw new IllegalArgumentException("poolSize must be at least 1 
when evaluationMode=wasm, was " + poolSize);
+        }
+        // the entrypoint is fixed at build time and is not the same thing as 
a data path, but opa build names it after
+        // the rule, so the policy path is the right default
+        String resolvedEntrypoint = ObjectHelper.isNotEmpty(entrypoint) ? 
entrypoint : policyPath;
+        Bundle bundle = loadPolicy(camelContext, policyBundle);
+        return new OpaWasmEvaluator(
+                bundle.wasm(), bundle.data(), resolvedEntrypoint, poolSize, 
borrowTimeout, policyPath, allowKey,
+                includeHeaders, includeProperties, includeBody, failOpen);
+    }
+
     /**
      * A loaded policy: the WebAssembly module, and the data document that 
{@code opa build} packed beside it when the
      * source was a bundle. A policy that reads {@code data.*} needs the 
latter to decide the same way it would against
diff --git 
a/components/camel-opa/src/main/java/org/apache/camel/component/opa/security/OpaSecurityPolicy.java
 
b/components/camel-opa/src/main/java/org/apache/camel/component/opa/security/OpaSecurityPolicy.java
index 54054bad0361..219c34ca68ca 100644
--- 
a/components/camel-opa/src/main/java/org/apache/camel/component/opa/security/OpaSecurityPolicy.java
+++ 
b/components/camel-opa/src/main/java/org/apache/camel/component/opa/security/OpaSecurityPolicy.java
@@ -30,6 +30,7 @@ import org.apache.camel.RuntimeCamelException;
 import org.apache.camel.component.opa.OpaHttpClient;
 import org.apache.camel.component.opa.OpaPolicyEvaluator;
 import org.apache.camel.component.opa.OpaRestEvaluator;
+import org.apache.camel.component.opa.OpaWasmEvaluator;
 import org.apache.camel.health.HealthCheckRegistry;
 import org.apache.camel.spi.AuthorizationPolicy;
 import org.apache.camel.support.jsse.SSLContextParameters;
@@ -57,7 +58,11 @@ public class OpaSecurityPolicy implements 
AuthorizationPolicy {
 
     private static final Logger LOG = 
LoggerFactory.getLogger(OpaSecurityPolicy.class);
 
-    private String serverUrl = "http://localhost:8181";;
+    private static final String WASM_MODE = "wasm";
+    private static final String REST_MODE = "rest";
+    private static final String DEFAULT_SERVER_URL = "http://localhost:8181";;
+
+    private String serverUrl = DEFAULT_SERVER_URL;
     private String policyPath;
     private String allowKey = "allow";
     private String includeHeaders = "*";
@@ -65,6 +70,11 @@ public class OpaSecurityPolicy implements 
AuthorizationPolicy {
     private boolean includeBody;
     private String bearerToken;
     private boolean failOpen;
+    private String evaluationMode = REST_MODE;
+    private String policyBundle;
+    private String entrypoint;
+    private int poolSize = 8;
+    private long borrowTimeout = 30000;
     private OPAClient opaClient;
 
     private boolean healthCheckEnabled = true;
@@ -92,20 +102,10 @@ public class OpaSecurityPolicy implements 
AuthorizationPolicy {
         this.camelContext = route.getCamelContext();
         if (evaluator == null) {
             StringHelper.notEmpty(policyPath, "policyPath", this);
-            OpaHttpClient transport = null;
-            if (opaClient == null) {
-                // createClient moved to OpaRestEvaluator when the evaluator 
became an abstract base
-                sslContext = createSslContext(route.getCamelContext());
-                transport = OpaRestEvaluator.createTransport(
-                        bearerToken, connectionTimeout, requestTimeout, 
sslContext);
-                opaClient = OpaRestEvaluator.createClient(serverUrl, 
transport);
-                ownsClient = true;
-            }
-            evaluator = new OpaRestEvaluator(
-                    opaClient, transport, policyPath, allowKey, 
includeHeaders, includeProperties, includeBody,
-                    failOpen);
-            // a Policy has no stop hook of its own, so the transport would 
outlive the routes it was built for.
-            // Registering the evaluator as a service hands its close() to the 
context's shutdown
+            evaluator = buildEvaluator(route.getCamelContext());
+            // a Policy has no stop hook of its own, so the evaluator - its 
HTTP transport in rest mode, or its
+            // WebAssembly instance pool in wasm mode - would outlive the 
routes it was built for. Registering it as a
+            // service hands its close() to the context's shutdown.
             try {
                 route.getCamelContext().addService(evaluator);
             } catch (Exception e) {
@@ -115,6 +115,60 @@ public class OpaSecurityPolicy implements 
AuthorizationPolicy {
         // The health check is registered and unregistered from the wrapped 
processors' lifecycle
         // (onProcessorStart/onProcessorStop), not here: beforeWrap does not 
run again when a route is merely
         // restarted, so a check registered here would be left behind when the 
guarded routes stop (CAMEL-24751).
+        // In wasm mode nothing sets ownsClient, so registerHealthCheck skips 
anyway - the policy is evaluated
+        // in-process and there is no server to probe (consistent with 
CAMEL-24743).
+    }
+
+    /**
+     * Builds the evaluator for the configured {@code evaluationMode}. Both 
share {@link OpaPolicyEvaluator}, so the
+     * decision contract - the {@code CamelOpaDecision} headers and a 
fail-closed default - is identical whichever
+     * engine runs.
+     */
+    private OpaPolicyEvaluator buildEvaluator(CamelContext camelContext) {
+        if (WASM_MODE.equalsIgnoreCase(evaluationMode)) {
+            warnIgnoredServerOptions();
+            try {
+                return OpaWasmEvaluator.create(camelContext, policyBundle, 
entrypoint, poolSize, borrowTimeout,
+                        policyPath, allowKey, includeHeaders, 
includeProperties, includeBody, failOpen);
+            } catch (RuntimeException e) {
+                // the validation messages (policyBundle, poolSize) already 
read correctly; do not bury them
+                throw e;
+            } catch (Exception e) {
+                throw new RuntimeCamelException("Could not load the wasm 
policy bundle for policy " + policyPath, e);
+            }
+        }
+        if (!REST_MODE.equalsIgnoreCase(evaluationMode)) {
+            throw new IllegalArgumentException(
+                    "Unknown evaluationMode '" + evaluationMode + "'; expected 
one of " + REST_MODE + ", " + WASM_MODE);
+        }
+        OpaHttpClient transport = null;
+        if (opaClient == null) {
+            // createClient moved to OpaRestEvaluator when the evaluator 
became an abstract base
+            sslContext = createSslContext(camelContext);
+            transport = OpaRestEvaluator.createTransport(bearerToken, 
connectionTimeout, requestTimeout, sslContext);
+            opaClient = OpaRestEvaluator.createClient(serverUrl, transport);
+            ownsClient = true;
+        }
+        return new OpaRestEvaluator(
+                opaClient, transport, policyPath, allowKey, includeHeaders, 
includeProperties, includeBody, failOpen);
+    }
+
+    /**
+     * Warns at startup when options that only make sense for the REST engine 
are set in {@code wasm} mode, matching
+     * {@code OpaEndpoint.warnAboutIgnoredServerOptions} so both entry points 
behave alike. {@code failOpen} is not
+     * among them - it still governs a {@code wasm} evaluation failure. {@code 
serverUrl}, {@code bearerToken} and an
+     * injected {@code opaClient} address, authenticate to or replace a server 
there is none of in {@code wasm} mode.
+     */
+    private void warnIgnoredServerOptions() {
+        if (opaClient != null) {
+            LOG.warn("opaClient is ignored when evaluationMode=wasm: the 
policy is evaluated in-process");
+        }
+        if (ObjectHelper.isNotEmpty(bearerToken)) {
+            LOG.warn("bearerToken is ignored when evaluationMode=wasm: there 
is no server to authenticate to");
+        }
+        if (ObjectHelper.isNotEmpty(serverUrl) && 
!DEFAULT_SERVER_URL.equals(serverUrl)) {
+            LOG.warn("serverUrl '{}' is ignored when evaluationMode=wasm: the 
policy is evaluated in-process", serverUrl);
+        }
     }
 
     /**
@@ -272,6 +326,71 @@ public class OpaSecurityPolicy implements 
AuthorizationPolicy {
         this.failOpen = failOpen;
     }
 
+    public String getEvaluationMode() {
+        return evaluationMode;
+    }
+
+    /**
+     * How the policy is evaluated. {@code rest} (the default) asks a running 
OPA server; {@code wasm} evaluates a
+     * WebAssembly bundle in-process, with no server involved - preferred for 
a hot path such as authorizing an AI tool
+     * call. {@code serverUrl}, {@code bearerToken} and the readiness check do 
not apply in {@code wasm} mode;
+     * {@code failOpen} still governs an evaluation failure in both.
+     */
+    public void setEvaluationMode(String evaluationMode) {
+        this.evaluationMode = evaluationMode;
+    }
+
+    public String getPolicyBundle() {
+        return policyBundle;
+    }
+
+    /**
+     * The WebAssembly policy to evaluate in {@code wasm} mode, as produced by 
{@code opa build -t wasm}. Accepts a
+     * {@code file:}, {@code classpath:} or {@code http:} location holding 
either the {@code bundle.tar.gz} that
+     * {@code opa build} emits or a bare {@code .wasm} module. Required when 
{@code evaluationMode=wasm}.
+     */
+    public void setPolicyBundle(String policyBundle) {
+        this.policyBundle = policyBundle;
+    }
+
+    public String getEntrypoint() {
+        return entrypoint;
+    }
+
+    /**
+     * The compiled entrypoint to evaluate in {@code wasm} mode, fixed when 
the bundle is built with {@code opa build
+     * -e}. This is not the same thing as the policy path; it defaults to the 
policy path, which is the name
+     * {@code opa build} gives it.
+     */
+    public void setEntrypoint(String entrypoint) {
+        this.entrypoint = entrypoint;
+    }
+
+    public int getPoolSize() {
+        return poolSize;
+    }
+
+    /**
+     * How many WebAssembly evaluation instances to pool in {@code wasm} mode. 
They carry mutable state and are not
+     * thread-safe, so this bounds how many exchanges are authorized at once; 
an exchange that arrives when all are busy
+     * waits up to {@code borrowTimeout}.
+     */
+    public void setPoolSize(int poolSize) {
+        this.poolSize = poolSize;
+    }
+
+    public long getBorrowTimeout() {
+        return borrowTimeout;
+    }
+
+    /**
+     * How long, in milliseconds, an exchange waits for a free WebAssembly 
instance in {@code wasm} mode before the
+     * evaluation fails. That failure is not a deny: it fails closed, or 
proceeds under {@code failOpen}.
+     */
+    public void setBorrowTimeout(long borrowTimeout) {
+        this.borrowTimeout = borrowTimeout;
+    }
+
     /**
      * The policy is a bean rather than a {@code CamelContextAware} service, 
so the context comes from the route it is
      * wrapping - which is the only place one is available.
diff --git 
a/components/camel-opa/src/test/java/org/apache/camel/component/opa/security/OpaSecurityPolicyWasmTest.java
 
b/components/camel-opa/src/test/java/org/apache/camel/component/opa/security/OpaSecurityPolicyWasmTest.java
new file mode 100644
index 000000000000..2de9e8da8c09
--- /dev/null
+++ 
b/components/camel-opa/src/test/java/org/apache/camel/component/opa/security/OpaSecurityPolicyWasmTest.java
@@ -0,0 +1,177 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.camel.component.opa.security;
+
+import java.util.List;
+
+import org.apache.camel.CamelAuthorizationException;
+import org.apache.camel.CamelExecutionException;
+import org.apache.camel.RuntimeCamelException;
+import org.apache.camel.builder.RouteBuilder;
+import org.apache.camel.component.mock.MockEndpoint;
+import org.apache.camel.health.HealthCheck;
+import org.apache.camel.health.HealthCheckRegistry;
+import org.apache.camel.test.junit6.CamelTestSupport;
+import org.junit.jupiter.api.Test;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+
+/**
+ * {@link OpaSecurityPolicy} enforcing a route with an in-process WebAssembly 
bundle. The decision contract is the same
+ * as the REST engine - a match proceeds, a non-match throws {@code 
CamelAuthorizationException} - and no server
+ * readiness check is registered, because the policy is evaluated in-process 
(CAMEL-24830).
+ */
+public class OpaSecurityPolicyWasmTest extends CamelTestSupport {
+
+    private final OpaSecurityPolicy wasmPolicy = new OpaSecurityPolicy();
+    private final OpaSecurityPolicy restPolicy = new OpaSecurityPolicy();
+
+    @Override
+    protected RouteBuilder createRouteBuilder() {
+        wasmPolicy.setEvaluationMode("wasm");
+        wasmPolicy.setPolicyBundle("classpath:authz.wasm");
+        wasmPolicy.setPolicyPath("authz/allow");
+
+        // a rest-mode policy is the positive control for the readiness-check 
assertion: it registers a check, the
+        // wasm one must not
+        restPolicy.setServerUrl("http://opa-rest:8181";);
+        restPolicy.setPolicyPath("authz/allow");
+
+        return new RouteBuilder() {
+            @Override
+            public void configure() {
+                from("direct:wasm").policy(wasmPolicy).to("mock:allowed");
+                from("direct:rest").policy(restPolicy).to("mock:rest");
+            }
+        };
+    }
+
+    @Test
+    void allowsWhenTheWasmPolicyMatches() throws Exception {
+        MockEndpoint allowed = getMockEndpoint("mock:allowed");
+        allowed.expectedMessageCount(1);
+
+        template.sendBodyAndHeader("direct:wasm", "payload", "user", "alice");
+
+        allowed.assertIsSatisfied();
+    }
+
+    @Test
+    void deniesWhenTheWasmPolicyDoesNotMatch() throws Exception {
+        MockEndpoint allowed = getMockEndpoint("mock:allowed");
+        allowed.expectedMessageCount(0);
+
+        assertThatThrownBy(() -> template.sendBodyAndHeader("direct:wasm", 
"payload", "user", "mallory"))
+                .isInstanceOf(CamelExecutionException.class)
+                .hasCauseInstanceOf(CamelAuthorizationException.class);
+
+        allowed.assertIsSatisfied();
+    }
+
+    @Test
+    void registersOnlyTheRestPolicysReadinessCheck() {
+        HealthCheckRegistry registry = HealthCheckRegistry.get(context);
+        assertThat(registry).isNotNull();
+        List<HealthCheck> checks = registry.stream()
+                .filter(hc -> hc.getId().startsWith("security-policy:opa-"))
+                .toList();
+
+        // exactly one, and it is the rest policy's - the wasm policy 
evaluates in-process with no server to probe.
+        // Assert the full ID contract, not just the hostname, so a refactor 
of the ID-building logic cannot pass here
+        // silently: OpaSecurityPolicyHealthCheck builds it as 
"security-policy:opa-" + sanitized serverUrl/policyPath.
+        assertThat(checks).hasSize(1);
+        
assertThat(checks.get(0).getId()).startsWith("security-policy:opa-").contains("opa-rest");
+    }
+
+    @Test
+    void failsRouteStartOnABundleThatLoadsButIsNotAValidModule() {
+        // OpaWasmEvaluator borrows an instance at startup so a broken bundle 
fails fast, and beforeWrap - which cannot
+        // throw a checked exception - must surface that rather than swallow 
it, or a route would start and then
+        // authorize nothing. authz.rego is the Rego source: it loads as bytes 
but is not a compiled wasm module.
+        OpaSecurityPolicy corrupt = new OpaSecurityPolicy();
+        corrupt.setEvaluationMode("wasm");
+        corrupt.setPolicyBundle("classpath:authz.rego");
+        corrupt.setPolicyPath("authz/allow");
+
+        // OpaPolicy rejects the module during the warmup borrow with an 
unchecked exception, which buildEvaluator's
+        // catch(RuntimeException) rethrows as-is; reaching the caller of 
addRoutes is what proves the route did not
+        // start (a first-exchange failure would not surface here).
+        assertThatThrownBy(() -> context.addRoutes(routeWith(corrupt)))
+                .isInstanceOf(RuntimeException.class);
+    }
+
+    @Test
+    void failsRouteStartWhenTheBundleResourceCannotBeLoaded() {
+        // a bundle location that resolves to nothing is a checked failure in 
loadPolicy, which beforeWrap wraps; the
+        // wrapper message is what proves the failure surfaced at startup 
rather than being swallowed
+        OpaSecurityPolicy missing = new OpaSecurityPolicy();
+        missing.setEvaluationMode("wasm");
+        missing.setPolicyBundle("classpath:does-not-exist.wasm");
+        missing.setPolicyPath("authz/allow");
+
+        assertThatThrownBy(() -> context.addRoutes(routeWith(missing)))
+                .isInstanceOf(RuntimeCamelException.class)
+                .hasMessageContaining("Could not load the wasm policy bundle");
+    }
+
+    @Test
+    void failsRouteStartOnAnUnknownEvaluationMode() {
+        // this path lives entirely in the policy's buildEvaluator and is 
covered by no endpoint test
+        OpaSecurityPolicy bogus = new OpaSecurityPolicy();
+        bogus.setEvaluationMode("bogus");
+        bogus.setPolicyPath("authz/allow");
+
+        assertThatThrownBy(() -> context.addRoutes(routeWith(bogus)))
+                .hasRootCauseInstanceOf(IllegalArgumentException.class)
+                .hasMessageContaining("Unknown evaluationMode");
+    }
+
+    @Test
+    void failsRouteStartWhenNoWasmBundleIsConfigured() {
+        // buildEvaluator forwards to OpaWasmEvaluator.create, so its 
validation applies through the policy too
+        OpaSecurityPolicy noBundle = new OpaSecurityPolicy();
+        noBundle.setEvaluationMode("wasm");
+        noBundle.setPolicyPath("authz/allow");
+
+        assertThatThrownBy(() -> context.addRoutes(routeWith(noBundle)))
+                .hasRootCauseInstanceOf(IllegalArgumentException.class)
+                .hasMessageContaining("policyBundle is required");
+    }
+
+    @Test
+    void failsRouteStartOnAPoolSizeBelowOne() {
+        OpaSecurityPolicy badPool = new OpaSecurityPolicy();
+        badPool.setEvaluationMode("wasm");
+        badPool.setPolicyBundle("classpath:authz.wasm");
+        badPool.setPolicyPath("authz/allow");
+        badPool.setPoolSize(0);
+
+        assertThatThrownBy(() -> context.addRoutes(routeWith(badPool)))
+                .hasRootCauseInstanceOf(IllegalArgumentException.class)
+                .hasMessageContaining("poolSize must be at least 1");
+    }
+
+    private static RouteBuilder routeWith(OpaSecurityPolicy policy) {
+        return new RouteBuilder() {
+            @Override
+            public void configure() {
+                from("direct:probe").policy(policy).to("mock:never");
+            }
+        };
+    }
+}

Reply via email to