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 30923b03ebae CAMEL-26879: camel-keycloak, camel-shiro - add the
security policy beans to the bean catalog (#29710)
30923b03ebae is described below
commit 30923b03ebaed556192475a1cd79a9b6b86cb62a
Author: Andrea Cosentino <[email protected]>
AuthorDate: Thu Nov 12 17:07:49 2026 +0100
CAMEL-26879: camel-keycloak, camel-shiro - add the security policy beans to
the bean catalog (#29710)
KeycloakSecurityPolicy and ShiroSecurityPolicy get pojo-bean metadata in
the catalog, as CAMEL-26559 did for the opa, openfga and spiffe beans, so that
camel run resolves their jars for a YAML route that declares them under beans:
without --dep. Credentials are marked secret, and the bean and shiro.adoc
descriptions of alwaysReauthenticate, rolesList, publicKey and the time options
are corrected.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Signed-off-by: Andrea Cosentino <[email protected]>
---
.../org/apache/camel/catalog/beans.properties | 2 +
.../catalog/beans/KeycloakSecurityPolicy.json | 16 ++
.../camel/catalog/beans/ShiroSecurityPolicy.json | 16 ++
.../org/apache/camel/catalog/docs/shiro.adoc | 10 +-
.../org/apache/camel/catalog/CamelCatalogTest.java | 6 +-
.../security/KeycloakSecurityPolicyConfigurer.java | 187 +++++++++++++++++++++
.../services/org/apache/camel/bean.properties | 7 +
.../apache/camel/bean/KeycloakSecurityPolicy.json | 16 ++
...ponent.keycloak.security.KeycloakSecurityPolicy | 2 +
.../keycloak/security/KeycloakSecurityPolicy.java | 70 ++++++++
.../KeycloakSecurityPolicyBindingTest.java | 91 ++++++++++
.../security/ShiroSecurityPolicyConfigurer.java | 108 ++++++++++++
.../services/org/apache/camel/bean.properties | 7 +
.../org/apache/camel/bean/ShiroSecurityPolicy.json | 16 ++
...el.component.shiro.security.ShiroSecurityPolicy | 2 +
components/camel-shiro/src/main/docs/shiro.adoc | 10 +-
.../shiro/security/ShiroSecurityPolicy.java | 30 ++++
.../security/ShiroSecurityPolicyBindingTest.java | 77 +++++++++
.../java/org/apache/camel/util/ArtifactUtils.java | 4 +
19 files changed, 668 insertions(+), 9 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 dc3b21b7dcaf..10f0d5a67519 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
@@ -41,6 +41,7 @@ KafkaIdempotentRepository
KafkaKeyValueRepository
KeyValueAggregationRepository
KeyValueIdempotentRepository
+KeycloakSecurityPolicy
KubernetesClusterService
LoggingHttpActivityListener
MemoryAggregationRepository
@@ -53,6 +54,7 @@ OpenFgaSecurityPolicy
OpensearchBulkRequestAggregationStrategy
RedisAggregationRepository
RedisKeyValueRepository
+ShiroSecurityPolicy
SimpleScheduledRoutePolicy
SpiffeSSLContextParameters
SpiffeSecurityPolicy
diff --git
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/beans/KeycloakSecurityPolicy.json
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/beans/KeycloakSecurityPolicy.json
new file mode 100644
index 000000000000..22a7f1d27fec
--- /dev/null
+++
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/beans/KeycloakSecurityPolicy.json
@@ -0,0 +1,16 @@
+{
+ "bean": {
+ "kind": "bean",
+ "name": "KeycloakSecurityPolicy",
+ "javaType":
"org.apache.camel.component.keycloak.security.KeycloakSecurityPolicy",
+ "interfaceType": "org.apache.camel.spi.AuthorizationPolicy",
+ "title": "Keycloak Security Policy",
+ "description": "An authorization policy that validates the caller's
Keycloak access token and checks its roles and permissions before the part of
the route it guards runs. A caller that fails is denied with
CamelAuthorizationException.",
+ "deprecated": false,
+ "groupId": "org.apache.camel",
+ "artifactId": "camel-keycloak",
+ "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,
"description": "The base URL of the Keycloak server, for example
http:\/\/localhost:8080." }, "realm": { "index": 1, "kind": "property",
"displayName": "Realm", "required": false, "type": "string", "javaType":
"java.lang.String", "deprecated": false, "autowired": false [...]
+ }
+}
+
diff --git
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/beans/ShiroSecurityPolicy.json
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/beans/ShiroSecurityPolicy.json
new file mode 100644
index 000000000000..9b3ef1a5f00d
--- /dev/null
+++
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/beans/ShiroSecurityPolicy.json
@@ -0,0 +1,16 @@
+{
+ "bean": {
+ "kind": "bean",
+ "name": "ShiroSecurityPolicy",
+ "javaType":
"org.apache.camel.component.shiro.security.ShiroSecurityPolicy",
+ "interfaceType": "org.apache.camel.spi.AuthorizationPolicy",
+ "title": "Shiro Security Policy",
+ "description": "An authorization policy that authenticates the caller with
Apache Shiro, from the encrypted ShiroSecurityToken sent with the message, and
checks its permissions and roles. Create it with the Shiro INI resource as its
constructor argument, or set securityManager.",
+ "deprecated": false,
+ "groupId": "org.apache.camel",
+ "artifactId": "camel-shiro",
+ "version": "4.24.0-SNAPSHOT",
+ "properties": { "cipherService": { "index": 0, "kind": "property",
"displayName": "Cipher Service", "label": "advanced", "required": false,
"type": "object", "javaType": "org.apache.shiro.crypto.cipher.CipherService",
"deprecated": false, "autowired": false, "secret": false, "description": "The
cipher service that decrypts the ShiroSecurityToken sent with the message. AES
by default; Shiro also ships a Blowfish one." }, "passPhrase": { "index": 1,
"kind": "property", "displayName": " [...]
+ }
+}
+
diff --git
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/shiro.adoc
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/shiro.adoc
index e57ce7d6e0e1..f617b5afd510 100644
---
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/shiro.adoc
+++
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/shiro.adoc
@@ -120,9 +120,10 @@ acquired from the file system, classpath, or URLs when
prefixed with
|`passPhrase` |`An AES 128 based key` |byte[] |A passPhrase to decrypt
ShiroSecurityToken(s) sent along with Message
Exchanges
-|`alwaysReauthenticate` |`true` |boolean |Setting to ensure re-authentication
on every request. If set
-to false, the user is authenticated and locked such than only requests
-from the same user going forward are authenticated.
+|`alwaysReauthenticate` |`true` |boolean |Whether to log the caller out after
every exchange. If set
+to false, the authenticated subject stays bound to the worker thread for
+the next exchange on that thread. The token of every exchange is
+authenticated either way.
|`permissionsList` |`none` |List<Permission> |A List of permissions required
in order for an authenticated user to be
authorized to perform further action i.e., continue further on the route.
@@ -136,7 +137,8 @@ user to be authorized to perform further action i.e.,
continue further on
the route. If no roles list or permissions list (see above) is provided
to the ShiroSecurityPolicy object, then authorization is deemed as not
required. Note that the default is that authorization is granted if any
-of the roles in the list are applicable.
+of the roles in the list are applicable. The roles are checked only when
+no permissions list is set: when both are set, the permissions decide.
|`cipherService` |`AES` |org.apache.shiro.crypto.CipherService |Shiro ships
with AES & Blowfish-based CipherServices. You may use one of
these or pass in your own Cipher implementation
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 7a8adb8a2ef2..e6fdb4a424ec 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
@@ -2276,7 +2276,11 @@ public class CamelCatalogTest {
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-spiffe",
"org.apache.camel.support.jsse.SSLContextParameters", "acceptedSpiffeIds"),
+ Arguments.of("KeycloakSecurityPolicy",
"org.apache.camel.component.keycloak.security.KeycloakSecurityPolicy",
+ "camel-keycloak",
"org.apache.camel.spi.AuthorizationPolicy", "realm"),
+ Arguments.of("ShiroSecurityPolicy",
"org.apache.camel.component.shiro.security.ShiroSecurityPolicy",
+ "camel-shiro",
"org.apache.camel.spi.AuthorizationPolicy", "passPhrase"));
}
// camel run looks up a bean class it cannot load by its simple name here,
to find the jar to download
diff --git
a/components/camel-keycloak/src/generated/java/org/apache/camel/component/keycloak/security/KeycloakSecurityPolicyConfigurer.java
b/components/camel-keycloak/src/generated/java/org/apache/camel/component/keycloak/security/KeycloakSecurityPolicyConfigurer.java
new file mode 100644
index 000000000000..7ffff53502b8
--- /dev/null
+++
b/components/camel-keycloak/src/generated/java/org/apache/camel/component/keycloak/security/KeycloakSecurityPolicyConfigurer.java
@@ -0,0 +1,187 @@
+/* Generated by camel build tools - do NOT edit this file! */
+package org.apache.camel.component.keycloak.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.keycloak.security.KeycloakSecurityPolicy;
+
+/**
+ * Generated by camel build tools - do NOT edit this file!
+ */
+@Generated("org.apache.camel.maven.packaging.GenerateConfigurerMojo")
+@SuppressWarnings("unchecked")
+public class KeycloakSecurityPolicyConfigurer 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.keycloak.security.KeycloakSecurityPolicy
target = (org.apache.camel.component.keycloak.security.KeycloakSecurityPolicy)
obj;
+ switch (ignoreCase ? name.toLowerCase(Locale.ROOT) : name) {
+ case "allpermissionsrequired":
+ case "allPermissionsRequired":
target.setAllPermissionsRequired(property(camelContext, boolean.class, value));
return true;
+ case "allrolesrequired":
+ case "allRolesRequired":
target.setAllRolesRequired(property(camelContext, boolean.class, value));
return true;
+ case "allowtokenfromheader":
+ case "allowTokenFromHeader":
target.setAllowTokenFromHeader(property(camelContext, boolean.class, value));
return true;
+ case "autofetchpublickey":
+ case "autoFetchPublicKey":
target.setAutoFetchPublicKey(property(camelContext, boolean.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 "expectedaudience":
+ case "expectedAudience":
target.setExpectedAudience(property(camelContext, java.lang.String.class,
value)); return true;
+ case "expectedauthorizedparty":
+ case "expectedAuthorizedParty":
target.setExpectedAuthorizedParty(property(camelContext,
java.lang.String.class, value)); return true;
+ case "expectedtokentypes":
+ case "expectedTokenTypes":
target.setExpectedTokenTypes(property(camelContext, java.lang.String.class,
value)); return true;
+ case "introspectioncacheenabled":
+ case "introspectionCacheEnabled":
target.setIntrospectionCacheEnabled(property(camelContext, boolean.class,
value)); return true;
+ case "introspectioncachettl":
+ case "introspectionCacheTtl":
target.setIntrospectionCacheTtl(property(camelContext, long.class, value));
return true;
+ case "jwksrefreshinterval":
+ case "jwksRefreshInterval":
target.setJwksRefreshInterval(property(camelContext, long.class, value));
return true;
+ case "minjwksrefreshinterval":
+ case "minJwksRefreshInterval":
target.setMinJwksRefreshInterval(property(camelContext, long.class, value));
return true;
+ case "password": target.setPassword(property(camelContext,
java.lang.String.class, value)); return true;
+ case "preferpropertyoverheader":
+ case "preferPropertyOverHeader":
target.setPreferPropertyOverHeader(property(camelContext, boolean.class,
value)); return true;
+ case "publickey":
+ case "publicKey": target.setPublicKey(property(camelContext,
java.security.PublicKey.class, value)); return true;
+ case "realm": target.setRealm(property(camelContext,
java.lang.String.class, value)); return true;
+ case "requiredpermissions":
+ case "requiredPermissions":
target.setRequiredPermissions(property(camelContext, java.lang.String.class,
value)); return true;
+ case "requiredroles":
+ case "requiredRoles": target.setRequiredRoles(property(camelContext,
java.lang.String.class, value)); return true;
+ case "serverurl":
+ case "serverUrl": target.setServerUrl(property(camelContext,
java.lang.String.class, value)); return true;
+ case "useresourceownerpasswordcredentials":
+ case "useResourceOwnerPasswordCredentials":
target.setUseResourceOwnerPasswordCredentials(property(camelContext,
boolean.class, value)); return true;
+ case "usetokenintrospection":
+ case "useTokenIntrospection":
target.setUseTokenIntrospection(property(camelContext, boolean.class, value));
return true;
+ case "username": target.setUsername(property(camelContext,
java.lang.String.class, value)); return true;
+ case "validateissuer":
+ case "validateIssuer": target.setValidateIssuer(property(camelContext,
boolean.class, value)); return true;
+ case "validatetokenbinding":
+ case "validateTokenBinding":
target.setValidateTokenBinding(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 "allpermissionsrequired":
+ case "allPermissionsRequired": return boolean.class;
+ case "allrolesrequired":
+ case "allRolesRequired": return boolean.class;
+ case "allowtokenfromheader":
+ case "allowTokenFromHeader": return boolean.class;
+ case "autofetchpublickey":
+ case "autoFetchPublicKey": return boolean.class;
+ case "clientid":
+ case "clientId": return java.lang.String.class;
+ case "clientsecret":
+ case "clientSecret": return java.lang.String.class;
+ case "expectedaudience":
+ case "expectedAudience": return java.lang.String.class;
+ case "expectedauthorizedparty":
+ case "expectedAuthorizedParty": return java.lang.String.class;
+ case "expectedtokentypes":
+ case "expectedTokenTypes": return java.lang.String.class;
+ case "introspectioncacheenabled":
+ case "introspectionCacheEnabled": return boolean.class;
+ case "introspectioncachettl":
+ case "introspectionCacheTtl": return long.class;
+ case "jwksrefreshinterval":
+ case "jwksRefreshInterval": return long.class;
+ case "minjwksrefreshinterval":
+ case "minJwksRefreshInterval": return long.class;
+ case "password": return java.lang.String.class;
+ case "preferpropertyoverheader":
+ case "preferPropertyOverHeader": return boolean.class;
+ case "publickey":
+ case "publicKey": return java.security.PublicKey.class;
+ case "realm": return java.lang.String.class;
+ case "requiredpermissions":
+ case "requiredPermissions": return java.lang.String.class;
+ case "requiredroles":
+ case "requiredRoles": return java.lang.String.class;
+ case "serverurl":
+ case "serverUrl": return java.lang.String.class;
+ case "useresourceownerpasswordcredentials":
+ case "useResourceOwnerPasswordCredentials": return boolean.class;
+ case "usetokenintrospection":
+ case "useTokenIntrospection": return boolean.class;
+ case "username": return java.lang.String.class;
+ case "validateissuer":
+ case "validateIssuer": return boolean.class;
+ case "validatetokenbinding":
+ case "validateTokenBinding": return boolean.class;
+ default: return null;
+ }
+ }
+
+ @Override
+ public Object getOptionValue(Object obj, String name, boolean ignoreCase) {
+ org.apache.camel.component.keycloak.security.KeycloakSecurityPolicy
target = (org.apache.camel.component.keycloak.security.KeycloakSecurityPolicy)
obj;
+ switch (ignoreCase ? name.toLowerCase(Locale.ROOT) : name) {
+ case "allpermissionsrequired":
+ case "allPermissionsRequired": return
target.isAllPermissionsRequired();
+ case "allrolesrequired":
+ case "allRolesRequired": return target.isAllRolesRequired();
+ case "allowtokenfromheader":
+ case "allowTokenFromHeader": return target.isAllowTokenFromHeader();
+ case "autofetchpublickey":
+ case "autoFetchPublicKey": return target.isAutoFetchPublicKey();
+ case "clientid":
+ case "clientId": return target.getClientId();
+ case "clientsecret":
+ case "clientSecret": return target.getClientSecret();
+ case "expectedaudience":
+ case "expectedAudience": return target.getExpectedAudience();
+ case "expectedauthorizedparty":
+ case "expectedAuthorizedParty": return
target.getExpectedAuthorizedParty();
+ case "expectedtokentypes":
+ case "expectedTokenTypes": return target.getExpectedTokenTypes();
+ case "introspectioncacheenabled":
+ case "introspectionCacheEnabled": return
target.isIntrospectionCacheEnabled();
+ case "introspectioncachettl":
+ case "introspectionCacheTtl": return target.getIntrospectionCacheTtl();
+ case "jwksrefreshinterval":
+ case "jwksRefreshInterval": return target.getJwksRefreshInterval();
+ case "minjwksrefreshinterval":
+ case "minJwksRefreshInterval": return
target.getMinJwksRefreshInterval();
+ case "password": return target.getPassword();
+ case "preferpropertyoverheader":
+ case "preferPropertyOverHeader": return
target.isPreferPropertyOverHeader();
+ case "publickey":
+ case "publicKey": return target.getPublicKey();
+ case "realm": return target.getRealm();
+ case "requiredpermissions":
+ case "requiredPermissions": return target.getRequiredPermissions();
+ case "requiredroles":
+ case "requiredRoles": return target.getRequiredRoles();
+ case "serverurl":
+ case "serverUrl": return target.getServerUrl();
+ case "useresourceownerpasswordcredentials":
+ case "useResourceOwnerPasswordCredentials": return
target.isUseResourceOwnerPasswordCredentials();
+ case "usetokenintrospection":
+ case "useTokenIntrospection": return target.isUseTokenIntrospection();
+ case "username": return target.getUsername();
+ case "validateissuer":
+ case "validateIssuer": return target.isValidateIssuer();
+ case "validatetokenbinding":
+ case "validateTokenBinding": return target.isValidateTokenBinding();
+ default: return null;
+ }
+ }
+}
+
diff --git
a/components/camel-keycloak/src/generated/resources/META-INF/services/org/apache/camel/bean.properties
b/components/camel-keycloak/src/generated/resources/META-INF/services/org/apache/camel/bean.properties
new file mode 100644
index 000000000000..da7d9ecb0ea4
--- /dev/null
+++
b/components/camel-keycloak/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=KeycloakSecurityPolicy
+groupId=org.apache.camel
+artifactId=camel-keycloak
+version=4.24.0-SNAPSHOT
+projectName=Camel :: Keycloak
+projectDescription=Security using Keycloak
diff --git
a/components/camel-keycloak/src/generated/resources/META-INF/services/org/apache/camel/bean/KeycloakSecurityPolicy.json
b/components/camel-keycloak/src/generated/resources/META-INF/services/org/apache/camel/bean/KeycloakSecurityPolicy.json
new file mode 100644
index 000000000000..22a7f1d27fec
--- /dev/null
+++
b/components/camel-keycloak/src/generated/resources/META-INF/services/org/apache/camel/bean/KeycloakSecurityPolicy.json
@@ -0,0 +1,16 @@
+{
+ "bean": {
+ "kind": "bean",
+ "name": "KeycloakSecurityPolicy",
+ "javaType":
"org.apache.camel.component.keycloak.security.KeycloakSecurityPolicy",
+ "interfaceType": "org.apache.camel.spi.AuthorizationPolicy",
+ "title": "Keycloak Security Policy",
+ "description": "An authorization policy that validates the caller's
Keycloak access token and checks its roles and permissions before the part of
the route it guards runs. A caller that fails is denied with
CamelAuthorizationException.",
+ "deprecated": false,
+ "groupId": "org.apache.camel",
+ "artifactId": "camel-keycloak",
+ "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,
"description": "The base URL of the Keycloak server, for example
http:\/\/localhost:8080." }, "realm": { "index": 1, "kind": "property",
"displayName": "Realm", "required": false, "type": "string", "javaType":
"java.lang.String", "deprecated": false, "autowired": false [...]
+ }
+}
+
diff --git
a/components/camel-keycloak/src/generated/resources/META-INF/services/org/apache/camel/configurer/org.apache.camel.component.keycloak.security.KeycloakSecurityPolicy
b/components/camel-keycloak/src/generated/resources/META-INF/services/org/apache/camel/configurer/org.apache.camel.component.keycloak.security.KeycloakSecurityPolicy
new file mode 100644
index 000000000000..185de325ffe3
--- /dev/null
+++
b/components/camel-keycloak/src/generated/resources/META-INF/services/org/apache/camel/configurer/org.apache.camel.component.keycloak.security.KeycloakSecurityPolicy
@@ -0,0 +1,2 @@
+# Generated by camel build tools - do NOT edit this file!
+class=org.apache.camel.component.keycloak.security.KeycloakSecurityPolicyConfigurer
diff --git
a/components/camel-keycloak/src/main/java/org/apache/camel/component/keycloak/security/KeycloakSecurityPolicy.java
b/components/camel-keycloak/src/main/java/org/apache/camel/component/keycloak/security/KeycloakSecurityPolicy.java
index 3bd318de6a7c..f8d10c3bcd05 100644
---
a/components/camel-keycloak/src/main/java/org/apache/camel/component/keycloak/security/KeycloakSecurityPolicy.java
+++
b/components/camel-keycloak/src/main/java/org/apache/camel/component/keycloak/security/KeycloakSecurityPolicy.java
@@ -26,61 +26,110 @@ 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.keycloak.admin.client.Keycloak;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
+@Metadata(label = "bean",
+ description = "An authorization policy that validates the caller's
Keycloak access token and checks its roles and"
+ + " permissions before the part of the route it guards
runs. A caller that fails is denied with"
+ + " CamelAuthorizationException.",
+ annotations = {
"interfaceName=org.apache.camel.spi.AuthorizationPolicy" })
+@Configurer(metadataOnly = true)
public class KeycloakSecurityPolicy implements AuthorizationPolicy {
private static final Logger LOG =
LoggerFactory.getLogger(KeycloakSecurityPolicy.class);
+ @Metadata(description = "The base URL of the Keycloak server, for example
http://localhost:8080.")
private String serverUrl;
+ @Metadata(description = "The Keycloak realm the tokens are issued in.")
private String realm;
+ @Metadata(description = "The client id of this application in the realm.")
private String clientId;
+ @Metadata(label = "security", security = "secret",
+ description = "The client secret, for a confidential client.")
private String clientSecret;
+ @Metadata(label = "security", security = "secret",
+ description = "The user to sign in as when
useResourceOwnerPasswordCredentials is enabled.")
private String username;
+ @Metadata(label = "security", security = "secret",
+ description = "The password of that user.")
private String password;
/**
* Comma-separated list of required roles for authorization. Example:
"admin,user,manager"
*/
+ @Metadata(description = "Comma-separated list of the roles the caller must
have, for example admin,user. Whether it needs"
+ + " all of them or any one is set by
allRolesRequired.")
private String requiredRoles;
/**
* Comma-separated list of required permissions for authorization.
Example: "read:documents,write:documents"
*/
+ @Metadata(description = "Comma-separated list of the permissions the
caller must have, for example"
+ + " read:documents,write:documents. Whether it
needs all of them or any one is set by"
+ + " allPermissionsRequired.")
private String requiredPermissions;
+ @Metadata(defaultValue = "true",
+ description = "Whether the caller must have all the required
roles, rather than any one of them.")
private boolean allRolesRequired = true;
+ @Metadata(defaultValue = "true",
+ description = "Whether the caller must have all the required
permissions, rather than any one of them.")
private boolean allPermissionsRequired = true;
+ @Metadata(label = "security", defaultValue = "false",
+ description = "Whether the policy's Keycloak client signs in
with username and password, the resource owner"
+ + " password credentials grant, rather than with
clientId and clientSecret.")
private boolean useResourceOwnerPasswordCredentials = false;
+ @Metadata(label = "security",
+ description = "The public key to verify token signatures with.
Used only when autoFetchPublicKey is false,"
+ + " which it is not by default.")
private PublicKey publicKey;
/**
* Enable OAuth 2.0 token introspection for real-time token validation.
When enabled, tokens are validated by
* calling Keycloak's introspection endpoint instead of local JWT parsing.
This allows detecting revoked tokens
* before expiration.
*/
+ @Metadata(label = "security", defaultValue = "false",
+ description = "Whether to validate tokens with Keycloak's token
introspection endpoint rather than by parsing"
+ + " the JWT locally, which detects a token revoked
before it expires.")
private boolean useTokenIntrospection = false;
/**
* Enable caching of token introspection results to reduce API calls to
Keycloak.
*/
+ @Metadata(label = "advanced", defaultValue = "true",
+ description = "Whether to cache token introspection results, to
reduce the calls to Keycloak.")
private boolean introspectionCacheEnabled = true;
/**
* Time-to-live for cached introspection results in seconds. Default is 60
seconds.
*/
+ @Metadata(label = "advanced", defaultValue = "60",
+ description = "How long a cached introspection result is kept,
in seconds, given as a plain number rather"
+ + " than a time pattern such as 5m.")
private long introspectionCacheTtl = 60;
/**
* Enable validation that tokens are bound to specific sessions. When
enabled, tokens from headers must match the
* session-bound token to prevent session fixation attacks. Default is
true for security.
*/
+ @Metadata(label = "security", defaultValue = "true",
+ description = "Whether a token from a header must match the
token bound to the session, which prevents session"
+ + " fixation.")
private boolean validateTokenBinding = true;
/**
* Allow token retrieval from HTTP headers. When disabled, tokens can only
come from exchange properties. Disabling
* this prevents attackers from injecting tokens via headers. Default is
true for backward compatibility, but should
* be set to false in production environments where tokens are set
programmatically.
*/
+ @Metadata(label = "security", defaultValue = "true",
+ description = "Whether the token may come from a message header.
Disable it where the route sets the token"
+ + " itself, so a caller cannot inject one through
a header.")
private boolean allowTokenFromHeader = true;
/**
* Prefer tokens from exchange properties over headers. When true, if a
token exists in both property and header,
* the property value is used. This prevents header-based token override
attacks. Default is true for security.
*/
+ @Metadata(label = "security", defaultValue = "true",
+ description = "Whether a token in an exchange property wins over
one in a header, which prevents a header"
+ + " from overriding the token the route
established.")
private boolean preferPropertyOverHeader = true;
private Keycloak keycloakClient;
@@ -91,21 +140,34 @@ public class KeycloakSecurityPolicy implements
AuthorizationPolicy {
* an issuer that does not match the configured serverUrl and realm will
be rejected. This prevents cross-realm
* token injection attacks in multi-tenant environments.
*/
+ @Metadata(label = "security", defaultValue = "true",
+ description = "Whether a token must have been issued by the
configured serverUrl and realm, which prevents"
+ + " cross-realm token injection.")
private boolean validateIssuer = true;
/**
* Enable automatic fetching of public keys from the Keycloak JWKS
endpoint for signature verification. When enabled
* (default), public keys are automatically fetched and cached from
* {serverUrl}/realms/{realm}/protocol/openid-connect/certs. This ensures
token signatures are properly verified.
*/
+ @Metadata(label = "security", defaultValue = "true",
+ description = "Whether to fetch the keys that verify token
signatures from the realm's JWKS endpoint, and"
+ + " cache them.")
private boolean autoFetchPublicKey = true;
/**
* Cache refresh interval for Keycloak JWKS public keys in seconds.
Default is 300 seconds (5 minutes).
*/
+ @Metadata(label = "advanced", defaultValue = "300",
+ description = "How often the cached JWKS keys are refreshed, in
seconds, given as a plain number rather than"
+ + " a time pattern such as 5m.")
private long jwksRefreshInterval = 300;
/**
* Minimum cooldown interval in seconds between remote JWKS refreshes when
an unknown key ID (kid) is encountered.
* This rate-limits outbound calls and prevents JWKS cache stampede.
Default is 10 seconds.
*/
+ @Metadata(label = "advanced", defaultValue = "10",
+ description = "The minimum time between two JWKS refreshes
triggered by an unknown key id, which"
+ + " rate-limits the calls to Keycloak, in seconds,
given as a plain number rather than a time"
+ + " pattern such as 5m.")
private long minJwksRefreshInterval = 10;
/**
* Comma-separated list of expected audiences for token validation. When
set, tokens whose "aud" claim does not
@@ -113,6 +175,9 @@ public class KeycloakSecurityPolicy implements
AuthorizationPolicy {
* behavior). This prevents a token issued for one client from being
accepted by routes intended for another client
* in multi-client Keycloak realms. Disabled by default for backward
compatibility. Example: "my-client"
*/
+ @Metadata(label = "security",
+ description = "Comma-separated list of audiences a token must
all carry in its aud claim, so that a token"
+ + " issued for another client of the realm is
rejected.")
private String expectedAudience;
/**
@@ -121,6 +186,9 @@ public class KeycloakSecurityPolicy implements
AuthorizationPolicy {
* being presented where an access token is expected. Disabled by default
for backward compatibility. Example:
* "Bearer"
*/
+ @Metadata(label = "security",
+ description = "Comma-separated list of the accepted token types
(typ claim), for example Bearer, so that an"
+ + " ID or refresh token is not accepted where an
access token is expected.")
private String expectedTokenTypes;
/**
@@ -128,6 +196,8 @@ public class KeycloakSecurityPolicy implements
AuthorizationPolicy {
* value is rejected, ensuring the token was issued for the expected
client. Disabled by default for backward
* compatibility. Example: "my-client"
*/
+ @Metadata(label = "security",
+ description = "The client a token must have been issued for (azp
claim).")
private String expectedAuthorizedParty;
public KeycloakSecurityPolicy() {
diff --git
a/components/camel-keycloak/src/test/java/org/apache/camel/component/keycloak/security/KeycloakSecurityPolicyBindingTest.java
b/components/camel-keycloak/src/test/java/org/apache/camel/component/keycloak/security/KeycloakSecurityPolicyBindingTest.java
new file mode 100644
index 000000000000..9f944ce61e10
--- /dev/null
+++
b/components/camel-keycloak/src/test/java/org/apache/camel/component/keycloak/security/KeycloakSecurityPolicyBindingTest.java
@@ -0,0 +1,91 @@
+/*
+ * 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.keycloak.security;
+
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+
+import org.apache.camel.CamelContext;
+import org.apache.camel.impl.DefaultCamelContext;
+import org.apache.camel.spi.PropertyConfigurer;
+import org.apache.camel.support.PluginHelper;
+import org.apache.camel.support.PropertyBindingSupport;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+/**
+ * Binds the policy the way a YAML {@code beans:} entry does, where every
value arrives as a string.
+ */
+class KeycloakSecurityPolicyBindingTest {
+
+ @Test
+ void bindsTheOptionsAsAYamlBeanDefinitionGivesThem() throws Exception {
+ try (CamelContext context = new DefaultCamelContext()) {
+ context.start();
+ Map<String, Object> properties = new LinkedHashMap<>();
+ properties.put("serverUrl", "https://keycloak:8443");
+ properties.put("realm", "camel");
+ properties.put("clientId", "camel-client");
+ properties.put("clientSecret", "s3cr3t");
+ // a string, although setRequiredRoles and setRequiredPermissions
also take a List
+ properties.put("requiredRoles", "admin,user");
+ properties.put("requiredPermissions", "read:documents");
+ properties.put("allRolesRequired", "false");
+ properties.put("useTokenIntrospection", "true");
+ properties.put("introspectionCacheTtl", "120");
+ properties.put("allowTokenFromHeader", "false");
+ properties.put("validateIssuer", "true");
+ properties.put("jwksRefreshInterval", "600");
+ properties.put("minJwksRefreshInterval", "30");
+ properties.put("expectedAudience", "camel-client");
+ properties.put("expectedTokenTypes", "Bearer");
+ properties.put("expectedAuthorizedParty", "camel-client");
+
+ KeycloakSecurityPolicy policy = new KeycloakSecurityPolicy();
+ PropertyBindingSupport.setPropertiesOnTarget(context, policy,
properties);
+
+ // the generated configurer binds the options, not reflection: it
is found by the class name
+ PropertyConfigurer configurer =
PluginHelper.getConfigurerResolver(context)
+
.resolvePropertyConfigurer(KeycloakSecurityPolicy.class.getName(), context);
+ assertNotNull(configurer);
+ assertEquals("KeycloakSecurityPolicyConfigurer",
configurer.getClass().getSimpleName());
+
+ assertEquals("https://keycloak:8443", policy.getServerUrl());
+ assertEquals("camel", policy.getRealm());
+ assertEquals("camel-client", policy.getClientId());
+ assertEquals("s3cr3t", policy.getClientSecret());
+ assertEquals(List.of("admin", "user"),
policy.getRequiredRolesAsList());
+ assertEquals(List.of("read:documents"),
policy.getRequiredPermissionsAsList());
+ assertFalse(policy.isAllRolesRequired());
+ assertTrue(policy.isAllPermissionsRequired());
+ assertTrue(policy.isUseTokenIntrospection());
+ assertEquals(120L, policy.getIntrospectionCacheTtl());
+ assertFalse(policy.isAllowTokenFromHeader());
+ assertTrue(policy.isValidateIssuer());
+ assertEquals(600L, policy.getJwksRefreshInterval());
+ assertEquals(30L, policy.getMinJwksRefreshInterval());
+ assertEquals("camel-client", policy.getExpectedAudience());
+ assertEquals("Bearer", policy.getExpectedTokenTypes());
+ assertEquals("camel-client", policy.getExpectedAuthorizedParty());
+ }
+ }
+}
diff --git
a/components/camel-shiro/src/generated/java/org/apache/camel/component/shiro/security/ShiroSecurityPolicyConfigurer.java
b/components/camel-shiro/src/generated/java/org/apache/camel/component/shiro/security/ShiroSecurityPolicyConfigurer.java
new file mode 100644
index 000000000000..2cd5ac7c353b
--- /dev/null
+++
b/components/camel-shiro/src/generated/java/org/apache/camel/component/shiro/security/ShiroSecurityPolicyConfigurer.java
@@ -0,0 +1,108 @@
+/* Generated by camel build tools - do NOT edit this file! */
+package org.apache.camel.component.shiro.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.shiro.security.ShiroSecurityPolicy;
+
+/**
+ * Generated by camel build tools - do NOT edit this file!
+ */
+@Generated("org.apache.camel.maven.packaging.GenerateConfigurerMojo")
+@SuppressWarnings("unchecked")
+public class ShiroSecurityPolicyConfigurer 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.shiro.security.ShiroSecurityPolicy target =
(org.apache.camel.component.shiro.security.ShiroSecurityPolicy) obj;
+ switch (ignoreCase ? name.toLowerCase(Locale.ROOT) : name) {
+ case "allpermissionsrequired":
+ case "allPermissionsRequired":
target.setAllPermissionsRequired(property(camelContext, boolean.class, value));
return true;
+ case "allrolesrequired":
+ case "allRolesRequired":
target.setAllRolesRequired(property(camelContext, boolean.class, value));
return true;
+ case "alwaysreauthenticate":
+ case "alwaysReauthenticate":
target.setAlwaysReauthenticate(property(camelContext, boolean.class, value));
return true;
+ case "base64": target.setBase64(property(camelContext, boolean.class,
value)); return true;
+ case "cipherservice":
+ case "cipherService": target.setCipherService(property(camelContext,
org.apache.shiro.crypto.cipher.CipherService.class, value)); return true;
+ case "passphrase":
+ case "passPhrase": target.setPassPhrase(property(camelContext,
byte[].class, value)); return true;
+ case "permissionslist":
+ case "permissionsList":
target.setPermissionsList(property(camelContext, java.util.List.class, value));
return true;
+ case "roleslist":
+ case "rolesList": target.setRolesList(property(camelContext,
java.util.List.class, value)); return true;
+ case "securitymanager":
+ case "securityManager":
target.setSecurityManager(property(camelContext,
org.apache.shiro.mgt.SecurityManager.class, value)); return true;
+ default: return false;
+ }
+ }
+
+ @Override
+ public Class<?> getOptionType(String name, boolean ignoreCase) {
+ switch (ignoreCase ? name.toLowerCase(Locale.ROOT) : name) {
+ case "allpermissionsrequired":
+ case "allPermissionsRequired": return boolean.class;
+ case "allrolesrequired":
+ case "allRolesRequired": return boolean.class;
+ case "alwaysreauthenticate":
+ case "alwaysReauthenticate": return boolean.class;
+ case "base64": return boolean.class;
+ case "cipherservice":
+ case "cipherService": return
org.apache.shiro.crypto.cipher.CipherService.class;
+ case "passphrase":
+ case "passPhrase": return byte[].class;
+ case "permissionslist":
+ case "permissionsList": return java.util.List.class;
+ case "roleslist":
+ case "rolesList": return java.util.List.class;
+ case "securitymanager":
+ case "securityManager": return
org.apache.shiro.mgt.SecurityManager.class;
+ default: return null;
+ }
+ }
+
+ @Override
+ public Object getOptionValue(Object obj, String name, boolean ignoreCase) {
+ org.apache.camel.component.shiro.security.ShiroSecurityPolicy target =
(org.apache.camel.component.shiro.security.ShiroSecurityPolicy) obj;
+ switch (ignoreCase ? name.toLowerCase(Locale.ROOT) : name) {
+ case "allpermissionsrequired":
+ case "allPermissionsRequired": return
target.isAllPermissionsRequired();
+ case "allrolesrequired":
+ case "allRolesRequired": return target.isAllRolesRequired();
+ case "alwaysreauthenticate":
+ case "alwaysReauthenticate": return target.isAlwaysReauthenticate();
+ case "base64": return target.isBase64();
+ case "cipherservice":
+ case "cipherService": return target.getCipherService();
+ case "passphrase":
+ case "passPhrase": return target.getPassPhrase();
+ case "permissionslist":
+ case "permissionsList": return target.getPermissionsList();
+ case "roleslist":
+ case "rolesList": return target.getRolesList();
+ case "securitymanager":
+ case "securityManager": return target.getSecurityManager();
+ default: return null;
+ }
+ }
+
+ @Override
+ public Object getCollectionValueType(Object target, String name, boolean
ignoreCase) {
+ switch (ignoreCase ? name.toLowerCase(Locale.ROOT) : name) {
+ case "permissionslist":
+ case "permissionsList": return org.apache.shiro.authz.Permission.class;
+ case "roleslist":
+ case "rolesList": return java.lang.String.class;
+ default: return null;
+ }
+ }
+}
+
diff --git
a/components/camel-shiro/src/generated/resources/META-INF/services/org/apache/camel/bean.properties
b/components/camel-shiro/src/generated/resources/META-INF/services/org/apache/camel/bean.properties
new file mode 100644
index 000000000000..6005a5a40a63
--- /dev/null
+++
b/components/camel-shiro/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=ShiroSecurityPolicy
+groupId=org.apache.camel
+artifactId=camel-shiro
+version=4.24.0-SNAPSHOT
+projectName=Camel :: Shiro
+projectDescription=Security using Shiro
diff --git
a/components/camel-shiro/src/generated/resources/META-INF/services/org/apache/camel/bean/ShiroSecurityPolicy.json
b/components/camel-shiro/src/generated/resources/META-INF/services/org/apache/camel/bean/ShiroSecurityPolicy.json
new file mode 100644
index 000000000000..9b3ef1a5f00d
--- /dev/null
+++
b/components/camel-shiro/src/generated/resources/META-INF/services/org/apache/camel/bean/ShiroSecurityPolicy.json
@@ -0,0 +1,16 @@
+{
+ "bean": {
+ "kind": "bean",
+ "name": "ShiroSecurityPolicy",
+ "javaType":
"org.apache.camel.component.shiro.security.ShiroSecurityPolicy",
+ "interfaceType": "org.apache.camel.spi.AuthorizationPolicy",
+ "title": "Shiro Security Policy",
+ "description": "An authorization policy that authenticates the caller with
Apache Shiro, from the encrypted ShiroSecurityToken sent with the message, and
checks its permissions and roles. Create it with the Shiro INI resource as its
constructor argument, or set securityManager.",
+ "deprecated": false,
+ "groupId": "org.apache.camel",
+ "artifactId": "camel-shiro",
+ "version": "4.24.0-SNAPSHOT",
+ "properties": { "cipherService": { "index": 0, "kind": "property",
"displayName": "Cipher Service", "label": "advanced", "required": false,
"type": "object", "javaType": "org.apache.shiro.crypto.cipher.CipherService",
"deprecated": false, "autowired": false, "secret": false, "description": "The
cipher service that decrypts the ShiroSecurityToken sent with the message. AES
by default; Shiro also ships a Blowfish one." }, "passPhrase": { "index": 1,
"kind": "property", "displayName": " [...]
+ }
+}
+
diff --git
a/components/camel-shiro/src/generated/resources/META-INF/services/org/apache/camel/configurer/org.apache.camel.component.shiro.security.ShiroSecurityPolicy
b/components/camel-shiro/src/generated/resources/META-INF/services/org/apache/camel/configurer/org.apache.camel.component.shiro.security.ShiroSecurityPolicy
new file mode 100644
index 000000000000..50ea33f9e232
--- /dev/null
+++
b/components/camel-shiro/src/generated/resources/META-INF/services/org/apache/camel/configurer/org.apache.camel.component.shiro.security.ShiroSecurityPolicy
@@ -0,0 +1,2 @@
+# Generated by camel build tools - do NOT edit this file!
+class=org.apache.camel.component.shiro.security.ShiroSecurityPolicyConfigurer
diff --git a/components/camel-shiro/src/main/docs/shiro.adoc
b/components/camel-shiro/src/main/docs/shiro.adoc
index e57ce7d6e0e1..f617b5afd510 100644
--- a/components/camel-shiro/src/main/docs/shiro.adoc
+++ b/components/camel-shiro/src/main/docs/shiro.adoc
@@ -120,9 +120,10 @@ acquired from the file system, classpath, or URLs when
prefixed with
|`passPhrase` |`An AES 128 based key` |byte[] |A passPhrase to decrypt
ShiroSecurityToken(s) sent along with Message
Exchanges
-|`alwaysReauthenticate` |`true` |boolean |Setting to ensure re-authentication
on every request. If set
-to false, the user is authenticated and locked such than only requests
-from the same user going forward are authenticated.
+|`alwaysReauthenticate` |`true` |boolean |Whether to log the caller out after
every exchange. If set
+to false, the authenticated subject stays bound to the worker thread for
+the next exchange on that thread. The token of every exchange is
+authenticated either way.
|`permissionsList` |`none` |List<Permission> |A List of permissions required
in order for an authenticated user to be
authorized to perform further action i.e., continue further on the route.
@@ -136,7 +137,8 @@ user to be authorized to perform further action i.e.,
continue further on
the route. If no roles list or permissions list (see above) is provided
to the ShiroSecurityPolicy object, then authorization is deemed as not
required. Note that the default is that authorization is granted if any
-of the roles in the list are applicable.
+of the roles in the list are applicable. The roles are checked only when
+no permissions list is set: when both are set, the permissions decide.
|`cipherService` |`AES` |org.apache.shiro.crypto.CipherService |Shiro ships
with AES & Blowfish-based CipherServices. You may use one of
these or pass in your own Cipher implementation
diff --git
a/components/camel-shiro/src/main/java/org/apache/camel/component/shiro/security/ShiroSecurityPolicy.java
b/components/camel-shiro/src/main/java/org/apache/camel/component/shiro/security/ShiroSecurityPolicy.java
index 93e794bac6bf..b0892f3c4bc8 100644
---
a/components/camel-shiro/src/main/java/org/apache/camel/component/shiro/security/ShiroSecurityPolicy.java
+++
b/components/camel-shiro/src/main/java/org/apache/camel/component/shiro/security/ShiroSecurityPolicy.java
@@ -23,6 +23,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.shiro.authz.Permission;
import org.apache.shiro.config.Ini;
import org.apache.shiro.crypto.cipher.AesCipherService;
@@ -32,16 +34,44 @@ import org.apache.shiro.mgt.SecurityManager;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
+@Metadata(label = "bean",
+ description = "An authorization policy that authenticates the caller
with Apache Shiro, from the encrypted"
+ + " ShiroSecurityToken sent with the message, and
checks its permissions and roles. Create it with"
+ + " the Shiro INI resource as its constructor
argument, or set securityManager.",
+ annotations = {
"interfaceName=org.apache.camel.spi.AuthorizationPolicy" })
+@Configurer(metadataOnly = true)
public class ShiroSecurityPolicy implements AuthorizationPolicy {
private static final Logger LOG =
LoggerFactory.getLogger(ShiroSecurityPolicy.class);
+ @Metadata(label = "advanced",
+ description = "The cipher service that decrypts the
ShiroSecurityToken sent with the message. AES by default;"
+ + " Shiro also ships a Blowfish one.")
private CipherService cipherService;
+ @Metadata(label = "security", security = "secret",
+ description = "The key the ShiroSecurityToken sent with the
message is decrypted with.")
private byte[] passPhrase;
+ @Metadata(description = "The Shiro security manager that authenticates and
authorizes the caller. The constructors build"
+ + " it from a Shiro INI resource; set it when the
security manager comes from elsewhere.")
private SecurityManager securityManager;
+ @Metadata(description = "The permissions an authenticated caller needs to
continue on the route. Any one of them is"
+ + " enough unless allPermissionsRequired is set.
When they are set, rolesList is not checked.")
private List<Permission> permissionsList;
+ @Metadata(description = "The roles an authenticated caller needs to
continue on the route, checked only when"
+ + " permissionsList is empty. Any one of them is
enough unless allRolesRequired is set.")
private List<String> rolesList;
+ @Metadata(defaultValue = "true",
+ description = "Whether to log the caller out after every
exchange. When false, the authenticated subject"
+ + " stays bound to the worker thread for the next
exchange on that thread; the token of every"
+ + " exchange is still authenticated.")
private boolean alwaysReauthenticate;
+ @Metadata(defaultValue = "false",
+ description = "Whether the ShiroSecurityToken header is a base64
string rather than the token object, which lets"
+ + " it travel over JMS and other transports.")
private boolean base64;
+ @Metadata(defaultValue = "false",
+ description = "Whether the caller needs all the permissions in
permissionsList, rather than any one of them.")
private boolean allPermissionsRequired;
+ @Metadata(defaultValue = "false",
+ description = "Whether the caller needs all the roles in
rolesList, rather than any one of them.")
private boolean allRolesRequired;
public ShiroSecurityPolicy() {
diff --git
a/components/camel-shiro/src/test/java/org/apache/camel/component/shiro/security/ShiroSecurityPolicyBindingTest.java
b/components/camel-shiro/src/test/java/org/apache/camel/component/shiro/security/ShiroSecurityPolicyBindingTest.java
new file mode 100644
index 000000000000..8cc92c9abcc1
--- /dev/null
+++
b/components/camel-shiro/src/test/java/org/apache/camel/component/shiro/security/ShiroSecurityPolicyBindingTest.java
@@ -0,0 +1,77 @@
+/*
+ * 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.shiro.security;
+
+import java.nio.charset.StandardCharsets;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+
+import org.apache.camel.CamelContext;
+import org.apache.camel.impl.DefaultCamelContext;
+import org.apache.camel.spi.PropertyConfigurer;
+import org.apache.camel.support.PluginHelper;
+import org.apache.camel.support.PropertyBindingSupport;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertArrayEquals;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+/**
+ * Binds the policy the way a YAML {@code beans:} entry does, where every
value arrives as a string.
+ */
+class ShiroSecurityPolicyBindingTest {
+
+ @Test
+ void bindsTheOptionsAsAYamlBeanDefinitionGivesThem() throws Exception {
+ try (CamelContext context = new DefaultCamelContext()) {
+ context.start();
+ Map<String, Object> properties = new LinkedHashMap<>();
+ properties.put("passPhrase", "CamelSecureRoute");
+ properties.put("alwaysReauthenticate", "false");
+ properties.put("base64", "true");
+ properties.put("allRolesRequired", "true");
+ properties.put("rolesList[0]", "admin");
+ properties.put("rolesList[1]", "auditor");
+
+ ShiroSecurityPolicy policy = new
ShiroSecurityPolicy("classpath:securityconfig.ini");
+ PropertyBindingSupport.setPropertiesOnTarget(context, policy,
properties);
+
+ // the generated configurer binds the options, not reflection: it
is found by the class name
+ PropertyConfigurer configurer =
PluginHelper.getConfigurerResolver(context)
+
.resolvePropertyConfigurer(ShiroSecurityPolicy.class.getName(), context);
+ assertNotNull(configurer);
+ assertEquals("ShiroSecurityPolicyConfigurer",
configurer.getClass().getSimpleName());
+
+
assertArrayEquals("CamelSecureRoute".getBytes(StandardCharsets.UTF_8),
policy.getPassPhrase());
+ assertFalse(policy.isAlwaysReauthenticate());
+ assertTrue(policy.isBase64());
+ assertTrue(policy.isAllRolesRequired());
+ assertFalse(policy.isAllPermissionsRequired());
+ assertEquals(List.of("admin", "auditor"), policy.getRolesList());
+ }
+ }
+
+ @Test
+ void reauthenticatesByDefault() {
+ // the catalog documents the default the constructor sets, not the
field's
+ assertTrue(new ShiroSecurityPolicy().isAlwaysReauthenticate());
+ }
+}
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 618571997e25..fd413175aaf4 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
@@ -646,6 +646,8 @@ public final class ArtifactUtils {
"org.apache.camel.support.KeyValueAggregationRepository|org.apache.camel.spi.AggregationRepository|camel-support"),
Map.<String, String> entry("KeyValueIdempotentRepository",
"org.apache.camel.support.KeyValueIdempotentRepository|org.apache.camel.spi.IdempotentRepository|camel-support"),
+ Map.<String, String> entry("KeycloakSecurityPolicy",
+
"org.apache.camel.component.keycloak.security.KeycloakSecurityPolicy|org.apache.camel.spi.AuthorizationPolicy|camel-keycloak"),
Map.<String, String> entry("KubernetesClusterService",
"org.apache.camel.component.kubernetes.cluster.KubernetesClusterService|org.apache.camel.cluster.CamelClusterService|camel-kubernetes"),
Map.<String, String> entry("LoggingHttpActivityListener",
@@ -670,6 +672,8 @@ public final class ArtifactUtils {
"org.apache.camel.component.redis.processor.aggregate.RedisAggregationRepository|org.apache.camel.spi.AggregationRepository|camel-redis"),
Map.<String, String> entry("RedisKeyValueRepository",
"org.apache.camel.component.redis.RedisKeyValueRepository|org.apache.camel.spi.KeyValueRepository|camel-redis"),
+ Map.<String, String> entry("ShiroSecurityPolicy",
+
"org.apache.camel.component.shiro.security.ShiroSecurityPolicy|org.apache.camel.spi.AuthorizationPolicy|camel-shiro"),
Map.<String, String> entry("SimpleScheduledRoutePolicy",
"org.apache.camel.routepolicy.quartz.SimpleScheduledRoutePolicy|org.apache.camel.spi.RoutePolicy|camel-quartz"),
Map.<String, String> entry("SpiffeSSLContextParameters",