This is an automated email from the ASF dual-hosted git repository.
jungm pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/tomee.git
The following commit(s) were added to refs/heads/main by this push:
new ec2cdf0960 TOMEE-4708 - fix race condition in shared EL processor
(#2946)
ec2cdf0960 is described below
commit ec2cdf096011ee72caaa6f0cd029bbdd144f73bf
Author: Markus Jung <[email protected]>
AuthorDate: Wed Sep 23 20:19:57 2026 +0200
TOMEE-4708 - fix race condition in shared EL processor (#2946)
---
.../tomee/security/TomEEELInvocationHandler.java | 28 ++++++--
.../tomee/security/cdi/TomEESecurityExtension.java | 3 +-
...dAuthenticationMechanismDefinitionDelegate.java | 2 +-
.../security/TomEEELInvocationHandlerTest.java | 75 ++++++++++++++++++++++
4 files changed, 98 insertions(+), 10 deletions(-)
diff --git
a/tomee/tomee-security/src/main/java/org/apache/tomee/security/TomEEELInvocationHandler.java
b/tomee/tomee-security/src/main/java/org/apache/tomee/security/TomEEELInvocationHandler.java
index 31fb01de5c..048856702c 100644
---
a/tomee/tomee-security/src/main/java/org/apache/tomee/security/TomEEELInvocationHandler.java
+++
b/tomee/tomee-security/src/main/java/org/apache/tomee/security/TomEEELInvocationHandler.java
@@ -26,6 +26,7 @@ import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.util.HashSet;
import java.util.Set;
+import java.util.function.Supplier;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
@@ -34,15 +35,22 @@ public class TomEEELInvocationHandler implements
InvocationHandler {
private static final Pattern EL_EXPRESSION_PATTERN =
Pattern.compile("[#$]\\{([^{}]+)}");
private final Annotation annotation;
- private final ELProcessor processor;
+
+ // ElProcessor isn't threadsafe, so use a Supplier
+ private final Supplier<ELProcessor> processors;
public TomEEELInvocationHandler(final Annotation annotation, final
ELProcessor processor) {
+ this(annotation, () -> processor);
+ }
+
+ private TomEEELInvocationHandler(final Annotation annotation, final
Supplier<ELProcessor> processors) {
this.annotation = annotation;
- this.processor = processor;
+ this.processors = processors;
}
@Override
public Object invoke(final Object proxy, final Method method, final
Object[] args) throws Throwable {
+ final ELProcessor processor = processors.get();
// todo optimize and cache methods
@@ -56,7 +64,7 @@ public class TomEEELInvocationHandler implements
InvocationHandler {
// Nested annotation with possible EL attributes (e.g.
OpenIdAuthenticationMechanismDefinition -> LogoutDefinition)
if (method.getReturnType().isAnnotation())
{
- return of(((Class<Annotation>) method.getReturnType()),
(Annotation) method.invoke(annotation, args), processor);
+ return of(((Class<Annotation>) method.getReturnType()),
(Annotation) method.invoke(annotation, args), processors);
}
// If return value is not a String or an array of string, there is
another method with "Expression" at the end and a return type String
@@ -200,15 +208,21 @@ public class TomEEELInvocationHandler implements
InvocationHandler {
}
public static <T extends Annotation> T of(final Class<T> annotationClass,
final T annotation, final BeanManager beanManager) {
- final ELProcessor elProcessor = new ELProcessor();
- elProcessor.getELManager().addELResolver(beanManager.getELResolver());
- return of(annotationClass, annotation, elProcessor);
+ return of(annotationClass, annotation, () -> {
+ final ELProcessor elProcessor = new ELProcessor();
+
elProcessor.getELManager().addELResolver(beanManager.getELResolver());
+ return elProcessor;
+ });
}
public static <T extends Annotation> T of(final Class<T> annotationClass,
final T annotation, final ELProcessor elProcessor) {
+ return of(annotationClass, annotation, () -> elProcessor);
+ }
+
+ private static <T extends Annotation> T of(final Class<T> annotationClass,
final T annotation, final Supplier<ELProcessor> processors) {
return (T)
Proxy.newProxyInstance(annotation.getClass().getClassLoader(),
new Class[]{annotationClass},
- new
TomEEELInvocationHandler(annotation, elProcessor));
+ new
TomEEELInvocationHandler(annotation, processors));
}
}
diff --git
a/tomee/tomee-security/src/main/java/org/apache/tomee/security/cdi/TomEESecurityExtension.java
b/tomee/tomee-security/src/main/java/org/apache/tomee/security/cdi/TomEESecurityExtension.java
index ab63f96e90..fca2f660b8 100644
---
a/tomee/tomee-security/src/main/java/org/apache/tomee/security/cdi/TomEESecurityExtension.java
+++
b/tomee/tomee-security/src/main/java/org/apache/tomee/security/cdi/TomEESecurityExtension.java
@@ -16,7 +16,6 @@
*/
package org.apache.tomee.security.cdi;
-import jakarta.enterprise.context.RequestScoped;
import jakarta.enterprise.inject.spi.Bean;
import
jakarta.security.enterprise.authentication.mechanism.http.OpenIdAuthenticationMechanismDefinition;
import org.apache.tomee.security.TomEEELInvocationHandler;
@@ -403,7 +402,7 @@ public class TomEESecurityExtension implements Extension {
.beanClass(OpenIdAuthenticationMechanismDefinition.class)
.types(Object.class,
OpenIdAuthenticationMechanismDefinition.class)
.qualifiers(Default.Literal.INSTANCE, Any.Literal.INSTANCE)
- .scope(RequestScoped.class)
+ .scope(ApplicationScoped.class)
.createWith(creationalContext ->
createOpenIdAuthenticationMechanismDefinition(defaultOidcDefinition,
beanManager));
afterBeanDiscovery.addBean()
diff --git
a/tomee/tomee-security/src/main/java/org/apache/tomee/security/http/openid/OpenIdAuthenticationMechanismDefinitionDelegate.java
b/tomee/tomee-security/src/main/java/org/apache/tomee/security/http/openid/OpenIdAuthenticationMechanismDefinitionDelegate.java
index c4d9bc7203..5fabe7f164 100644
---
a/tomee/tomee-security/src/main/java/org/apache/tomee/security/http/openid/OpenIdAuthenticationMechanismDefinitionDelegate.java
+++
b/tomee/tomee-security/src/main/java/org/apache/tomee/security/http/openid/OpenIdAuthenticationMechanismDefinitionDelegate.java
@@ -210,7 +210,7 @@ public class
OpenIdAuthenticationMechanismDefinitionDelegate implements OpenIdAu
private static final String WELL_KNOWN_CONFIGURATION_PATH =
"/.well-known/openid-configuration";
- private OpenIdProviderMetadata cached = null;
+ private volatile OpenIdProviderMetadata cached = null;
public
AutoResolvingProviderMetadata(OpenIdAuthenticationMechanismDefinition delegate)
{
super(delegate);
diff --git
a/tomee/tomee-security/src/test/java/org/apache/tomee/security/TomEEELInvocationHandlerTest.java
b/tomee/tomee-security/src/test/java/org/apache/tomee/security/TomEEELInvocationHandlerTest.java
index f4948a0819..cc5f3c10eb 100644
---
a/tomee/tomee-security/src/test/java/org/apache/tomee/security/TomEEELInvocationHandlerTest.java
+++
b/tomee/tomee-security/src/test/java/org/apache/tomee/security/TomEEELInvocationHandlerTest.java
@@ -18,6 +18,7 @@ package org.apache.tomee.security;
import jakarta.el.ELProcessor;
import jakarta.el.ELResolver;
+import jakarta.enterprise.context.ApplicationScoped;
import jakarta.enterprise.inject.Vetoed;
import jakarta.enterprise.inject.spi.BeanManager;
import jakarta.enterprise.inject.spi.CDI;
@@ -30,8 +31,15 @@ import
jakarta.security.enterprise.identitystore.PasswordHash;
import org.junit.Assert;
import org.junit.Test;
+import java.util.List;
import java.util.Map;
import java.util.Set;
+import java.util.concurrent.CopyOnWriteArrayList;
+import java.util.concurrent.CyclicBarrier;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.Future;
+import java.util.concurrent.TimeUnit;
import static java.util.Arrays.stream;
import static java.util.stream.Collectors.toMap;
@@ -150,6 +158,51 @@ public class TomEEELInvocationHandlerTest extends
AbstractTomEESecurityTest {
Assert.assertThrows(IllegalArgumentException.class, () ->
proxiedAnnotation.clientSecret());
}
+ // See TOMEE-4708
+ @Test
+ public void sharedProxyIsSafeUnderConcurrentEvaluation() throws Exception {
+ final OpenIdAuthenticationMechanismDefinition annotation =
+
ConfigDrivenDefinition.class.getAnnotation(OpenIdAuthenticationMechanismDefinition.class);
+
+ // the BeanManager variant is what TomEESecurityExtension uses for
application-wide definitions
+ final OpenIdAuthenticationMechanismDefinition proxiedAnnotation =
TomEEELInvocationHandler.of(
+ OpenIdAuthenticationMechanismDefinition.class, annotation,
bm());
+
+ final int threads = 16;
+ final int iterations = 2000;
+ final CyclicBarrier barrier = new CyclicBarrier(threads);
+ final List<Throwable> failures = new CopyOnWriteArrayList<>();
+ final ExecutorService executor = Executors.newFixedThreadPool(threads);
+ try {
+ final List<Future<?>> futures = new java.util.ArrayList<>();
+ for (int t = 0; t < threads; t++) {
+ futures.add(executor.submit(() -> {
+ try {
+ barrier.await();
+ for (int i = 0; i < iterations; i++) {
+
Assert.assertFalse(proxiedAnnotation.tokenAutoRefresh());
+ Assert.assertEquals(10000,
proxiedAnnotation.tokenMinValidity());
+ }
+ } catch (final Throwable e) {
+ failures.add(e);
+ }
+ }));
+ }
+ for (final Future<?> future : futures) {
+ future.get(2, TimeUnit.MINUTES);
+ }
+ } finally {
+ executor.shutdownNow();
+ }
+
+ if (!failures.isEmpty()) {
+ final AssertionError error = new AssertionError(failures.size() +
" of " + threads
+ + " threads failed, first: " + failures.get(0));
+ failures.forEach(error::addSuppressed);
+ throw error;
+ }
+ }
+
private BeanManager bm() {
return CDI.current().getBeanManager();
}
@@ -227,4 +280,26 @@ public class TomEEELInvocationHandlerTest extends
AbstractTomEESecurityTest {
}
}
+
+ @OpenIdAuthenticationMechanismDefinition(
+ providerURI = "https://server.example.com",
+ clientId = "client",
+ tokenAutoRefreshExpression =
"#{elHandlerTestConfig.isTrue('tokenAutoRefresh', false)}",
+ tokenMinValidityExpression =
"#{elHandlerTestConfig.getInt('tokenMinValidity', 10000)}")
+ @Vetoed // keep the extension from registering this as a real OpenID
mechanism
+ public static class ConfigDrivenDefinition {
+ }
+
+ // discovered by CDI so the BeanManager's ELResolver can resolve it, like
an application's config bean
+ @Named("elHandlerTestConfig")
+ @ApplicationScoped
+ public static class Config {
+ public boolean isTrue(final String key, final boolean defaultValue) {
+ return defaultValue;
+ }
+
+ public int getInt(final String key, final int defaultValue) {
+ return defaultValue;
+ }
+ }
}