This is an automated email from the ASF dual-hosted git repository.

rzo1 pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/storm.git


The following commit(s) were added to refs/heads/master by this push:
     new c8d41cf60 Reject a topology submission that lists a blob key which is 
not a dependency blob key (#9012)
c8d41cf60 is described below

commit c8d41cf607d256bb39eaa636b24801232c58fb67
Author: Richard Zowalla <[email protected]>
AuthorDate: Mon Aug 24 12:57:21 2026 +0200

    Reject a topology submission that lists a blob key which is not a 
dependency blob key (#9012)
---
 .../storm/dependency/DependencyBlobStoreUtils.java |  18 ++-
 .../org/apache/storm/daemon/nimbus/Nimbus.java     |  57 ++++++++
 .../org/apache/storm/daemon/nimbus/NimbusTest.java | 146 +++++++++++++++++++++
 3 files changed, 220 insertions(+), 1 deletion(-)

diff --git 
a/storm-client/src/jvm/org/apache/storm/dependency/DependencyBlobStoreUtils.java
 
b/storm-client/src/jvm/org/apache/storm/dependency/DependencyBlobStoreUtils.java
index 5211e5570..f692323b9 100644
--- 
a/storm-client/src/jvm/org/apache/storm/dependency/DependencyBlobStoreUtils.java
+++ 
b/storm-client/src/jvm/org/apache/storm/dependency/DependencyBlobStoreUtils.java
@@ -24,12 +24,28 @@ import 
org.apache.storm.shade.org.apache.commons.lang3.StringUtils;
 
 public class DependencyBlobStoreUtils {
 
-    private static final String BLOB_DEPENDENCIES_PREFIX = "dep-";
+    /**
+     * The prefix every blob key holding a topology dependency starts with.
+     */
+    public static final String BLOB_DEPENDENCIES_PREFIX = "dep-";
 
     public static String generateDependencyBlobKey(String key) {
         return BLOB_DEPENDENCIES_PREFIX + key;
     }
 
+    /**
+     * Tell whether a blob key names a topology dependency, i.e. whether it 
could have been produced by
+     * {@link #generateDependencyBlobKey(String)}. Keys that a topology only 
refers to, rather than owns, must be
+     * checked with this before they are acted upon, because the dependency 
lists of a submitted topology are filled
+     * in by the client and can name any blob at all.
+     *
+     * @param key the blob key to check, may be null
+     * @return true if the key is a dependency blob key
+     */
+    public static boolean isDependencyBlobKey(String key) {
+        return key != null && key.startsWith(BLOB_DEPENDENCIES_PREFIX);
+    }
+
     @SuppressWarnings("checkstyle:AbbreviationAsWordInName")
     public static String applyUUIDToFileName(String fileName) {
         String fileNameWithExt = Files.getNameWithoutExtension(fileName);
diff --git 
a/storm-server/src/main/java/org/apache/storm/daemon/nimbus/Nimbus.java 
b/storm-server/src/main/java/org/apache/storm/daemon/nimbus/Nimbus.java
index ca45abc24..4469f8dfa 100644
--- a/storm-server/src/main/java/org/apache/storm/daemon/nimbus/Nimbus.java
+++ b/storm-server/src/main/java/org/apache/storm/daemon/nimbus/Nimbus.java
@@ -88,6 +88,7 @@ import org.apache.storm.daemon.DaemonCommon;
 import org.apache.storm.daemon.Shutdownable;
 import org.apache.storm.daemon.StormCommon;
 import org.apache.storm.daemon.common.FileWatcher;
+import org.apache.storm.dependency.DependencyBlobStoreUtils;
 import org.apache.storm.generated.AlreadyAliveException;
 import org.apache.storm.generated.Assignment;
 import org.apache.storm.generated.AuthorizationException;
@@ -1308,6 +1309,61 @@ public class Nimbus implements Iface, Shutdownable, 
DaemonCommon {
         }
     }
 
+    /**
+     * Check that a submitted topology only claims blobs that are topology 
dependencies, and that every one of them
+     * exists. The dependency lists of a submitted topology are filled in by 
the client, so without this a submission
+     * could name any blob at all, for example another topology's code or 
configuration blob, and nimbus would delete
+     * it as its own dependency once the submitted topology is cleaned up. A 
key that exists nowhere is just as
+     * damaging: on gaining leadership a nimbus compares the dependencies of 
every active topology against its
+     * blobstore and gives up leadership when one is missing, so a single 
unresolvable key on a single active topology
+     * leaves the cluster without a leader for as long as that topology is 
active.
+     *
+     * <p>Existence is probed with {@code getBlobMeta} as the submitter, 
exactly as
+     * {@link Utils#validateTopologyBlobStoreMap(Map, BlobStore)} probes the 
blobs of
+     * {@link Config#TOPOLOGY_BLOBSTORE_MAP}, so a submitter can only claim a 
dependency it is allowed to read. The
+     * client uploads the dependency blobs before it submits, so they are 
present by the time this runs.
+     *
+     * @param topology  the submitted topology
+     * @param blobStore the blobstore to look the keys up in
+     * @param subject   the subject to look the keys up as, i.e. the submitter
+     * @throws InvalidTopologyException if a dependency list holds something 
that is not a dependency blob key, or
+     *                                  names a dependency blob that does not 
exist
+     * @throws AuthorizationException   if the submitter may not read one of 
the dependency blobs it named
+     */
+    @VisibleForTesting
+    static void validateDependencyBlobKeys(StormTopology topology, BlobStore 
blobStore, Subject subject)
+        throws InvalidTopologyException, AuthorizationException {
+        Set<String> checked = new HashSet<>();
+        validateDependencyBlobKeys(topology.get_dependency_jars(), 
"dependency_jars", blobStore, subject, checked);
+        validateDependencyBlobKeys(topology.get_dependency_artifacts(), 
"dependency_artifacts", blobStore, subject, checked);
+    }
+
+    private static void validateDependencyBlobKeys(List<String> keys, String 
fieldName, BlobStore blobStore, Subject subject,
+                                                   Set<String> checked) throws 
InvalidTopologyException, AuthorizationException {
+        if (keys == null) {
+            return;
+        }
+        for (String key : keys) {
+            if (!DependencyBlobStoreUtils.isDependencyBlobKey(key)) {
+                throw new WrappedInvalidTopologyException("Topology " + 
fieldName + " lists [" + key
+                    + "], which is not a dependency blob key; every entry must 
start with \""
+                    + DependencyBlobStoreUtils.BLOB_DEPENDENCIES_PREFIX + 
"\"");
+            }
+            if (!checked.add(key)) {
+                // the same dependency may be listed twice, one lookup for it 
is enough
+                continue;
+            }
+            try {
+                blobStore.getBlobMeta(key, subject);
+            } catch (KeyNotFoundException keyNotFound) {
+                throw new WrappedInvalidTopologyException("Topology " + 
fieldName + " lists [" + key
+                    + "], which is not in the blobstore; upload the dependency 
before submitting the topology, and if it "
+                    + "was uploaded earlier note that a dependency blob is 
deleted once no topology uses it any more, so "
+                    + "it has to be uploaded again");
+            }
+        }
+    }
+
     private static StormTopology tryReadTopology(String topoId, TopoCache tc)
         throws NotAliveException, AuthorizationException, IOException {
         try {
@@ -3440,6 +3496,7 @@ public class Nimbus implements Iface, Shutdownable, 
DaemonCommon {
                 throw new WrappedInvalidTopologyException(ex.getMessage());
             }
             validator.validate(topoName, topoConf, topology);
+            validateDependencyBlobKeys(topology, blobStore, getSubject());
             if ((boolean) conf.getOrDefault(Config.DISABLE_SYMLINKS, false)) {
                 @SuppressWarnings("unchecked")
                 Map<String, Object> blobMap = (Map<String, Object>) 
topoConf.get(Config.TOPOLOGY_BLOBSTORE_MAP);
diff --git 
a/storm-server/src/test/java/org/apache/storm/daemon/nimbus/NimbusTest.java 
b/storm-server/src/test/java/org/apache/storm/daemon/nimbus/NimbusTest.java
index 92d1a9ecf..e27b5b4ec 100644
--- a/storm-server/src/test/java/org/apache/storm/daemon/nimbus/NimbusTest.java
+++ b/storm-server/src/test/java/org/apache/storm/daemon/nimbus/NimbusTest.java
@@ -52,6 +52,8 @@ import org.apache.storm.generated.RebalanceOptions;
 import org.apache.storm.generated.ReadableBlobMeta;
 import org.apache.storm.generated.SettableBlobMeta;
 import org.apache.storm.generated.StormTopology;
+import org.apache.storm.generated.SubmitOptions;
+import org.apache.storm.generated.TopologyInitialStatus;
 import org.apache.storm.metric.StormMetricsRegistry;
 import org.apache.storm.nimbus.ILeaderElector;
 import org.apache.storm.nimbus.NimbusInfo;
@@ -87,6 +89,7 @@ import static org.junit.jupiter.api.Assertions.assertFalse;
 import static org.junit.jupiter.api.Assertions.assertThrows;
 import static org.junit.jupiter.api.Assertions.assertTrue;
 import static org.junit.jupiter.api.Assertions.fail;
+import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
 import static org.junit.jupiter.api.Assertions.assertNull;
 import static org.junit.jupiter.api.Assertions.assertSame;
 import static org.mockito.ArgumentMatchers.any;
@@ -97,6 +100,7 @@ import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.mockConstruction;
 import static org.mockito.Mockito.atLeastOnce;
 import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.times;
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
 
@@ -620,4 +624,146 @@ class NimbusTest {
         subject.getPrincipals().add(new SingleUserPrincipal(user));
         ReqContext.context().setSubject(subject);
     }
+
+    @Test
+    void testValidateDependencyBlobKeysRejectsKeysThatAreNotDependencies() 
throws Exception {
+        // a topology fills its own dependency lists in on the client side, so 
nimbus has to check that they only
+        // name dependency blobs before it takes ownership of them and deletes 
them during cleanup
+        String victimJarKey = 
ConfigUtils.masterStormJarKey("victim-1-1234567890");
+        String victimConfKey = 
ConfigUtils.masterStormConfKey("victim-1-1234567890");
+        Subject submitter = new Subject();
+
+        StormTopology jarField = new StormTopology();
+        jarField.set_dependency_jars(List.of(victimJarKey));
+        InvalidTopologyException jarException = 
assertThrows(InvalidTopologyException.class,
+            () -> Nimbus.validateDependencyBlobKeys(jarField, localBlobStore, 
submitter));
+        assertTrue(jarException.get_msg().contains(victimJarKey), 
jarException.get_msg());
+        assertTrue(jarException.get_msg().contains("dependency_jars"), 
jarException.get_msg());
+
+        StormTopology artifactField = new StormTopology();
+        artifactField.set_dependency_artifacts(List.of(victimConfKey));
+        InvalidTopologyException artifactException = 
assertThrows(InvalidTopologyException.class,
+            () -> Nimbus.validateDependencyBlobKeys(artifactField, 
localBlobStore, submitter));
+        assertTrue(artifactException.get_msg().contains(victimConfKey), 
artifactException.get_msg());
+        
assertTrue(artifactException.get_msg().contains("dependency_artifacts"), 
artifactException.get_msg());
+
+        // a good key followed by a bad one is caught too, and the message 
names the bad one
+        StormTopology mixed = new StormTopology();
+        mixed.set_dependency_jars(List.of(dependencyKey("a.jar"), 
victimJarKey));
+        InvalidTopologyException mixedException = 
assertThrows(InvalidTopologyException.class,
+            () -> Nimbus.validateDependencyBlobKeys(mixed, localBlobStore, 
submitter));
+        assertTrue(mixedException.get_msg().contains(victimJarKey), 
mixedException.get_msg());
+
+        // a key that is not a dependency key at all is rejected on its name, 
without asking the blobstore about it
+        verify(localBlobStore, never()).getBlobMeta(eq(victimJarKey), any());
+        verify(localBlobStore, never()).getBlobMeta(eq(victimConfKey), any());
+    }
+
+    @Test
+    void testValidateDependencyBlobKeysRejectsKeyThatIsNotInTheBlobStore() 
throws Exception {
+        // a key that merely looks like a dependency key is just as damaging: 
on gaining leadership a nimbus gives up
+        // leadership again when an active topology names a dependency it 
cannot find, so an unresolvable key leaves
+        // the cluster without a leader
+        String presentKey = dependencyKey("present.jar");
+        String missingKey = dependencyKey("missing.jar");
+        Subject submitter = new Subject();
+        when(localBlobStore.getBlobMeta(eq(missingKey), any())).thenThrow(new 
KeyNotFoundException(missingKey));
+
+        StormTopology jarField = new StormTopology();
+        jarField.set_dependency_jars(List.of(presentKey, missingKey));
+        InvalidTopologyException jarException = 
assertThrows(InvalidTopologyException.class,
+            () -> Nimbus.validateDependencyBlobKeys(jarField, localBlobStore, 
submitter));
+        assertTrue(jarException.get_msg().contains(missingKey), 
jarException.get_msg());
+        assertTrue(jarException.get_msg().contains("dependency_jars"), 
jarException.get_msg());
+        assertTrue(jarException.get_msg().contains("not in the blobstore"), 
jarException.get_msg());
+
+        StormTopology artifactField = new StormTopology();
+        artifactField.set_dependency_artifacts(List.of(missingKey));
+        InvalidTopologyException artifactException = 
assertThrows(InvalidTopologyException.class,
+            () -> Nimbus.validateDependencyBlobKeys(artifactField, 
localBlobStore, submitter));
+        assertTrue(artifactException.get_msg().contains(missingKey), 
artifactException.get_msg());
+        
assertTrue(artifactException.get_msg().contains("dependency_artifacts"), 
artifactException.get_msg());
+    }
+
+    @Test
+    void testValidateDependencyBlobKeysLooksBlobsUpAsTheSubmitter() throws 
Exception {
+        // looking the blob up as the submitter, the way the 
TOPOLOGY_BLOBSTORE_MAP entries are looked up, also
+        // answers whether the submitter is allowed to read the dependency it 
claims; a dependency blob is uploaded
+        // with OTHER READ, so a legitimate submission passes
+        String key = dependencyKey("some-jar.jar");
+        Subject submitter = new Subject();
+
+        StormTopology topology = new StormTopology();
+        // listed under both fields to show that a key is looked up once no 
matter how often it is named
+        topology.set_dependency_jars(List.of(key, key));
+        topology.set_dependency_artifacts(List.of(key));
+        assertDoesNotThrow(() -> Nimbus.validateDependencyBlobKeys(topology, 
localBlobStore, submitter));
+
+        verify(localBlobStore, times(1)).getBlobMeta(key, submitter);
+    }
+
+    @Test
+    void testValidateDependencyBlobKeysAcceptsGeneratedKeysThatExist() throws 
Exception {
+        Subject submitter = new Subject();
+        StormTopology topology = new StormTopology();
+        topology.set_dependency_jars(List.of(dependencyKey("some-jar.jar"), 
dependencyKey("no-extension")));
+        
topology.set_dependency_artifacts(List.of(dependencyKey("group-artifact-1.0.jar")));
+        assertDoesNotThrow(() -> Nimbus.validateDependencyBlobKeys(topology, 
localBlobStore, submitter));
+
+        // unset lists are how a topology submitted without dependencies looks
+        assertDoesNotThrow(() -> Nimbus.validateDependencyBlobKeys(new 
StormTopology(), localBlobStore, submitter));
+        verify(localBlobStore, never()).getBlobMeta(eq(null), any());
+    }
+
+    @Test
+    void testSubmitTopologyRejectsDependencyBlobKeyOfAnotherTopology() throws 
Exception {
+        Map<String, Object> conf = 
Map.of(DaemonConfig.NIMBUS_MONITOR_FREQ_SECS, 10);
+        Nimbus submitNimbus = new Nimbus(conf, iNimbus, stormClusterState, 
nimbusInfo, localBlobStore, leaderElector,
+            groupMapper, new StormMetricsRegistry());
+        when(leaderElector.isLeader()).thenReturn(true);
+        when(stormClusterState.getTopoId(any())).thenReturn(Optional.empty());
+
+        TopologyBuilder builder = new TopologyBuilder();
+        builder.setSpout("wordSpout", new TestWordSpout(), 1);
+        StormTopology topology = builder.createTopology();
+        String victimJarKey = 
ConfigUtils.masterStormJarKey("victim-1-1234567890");
+        topology.set_dependency_artifacts(List.of(victimJarKey));
+
+        InvalidTopologyException exception = 
assertThrows(InvalidTopologyException.class,
+            () -> submitNimbus.submitTopologyWithOpts("thief", "/dev/null", 
"{}", topology,
+                new SubmitOptions(TopologyInitialStatus.ACTIVE)));
+        assertTrue(exception.get_msg().contains(victimJarKey), 
exception.get_msg());
+
+        // the submission was rejected before anything was stored for it
+        verify(stormClusterState, never()).setupHeatbeats(any(), any());
+    }
+
+    @Test
+    void testSubmitTopologyRejectsDependencyBlobKeyThatDoesNotExist() throws 
Exception {
+        Map<String, Object> conf = 
Map.of(DaemonConfig.NIMBUS_MONITOR_FREQ_SECS, 10);
+        Nimbus submitNimbus = new Nimbus(conf, iNimbus, stormClusterState, 
nimbusInfo, localBlobStore, leaderElector,
+            groupMapper, new StormMetricsRegistry());
+        when(leaderElector.isLeader()).thenReturn(true);
+        when(stormClusterState.getTopoId(any())).thenReturn(Optional.empty());
+        String missingKey = dependencyKey("missing.jar");
+        when(localBlobStore.getBlobMeta(eq(missingKey), any())).thenThrow(new 
KeyNotFoundException(missingKey));
+
+        TopologyBuilder builder = new TopologyBuilder();
+        builder.setSpout("wordSpout", new TestWordSpout(), 1);
+        StormTopology topology = builder.createTopology();
+        topology.set_dependency_jars(List.of(missingKey));
+
+        InvalidTopologyException exception = 
assertThrows(InvalidTopologyException.class,
+            () -> submitNimbus.submitTopologyWithOpts("ghost", "/dev/null", 
"{}", topology,
+                new SubmitOptions(TopologyInitialStatus.ACTIVE)));
+        assertTrue(exception.get_msg().contains(missingKey), 
exception.get_msg());
+        assertTrue(exception.get_msg().contains("not in the blobstore"), 
exception.get_msg());
+
+        // the submission was rejected before anything was stored for it
+        verify(stormClusterState, never()).setupHeatbeats(any(), any());
+    }
+
+    private static String dependencyKey(String fileName) {
+        return 
DependencyBlobStoreUtils.generateDependencyBlobKey(DependencyBlobStoreUtils.applyUUIDToFileName(fileName));
+    }
 }

Reply via email to