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

liyang-kylin pushed a commit to branch kylin5
in repository https://gitbox.apache.org/repos/asf/kylin.git

commit 82ba881c4da629c93b6978167dcf4065e05f3326
Author: Li Yang <[email protected]>
AuthorDate: Fri Oct 2 14:24:02 2026 +0800

    KYLIN-6108 improve gluten cache access control
---
 .../kylin/rest/controller/BroadcastController.java | 22 +-------
 .../rest/security/BroadcastSecurityContext.java    |  9 +--
 ...curityContext.java => InternalRpcSecurity.java} | 64 +++++++++-------------
 .../apache/kylin/rest/service/BasicService.java    | 13 +++++
 .../apache/kylin/rest/service/RouteService.java    | 10 +++-
 .../kylin/tool/restclient/RestClientTest.java      |  5 ++
 .../kylin/rest/controller/JobController.java       | 12 +++-
 .../kylin/rest/controller/JobControllerTest.java   |  7 ++-
 .../apache/kylin/utils/GlutenCacheUtilsTest.java   | 11 ++++
 .../rest/controller/GlutenCacheController.java     | 21 ++++++-
 .../rest/controller/GlutenCacheControllerTest.java | 19 ++++++-
 .../kylin/rest/service/GlutenCacheService.java     |  2 +
 .../kylin/rest/service/GlutenCacheServiceTest.java | 12 ++--
 .../kylin/engine/spark/job/LoadCacheStep.java      |  5 ++
 .../org/apache/kylin/utils/GlutenCacheUtils.java   | 30 ++++++++++
 15 files changed, 162 insertions(+), 80 deletions(-)

diff --git 
a/src/common-server/src/main/java/org/apache/kylin/rest/controller/BroadcastController.java
 
b/src/common-server/src/main/java/org/apache/kylin/rest/controller/BroadcastController.java
index 43dc8a9a14..a6f608ff84 100644
--- 
a/src/common-server/src/main/java/org/apache/kylin/rest/controller/BroadcastController.java
+++ 
b/src/common-server/src/main/java/org/apache/kylin/rest/controller/BroadcastController.java
@@ -20,21 +20,17 @@ package org.apache.kylin.rest.controller;
 
 import static 
org.apache.kylin.common.constant.HttpConstant.HTTP_VND_APACHE_KYLIN_JSON;
 import static 
org.apache.kylin.common.constant.HttpConstant.HTTP_VND_APACHE_KYLIN_V4_PUBLIC_JSON;
-import static 
org.apache.kylin.common.exception.ServerErrorCode.PERMISSION_DENIED;
 
 import java.io.IOException;
-import java.nio.charset.StandardCharsets;
-import java.security.MessageDigest;
 
 import javax.servlet.http.HttpServletRequest;
 
-import org.apache.commons.lang3.StringUtils;
-import org.apache.kylin.common.KylinConfig;
 import org.apache.kylin.common.exception.KylinException;
 import 
org.apache.kylin.common.persistence.transaction.BroadcastEventReadyNotifier;
 import org.apache.kylin.rest.config.initialize.BroadcastListener;
 import org.apache.kylin.rest.response.EnvelopeResponse;
 import org.apache.kylin.rest.security.BroadcastSecurityContext;
+import org.apache.kylin.rest.security.InternalRpcSecurity;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.stereotype.Controller;
 import org.springframework.web.bind.annotation.PostMapping;
