This is an automated email from the ASF dual-hosted git repository.
davsclaus pushed a commit to branch camel-4.18.x
in repository https://gitbox.apache.org/repos/asf/camel.git
The following commit(s) were added to refs/heads/camel-4.18.x by this push:
new 63cb8ff1a39d [backport camel-4.18.x] CAMEL-26323: camel-shiro -
ShiroSecurityProcessor should authenticate against policy security manager
(#28569)
63cb8ff1a39d is described below
commit 63cb8ff1a39d6251805a6fa1da1b367c728393dc
Author: Guillaume Nodet <[email protected]>
AuthorDate: Thu Oct 29 07:18:48 2026 +0100
[backport camel-4.18.x] CAMEL-26323: camel-shiro - ShiroSecurityProcessor
should authenticate against policy security manager (#28569)
---
.../shiro/security/ShiroSecurityPolicy.java | 3 -
.../shiro/security/ShiroSecurityProcessor.java | 71 ++++-
.../security/ShiroMultiplePolicyIsolationTest.java | 303 +++++++++++++++++++++
.../ROOT/pages/camel-4x-upgrade-guide-4_18.adoc | 18 ++
4 files changed, 388 insertions(+), 7 deletions(-)
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 423c304c1f1d..93e794bac6bf 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,7 +23,6 @@ import org.apache.camel.NamedNode;
import org.apache.camel.Processor;
import org.apache.camel.Route;
import org.apache.camel.spi.AuthorizationPolicy;
-import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authz.Permission;
import org.apache.shiro.config.Ini;
import org.apache.shiro.crypto.cipher.AesCipherService;
@@ -58,13 +57,11 @@ public class ShiroSecurityPolicy implements
AuthorizationPolicy {
Ini ini = new Ini();
ini.loadFromPath(iniResourcePath);
securityManager = new BasicIniEnvironment(ini).getSecurityManager();
- SecurityUtils.setSecurityManager(securityManager);
}
public ShiroSecurityPolicy(Ini ini) {
this();
securityManager = new BasicIniEnvironment(ini).getSecurityManager();
- SecurityUtils.setSecurityManager(securityManager);
}
public ShiroSecurityPolicy(String iniResourcePath, byte[] passPhrase) {
diff --git
a/components/camel-shiro/src/main/java/org/apache/camel/component/shiro/security/ShiroSecurityProcessor.java
b/components/camel-shiro/src/main/java/org/apache/camel/component/shiro/security/ShiroSecurityProcessor.java
index 98f18bec1221..f620c9d0433c 100644
---
a/components/camel-shiro/src/main/java/org/apache/camel/component/shiro/security/ShiroSecurityProcessor.java
+++
b/components/camel-shiro/src/main/java/org/apache/camel/component/shiro/security/ShiroSecurityProcessor.java
@@ -32,6 +32,9 @@ import org.apache.shiro.crypto.cipher.ByteSourceBroker;
import org.apache.shiro.lang.codec.Base64;
import org.apache.shiro.lang.util.ByteSource;
import org.apache.shiro.subject.Subject;
+import org.apache.shiro.subject.support.DelegatingSubject;
+import org.apache.shiro.subject.support.SubjectThreadState;
+import org.apache.shiro.util.ThreadContext;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -42,6 +45,9 @@ import org.slf4j.LoggerFactory;
public class ShiroSecurityProcessor extends DelegateAsyncProcessor {
private static final Logger LOG =
LoggerFactory.getLogger(ShiroSecurityProcessor.class);
+ private static final String MANAGED_SUBJECT =
ShiroSecurityProcessor.class.getName() + ".MANAGED_SUBJECT";
+ private static final String MANAGED_EXCHANGE =
ShiroSecurityProcessor.class.getName() + ".MANAGED_EXCHANGE";
+
private final ShiroSecurityPolicy policy;
public ShiroSecurityProcessor(Processor processor, ShiroSecurityPolicy
policy) {
@@ -114,18 +120,75 @@ public class ShiroSecurityProcessor extends
DelegateAsyncProcessor {
ByteSourceBroker decryptedToken =
policy.getCipherService().decrypt(encryptedToken.getBytes(),
policy.getPassPhrase());
ShiroSecurityToken securityToken =
ShiroSecurityHelper.deserialize(decryptedToken.getClonedBytes());
- Subject currentUser = SecurityUtils.getSubject();
+ SubjectThreadState threadState = null;
+ Subject currentUser = null;
+ boolean success = false;
- // Authenticate user if not authenticated
try {
+ Subject threadSubject = ThreadContext.getSubject();
+ if (!policy.isAlwaysReauthenticate()
+ && threadSubject instanceof DelegatingSubject
delegatingSubject
+ && policy.getSecurityManager() != null
+ && delegatingSubject.getSecurityManager() ==
policy.getSecurityManager()) {
+ currentUser = threadSubject;
+ } else {
+ cleanUpManagedSubject(exchange);
+
+ if (policy.getSecurityManager() != null) {
+ currentUser = new
Subject.Builder(policy.getSecurityManager()).buildSubject();
+ threadState = new SubjectThreadState(currentUser);
+ threadState.bind();
+ } else {
+ currentUser = SecurityUtils.getSubject();
+ }
+ }
+
+ // Authenticate user if not authenticated
authenticateUser(currentUser, securityToken);
// Test whether user's role is authorized to perform functions in
the permissions list
authorizeUser(currentUser, exchange);
+ success = true;
} finally {
- if (policy.isAlwaysReauthenticate()) {
- currentUser.logout();
+ if (policy.isAlwaysReauthenticate() || !success) {
+ try {
+ if (currentUser != null) {
+ currentUser.logout();
+ }
+ } finally {
+ ThreadContext.remove(MANAGED_SUBJECT);
+ ThreadContext.remove(MANAGED_EXCHANGE);
+ if (threadState != null) {
+ threadState.restore();
+ } else {
+ ThreadContext.unbindSubject();
+ ThreadContext.unbindSecurityManager();
+ }
+ }
+ } else {
+ // When alwaysReauthenticate=false and the exchange succeeds,
the subject remains bound for subsequent
+ // exchanges on this thread. cleanUpManagedSubject will log it
out when a different exchange arrives.
+ ThreadContext.put(MANAGED_SUBJECT, currentUser);
+ ThreadContext.put(MANAGED_EXCHANGE, exchange);
+ }
+ }
+ }
+
+ private void cleanUpManagedSubject(Exchange currentExchange) {
+ Object managedExchange = ThreadContext.get(MANAGED_EXCHANGE);
+ // Use reference inequality (!=) to skip cleanup during synchronous
nested routing on the same exchange
+ // (such as direct: routes), avoiding premature logout of the outer
subject.
+ if (managedExchange != null && managedExchange != currentExchange) {
+ Object managed = ThreadContext.get(MANAGED_SUBJECT);
+ if (managed instanceof Subject oldSubject) {
+ try {
+ oldSubject.logout();
+ } catch (Exception e) {
+ LOG.debug("Error logging out previous managed subject", e);
+ }
}
+ ThreadContext.remove(MANAGED_SUBJECT);
+ ThreadContext.remove(MANAGED_EXCHANGE);
}
}
diff --git
a/components/camel-shiro/src/test/java/org/apache/camel/component/shiro/security/ShiroMultiplePolicyIsolationTest.java
b/components/camel-shiro/src/test/java/org/apache/camel/component/shiro/security/ShiroMultiplePolicyIsolationTest.java
new file mode 100644
index 000000000000..3dd1d1923ee7
--- /dev/null
+++
b/components/camel-shiro/src/test/java/org/apache/camel/component/shiro/security/ShiroMultiplePolicyIsolationTest.java
@@ -0,0 +1,303 @@
+/*
+ * 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.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import org.apache.camel.CamelAuthorizationException;
+import org.apache.camel.EndpointInject;
+import org.apache.camel.builder.RouteBuilder;
+import org.apache.camel.component.mock.MockEndpoint;
+import org.apache.camel.test.junit5.CamelTestSupport;
+import org.apache.shiro.SecurityUtils;
+import org.apache.shiro.authc.AuthenticationException;
+import org.apache.shiro.config.Ini;
+import org.apache.shiro.subject.Subject;
+import org.apache.shiro.util.ThreadContext;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+public class ShiroMultiplePolicyIsolationTest extends CamelTestSupport {
+
+ private static final byte[] KEY_A =
"0123456789abcdef".getBytes(StandardCharsets.US_ASCII);
+ private static final byte[] KEY_B =
"fedcba9876543210".getBytes(StandardCharsets.US_ASCII);
+
+ @EndpointInject("mock:successA")
+ protected MockEndpoint successEndpointA;
+
+ @EndpointInject("mock:failureA")
+ protected MockEndpoint failureEndpointA;
+
+ @EndpointInject("mock:successB")
+ protected MockEndpoint successEndpointB;
+
+ @EndpointInject("mock:failureB")
+ protected MockEndpoint failureEndpointB;
+
+ private ShiroSecurityPolicy policyA;
+ private ShiroSecurityPolicy policyB;
+ private ShiroSecurityPolicy policyNoReauthA;
+ private ShiroSecurityPolicy policyNoReauthB;
+
+ @EndpointInject("mock:successNoReauthA")
+ protected MockEndpoint successEndpointNoReauthA;
+
+ @EndpointInject("mock:failureNoReauthA")
+ protected MockEndpoint failureEndpointNoReauthA;
+
+ @EndpointInject("mock:successNoReauthB")
+ protected MockEndpoint successEndpointNoReauthB;
+
+ @EndpointInject("mock:failureNoReauthB")
+ protected MockEndpoint failureEndpointNoReauthB;
+
+ @EndpointInject("mock:successAuthB")
+ protected MockEndpoint successEndpointAuthB;
+
+ @EndpointInject("mock:failureAuthB")
+ protected MockEndpoint failureEndpointAuthB;
+
+ @EndpointInject("mock:nestedSuccess")
+ protected MockEndpoint nestedSuccessEndpoint;
+
+ @EndpointInject("mock:innerSuccess")
+ protected MockEndpoint innerSuccessEndpoint;
+
+ private ShiroSecurityPolicy policyAuthB;
+
+ @AfterEach
+ public void tearDownThreadContext() {
+ ThreadContext.remove();
+ }
+
+ @Test
+ public void testPolicyIsolationBetweenDifferentRealms() throws Exception {
+ successEndpointA.expectedMessageCount(1);
+ failureEndpointA.expectedMessageCount(1);
+ successEndpointB.expectedMessageCount(1);
+ failureEndpointB.expectedMessageCount(1);
+
+ // 1. Valid user for policy A should succeed on route A
+ Map<String, Object> headersAlice = new HashMap<>();
+ headersAlice.put(ShiroSecurityConstants.SHIRO_SECURITY_USERNAME,
"alice");
+ headersAlice.put(ShiroSecurityConstants.SHIRO_SECURITY_PASSWORD,
"passwordA");
+ template.sendBodyAndHeaders("direct:routeA", "payloadA", headersAlice);
+
+ // 2. Valid user for policy B should fail on route A
+ Map<String, Object> headersBobOnA = new HashMap<>();
+ headersBobOnA.put(ShiroSecurityConstants.SHIRO_SECURITY_USERNAME,
"bob");
+ headersBobOnA.put(ShiroSecurityConstants.SHIRO_SECURITY_PASSWORD,
"passwordB");
+ template.sendBodyAndHeaders("direct:routeA", "payloadA",
headersBobOnA);
+
+ // 3. Valid user for policy B should succeed on route B
+ Map<String, Object> headersBobOnB = new HashMap<>();
+ headersBobOnB.put(ShiroSecurityConstants.SHIRO_SECURITY_USERNAME,
"bob");
+ headersBobOnB.put(ShiroSecurityConstants.SHIRO_SECURITY_PASSWORD,
"passwordB");
+ template.sendBodyAndHeaders("direct:routeB", "payloadB",
headersBobOnB);
+
+ // 4. User for policy A should fail on route B
+ Map<String, Object> headersAliceOnB = new HashMap<>();
+ headersAliceOnB.put(ShiroSecurityConstants.SHIRO_SECURITY_USERNAME,
"alice");
+ headersAliceOnB.put(ShiroSecurityConstants.SHIRO_SECURITY_PASSWORD,
"passwordA");
+ template.sendBodyAndHeaders("direct:routeB", "payloadB",
headersAliceOnB);
+
+ successEndpointA.assertIsSatisfied();
+ failureEndpointA.assertIsSatisfied();
+ successEndpointB.assertIsSatisfied();
+ failureEndpointB.assertIsSatisfied();
+ }
+
+ @Test
+ public void testPolicyIsolationWithReauthenticateFalse() throws Exception {
+ successEndpointNoReauthA.expectedMessageCount(1);
+ failureEndpointNoReauthA.expectedMessageCount(1);
+ successEndpointNoReauthB.expectedMessageCount(1);
+ failureEndpointNoReauthB.expectedMessageCount(1);
+
+ // 1. Valid user for policy A on route A
+ Map<String, Object> headersAlice = new HashMap<>();
+ headersAlice.put(ShiroSecurityConstants.SHIRO_SECURITY_USERNAME,
"alice");
+ headersAlice.put(ShiroSecurityConstants.SHIRO_SECURITY_PASSWORD,
"passwordA");
+ template.sendBodyAndHeaders("direct:routeNoReauthA", "payloadA",
headersAlice);
+
+ // 2. Valid user for policy B on route B
+ Map<String, Object> headersBob = new HashMap<>();
+ headersBob.put(ShiroSecurityConstants.SHIRO_SECURITY_USERNAME, "bob");
+ headersBob.put(ShiroSecurityConstants.SHIRO_SECURITY_PASSWORD,
"passwordB");
+ template.sendBodyAndHeaders("direct:routeNoReauthB", "payloadB",
headersBob);
+
+ // 3. User for policy B should fail on route A even after B succeeded
+ Map<String, Object> headersBobOnA = new HashMap<>();
+ headersBobOnA.put(ShiroSecurityConstants.SHIRO_SECURITY_USERNAME,
"bob");
+ headersBobOnA.put(ShiroSecurityConstants.SHIRO_SECURITY_PASSWORD,
"passwordB");
+ template.sendBodyAndHeaders("direct:routeNoReauthA", "payloadA",
headersBobOnA);
+
+ // 4. User for policy A should fail on route B
+ Map<String, Object> headersAliceOnB = new HashMap<>();
+ headersAliceOnB.put(ShiroSecurityConstants.SHIRO_SECURITY_USERNAME,
"alice");
+ headersAliceOnB.put(ShiroSecurityConstants.SHIRO_SECURITY_PASSWORD,
"passwordA");
+ template.sendBodyAndHeaders("direct:routeNoReauthB", "payloadB",
headersAliceOnB);
+
+ successEndpointNoReauthA.assertIsSatisfied();
+ failureEndpointNoReauthA.assertIsSatisfied();
+ successEndpointNoReauthB.assertIsSatisfied();
+ failureEndpointNoReauthB.assertIsSatisfied();
+ }
+
+ @Test
+ public void testThreadContextCleanupOnFailureWithReauthenticateFalse()
throws Exception {
+ failureEndpointNoReauthA.expectedMessageCount(1);
+
+ Map<String, Object> invalidHeaders = new HashMap<>();
+ invalidHeaders.put(ShiroSecurityConstants.SHIRO_SECURITY_USERNAME,
"alice");
+ invalidHeaders.put(ShiroSecurityConstants.SHIRO_SECURITY_PASSWORD,
"wrongpassword");
+ template.sendBodyAndHeaders("direct:routeNoReauthA", "payloadA",
invalidHeaders);
+
+ failureEndpointNoReauthA.assertIsSatisfied();
+ assertNull(ThreadContext.getSubject(), "ThreadContext subject should
be cleaned up on failure");
+ assertNull(ThreadContext.getSecurityManager(), "ThreadContext security
manager should be cleaned up on failure");
+ }
+
+ @Test
+ public void testRoleAuthorizationIsolation() throws Exception {
+ successEndpointAuthB.expectedMessageCount(1);
+ failureEndpointAuthB.expectedMessageCount(1);
+
+ // 1. Bob has roleB in realm B, passes authorization
+ Map<String, Object> headersBob = new HashMap<>();
+ headersBob.put(ShiroSecurityConstants.SHIRO_SECURITY_USERNAME, "bob");
+ headersBob.put(ShiroSecurityConstants.SHIRO_SECURITY_PASSWORD,
"passwordB");
+ template.sendBodyAndHeaders("direct:routeAuthB", "payload",
headersBob);
+
+ // 2. Charlie is in realm B with roleA (from realm A's permission
model), fails authorization on policy B
+ Map<String, Object> headersCharlie = new HashMap<>();
+ headersCharlie.put(ShiroSecurityConstants.SHIRO_SECURITY_USERNAME,
"charlie");
+ headersCharlie.put(ShiroSecurityConstants.SHIRO_SECURITY_PASSWORD,
"passwordB");
+ template.sendBodyAndHeaders("direct:routeAuthB", "payload",
headersCharlie);
+
+ successEndpointAuthB.assertIsSatisfied();
+ failureEndpointAuthB.assertIsSatisfied();
+ }
+
+ @Test
+ public void testNestedRouteSubjectThreadStateRestoration() throws
Exception {
+ nestedSuccessEndpoint.expectedMessageCount(1);
+ innerSuccessEndpoint.expectedMessageCount(1);
+
+ Map<String, Object> headersAlice = new HashMap<>();
+ headersAlice.put(ShiroSecurityConstants.SHIRO_SECURITY_USERNAME,
"alice");
+ headersAlice.put(ShiroSecurityConstants.SHIRO_SECURITY_PASSWORD,
"passwordA");
+ template.sendBodyAndHeaders("direct:outerRoute", "payload",
headersAlice);
+
+ nestedSuccessEndpoint.assertIsSatisfied();
+ innerSuccessEndpoint.assertIsSatisfied();
+ }
+
+ @Override
+ protected RouteBuilder createRouteBuilder() {
+ Ini iniA = new Ini();
+ iniA.setSectionProperty("users", "alice", "passwordA, roleA");
+ policyA = new ShiroSecurityPolicy(iniA, KEY_A);
+ policyNoReauthA = new ShiroSecurityPolicy(iniA, KEY_A, false);
+
+ Ini iniB = new Ini();
+ iniB.setSectionProperty("users", "bob", "passwordB, roleB");
+ iniB.setSectionProperty("users", "charlie", "passwordB, roleA");
+ policyB = new ShiroSecurityPolicy(iniB, KEY_B);
+ policyNoReauthB = new ShiroSecurityPolicy(iniB, KEY_B, false);
+
+ policyAuthB = new ShiroSecurityPolicy(iniB, KEY_B);
+ policyAuthB.setRolesList(List.of("roleB"));
+
+ return new RouteBuilder() {
+ @Override
+ public void configure() {
+ from("direct:routeA")
+ .onException(AuthenticationException.class)
+ .handled(true)
+ .to("mock:failureA")
+ .end()
+ .policy(policyA)
+ .to("mock:successA");
+
+ from("direct:routeB")
+ .onException(AuthenticationException.class)
+ .handled(true)
+ .to("mock:failureB")
+ .end()
+ .policy(policyB)
+ .to("mock:successB");
+
+ from("direct:routeNoReauthA")
+ .onException(AuthenticationException.class)
+ .handled(true)
+ .to("mock:failureNoReauthA")
+ .end()
+ .policy(policyNoReauthA)
+ .to("mock:successNoReauthA");
+
+ from("direct:routeNoReauthB")
+ .onException(AuthenticationException.class)
+ .handled(true)
+ .to("mock:failureNoReauthB")
+ .end()
+ .policy(policyNoReauthB)
+ .to("mock:successNoReauthB");
+
+ from("direct:routeAuthB")
+ .onException(CamelAuthorizationException.class)
+ .handled(true)
+ .to("mock:failureAuthB")
+ .end()
+ .policy(policyAuthB)
+ .to("mock:successAuthB");
+
+ from("direct:outerRoute")
+ .policy(policyNoReauthA)
+ .process(exchange -> {
+ Subject subject = SecurityUtils.getSubject();
+ assertEquals("alice", subject.getPrincipal());
+ assertTrue(subject.isAuthenticated());
+ })
+ .to("direct:innerRoute")
+ .process(exchange -> {
+ Subject restored = SecurityUtils.getSubject();
+ assertEquals("alice", restored.getPrincipal(),
+ "Outer subject should be restored after
calling inner route");
+ assertTrue(restored.isAuthenticated(),
+ "Restored subject must still be
authenticated");
+ })
+ .to("mock:nestedSuccess");
+
+ from("direct:innerRoute")
+ .process(exchange -> {
+
exchange.getIn().setHeader(ShiroSecurityConstants.SHIRO_SECURITY_USERNAME,
"bob");
+
exchange.getIn().setHeader(ShiroSecurityConstants.SHIRO_SECURITY_PASSWORD,
"passwordB");
+ })
+ .policy(policyB)
+ .to("mock:innerSuccess");
+ }
+ };
+ }
+}
diff --git
a/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_18.adoc
b/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_18.adoc
index 216fe257f20e..07f1886b863f 100644
--- a/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_18.adoc
+++ b/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_18.adoc
@@ -80,6 +80,24 @@ first consumer's settings. Such a consumer now fails to
start with an `IllegalAr
port. Give every consumer on a host and port the same `SSLContextParameters`
instance (for example the same bean
reference, or the global SSL context parameters), or move consumers that need
other settings to another port.
+=== camel-shiro - ShiroSecurityPolicy scopes SecurityManager to policy instance
+
+The `ShiroSecurityPolicy` constructors that load configuration from an INI
file or object no longer set the resulting
+`SecurityManager` as Shiro's global security manager via
`SecurityUtils.setSecurityManager(...)`. `ShiroSecurityProcessor`
+now creates and authenticates the caller identity (principal) using the route
policy's `SecurityManager` directly.
+Applications configuring multiple `ShiroSecurityPolicy` instances in the same
JVM now have their realms evaluated
+independently per policy.
+
+Code that accesses the ambient security context or calls
`SecurityUtils.getSecurityManager()` outside the policy and
+previously relied on the policy constructor setting the global security
manager will now receive
+`UnavailableSecurityManagerException`. Such applications should explicitly
configure
+`SecurityUtils.setSecurityManager(policy.getSecurityManager())` at startup.
+
+Additionally:
+- A `ShiroSecurityPolicy` created with the no-arg constructor must be
configured with `setSecurityManager(...)`.
+- When `alwaysReauthenticate=true` (the default), no caller identity stays
bound to the processing thread after the
+ policy check completes.
+
== Upgrading from 4.18.4 to 4.18.5
=== camel-docling - output path handling