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 0e7057e06cff CAMEL-24643: camel-opa - allow exchange properties in the
OPA input document (#26187)
0e7057e06cff is described below
commit 0e7057e06cff595c85beafeb6999e1a09f477c29
Author: Andrea Cosentino <[email protected]>
AuthorDate: Tue Sep 8 14:18:50 2026 +0200
CAMEL-24643: camel-opa - allow exchange properties in the OPA input
document (#26187)
* CAMEL-24643: camel-opa - allow exchange properties in the OPA input
document
The input document was built from message headers only, but the
authentication components deliberately keep the identity they verified in
exchange properties: camel-keycloak stores the access token and its
subject that way, and defaults preferPropertyOverHeader to true precisely
because a header can be set by the caller.
That left the two halves unable to compose. Authorizing an identity that
an earlier step established meant copying it from an exchange property
into a header first, which is the one channel the authentication step
treats as untrusted.
Adds an includeProperties option, mirroring includeHeaders, contributing a
properties object to the input document. It defaults to empty rather than
to everything: exchange properties mostly carry state between processors,
so sending them all would be noise the policy has to wade through. When
nothing is selected the properties key is omitted entirely, so the
document an existing route sends is unchanged. Only custom properties are
sent, never Camel's internal ones.
Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_017VuTsZ4GyaZyvP8zxFvCRu
Signed-off-by: Andrea Cosentino <[email protected]>
* CAMEL-24643: address review - omit the empty "properties" key from the
OPA input
When includeProperties is configured but the exchange carries none of the
listed
properties, the input document added an empty "properties" map. A Rego
policy
doing has(input, "properties") would then see true and could make a silent
wrong
authorization decision. Only add "properties" when something was actually
collected, and cover it with a test.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Claude-Session: https://claude.ai/code/session_01HQ3k5eceny77eriMtgXhco
Signed-off-by: Andrea Cosentino <[email protected]>
---------
Signed-off-by: Andrea Cosentino <[email protected]>
Co-authored-by: Claude Opus 5 (1M context) <[email protected]>
---
.../org/apache/camel/catalog/components/opa.json | 24 ++++----
.../apache/camel/catalog/docs/opa-component.adoc | 49 ++++++++++++++-
.../component/opa/OpaComponentConfigurer.java | 6 ++
.../camel/component/opa/OpaEndpointConfigurer.java | 6 ++
.../camel/component/opa/OpaEndpointUriFactory.java | 3 +-
.../org/apache/camel/component/opa/opa.json | 24 ++++----
.../camel-opa/src/main/docs/opa-component.adoc | 49 ++++++++++++++-
.../camel/component/opa/OpaConfiguration.java | 21 +++++++
.../apache/camel/component/opa/OpaEndpoint.java | 2 +-
.../camel/component/opa/OpaPolicyEvaluator.java | 58 ++++++++++++++----
.../component/opa/security/OpaSecurityPolicy.java | 16 ++++-
.../camel/component/opa/OpaInputDocumentTest.java | 69 ++++++++++++++++++++++
.../opa/security/OpaSecurityPolicyTest.java | 16 +++++
.../component/dsl/OpaComponentBuilderFactory.java | 26 ++++++++
.../endpoint/dsl/OpaEndpointBuilderFactory.java | 24 ++++++++
15 files changed, 351 insertions(+), 42 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 6ce49fbe7175..69aed6a2f817 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
@@ -28,12 +28,13 @@
"configuration": { "index": 1, "kind": "property", "displayName":
"Configuration", "group": "producer", "label": "", "required": false, "type":
"object", "javaType": "org.apache.camel.component.opa.OpaConfiguration",
"deprecated": false, "autowired": false, "secret": false, "description": "The
component configuration." },
"includeBody": { "index": 2, "kind": "property", "displayName": "Include
Body", "group": "producer", "label": "", "required": false, "type": "boolean",
"javaType": "boolean", "deprecated": false, "autowired": false, "secret":
false, "defaultValue": false, "configurationClass":
"org.apache.camel.component.opa.OpaConfiguration", "configurationField":
"configuration", "description": "Whether to send the message body to OPA as
part of the input document. Disabled by default: bodies can b [...]
"includeHeaders": { "index": 3, "kind": "property", "displayName":
"Include Headers", "group": "producer", "label": "", "required": false, "type":
"string", "javaType": "java.lang.String", "deprecated": false, "autowired":
false, "secret": false, "defaultValue": "*", "configurationClass":
"org.apache.camel.component.opa.OpaConfiguration", "configurationField":
"configuration", "description": "Comma-separated list of message header names
to send to OPA in the input document. The defau [...]
- "lazyStartProducer": { "index": 4, "kind": "property", "displayName":
"Lazy Start Producer", "group": "producer", "label": "producer", "required":
false, "type": "boolean", "javaType": "boolean", "deprecated": false,
"autowired": false, "secret": false, "defaultValue": false, "description":
"Whether the producer should be started lazy (on the first message). By
starting lazy you can use this to allow CamelContext and routes to startup in
situations where a producer may otherwise fail [...]
- "serverUrl": { "index": 5, "kind": "property", "displayName": "Server
Url", "group": "producer", "label": "", "required": false, "type": "string",
"javaType": "java.lang.String", "deprecated": false, "autowired": false,
"secret": false, "defaultValue": "http:\/\/localhost:8181",
"configurationClass": "org.apache.camel.component.opa.OpaConfiguration",
"configurationField": "configuration", "description": "The base URL of the OPA
server, without the {code \/v1\/data} suffix. The defaul [...]
- "autowiredEnabled": { "index": 6, "kind": "property", "displayName":
"Autowired Enabled", "group": "advanced", "label": "advanced", "required":
false, "type": "boolean", "javaType": "boolean", "deprecated": false,
"autowired": false, "secret": false, "defaultValue": true, "description":
"Whether autowiring is enabled. This is used for automatic autowiring options
(the option must be marked as autowired) by looking up in the registry to find
if there is a single instance of matching t [...]
- "opaClient": { "index": 7, "kind": "property", "displayName": "Opa
Client", "group": "advanced", "label": "advanced", "required": false, "type":
"object", "javaType": "com.styra.opa.OPAClient", "deprecated": false,
"deprecationNote": "", "autowired": true, "secret": false,
"configurationClass": "org.apache.camel.component.opa.OpaConfiguration",
"configurationField": "configuration", "description": "An existing OPAClient to
use. When set, serverUrl and bearerToken are ignored." },
- "bearerToken": { "index": 8, "kind": "property", "displayName": "Bearer
Token", "group": "security", "label": "security", "required": false, "type":
"string", "javaType": "java.lang.String", "deprecated": false, "autowired":
false, "secret": true, "security": "secret", "configurationClass":
"org.apache.camel.component.opa.OpaConfiguration", "configurationField":
"configuration", "description": "Bearer token sent to the OPA server in the
Authorization header, for an OPA instance that [...]
- "failOpen": { "index": 9, "kind": "property", "displayName": "Fail Open",
"group": "security", "label": "security", "required": false, "type": "boolean",
"javaType": "boolean", "deprecated": false, "autowired": false, "secret":
false, "security": "insecure:dev", "defaultValue": false, "configurationClass":
"org.apache.camel.component.opa.OpaConfiguration", "configurationField":
"configuration", "description": "Whether to allow the exchange to proceed when
the policy cannot be evaluat [...]
+ "includeProperties": { "index": 4, "kind": "property", "displayName":
"Include Properties", "group": "producer", "label": "", "required": false,
"type": "string", "javaType": "java.lang.String", "deprecated": false,
"autowired": false, "secret": false, "configurationClass":
"org.apache.camel.component.opa.OpaConfiguration", "configurationField":
"configuration", "description": "Comma-separated list of exchange property
names to send to OPA in the input document, or {code } for all of [...]
+ "lazyStartProducer": { "index": 5, "kind": "property", "displayName":
"Lazy Start Producer", "group": "producer", "label": "producer", "required":
false, "type": "boolean", "javaType": "boolean", "deprecated": false,
"autowired": false, "secret": false, "defaultValue": false, "description":
"Whether the producer should be started lazy (on the first message). By
starting lazy you can use this to allow CamelContext and routes to startup in
situations where a producer may otherwise fail [...]
+ "serverUrl": { "index": 6, "kind": "property", "displayName": "Server
Url", "group": "producer", "label": "", "required": false, "type": "string",
"javaType": "java.lang.String", "deprecated": false, "autowired": false,
"secret": false, "defaultValue": "http:\/\/localhost:8181",
"configurationClass": "org.apache.camel.component.opa.OpaConfiguration",
"configurationField": "configuration", "description": "The base URL of the OPA
server, without the {code \/v1\/data} suffix. The defaul [...]
+ "autowiredEnabled": { "index": 7, "kind": "property", "displayName":
"Autowired Enabled", "group": "advanced", "label": "advanced", "required":
false, "type": "boolean", "javaType": "boolean", "deprecated": false,
"autowired": false, "secret": false, "defaultValue": true, "description":
"Whether autowiring is enabled. This is used for automatic autowiring options
(the option must be marked as autowired) by looking up in the registry to find
if there is a single instance of matching t [...]
+ "opaClient": { "index": 8, "kind": "property", "displayName": "Opa
Client", "group": "advanced", "label": "advanced", "required": false, "type":
"object", "javaType": "com.styra.opa.OPAClient", "deprecated": false,
"deprecationNote": "", "autowired": true, "secret": false,
"configurationClass": "org.apache.camel.component.opa.OpaConfiguration",
"configurationField": "configuration", "description": "An existing OPAClient to
use. When set, serverUrl and bearerToken are ignored." },
+ "bearerToken": { "index": 9, "kind": "property", "displayName": "Bearer
Token", "group": "security", "label": "security", "required": false, "type":
"string", "javaType": "java.lang.String", "deprecated": false, "autowired":
false, "secret": true, "security": "secret", "configurationClass":
"org.apache.camel.component.opa.OpaConfiguration", "configurationField":
"configuration", "description": "Bearer token sent to the OPA server in the
Authorization header, for an OPA instance that [...]
+ "failOpen": { "index": 10, "kind": "property", "displayName": "Fail Open",
"group": "security", "label": "security", "required": false, "type": "boolean",
"javaType": "boolean", "deprecated": false, "autowired": false, "secret":
false, "security": "insecure:dev", "defaultValue": false, "configurationClass":
"org.apache.camel.component.opa.OpaConfiguration", "configurationField":
"configuration", "description": "Whether to allow the exchange to proceed when
the policy cannot be evalua [...]
},
"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" },
@@ -45,10 +46,11 @@
"allowKey": { "index": 1, "kind": "parameter", "displayName": "Allow Key",
"group": "producer", "label": "", "required": false, "type": "string",
"javaType": "java.lang.String", "deprecated": false, "autowired": false,
"secret": false, "defaultValue": "allow", "configurationClass":
"org.apache.camel.component.opa.OpaConfiguration", "configurationField":
"configuration", "description": "The key to read the allow\/deny verdict from
when the policy returns an object rather than a plain [...]
"includeBody": { "index": 2, "kind": "parameter", "displayName": "Include
Body", "group": "producer", "label": "", "required": false, "type": "boolean",
"javaType": "boolean", "deprecated": false, "autowired": false, "secret":
false, "defaultValue": false, "configurationClass":
"org.apache.camel.component.opa.OpaConfiguration", "configurationField":
"configuration", "description": "Whether to send the message body to OPA as
part of the input document. Disabled by default: bodies can [...]
"includeHeaders": { "index": 3, "kind": "parameter", "displayName":
"Include Headers", "group": "producer", "label": "", "required": false, "type":
"string", "javaType": "java.lang.String", "deprecated": false, "autowired":
false, "secret": false, "defaultValue": "*", "configurationClass":
"org.apache.camel.component.opa.OpaConfiguration", "configurationField":
"configuration", "description": "Comma-separated list of message header names
to send to OPA in the input document. The defa [...]
- "serverUrl": { "index": 4, "kind": "parameter", "displayName": "Server
Url", "group": "producer", "label": "", "required": false, "type": "string",
"javaType": "java.lang.String", "deprecated": false, "autowired": false,
"secret": false, "defaultValue": "http:\/\/localhost:8181",
"configurationClass": "org.apache.camel.component.opa.OpaConfiguration",
"configurationField": "configuration", "description": "The base URL of the OPA
server, without the {code \/v1\/data} suffix. The defau [...]
- "lazyStartProducer": { "index": 5, "kind": "parameter", "displayName":
"Lazy Start Producer", "group": "producer (advanced)", "label":
"producer,advanced", "required": false, "type": "boolean", "javaType":
"boolean", "deprecated": false, "autowired": false, "secret": false,
"defaultValue": false, "description": "Whether the producer should be started
lazy (on the first message). By starting lazy you can use this to allow
CamelContext and routes to startup in situations where a produc [...]
- "opaClient": { "index": 6, "kind": "parameter", "displayName": "Opa
Client", "group": "advanced", "label": "advanced", "required": false, "type":
"object", "javaType": "com.styra.opa.OPAClient", "deprecated": false,
"deprecationNote": "", "autowired": true, "secret": false,
"configurationClass": "org.apache.camel.component.opa.OpaConfiguration",
"configurationField": "configuration", "description": "An existing OPAClient to
use. When set, serverUrl and bearerToken are ignored." },
- "bearerToken": { "index": 7, "kind": "parameter", "displayName": "Bearer
Token", "group": "security", "label": "security", "required": false, "type":
"string", "javaType": "java.lang.String", "deprecated": false, "autowired":
false, "secret": true, "security": "secret", "configurationClass":
"org.apache.camel.component.opa.OpaConfiguration", "configurationField":
"configuration", "description": "Bearer token sent to the OPA server in the
Authorization header, for an OPA instance that [...]
- "failOpen": { "index": 8, "kind": "parameter", "displayName": "Fail Open",
"group": "security", "label": "security", "required": false, "type": "boolean",
"javaType": "boolean", "deprecated": false, "autowired": false, "secret":
false, "security": "insecure:dev", "defaultValue": false, "configurationClass":
"org.apache.camel.component.opa.OpaConfiguration", "configurationField":
"configuration", "description": "Whether to allow the exchange to proceed when
the policy cannot be evalua [...]
+ "includeProperties": { "index": 4, "kind": "parameter", "displayName":
"Include Properties", "group": "producer", "label": "", "required": false,
"type": "string", "javaType": "java.lang.String", "deprecated": false,
"autowired": false, "secret": false, "configurationClass":
"org.apache.camel.component.opa.OpaConfiguration", "configurationField":
"configuration", "description": "Comma-separated list of exchange property
names to send to OPA in the input document, or {code } for all o [...]
+ "serverUrl": { "index": 5, "kind": "parameter", "displayName": "Server
Url", "group": "producer", "label": "", "required": false, "type": "string",
"javaType": "java.lang.String", "deprecated": false, "autowired": false,
"secret": false, "defaultValue": "http:\/\/localhost:8181",
"configurationClass": "org.apache.camel.component.opa.OpaConfiguration",
"configurationField": "configuration", "description": "The base URL of the OPA
server, without the {code \/v1\/data} suffix. The defau [...]
+ "lazyStartProducer": { "index": 6, "kind": "parameter", "displayName":
"Lazy Start Producer", "group": "producer (advanced)", "label":
"producer,advanced", "required": false, "type": "boolean", "javaType":
"boolean", "deprecated": false, "autowired": false, "secret": false,
"defaultValue": false, "description": "Whether the producer should be started
lazy (on the first message). By starting lazy you can use this to allow
CamelContext and routes to startup in situations where a produc [...]
+ "opaClient": { "index": 7, "kind": "parameter", "displayName": "Opa
Client", "group": "advanced", "label": "advanced", "required": false, "type":
"object", "javaType": "com.styra.opa.OPAClient", "deprecated": false,
"deprecationNote": "", "autowired": true, "secret": false,
"configurationClass": "org.apache.camel.component.opa.OpaConfiguration",
"configurationField": "configuration", "description": "An existing OPAClient to
use. When set, serverUrl and bearerToken are ignored." },
+ "bearerToken": { "index": 8, "kind": "parameter", "displayName": "Bearer
Token", "group": "security", "label": "security", "required": false, "type":
"string", "javaType": "java.lang.String", "deprecated": false, "autowired":
false, "secret": true, "security": "secret", "configurationClass":
"org.apache.camel.component.opa.OpaConfiguration", "configurationField":
"configuration", "description": "Bearer token sent to the OPA server in the
Authorization header, for an OPA instance that [...]
+ "failOpen": { "index": 9, "kind": "parameter", "displayName": "Fail Open",
"group": "security", "label": "security", "required": false, "type": "boolean",
"javaType": "boolean", "deprecated": false, "autowired": false, "secret":
false, "security": "insecure:dev", "defaultValue": false, "configurationClass":
"org.apache.camel.component.opa.OpaConfiguration", "configurationField":
"configuration", "description": "Whether to allow the exchange to proceed when
the policy cannot be evalua [...]
}
}
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 aa9dce34bf68..a42b872019e1 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
@@ -95,6 +95,7 @@ Camel sends OPA an `input` document shaped like this:
------------------------------------------------------------
{
"headers": { "user": "alice", "CamelHttpMethod": "POST" },
+ "properties": { "CamelKeycloakTokenSubject": "alice" },
"body": "...",
"exchangeId": "1767DC33923810E-0000000000000000",
"routeId": "orders"
@@ -124,6 +125,8 @@ alongside it shows the matching routes end to end.
is enabled: bodies can be large or streaming, and most authorization decisions
do not need them. When you do
enable it on a streaming body, enable stream caching so the body is still
readable by the rest of the route.
+`properties` is empty unless you ask for it — see <<authorizing-an-identity>>
below.
+
Header and body values that are not JSON-native are converted to their string
form. That conversion is
shallow: a `Map` or `List` value is passed through as-is, so anything
non-JSON-native nested inside it is left
for the OPA SDK's serializer to render. A policy that reads nested structures
should not assume the same string
@@ -146,6 +149,48 @@ boolean verdict read out of it:
Both headers are written on every evaluation, so a verdict set by an inbound
message never survives into the
route.
+[#authorizing-an-identity]
+== Authorizing an identity
+
+The component decides; it does not authenticate. Establish who the caller is
first, then let the policy authorize
+the identity that step produced.
+
+The catch is that Camel's authentication components deliberately keep the
identity they verified in *exchange
+properties* rather than in headers — `camel-keycloak` stores the access token
and its subject that way and its
+`preferPropertyOverHeader` option defaults to `true`, precisely because a
header can be set by the caller. So the
+identity you want to authorize is usually not in the header map.
+
+Name those properties in `includeProperties` and they arrive in the input
document under `properties`:
+
+[source,java]
+------------------------------------------------------------
+from("platform-http:/orders")
+ .policy(keycloakPolicy) // authenticates,
stores the subject as a property
+ .to("opa:authz/orders/allow?includeProperties=CamelKeycloakTokenSubject")
+ .filter(header(OpaConstants.DECISION_ALLOW).isEqualTo(true))
+ .to("direct:handleOrder");
+------------------------------------------------------------
+
+and the policy reads them the same way it reads headers:
+
+[source,rego]
+------------------------------------------------------------
+allow if {
+ input.properties.CamelKeycloakTokenSubject == "alice"
+}
+------------------------------------------------------------
+
+`includeProperties` takes a comma-separated list, or `*` for all of them, and
matches names case-insensitively.
+Unlike `includeHeaders` it is *empty by default*: exchange properties are
mostly used to carry state between
+processors, so sending them all would be noise the policy has to wade through.
Only custom properties are sent —
+Camel's own internal exchange properties are never included.
+
+Prefer this over copying the identity into a header before the `opa:`
endpoint. A header is exactly the channel the
+authentication step treated as untrusted, so moving a verified identity into
one to get it past this component
+undoes the check that produced it.
+
+`OpaSecurityPolicy` takes the same option through `setIncludeProperties`.
+
== Failure handling
The component fails closed. If the policy cannot be evaluated at all — the OPA
server is unreachable, times out,
@@ -168,5 +213,5 @@ so the policy always returns a verdict.
* OPA is a trusted component, typically running as a sidecar reachable only
from the application. When it is not,
put the connection on a trusted network and authenticate to it with
`bearerToken`.
* The component decides; it does not authenticate. Establish *who* the caller
is first — with SPIFFE workload
- identity, a verified JWT, or the surrounding transport's authentication —
and let the policy authorize the
- identity that step produced.
+ identity, a verified JWT, or the surrounding transport's authentication —
and hand the policy that identity
+ through `includeProperties` rather than through a header, as
<<authorizing-an-identity>> explains.
diff --git
a/components/camel-opa/src/generated/java/org/apache/camel/component/opa/OpaComponentConfigurer.java
b/components/camel-opa/src/generated/java/org/apache/camel/component/opa/OpaComponentConfigurer.java
index ad6b93cbe7f1..e41a4fe473e6 100644
---
a/components/camel-opa/src/generated/java/org/apache/camel/component/opa/OpaComponentConfigurer.java
+++
b/components/camel-opa/src/generated/java/org/apache/camel/component/opa/OpaComponentConfigurer.java
@@ -43,6 +43,8 @@ public class OpaComponentConfigurer extends
PropertyConfigurerSupport implements
case "includeBody":
getOrCreateConfiguration(target).setIncludeBody(property(camelContext,
boolean.class, value)); return true;
case "includeheaders":
case "includeHeaders":
getOrCreateConfiguration(target).setIncludeHeaders(property(camelContext,
java.lang.String.class, value)); return true;
+ case "includeproperties":
+ case "includeProperties":
getOrCreateConfiguration(target).setIncludeProperties(property(camelContext,
java.lang.String.class, value)); return true;
case "lazystartproducer":
case "lazyStartProducer":
target.setLazyStartProducer(property(camelContext, boolean.class, value));
return true;
case "opaclient":
@@ -74,6 +76,8 @@ public class OpaComponentConfigurer extends
PropertyConfigurerSupport implements
case "includeBody": return boolean.class;
case "includeheaders":
case "includeHeaders": return java.lang.String.class;
+ case "includeproperties":
+ case "includeProperties": return java.lang.String.class;
case "lazystartproducer":
case "lazyStartProducer": return boolean.class;
case "opaclient":
@@ -101,6 +105,8 @@ public class OpaComponentConfigurer extends
PropertyConfigurerSupport implements
case "includeBody": return
getOrCreateConfiguration(target).isIncludeBody();
case "includeheaders":
case "includeHeaders": return
getOrCreateConfiguration(target).getIncludeHeaders();
+ case "includeproperties":
+ case "includeProperties": return
getOrCreateConfiguration(target).getIncludeProperties();
case "lazystartproducer":
case "lazyStartProducer": return target.isLazyStartProducer();
case "opaclient":
diff --git
a/components/camel-opa/src/generated/java/org/apache/camel/component/opa/OpaEndpointConfigurer.java
b/components/camel-opa/src/generated/java/org/apache/camel/component/opa/OpaEndpointConfigurer.java
index 94c7d0294376..4adc1c32c46a 100644
---
a/components/camel-opa/src/generated/java/org/apache/camel/component/opa/OpaEndpointConfigurer.java
+++
b/components/camel-opa/src/generated/java/org/apache/camel/component/opa/OpaEndpointConfigurer.java
@@ -33,6 +33,8 @@ public class OpaEndpointConfigurer extends
PropertyConfigurerSupport implements
case "includeBody":
target.getConfiguration().setIncludeBody(property(camelContext, boolean.class,
value)); return true;
case "includeheaders":
case "includeHeaders":
target.getConfiguration().setIncludeHeaders(property(camelContext,
java.lang.String.class, value)); return true;
+ case "includeproperties":
+ case "includeProperties":
target.getConfiguration().setIncludeProperties(property(camelContext,
java.lang.String.class, value)); return true;
case "lazystartproducer":
case "lazyStartProducer":
target.setLazyStartProducer(property(camelContext, boolean.class, value));
return true;
case "opaclient":
@@ -61,6 +63,8 @@ public class OpaEndpointConfigurer extends
PropertyConfigurerSupport implements
case "includeBody": return boolean.class;
case "includeheaders":
case "includeHeaders": return java.lang.String.class;
+ case "includeproperties":
+ case "includeProperties": return java.lang.String.class;
case "lazystartproducer":
case "lazyStartProducer": return boolean.class;
case "opaclient":
@@ -85,6 +89,8 @@ public class OpaEndpointConfigurer extends
PropertyConfigurerSupport implements
case "includeBody": return target.getConfiguration().isIncludeBody();
case "includeheaders":
case "includeHeaders": return
target.getConfiguration().getIncludeHeaders();
+ case "includeproperties":
+ case "includeProperties": return
target.getConfiguration().getIncludeProperties();
case "lazystartproducer":
case "lazyStartProducer": return target.isLazyStartProducer();
case "opaclient":
diff --git
a/components/camel-opa/src/generated/java/org/apache/camel/component/opa/OpaEndpointUriFactory.java
b/components/camel-opa/src/generated/java/org/apache/camel/component/opa/OpaEndpointUriFactory.java
index 5940cbbc4222..afd9836e8644 100644
---
a/components/camel-opa/src/generated/java/org/apache/camel/component/opa/OpaEndpointUriFactory.java
+++
b/components/camel-opa/src/generated/java/org/apache/camel/component/opa/OpaEndpointUriFactory.java
@@ -24,12 +24,13 @@ public class OpaEndpointUriFactory extends
org.apache.camel.support.component.En
private static final Set<String> ENDPOINT_IDENTITY_PROPERTY_NAMES;
private static final Map<String, String> MULTI_VALUE_PREFIXES;
static {
- Set<String> props = new HashSet<>(9);
+ Set<String> props = new HashSet<>(10);
props.add("allowKey");
props.add("bearerToken");
props.add("failOpen");
props.add("includeBody");
props.add("includeHeaders");
+ props.add("includeProperties");
props.add("lazyStartProducer");
props.add("opaClient");
props.add("policyPath");
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 6ce49fbe7175..69aed6a2f817 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
@@ -28,12 +28,13 @@
"configuration": { "index": 1, "kind": "property", "displayName":
"Configuration", "group": "producer", "label": "", "required": false, "type":
"object", "javaType": "org.apache.camel.component.opa.OpaConfiguration",
"deprecated": false, "autowired": false, "secret": false, "description": "The
component configuration." },
"includeBody": { "index": 2, "kind": "property", "displayName": "Include
Body", "group": "producer", "label": "", "required": false, "type": "boolean",
"javaType": "boolean", "deprecated": false, "autowired": false, "secret":
false, "defaultValue": false, "configurationClass":
"org.apache.camel.component.opa.OpaConfiguration", "configurationField":
"configuration", "description": "Whether to send the message body to OPA as
part of the input document. Disabled by default: bodies can b [...]
"includeHeaders": { "index": 3, "kind": "property", "displayName":
"Include Headers", "group": "producer", "label": "", "required": false, "type":
"string", "javaType": "java.lang.String", "deprecated": false, "autowired":
false, "secret": false, "defaultValue": "*", "configurationClass":
"org.apache.camel.component.opa.OpaConfiguration", "configurationField":
"configuration", "description": "Comma-separated list of message header names
to send to OPA in the input document. The defau [...]
- "lazyStartProducer": { "index": 4, "kind": "property", "displayName":
"Lazy Start Producer", "group": "producer", "label": "producer", "required":
false, "type": "boolean", "javaType": "boolean", "deprecated": false,
"autowired": false, "secret": false, "defaultValue": false, "description":
"Whether the producer should be started lazy (on the first message). By
starting lazy you can use this to allow CamelContext and routes to startup in
situations where a producer may otherwise fail [...]
- "serverUrl": { "index": 5, "kind": "property", "displayName": "Server
Url", "group": "producer", "label": "", "required": false, "type": "string",
"javaType": "java.lang.String", "deprecated": false, "autowired": false,
"secret": false, "defaultValue": "http:\/\/localhost:8181",
"configurationClass": "org.apache.camel.component.opa.OpaConfiguration",
"configurationField": "configuration", "description": "The base URL of the OPA
server, without the {code \/v1\/data} suffix. The defaul [...]
- "autowiredEnabled": { "index": 6, "kind": "property", "displayName":
"Autowired Enabled", "group": "advanced", "label": "advanced", "required":
false, "type": "boolean", "javaType": "boolean", "deprecated": false,
"autowired": false, "secret": false, "defaultValue": true, "description":
"Whether autowiring is enabled. This is used for automatic autowiring options
(the option must be marked as autowired) by looking up in the registry to find
if there is a single instance of matching t [...]
- "opaClient": { "index": 7, "kind": "property", "displayName": "Opa
Client", "group": "advanced", "label": "advanced", "required": false, "type":
"object", "javaType": "com.styra.opa.OPAClient", "deprecated": false,
"deprecationNote": "", "autowired": true, "secret": false,
"configurationClass": "org.apache.camel.component.opa.OpaConfiguration",
"configurationField": "configuration", "description": "An existing OPAClient to
use. When set, serverUrl and bearerToken are ignored." },
- "bearerToken": { "index": 8, "kind": "property", "displayName": "Bearer
Token", "group": "security", "label": "security", "required": false, "type":
"string", "javaType": "java.lang.String", "deprecated": false, "autowired":
false, "secret": true, "security": "secret", "configurationClass":
"org.apache.camel.component.opa.OpaConfiguration", "configurationField":
"configuration", "description": "Bearer token sent to the OPA server in the
Authorization header, for an OPA instance that [...]
- "failOpen": { "index": 9, "kind": "property", "displayName": "Fail Open",
"group": "security", "label": "security", "required": false, "type": "boolean",
"javaType": "boolean", "deprecated": false, "autowired": false, "secret":
false, "security": "insecure:dev", "defaultValue": false, "configurationClass":
"org.apache.camel.component.opa.OpaConfiguration", "configurationField":
"configuration", "description": "Whether to allow the exchange to proceed when
the policy cannot be evaluat [...]
+ "includeProperties": { "index": 4, "kind": "property", "displayName":
"Include Properties", "group": "producer", "label": "", "required": false,
"type": "string", "javaType": "java.lang.String", "deprecated": false,
"autowired": false, "secret": false, "configurationClass":
"org.apache.camel.component.opa.OpaConfiguration", "configurationField":
"configuration", "description": "Comma-separated list of exchange property
names to send to OPA in the input document, or {code } for all of [...]
+ "lazyStartProducer": { "index": 5, "kind": "property", "displayName":
"Lazy Start Producer", "group": "producer", "label": "producer", "required":
false, "type": "boolean", "javaType": "boolean", "deprecated": false,
"autowired": false, "secret": false, "defaultValue": false, "description":
"Whether the producer should be started lazy (on the first message). By
starting lazy you can use this to allow CamelContext and routes to startup in
situations where a producer may otherwise fail [...]
+ "serverUrl": { "index": 6, "kind": "property", "displayName": "Server
Url", "group": "producer", "label": "", "required": false, "type": "string",
"javaType": "java.lang.String", "deprecated": false, "autowired": false,
"secret": false, "defaultValue": "http:\/\/localhost:8181",
"configurationClass": "org.apache.camel.component.opa.OpaConfiguration",
"configurationField": "configuration", "description": "The base URL of the OPA
server, without the {code \/v1\/data} suffix. The defaul [...]
+ "autowiredEnabled": { "index": 7, "kind": "property", "displayName":
"Autowired Enabled", "group": "advanced", "label": "advanced", "required":
false, "type": "boolean", "javaType": "boolean", "deprecated": false,
"autowired": false, "secret": false, "defaultValue": true, "description":
"Whether autowiring is enabled. This is used for automatic autowiring options
(the option must be marked as autowired) by looking up in the registry to find
if there is a single instance of matching t [...]
+ "opaClient": { "index": 8, "kind": "property", "displayName": "Opa
Client", "group": "advanced", "label": "advanced", "required": false, "type":
"object", "javaType": "com.styra.opa.OPAClient", "deprecated": false,
"deprecationNote": "", "autowired": true, "secret": false,
"configurationClass": "org.apache.camel.component.opa.OpaConfiguration",
"configurationField": "configuration", "description": "An existing OPAClient to
use. When set, serverUrl and bearerToken are ignored." },
+ "bearerToken": { "index": 9, "kind": "property", "displayName": "Bearer
Token", "group": "security", "label": "security", "required": false, "type":
"string", "javaType": "java.lang.String", "deprecated": false, "autowired":
false, "secret": true, "security": "secret", "configurationClass":
"org.apache.camel.component.opa.OpaConfiguration", "configurationField":
"configuration", "description": "Bearer token sent to the OPA server in the
Authorization header, for an OPA instance that [...]
+ "failOpen": { "index": 10, "kind": "property", "displayName": "Fail Open",
"group": "security", "label": "security", "required": false, "type": "boolean",
"javaType": "boolean", "deprecated": false, "autowired": false, "secret":
false, "security": "insecure:dev", "defaultValue": false, "configurationClass":
"org.apache.camel.component.opa.OpaConfiguration", "configurationField":
"configuration", "description": "Whether to allow the exchange to proceed when
the policy cannot be evalua [...]
},
"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" },
@@ -45,10 +46,11 @@
"allowKey": { "index": 1, "kind": "parameter", "displayName": "Allow Key",
"group": "producer", "label": "", "required": false, "type": "string",
"javaType": "java.lang.String", "deprecated": false, "autowired": false,
"secret": false, "defaultValue": "allow", "configurationClass":
"org.apache.camel.component.opa.OpaConfiguration", "configurationField":
"configuration", "description": "The key to read the allow\/deny verdict from
when the policy returns an object rather than a plain [...]
"includeBody": { "index": 2, "kind": "parameter", "displayName": "Include
Body", "group": "producer", "label": "", "required": false, "type": "boolean",
"javaType": "boolean", "deprecated": false, "autowired": false, "secret":
false, "defaultValue": false, "configurationClass":
"org.apache.camel.component.opa.OpaConfiguration", "configurationField":
"configuration", "description": "Whether to send the message body to OPA as
part of the input document. Disabled by default: bodies can [...]
"includeHeaders": { "index": 3, "kind": "parameter", "displayName":
"Include Headers", "group": "producer", "label": "", "required": false, "type":
"string", "javaType": "java.lang.String", "deprecated": false, "autowired":
false, "secret": false, "defaultValue": "*", "configurationClass":
"org.apache.camel.component.opa.OpaConfiguration", "configurationField":
"configuration", "description": "Comma-separated list of message header names
to send to OPA in the input document. The defa [...]
- "serverUrl": { "index": 4, "kind": "parameter", "displayName": "Server
Url", "group": "producer", "label": "", "required": false, "type": "string",
"javaType": "java.lang.String", "deprecated": false, "autowired": false,
"secret": false, "defaultValue": "http:\/\/localhost:8181",
"configurationClass": "org.apache.camel.component.opa.OpaConfiguration",
"configurationField": "configuration", "description": "The base URL of the OPA
server, without the {code \/v1\/data} suffix. The defau [...]
- "lazyStartProducer": { "index": 5, "kind": "parameter", "displayName":
"Lazy Start Producer", "group": "producer (advanced)", "label":
"producer,advanced", "required": false, "type": "boolean", "javaType":
"boolean", "deprecated": false, "autowired": false, "secret": false,
"defaultValue": false, "description": "Whether the producer should be started
lazy (on the first message). By starting lazy you can use this to allow
CamelContext and routes to startup in situations where a produc [...]
- "opaClient": { "index": 6, "kind": "parameter", "displayName": "Opa
Client", "group": "advanced", "label": "advanced", "required": false, "type":
"object", "javaType": "com.styra.opa.OPAClient", "deprecated": false,
"deprecationNote": "", "autowired": true, "secret": false,
"configurationClass": "org.apache.camel.component.opa.OpaConfiguration",
"configurationField": "configuration", "description": "An existing OPAClient to
use. When set, serverUrl and bearerToken are ignored." },
- "bearerToken": { "index": 7, "kind": "parameter", "displayName": "Bearer
Token", "group": "security", "label": "security", "required": false, "type":
"string", "javaType": "java.lang.String", "deprecated": false, "autowired":
false, "secret": true, "security": "secret", "configurationClass":
"org.apache.camel.component.opa.OpaConfiguration", "configurationField":
"configuration", "description": "Bearer token sent to the OPA server in the
Authorization header, for an OPA instance that [...]
- "failOpen": { "index": 8, "kind": "parameter", "displayName": "Fail Open",
"group": "security", "label": "security", "required": false, "type": "boolean",
"javaType": "boolean", "deprecated": false, "autowired": false, "secret":
false, "security": "insecure:dev", "defaultValue": false, "configurationClass":
"org.apache.camel.component.opa.OpaConfiguration", "configurationField":
"configuration", "description": "Whether to allow the exchange to proceed when
the policy cannot be evalua [...]
+ "includeProperties": { "index": 4, "kind": "parameter", "displayName":
"Include Properties", "group": "producer", "label": "", "required": false,
"type": "string", "javaType": "java.lang.String", "deprecated": false,
"autowired": false, "secret": false, "configurationClass":
"org.apache.camel.component.opa.OpaConfiguration", "configurationField":
"configuration", "description": "Comma-separated list of exchange property
names to send to OPA in the input document, or {code } for all o [...]
+ "serverUrl": { "index": 5, "kind": "parameter", "displayName": "Server
Url", "group": "producer", "label": "", "required": false, "type": "string",
"javaType": "java.lang.String", "deprecated": false, "autowired": false,
"secret": false, "defaultValue": "http:\/\/localhost:8181",
"configurationClass": "org.apache.camel.component.opa.OpaConfiguration",
"configurationField": "configuration", "description": "The base URL of the OPA
server, without the {code \/v1\/data} suffix. The defau [...]
+ "lazyStartProducer": { "index": 6, "kind": "parameter", "displayName":
"Lazy Start Producer", "group": "producer (advanced)", "label":
"producer,advanced", "required": false, "type": "boolean", "javaType":
"boolean", "deprecated": false, "autowired": false, "secret": false,
"defaultValue": false, "description": "Whether the producer should be started
lazy (on the first message). By starting lazy you can use this to allow
CamelContext and routes to startup in situations where a produc [...]
+ "opaClient": { "index": 7, "kind": "parameter", "displayName": "Opa
Client", "group": "advanced", "label": "advanced", "required": false, "type":
"object", "javaType": "com.styra.opa.OPAClient", "deprecated": false,
"deprecationNote": "", "autowired": true, "secret": false,
"configurationClass": "org.apache.camel.component.opa.OpaConfiguration",
"configurationField": "configuration", "description": "An existing OPAClient to
use. When set, serverUrl and bearerToken are ignored." },
+ "bearerToken": { "index": 8, "kind": "parameter", "displayName": "Bearer
Token", "group": "security", "label": "security", "required": false, "type":
"string", "javaType": "java.lang.String", "deprecated": false, "autowired":
false, "secret": true, "security": "secret", "configurationClass":
"org.apache.camel.component.opa.OpaConfiguration", "configurationField":
"configuration", "description": "Bearer token sent to the OPA server in the
Authorization header, for an OPA instance that [...]
+ "failOpen": { "index": 9, "kind": "parameter", "displayName": "Fail Open",
"group": "security", "label": "security", "required": false, "type": "boolean",
"javaType": "boolean", "deprecated": false, "autowired": false, "secret":
false, "security": "insecure:dev", "defaultValue": false, "configurationClass":
"org.apache.camel.component.opa.OpaConfiguration", "configurationField":
"configuration", "description": "Whether to allow the exchange to proceed when
the policy cannot be evalua [...]
}
}
diff --git a/components/camel-opa/src/main/docs/opa-component.adoc
b/components/camel-opa/src/main/docs/opa-component.adoc
index aa9dce34bf68..a42b872019e1 100644
--- a/components/camel-opa/src/main/docs/opa-component.adoc
+++ b/components/camel-opa/src/main/docs/opa-component.adoc
@@ -95,6 +95,7 @@ Camel sends OPA an `input` document shaped like this:
------------------------------------------------------------
{
"headers": { "user": "alice", "CamelHttpMethod": "POST" },
+ "properties": { "CamelKeycloakTokenSubject": "alice" },
"body": "...",
"exchangeId": "1767DC33923810E-0000000000000000",
"routeId": "orders"
@@ -124,6 +125,8 @@ alongside it shows the matching routes end to end.
is enabled: bodies can be large or streaming, and most authorization decisions
do not need them. When you do
enable it on a streaming body, enable stream caching so the body is still
readable by the rest of the route.
+`properties` is empty unless you ask for it — see <<authorizing-an-identity>>
below.
+
Header and body values that are not JSON-native are converted to their string
form. That conversion is
shallow: a `Map` or `List` value is passed through as-is, so anything
non-JSON-native nested inside it is left
for the OPA SDK's serializer to render. A policy that reads nested structures
should not assume the same string
@@ -146,6 +149,48 @@ boolean verdict read out of it:
Both headers are written on every evaluation, so a verdict set by an inbound
message never survives into the
route.
+[#authorizing-an-identity]
+== Authorizing an identity
+
+The component decides; it does not authenticate. Establish who the caller is
first, then let the policy authorize
+the identity that step produced.
+
+The catch is that Camel's authentication components deliberately keep the
identity they verified in *exchange
+properties* rather than in headers — `camel-keycloak` stores the access token
and its subject that way and its
+`preferPropertyOverHeader` option defaults to `true`, precisely because a
header can be set by the caller. So the
+identity you want to authorize is usually not in the header map.
+
+Name those properties in `includeProperties` and they arrive in the input
document under `properties`:
+
+[source,java]
+------------------------------------------------------------
+from("platform-http:/orders")
+ .policy(keycloakPolicy) // authenticates,
stores the subject as a property
+ .to("opa:authz/orders/allow?includeProperties=CamelKeycloakTokenSubject")
+ .filter(header(OpaConstants.DECISION_ALLOW).isEqualTo(true))
+ .to("direct:handleOrder");
+------------------------------------------------------------
+
+and the policy reads them the same way it reads headers:
+
+[source,rego]
+------------------------------------------------------------
+allow if {
+ input.properties.CamelKeycloakTokenSubject == "alice"
+}
+------------------------------------------------------------
+
+`includeProperties` takes a comma-separated list, or `*` for all of them, and
matches names case-insensitively.
+Unlike `includeHeaders` it is *empty by default*: exchange properties are
mostly used to carry state between
+processors, so sending them all would be noise the policy has to wade through.
Only custom properties are sent —
+Camel's own internal exchange properties are never included.
+
+Prefer this over copying the identity into a header before the `opa:`
endpoint. A header is exactly the channel the
+authentication step treated as untrusted, so moving a verified identity into
one to get it past this component
+undoes the check that produced it.
+
+`OpaSecurityPolicy` takes the same option through `setIncludeProperties`.
+
== Failure handling
The component fails closed. If the policy cannot be evaluated at all — the OPA
server is unreachable, times out,
@@ -168,5 +213,5 @@ so the policy always returns a verdict.
* OPA is a trusted component, typically running as a sidecar reachable only
from the application. When it is not,
put the connection on a trusted network and authenticate to it with
`bearerToken`.
* The component decides; it does not authenticate. Establish *who* the caller
is first — with SPIFFE workload
- identity, a verified JWT, or the surrounding transport's authentication —
and let the policy authorize the
- identity that step produced.
+ identity, a verified JWT, or the surrounding transport's authentication —
and hand the policy that identity
+ through `includeProperties` rather than through a header, as
<<authorizing-an-identity>> explains.
diff --git
a/components/camel-opa/src/main/java/org/apache/camel/component/opa/OpaConfiguration.java
b/components/camel-opa/src/main/java/org/apache/camel/component/opa/OpaConfiguration.java
index ec18c3777cf0..acf7c943d33c 100644
---
a/components/camel-opa/src/main/java/org/apache/camel/component/opa/OpaConfiguration.java
+++
b/components/camel-opa/src/main/java/org/apache/camel/component/opa/OpaConfiguration.java
@@ -34,6 +34,9 @@ public class OpaConfiguration implements Cloneable {
@UriParam(defaultValue = "*")
private String includeHeaders = "*";
+ @UriParam
+ private String includeProperties;
+
@UriParam
private boolean includeBody;
@@ -85,6 +88,24 @@ public class OpaConfiguration implements Cloneable {
this.includeHeaders = includeHeaders;
}
+ /**
+ * Comma-separated list of exchange property names to send to OPA in the
input document, or {@code *} for all of
+ * them. Empty by default, so no properties are sent unless asked for.
+ * <p/>
+ * This is where the authentication components put the identity they
verified: {@code camel-keycloak} stores the
+ * access token and its subject as exchange properties and prefers them
over the equivalent headers, precisely
+ * because headers can be set by the caller. List those property names
here to let a policy authorize the identity
+ * an earlier step established, instead of copying it into a header first.
Only custom properties are sent; Camel's
+ * own internal exchange properties are never included.
+ */
+ public String getIncludeProperties() {
+ return includeProperties;
+ }
+
+ public void setIncludeProperties(String includeProperties) {
+ this.includeProperties = includeProperties;
+ }
+
/**
* Whether to send the message body to OPA as part of the input document.
Disabled by default: bodies can be large
* or streaming, and most authorization decisions only need headers. When
enabled on a streaming body, enable stream
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 410238871527..9bb828df47fc 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
@@ -62,7 +62,7 @@ public class OpaEndpoint extends DefaultEndpoint {
:
OpaPolicyEvaluator.createClient(configuration.getServerUrl(),
configuration.getBearerToken());
evaluator = new OpaPolicyEvaluator(
opaClient, policyPath, configuration.getAllowKey(),
configuration.getIncludeHeaders(),
- configuration.isIncludeBody(), configuration.isFailOpen());
+ configuration.getIncludeProperties(),
configuration.isIncludeBody(), configuration.isFailOpen());
}
@Override
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 6758e17ae1a3..332ce66dd8ba 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
@@ -38,21 +38,25 @@ public class OpaPolicyEvaluator {
private static final Logger LOG =
LoggerFactory.getLogger(OpaPolicyEvaluator.class);
- private static final String ALL_HEADERS = "*";
+ private static final String ALL_NAMES = "*";
private final OPAClient client;
private final String policyPath;
private final String allowKey;
private final Set<String> includedHeaders;
+ private final Set<String> includedProperties;
private final boolean includeBody;
private final boolean failOpen;
public OpaPolicyEvaluator(OPAClient client, String policyPath, String
allowKey, String includeHeaders,
- boolean includeBody, boolean failOpen) {
+ String includeProperties, boolean includeBody,
boolean failOpen) {
this.client = ObjectHelper.notNull(client, "client");
this.policyPath = ObjectHelper.notNull(policyPath, "policyPath");
this.allowKey = ObjectHelper.isNotEmpty(allowKey) ? allowKey : "allow";
- this.includedHeaders = parseIncludedHeaders(includeHeaders);
+ // headers default to all of them, exchange properties to none:
properties are mostly used to carry
+ // state between processors, so sending them all would be noise the
policy has to wade through
+ this.includedHeaders = parseNameFilter(includeHeaders, true);
+ this.includedProperties = parseNameFilter(includeProperties, false);
this.includeBody = includeBody;
this.failOpen = failOpen;
}
@@ -111,7 +115,7 @@ public class OpaPolicyEvaluator {
String name = entry.getKey();
// never feed our own decision headers back in: a policy must not
be able to read a verdict
// that an inbound message claimed for itself
- if (isDecisionHeader(name) || !isIncluded(name)) {
+ if (isDecisionHeader(name) || !isIncluded(includedHeaders, name)) {
continue;
}
Object value = toJsonSafe(exchange, entry.getValue());
@@ -120,6 +124,23 @@ public class OpaPolicyEvaluator {
}
}
input.put("headers", headers);
+ if (includesAnything(includedProperties)) {
+ Map<String, Object> properties = new LinkedHashMap<>();
+ for (Map.Entry<String, Object> entry :
exchange.getProperties().entrySet()) {
+ if (!isIncluded(includedProperties, entry.getKey())) {
+ continue;
+ }
+ Object value = toJsonSafe(exchange, entry.getValue());
+ if (value != null) {
+ properties.put(entry.getKey(), value);
+ }
+ }
+ // only expose "properties" when something was actually collected,
so a Rego policy doing
+ // has(input, "properties") is not misled into seeing an (empty)
identity that is not there
+ if (!properties.isEmpty()) {
+ input.put("properties", properties);
+ }
+ }
if (includeBody) {
input.put("body", toJsonSafe(exchange,
exchange.getMessage().getBody()));
}
@@ -156,8 +177,12 @@ public class OpaPolicyEvaluator {
exchange.getMessage().setHeader(OpaConstants.POLICY_PATH, policyPath);
}
- private boolean isIncluded(String name) {
- return includedHeaders == null || includedHeaders.contains(name);
+ private static boolean isIncluded(Set<String> filter, String name) {
+ return filter == null || filter.contains(name);
+ }
+
+ private static boolean includesAnything(Set<String> filter) {
+ return filter == null || !filter.isEmpty();
}
private static boolean isDecisionHeader(String name) {
@@ -167,20 +192,27 @@ public class OpaPolicyEvaluator {
}
/**
- * @return the header names to include, or null when every header is
included
+ * Parses a comma-separated name filter.
+ *
+ * @param names the configured value, or {@code *} for everything
+ * @param emptyMeansAll whether leaving the option unset includes every
name or none of them
+ * @return the names to include, or null when every name is
included
*/
- private static Set<String> parseIncludedHeaders(String includeHeaders) {
- if (ObjectHelper.isEmpty(includeHeaders) ||
ALL_HEADERS.equals(includeHeaders.trim())) {
+ private static Set<String> parseNameFilter(String names, boolean
emptyMeansAll) {
+ if (ObjectHelper.isEmpty(names)) {
+ return emptyMeansAll ? null : Set.of();
+ }
+ if (ALL_NAMES.equals(names.trim())) {
return null;
}
- Set<String> names = new TreeSet<>(String.CASE_INSENSITIVE_ORDER);
- for (String name : includeHeaders.split(",")) {
+ Set<String> filter = new TreeSet<>(String.CASE_INSENSITIVE_ORDER);
+ for (String name : names.split(",")) {
String trimmed = name.trim();
if (!trimmed.isEmpty()) {
- names.add(trimmed);
+ filter.add(trimmed);
}
}
- return names;
+ return filter;
}
/**
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 f1414e8d40f0..40c9ceb9c68e 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
@@ -49,6 +49,7 @@ public class OpaSecurityPolicy implements AuthorizationPolicy
{
private String policyPath;
private String allowKey = "allow";
private String includeHeaders = "*";
+ private String includeProperties;
private boolean includeBody;
private String bearerToken;
private boolean failOpen;
@@ -71,7 +72,8 @@ public class OpaSecurityPolicy implements AuthorizationPolicy
{
if (opaClient == null) {
opaClient = OpaPolicyEvaluator.createClient(serverUrl,
bearerToken);
}
- evaluator = new OpaPolicyEvaluator(opaClient, policyPath,
allowKey, includeHeaders, includeBody, failOpen);
+ evaluator = new OpaPolicyEvaluator(
+ opaClient, policyPath, allowKey, includeHeaders,
includeProperties, includeBody, failOpen);
}
}
@@ -131,6 +133,18 @@ public class OpaSecurityPolicy implements
AuthorizationPolicy {
this.includeHeaders = includeHeaders;
}
+ public String getIncludeProperties() {
+ return includeProperties;
+ }
+
+ /**
+ * Comma-separated list of exchange property names to send to OPA, or
{@code *} for all of them. Empty by default.
+ * Use it to hand the policy an identity that an earlier authentication
step stored as an exchange property.
+ */
+ public void setIncludeProperties(String includeProperties) {
+ this.includeProperties = includeProperties;
+ }
+
public boolean isIncludeBody() {
return includeBody;
}
diff --git
a/components/camel-opa/src/test/java/org/apache/camel/component/opa/OpaInputDocumentTest.java
b/components/camel-opa/src/test/java/org/apache/camel/component/opa/OpaInputDocumentTest.java
index cb0c7d7c8e37..c39ccb5d0693 100644
---
a/components/camel-opa/src/test/java/org/apache/camel/component/opa/OpaInputDocumentTest.java
+++
b/components/camel-opa/src/test/java/org/apache/camel/component/opa/OpaInputDocumentTest.java
@@ -129,6 +129,75 @@ class OpaInputDocumentTest extends CamelTestSupport {
assertThat(headersOf(input).get("when")).isInstanceOf(String.class);
}
+ @SuppressWarnings("unchecked")
+ private static Map<String, Object> propertiesOf(Map<String, Object> input)
{
+ return (Map<String, Object>) input.get("properties");
+ }
+
+ @Test
+ void sendsNoExchangePropertiesByDefault() throws Exception {
+ Map<String, Object> input = inputSentFor(ENDPOINT, e ->
e.setProperty("subject", "alice"));
+
+ assertThat(input).doesNotContainKey("properties");
+ }
+
+ @Test
+ void sendsOnlyTheListedExchangeProperties() throws Exception {
+ Map<String, Object> input = inputSentFor(ENDPOINT +
"&includeProperties=subject", e -> {
+ e.setProperty("subject", "alice");
+ e.setProperty("internalScratch", "not for the policy");
+ });
+
+
assertThat(propertiesOf(input)).containsOnlyKeys("subject").containsEntry("subject",
"alice");
+ }
+
+ @Test
+ void omitsPropertiesKeyWhenIncludeMatchesNothing() throws Exception {
+ // includeProperties is configured but the exchange carries none of
the listed properties, so the
+ // "properties" key must be absent rather than present-and-empty
(otherwise has(input, "properties") lies)
+ Map<String, Object> input = inputSentFor(ENDPOINT +
"&includeProperties=subject",
+ e -> e.setProperty("internalScratch", "not for the policy"));
+
+ assertThat(input).doesNotContainKey("properties");
+ }
+
+ @Test
+ void sendsEveryExchangePropertyWhenAskedForAll() throws Exception {
+ Map<String, Object> input = inputSentFor(ENDPOINT +
"&includeProperties=*", e -> {
+ e.setProperty("subject", "alice");
+ e.setProperty("tenant", "acme");
+ });
+
+ assertThat(propertiesOf(input)).containsEntry("subject",
"alice").containsEntry("tenant", "acme");
+ }
+
+ @Test
+ void matchesTheListedPropertyNamesCaseInsensitively() throws Exception {
+ Map<String, Object> input = inputSentFor(ENDPOINT +
"&includeProperties=subject",
+ e -> e.setProperty("SUBJECT", "alice"));
+
+ assertThat(propertiesOf(input)).containsEntry("SUBJECT", "alice");
+ }
+
+ @Test
+ void convertsANonJsonPropertyValueToItsStringForm() throws Exception {
+ Map<String, Object> input = inputSentFor(ENDPOINT +
"&includeProperties=when",
+ e -> e.setProperty("when", new Date(0)));
+
+ assertThat(propertiesOf(input).get("when")).isInstanceOf(String.class);
+ }
+
+ @Test
+ void keepsHeadersAndPropertiesInSeparateObjects() throws Exception {
+ Map<String, Object> input = inputSentFor(ENDPOINT +
"&includeProperties=subject", e -> {
+ e.getMessage().setHeader("subject", "mallory");
+ e.setProperty("subject", "alice");
+ });
+
+ assertThat(headersOf(input)).containsEntry("subject", "mallory");
+ assertThat(propertiesOf(input)).containsEntry("subject", "alice");
+ }
+
@SuppressWarnings("unchecked")
@Test
void reportsTheRouteTheExchangeCameFrom() throws Exception {
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 5d07a686ddaf..55589ec971a6 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
@@ -28,11 +28,13 @@ import org.apache.camel.component.opa.OpaConstants;
import org.apache.camel.component.opa.OpaPolicyEvaluationException;
import org.apache.camel.test.junit6.CamelTestSupport;
import org.junit.jupiter.api.Test;
+import org.mockito.ArgumentCaptor;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.anyMap;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
class OpaSecurityPolicyTest extends CamelTestSupport {
@@ -46,6 +48,7 @@ class OpaSecurityPolicyTest extends CamelTestSupport {
protected RouteBuilder createRouteBuilder() {
policy.setPolicyPath(PATH);
policy.setOpaClient(client);
+ policy.setIncludeProperties("subject");
return new RouteBuilder() {
@Override
public void configure() {
@@ -111,6 +114,19 @@ class OpaSecurityPolicyTest extends CamelTestSupport {
result.assertIsSatisfied();
}
+ @SuppressWarnings("unchecked")
+ @Test
+ void handsThePolicyAnIdentityCarriedAsAnExchangeProperty() throws
Exception {
+ givenDecision(Boolean.TRUE);
+
+ template.request("direct:start", e -> e.setProperty("subject",
"alice"));
+
+ ArgumentCaptor<Map<String, Object>> captor =
ArgumentCaptor.forClass(Map.class);
+ verify(client).evaluate(eq(PATH), captor.capture(), eq(Object.class));
+ assertThat((Map<String, Object>) captor.getValue().get("properties"))
+ .containsEntry("subject", "alice");
+ }
+
@Test
void overwritesAVerdictClaimedByTheInboundMessage() throws Exception {
givenDecision(Boolean.FALSE);
diff --git
a/dsl/camel-componentdsl/src/generated/java/org/apache/camel/builder/component/dsl/OpaComponentBuilderFactory.java
b/dsl/camel-componentdsl/src/generated/java/org/apache/camel/builder/component/dsl/OpaComponentBuilderFactory.java
index 09cdea3d61bc..abc8d8031c24 100644
---
a/dsl/camel-componentdsl/src/generated/java/org/apache/camel/builder/component/dsl/OpaComponentBuilderFactory.java
+++
b/dsl/camel-componentdsl/src/generated/java/org/apache/camel/builder/component/dsl/OpaComponentBuilderFactory.java
@@ -128,6 +128,31 @@ public interface OpaComponentBuilderFactory {
return this;
}
+ /**
+ * Comma-separated list of exchange property names to send to OPA in
the
+ * input document, or {code } for all of them. Empty by default, so no
+ * properties are sent unless asked for. This is where the
+ * authentication components put the identity they verified: {code
+ * camel-keycloak} stores the access token and its subject as exchange
+ * properties and prefers them over the equivalent headers, precisely
+ * because headers can be set by the caller. List those property names
+ * here to let a policy authorize the identity an earlier step
+ * established, instead of copying it into a header first. Only custom
+ * properties are sent; Camel's own internal exchange properties are
+ * never included.
+ *
+ * The option is a: <code>java.lang.String</code> type.
+ *
+ * Group: producer
+ *
+ * @param includeProperties the value to set
+ * @return the dsl builder
+ */
+ default OpaComponentBuilder includeProperties(java.lang.String
includeProperties) {
+ doSetProperty("includeProperties", includeProperties);
+ return this;
+ }
+
/**
* Whether the producer should be started lazy (on the first message).
@@ -270,6 +295,7 @@ public interface OpaComponentBuilderFactory {
case "configuration": ((OpaComponent)
component).setConfiguration((org.apache.camel.component.opa.OpaConfiguration)
value); return true;
case "includeBody": getOrCreateConfiguration((OpaComponent)
component).setIncludeBody((boolean) value); return true;
case "includeHeaders": getOrCreateConfiguration((OpaComponent)
component).setIncludeHeaders((java.lang.String) value); return true;
+ case "includeProperties": getOrCreateConfiguration((OpaComponent)
component).setIncludeProperties((java.lang.String) value); return true;
case "lazyStartProducer": ((OpaComponent)
component).setLazyStartProducer((boolean) value); return true;
case "serverUrl": getOrCreateConfiguration((OpaComponent)
component).setServerUrl((java.lang.String) value); return true;
case "autowiredEnabled": ((OpaComponent)
component).setAutowiredEnabled((boolean) value); return true;
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 9c526562e6e9..8b3f6f3bc950 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
@@ -118,6 +118,30 @@ public interface OpaEndpointBuilderFactory {
doSetProperty("includeHeaders", includeHeaders);
return this;
}
+ /**
+ * Comma-separated list of exchange property names to send to OPA in
the
+ * input document, or {code } for all of them. Empty by default, so no
+ * properties are sent unless asked for. This is where the
+ * authentication components put the identity they verified: {code
+ * camel-keycloak} stores the access token and its subject as exchange
+ * properties and prefers them over the equivalent headers, precisely
+ * because headers can be set by the caller. List those property names
+ * here to let a policy authorize the identity an earlier step
+ * established, instead of copying it into a header first. Only custom
+ * properties are sent; Camel's own internal exchange properties are
+ * never included.
+ *
+ * The option is a: <code>java.lang.String</code> type.
+ *
+ * Group: producer
+ *
+ * @param includeProperties the value to set
+ * @return the dsl builder
+ */
+ default OpaEndpointBuilder includeProperties(String includeProperties)
{
+ doSetProperty("includeProperties", includeProperties);
+ return this;
+ }
/**
* The base URL of the OPA server, without the {code /v1/data} suffix.
* The default assumes OPA running as a sidecar on the standard port.