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 00a32e2ed15a CAMEL-26828: Close standalone SPIFFE SSL context sources 
explicitly (#29632)
00a32e2ed15a is described below

commit 00a32e2ed15a114071f5ca9777c364beef23e5ca
Author: Andrea Cosentino <[email protected]>
AuthorDate: Wed Nov 11 19:51:00 2026 +0100

    CAMEL-26828: Close standalone SPIFFE SSL context sources explicitly (#29632)
    
    camel-spiffe: a standalone SSL context closes its X.509 source explicitly, 
defining who owns the cleanup.
    
    Co-authored-by: Codex <[email protected]>
    Signed-off-by: Andrea Cosentino <[email protected]>
---
 .../camel/catalog/docs/spiffe-component.adoc       |   6 +
 .../src/main/docs/spiffe-component.adoc            |   6 +
 .../spiffe/SpiffeSSLContextParameters.java         |  44 +++++-
 .../SpiffeSSLContextParametersLifecycleTest.java   | 172 +++++++++++++++++++++
 4 files changed, 220 insertions(+), 8 deletions(-)

diff --git 
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/spiffe-component.adoc
 
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/spiffe-component.adoc
index 52027bb966b7..a7ad55e2bb67 100644
--- 
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/spiffe-component.adoc
+++ 
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/spiffe-component.adoc
@@ -138,6 +138,12 @@ Because it extends `SSLContextParameters`, the inherited 
configuration is still
 `CamelContext` shutdown, and the cached context is invalidated at the same 
time so a restarted context rebuilds a
 fresh source.
 
+`SpiffeSSLContextParameters` also implements `AutoCloseable` for standalone 
use. When neither the
+`createSSLContext` argument nor the parameters instance supplies a 
`CamelContext`, use try-with-resources or call
+`close()` explicitly to release the Workload API watcher. Closing invalidates 
the cached context and is idempotent;
+a subsequent `createSSLContext` call creates a new source. Stop using existing 
SSL contexts and TLS connections
+before closing their parameters.
+
 [tabs]
 ====
 Java::
diff --git a/components/camel-spiffe/src/main/docs/spiffe-component.adoc 
b/components/camel-spiffe/src/main/docs/spiffe-component.adoc
index 52027bb966b7..a7ad55e2bb67 100644
--- a/components/camel-spiffe/src/main/docs/spiffe-component.adoc
+++ b/components/camel-spiffe/src/main/docs/spiffe-component.adoc
@@ -138,6 +138,12 @@ Because it extends `SSLContextParameters`, the inherited 
configuration is still
 `CamelContext` shutdown, and the cached context is invalidated at the same 
time so a restarted context rebuilds a
 fresh source.
 
+`SpiffeSSLContextParameters` also implements `AutoCloseable` for standalone 
use. When neither the
+`createSSLContext` argument nor the parameters instance supplies a 
`CamelContext`, use try-with-resources or call
+`close()` explicitly to release the Workload API watcher. Closing invalidates 
the cached context and is idempotent;
+a subsequent `createSSLContext` call creates a new source. Stop using existing 
SSL contexts and TLS connections
+before closing their parameters.
+
 [tabs]
 ====
 Java::
diff --git 
a/components/camel-spiffe/src/main/java/org/apache/camel/component/spiffe/SpiffeSSLContextParameters.java
 
b/components/camel-spiffe/src/main/java/org/apache/camel/component/spiffe/SpiffeSSLContextParameters.java
index 873203a951ec..ff335444ff5a 100644
--- 
a/components/camel-spiffe/src/main/java/org/apache/camel/component/spiffe/SpiffeSSLContextParameters.java
+++ 
b/components/camel-spiffe/src/main/java/org/apache/camel/component/spiffe/SpiffeSSLContextParameters.java
@@ -56,7 +56,8 @@ import org.apache.camel.util.ObjectHelper;
  * {@link #getSecureSocketProtocol()} (default {@code TLSv1.3}).
  * <p>
  * The underlying {@code X509Source} is created lazily on the first call, 
closed when the {@link CamelContext} shuts
- * down, and the cached context is invalidated at the same time so a restarted 
context rebuilds a fresh source.
+ * down, and the cached context is invalidated at the same time so a restarted 
context rebuilds a fresh source. When
+ * used without a CamelContext, close this instance explicitly, for example 
with try-with-resources.
  */
 @Metadata(label = "bean",
           description = "SSLContextParameters whose SSLContext is backed by 
the SPIFFE Workload API: the X.509-SVID and"
@@ -66,7 +67,7 @@ import org.apache.camel.util.ObjectHelper;
                         + " still apply.",
           annotations = { 
"interfaceName=org.apache.camel.support.jsse.SSLContextParameters" })
 @Configurer(metadataOnly = true)
-public class SpiffeSSLContextParameters extends SSLContextParameters {
+public class SpiffeSSLContextParameters extends SSLContextParameters 
implements AutoCloseable {
 
     private static final String DEFAULT_PROTOCOL = "TLSv1.3";
 
@@ -90,6 +91,7 @@ public class SpiffeSSLContextParameters extends 
SSLContextParameters {
     private long initTimeout = 30000L;
 
     private volatile SSLContext sslContext;
+    private X509SourceService sourceService;
 
     public String getSpiffeSocketPath() {
         return spiffeSocketPath;
@@ -179,6 +181,7 @@ public class SpiffeSSLContextParameters extends 
SSLContextParameters {
             }
 
             X509Source source = createX509Source();
+            X509SourceService service = new X509SourceService(source);
             try {
                 SslContextOptions.SslContextOptionsBuilder options
                         = 
SslContextOptions.builder().x509Source(source).sslProtocol(protocol);
@@ -196,11 +199,12 @@ public class SpiffeSSLContextParameters extends 
SSLContextParameters {
                                 getSSLEngineConfigurers(spiffeContext),
                                 getSSLSocketFactoryConfigurers(spiffeContext),
                                 
getSSLServerSocketFactoryConfigurers(spiffeContext)));
-                registerForShutdown(camelContext, source);
+                registerForShutdown(camelContext, service);
+                this.sourceService = service;
                 this.sslContext = decorated;
                 return decorated;
             } catch (GeneralSecurityException | RuntimeException e) {
-                closeQuietly(source);
+                service.stop();
                 throw e;
             }
         }
@@ -219,19 +223,35 @@ public class SpiffeSSLContextParameters extends 
SSLContextParameters {
         }
     }
 
-    private void registerForShutdown(CamelContext camelContext, X509Source 
source) {
+    private void registerForShutdown(CamelContext camelContext, 
X509SourceService service) {
         // fall back to the context this instance already knows 
(JsseParameters is CamelContextAware), so callers
         // that pass null - e.g. some HTTP server factories - still get the 
source closed on shutdown
         CamelContext context = camelContext != null ? camelContext : 
getCamelContext();
         if (context != null) {
             try {
-                context.addService(new X509SourceService(source));
+                context.addService(service);
             } catch (Exception e) {
                 throw new RuntimeCamelException(e);
             }
         }
     }
 
+    /**
+     * Closes the current Workload API source and invalidates the cached 
SSLContext. This is required when no
+     * CamelContext owns this instance, and is safe to call more than once. A 
later call to createSSLContext creates a
+     * fresh source; existing TLS connections and SSLContext references must 
no longer be used after closing.
+     */
+    @Override
+    public void close() {
+        X509SourceService service;
+        synchronized (this) {
+            service = sourceService;
+        }
+        if (service != null) {
+            service.stop();
+        }
+    }
+
     static Set<SpiffeId> parseSpiffeIds(String csv) {
         Set<SpiffeId> ids = new LinkedHashSet<>();
         for (String part : csv.split(",")) {
@@ -259,6 +279,7 @@ public class SpiffeSSLContextParameters extends 
SSLContextParameters {
      */
     private final class X509SourceService implements Service {
         private final X509Source source;
+        private boolean stopped;
 
         X509SourceService(X509Source source) {
             this.source = source;
@@ -271,10 +292,17 @@ public class SpiffeSSLContextParameters extends 
SSLContextParameters {
 
         @Override
         public void stop() {
-            closeQuietly(source);
             synchronized (SpiffeSSLContextParameters.this) {
-                sslContext = null;
+                if (stopped) {
+                    return;
+                }
+                stopped = true;
+                if (sourceService == this) {
+                    sourceService = null;
+                    sslContext = null;
+                }
             }
+            closeQuietly(source);
         }
     }
 }
diff --git 
a/components/camel-spiffe/src/test/java/org/apache/camel/component/spiffe/SpiffeSSLContextParametersLifecycleTest.java
 
b/components/camel-spiffe/src/test/java/org/apache/camel/component/spiffe/SpiffeSSLContextParametersLifecycleTest.java
new file mode 100644
index 000000000000..fc25eefe744f
--- /dev/null
+++ 
b/components/camel-spiffe/src/test/java/org/apache/camel/component/spiffe/SpiffeSSLContextParametersLifecycleTest.java
@@ -0,0 +1,172 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.camel.component.spiffe;
+
+import javax.net.ssl.SSLContext;
+
+import io.spiffe.provider.SpiffeSslContextFactory;
+import io.spiffe.provider.SpiffeSslContextFactory.SslContextOptions;
+import io.spiffe.workloadapi.DefaultX509Source;
+import io.spiffe.workloadapi.DefaultX509Source.X509SourceOptions;
+import org.apache.camel.CamelContext;
+import org.apache.camel.RuntimeCamelException;
+import org.apache.camel.impl.DefaultCamelContext;
+import org.junit.jupiter.api.Test;
+import org.mockito.MockedStatic;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.doThrow;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.mockStatic;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+
+class SpiffeSSLContextParametersLifecycleTest {
+
+    @Test
+    void standaloneSourceIsClosedExactlyOnceAndCanBeRecreated() throws 
Exception {
+        try (Sources sources = new Sources(); SpiffeSSLContextParameters 
parameters = parameters()) {
+            parameters.close();
+            SSLContext first = parameters.createSSLContext(null);
+            assertThat(parameters.createSSLContext(null)).isSameAs(first);
+            verify(sources.first, never()).close();
+
+            parameters.close();
+            parameters.close();
+            verify(sources.first).close();
+
+            SSLContext second = parameters.createSSLContext(null);
+            assertThat(second).isNotSameAs(first);
+            parameters.close();
+            verify(sources.first).close();
+            verify(sources.second).close();
+        }
+    }
+
+    @Test
+    void nullArgumentUsesTheAssociatedCamelContextForShutdown() throws 
Exception {
+        try (Sources sources = new Sources(); CamelContext context = new 
DefaultCamelContext()) {
+            context.start();
+            SpiffeSSLContextParameters parameters = parameters();
+            parameters.setCamelContext(context);
+            parameters.createSSLContext(null);
+
+            context.stop();
+            verify(sources.first).close();
+            parameters.close();
+            verify(sources.first).close();
+        }
+    }
+
+    @Test
+    void contextRestartCreatesAFreshSource() throws Exception {
+        try (Sources sources = new Sources(); CamelContext context = new 
DefaultCamelContext()) {
+            context.start();
+            SpiffeSSLContextParameters parameters = parameters();
+            SSLContext first = parameters.createSSLContext(context);
+            context.stop();
+            verify(sources.first).close();
+
+            context.start();
+            
assertThat(parameters.createSSLContext(context)).isNotSameAs(first);
+            context.stop();
+            verify(sources.first).close();
+            verify(sources.second).close();
+        }
+    }
+
+    @Test
+    void stoppingAnOldContextDoesNotInvalidateARecreatedSource() throws 
Exception {
+        try (Sources sources = new Sources(); CamelContext context = new 
DefaultCamelContext();
+             CamelContext replacementContext = new DefaultCamelContext();
+             SpiffeSSLContextParameters parameters = parameters()) {
+            context.start();
+            replacementContext.start();
+            parameters.createSSLContext(context);
+            parameters.close();
+            SSLContext replacement = 
parameters.createSSLContext(replacementContext);
+
+            context.stop();
+            
assertThat(parameters.createSSLContext(replacementContext)).isSameAs(replacement);
+            verify(sources.first).close();
+            verify(sources.second, never()).close();
+            parameters.close();
+            verify(sources.second).close();
+        }
+    }
+
+    @Test
+    void tlsContextCreationFailureClosesTheSourceAndAllowsRetry() throws 
Exception {
+        try (Sources sources = new Sources(); SpiffeSSLContextParameters 
parameters = parameters()) {
+            sources.factory.when(() -> 
SpiffeSslContextFactory.getSslContext(any(SslContextOptions.class)))
+                    .thenThrow(new IllegalStateException("TLS initialization 
failed"))
+                    .thenReturn(SSLContext.getDefault());
+
+            assertThatThrownBy(() -> parameters.createSSLContext(null))
+                    .isInstanceOf(IllegalStateException.class)
+                    .hasMessage("TLS initialization failed");
+            verify(sources.first).close();
+
+            assertThat(parameters.createSSLContext(null)).isNotNull();
+            parameters.close();
+            verify(sources.first).close();
+            verify(sources.second).close();
+        }
+    }
+
+    @Test
+    void shutdownRegistrationFailureClosesTheSource() throws Exception {
+        try (Sources sources = new Sources(); SpiffeSSLContextParameters 
parameters = parameters()) {
+            CamelContext context = mock(CamelContext.class);
+            doThrow(new IllegalStateException("Cannot register 
service")).when(context).addService(any());
+
+            assertThatThrownBy(() -> parameters.createSSLContext(context))
+                    .isInstanceOf(RuntimeCamelException.class)
+                    .hasRootCauseMessage("Cannot register service");
+            parameters.close();
+            verify(sources.first, times(1)).close();
+        }
+    }
+
+    private static SpiffeSSLContextParameters parameters() {
+        SpiffeSSLContextParameters parameters = new 
SpiffeSSLContextParameters();
+        parameters.setAcceptAnySpiffeId(true);
+        return parameters;
+    }
+
+    private static final class Sources implements AutoCloseable {
+        private final DefaultX509Source first = mock(DefaultX509Source.class);
+        private final DefaultX509Source second = mock(DefaultX509Source.class);
+        private final MockedStatic<DefaultX509Source> source = 
mockStatic(DefaultX509Source.class);
+        private final MockedStatic<SpiffeSslContextFactory> factory = 
mockStatic(SpiffeSslContextFactory.class);
+
+        private Sources() throws Exception {
+            source.when(() -> 
DefaultX509Source.newSource(any(X509SourceOptions.class))).thenReturn(first, 
second);
+            factory.when(() -> 
SpiffeSslContextFactory.getSslContext(any(SslContextOptions.class)))
+                    .thenReturn(SSLContext.getDefault());
+        }
+
+        @Override
+        public void close() {
+            factory.close();
+            source.close();
+        }
+    }
+}

Reply via email to