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 1f762d21591e CAMEL-24738: camel-opa - make a failOpen decision 
distinguishable from a real one
1f762d21591e is described below

commit 1f762d21591e06b5ccb067b80a981234ee6c824a
Author: Andrea Cosentino <[email protected]>
AuthorDate: Wed Sep 23 10:32:33 2026 +0200

    CAMEL-24738: camel-opa - make a failOpen decision distinguishable from a 
real one
    
    When failOpen lets an exchange proceed despite an evaluation failure it now
    carries CamelOpaDecisionFailedOpen=true, so a route or an audit can tell a
    fail-open allow apart from a genuine policy allow. The marker is set only 
on the
    deliberate failOpen path, cleared on entry like the other decision headers, 
and
    withheld from the OPA input document so a sender cannot inject it. It 
applies to
    both the producer and OpaSecurityPolicy, which share the evaluator.
    
    Closes #26664
    
    Co-authored-by: Claude Opus 4.8 <[email protected]>
---
 .../org/apache/camel/catalog/components/opa.json   |  3 +-
 .../apache/camel/catalog/docs/opa-component.adoc   | 24 ++++++++++-
 .../org/apache/camel/component/opa/opa.json        |  3 +-
 .../camel-opa/src/main/docs/opa-component.adoc     | 24 ++++++++++-
 .../apache/camel/component/opa/OpaConstants.java   |  8 ++++
 .../camel/component/opa/OpaPolicyEvaluator.java    | 10 ++++-
 .../camel/component/opa/OpaProducerTest.java       | 48 ++++++++++++++++++++++
 .../opa/security/OpaSecurityPolicyTest.java        | 23 +++++++++++
 .../endpoint/dsl/OpaEndpointBuilderFactory.java    | 16 ++++++++
 9 files changed, 154 insertions(+), 5 deletions(-)

