This is an automated email from the ASF dual-hosted git repository. gnodet pushed a commit to branch backport/28522-to-camel-4.18.x in repository https://gitbox.apache.org/repos/asf/camel.git
commit 79f2d7437c14d1b9aa1e83f99fa37bb01f25d4e4 Author: Guillaume Nodet <[email protected]> AuthorDate: Tue Oct 27 13:42:59 2026 +0000 [backport camel-4.18.x] CAMEL-26323: camel-shiro - ShiroSecurityProcessor should authenticate against policy security manager --- .../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..c155be582737 --- /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.junit6.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 4cb7a34343d5..768b96916558 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 @@ -71,6 +71,24 @@ Use `0.0.0.0` as the host to accept connections on all network interfaces. `AvroRpcHttpServerFactory` has a new method `create(SpecificResponder, String, int)` that receives the host. A custom factory should implement it. +=== 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
