This is an automated email from the ASF dual-hosted git repository.
davsclaus 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 d53bba35d81e CAMEL-26559: camel-opa, camel-openfga, camel-spiffe - add
the security policy beans to the bean catalog (#29144)
d53bba35d81e is described below
commit d53bba35d81e35dc6c11e252a9fff8d24ca58007
Author: Andrea Cosentino <[email protected]>
AuthorDate: Wed Nov 4 19:53:02 2026 +0100
CAMEL-26559: camel-opa, camel-openfga, camel-spiffe - add the security
policy beans to the bean catalog (#29144)
The OPA, OpenFGA and SPIFFE security policies are now listed in the bean
catalog with their options, including useGlobalSslContextParameters, with
generated configurers.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
---
.../org/apache/camel/catalog/beans.properties | 4 +
.../camel/catalog/beans/OpaSecurityPolicy.json | 16 ++
.../camel/catalog/beans/OpenFgaSecurityPolicy.json | 16 ++
.../catalog/beans/SpiffeSSLContextParameters.json | 16 ++
.../camel/catalog/beans/SpiffeSecurityPolicy.json | 16 ++
.../org/apache/camel/catalog/CamelCatalogTest.java | 27 +++
.../opa/security/OpaSecurityPolicyConfigurer.java | 157 +++++++++++++
.../services/org/apache/camel/bean.properties | 7 +
.../org/apache/camel/bean/OpaSecurityPolicy.json | 16 ++
....camel.component.opa.security.OpaSecurityPolicy | 2 +
.../component/opa/security/OpaSecurityPolicy.java | 73 ++++++
.../opa/security/OpaSecurityPolicyBindingTest.java | 95 ++++++++
.../security/OpenFgaSecurityPolicyConfigurer.java | 178 +++++++++++++++
.../services/org/apache/camel/bean.properties | 7 +
.../apache/camel/bean/OpenFgaSecurityPolicy.json | 16 ++
...omponent.openfga.security.OpenFgaSecurityPolicy | 2 +
.../openfga/security/OpenFgaSecurityPolicy.java | 252 ++++++++++++++++-----
.../security/OpenFgaSecurityPolicyOptionsTest.java | 178 +++++++++++++++
.../SpiffeSSLContextParametersConfigurer.java | 76 +++++++
.../spiffe/SpiffeSecurityPolicyConfigurer.java | 64 ++++++
.../services/org/apache/camel/bean.properties | 7 +
.../camel/bean/SpiffeSSLContextParameters.json | 16 ++
.../apache/camel/bean/SpiffeSecurityPolicy.json | 16 ++
...mel.component.spiffe.SpiffeSSLContextParameters | 2 +
...che.camel.component.spiffe.SpiffeSecurityPolicy | 2 +
.../spiffe/SpiffeSSLContextParameters.java | 9 +
.../component/spiffe/SpiffeSecurityPolicy.java | 19 ++
.../component/spiffe/SpiffeBeanBindingTest.java | 86 +++++++
.../java/org/apache/camel/util/ArtifactUtils.java | 8 +
29 files changed, 1321 insertions(+), 62 deletions(-)
diff --git
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/beans.properties
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/beans.properties
index db6b94aa0746..b54d21b7ebc8 100644
---
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/beans.properties
+++
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/beans.properties
@@ -47,10 +47,14 @@ MemoryAggregationRepository
MemoryIdempotentRepository
MemoryKeyValueRepository
MongoDbIdempotentRepository
+OpaSecurityPolicy
+OpenFgaSecurityPolicy
OpensearchBulkRequestAggregationStrategy
RedisAggregationRepository
RedisKeyValueRepository
SimpleScheduledRoutePolicy
+SpiffeSSLContextParameters
+SpiffeSecurityPolicy
SpringCacheIdempotentRepository
SpringRedisIdempotentRepository
StringAggregationStrategy
diff --git
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/beans/OpaSecurityPolicy.json
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/beans/OpaSecurityPolicy.json
new file mode 100644
index 000000000000..aa6c5e6247e8
--- /dev/null
+++
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/beans/OpaSecurityPolicy.json
@@ -0,0 +1,16 @@
+{
+ "bean": {
+ "kind": "bean",
+ "name": "OpaSecurityPolicy",
+ "javaType": "org.apache.camel.component.opa.security.OpaSecurityPolicy",
+ "interfaceType": "org.apache.camel.spi.AuthorizationPolicy",
+ "title": "Opa Security Policy",
+ "description": "An authorization policy that asks Open Policy Agent
whether the exchange may proceed. A deny throws CamelAuthorizationException, so
the part of the route it guards does not run.",
+ "deprecated": false,
+ "groupId": "org.apache.camel",
+ "artifactId": "camel-opa",
+ "version": "4.24.0-SNAPSHOT",
+ "properties": { "serverUrl": { "index": 0, "kind": "property",
"displayName": "Server Url", "required": false, "type": "string", "javaType":
"java.lang.String", "deprecated": false, "autowired": false, "secret": false,
"defaultValue": "http:\/\/localhost:8181", "description": "The base URL of the
OPA server, without the \/v1\/data suffix. Not used in wasm mode." },
"policyPath": { "index": 1, "kind": "property", "displayName": "Policy Path",
"required": true, "type": "string", "javaT [...]
+ }
+}
+
diff --git
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/beans/OpenFgaSecurityPolicy.json
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/beans/OpenFgaSecurityPolicy.json
new file mode 100644
index 000000000000..967a61fc9e11
--- /dev/null
+++
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/beans/OpenFgaSecurityPolicy.json
@@ -0,0 +1,16 @@
+{
+ "bean": {
+ "kind": "bean",
+ "name": "OpenFgaSecurityPolicy",
+ "javaType":
"org.apache.camel.component.openfga.security.OpenFgaSecurityPolicy",
+ "interfaceType": "org.apache.camel.spi.AuthorizationPolicy",
+ "title": "Open Fga Security Policy",
+ "description": "An authorization policy that asks OpenFGA whether a
subject has a relation to an object before the part of the route it guards
runs. A deny throws CamelAuthorizationException.",
+ "deprecated": false,
+ "groupId": "org.apache.camel",
+ "artifactId": "camel-openfga",
+ "version": "4.24.0-SNAPSHOT",
+ "properties": { "apiUrl": { "index": 0, "kind": "property", "displayName":
"Api Url", "required": false, "type": "string", "javaType": "java.lang.String",
"deprecated": false, "autowired": false, "secret": false, "defaultValue":
"http:\/\/localhost:8080", "description": "The base URL of the OpenFGA HTTP
API." }, "storeId": { "index": 1, "kind": "property", "displayName": "Store
Id", "required": true, "type": "string", "javaType": "java.lang.String",
"deprecated": false, "autowired": [...]
+ }
+}
+
diff --git
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/beans/SpiffeSSLContextParameters.json
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/beans/SpiffeSSLContextParameters.json
new file mode 100644
index 000000000000..ca445236ce47
--- /dev/null
+++
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/beans/SpiffeSSLContextParameters.json
@@ -0,0 +1,16 @@
+{
+ "bean": {
+ "kind": "bean",
+ "name": "SpiffeSSLContextParameters",
+ "javaType": "org.apache.camel.component.spiffe.SpiffeSSLContextParameters",
+ "interfaceType": "org.apache.camel.support.jsse.SSLContextParameters",
+ "title": "Spiffe SSLContext Parameters",
+ "description": "SSLContextParameters whose SSLContext is backed by the
SPIFFE Workload API: the X.509-SVID and the trust bundles are fetched live and
rotated automatically, giving mutual TLS to any component that accepts an
sslContextParameters reference. Set acceptedSpiffeIds or acceptAnySpiffeId; the
inherited options, such as serverParameters.clientAuthentication, still apply.",
+ "deprecated": false,
+ "groupId": "org.apache.camel",
+ "artifactId": "camel-spiffe",
+ "version": "4.24.0-SNAPSHOT",
+ "properties": { "spiffeSocketPath": { "index": 0, "kind": "property",
"displayName": "Spiffe Socket Path", "label": "security", "required": false,
"type": "string", "javaType": "java.lang.String", "deprecated": false,
"autowired": false, "secret": false, "description": "Path to the SPIFFE
Workload API endpoint (for example unix:\/\/\/tmp\/agent.sock). When not set,
the SPIFFE_ENDPOINT_SOCKET environment variable is used." },
"acceptedSpiffeIds": { "index": 1, "kind": "property", "dis [...]
+ }
+}
+
diff --git
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/beans/SpiffeSecurityPolicy.json
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/beans/SpiffeSecurityPolicy.json
new file mode 100644
index 000000000000..f132d90abb38
--- /dev/null
+++
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/beans/SpiffeSecurityPolicy.json
@@ -0,0 +1,16 @@
+{
+ "bean": {
+ "kind": "bean",
+ "name": "SpiffeSecurityPolicy",
+ "javaType": "org.apache.camel.component.spiffe.SpiffeSecurityPolicy",
+ "interfaceType": "org.apache.camel.spi.AuthorizationPolicy",
+ "title": "Spiffe Security Policy",
+ "description": "An authorization policy that authorizes a route segment on
the caller's SPIFFE ID, read from the peer certificate that SPIFFE mutual TLS
verified. A peer that is not accepted, or presents no verified SVID, is denied
with CamelAuthorizationException.",
+ "deprecated": false,
+ "groupId": "org.apache.camel",
+ "artifactId": "camel-spiffe",
+ "version": "4.24.0-SNAPSHOT",
+ "properties": { "acceptedSpiffeIds": { "index": 0, "kind": "property",
"displayName": "Accepted Spiffe Ids", "required": false, "type": "string",
"javaType": "java.lang.String", "deprecated": false, "autowired": false,
"secret": false, "description": "Comma-separated allow-list of the peer SPIFFE
IDs this route segment authorizes, for example
spiffe:\/\/example.org\/frontend. Set exactly one of acceptedSpiffeIds and
acceptAnySpiffeId: setting both is rejected, and setting neither fai [...]
+ }
+}
+
diff --git
a/catalog/camel-catalog/src/test/java/org/apache/camel/catalog/CamelCatalogTest.java
b/catalog/camel-catalog/src/test/java/org/apache/camel/catalog/CamelCatalogTest.java
index 473a9e7b81f6..f7dfaf283c34 100644
---
a/catalog/camel-catalog/src/test/java/org/apache/camel/catalog/CamelCatalogTest.java
+++
b/catalog/camel-catalog/src/test/java/org/apache/camel/catalog/CamelCatalogTest.java
@@ -2256,6 +2256,33 @@ public class CamelCatalogTest {
assertEquals(7, model.getOptions().size());
}
+ private static Stream<Arguments> securityBeans() {
+ return Stream.of(
+ Arguments.of("OpaSecurityPolicy",
"org.apache.camel.component.opa.security.OpaSecurityPolicy", "camel-opa",
+ "org.apache.camel.spi.AuthorizationPolicy",
"policyPath"),
+ Arguments.of("OpenFgaSecurityPolicy",
"org.apache.camel.component.openfga.security.OpenFgaSecurityPolicy",
+ "camel-openfga",
"org.apache.camel.spi.AuthorizationPolicy", "storeId"),
+ Arguments.of("SpiffeSecurityPolicy",
"org.apache.camel.component.spiffe.SpiffeSecurityPolicy",
+ "camel-spiffe",
"org.apache.camel.spi.AuthorizationPolicy", "acceptedSpiffeIds"),
+ Arguments.of("SpiffeSSLContextParameters",
"org.apache.camel.component.spiffe.SpiffeSSLContextParameters",
+ "camel-spiffe",
"org.apache.camel.support.jsse.SSLContextParameters", "acceptedSpiffeIds"));
+ }
+
+ // camel run looks up a bean class it cannot load by its simple name here,
to find the jar to download
+ @ParameterizedTest
+ @MethodSource("securityBeans")
+ void testSecurityBeanNamesItsComponent(
+ String name, String javaType, String artifactId, String
interfaceType, String option) {
+ PojoBeanModel model = catalog.pojoBeanModel(name);
+ assertNotNull(model, name);
+
+ assertEquals(javaType, model.getJavaType());
+ assertEquals("org.apache.camel", model.getGroupId());
+ assertEquals(artifactId, model.getArtifactId());
+ assertEquals(interfaceType, model.getInterfaceType());
+ assertTrue(model.getOptions().stream().anyMatch(o ->
option.equals(o.getName())), option);
+ }
+
@Test
public void testFindApiReferenceNames() {
List<String> names = catalog.findApiReferenceNames();
diff --git
a/components/camel-opa/src/generated/java/org/apache/camel/component/opa/security/OpaSecurityPolicyConfigurer.java
b/components/camel-opa/src/generated/java/org/apache/camel/component/opa/security/OpaSecurityPolicyConfigurer.java
new file mode 100644
index 000000000000..422629ed63db
--- /dev/null
+++
b/components/camel-opa/src/generated/java/org/apache/camel/component/opa/security/OpaSecurityPolicyConfigurer.java
@@ -0,0 +1,157 @@
+/* Generated by camel build tools - do NOT edit this file! */
+package org.apache.camel.component.opa.security;
+
+import javax.annotation.processing.Generated;
+import java.util.Locale;
+import java.util.Map;
+
+import org.apache.camel.CamelContext;
+import org.apache.camel.spi.ExtendedPropertyConfigurerGetter;
+import org.apache.camel.spi.PropertyConfigurerGetter;
+import org.apache.camel.spi.ConfigurerStrategy;
+import org.apache.camel.spi.GeneratedPropertyConfigurer;
+import org.apache.camel.util.CaseInsensitiveMap;
+import org.apache.camel.component.opa.security.OpaSecurityPolicy;
+
+/**
+ * Generated by camel build tools - do NOT edit this file!
+ */
+@Generated("org.apache.camel.maven.packaging.GenerateConfigurerMojo")
+@SuppressWarnings("unchecked")
+public class OpaSecurityPolicyConfigurer extends
org.apache.camel.support.component.PropertyConfigurerSupport implements
GeneratedPropertyConfigurer, PropertyConfigurerGetter {
+
+ @Override
+ public boolean configure(CamelContext camelContext, Object obj, String
name, Object value, boolean ignoreCase) {
+ org.apache.camel.component.opa.security.OpaSecurityPolicy target =
(org.apache.camel.component.opa.security.OpaSecurityPolicy) obj;
+ switch (ignoreCase ? name.toLowerCase(Locale.ROOT) : name) {
+ case "allowkey":
+ case "allowKey": target.setAllowKey(property(camelContext,
java.lang.String.class, value)); return true;
+ case "bearertoken":
+ case "bearerToken": target.setBearerToken(property(camelContext,
java.lang.String.class, value)); return true;
+ case "borrowtimeout":
+ case "borrowTimeout": target.setBorrowTimeout(property(camelContext,
long.class, value)); return true;
+ case "connectiontimeout":
+ case "connectionTimeout":
target.setConnectionTimeout(property(camelContext, long.class, value)); return
true;
+ case "entrypoint": target.setEntrypoint(property(camelContext,
java.lang.String.class, value)); return true;
+ case "evaluationmode":
+ case "evaluationMode": target.setEvaluationMode(property(camelContext,
java.lang.String.class, value)); return true;
+ case "failopen":
+ case "failOpen": target.setFailOpen(property(camelContext,
boolean.class, value)); return true;
+ case "healthcheckenabled":
+ case "healthCheckEnabled":
target.setHealthCheckEnabled(property(camelContext, boolean.class, value));
return true;
+ case "includebody":
+ case "includeBody": target.setIncludeBody(property(camelContext,
boolean.class, value)); return true;
+ case "includeheaders":
+ case "includeHeaders": target.setIncludeHeaders(property(camelContext,
java.lang.String.class, value)); return true;
+ case "includeproperties":
+ case "includeProperties":
target.setIncludeProperties(property(camelContext, java.lang.String.class,
value)); return true;
+ case "opaclient":
+ case "opaClient": target.setOpaClient(property(camelContext,
com.styra.opa.OPAClient.class, value)); return true;
+ case "policybundle":
+ case "policyBundle": target.setPolicyBundle(property(camelContext,
java.lang.String.class, value)); return true;
+ case "policypath":
+ case "policyPath": target.setPolicyPath(property(camelContext,
java.lang.String.class, value)); return true;
+ case "poolsize":
+ case "poolSize": target.setPoolSize(property(camelContext, int.class,
value)); return true;
+ case "requesttimeout":
+ case "requestTimeout": target.setRequestTimeout(property(camelContext,
long.class, value)); return true;
+ case "serverurl":
+ case "serverUrl": target.setServerUrl(property(camelContext,
java.lang.String.class, value)); return true;
+ case "sslcontextparameters":
+ case "sslContextParameters":
target.setSslContextParameters(property(camelContext,
org.apache.camel.support.jsse.SSLContextParameters.class, value)); return true;
+ case "useglobalsslcontextparameters":
+ case "useGlobalSslContextParameters":
target.setUseGlobalSslContextParameters(property(camelContext, boolean.class,
value)); return true;
+ default: return false;
+ }
+ }
+
+ @Override
+ public Class<?> getOptionType(String name, boolean ignoreCase) {
+ switch (ignoreCase ? name.toLowerCase(Locale.ROOT) : name) {
+ case "allowkey":
+ case "allowKey": return java.lang.String.class;
+ case "bearertoken":
+ case "bearerToken": return java.lang.String.class;
+ case "borrowtimeout":
+ case "borrowTimeout": return long.class;
+ case "connectiontimeout":
+ case "connectionTimeout": return long.class;
+ case "entrypoint": return java.lang.String.class;
+ case "evaluationmode":
+ case "evaluationMode": return java.lang.String.class;
+ case "failopen":
+ case "failOpen": return boolean.class;
+ case "healthcheckenabled":
+ case "healthCheckEnabled": return boolean.class;
+ case "includebody":
+ case "includeBody": return boolean.class;
+ case "includeheaders":
+ case "includeHeaders": return java.lang.String.class;
+ case "includeproperties":
+ case "includeProperties": return java.lang.String.class;
+ case "opaclient":
+ case "opaClient": return com.styra.opa.OPAClient.class;
+ case "policybundle":
+ case "policyBundle": return java.lang.String.class;
+ case "policypath":
+ case "policyPath": return java.lang.String.class;
+ case "poolsize":
+ case "poolSize": return int.class;
+ case "requesttimeout":
+ case "requestTimeout": return long.class;
+ case "serverurl":
+ case "serverUrl": return java.lang.String.class;
+ case "sslcontextparameters":
+ case "sslContextParameters": return
org.apache.camel.support.jsse.SSLContextParameters.class;
+ case "useglobalsslcontextparameters":
+ case "useGlobalSslContextParameters": return boolean.class;
+ default: return null;
+ }
+ }
+
+ @Override
+ public Object getOptionValue(Object obj, String name, boolean ignoreCase) {
+ org.apache.camel.component.opa.security.OpaSecurityPolicy target =
(org.apache.camel.component.opa.security.OpaSecurityPolicy) obj;
+ switch (ignoreCase ? name.toLowerCase(Locale.ROOT) : name) {
+ case "allowkey":
+ case "allowKey": return target.getAllowKey();
+ case "bearertoken":
+ case "bearerToken": return target.getBearerToken();
+ case "borrowtimeout":
+ case "borrowTimeout": return target.getBorrowTimeout();
+ case "connectiontimeout":
+ case "connectionTimeout": return target.getConnectionTimeout();
+ case "entrypoint": return target.getEntrypoint();
+ case "evaluationmode":
+ case "evaluationMode": return target.getEvaluationMode();
+ case "failopen":
+ case "failOpen": return target.isFailOpen();
+ case "healthcheckenabled":
+ case "healthCheckEnabled": return target.isHealthCheckEnabled();
+ case "includebody":
+ case "includeBody": return target.isIncludeBody();
+ case "includeheaders":
+ case "includeHeaders": return target.getIncludeHeaders();
+ case "includeproperties":
+ case "includeProperties": return target.getIncludeProperties();
+ case "opaclient":
+ case "opaClient": return target.getOpaClient();
+ case "policybundle":
+ case "policyBundle": return target.getPolicyBundle();
+ case "policypath":
+ case "policyPath": return target.getPolicyPath();
+ case "poolsize":
+ case "poolSize": return target.getPoolSize();
+ case "requesttimeout":
+ case "requestTimeout": return target.getRequestTimeout();
+ case "serverurl":
+ case "serverUrl": return target.getServerUrl();
+ case "sslcontextparameters":
+ case "sslContextParameters": return target.getSslContextParameters();
+ case "useglobalsslcontextparameters":
+ case "useGlobalSslContextParameters": return
target.isUseGlobalSslContextParameters();
+ default: return null;
+ }
+ }
+}
+
diff --git
a/components/camel-opa/src/generated/resources/META-INF/services/org/apache/camel/bean.properties
b/components/camel-opa/src/generated/resources/META-INF/services/org/apache/camel/bean.properties
new file mode 100644
index 000000000000..ab65de040613
--- /dev/null
+++
b/components/camel-opa/src/generated/resources/META-INF/services/org/apache/camel/bean.properties
@@ -0,0 +1,7 @@
+# Generated by camel build tools - do NOT edit this file!
+bean=OpaSecurityPolicy
+groupId=org.apache.camel
+artifactId=camel-opa
+version=4.24.0-SNAPSHOT
+projectName=Camel :: OPA
+projectDescription=Camel Open Policy Agent Component
diff --git
a/components/camel-opa/src/generated/resources/META-INF/services/org/apache/camel/bean/OpaSecurityPolicy.json
b/components/camel-opa/src/generated/resources/META-INF/services/org/apache/camel/bean/OpaSecurityPolicy.json
new file mode 100644
index 000000000000..aa6c5e6247e8
--- /dev/null
+++
b/components/camel-opa/src/generated/resources/META-INF/services/org/apache/camel/bean/OpaSecurityPolicy.json
@@ -0,0 +1,16 @@
+{
+ "bean": {
+ "kind": "bean",
+ "name": "OpaSecurityPolicy",
+ "javaType": "org.apache.camel.component.opa.security.OpaSecurityPolicy",
+ "interfaceType": "org.apache.camel.spi.AuthorizationPolicy",
+ "title": "Opa Security Policy",
+ "description": "An authorization policy that asks Open Policy Agent
whether the exchange may proceed. A deny throws CamelAuthorizationException, so
the part of the route it guards does not run.",
+ "deprecated": false,
+ "groupId": "org.apache.camel",
+ "artifactId": "camel-opa",
+ "version": "4.24.0-SNAPSHOT",
+ "properties": { "serverUrl": { "index": 0, "kind": "property",
"displayName": "Server Url", "required": false, "type": "string", "javaType":
"java.lang.String", "deprecated": false, "autowired": false, "secret": false,
"defaultValue": "http:\/\/localhost:8181", "description": "The base URL of the
OPA server, without the \/v1\/data suffix. Not used in wasm mode." },
"policyPath": { "index": 1, "kind": "property", "displayName": "Policy Path",
"required": true, "type": "string", "javaT [...]
+ }
+}
+
diff --git
a/components/camel-opa/src/generated/resources/META-INF/services/org/apache/camel/configurer/org.apache.camel.component.opa.security.OpaSecurityPolicy
b/components/camel-opa/src/generated/resources/META-INF/services/org/apache/camel/configurer/org.apache.camel.component.opa.security.OpaSecurityPolicy
new file mode 100644
index 000000000000..8976a9ba14da
--- /dev/null
+++
b/components/camel-opa/src/generated/resources/META-INF/services/org/apache/camel/configurer/org.apache.camel.component.opa.security.OpaSecurityPolicy
@@ -0,0 +1,2 @@
+# Generated by camel build tools - do NOT edit this file!
+class=org.apache.camel.component.opa.security.OpaSecurityPolicyConfigurer
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 e60e1406e198..af4f7bc11a71 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
@@ -33,6 +33,8 @@ import org.apache.camel.component.opa.OpaRestEvaluator;
import org.apache.camel.component.opa.OpaWasmEvaluator;
import org.apache.camel.health.HealthCheckRegistry;
import org.apache.camel.spi.AuthorizationPolicy;
+import org.apache.camel.spi.Configurer;
+import org.apache.camel.spi.Metadata;
import org.apache.camel.support.jsse.SSLContextParameters;
import org.apache.camel.util.ObjectHelper;
import org.apache.camel.util.StringHelper;
@@ -54,6 +56,11 @@ import org.slf4j.LoggerFactory;
* {@code onException} machinery applies. The decision is also recorded on the
exchange in the {@code CamelOpaDecision}
* headers.
*/
+@Metadata(label = "bean",
+ description = "An authorization policy that asks Open Policy Agent
whether the exchange may proceed. A deny throws"
+ + " CamelAuthorizationException, so the part of the
route it guards does not run.",
+ annotations = {
"interfaceName=org.apache.camel.spi.AuthorizationPolicy" })
+@Configurer(metadataOnly = true)
public class OpaSecurityPolicy implements AuthorizationPolicy {
private static final Logger LOG =
LoggerFactory.getLogger(OpaSecurityPolicy.class);
@@ -62,25 +69,91 @@ public class OpaSecurityPolicy implements
AuthorizationPolicy {
private static final String REST_MODE = "rest";
private static final String DEFAULT_SERVER_URL = "http://localhost:8181";
+ @Metadata(defaultValue = DEFAULT_SERVER_URL,
+ description = "The base URL of the OPA server, without the
/v1/data suffix. Not used in wasm mode.")
private String serverUrl = DEFAULT_SERVER_URL;
+ @Metadata(required = true,
+ description = "Path of the Rego rule head to evaluate, relative
to the OPA data document, for example"
+ + " authz/orders/allow. It is fixed by the policy:
an inbound message cannot select which"
+ + " rule judges it.")
private String policyPath;
+ @Metadata(defaultValue = "allow",
+ description = "The key to read the verdict from when the policy
returns an object rather than a plain"
+ + " boolean. A dotted path such as result.allow
reaches a verdict nested inside the document.")
private String allowKey = "allow";
+ @Metadata(defaultValue = "*",
+ description = "Comma-separated list of message header names to
send to OPA in the input document. The"
+ + " default sends every header except
Authorization, Proxy-Authorization, Cookie and"
+ + " Set-Cookie, which carry a caller credential
and are sent only when named here.")
private String includeHeaders = "*";
+ @Metadata(description = "Comma-separated list of exchange property names
to send to OPA in the input document, or *"
+ + " for all of them. Empty by default. Use it to
hand the policy an identity an earlier"
+ + " authentication step stored as an exchange
property, such as CamelKeycloakTokenSubject or"
+ + " CamelSpiffePeerId.")
private String includeProperties;
+ @Metadata(defaultValue = "false",
+ description = "Whether to send the message body to OPA as part
of the input document.")
private boolean includeBody;
+ @Metadata(label = "security", security = "secret",
+ description = "Bearer token sent to the OPA server in the
Authorization header, for an OPA instance that"
+ + " has its API authentication enabled. Not used
in wasm mode.")
private String bearerToken;
+ @Metadata(label = "security", defaultValue = "false", security =
"insecure:dev",
+ description = "Whether to let the exchange proceed when the
policy decision point is unavailable: the OPA"
+ + " server cannot be reached, times out, or a
gateway in front of it answers 502, 503, 504 or"
+ + " 429, or in wasm mode no instance frees up
within borrowTimeout. It never applies to an"
+ + " answer: an undefined decision, a rejected
request and an error evaluating the policy fail"
+ + " closed even when this is set. Do not enable
this in production.")
private boolean failOpen;
+ @Metadata(defaultValue = REST_MODE, enums = "rest,wasm",
+ description = "How the policy is evaluated. rest asks a running
OPA server over its Data API; wasm"
+ + " evaluates a WebAssembly bundle in-process,
with no server involved. serverUrl,"
+ + " bearerToken, connectionTimeout,
requestTimeout, sslContextParameters and the readiness"
+ + " check do not apply in wasm mode.")
private String evaluationMode = REST_MODE;
+ @Metadata(description = "The WebAssembly policy to evaluate in wasm mode,
as produced by opa build -t wasm: a file:,"
+ + " classpath: or http: location holding either
the bundle.tar.gz that opa build emits or a"
+ + " bare .wasm module. Required when
evaluationMode is wasm.")
private String policyBundle;
+ @Metadata(description = "The compiled entrypoint to evaluate in wasm mode,
fixed when the bundle is built with opa"
+ + " build -e. Defaults to the policy path, which
is the name opa build gives it.")
private String entrypoint;
+ @Metadata(label = "advanced", defaultValue = "8",
+ description = "How many WebAssembly instances to pool in wasm
mode. An instance is not thread-safe, so this"
+ + " bounds how many exchanges are authorized at
once.")
private int poolSize = 8;
+ @Metadata(label = "advanced", defaultValue = "30000",
+ description = "How long, in milliseconds, an exchange waits for
a free WebAssembly instance in wasm mode."
+ + " Running out of time is an evaluation failure
rather than a deny: it fails closed, or"
+ + " proceeds when failOpen is set.")
private long borrowTimeout = 30000;
+ @Metadata(label = "advanced",
+ description = "An existing OPAClient to use instead of building
one from serverUrl, bearerToken and"
+ + " sslContextParameters. No readiness check is
registered for it.")
private OPAClient opaClient;
+ @Metadata(label = "advanced", defaultValue = "true",
+ description = "Whether to register a readiness check for the OPA
server this policy queries. The policy"
+ + " denies every exchange it guards while the
server is unreachable, so a route that cannot"
+ + " reach it is not ready.")
private boolean healthCheckEnabled = true;
+ @Metadata(label = "advanced", defaultValue = "10000",
+ description = "How long to wait, in milliseconds, for the
connection to the OPA server to be established.")
private long connectionTimeout = 10000;
+ @Metadata(label = "advanced", defaultValue = "30000",
+ description = "How long to wait, in milliseconds, for the
decision once connected. A request that times"
+ + " out is an evaluation failure rather than a
deny: it fails closed, or proceeds when"
+ + " failOpen is set.")
private long requestTimeout = 30000;
+ @Metadata(label = "security",
+ description = "TLS configuration for the connection to the OPA
server, to trust a server whose"
+ + " certificate comes from a private CA or to
present a client certificate to one that"
+ + " requires mutual TLS. Takes precedence over
useGlobalSslContextParameters.")
private SSLContextParameters sslContextParameters;
+ @Metadata(label = "security", defaultValue = "false",
+ description = "Whether to use the global SSL context parameters,
the context-wide SSLContextParameters"
+ + " camel-main builds from camel.ssl.*, when
sslContextParameters is not set. Not used in"
+ + " wasm mode.")
private boolean useGlobalSslContextParameters;
private volatile OpaPolicyEvaluator evaluator;
diff --git
a/components/camel-opa/src/test/java/org/apache/camel/component/opa/security/OpaSecurityPolicyBindingTest.java
b/components/camel-opa/src/test/java/org/apache/camel/component/opa/security/OpaSecurityPolicyBindingTest.java
new file mode 100644
index 000000000000..3ea487c8b1f1
--- /dev/null
+++
b/components/camel-opa/src/test/java/org/apache/camel/component/opa/security/OpaSecurityPolicyBindingTest.java
@@ -0,0 +1,95 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.camel.component.opa.security;
+
+import java.util.LinkedHashMap;
+import java.util.Map;
+
+import org.apache.camel.CamelContext;
+import org.apache.camel.impl.DefaultCamelContext;
+import org.apache.camel.support.PropertyBindingSupport;
+import org.apache.camel.support.jsse.SSLContextParameters;
+import org.junit.jupiter.api.Test;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+/**
+ * Binds the policy the way a YAML {@code beans:} entry does, where every
value arrives as a string.
+ */
+class OpaSecurityPolicyBindingTest {
+
+ @Test
+ void bindsTheOptionsAsAYamlBeanDefinitionGivesThem() throws Exception {
+ try (CamelContext context = new DefaultCamelContext()) {
+ context.start();
+ SSLContextParameters tls = new SSLContextParameters();
+ context.getRegistry().bind("tls", tls);
+ Map<String, Object> properties = new LinkedHashMap<>();
+ properties.put("serverUrl", "https://opa:8181");
+ properties.put("policyPath", "authz/orders/allow");
+ properties.put("allowKey", "result.allow");
+ properties.put("includeHeaders", "X-Tenant,Content-Type");
+ properties.put("includeProperties", "CamelSpiffePeerId");
+ properties.put("includeBody", "true");
+ properties.put("bearerToken", "s3cr3t");
+ properties.put("failOpen", "false");
+ properties.put("connectionTimeout", "2000");
+ properties.put("requestTimeout", "3000");
+ properties.put("healthCheckEnabled", "false");
+ properties.put("sslContextParameters", "#bean:tls");
+
+ OpaSecurityPolicy policy = new OpaSecurityPolicy();
+ PropertyBindingSupport.setPropertiesOnTarget(context, policy,
properties);
+
+ assertThat(policy.getServerUrl()).isEqualTo("https://opa:8181");
+ assertThat(policy.getPolicyPath()).isEqualTo("authz/orders/allow");
+ assertThat(policy.getAllowKey()).isEqualTo("result.allow");
+
assertThat(policy.getIncludeHeaders()).isEqualTo("X-Tenant,Content-Type");
+
assertThat(policy.getIncludeProperties()).isEqualTo("CamelSpiffePeerId");
+ assertThat(policy.isIncludeBody()).isTrue();
+ assertThat(policy.getBearerToken()).isEqualTo("s3cr3t");
+ assertThat(policy.isFailOpen()).isFalse();
+ assertThat(policy.getConnectionTimeout()).isEqualTo(2000L);
+ assertThat(policy.getRequestTimeout()).isEqualTo(3000L);
+ assertThat(policy.isHealthCheckEnabled()).isFalse();
+ assertThat(policy.getSslContextParameters()).isSameAs(tls);
+ }
+ }
+
+ @Test
+ void bindsTheWasmOptions() throws Exception {
+ try (CamelContext context = new DefaultCamelContext()) {
+ context.start();
+ Map<String, Object> properties = new LinkedHashMap<>();
+ properties.put("policyPath", "authz/orders/allow");
+ properties.put("evaluationMode", "wasm");
+ properties.put("policyBundle", "classpath:orders-bundle.tar.gz");
+ properties.put("entrypoint", "authz/orders/allow");
+ properties.put("poolSize", "4");
+ properties.put("borrowTimeout", "1500");
+
+ OpaSecurityPolicy policy = new OpaSecurityPolicy();
+ PropertyBindingSupport.setPropertiesOnTarget(context, policy,
properties);
+
+ assertThat(policy.getEvaluationMode()).isEqualTo("wasm");
+
assertThat(policy.getPolicyBundle()).isEqualTo("classpath:orders-bundle.tar.gz");
+ assertThat(policy.getEntrypoint()).isEqualTo("authz/orders/allow");
+ assertThat(policy.getPoolSize()).isEqualTo(4);
+ assertThat(policy.getBorrowTimeout()).isEqualTo(1500L);
+ }
+ }
+}
diff --git
a/components/camel-openfga/src/generated/java/org/apache/camel/component/openfga/security/OpenFgaSecurityPolicyConfigurer.java
b/components/camel-openfga/src/generated/java/org/apache/camel/component/openfga/security/OpenFgaSecurityPolicyConfigurer.java
new file mode 100644
index 000000000000..d6285069ca38
--- /dev/null
+++
b/components/camel-openfga/src/generated/java/org/apache/camel/component/openfga/security/OpenFgaSecurityPolicyConfigurer.java
@@ -0,0 +1,178 @@
+/* Generated by camel build tools - do NOT edit this file! */
+package org.apache.camel.component.openfga.security;
+
+import javax.annotation.processing.Generated;
+import java.util.Locale;
+import java.util.Map;
+
+import org.apache.camel.CamelContext;
+import org.apache.camel.spi.ExtendedPropertyConfigurerGetter;
+import org.apache.camel.spi.PropertyConfigurerGetter;
+import org.apache.camel.spi.ConfigurerStrategy;
+import org.apache.camel.spi.GeneratedPropertyConfigurer;
+import org.apache.camel.util.CaseInsensitiveMap;
+import org.apache.camel.component.openfga.security.OpenFgaSecurityPolicy;
+
+/**
+ * Generated by camel build tools - do NOT edit this file!
+ */
+@Generated("org.apache.camel.maven.packaging.GenerateConfigurerMojo")
+@SuppressWarnings("unchecked")
+public class OpenFgaSecurityPolicyConfigurer extends
org.apache.camel.support.component.PropertyConfigurerSupport implements
GeneratedPropertyConfigurer, PropertyConfigurerGetter {
+
+ @Override
+ public boolean configure(CamelContext camelContext, Object obj, String
name, Object value, boolean ignoreCase) {
+ org.apache.camel.component.openfga.security.OpenFgaSecurityPolicy
target = (org.apache.camel.component.openfga.security.OpenFgaSecurityPolicy)
obj;
+ switch (ignoreCase ? name.toLowerCase(Locale.ROOT) : name) {
+ case "apiaudience":
+ case "apiAudience": target.setApiAudience(property(camelContext,
java.lang.String.class, value)); return true;
+ case "apitoken":
+ case "apiToken": target.setApiToken(property(camelContext,
java.lang.String.class, value)); return true;
+ case "apitokenissuer":
+ case "apiTokenIssuer": target.setApiTokenIssuer(property(camelContext,
java.lang.String.class, value)); return true;
+ case "apiurl":
+ case "apiUrl": target.setApiUrl(property(camelContext,
java.lang.String.class, value)); return true;
+ case "authorizationmodelid":
+ case "authorizationModelId":
target.setAuthorizationModelId(property(camelContext, java.lang.String.class,
value)); return true;
+ case "clientid":
+ case "clientId": target.setClientId(property(camelContext,
java.lang.String.class, value)); return true;
+ case "clientsecret":
+ case "clientSecret": target.setClientSecret(property(camelContext,
java.lang.String.class, value)); return true;
+ case "conditioncontext":
+ case "conditionContext":
target.setConditionContext(property(camelContext, java.util.Map.class, value));
return true;
+ case "connecttimeout":
+ case "connectTimeout": target.setConnectTimeout(property(camelContext,
long.class, value)); return true;
+ case "consistency": target.setConsistency(property(camelContext,
java.lang.String.class, value)); return true;
+ case "contextualtuples":
+ case "contextualTuples":
target.setContextualTuples(property(camelContext, java.lang.String.class,
value)); return true;
+ case "failopen":
+ case "failOpen": target.setFailOpen(property(camelContext,
boolean.class, value)); return true;
+ case "healthcheckenabled":
+ case "healthCheckEnabled":
target.setHealthCheckEnabled(property(camelContext, boolean.class, value));
return true;
+ case "maxretries":
+ case "maxRetries": target.setMaxRetries(property(camelContext,
int.class, value)); return true;
+ case "object": target.setObject(property(camelContext,
java.lang.String.class, value)); return true;
+ case "openfgaclient":
+ case "openFgaClient": target.setOpenFgaClient(property(camelContext,
dev.openfga.sdk.api.client.OpenFgaClient.class, value)); return true;
+ case "readtimeout":
+ case "readTimeout": target.setReadTimeout(property(camelContext,
long.class, value)); return true;
+ case "relation": target.setRelation(property(camelContext,
java.lang.String.class, value)); return true;
+ case "scopes": target.setScopes(property(camelContext,
java.lang.String.class, value)); return true;
+ case "sslcontextparameters":
+ case "sslContextParameters":
target.setSslContextParameters(property(camelContext,
org.apache.camel.support.jsse.SSLContextParameters.class, value)); return true;
+ case "storeid":
+ case "storeId": target.setStoreId(property(camelContext,
java.lang.String.class, value)); return true;
+ case "useglobalsslcontextparameters":
+ case "useGlobalSslContextParameters":
target.setUseGlobalSslContextParameters(property(camelContext, boolean.class,
value)); return true;
+ case "user": target.setUser(property(camelContext,
java.lang.String.class, value)); return true;
+ default: return false;
+ }
+ }
+
+ @Override
+ public Class<?> getOptionType(String name, boolean ignoreCase) {
+ switch (ignoreCase ? name.toLowerCase(Locale.ROOT) : name) {
+ case "apiaudience":
+ case "apiAudience": return java.lang.String.class;
+ case "apitoken":
+ case "apiToken": return java.lang.String.class;
+ case "apitokenissuer":
+ case "apiTokenIssuer": return java.lang.String.class;
+ case "apiurl":
+ case "apiUrl": return java.lang.String.class;
+ case "authorizationmodelid":
+ case "authorizationModelId": return java.lang.String.class;
+ case "clientid":
+ case "clientId": return java.lang.String.class;
+ case "clientsecret":
+ case "clientSecret": return java.lang.String.class;
+ case "conditioncontext":
+ case "conditionContext": return java.util.Map.class;
+ case "connecttimeout":
+ case "connectTimeout": return long.class;
+ case "consistency": return java.lang.String.class;
+ case "contextualtuples":
+ case "contextualTuples": return java.lang.String.class;
+ case "failopen":
+ case "failOpen": return boolean.class;
+ case "healthcheckenabled":
+ case "healthCheckEnabled": return boolean.class;
+ case "maxretries":
+ case "maxRetries": return int.class;
+ case "object": return java.lang.String.class;
+ case "openfgaclient":
+ case "openFgaClient": return
dev.openfga.sdk.api.client.OpenFgaClient.class;
+ case "readtimeout":
+ case "readTimeout": return long.class;
+ case "relation": return java.lang.String.class;
+ case "scopes": return java.lang.String.class;
+ case "sslcontextparameters":
+ case "sslContextParameters": return
org.apache.camel.support.jsse.SSLContextParameters.class;
+ case "storeid":
+ case "storeId": return java.lang.String.class;
+ case "useglobalsslcontextparameters":
+ case "useGlobalSslContextParameters": return boolean.class;
+ case "user": return java.lang.String.class;
+ default: return null;
+ }
+ }
+
+ @Override
+ public Object getOptionValue(Object obj, String name, boolean ignoreCase) {
+ org.apache.camel.component.openfga.security.OpenFgaSecurityPolicy
target = (org.apache.camel.component.openfga.security.OpenFgaSecurityPolicy)
obj;
+ switch (ignoreCase ? name.toLowerCase(Locale.ROOT) : name) {
+ case "apiaudience":
+ case "apiAudience": return target.getApiAudience();
+ case "apitoken":
+ case "apiToken": return target.getApiToken();
+ case "apitokenissuer":
+ case "apiTokenIssuer": return target.getApiTokenIssuer();
+ case "apiurl":
+ case "apiUrl": return target.getApiUrl();
+ case "authorizationmodelid":
+ case "authorizationModelId": return target.getAuthorizationModelId();
+ case "clientid":
+ case "clientId": return target.getClientId();
+ case "clientsecret":
+ case "clientSecret": return target.getClientSecret();
+ case "conditioncontext":
+ case "conditionContext": return target.getConditionContext();
+ case "connecttimeout":
+ case "connectTimeout": return target.getConnectTimeout();
+ case "consistency": return target.getConsistency();
+ case "contextualtuples":
+ case "contextualTuples": return target.getContextualTuples();
+ case "failopen":
+ case "failOpen": return target.isFailOpen();
+ case "healthcheckenabled":
+ case "healthCheckEnabled": return target.isHealthCheckEnabled();
+ case "maxretries":
+ case "maxRetries": return target.getMaxRetries();
+ case "object": return target.getObject();
+ case "openfgaclient":
+ case "openFgaClient": return target.getOpenFgaClient();
+ case "readtimeout":
+ case "readTimeout": return target.getReadTimeout();
+ case "relation": return target.getRelation();
+ case "scopes": return target.getScopes();
+ case "sslcontextparameters":
+ case "sslContextParameters": return target.getSslContextParameters();
+ case "storeid":
+ case "storeId": return target.getStoreId();
+ case "useglobalsslcontextparameters":
+ case "useGlobalSslContextParameters": return
target.isUseGlobalSslContextParameters();
+ case "user": return target.getUser();
+ default: return null;
+ }
+ }
+
+ @Override
+ public Object getCollectionValueType(Object target, String name, boolean
ignoreCase) {
+ switch (ignoreCase ? name.toLowerCase(Locale.ROOT) : name) {
+ case "conditioncontext":
+ case "conditionContext": return java.lang.Object.class;
+ default: return null;
+ }
+ }
+}
+
diff --git
a/components/camel-openfga/src/generated/resources/META-INF/services/org/apache/camel/bean.properties
b/components/camel-openfga/src/generated/resources/META-INF/services/org/apache/camel/bean.properties
new file mode 100644
index 000000000000..24541361a204
--- /dev/null
+++
b/components/camel-openfga/src/generated/resources/META-INF/services/org/apache/camel/bean.properties
@@ -0,0 +1,7 @@
+# Generated by camel build tools - do NOT edit this file!
+bean=OpenFgaSecurityPolicy
+groupId=org.apache.camel
+artifactId=camel-openfga
+version=4.24.0-SNAPSHOT
+projectName=Camel :: OpenFGA
+projectDescription=Camel OpenFGA Component
diff --git
a/components/camel-openfga/src/generated/resources/META-INF/services/org/apache/camel/bean/OpenFgaSecurityPolicy.json
b/components/camel-openfga/src/generated/resources/META-INF/services/org/apache/camel/bean/OpenFgaSecurityPolicy.json
new file mode 100644
index 000000000000..967a61fc9e11
--- /dev/null
+++
b/components/camel-openfga/src/generated/resources/META-INF/services/org/apache/camel/bean/OpenFgaSecurityPolicy.json
@@ -0,0 +1,16 @@
+{
+ "bean": {
+ "kind": "bean",
+ "name": "OpenFgaSecurityPolicy",
+ "javaType":
"org.apache.camel.component.openfga.security.OpenFgaSecurityPolicy",
+ "interfaceType": "org.apache.camel.spi.AuthorizationPolicy",
+ "title": "Open Fga Security Policy",
+ "description": "An authorization policy that asks OpenFGA whether a
subject has a relation to an object before the part of the route it guards
runs. A deny throws CamelAuthorizationException.",
+ "deprecated": false,
+ "groupId": "org.apache.camel",
+ "artifactId": "camel-openfga",
+ "version": "4.24.0-SNAPSHOT",
+ "properties": { "apiUrl": { "index": 0, "kind": "property", "displayName":
"Api Url", "required": false, "type": "string", "javaType": "java.lang.String",
"deprecated": false, "autowired": false, "secret": false, "defaultValue":
"http:\/\/localhost:8080", "description": "The base URL of the OpenFGA HTTP
API." }, "storeId": { "index": 1, "kind": "property", "displayName": "Store
Id", "required": true, "type": "string", "javaType": "java.lang.String",
"deprecated": false, "autowired": [...]
+ }
+}
+
diff --git
a/components/camel-openfga/src/generated/resources/META-INF/services/org/apache/camel/configurer/org.apache.camel.component.openfga.security.OpenFgaSecurityPolicy
b/components/camel-openfga/src/generated/resources/META-INF/services/org/apache/camel/configurer/org.apache.camel.component.openfga.security.OpenFgaSecurityPolicy
new file mode 100644
index 000000000000..c7001ab84487
--- /dev/null
+++
b/components/camel-openfga/src/generated/resources/META-INF/services/org/apache/camel/configurer/org.apache.camel.component.openfga.security.OpenFgaSecurityPolicy
@@ -0,0 +1,2 @@
+# Generated by camel build tools - do NOT edit this file!
+class=org.apache.camel.component.openfga.security.OpenFgaSecurityPolicyConfigurer
diff --git
a/components/camel-openfga/src/main/java/org/apache/camel/component/openfga/security/OpenFgaSecurityPolicy.java
b/components/camel-openfga/src/main/java/org/apache/camel/component/openfga/security/OpenFgaSecurityPolicy.java
index e2d0a5f102b6..be331da8d2d5 100644
---
a/components/camel-openfga/src/main/java/org/apache/camel/component/openfga/security/OpenFgaSecurityPolicy.java
+++
b/components/camel-openfga/src/main/java/org/apache/camel/component/openfga/security/OpenFgaSecurityPolicy.java
@@ -33,6 +33,8 @@ import
org.apache.camel.component.openfga.OpenFgaClientFactory;
import org.apache.camel.component.openfga.OpenFgaConfiguration;
import org.apache.camel.health.HealthCheckRegistry;
import org.apache.camel.spi.AuthorizationPolicy;
+import org.apache.camel.spi.Configurer;
+import org.apache.camel.spi.Metadata;
import org.apache.camel.support.jsse.SSLContextParameters;
import org.apache.camel.util.ObjectHelper;
import org.apache.camel.util.StringHelper;
@@ -61,14 +63,110 @@ import org.slf4j.LoggerFactory;
* {@code onException} machinery applies. The decision is also recorded on the
exchange in the {@code CamelOpenFga}
* headers.
*/
+@Metadata(label = "bean",
+ description = "An authorization policy that asks OpenFGA whether a
subject has a relation to an object before the"
+ + " part of the route it guards runs. A deny throws
CamelAuthorizationException.",
+ annotations = {
"interfaceName=org.apache.camel.spi.AuthorizationPolicy" })
+@Configurer(metadataOnly = true)
public class OpenFgaSecurityPolicy implements AuthorizationPolicy {
private static final Logger LOG =
LoggerFactory.getLogger(OpenFgaSecurityPolicy.class);
- private final OpenFgaConfiguration configuration = new
OpenFgaConfiguration();
-
- private boolean healthCheckEnabled = true;
+ // the defaults of the endpoint options this policy shares, so that the
two cannot drift apart
+ private static final OpenFgaConfiguration DEFAULTS = new
OpenFgaConfiguration();
+
+ @Metadata(defaultValue = "http://localhost:8080", description = "The base
URL of the OpenFGA HTTP API.")
+ private String apiUrl = DEFAULTS.getApiUrl();
+ @Metadata(required = true,
+ description = "The identifier of the OpenFGA store holding the
relationship tuples and the authorization"
+ + " model.")
+ private String storeId;
+ @Metadata(description = "The authorization model revision to evaluate
against. Pin it in production: left empty, the"
+ + " store's latest model is used, and that moves
whenever a new model is written.")
+ private String authorizationModelId;
+ @Metadata(required = true,
+ description = "The subject to authorize, as a Simple expression
resolving to an OpenFGA user identifier"
+ + " such as
user:${exchangeProperty.CamelKeycloakTokenSubject}. Read it from an exchange"
+ + " property the step that verified the caller
set, rather than from a header the caller sent."
+ + " An exchange whose subject resolves to blank is
denied.")
+ private String user;
+ @Metadata(required = true,
+ description = "The object being accessed, as a Simple expression
resolving to an OpenFGA object"
+ + " identifier such as
document:${header.documentId}. Taking it from a header is normal: the"
+ + " caller may say which resource it wants, and
the check decides whether it may have it.")
+ private String object;
+ @Metadata(required = true,
+ description = "The relation to demand, such as reader. Keep it
literal: it is the permission being"
+ + " demanded, so resolving it from an inbound
header would let the caller pick the weakest one"
+ + " the model defines.")
+ private String relation;
+ @Metadata(description = "Relationship tuples supplied for the duration of
one check and never stored, as"
+ + " semicolon-separated user,relation,object
triples, each part a Simple expression. A"
+ + " contextual tuple grants exactly like a stored
one, so derive it from something the route"
+ + " established, never from caller input.")
+ private String contextualTuples;
+ @Metadata(description = "Context the CEL expression of any conditioned
relation the check touches is evaluated with,"
+ + " as a map of parameter values. A value written
in YAML or a properties file is a string,"
+ + " which OpenFGA rejects for a bool parameter:
give that one as #valueAs(boolean):true.")
+ private Map<String, Object> conditionContext;
+ @Metadata(enums = "UNSPECIFIED,MINIMIZE_LATENCY,HIGHER_CONSISTENCY",
+ description = "The consistency the check is answered with. Set
HIGHER_CONSISTENCY where the window in which"
+ + " a just-revoked tuple can still grant access
matters.")
+ private String consistency;
+ @Metadata(label = "security", security = "secret",
+ description = "Pre-shared token sent to OpenFGA in the
Authorization header, for a server started with"
+ + " --authn-method preshared.")
+ private String apiToken;
+ @Metadata(label = "security",
+ description = "Client identifier for the OAuth 2.0
client-credentials flow. Setting it selects that flow,"
+ + " so clientSecret, apiTokenIssuer and
apiAudience are then required too.")
+ private String clientId;
+ @Metadata(label = "security", security = "secret",
+ description = "Client secret for the OAuth 2.0
client-credentials flow.")
+ private String clientSecret;
+ @Metadata(label = "security",
+ description = "The token endpoint the client-credentials flow
exchanges its credentials at.")
+ private String apiTokenIssuer;
+ @Metadata(label = "security",
+ description = "The audience to request the access token for in
the client-credentials flow.")
+ private String apiAudience;
+ @Metadata(label = "security", description = "Space-separated scopes to
request in the client-credentials flow.")
+ private String scopes;
+ @Metadata(label = "security", defaultValue = "false", security =
"insecure:dev",
+ description = "Whether to let the exchange proceed when OpenFGA
could not be asked at all, for example"
+ + " because the server is unreachable. It never
applies to a deny, nor to an exchange whose"
+ + " subject or object did not resolve. Do not
enable this in production.")
+ private boolean failOpen;
+ @Metadata(label = "advanced", defaultValue = "10000",
+ description = "How long to wait, in milliseconds, for the
connection to OpenFGA to be established.")
+ private long connectTimeout = DEFAULTS.getConnectTimeout();
+ @Metadata(label = "advanced", defaultValue = "10000",
+ description = "How long to wait, in milliseconds, for the check
to complete once connected. A request that"
+ + " times out is a failure to obtain a verdict
rather than a deny, so the exchange is denied"
+ + " unless failOpen is set.")
+ private long readTimeout = DEFAULTS.getReadTimeout();
+ @Metadata(label = "advanced", defaultValue = "3",
+ description = "How many times to retry a request that failed in
a way worth retrying, such as a rate limit"
+ + " or a 5xx.")
+ private int maxRetries = DEFAULTS.getMaxRetries();
+ @Metadata(label = "security",
+ description = "TLS configuration for the connection to OpenFGA,
to trust a server whose certificate comes"
+ + " from a private CA or to present a client
certificate to one that requires mutual TLS."
+ + " Takes precedence over
useGlobalSslContextParameters.")
+ private SSLContextParameters sslContextParameters;
+ @Metadata(label = "security", defaultValue = "false",
+ description = "Whether to use the global SSL context parameters,
the context-wide SSLContextParameters"
+ + " camel-main builds from camel.ssl.*, when
sslContextParameters is not set.")
private boolean useGlobalSslContextParameters;
+ @Metadata(label = "advanced", defaultValue = "true",
+ description = "Whether to register a readiness check for the
OpenFGA server this policy queries. The policy"
+ + " denies every exchange it guards while the
server is unreachable, so a route that cannot"
+ + " reach it is not ready.")
+ private boolean healthCheckEnabled = true;
+ @Metadata(label = "advanced",
+ description = "An existing OpenFgaClient to use instead of
building one from apiUrl, the credentials and"
+ + " sslContextParameters. No readiness check is
registered for it.")
+ private OpenFgaClient openFgaClient;
private volatile OpenFgaAuthorizer authorizer;
private volatile OpenFgaSecurityPolicyHealthCheck healthCheck;
@@ -81,25 +179,26 @@ public class OpenFgaSecurityPolicy implements
AuthorizationPolicy {
}
public OpenFgaSecurityPolicy(String apiUrl, String storeId) {
- configuration.setApiUrl(apiUrl);
- configuration.setStoreId(storeId);
+ this.apiUrl = apiUrl;
+ this.storeId = storeId;
}
@Override
public void beforeWrap(Route route, NamedNode definition) {
this.camelContext = route.getCamelContext();
if (authorizer == null) {
- StringHelper.notEmpty(configuration.getStoreId(), "storeId", this);
- StringHelper.notEmpty(configuration.getRelation(), "relation",
this);
- StringHelper.notEmpty(configuration.getUser(), "user", this);
- StringHelper.notEmpty(configuration.getObject(), "object", this);
- if (ObjectHelper.isEmpty(configuration.getAuthorizationModelId()))
{
+ StringHelper.notEmpty(storeId, "storeId", this);
+ StringHelper.notEmpty(relation, "relation", this);
+ StringHelper.notEmpty(user, "user", this);
+ StringHelper.notEmpty(object, "object", this);
+ if (ObjectHelper.isEmpty(authorizationModelId)) {
LOG.warn("authorizationModelId is not set on the OpenFGA
policy guarding route {}: it will evaluate"
+ " against store {}'s latest authorization model,
which changes as soon as a new model is"
+ " written. Pin the model id in production.",
- route.getRouteId(), configuration.getStoreId());
+ route.getRouteId(), storeId);
}
- OpenFgaClient client = configuration.getOpenFgaClient();
+ OpenFgaConfiguration configuration = toConfiguration();
+ OpenFgaClient client = openFgaClient;
if (client == null) {
sslContext = createSslContext(route.getCamelContext());
try {
@@ -108,7 +207,7 @@ public class OpenFgaSecurityPolicy implements
AuthorizationPolicy {
// beforeWrap cannot throw a checked exception, and a
policy that could not build its client must
// not start a route it would then fail to guard
throw new RuntimeCamelException(
- "Could not build the OpenFGA client for store " +
configuration.getStoreId(), e);
+ "Could not build the OpenFGA client for store " +
storeId, e);
}
ownsClient = true;
}
@@ -128,16 +227,14 @@ public class OpenFgaSecurityPolicy implements
AuthorizationPolicy {
*/
private synchronized void registerHealthCheck() {
if (!healthCheckEnabled || healthCheck != null || !ownsClient
- || ObjectHelper.isEmpty(configuration.getApiUrl()) ||
camelContext == null) {
+ || ObjectHelper.isEmpty(apiUrl) || camelContext == null) {
return;
}
HealthCheckRegistry registry = HealthCheckRegistry.get(camelContext);
if (registry == null) {
return;
}
- healthCheck = new OpenFgaSecurityPolicyHealthCheck(
- configuration.getApiUrl(), configuration.getApiToken(),
configuration.getStoreId(),
- configuration.getRelation(), sslContext);
+ healthCheck = new OpenFgaSecurityPolicyHealthCheck(apiUrl, apiToken,
storeId, relation, sslContext);
registry.register(healthCheck);
}
@@ -178,7 +275,7 @@ public class OpenFgaSecurityPolicy implements
AuthorizationPolicy {
public Processor wrap(Route route, final Processor processor) {
if (LOG.isDebugEnabled()) {
LOG.debug("Securing route {} with OpenFGA relation {} in store
{}", route.getRouteId(),
- configuration.getRelation(), configuration.getStoreId());
+ relation, storeId);
}
return new OpenFgaSecurityProcessor(processor, this);
}
@@ -193,7 +290,7 @@ public class OpenFgaSecurityPolicy implements
AuthorizationPolicy {
*/
private SSLContext createSslContext(CamelContext context) {
// the same precedence as the endpoint: an explicit configuration wins
over the context-wide one
- SSLContextParameters parameters =
configuration.getSslContextParameters();
+ SSLContextParameters parameters = sslContextParameters;
if (parameters == null && useGlobalSslContextParameters) {
parameters = context.getSSLContextParameters();
}
@@ -206,34 +303,65 @@ public class OpenFgaSecurityPolicy implements
AuthorizationPolicy {
// as above: a policy whose TLS configuration is broken must not
start a route that would then talk to
// OpenFGA over the JVM default trust material instead
throw new RuntimeCamelException(
- "Could not build the SSLContext for the OpenFGA policy on
store " + configuration.getStoreId(), e);
+ "Could not build the SSLContext for the OpenFGA policy on
store " + storeId, e);
}
}
+ /**
+ * The endpoint configuration the client and the authorizer are built
from, holding this policy's options. An option
+ * this method does not copy would be accepted and then silently ignored,
which
+ * {@code OpenFgaSecurityPolicyOptionsTest} guards against.
+ */
+ OpenFgaConfiguration toConfiguration() {
+ OpenFgaConfiguration answer = new OpenFgaConfiguration();
+ answer.setApiUrl(apiUrl);
+ answer.setStoreId(storeId);
+ answer.setAuthorizationModelId(authorizationModelId);
+ answer.setUser(user);
+ answer.setObject(object);
+ answer.setRelation(relation);
+ answer.setContextualTuples(contextualTuples);
+ answer.setConditionContext(conditionContext);
+ answer.setConsistency(consistency);
+ answer.setApiToken(apiToken);
+ answer.setClientId(clientId);
+ answer.setClientSecret(clientSecret);
+ answer.setApiTokenIssuer(apiTokenIssuer);
+ answer.setApiAudience(apiAudience);
+ answer.setScopes(scopes);
+ answer.setFailOpen(failOpen);
+ answer.setConnectTimeout(connectTimeout);
+ answer.setReadTimeout(readTimeout);
+ answer.setMaxRetries(maxRetries);
+ answer.setSslContextParameters(sslContextParameters);
+ answer.setOpenFgaClient(openFgaClient);
+ return answer;
+ }
+
public String getApiUrl() {
- return configuration.getApiUrl();
+ return apiUrl;
}
/**
* The base URL of the OpenFGA HTTP API.
*/
public void setApiUrl(String apiUrl) {
- configuration.setApiUrl(apiUrl);
+ this.apiUrl = apiUrl;
}
public String getStoreId() {
- return configuration.getStoreId();
+ return storeId;
}
/**
* The identifier of the OpenFGA store holding the relationship tuples and
the authorization model.
*/
public void setStoreId(String storeId) {
- configuration.setStoreId(storeId);
+ this.storeId = storeId;
}
public String getAuthorizationModelId() {
- return configuration.getAuthorizationModelId();
+ return authorizationModelId;
}
/**
@@ -241,11 +369,11 @@ public class OpenFgaSecurityPolicy implements
AuthorizationPolicy {
* is used and that moves whenever a new model is written.
*/
public void setAuthorizationModelId(String authorizationModelId) {
- configuration.setAuthorizationModelId(authorizationModelId);
+ this.authorizationModelId = authorizationModelId;
}
public String getUser() {
- return configuration.getUser();
+ return user;
}
/**
@@ -259,11 +387,11 @@ public class OpenFgaSecurityPolicy implements
AuthorizationPolicy {
* to record it. An exchange whose subject resolves to blank is denied.
*/
public void setUser(String user) {
- configuration.setUser(user);
+ this.user = user;
}
public String getObject() {
- return configuration.getObject();
+ return object;
}
/**
@@ -272,11 +400,11 @@ public class OpenFgaSecurityPolicy implements
AuthorizationPolicy {
* resource it wants, and the check decides whether it may have it.
*/
public void setObject(String object) {
- configuration.setObject(object);
+ this.object = object;
}
public String getRelation() {
- return configuration.getRelation();
+ return relation;
}
/**
@@ -284,11 +412,11 @@ public class OpenFgaSecurityPolicy implements
AuthorizationPolicy {
* resolving it from an inbound header would let the caller pick the
weakest one the model defines.
*/
public void setRelation(String relation) {
- configuration.setRelation(relation);
+ this.relation = relation;
}
public String getContextualTuples() {
- return configuration.getContextualTuples();
+ return contextualTuples;
}
/**
@@ -301,11 +429,11 @@ public class OpenFgaSecurityPolicy implements
AuthorizationPolicy {
* route established, such as a claim an earlier authentication step put
on an exchange property.
*/
public void setContextualTuples(String contextualTuples) {
- configuration.setContextualTuples(contextualTuples);
+ this.contextualTuples = contextualTuples;
}
public Map<String, Object> getConditionContext() {
- return configuration.getConditionContext();
+ return conditionContext;
}
/**
@@ -314,11 +442,11 @@ public class OpenFgaSecurityPolicy implements
AuthorizationPolicy {
* {@code timestamp}, {@code ipaddress}) instead of strings the server
would reject.
*/
public void setConditionContext(Map<String, Object> conditionContext) {
- configuration.setConditionContext(conditionContext);
+ this.conditionContext = conditionContext;
}
public String getConsistency() {
- return configuration.getConsistency();
+ return consistency;
}
/**
@@ -326,77 +454,77 @@ public class OpenFgaSecurityPolicy implements
AuthorizationPolicy {
* just-revoked tuple can still grant access matters.
*/
public void setConsistency(String consistency) {
- configuration.setConsistency(consistency);
+ this.consistency = consistency;
}
public String getApiToken() {
- return configuration.getApiToken();
+ return apiToken;
}
/**
* Pre-shared token for an OpenFGA server started with {@code
--authn-method preshared}.
*/
public void setApiToken(String apiToken) {
- configuration.setApiToken(apiToken);
+ this.apiToken = apiToken;
}
public String getClientId() {
- return configuration.getClientId();
+ return clientId;
}
/**
* Client identifier for the OAuth 2.0 client-credentials flow.
*/
public void setClientId(String clientId) {
- configuration.setClientId(clientId);
+ this.clientId = clientId;
}
public String getClientSecret() {
- return configuration.getClientSecret();
+ return clientSecret;
}
/**
* Client secret for the OAuth 2.0 client-credentials flow.
*/
public void setClientSecret(String clientSecret) {
- configuration.setClientSecret(clientSecret);
+ this.clientSecret = clientSecret;
}
public String getApiTokenIssuer() {
- return configuration.getApiTokenIssuer();
+ return apiTokenIssuer;
}
/**
* The token endpoint the client-credentials flow exchanges its
credentials at.
*/
public void setApiTokenIssuer(String apiTokenIssuer) {
- configuration.setApiTokenIssuer(apiTokenIssuer);
+ this.apiTokenIssuer = apiTokenIssuer;
}
public String getApiAudience() {
- return configuration.getApiAudience();
+ return apiAudience;
}
/**
* The audience to request the access token for in the client-credentials
flow.
*/
public void setApiAudience(String apiAudience) {
- configuration.setApiAudience(apiAudience);
+ this.apiAudience = apiAudience;
}
public String getScopes() {
- return configuration.getScopes();
+ return scopes;
}
/**
* Space-separated scopes to request in the OAuth 2.0 client-credentials
flow.
*/
public void setScopes(String scopes) {
- configuration.setScopes(scopes);
+ this.scopes = scopes;
}
public boolean isFailOpen() {
- return configuration.isFailOpen();
+ return failOpen;
}
/**
@@ -407,22 +535,22 @@ public class OpenFgaSecurityPolicy implements
AuthorizationPolicy {
* failures.
*/
public void setFailOpen(boolean failOpen) {
- configuration.setFailOpen(failOpen);
+ this.failOpen = failOpen;
}
public long getConnectTimeout() {
- return configuration.getConnectTimeout();
+ return connectTimeout;
}
/**
* How long to wait for the connection to OpenFGA to be established.
*/
public void setConnectTimeout(long connectTimeout) {
- configuration.setConnectTimeout(connectTimeout);
+ this.connectTimeout = connectTimeout;
}
public long getReadTimeout() {
- return configuration.getReadTimeout();
+ return readTimeout;
}
/**
@@ -430,22 +558,22 @@ public class OpenFgaSecurityPolicy implements
AuthorizationPolicy {
* verdict rather than a deny, so the policy denies the exchange unless
{@code failOpen} is set.
*/
public void setReadTimeout(long readTimeout) {
- configuration.setReadTimeout(readTimeout);
+ this.readTimeout = readTimeout;
}
public int getMaxRetries() {
- return configuration.getMaxRetries();
+ return maxRetries;
}
/**
* How many times to retry a request that failed in a way worth retrying.
*/
public void setMaxRetries(int maxRetries) {
- configuration.setMaxRetries(maxRetries);
+ this.maxRetries = maxRetries;
}
public SSLContextParameters getSslContextParameters() {
- return configuration.getSslContextParameters();
+ return sslContextParameters;
}
/**
@@ -454,7 +582,7 @@ public class OpenFgaSecurityPolicy implements
AuthorizationPolicy {
* {@code useGlobalSslContextParameters}.
*/
public void setSslContextParameters(SSLContextParameters
sslContextParameters) {
- configuration.setSslContextParameters(sslContextParameters);
+ this.sslContextParameters = sslContextParameters;
}
public boolean isUseGlobalSslContextParameters() {
@@ -486,13 +614,13 @@ public class OpenFgaSecurityPolicy implements
AuthorizationPolicy {
}
public OpenFgaClient getOpenFgaClient() {
- return configuration.getOpenFgaClient();
+ return openFgaClient;
}
/**
* An already-configured {@link OpenFgaClient} to use instead of building
one.
*/
public void setOpenFgaClient(OpenFgaClient openFgaClient) {
- configuration.setOpenFgaClient(openFgaClient);
+ this.openFgaClient = openFgaClient;
}
}
diff --git
a/components/camel-openfga/src/test/java/org/apache/camel/component/openfga/security/OpenFgaSecurityPolicyOptionsTest.java
b/components/camel-openfga/src/test/java/org/apache/camel/component/openfga/security/OpenFgaSecurityPolicyOptionsTest.java
new file mode 100644
index 000000000000..1ef0a694eac4
--- /dev/null
+++
b/components/camel-openfga/src/test/java/org/apache/camel/component/openfga/security/OpenFgaSecurityPolicyOptionsTest.java
@@ -0,0 +1,178 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.camel.component.openfga.security;
+
+import java.lang.reflect.Method;
+import java.lang.reflect.Modifier;
+import java.util.LinkedHashMap;
+import java.util.Map;
+import java.util.Set;
+import java.util.TreeMap;
+
+import dev.openfga.sdk.api.client.OpenFgaClient;
+import org.apache.camel.CamelContext;
+import org.apache.camel.component.openfga.OpenFgaConfiguration;
+import org.apache.camel.impl.DefaultCamelContext;
+import org.apache.camel.support.PropertyBindingSupport;
+import org.apache.camel.support.jsse.SSLContextParameters;
+import org.junit.jupiter.api.Test;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.Mockito.mock;
+
+/**
+ * The policy keeps its options in fields of its own, so that the bean catalog
can describe them, and builds the
+ * endpoint configuration its client and authorizer use from them when it
wraps a route. An option it forgets to copy
+ * would be accepted and then silently ignored - for a security policy, a
check that quietly does not apply.
+ */
+class OpenFgaSecurityPolicyOptionsTest {
+
+ // options of the policy itself, which the endpoint configuration has no
counterpart for
+ // (useGlobalSslContextParameters is a component option there, applied
where the SSLContext is built)
+ private static final Set<String> POLICY_ONLY =
Set.of("healthCheckEnabled", "useGlobalSslContextParameters");
+
+ @Test
+ void
everyOptionReachesTheConfigurationTheClientAndAuthorizerAreBuiltFrom() throws
Exception {
+ OpenFgaSecurityPolicy policy = new OpenFgaSecurityPolicy();
+ Map<String, Object> values = new TreeMap<>();
+ int n = 0;
+ for (Method setter : setters(OpenFgaSecurityPolicy.class).values()) {
+ String option = optionName(setter);
+ if (POLICY_ONLY.contains(option)) {
+ continue;
+ }
+ Object value = distinctValue(setter.getParameterTypes()[0],
option, n++);
+ setter.invoke(policy, value);
+ values.put(option, value);
+ }
+
+ // a new option of the policy is either an endpoint option the
configuration carries, or listed in POLICY_ONLY
+
assertThat(setters(OpenFgaConfiguration.class).keySet()).containsAll(values.keySet());
+ // the reflection above found the options at all
+ assertThat(values).hasSizeGreaterThanOrEqualTo(21);
+
+ OpenFgaConfiguration configuration = policy.toConfiguration();
+ for (Map.Entry<String, Object> entry : values.entrySet()) {
+ Object copied = getter(OpenFgaConfiguration.class,
entry.getKey()).invoke(configuration);
+ assertThat(copied).as(entry.getKey()).isEqualTo(entry.getValue());
+ assertThat(getter(OpenFgaSecurityPolicy.class,
entry.getKey()).invoke(policy)).as(entry.getKey())
+ .isEqualTo(entry.getValue());
+ }
+ }
+
+ @Test
+ void keepsTheDefaultsOfTheEndpointOptions() {
+ OpenFgaConfiguration defaults = new OpenFgaConfiguration();
+ OpenFgaSecurityPolicy policy = new OpenFgaSecurityPolicy();
+
+ assertThat(policy.getApiUrl()).isEqualTo(defaults.getApiUrl());
+
assertThat(policy.getConnectTimeout()).isEqualTo(defaults.getConnectTimeout());
+
assertThat(policy.getReadTimeout()).isEqualTo(defaults.getReadTimeout());
+ assertThat(policy.getMaxRetries()).isEqualTo(defaults.getMaxRetries());
+ assertThat(policy.isFailOpen()).isFalse();
+ assertThat(policy.isHealthCheckEnabled()).isTrue();
+ }
+
+ @Test
+ void bindsTheOptionsAsAYamlBeanDefinitionGivesThem() throws Exception {
+ // the YAML DSL hands every bean property over as a string, and a map
value as a nested map
+ try (CamelContext context = new DefaultCamelContext()) {
+ context.start();
+ SSLContextParameters tls = new SSLContextParameters();
+ context.getRegistry().bind("tls", tls);
+ Map<String, Object> conditionContext = new LinkedHashMap<>();
+ conditionContext.put("region", "eu");
+ conditionContext.put("trusted", "#valueAs(boolean):true");
+ Map<String, Object> properties = new LinkedHashMap<>();
+ properties.put("apiUrl", "https://openfga:8443");
+ properties.put("storeId", "01HSTORE");
+ properties.put("authorizationModelId", "01HMODEL");
+ properties.put("relation", "reader");
+ properties.put("user",
"user:${exchangeProperty.CamelSpiffePeerId}");
+ properties.put("object", "document:${header.documentId}");
+ properties.put("consistency", "HIGHER_CONSISTENCY");
+ properties.put("connectTimeout", "1500");
+ properties.put("maxRetries", "0");
+ properties.put("failOpen", "false");
+ properties.put("healthCheckEnabled", "false");
+ properties.put("sslContextParameters", "#bean:tls");
+ properties.put("conditionContext", conditionContext);
+
+ OpenFgaSecurityPolicy policy = new OpenFgaSecurityPolicy();
+ PropertyBindingSupport.setPropertiesOnTarget(context, policy,
properties);
+
+ assertThat(policy.getApiUrl()).isEqualTo("https://openfga:8443");
+ assertThat(policy.getStoreId()).isEqualTo("01HSTORE");
+ assertThat(policy.getAuthorizationModelId()).isEqualTo("01HMODEL");
+ assertThat(policy.getRelation()).isEqualTo("reader");
+
assertThat(policy.getUser()).isEqualTo("user:${exchangeProperty.CamelSpiffePeerId}");
+
assertThat(policy.getObject()).isEqualTo("document:${header.documentId}");
+
assertThat(policy.getConsistency()).isEqualTo("HIGHER_CONSISTENCY");
+ assertThat(policy.getConnectTimeout()).isEqualTo(1500L);
+ assertThat(policy.getMaxRetries()).isZero();
+ assertThat(policy.isFailOpen()).isFalse();
+ assertThat(policy.isHealthCheckEnabled()).isFalse();
+ assertThat(policy.getSslContextParameters()).isSameAs(tls);
+ assertThat(policy.getConditionContext()).containsEntry("region",
"eu").containsEntry("trusted", true);
+ }
+ }
+
+ private static Map<String, Method> setters(Class<?> type) {
+ Map<String, Method> answer = new TreeMap<>();
+ for (Method m : type.getMethods()) {
+ if (m.getDeclaringClass() == type && m.getName().startsWith("set")
&& m.getParameterCount() == 1
+ && !Modifier.isStatic(m.getModifiers())) {
+ answer.put(optionName(m), m);
+ }
+ }
+ return answer;
+ }
+
+ private static String optionName(Method setter) {
+ String name = setter.getName().substring(3);
+ return Character.toLowerCase(name.charAt(0)) + name.substring(1);
+ }
+
+ private static Method getter(Class<?> type, String option) throws
NoSuchMethodException {
+ String suffix = Character.toUpperCase(option.charAt(0)) +
option.substring(1);
+ try {
+ return type.getMethod("get" + suffix);
+ } catch (NoSuchMethodException e) {
+ return type.getMethod("is" + suffix);
+ }
+ }
+
+ // a value no option has by default, and no two options share
+ private static Object distinctValue(Class<?> type, String option, int n) {
+ if (type == String.class) {
+ return "value-of-" + option;
+ } else if (type == boolean.class) {
+ return true;
+ } else if (type == long.class) {
+ return 4200L + n;
+ } else if (type == int.class) {
+ return 42 + n;
+ } else if (type == Map.class) {
+ return Map.of("option", option);
+ } else if (type == SSLContextParameters.class) {
+ return new SSLContextParameters();
+ } else if (type == OpenFgaClient.class) {
+ return mock(OpenFgaClient.class);
+ }
+ throw new IllegalStateException("No test value for option " + option +
" of type " + type.getName());
+ }
+}
diff --git
a/components/camel-spiffe/src/generated/java/org/apache/camel/component/spiffe/SpiffeSSLContextParametersConfigurer.java
b/components/camel-spiffe/src/generated/java/org/apache/camel/component/spiffe/SpiffeSSLContextParametersConfigurer.java
new file mode 100644
index 000000000000..dd711cd681aa
--- /dev/null
+++
b/components/camel-spiffe/src/generated/java/org/apache/camel/component/spiffe/SpiffeSSLContextParametersConfigurer.java
@@ -0,0 +1,76 @@
+/* Generated by camel build tools - do NOT edit this file! */
+package org.apache.camel.component.spiffe;
+
+import javax.annotation.processing.Generated;
+import java.util.Locale;
+import java.util.Map;
+
+import org.apache.camel.CamelContext;
+import org.apache.camel.spi.ExtendedPropertyConfigurerGetter;
+import org.apache.camel.spi.PropertyConfigurerGetter;
+import org.apache.camel.spi.ConfigurerStrategy;
+import org.apache.camel.spi.GeneratedPropertyConfigurer;
+import org.apache.camel.util.CaseInsensitiveMap;
+import org.apache.camel.component.spiffe.SpiffeSSLContextParameters;
+
+/**
+ * Generated by camel build tools - do NOT edit this file!
+ */
+@Generated("org.apache.camel.maven.packaging.GenerateConfigurerMojo")
+@SuppressWarnings("unchecked")
+public class SpiffeSSLContextParametersConfigurer extends
org.apache.camel.support.component.PropertyConfigurerSupport implements
GeneratedPropertyConfigurer, PropertyConfigurerGetter {
+
+ @Override
+ public boolean configure(CamelContext camelContext, Object obj, String
name, Object value, boolean ignoreCase) {
+ org.apache.camel.component.spiffe.SpiffeSSLContextParameters target =
(org.apache.camel.component.spiffe.SpiffeSSLContextParameters) obj;
+ switch (ignoreCase ? name.toLowerCase(Locale.ROOT) : name) {
+ case "acceptanyspiffeid":
+ case "acceptAnySpiffeId":
target.setAcceptAnySpiffeId(property(camelContext, boolean.class, value));
return true;
+ case "acceptedspiffeids":
+ case "acceptedSpiffeIds":
target.setAcceptedSpiffeIds(property(camelContext, java.lang.String.class,
value)); return true;
+ case "camelcontext":
+ case "camelContext": target.setCamelContext(property(camelContext,
org.apache.camel.CamelContext.class, value)); return true;
+ case "inittimeout":
+ case "initTimeout": target.setInitTimeout(property(camelContext,
long.class, value)); return true;
+ case "spiffesocketpath":
+ case "spiffeSocketPath":
target.setSpiffeSocketPath(property(camelContext, java.lang.String.class,
value)); return true;
+ default: return false;
+ }
+ }
+
+ @Override
+ public Class<?> getOptionType(String name, boolean ignoreCase) {
+ switch (ignoreCase ? name.toLowerCase(Locale.ROOT) : name) {
+ case "acceptanyspiffeid":
+ case "acceptAnySpiffeId": return boolean.class;
+ case "acceptedspiffeids":
+ case "acceptedSpiffeIds": return java.lang.String.class;
+ case "camelcontext":
+ case "camelContext": return org.apache.camel.CamelContext.class;
+ case "inittimeout":
+ case "initTimeout": return long.class;
+ case "spiffesocketpath":
+ case "spiffeSocketPath": return java.lang.String.class;
+ default: return null;
+ }
+ }
+
+ @Override
+ public Object getOptionValue(Object obj, String name, boolean ignoreCase) {
+ org.apache.camel.component.spiffe.SpiffeSSLContextParameters target =
(org.apache.camel.component.spiffe.SpiffeSSLContextParameters) obj;
+ switch (ignoreCase ? name.toLowerCase(Locale.ROOT) : name) {
+ case "acceptanyspiffeid":
+ case "acceptAnySpiffeId": return target.isAcceptAnySpiffeId();
+ case "acceptedspiffeids":
+ case "acceptedSpiffeIds": return target.getAcceptedSpiffeIds();
+ case "camelcontext":
+ case "camelContext": return target.getCamelContext();
+ case "inittimeout":
+ case "initTimeout": return target.getInitTimeout();
+ case "spiffesocketpath":
+ case "spiffeSocketPath": return target.getSpiffeSocketPath();
+ default: return null;
+ }
+ }
+}
+
diff --git
a/components/camel-spiffe/src/generated/java/org/apache/camel/component/spiffe/SpiffeSecurityPolicyConfigurer.java
b/components/camel-spiffe/src/generated/java/org/apache/camel/component/spiffe/SpiffeSecurityPolicyConfigurer.java
new file mode 100644
index 000000000000..f25a58f3bf43
--- /dev/null
+++
b/components/camel-spiffe/src/generated/java/org/apache/camel/component/spiffe/SpiffeSecurityPolicyConfigurer.java
@@ -0,0 +1,64 @@
+/* Generated by camel build tools - do NOT edit this file! */
+package org.apache.camel.component.spiffe;
+
+import javax.annotation.processing.Generated;
+import java.util.Locale;
+import java.util.Map;
+
+import org.apache.camel.CamelContext;
+import org.apache.camel.spi.ExtendedPropertyConfigurerGetter;
+import org.apache.camel.spi.PropertyConfigurerGetter;
+import org.apache.camel.spi.ConfigurerStrategy;
+import org.apache.camel.spi.GeneratedPropertyConfigurer;
+import org.apache.camel.util.CaseInsensitiveMap;
+import org.apache.camel.component.spiffe.SpiffeSecurityPolicy;
+
+/**
+ * Generated by camel build tools - do NOT edit this file!
+ */
+@Generated("org.apache.camel.maven.packaging.GenerateConfigurerMojo")
+@SuppressWarnings("unchecked")
+public class SpiffeSecurityPolicyConfigurer extends
org.apache.camel.support.component.PropertyConfigurerSupport implements
GeneratedPropertyConfigurer, PropertyConfigurerGetter {
+
+ @Override
+ public boolean configure(CamelContext camelContext, Object obj, String
name, Object value, boolean ignoreCase) {
+ org.apache.camel.component.spiffe.SpiffeSecurityPolicy target =
(org.apache.camel.component.spiffe.SpiffeSecurityPolicy) obj;
+ switch (ignoreCase ? name.toLowerCase(Locale.ROOT) : name) {
+ case "acceptanyspiffeid":
+ case "acceptAnySpiffeId":
target.setAcceptAnySpiffeId(property(camelContext, boolean.class, value));
return true;
+ case "acceptedspiffeids":
+ case "acceptedSpiffeIds":
target.setAcceptedSpiffeIds(property(camelContext, java.lang.String.class,
value)); return true;
+ case "sslsessionheader":
+ case "sslSessionHeader":
target.setSslSessionHeader(property(camelContext, java.lang.String.class,
value)); return true;
+ default: return false;
+ }
+ }
+
+ @Override
+ public Class<?> getOptionType(String name, boolean ignoreCase) {
+ switch (ignoreCase ? name.toLowerCase(Locale.ROOT) : name) {
+ case "acceptanyspiffeid":
+ case "acceptAnySpiffeId": return boolean.class;
+ case "acceptedspiffeids":
+ case "acceptedSpiffeIds": return java.lang.String.class;
+ case "sslsessionheader":
+ case "sslSessionHeader": return java.lang.String.class;
+ default: return null;
+ }
+ }
+
+ @Override
+ public Object getOptionValue(Object obj, String name, boolean ignoreCase) {
+ org.apache.camel.component.spiffe.SpiffeSecurityPolicy target =
(org.apache.camel.component.spiffe.SpiffeSecurityPolicy) obj;
+ switch (ignoreCase ? name.toLowerCase(Locale.ROOT) : name) {
+ case "acceptanyspiffeid":
+ case "acceptAnySpiffeId": return target.isAcceptAnySpiffeId();
+ case "acceptedspiffeids":
+ case "acceptedSpiffeIds": return target.getAcceptedSpiffeIds();
+ case "sslsessionheader":
+ case "sslSessionHeader": return target.getSslSessionHeader();
+ default: return null;
+ }
+ }
+}
+
diff --git
a/components/camel-spiffe/src/generated/resources/META-INF/services/org/apache/camel/bean.properties
b/components/camel-spiffe/src/generated/resources/META-INF/services/org/apache/camel/bean.properties
new file mode 100644
index 000000000000..094c2262df2c
--- /dev/null
+++
b/components/camel-spiffe/src/generated/resources/META-INF/services/org/apache/camel/bean.properties
@@ -0,0 +1,7 @@
+# Generated by camel build tools - do NOT edit this file!
+bean=SpiffeSSLContextParameters SpiffeSecurityPolicy
+groupId=org.apache.camel
+artifactId=camel-spiffe
+version=4.24.0-SNAPSHOT
+projectName=Camel :: SPIFFE
+projectDescription=Camel SPIFFE Workload Identity Component
diff --git
a/components/camel-spiffe/src/generated/resources/META-INF/services/org/apache/camel/bean/SpiffeSSLContextParameters.json
b/components/camel-spiffe/src/generated/resources/META-INF/services/org/apache/camel/bean/SpiffeSSLContextParameters.json
new file mode 100644
index 000000000000..ca445236ce47
--- /dev/null
+++
b/components/camel-spiffe/src/generated/resources/META-INF/services/org/apache/camel/bean/SpiffeSSLContextParameters.json
@@ -0,0 +1,16 @@
+{
+ "bean": {
+ "kind": "bean",
+ "name": "SpiffeSSLContextParameters",
+ "javaType": "org.apache.camel.component.spiffe.SpiffeSSLContextParameters",
+ "interfaceType": "org.apache.camel.support.jsse.SSLContextParameters",
+ "title": "Spiffe SSLContext Parameters",
+ "description": "SSLContextParameters whose SSLContext is backed by the
SPIFFE Workload API: the X.509-SVID and the trust bundles are fetched live and
rotated automatically, giving mutual TLS to any component that accepts an
sslContextParameters reference. Set acceptedSpiffeIds or acceptAnySpiffeId; the
inherited options, such as serverParameters.clientAuthentication, still apply.",
+ "deprecated": false,
+ "groupId": "org.apache.camel",
+ "artifactId": "camel-spiffe",
+ "version": "4.24.0-SNAPSHOT",
+ "properties": { "spiffeSocketPath": { "index": 0, "kind": "property",
"displayName": "Spiffe Socket Path", "label": "security", "required": false,
"type": "string", "javaType": "java.lang.String", "deprecated": false,
"autowired": false, "secret": false, "description": "Path to the SPIFFE
Workload API endpoint (for example unix:\/\/\/tmp\/agent.sock). When not set,
the SPIFFE_ENDPOINT_SOCKET environment variable is used." },
"acceptedSpiffeIds": { "index": 1, "kind": "property", "dis [...]
+ }
+}
+
diff --git
a/components/camel-spiffe/src/generated/resources/META-INF/services/org/apache/camel/bean/SpiffeSecurityPolicy.json
b/components/camel-spiffe/src/generated/resources/META-INF/services/org/apache/camel/bean/SpiffeSecurityPolicy.json
new file mode 100644
index 000000000000..f132d90abb38
--- /dev/null
+++
b/components/camel-spiffe/src/generated/resources/META-INF/services/org/apache/camel/bean/SpiffeSecurityPolicy.json
@@ -0,0 +1,16 @@
+{
+ "bean": {
+ "kind": "bean",
+ "name": "SpiffeSecurityPolicy",
+ "javaType": "org.apache.camel.component.spiffe.SpiffeSecurityPolicy",
+ "interfaceType": "org.apache.camel.spi.AuthorizationPolicy",
+ "title": "Spiffe Security Policy",
+ "description": "An authorization policy that authorizes a route segment on
the caller's SPIFFE ID, read from the peer certificate that SPIFFE mutual TLS
verified. A peer that is not accepted, or presents no verified SVID, is denied
with CamelAuthorizationException.",
+ "deprecated": false,
+ "groupId": "org.apache.camel",
+ "artifactId": "camel-spiffe",
+ "version": "4.24.0-SNAPSHOT",
+ "properties": { "acceptedSpiffeIds": { "index": 0, "kind": "property",
"displayName": "Accepted Spiffe Ids", "required": false, "type": "string",
"javaType": "java.lang.String", "deprecated": false, "autowired": false,
"secret": false, "description": "Comma-separated allow-list of the peer SPIFFE
IDs this route segment authorizes, for example
spiffe:\/\/example.org\/frontend. Set exactly one of acceptedSpiffeIds and
acceptAnySpiffeId: setting both is rejected, and setting neither fai [...]
+ }
+}
+
diff --git
a/components/camel-spiffe/src/generated/resources/META-INF/services/org/apache/camel/configurer/org.apache.camel.component.spiffe.SpiffeSSLContextParameters
b/components/camel-spiffe/src/generated/resources/META-INF/services/org/apache/camel/configurer/org.apache.camel.component.spiffe.SpiffeSSLContextParameters
new file mode 100644
index 000000000000..2bb4bac8e0f8
--- /dev/null
+++
b/components/camel-spiffe/src/generated/resources/META-INF/services/org/apache/camel/configurer/org.apache.camel.component.spiffe.SpiffeSSLContextParameters
@@ -0,0 +1,2 @@
+# Generated by camel build tools - do NOT edit this file!
+class=org.apache.camel.component.spiffe.SpiffeSSLContextParametersConfigurer
diff --git
a/components/camel-spiffe/src/generated/resources/META-INF/services/org/apache/camel/configurer/org.apache.camel.component.spiffe.SpiffeSecurityPolicy
b/components/camel-spiffe/src/generated/resources/META-INF/services/org/apache/camel/configurer/org.apache.camel.component.spiffe.SpiffeSecurityPolicy
new file mode 100644
index 000000000000..2f2e257bd480
--- /dev/null
+++
b/components/camel-spiffe/src/generated/resources/META-INF/services/org/apache/camel/configurer/org.apache.camel.component.spiffe.SpiffeSecurityPolicy
@@ -0,0 +1,2 @@
+# Generated by camel build tools - do NOT edit this file!
+class=org.apache.camel.component.spiffe.SpiffeSecurityPolicyConfigurer
diff --git
a/components/camel-spiffe/src/main/java/org/apache/camel/component/spiffe/SpiffeSSLContextParameters.java
b/components/camel-spiffe/src/main/java/org/apache/camel/component/spiffe/SpiffeSSLContextParameters.java
index 42e20ebb5425..873203a951ec 100644
---
a/components/camel-spiffe/src/main/java/org/apache/camel/component/spiffe/SpiffeSSLContextParameters.java
+++
b/components/camel-spiffe/src/main/java/org/apache/camel/component/spiffe/SpiffeSSLContextParameters.java
@@ -33,6 +33,7 @@ import io.spiffe.workloadapi.X509Source;
import org.apache.camel.CamelContext;
import org.apache.camel.RuntimeCamelException;
import org.apache.camel.Service;
+import org.apache.camel.spi.Configurer;
import org.apache.camel.spi.Metadata;
import org.apache.camel.support.jsse.SSLContextParameters;
import org.apache.camel.util.ObjectHelper;
@@ -57,6 +58,14 @@ import org.apache.camel.util.ObjectHelper;
* The underlying {@code X509Source} is created lazily on the first call,
closed when the {@link CamelContext} shuts
* down, and the cached context is invalidated at the same time so a restarted
context rebuilds a fresh source.
*/
+@Metadata(label = "bean",
+ description = "SSLContextParameters whose SSLContext is backed by
the SPIFFE Workload API: the X.509-SVID and"
+ + " the trust bundles are fetched live and rotated
automatically, giving mutual TLS to any"
+ + " component that accepts an sslContextParameters
reference. Set acceptedSpiffeIds or"
+ + " acceptAnySpiffeId; the inherited options, such as
serverParameters.clientAuthentication,"
+ + " still apply.",
+ annotations = {
"interfaceName=org.apache.camel.support.jsse.SSLContextParameters" })
+@Configurer(metadataOnly = true)
public class SpiffeSSLContextParameters extends SSLContextParameters {
private static final String DEFAULT_PROTOCOL = "TLSv1.3";
diff --git
a/components/camel-spiffe/src/main/java/org/apache/camel/component/spiffe/SpiffeSecurityPolicy.java
b/components/camel-spiffe/src/main/java/org/apache/camel/component/spiffe/SpiffeSecurityPolicy.java
index 150ad4fe3449..a5e6b58d4bdb 100644
---
a/components/camel-spiffe/src/main/java/org/apache/camel/component/spiffe/SpiffeSecurityPolicy.java
+++
b/components/camel-spiffe/src/main/java/org/apache/camel/component/spiffe/SpiffeSecurityPolicy.java
@@ -24,6 +24,8 @@ import org.apache.camel.NamedNode;
import org.apache.camel.Processor;
import org.apache.camel.Route;
import org.apache.camel.spi.AuthorizationPolicy;
+import org.apache.camel.spi.Configurer;
+import org.apache.camel.spi.Metadata;
import org.apache.camel.util.ObjectHelper;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -50,6 +52,12 @@ import org.slf4j.LoggerFactory;
* {@link SpiffeConstants#PEER_SPIFFE_ID} exchange property, so an OPA policy
downstream can forward it to a Rego rule
* through its {@code includeProperties} option (CAMEL-24643).
*/
+@Metadata(label = "bean",
+ description = "An authorization policy that authorizes a route
segment on the caller's SPIFFE ID, read from the"
+ + " peer certificate that SPIFFE mutual TLS verified.
A peer that is not accepted, or presents no"
+ + " verified SVID, is denied with
CamelAuthorizationException.",
+ annotations = {
"interfaceName=org.apache.camel.spi.AuthorizationPolicy" })
+@Configurer(metadataOnly = true)
public class SpiffeSecurityPolicy implements AuthorizationPolicy {
/**
@@ -59,8 +67,19 @@ public class SpiffeSecurityPolicy implements
AuthorizationPolicy {
private static final Logger LOG =
LoggerFactory.getLogger(SpiffeSecurityPolicy.class);
+ @Metadata(description = "Comma-separated allow-list of the peer SPIFFE IDs
this route segment authorizes, for example"
+ + " spiffe://example.org/frontend. Set exactly one
of acceptedSpiffeIds and"
+ + " acceptAnySpiffeId: setting both is rejected,
and setting neither fails closed.")
private String acceptedSpiffeIds;
+ @Metadata(defaultValue = "false",
+ description = "Authorize any peer that presents a verified
SPIFFE SVID, leaving the which-peer decision to"
+ + " a downstream policy such as OPA. It still
fails closed when no verified peer certificate"
+ + " is present. Mutually exclusive with
acceptedSpiffeIds.")
private boolean acceptAnySpiffeId;
+ @Metadata(label = "advanced", defaultValue = DEFAULT_SSL_SESSION_HEADER,
+ description = "Name of the message header that carries the
verified SSLSession the peer certificate is"
+ + " read from. The default is the header
camel-netty and camel-netty-http populate; set it"
+ + " for another TLS consumer that exposes the
session under a different name.")
private String sslSessionHeader = DEFAULT_SSL_SESSION_HEADER;
private volatile Set<SpiffeId> acceptedSet = Set.of();
diff --git
a/components/camel-spiffe/src/test/java/org/apache/camel/component/spiffe/SpiffeBeanBindingTest.java
b/components/camel-spiffe/src/test/java/org/apache/camel/component/spiffe/SpiffeBeanBindingTest.java
new file mode 100644
index 000000000000..f58e162127a0
--- /dev/null
+++
b/components/camel-spiffe/src/test/java/org/apache/camel/component/spiffe/SpiffeBeanBindingTest.java
@@ -0,0 +1,86 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.camel.component.spiffe;
+
+import java.util.LinkedHashMap;
+import java.util.Map;
+
+import org.apache.camel.CamelContext;
+import org.apache.camel.impl.DefaultCamelContext;
+import org.apache.camel.support.PropertyBindingSupport;
+import org.junit.jupiter.api.Test;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+/**
+ * Binds the SPIFFE beans the way a YAML {@code beans:} entry does: every
value a string, a nested mapping a nested map.
+ */
+class SpiffeBeanBindingTest {
+
+ @Test
+ void bindsTheSslContextParametersOwnAndInheritedOptions() throws Exception
{
+ try (CamelContext context = new DefaultCamelContext()) {
+ context.start();
+ Map<String, Object> serverParameters = new LinkedHashMap<>();
+ serverParameters.put("clientAuthentication", "REQUIRE");
+ Map<String, Object> protocols = new LinkedHashMap<>();
+ protocols.put("secureSocketProtocol[0]", "TLSv1.3");
+ Map<String, Object> properties = new LinkedHashMap<>();
+ properties.put("spiffeSocketPath", "unix:///run/spire/agent.sock");
+ properties.put("acceptedSpiffeIds",
"spiffe://example.org/frontend,spiffe://example.org/batch");
+ properties.put("initTimeout", "5000");
+ // inherited from SSLContextParameters, which the generated
configurer does not cover
+ properties.put("serverParameters", serverParameters);
+ properties.put("secureSocketProtocols", protocols);
+ properties.put("secureSocketProtocol", "TLSv1.3");
+
+ SpiffeSSLContextParameters ssl = new SpiffeSSLContextParameters();
+ PropertyBindingSupport.setPropertiesOnTarget(context, ssl,
properties);
+
+
assertThat(ssl.getSpiffeSocketPath()).isEqualTo("unix:///run/spire/agent.sock");
+
assertThat(ssl.getAcceptedSpiffeIds()).isEqualTo("spiffe://example.org/frontend,spiffe://example.org/batch");
+ assertThat(ssl.isAcceptAnySpiffeId()).isFalse();
+ assertThat(ssl.getInitTimeout()).isEqualTo(5000L);
+
assertThat(ssl.getServerParameters().getClientAuthentication()).isEqualTo("REQUIRE");
+
assertThat(ssl.getSecureSocketProtocols().getSecureSocketProtocol()).containsExactly("TLSv1.3");
+ assertThat(ssl.getSecureSocketProtocol()).isEqualTo("TLSv1.3");
+ }
+ }
+
+ @Test
+ void bindsTheSecurityPolicyOptions() throws Exception {
+ try (CamelContext context = new DefaultCamelContext()) {
+ context.start();
+ Map<String, Object> properties = new LinkedHashMap<>();
+ properties.put("acceptedSpiffeIds",
"spiffe://example.org/frontend");
+ properties.put("sslSessionHeader", "CamelCustomSSLSession");
+
+ SpiffeSecurityPolicy policy = new SpiffeSecurityPolicy();
+ PropertyBindingSupport.setPropertiesOnTarget(context, policy,
properties);
+
+
assertThat(policy.getAcceptedSpiffeIds()).isEqualTo("spiffe://example.org/frontend");
+ assertThat(policy.isAcceptAnySpiffeId()).isFalse();
+
assertThat(policy.getSslSessionHeader()).isEqualTo("CamelCustomSSLSession");
+
+ SpiffeSecurityPolicy acceptAny = new SpiffeSecurityPolicy();
+ PropertyBindingSupport.setPropertiesOnTarget(context, acceptAny,
Map.of("acceptAnySpiffeId", "true"));
+
+ assertThat(acceptAny.isAcceptAnySpiffeId()).isTrue();
+
assertThat(acceptAny.getSslSessionHeader()).isEqualTo(SpiffeSecurityPolicy.DEFAULT_SSL_SESSION_HEADER);
+ }
+ }
+}
diff --git
a/core/camel-util/src/main/java/org/apache/camel/util/ArtifactUtils.java
b/core/camel-util/src/main/java/org/apache/camel/util/ArtifactUtils.java
index 6f54ab180d32..c526a64fa166 100644
--- a/core/camel-util/src/main/java/org/apache/camel/util/ArtifactUtils.java
+++ b/core/camel-util/src/main/java/org/apache/camel/util/ArtifactUtils.java
@@ -656,6 +656,10 @@ public final class ArtifactUtils {
"org.apache.camel.support.MemoryKeyValueRepository|org.apache.camel.spi.KeyValueRepository|camel-support"),
Map.<String, String> entry("MongoDbIdempotentRepository",
"org.apache.camel.component.mongodb.processor.idempotent.MongoDbIdempotentRepository|org.apache.camel.spi.IdempotentRepository|camel-mongodb"),
+ Map.<String, String> entry("OpaSecurityPolicy",
+
"org.apache.camel.component.opa.security.OpaSecurityPolicy|org.apache.camel.spi.AuthorizationPolicy|camel-opa"),
+ Map.<String, String> entry("OpenFgaSecurityPolicy",
+
"org.apache.camel.component.openfga.security.OpenFgaSecurityPolicy|org.apache.camel.spi.AuthorizationPolicy|camel-openfga"),
Map.<String, String>
entry("OpensearchBulkRequestAggregationStrategy",
"org.apache.camel.component.opensearch.aggregation.OpensearchBulkRequestAggregationStrategy|org.apache.camel.AggregationStrategy|camel-opensearch"),
Map.<String, String> entry("RedisAggregationRepository",
@@ -664,6 +668,10 @@ public final class ArtifactUtils {
"org.apache.camel.component.redis.RedisKeyValueRepository|org.apache.camel.spi.KeyValueRepository|camel-redis"),
Map.<String, String> entry("SimpleScheduledRoutePolicy",
"org.apache.camel.routepolicy.quartz.SimpleScheduledRoutePolicy|org.apache.camel.spi.RoutePolicy|camel-quartz"),
+ Map.<String, String> entry("SpiffeSSLContextParameters",
+
"org.apache.camel.component.spiffe.SpiffeSSLContextParameters|org.apache.camel.support.jsse.SSLContextParameters|camel-spiffe"),
+ Map.<String, String> entry("SpiffeSecurityPolicy",
+
"org.apache.camel.component.spiffe.SpiffeSecurityPolicy|org.apache.camel.spi.AuthorizationPolicy|camel-spiffe"),
Map.<String, String> entry("SpringCacheIdempotentRepository",
"org.apache.camel.spring.processor.idempotent.SpringCacheIdempotentRepository|org.apache.camel.spi.IdempotentRepository|camel-spring"),
Map.<String, String> entry("SpringRedisIdempotentRepository",