diff --git 
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/components/opa.json
 
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/components/opa.json
index 27178ee61b22..099541bf5a47 100644
--- 
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/components/opa.json
+++ 
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/components/opa.json
@@ -50,7 +50,8 @@
   "headers": {
     "CamelOpaDecisionAllow": { "index": 0, "kind": "header", "displayName": 
"", "group": "producer", "label": "producer", "required": false, "javaType": 
"Boolean", "deprecated": false, "deprecationNote": "", "autowired": false, 
"secret": false, "description": "The allow\/deny verdict of the policy 
evaluation. Always overwritten by the component, so a value set by an inbound 
message never survives into the route.", "constantName": 
"org.apache.camel.component.opa.OpaConstants#DECISION_ALLOW" },
     "CamelOpaDecision": { "index": 1, "kind": "header", "displayName": "", 
"group": "producer", "label": "producer", "required": false, "javaType": 
"Object", "deprecated": false, "deprecationNote": "", "autowired": false, 
"secret": false, "description": "The raw decision document returned by OPA. 
Useful for policies that return more than a boolean, such as obligations, row 
filters or deny reasons.", "constantName": 
"org.apache.camel.component.opa.OpaConstants#DECISION" },
-    "CamelOpaPolicyPath": { "index": 2, "kind": "header", "displayName": "", 
"group": "producer", "label": "producer", "required": false, "javaType": 
"String", "deprecated": false, "deprecationNote": "", "autowired": false, 
"secret": false, "description": "The policy path that was evaluated. Set by the 
component for observability; it is not read as an input and cannot be used to 
select a different policy.", "constantName": 
"org.apache.camel.component.opa.OpaConstants#POLICY_PATH" }
+    "CamelOpaPolicyPath": { "index": 2, "kind": "header", "displayName": "", 
"group": "producer", "label": "producer", "required": false, "javaType": 
"String", "deprecated": false, "deprecationNote": "", "autowired": false, 
"secret": false, "description": "The policy path that was evaluated. Set by the 
component for observability; it is not read as an input and cannot be used to 
select a different policy.", "constantName": 
"org.apache.camel.component.opa.OpaConstants#POLICY_PATH" },
+    "CamelOpaDecisionFailedOpen": { "index": 3, "kind": "header", 
"displayName": "", "group": "producer", "label": "producer", "required": false, 
"javaType": "Boolean", "deprecated": false, "deprecationNote": "", "autowired": 
false, "secret": false, "description": "Set to true only when the exchange 
proceeded because failOpen is enabled and the policy could not be evaluated - 
nothing authorized it. Absent on every decision an actual policy made, so a 
route or an audit trail can tell the  [...]
   },
   "properties": {
     "policyPath": { "index": 0, "kind": "path", "displayName": "Policy Path", 
"group": "producer", "label": "", "required": true, "type": "string", 
"javaType": "java.lang.String", "deprecated": false, "deprecationNote": "", 
"autowired": false, "secret": false, "description": "Path of the Rego rule head 
to evaluate, relative to the OPA data document. For a rule named allow in a 
policy declaring package authz.orders, this is authz\/orders\/allow. The path 
is taken from the endpoint only: i [...]
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 89752125d723..e4a344a4eb86 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
@@ -317,6 +317,7 @@ context.getRegistry().bind("opaTls", spiffe);
 
 None of this applies in `wasm` mode, where there is no server to reach.
 
+[[failure-handling]]
 == Failure handling
 
 The component fails closed. If the policy cannot be evaluated at all — the OPA 
server is unreachable, times out,
@@ -328,6 +329,25 @@ decision point available".
 Setting `failOpen=true` reverses this and lets the exchange proceed when the 
policy cannot be evaluated. It exists
 for development and for non-critical policies, and should not be enabled in 
production.
 
+An exchange that proceeds that way carries `CamelOpaDecisionFailedOpen=true`. 
`CamelOpaDecisionAllow` is `true`
+in both cases, and on its own it cannot tell "a policy allowed this" from "no 
policy ran and we were told to
+proceed" — which is exactly the distinction an audit trail needs. The marker 
is set only on the fail-open path,
+so a route can branch on it and an operator can alert on its presence:
+
+[source,java]
+------------------------------------------------------------
+from("platform-http:/orders")
+    .to("opa:authz/orders/allow?failOpen=true")
+    .choice()
+        .when(header(OpaConstants.DECISION_FAILED_OPEN).isEqualTo(true))
+            .to("log:unauthorized?level=WARN")
+    .end()
+    .to("direct:orders")
+------------------------------------------------------------
+
+Like the other decision headers it is cleared before every evaluation, so a 
message cannot arrive claiming
+`CamelOpaDecisionFailedOpen=false` and disguise an unauthorized exchange as 
one a policy allowed.
+
 Note that OPA reports an *undefined* decision — no rule matched and the policy 
declares no default — as an error
 rather than as a deny, so it fails closed as well. Give every decision rule a 
default, as in the example above,
 so the policy always returns a verdict.
@@ -348,7 +368,9 @@ more important of the two: a denied producer merely records 
a verdict the route
 `CamelAuthorizationException` and stops the exchange, so an unreachable server 
there fails every message outright.
 That is why the policy's check is on by default rather than opt-in like the 
producer's. Set
 `healthCheckEnabled=false` on the policy for a route that should stay ready 
regardless — one running `failOpen`, say
-— in preference to hiding the check with `camel.health.exclude-pattern`.
+— in preference to hiding the check with `camel.health.exclude-pattern`. A 
policy running `failOpen` marks the
+exchanges it let through with `CamelOpaDecisionFailedOpen`, exactly as the 
producer does; the evaluator is shared,
+so everything in <<failure-handling>> about that header applies to 
`.policy(...)` too.
 
 Neither check is registered when an `opaClient` was injected: that client may 
point anywhere and neither the
 endpoint nor the policy has a way to ask it where, so probing the configured 
`serverUrl` would report on a server
diff --git 
a/components/camel-opa/src/generated/resources/META-INF/org/apache/camel/component/opa/opa.json
 
b/components/camel-opa/src/generated/resources/META-INF/org/apache/camel/component/opa/opa.json
index 27178ee61b22..099541bf5a47 100644
--- 
a/components/camel-opa/src/generated/resources/META-INF/org/apache/camel/component/opa/opa.json
+++ 
b/components/camel-opa/src/generated/resources/META-INF/org/apache/camel/component/opa/opa.json
@@ -50,7 +50,8 @@
   "headers": {
     "CamelOpaDecisionAllow": { "index": 0, "kind": "header", "displayName": 
"", "group": "producer", "label": "producer", "required": false, "javaType": 
"Boolean", "deprecated": false, "deprecationNote": "", "autowired": false, 
"secret": false, "description": "The allow\/deny verdict of the policy 
evaluation. Always overwritten by the component, so a value set by an inbound 
message never survives into the route.", "constantName": 
"org.apache.camel.component.opa.OpaConstants#DECISION_ALLOW" },
     "CamelOpaDecision": { "index": 1, "kind": "header", "displayName": "", 
"group": "producer", "label": "producer", "required": false, "javaType": 
"Object", "deprecated": false, "deprecationNote": "", "autowired": false, 
"secret": false, "description": "The raw decision document returned by OPA. 
Useful for policies that return more than a boolean, such as obligations, row 
filters or deny reasons.", "constantName": 
"org.apache.camel.component.opa.OpaConstants#DECISION" },
-    "CamelOpaPolicyPath": { "index": 2, "kind": "header", "displayName": "", 
"group": "producer", "label": "producer", "required": false, "javaType": 
"String", "deprecated": false, "deprecationNote": "", "autowired": false, 
"secret": false, "description": "The policy path that was evaluated. Set by the 
component for observability; it is not read as an input and cannot be used to 
select a different policy.", "constantName": 
"org.apache.camel.component.opa.OpaConstants#POLICY_PATH" }
+    "CamelOpaPolicyPath": { "index": 2, "kind": "header", "displayName": "", 
"group": "producer", "label": "producer", "required": false, "javaType": 
"String", "deprecated": false, "deprecationNote": "", "autowired": false, 
"secret": false, "description": "The policy path that was evaluated. Set by the 
component for observability; it is not read as an input and cannot be used to 
select a different policy.", "constantName": 
"org.apache.camel.component.opa.OpaConstants#POLICY_PATH" },
+    "CamelOpaDecisionFailedOpen": { "index": 3, "kind": "header", 
"displayName": "", "group": "producer", "label": "producer", "required": false, 
"javaType": "Boolean", "deprecated": false, "deprecationNote": "", "autowired": 
false, "secret": false, "description": "Set to true only when the exchange 
proceeded because failOpen is enabled and the policy could not be evaluated - 
nothing authorized it. Absent on every decision an actual policy made, so a 
route or an audit trail can tell the  [...]
   },
   "properties": {
     "policyPath": { "index": 0, "kind": "path", "displayName": "Policy Path", 
"group": "producer", "label": "", "required": true, "type": "string", 
"javaType": "java.lang.String", "deprecated": false, "deprecationNote": "", 
"autowired": false, "secret": false, "description": "Path of the Rego rule head 
to evaluate, relative to the OPA data document. For a rule named allow in a 
policy declaring package authz.orders, this is authz\/orders\/allow. The path 
is taken from the endpoint only: i [...]
diff --git a/components/camel-opa/src/main/docs/opa-component.adoc 
b/components/camel-opa/src/main/docs/opa-component.adoc
index 89752125d723..e4a344a4eb86 100644
--- a/components/camel-opa/src/main/docs/opa-component.adoc
+++ b/components/camel-opa/src/main/docs/opa-component.adoc
@@ -317,6 +317,7 @@ context.getRegistry().bind("opaTls", spiffe);
 
 None of this applies in `wasm` mode, where there is no server to reach.
 
+[[failure-handling]]
 == Failure handling
 
 The component fails closed. If the policy cannot be evaluated at all — the OPA 
server is unreachable, times out,
@@ -328,6 +329,25 @@ decision point available".
 Setting `failOpen=true` reverses this and lets the exchange proceed when the 
policy cannot be evaluated. It exists
 for development and for non-critical policies, and should not be enabled in 
production.
 
+An exchange that proceeds that way carries `CamelOpaDecisionFailedOpen=true`. 
`CamelOpaDecisionAllow` is `true`
+in both cases, and on its own it cannot tell "a policy allowed this" from "no 
policy ran and we were told to
+proceed" — which is exactly the distinction an audit trail needs. The marker 
is set only on the fail-open path,
+so a route can branch on it and an operator can alert on its presence:
+
+[source,java]
+------------------------------------------------------------
+from("platform-http:/orders")
+    .to("opa:authz/orders/allow?failOpen=true")
+    .choice()
+        .when(header(OpaConstants.DECISION_FAILED_OPEN).isEqualTo(true))
+            .to("log:unauthorized?level=WARN")
+    .end()
+    .to("direct:orders")
+------------------------------------------------------------
+
+Like the other decision headers it is cleared before every evaluation, so a 
message cannot arrive claiming
+`CamelOpaDecisionFailedOpen=false` and disguise an unauthorized exchange as 
one a policy allowed.
+
 Note that OPA reports an *undefined* decision — no rule matched and the policy 
declares no default — as an error
 rather than as a deny, so it fails closed as well. Give every decision rule a 
default, as in the example above,
 so the policy always returns a verdict.
@@ -348,7 +368,9 @@ more important of the two: a denied producer merely records 
a verdict the route
 `CamelAuthorizationException` and stops the exchange, so an unreachable server 
there fails every message outright.
 That is why the policy's check is on by default rather than opt-in like the 
producer's. Set
 `healthCheckEnabled=false` on the policy for a route that should stay ready 
regardless — one running `failOpen`, say
-— in preference to hiding the check with `camel.health.exclude-pattern`.
+— in preference to hiding the check with `camel.health.exclude-pattern`. A 
policy running `failOpen` marks the
+exchanges it let through with `CamelOpaDecisionFailedOpen`, exactly as the 
producer does; the evaluator is shared,
+so everything in <<failure-handling>> about that header applies to 
`.policy(...)` too.
 
 Neither check is registered when an `opaClient` was injected: that client may 
point anywhere and neither the
 endpoint nor the policy has a way to ask it where, so probing the configured 
`serverUrl` would report on a server
diff --git 
a/components/camel-opa/src/main/java/org/apache/camel/component/opa/OpaConstants.java
 
b/components/camel-opa/src/main/java/org/apache/camel/component/opa/OpaConstants.java
index 2c04119f583c..60811d4b9b83 100644
--- 
a/components/camel-opa/src/main/java/org/apache/camel/component/opa/OpaConstants.java
+++ 
b/components/camel-opa/src/main/java/org/apache/camel/component/opa/OpaConstants.java
@@ -39,6 +39,14 @@ public final class OpaConstants {
               javaType = "String")
     public static final String POLICY_PATH = HEADER_PREFIX + "PolicyPath";
 
+    @Metadata(label = "producer",
+              description = "Set to true only when the exchange proceeded 
because failOpen is enabled and the policy"
+                            + " could not be evaluated - nothing authorized 
it. Absent on every decision an actual"
+                            + " policy made, so a route or an audit trail can 
tell the two apart rather than seeing"
+                            + " the same CamelOpaDecisionAllow=true for both.",
+              javaType = "Boolean")
+    public static final String DECISION_FAILED_OPEN = HEADER_PREFIX + 
"DecisionFailedOpen";
+
     private OpaConstants() {
     }
 }
diff --git 
a/components/camel-opa/src/main/java/org/apache/camel/component/opa/OpaPolicyEvaluator.java
 
b/components/camel-opa/src/main/java/org/apache/camel/component/opa/OpaPolicyEvaluator.java
index 927d186d1e76..e7ff69f0f5ea 100644
--- 
a/components/camel-opa/src/main/java/org/apache/camel/component/opa/OpaPolicyEvaluator.java
+++ 
b/components/camel-opa/src/main/java/org/apache/camel/component/opa/OpaPolicyEvaluator.java
@@ -100,6 +100,10 @@ public abstract class OpaPolicyEvaluator {
                          + " enabled. Reason: {}",
                         policyPath, e.getMessage());
                 setDecisionHeaders(exchange, null, true);
+                // the verdict header alone cannot distinguish "a policy 
allowed this" from "no policy ran and we
+                // were told to proceed"; an auditor asking which exchanges 
went through unauthorized needs a
+                // signal it can filter on, not a log line
+                
exchange.getMessage().setHeader(OpaConstants.DECISION_FAILED_OPEN, true);
                 return true;
             }
             throw new OpaPolicyEvaluationException(
@@ -233,6 +237,9 @@ public abstract class OpaPolicyEvaluator {
         message.removeHeader(OpaConstants.DECISION_ALLOW);
         message.removeHeader(OpaConstants.DECISION);
         message.removeHeader(OpaConstants.POLICY_PATH);
+        // as attacker-settable as the verdict itself: left in place, a sender 
could preload it false and make a
+        // fail-open read as a decision a policy actually made
+        message.removeHeader(OpaConstants.DECISION_FAILED_OPEN);
     }
 
     private void setDecisionHeaders(Exchange exchange, Object decision, 
boolean allowed) {
@@ -269,7 +276,8 @@ public abstract class OpaPolicyEvaluator {
     private static boolean isDecisionHeader(String name) {
         return OpaConstants.DECISION_ALLOW.equalsIgnoreCase(name)
                 || OpaConstants.DECISION.equalsIgnoreCase(name)
-                || OpaConstants.POLICY_PATH.equalsIgnoreCase(name);
+                || OpaConstants.POLICY_PATH.equalsIgnoreCase(name)
+                || OpaConstants.DECISION_FAILED_OPEN.equalsIgnoreCase(name);
     }
 
     /**
diff --git 
a/components/camel-opa/src/test/java/org/apache/camel/component/opa/OpaProducerTest.java
 
b/components/camel-opa/src/test/java/org/apache/camel/component/opa/OpaProducerTest.java
index 6127fe65fbf2..a3162cce859e 100644
--- 
a/components/camel-opa/src/test/java/org/apache/camel/component/opa/OpaProducerTest.java
+++ 
b/components/camel-opa/src/test/java/org/apache/camel/component/opa/OpaProducerTest.java
@@ -157,6 +157,52 @@ class OpaProducerTest extends CamelTestSupport {
         
assertThat(out.getMessage().getHeader(OpaConstants.DECISION_ALLOW)).isEqualTo(false);
     }
 
+    @Test
+    void marksAnExchangeThatOnlyProceededBecauseOfFailOpen() throws Exception {
+        when(client.evaluate(eq(PATH), anyMap(), 
eq(Object.class))).thenThrow(new OPAException("connection refused"));
+
+        Exchange out = template.request(ENDPOINT + "&failOpen=true", e -> {
+        });
+
+        assertThat(out.getException()).isNull();
+        
assertThat(out.getMessage().getHeader(OpaConstants.DECISION_ALLOW)).isEqualTo(true);
+        
assertThat(out.getMessage().getHeader(OpaConstants.DECISION_FAILED_OPEN)).isEqualTo(true);
+    }
+
+    @Test
+    void doesNotMarkADecisionAPolicyActuallyMade() throws Exception {
+        // the point of the marker is that it separates the two, so an allow 
from a real policy must not carry it
+        givenDecision(Boolean.TRUE);
+
+        Exchange out = template.request(ENDPOINT + "&failOpen=true", e -> {
+        });
+
+        
assertThat(out.getMessage().getHeader(OpaConstants.DECISION_ALLOW)).isEqualTo(true);
+        
assertThat(out.getMessage().getHeader(OpaConstants.DECISION_FAILED_OPEN)).isNull();
+    }
+
+    @Test
+    void doesNotLetAnInboundMessageClaimItDidNotFailOpen() throws Exception {
+        // as settable by a sender as the verdict was: left in place, 
"FailedOpen=false" would disguise an
+        // unauthorized exchange as one a policy allowed - which is the audit 
trail this header exists to give
+        when(client.evaluate(eq(PATH), anyMap(), 
eq(Object.class))).thenThrow(new OPAException("connection refused"));
+
+        Exchange out = template.request(ENDPOINT + "&failOpen=true",
+                e -> 
e.getMessage().setHeader(OpaConstants.DECISION_FAILED_OPEN, false));
+
+        
assertThat(out.getMessage().getHeader(OpaConstants.DECISION_FAILED_OPEN)).isEqualTo(true);
+    }
+
+    @Test
+    void clearsAClaimedFailOpenMarkerOnAnOrdinaryDecision() throws Exception {
+        givenDecision(Boolean.TRUE);
+
+        Exchange out = template.request(ENDPOINT,
+                e -> 
e.getMessage().setHeader(OpaConstants.DECISION_FAILED_OPEN, true));
+
+        
assertThat(out.getMessage().getHeader(OpaConstants.DECISION_FAILED_OPEN)).isNull();
+    }
+
     @Test
     void failsClosedWhenThePolicyCannotBeEvaluated() throws Exception {
         when(client.evaluate(eq(PATH), anyMap(), 
eq(Object.class))).thenThrow(new OPAException("connection refused"));
@@ -167,6 +213,8 @@ class OpaProducerTest extends CamelTestSupport {
         
assertThat(out.getException()).isInstanceOf(OpaPolicyEvaluationException.class)
                 .hasMessageContaining(PATH);
         
assertThat(out.getMessage().getHeader(OpaConstants.DECISION_ALLOW)).isNull();
+        // the marker is for the deliberate failOpen path only, not for any 
exception the evaluator happens to hit
+        
assertThat(out.getMessage().getHeader(OpaConstants.DECISION_FAILED_OPEN)).isNull();
     }
 
     @Test
diff --git 
a/components/camel-opa/src/test/java/org/apache/camel/component/opa/security/OpaSecurityPolicyTest.java
 
b/components/camel-opa/src/test/java/org/apache/camel/component/opa/security/OpaSecurityPolicyTest.java
index 55589ec971a6..c36ed3a365f3 100644
--- 
a/components/camel-opa/src/test/java/org/apache/camel/component/opa/security/OpaSecurityPolicyTest.java
+++ 
b/components/camel-opa/src/test/java/org/apache/camel/component/opa/security/OpaSecurityPolicyTest.java
@@ -43,18 +43,25 @@ class OpaSecurityPolicyTest extends CamelTestSupport {
 
     private final OPAClient client = mock(OPAClient.class);
     private final OpaSecurityPolicy policy = new OpaSecurityPolicy();
+    private final OpaSecurityPolicy failOpenPolicy = new OpaSecurityPolicy();
 
     @Override
     protected RouteBuilder createRouteBuilder() {
         policy.setPolicyPath(PATH);
         policy.setOpaClient(client);
         policy.setIncludeProperties("subject");
+        failOpenPolicy.setPolicyPath(PATH);
+        failOpenPolicy.setOpaClient(client);
+        failOpenPolicy.setFailOpen(true);
         return new RouteBuilder() {
             @Override
             public void configure() {
                 from("direct:start")
                         .policy(policy)
                         .to("mock:result");
+                from("direct:failOpen")
+                        .policy(failOpenPolicy)
+                        .to("mock:failOpen");
             }
         };
     }
@@ -111,9 +118,25 @@ class OpaSecurityPolicyTest extends CamelTestSupport {
 
         
assertThat(out.getException()).isInstanceOf(CamelAuthorizationException.class)
                 .hasCauseInstanceOf(OpaPolicyEvaluationException.class);
+        // the marker belongs to the deliberate failOpen path only - a denial 
is not a fail-open
+        
assertThat(out.getMessage().getHeader(OpaConstants.DECISION_FAILED_OPEN)).isNull();
         result.assertIsSatisfied();
     }
 
+    @Test
+    void marksAnExchangeTheFailOpenPolicyLetThrough() throws Exception {
+        when(client.evaluate(eq(PATH), anyMap(), 
eq(Object.class))).thenThrow(new OPAException("connection refused"));
+        MockEndpoint result = getMockEndpoint("mock:failOpen");
+        result.expectedMessageCount(1);
+
+        Exchange out = template.request("direct:failOpen", e -> 
e.getMessage().setBody("an order"));
+
+        assertThat(out.getException()).isNull();
+        result.assertIsSatisfied();
+        
assertThat(out.getMessage().getHeader(OpaConstants.DECISION_ALLOW)).isEqualTo(true);
+        
assertThat(out.getMessage().getHeader(OpaConstants.DECISION_FAILED_OPEN)).isEqualTo(true);
+    }
+
     @SuppressWarnings("unchecked")
     @Test
     void handsThePolicyAnIdentityCarriedAsAnExchangeProperty() throws 
Exception {
diff --git 
a/dsl/camel-endpointdsl/src/generated/java/org/apache/camel/builder/endpoint/dsl/OpaEndpointBuilderFactory.java
 
b/dsl/camel-endpointdsl/src/generated/java/org/apache/camel/builder/endpoint/dsl/OpaEndpointBuilderFactory.java
index 789bb694faa4..d71b57df2d9f 100644
--- 
a/dsl/camel-endpointdsl/src/generated/java/org/apache/camel/builder/endpoint/dsl/OpaEndpointBuilderFactory.java
+++ 
b/dsl/camel-endpointdsl/src/generated/java/org/apache/camel/builder/endpoint/dsl/OpaEndpointBuilderFactory.java
@@ -674,6 +674,22 @@ public interface OpaEndpointBuilderFactory {
         public String opaPolicyPath() {
             return "CamelOpaPolicyPath";
         }
+        /**
+         * Set to true only when the exchange proceeded because failOpen is
+         * enabled and the policy could not be evaluated - nothing authorized
+         * it. Absent on every decision an actual policy made, so a route or an
+         * audit trail can tell the two apart rather than seeing the same
+         * CamelOpaDecisionAllow=true for both.
+         * 
+         * The option is a: {@code Boolean} type.
+         * 
+         * Group: producer
+         * 
+         * @return the name of the header {@code OpaDecisionFailedOpen}.
+         */
+        public String opaDecisionFailedOpen() {
+            return "CamelOpaDecisionFailedOpen";
+        }
     }
     static OpaEndpointBuilder endpointBuilder(String componentName, String 
path) {
         class OpaEndpointBuilderImpl extends AbstractEndpointBuilder 
implements OpaEndpointBuilder, AdvancedOpaEndpointBuilder {

Reply via email to