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 40d14c004 Return only the log files the requesting user may access 
from the log file listing (#8974)
40d14c004 is described below

commit 40d14c0041a33da2e203f95805d1b72f2a4676ed
Author: Richard Zowalla <[email protected]>
AuthorDate: Sat Aug 22 18:11:45 2026 +0200

    Return only the log files the requesting user may access from the log file 
listing (#8974)
---
 .../logviewer/handler/LogviewerLogPageHandler.java | 17 ++++++-
 .../handler/LogviewerLogPageHandlerTest.java       | 56 ++++++++++++++++++++--
 2 files changed, 67 insertions(+), 6 deletions(-)

diff --git 
a/storm-webapp/src/main/java/org/apache/storm/daemon/logviewer/handler/LogviewerLogPageHandler.java
 
b/storm-webapp/src/main/java/org/apache/storm/daemon/logviewer/handler/LogviewerLogPageHandler.java
index 3ffdf75f4..5917e9746 100644
--- 
a/storm-webapp/src/main/java/org/apache/storm/daemon/logviewer/handler/LogviewerLogPageHandler.java
+++ 
b/storm-webapp/src/main/java/org/apache/storm/daemon/logviewer/handler/LogviewerLogPageHandler.java
@@ -114,7 +114,7 @@ public class LogviewerLogPageHandler {
     }
 
     /**
-     * Enumerate worker log files for given criteria.
+     * Enumerate worker log files for given criteria. Only the files the user 
is allowed to access are returned.
      *
      * @param user username
      * @param port worker's port, null for all workers
@@ -175,8 +175,10 @@ public class LogviewerLogPageHandler {
 
         List<String> files;
         if (fileResults != null) {
+            Map<String, Boolean> authorizedPortDirs = new HashMap<>();
             files = fileResults.stream()
                     .map(WorkerLogs::getTopologyPortWorkerLog)
+                    .filter(fileStr -> isUserAllowedToAccessLog(user, fileStr, 
authorizedPortDirs))
                     .sorted().collect(toList());
         } else {
             files = new ArrayList<>();
@@ -185,6 +187,19 @@ public class LogviewerLogPageHandler {
         return LogviewerResponseBuilder.buildSuccessJsonResponse(files, 
callback, origin);
     }
 
+    /**
+     * Check whether the user may access the given "topologyId/port/fileName" 
worker log. The authorization only depends on the
+     * topology and the port, so the answer is cached per port directory to 
avoid re-reading the log metadata for every file.
+     */
+    private boolean isUserAllowedToAccessLog(String user, String fileStr, 
Map<String, Boolean> authorizedPortDirs) {
+        Path portDir = Paths.get(fileStr).getParent();
+        if (portDir == null) {
+            return resourceAuthorizer.isUserAllowedToAccessFile(user, fileStr);
+        }
+        return authorizedPortDirs.computeIfAbsent(portDir.toString(),
+            key -> resourceAuthorizer.isUserAllowedToAccessFile(user, 
fileStr));
+    }
+
     /**
      * Provides a worker log file to view, starting from the specified position
      * or default starting position of the most recent page.
diff --git 
a/storm-webapp/src/test/java/org/apache/storm/daemon/logviewer/handler/LogviewerLogPageHandlerTest.java
 
b/storm-webapp/src/test/java/org/apache/storm/daemon/logviewer/handler/LogviewerLogPageHandlerTest.java
index 2114a9d8c..b6de62009 100644
--- 
a/storm-webapp/src/test/java/org/apache/storm/daemon/logviewer/handler/LogviewerLogPageHandlerTest.java
+++ 
b/storm-webapp/src/test/java/org/apache/storm/daemon/logviewer/handler/LogviewerLogPageHandlerTest.java
@@ -22,6 +22,12 @@ import static org.hamcrest.CoreMatchers.is;
 import static org.hamcrest.MatcherAssert.assertThat;
 import static org.junit.jupiter.api.Assertions.assertEquals;
 import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.ArgumentMatchers.startsWith;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
 
 import com.fasterxml.jackson.databind.ObjectMapper;
 
@@ -69,19 +75,22 @@ public class LogviewerLogPageHandlerTest {
                 new WorkerLogs(stormConf, Paths.get(rootPath), 
metricsRegistry), new ResourceAuthorizer(stormConf), metricsRegistry);
 
         final Response expectedAll = 
LogviewerResponseBuilder.buildSuccessJsonResponse(
-                List.of("topoA/port1/worker.log", "topoA/port2/worker.log", 
"topoB/port1/worker.log"),
+                List.of(String.join(File.separator, "topoA", "1111", 
"worker.log"),
+                        String.join(File.separator, "topoA", "2222", 
"worker.log"),
+                        String.join(File.separator, "topoB", "1111", 
"worker.log")),
                 null,
                 origin
         );
 
         final Response expectedFilterPort = 
LogviewerResponseBuilder.buildSuccessJsonResponse(
-                List.of("topoA/port1/worker.log", "topoB/port1/worker.log"),
+                List.of(String.join(File.separator, "topoA", "1111", 
"worker.log"),
+                        String.join(File.separator, "topoB", "1111", 
"worker.log")),
                 null,
                 origin
         );
 
         final Response expectedFilterTopoId = 
LogviewerResponseBuilder.buildSuccessJsonResponse(
-                List.of("topoB/port1/worker.log"),
+                List.of(String.join(File.separator, "topoB", "1111", 
"worker.log")),
                 null,
                 origin
         );
@@ -97,11 +106,48 @@ public class LogviewerLogPageHandlerTest {
         assertEqualsJsonResponse(expectedFilterTopoId, returnedFilterTopoId, 
List.class);
     }
 
+    /**
+     * list-log-files only returns the log files the user is allowed to access.
+     */
+    @Test
+    public void testListLogFilesFiltersFilesTheUserMayNotAccess() throws 
IOException {
+        String rootPath = 
Files.createTempDirectory("workers-artifacts").toFile().getCanonicalPath();
+        File file1 = new File(String.join(File.separator, rootPath, "topoA", 
"1111"), "worker.log");
+        File file2 = new File(String.join(File.separator, rootPath, "topoA", 
"1111"), "worker.log.1");
+        File file3 = new File(String.join(File.separator, rootPath, "topoB", 
"1111"), "worker.log");
+
+        file1.getParentFile().mkdirs();
+        file3.getParentFile().mkdirs();
+        file1.createNewFile();
+        file2.createNewFile();
+        file3.createNewFile();
+
+        String origin = "www.origin.server.net";
+        String topoAPortDir = String.join(File.separator, "topoA", "1111");
+        Map<String, Object> stormConf = Utils.readStormConfig();
+        StormMetricsRegistry metricsRegistry = new StormMetricsRegistry();
+        ResourceAuthorizer resourceAuthorizer = mock(ResourceAuthorizer.class);
+        when(resourceAuthorizer.isUserAllowedToAccessFile(anyString(), 
startsWith(topoAPortDir))).thenReturn(true);
+        LogviewerLogPageHandler handler = new 
LogviewerLogPageHandler(rootPath, rootPath,
+                new WorkerLogs(stormConf, Paths.get(rootPath), 
metricsRegistry), resourceAuthorizer, metricsRegistry);
+
+        final Response returned = handler.listLogFiles("user", null, null, 
null, origin);
+
+        List<?> files = new ObjectMapper().readValue((String) 
returned.getEntity(), List.class);
+
+        Utils.forceDelete(rootPath);
+
+        assertEquals(List.of(String.join(File.separator, topoAPortDir, 
"worker.log"),
+                String.join(File.separator, topoAPortDir, "worker.log.1")), 
files);
+        //The authorization only depends on the port directory, so it is 
checked once per port directory, not once per file.
+        verify(resourceAuthorizer, 
times(2)).isUserAllowedToAccessFile(anyString(), anyString());
+    }
+
     private <T> void assertEqualsJsonResponse(Response expected, Response 
actual, Class<T> entityClass) throws IOException {
         ObjectMapper objectMapper = new ObjectMapper();
         T entityFromExpected = objectMapper.readValue((String) 
expected.getEntity(), entityClass);
-        T actualFromExpected = objectMapper.readValue((String) 
expected.getEntity(), entityClass);
-        assertEquals(entityFromExpected, actualFromExpected);
+        T entityFromActual = objectMapper.readValue((String) 
actual.getEntity(), entityClass);
+        assertEquals(entityFromExpected, entityFromActual);
 
         assertEquals(expected.getStatus(), actual.getStatus());
         
assertTrue(expected.getHeaders().equalsIgnoreValueOrder(actual.getHeaders()));

Reply via email to