@@ -64,21 +60,7 @@ public class BroadcastController extends NBasicController {
     }
 
     private void authorizeBroadcast(HttpServletRequest request) {
-        if (isAdmin()) {
-            return;
-        }
-
-        String configuredToken = 
KylinConfig.getInstanceFromEnv().getBroadcastToken();
-        String providedToken = request == null ? null
-                : 
request.getHeader(BroadcastEventReadyNotifier.BROADCAST_TOKEN_HEADER);
-        if (StringUtils.isNotBlank(configuredToken) && providedToken != null
-                && 
MessageDigest.isEqual(configuredToken.getBytes(StandardCharsets.UTF_8),
-                        providedToken.getBytes(StandardCharsets.UTF_8))) {
-            return;
-        }
-
-        throw new KylinException(PERMISSION_DENIED,
-                "Broadcast endpoint requires an authenticated global admin or 
a valid broadcast token.");
+        InternalRpcSecurity.requireGlobalAdminOrServiceToken(request);
     }
 
     @PutMapping(value = "/capacity/refresh_all")
diff --git 
a/src/common-service/src/main/java/org/apache/kylin/rest/security/BroadcastSecurityContext.java
 
b/src/common-service/src/main/java/org/apache/kylin/rest/security/BroadcastSecurityContext.java
index 57f3563ab3..fbcafb1dbf 100644
--- 
a/src/common-service/src/main/java/org/apache/kylin/rest/security/BroadcastSecurityContext.java
+++ 
b/src/common-service/src/main/java/org/apache/kylin/rest/security/BroadcastSecurityContext.java
@@ -22,9 +22,6 @@ import static 
org.apache.kylin.common.exception.ServerErrorCode.PERMISSION_DENIE
 import java.io.IOException;
 
 import org.apache.kylin.common.exception.KylinException;
-import org.apache.kylin.rest.constant.Constant;
-import org.springframework.security.core.Authentication;
-import org.springframework.security.core.context.SecurityContextHolder;
 
 import lombok.extern.slf4j.Slf4j;
 
@@ -59,11 +56,7 @@ public final class BroadcastSecurityContext {
             return;
         }
 
-        Authentication authentication = 
SecurityContextHolder.getContext().getAuthentication();
-        boolean globalAdmin = authentication != null && 
authentication.isAuthenticated()
-                && authentication.getAuthorities().stream()
-                        .anyMatch(authority -> 
Constant.ROLE_ADMIN.equals(authority.getAuthority()));
-        if (!globalAdmin) {
+        if (!InternalRpcSecurity.isGlobalAdmin()) {
             log.warn("Rejected an untrusted broadcast-originated ACL 
mutation");
             throw new KylinException(PERMISSION_DENIED, "Broadcast ACL 
mutation requires a trusted internal caller.");
         }
diff --git 
a/src/common-service/src/main/java/org/apache/kylin/rest/security/BroadcastSecurityContext.java
 
b/src/common-service/src/main/java/org/apache/kylin/rest/security/InternalRpcSecurity.java
similarity index 52%
copy from 
src/common-service/src/main/java/org/apache/kylin/rest/security/BroadcastSecurityContext.java
copy to 
src/common-service/src/main/java/org/apache/kylin/rest/security/InternalRpcSecurity.java
index 57f3563ab3..84bcab9878 100644
--- 
a/src/common-service/src/main/java/org/apache/kylin/rest/security/BroadcastSecurityContext.java
+++ 
b/src/common-service/src/main/java/org/apache/kylin/rest/security/InternalRpcSecurity.java
@@ -19,58 +19,44 @@ package org.apache.kylin.rest.security;
 
 import static 
org.apache.kylin.common.exception.ServerErrorCode.PERMISSION_DENIED;
 
-import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import java.security.MessageDigest;
 
+import javax.servlet.http.HttpServletRequest;
+
+import org.apache.commons.lang3.StringUtils;
+import org.apache.kylin.common.KylinConfig;
 import org.apache.kylin.common.exception.KylinException;
+import 
org.apache.kylin.common.persistence.transaction.BroadcastEventReadyNotifier;
 import org.apache.kylin.rest.constant.Constant;
 import org.springframework.security.core.Authentication;
 import org.springframework.security.core.context.SecurityContextHolder;
 
-import lombok.extern.slf4j.Slf4j;
-
-@Slf4j
-public final class BroadcastSecurityContext {
-
-    private static final ThreadLocal<Boolean> TRUSTED_BROADCAST = new 
ThreadLocal<>();
-
-    private BroadcastSecurityContext() {
-    }
-
-    public static boolean isTrustedBroadcast() {
-        return Boolean.TRUE.equals(TRUSTED_BROADCAST.get());
-    }
+public final class InternalRpcSecurity {
 
-    public static void runAsTrusted(CheckedRunnable action) throws IOException 
{
-        boolean previous = isTrustedBroadcast();
-        TRUSTED_BROADCAST.set(Boolean.TRUE);
-        try {
-            action.run();
-        } finally {
-            if (previous) {
-                TRUSTED_BROADCAST.set(Boolean.TRUE);
-            } else {
-                TRUSTED_BROADCAST.remove();
-            }
-        }
+    private InternalRpcSecurity() {
     }
 
-    public static void requireTrustedBroadcastOrGlobalAdmin() {
-        if (isTrustedBroadcast()) {
-            return;
-        }
-
+    public static boolean isGlobalAdmin() {
         Authentication authentication = 
SecurityContextHolder.getContext().getAuthentication();
-        boolean globalAdmin = authentication != null && 
authentication.isAuthenticated()
+        return authentication != null && authentication.isAuthenticated()
                 && authentication.getAuthorities().stream()
                         .anyMatch(authority -> 
Constant.ROLE_ADMIN.equals(authority.getAuthority()));
-        if (!globalAdmin) {
-            log.warn("Rejected an untrusted broadcast-originated ACL 
mutation");
-            throw new KylinException(PERMISSION_DENIED, "Broadcast ACL 
mutation requires a trusted internal caller.");
-        }
     }
 
-    @FunctionalInterface
-    public interface CheckedRunnable {
-        void run() throws IOException;
+    public static boolean hasValidServiceToken(HttpServletRequest request) {
+        String configuredToken = 
KylinConfig.getInstanceFromEnv().getBroadcastToken();
+        String providedToken = request == null ? null
+                : 
request.getHeader(BroadcastEventReadyNotifier.BROADCAST_TOKEN_HEADER);
+        return StringUtils.isNotBlank(configuredToken) && providedToken != null
+                && 
MessageDigest.isEqual(configuredToken.getBytes(StandardCharsets.UTF_8),
+                        providedToken.getBytes(StandardCharsets.UTF_8));
+    }
+
+    public static void requireGlobalAdminOrServiceToken(HttpServletRequest 
request) {
+        if (!isGlobalAdmin() && !hasValidServiceToken(request)) {
+            throw new KylinException(PERMISSION_DENIED,
+                    "Internal RPC requires an authenticated global admin or a 
valid service token.");
+        }
     }
 }
diff --git 
a/src/common-service/src/main/java/org/apache/kylin/rest/service/BasicService.java
 
b/src/common-service/src/main/java/org/apache/kylin/rest/service/BasicService.java
index ec5ac3d401..e7302865ec 100644
--- 
a/src/common-service/src/main/java/org/apache/kylin/rest/service/BasicService.java
+++ 
b/src/common-service/src/main/java/org/apache/kylin/rest/service/BasicService.java
@@ -24,6 +24,7 @@ import java.io.IOException;
 import java.util.Collections;
 import java.util.Comparator;
 import java.util.List;
+import java.util.Map;
 import java.util.Set;
 
 import javax.servlet.http.HttpServletRequest;
@@ -126,12 +127,24 @@ public abstract class BasicService {
         return JsonUtil.readValue(response.getBody(), EnvelopeResponse.class);
     }
 
+    public <T> EnvelopeResponse<T> generateTaskForRemoteHost(final 
HttpServletRequest request, String url, byte[] body,
+            Map<String, String> additionalHeaders) throws Exception {
+        val response = getHttpResponse(request, url, body, additionalHeaders);
+        return JsonUtil.readValue(response.getBody(), EnvelopeResponse.class);
+    }
+
     private ResponseEntity<byte[]> getHttpResponse(final HttpServletRequest 
request, String url, byte[] body) {
+        return getHttpResponse(request, url, body, Collections.emptyMap());
+    }
+
+    private ResponseEntity<byte[]> getHttpResponse(final HttpServletRequest 
request, String url, byte[] body,
+            Map<String, String> additionalHeaders) {
         HttpHeaders headers = new HttpHeaders();
         Collections.list(request.getHeaderNames())
                 .forEach(k -> headers.put(k, 
Collections.list(request.getHeaders(k))));
         //remove gzip
         headers.remove(ACCEPT_ENCODING);
+        additionalHeaders.forEach(headers::set);
         return restTemplate.exchange(url, 
HttpMethod.valueOf(request.getMethod()), new HttpEntity<>(body, headers),
                 byte[].class);
     }
diff --git 
a/src/common-service/src/main/java/org/apache/kylin/rest/service/RouteService.java
 
b/src/common-service/src/main/java/org/apache/kylin/rest/service/RouteService.java
index 7bf6a1cda2..728ff87e74 100644
--- 
a/src/common-service/src/main/java/org/apache/kylin/rest/service/RouteService.java
+++ 
b/src/common-service/src/main/java/org/apache/kylin/rest/service/RouteService.java
@@ -20,6 +20,7 @@ package org.apache.kylin.rest.service;
 
 import static 
org.apache.kylin.common.exception.ServerErrorCode.PERMISSION_DENIED;
 
+import java.util.Collections;
 import java.util.List;
 import java.util.Map;
 import java.util.Objects;
@@ -42,6 +43,7 @@ import org.apache.kylin.common.constant.LogConstant;
 import org.apache.kylin.common.exception.KylinException;
 import org.apache.kylin.common.logging.SetLogCategory;
 import org.apache.kylin.common.msg.MsgPicker;
+import 
org.apache.kylin.common.persistence.transaction.BroadcastEventReadyNotifier;
 import org.apache.kylin.common.persistence.transaction.UnitOfWork;
 import org.apache.kylin.common.util.JsonUtil;
 import org.apache.kylin.common.util.NamedThreadFactory;
@@ -270,7 +272,7 @@ public class RouteService extends BasicService {
             CountDownLatch result) throws Exception {
         try (SetLogCategory ignore = new 
SetLogCategory(LogConstant.BUILD_CATEGORY)) {
             val fullUrl = "http://"; + instance + url;
-            val response = generateTaskForRemoteHost(request, fullUrl, 
requestEntity);
+            val response = generateTaskForRemoteHost(request, fullUrl, 
requestEntity, getInternalRpcHeaders());
             log.info("cacheGluten instance is [{}], result is [{}]", instance, 
response);
             if (StringUtils.equals(url, CACHE_GLUTEN_API)) {
                 val data = JsonUtil.convert(response.getData(), 
GlutenCacheResponse.class);
@@ -284,6 +286,12 @@ public class RouteService extends BasicService {
         }
     }
 
+    private Map<String, String> getInternalRpcHeaders() {
+        String token = KylinConfig.getInstanceFromEnv().getBroadcastToken();
+        return StringUtils.isBlank(token) ? Collections.emptyMap()
+                : 
Collections.singletonMap(BroadcastEventReadyNotifier.BROADCAST_TOKEN_HEADER, 
token);
+    }
+
     public boolean checkGlutenCacheLimits(Map<Future<?>, Long> tasks) {
         var limitFlag = false;
         for (Future<?> doneTask : tasks.keySet()) {
diff --git 
a/src/core-common/src/test/java/org/apache/kylin/tool/restclient/RestClientTest.java
 
b/src/core-common/src/test/java/org/apache/kylin/tool/restclient/RestClientTest.java
index 28f508b357..95f3510dbf 100644
--- 
a/src/core-common/src/test/java/org/apache/kylin/tool/restclient/RestClientTest.java
+++ 
b/src/core-common/src/test/java/org/apache/kylin/tool/restclient/RestClientTest.java
@@ -46,6 +46,11 @@ public class RestClientTest extends 
NLocalFileMetadataTestCase {
         cleanupTestMetadata();
     }
 
+    @Test
+    public void testNoDefaultBroadcastCredential() {
+        Assert.assertEquals("", getTestConfig().getBroadcastToken());
+    }
+
     @Test
     public void testNotifyAddsBroadcastToken() throws Exception {
         getTestConfig().setProperty("kylin.server.broadcast-token", "secret");
diff --git 
a/src/data-loading-server/src/main/java/org/apache/kylin/rest/controller/JobController.java
 
b/src/data-loading-server/src/main/java/org/apache/kylin/rest/controller/JobController.java
index dfba66a6d3..26bf843c1d 100644
--- 
a/src/data-loading-server/src/main/java/org/apache/kylin/rest/controller/JobController.java
+++ 
b/src/data-loading-server/src/main/java/org/apache/kylin/rest/controller/JobController.java
@@ -59,10 +59,13 @@ import org.apache.kylin.rest.response.EventResponse;
 import org.apache.kylin.rest.response.ExecutableResponse;
 import org.apache.kylin.rest.response.ExecutableStepResponse;
 import org.apache.kylin.rest.response.JobStatisticsResponse;
+import org.apache.kylin.rest.security.InternalRpcSecurity;
 import org.apache.kylin.rest.service.JobInfoService;
 import org.apache.kylin.rest.service.JobService;
 import org.apache.kylin.rest.service.RouteService;
+import org.apache.kylin.rest.util.AclEvaluate;
 import org.apache.kylin.rest.util.SparkUIUtil;
+import org.apache.kylin.utils.GlutenCacheUtils;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 import org.springframework.beans.factory.annotation.Autowired;
@@ -101,6 +104,9 @@ public class JobController extends BaseController {
     @Autowired
     private RouteService routeService;
 
+    @Autowired
+    private AclEvaluate aclEvaluate;
+
     @Override
     protected Logger getLogger() {
         return logger;
@@ -488,8 +494,12 @@ public class JobController extends BaseController {
     public EnvelopeResponse<Boolean> routeGlutenCache(@RequestBody 
LoadGlutenCacheRequest request,
             HttpServletRequest servletRequest) throws Exception {
         logger.info("routeGlutenCache request is [{}]", request);
-        checkProjectName(request.getProject());
+        val projectName = checkProjectName(request.getProject());
+        if (!InternalRpcSecurity.isGlobalAdmin() && 
!InternalRpcSecurity.hasValidServiceToken(servletRequest)) {
+            aclEvaluate.checkProjectAdminPermission(projectName);
+        }
         checkCollectionRequiredArg("cache_commands", 
request.getCacheCommands());
+        GlutenCacheUtils.validateCacheCommands(request.getCacheCommands());
         val result = routeService.routeGlutenCache(request.getCacheCommands(), 
servletRequest);
         return new EnvelopeResponse<>(KylinException.CODE_SUCCESS, result, "");
     }
diff --git 
a/src/data-loading-server/src/test/java/org/apache/kylin/rest/controller/JobControllerTest.java
 
b/src/data-loading-server/src/test/java/org/apache/kylin/rest/controller/JobControllerTest.java
index 9eb47f0214..0285ba5a4e 100644
--- 
a/src/data-loading-server/src/test/java/org/apache/kylin/rest/controller/JobControllerTest.java
+++ 
b/src/data-loading-server/src/test/java/org/apache/kylin/rest/controller/JobControllerTest.java
@@ -58,6 +58,7 @@ import org.apache.kylin.rest.response.ExecutableStepResponse;
 import org.apache.kylin.rest.service.JobInfoService;
 import org.apache.kylin.rest.service.JobService;
 import org.apache.kylin.rest.service.RouteService;
+import org.apache.kylin.rest.util.AclEvaluate;
 import org.junit.After;
 import org.junit.Assert;
 import org.junit.Before;
@@ -97,6 +98,9 @@ public class JobControllerTest extends 
NLocalFileMetadataTestCase {
     @Mock
     private JobInfoService jobInfoService;
 
+    @Mock
+    private AclEvaluate aclEvaluate;
+
     @InjectMocks
     private final JobController jobController = Mockito.spy(new 
JobController());
 
@@ -642,7 +646,8 @@ public class JobControllerTest extends 
NLocalFileMetadataTestCase {
     @Test
     public void routeGlutenCache() throws Exception {
         val servletRequest = new MockHttpServletRequest();
-        val request = new LoadGlutenCacheRequest("default", 
Lists.newArrayList("test command"));
+        val request = new LoadGlutenCacheRequest("default",
+                Lists.newArrayList("CACHE DATA SELECT * FROM 'test-path'"));
         Mockito.when(routeService.routeGlutenCache(request.getCacheCommands(), 
servletRequest)).thenReturn(true);
         val mvcResult = 
mockMvc.perform(MockMvcRequestBuilders.post("/api/jobs/gluten_cache") //
                 
.contentType(MediaType.APPLICATION_JSON).content(JsonUtil.writeValueAsString(request)))
diff --git 
a/src/data-loading-service/src/test/java/org/apache/kylin/utils/GlutenCacheUtilsTest.java
 
b/src/data-loading-service/src/test/java/org/apache/kylin/utils/GlutenCacheUtilsTest.java
index b63514a716..5390ab03b3 100644
--- 
a/src/data-loading-service/src/test/java/org/apache/kylin/utils/GlutenCacheUtilsTest.java
+++ 
b/src/data-loading-service/src/test/java/org/apache/kylin/utils/GlutenCacheUtilsTest.java
@@ -137,6 +137,17 @@ class GlutenCacheUtilsTest extends AbstractTestCase {
         generateTableCommandError(config);
     }
 
+    @Test
+    void validateCacheCommands() {
+        Assertions.assertDoesNotThrow(() -> 
GlutenCacheUtils.validateCacheCommands(Lists.newArrayList(
+                "CACHE DATA SELECT * FROM 'test-path'",
+                "CACHE FILES ASYNC SELECT * FROM 'test-path' CACHEPROPERTIES 
(recursive=true)")));
+        Assertions.assertThrows(KylinRuntimeException.class,
+                () -> 
GlutenCacheUtils.validateCacheCommands(Lists.newArrayList("DROP TABLE test")));
+        Assertions.assertThrows(KylinRuntimeException.class, () -> 
GlutenCacheUtils
+                .validateCacheCommands(Lists.newArrayList("CACHE DATA SELECT * 
FROM 'test'; DROP TABLE test")));
+    }
+
     private static void generateTableCommand(KylinConfig config, String 
location, InternalTableDesc internalTable,
             String datePartitionFormat) {
         var start = "";
diff --git 
a/src/query-server/src/main/java/org/apache/kylin/rest/controller/GlutenCacheController.java
 
b/src/query-server/src/main/java/org/apache/kylin/rest/controller/GlutenCacheController.java
index 90e23213f0..a605bac0b5 100644
--- 
a/src/query-server/src/main/java/org/apache/kylin/rest/controller/GlutenCacheController.java
+++ 
b/src/query-server/src/main/java/org/apache/kylin/rest/controller/GlutenCacheController.java
@@ -30,7 +30,9 @@ import org.apache.kylin.rest.request.IndexGlutenCacheRequest;
 import org.apache.kylin.rest.request.InternalTableGlutenCacheRequest;
 import org.apache.kylin.rest.response.EnvelopeResponse;
 import org.apache.kylin.rest.response.GlutenCacheResponse;
+import org.apache.kylin.rest.security.InternalRpcSecurity;
 import org.apache.kylin.rest.service.GlutenCacheService;
+import org.apache.kylin.rest.util.AclEvaluate;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.web.bind.annotation.PostMapping;
 import org.springframework.web.bind.annotation.RequestBody;
@@ -50,13 +52,18 @@ public class GlutenCacheController extends NBasicController 
{
     @Autowired
     private GlutenCacheService glutenCacheService;
 
+    @Autowired
+    private AclEvaluate aclEvaluate;
+
     /**
      * RPC Call: for build job update gluten cache
      */
     @PostMapping(value = "/cache/gluten_cache")
     @ApiOperation(value = "gluten cache", tags = { "Gluten" })
     @ResponseBody
-    public EnvelopeResponse<GlutenCacheResponse> glutenCache(@RequestBody 
List<String> cacheCommands) {
+    public EnvelopeResponse<GlutenCacheResponse> glutenCache(@RequestBody 
List<String> cacheCommands,
+            HttpServletRequest request) {
+        InternalRpcSecurity.requireGlobalAdminOrServiceToken(request);
         checkCollectionRequiredArg("cacheCommands", cacheCommands);
         val result = glutenCacheService.glutenCache(cacheCommands);
         return new EnvelopeResponse<>(KylinException.CODE_SUCCESS, result, "");
@@ -68,7 +75,9 @@ public class GlutenCacheController extends NBasicController {
     @PostMapping(value = "/cache/gluten_cache_async")
     @ApiOperation(value = "gluten cache", tags = { "Gluten" })
     @ResponseBody
-    public EnvelopeResponse<String> glutenCacheAsync(@RequestBody List<String> 
cacheCommands) {
+    public EnvelopeResponse<String> glutenCacheAsync(@RequestBody List<String> 
cacheCommands,
+            HttpServletRequest request) {
+        InternalRpcSecurity.requireGlobalAdminOrServiceToken(request);
         checkCollectionRequiredArg("cacheCommands", cacheCommands);
         glutenCacheService.glutenCacheAsync(cacheCommands);
         return new EnvelopeResponse<>(KylinException.CODE_SUCCESS, "", "");
@@ -81,6 +90,7 @@ public class GlutenCacheController extends NBasicController {
             HttpServletRequest servletRequest) throws Exception {
         log.info("Internal table gluten cache request is [{}]", request);
         val projectName = checkProjectName(request.getProject());
+        checkProjectAdminPermission(projectName, servletRequest);
         request.setProject(projectName);
         checkRequiredArg("database", request.getDatabase());
         checkRequiredArg("table", request.getTable());
@@ -95,9 +105,16 @@ public class GlutenCacheController extends NBasicController 
{
             HttpServletRequest servletRequest) throws Exception {
         log.info("Index gluten cache request is [{}]", request);
         val projectName = checkProjectName(request.getProject());
+        checkProjectAdminPermission(projectName, servletRequest);
         request.setProject(projectName);
         checkRequiredArg("model", request.getModel());
         glutenCacheService.indexGlutenCache(request, servletRequest);
         return new EnvelopeResponse<>(KylinException.CODE_SUCCESS, "", "");
     }
+
+    private void checkProjectAdminPermission(String projectName, 
HttpServletRequest request) {
+        if (!InternalRpcSecurity.isGlobalAdmin() && 
!InternalRpcSecurity.hasValidServiceToken(request)) {
+            aclEvaluate.checkProjectAdminPermission(projectName);
+        }
+    }
 }
diff --git 
a/src/query-server/src/test/java/org/apache/kylin/rest/controller/GlutenCacheControllerTest.java
 
b/src/query-server/src/test/java/org/apache/kylin/rest/controller/GlutenCacheControllerTest.java
index 8773618b08..79e432f93f 100644
--- 
a/src/query-server/src/test/java/org/apache/kylin/rest/controller/GlutenCacheControllerTest.java
+++ 
b/src/query-server/src/test/java/org/apache/kylin/rest/controller/GlutenCacheControllerTest.java
@@ -20,6 +20,7 @@ package org.apache.kylin.rest.controller;
 
 import org.apache.kylin.common.util.JsonUtil;
 import org.apache.kylin.common.util.NLocalFileMetadataTestCase;
+import org.apache.kylin.common.exception.KylinException;
 import org.apache.kylin.guava30.shaded.common.collect.Lists;
 import org.apache.kylin.job.util.JobContextUtil;
 import org.apache.kylin.rest.constant.Constant;
@@ -27,7 +28,9 @@ import org.apache.kylin.rest.request.IndexGlutenCacheRequest;
 import org.apache.kylin.rest.request.InternalTableGlutenCacheRequest;
 import org.apache.kylin.rest.response.GlutenCacheResponse;
 import org.apache.kylin.rest.service.GlutenCacheService;
+import org.apache.kylin.rest.util.AclEvaluate;
 import org.junit.After;
+import org.junit.Assert;
 import org.junit.Before;
 import org.junit.Test;
 import org.mockito.InjectMocks;
@@ -55,6 +58,9 @@ public class GlutenCacheControllerTest extends 
NLocalFileMetadataTestCase {
     @Mock
     private GlutenCacheService glutenCacheService;
 
+    @Mock
+    private AclEvaluate aclEvaluate;
+
     @InjectMocks
     private GlutenCacheController glutenCacheController = Mockito.spy(new 
GlutenCacheController());
 
@@ -87,7 +93,7 @@ public class GlutenCacheControllerTest extends 
NLocalFileMetadataTestCase {
                 
.contentType(MediaType.APPLICATION_JSON).content(JsonUtil.writeValueAsString(request)))
                 .andExpect(MockMvcResultMatchers.status().isOk()).andReturn();
 
-        Mockito.verify(glutenCacheController).glutenCache(request);
+        Mockito.verify(glutenCacheController).glutenCache(Mockito.eq(request), 
Mockito.any());
     }
 
     @Test
@@ -99,7 +105,7 @@ public class GlutenCacheControllerTest extends 
NLocalFileMetadataTestCase {
                 
.contentType(MediaType.APPLICATION_JSON).content(JsonUtil.writeValueAsString(request)))
                 .andExpect(MockMvcResultMatchers.status().isOk()).andReturn();
 
-        Mockito.verify(glutenCacheController).glutenCacheAsync(request);
+        
Mockito.verify(glutenCacheController).glutenCacheAsync(Mockito.eq(request), 
Mockito.any());
     }
 
     @Test
@@ -130,4 +136,13 @@ public class GlutenCacheControllerTest extends 
NLocalFileMetadataTestCase {
 
         Mockito.verify(glutenCacheController).indexGlutenCache(request, 
mvcResult.getRequest());
     }
+
+    @Test
+    public void anonymousCacheRequestRejected() {
+        SecurityContextHolder.clearContext();
+
+        Assert.assertThrows(KylinException.class,
+                () -> 
glutenCacheController.glutenCache(Lists.newArrayList("DROP TABLE test"),
+                        new MockHttpServletRequest()));
+    }
 }
diff --git 
a/src/query-service/src/main/java/org/apache/kylin/rest/service/GlutenCacheService.java
 
b/src/query-service/src/main/java/org/apache/kylin/rest/service/GlutenCacheService.java
index d0f95ac024..3fb853003a 100644
--- 
a/src/query-service/src/main/java/org/apache/kylin/rest/service/GlutenCacheService.java
+++ 
b/src/query-service/src/main/java/org/apache/kylin/rest/service/GlutenCacheService.java
@@ -68,6 +68,7 @@ public class GlutenCacheService extends BasicService {
     public GlutenCacheResponse glutenCache(List<String> cacheCommands) {
         val executeResults = Lists.<GlutenCacheExecuteResult> newArrayList();
         try (SetLogCategory ignore = new 
SetLogCategory(LogConstant.BUILD_CATEGORY)) {
+            GlutenCacheUtils.validateCacheCommands(cacheCommands);
             val sparkSession = SparderEnv.getSparkSession();
             val successCount = new AtomicInteger(0);
             for (String cacheCommand : cacheCommands) {
@@ -172,6 +173,7 @@ public class GlutenCacheService extends BasicService {
     public void glutenCacheAsync(List<String> cacheCommands) {
         try (GlutenCacheRequestLimits ignored = new GlutenCacheRequestLimits();
                 SetLogCategory ignore = new 
SetLogCategory(LogConstant.BUILD_CATEGORY)) {
+            GlutenCacheUtils.validateCacheCommands(cacheCommands);
             val sparkSession = SparderEnv.getSparkSession();
             for (String cacheCommand : cacheCommands) {
                 glutenCacheExecute(cacheCommand, sparkSession);
diff --git 
a/src/query-service/src/test/java/org/apache/kylin/rest/service/GlutenCacheServiceTest.java
 
b/src/query-service/src/test/java/org/apache/kylin/rest/service/GlutenCacheServiceTest.java
index 671d9363ca..12cf99447d 100644
--- 
a/src/query-service/src/test/java/org/apache/kylin/rest/service/GlutenCacheServiceTest.java
+++ 
b/src/query-service/src/test/java/org/apache/kylin/rest/service/GlutenCacheServiceTest.java
@@ -97,12 +97,12 @@ public class GlutenCacheServiceTest extends 
NLocalFileMetadataTestCase {
     @Mock
     private Appender appender = Mockito.mock(Appender.class);
 
-    private static final String COMMAND_0 = "test command 0";
-    private static final String COMMAND_1 = "test command 1";
-    private static final String COMMAND_2 = "test command 2";
-    private static final String COMMAND_3 = "test command 3";
-    private static final String COMMAND_4 = "test command 4";
-    private static final String COMMAND_5 = "test command 5";
+    private static final String COMMAND_0 = "CACHE DATA SELECT * FROM 
'test-command-0'";
+    private static final String COMMAND_1 = "CACHE DATA SELECT * FROM 
'test-command-1'";
+    private static final String COMMAND_2 = "CACHE DATA SELECT * FROM 
'test-command-2'";
+    private static final String COMMAND_3 = "CACHE DATA SELECT * FROM 
'test-command-3'";
+    private static final String COMMAND_4 = "CACHE DATA SELECT * FROM 
'test-command-4'";
+    private static final String COMMAND_5 = "CACHE DATA SELECT * FROM 
'test-command-5'";
     private static final String PROJECT = "default";
     private static final String DATABASE = "databases";
     private static final String TABLE = "table";
diff --git 
a/src/spark-project/engine-spark/src/main/java/org/apache/kylin/engine/spark/job/LoadCacheStep.java
 
b/src/spark-project/engine-spark/src/main/java/org/apache/kylin/engine/spark/job/LoadCacheStep.java
index 79a2dca4aa..fb3ed72d4c 100644
--- 
a/src/spark-project/engine-spark/src/main/java/org/apache/kylin/engine/spark/job/LoadCacheStep.java
+++ 
b/src/spark-project/engine-spark/src/main/java/org/apache/kylin/engine/spark/job/LoadCacheStep.java
@@ -37,6 +37,7 @@ import org.apache.http.util.EntityUtils;
 import org.apache.kylin.common.KylinConfig;
 import org.apache.kylin.common.exception.KylinException;
 import org.apache.kylin.common.exception.KylinRuntimeException;
+import 
org.apache.kylin.common.persistence.transaction.BroadcastEventReadyNotifier;
 import org.apache.kylin.common.util.AddressUtil;
 import org.apache.kylin.common.util.JsonUtil;
 import org.apache.kylin.guava30.shaded.common.collect.Maps;
@@ -79,6 +80,10 @@ public abstract class LoadCacheStep extends 
AbstractExecutable {
 
         val httpPost = new HttpPost(requestApi);
         httpPost.addHeader(HttpHeaders.CONTENT_TYPE, 
HTTP_VND_APACHE_KYLIN_JSON);
+        String serviceToken = config.getBroadcastToken();
+        if (StringUtils.isNotBlank(serviceToken)) {
+            
httpPost.addHeader(BroadcastEventReadyNotifier.BROADCAST_TOKEN_HEADER, 
serviceToken);
+        }
         httpPost.setEntity(new ByteArrayEntity(requestEntity, 
ContentType.APPLICATION_JSON));
 
         val httpResponse = httpClient.execute(httpPost);
diff --git 
a/src/spark-project/engine-spark/src/main/java/org/apache/kylin/utils/GlutenCacheUtils.java
 
b/src/spark-project/engine-spark/src/main/java/org/apache/kylin/utils/GlutenCacheUtils.java
index 7f4b2106dd..9b1513fb4e 100644
--- 
a/src/spark-project/engine-spark/src/main/java/org/apache/kylin/utils/GlutenCacheUtils.java
+++ 
b/src/spark-project/engine-spark/src/main/java/org/apache/kylin/utils/GlutenCacheUtils.java
@@ -55,6 +55,36 @@ import lombok.experimental.UtilityClass;
 @UtilityClass
 public class GlutenCacheUtils {
 
+    private static final String CACHE_DATA_PREFIX = "CACHE DATA ";
+    private static final String CACHE_FILES_PREFIX = "CACHE FILES ";
+
+    public static void validateCacheCommands(Collection<String> cacheCommands) 
{
+        if (CollectionUtils.isEmpty(cacheCommands)) {
+            return;
+        }
+        for (String cacheCommand : cacheCommands) {
+            if (StringUtils.isBlank(cacheCommand)) {
+                throw new KylinRuntimeException("Invalid gluten cache command: 
command is empty");
+            }
+            String command = cacheCommand.trim();
+            if (command.indexOf(';') >= 0 || command.indexOf('\n') >= 0 || 
command.indexOf('\r') >= 0) {
+                throw new KylinRuntimeException("Invalid gluten cache command: 
statement separator is not allowed");
+            }
+            if (command.startsWith(CACHE_DATA_PREFIX)) {
+                if (!command.contains(" SELECT ") || !command.contains(" FROM 
'")) {
+                    throw new KylinRuntimeException("Invalid gluten cache data 
command");
+                }
+            } else if (command.startsWith(CACHE_FILES_PREFIX)) {
+                if (!command.contains(" SELECT * FROM '")
+                        || !command.endsWith(" CACHEPROPERTIES 
(recursive=true)")) {
+                    throw new KylinRuntimeException("Invalid gluten cache 
files command");
+                }
+            } else {
+                throw new KylinRuntimeException("Invalid gluten cache 
command");
+            }
+        }
+    }
+
     public static String generateCacheTableCommand(KylinConfig config, String 
project, String table, String start,
             List<String> columns, boolean isAsync) {
         val internalTableManager = InternalTableManager.getInstance(config, 
project);

Reply via email to